Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/ec-evaluator/__tests__/natives.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { natives } from '../natives'
import {
// ControlStub,
// StashStub,
createContextStub
// getControlItemStr,
// getStashItemStr
} from './__utils__/utils'
import { parse } from '../../ast/parser'
import { evaluate } from '../interpreter'

describe('native functions', () => {
it('should invoke external native function', () => {
const mockForeignFn = jest.fn()

natives['testNative(): void'] = mockForeignFn

const programStr = `
class C {
public native void testNative();

public static void main(String[] args) {
C c = new C();
c.testNative();
}
}`

const compilationUnit = parse(programStr)
expect(compilationUnit).toBeTruthy()

const context = createContextStub()
context.control.push(compilationUnit!)

evaluate(context)

expect(mockForeignFn.mock.calls).toHaveLength(1)
})

it('should invoke external native function with correct environment', () => {
const foreignFn = jest.fn(({ environment }) => {
const s = environment.getVariable('s').value.literalType.value
expect(s).toBe('"Test"')
})

natives['testNative(String s): void'] = foreignFn

const programStr = `
class C {
public native void testNative(String s);

public static void main(String[] args) {
C c = new C();
c.testNative("Test");
}
}`

const compilationUnit = parse(programStr)
expect(compilationUnit).toBeTruthy()

const context = createContextStub()
context.control.push(compilationUnit!)

evaluate(context)

expect(foreignFn.mock.calls).toHaveLength(1)
})
})
10 changes: 10 additions & 0 deletions src/ec-evaluator/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,13 @@
return `public static void main(String[] args) is not defined in any class.`
}
}

export class UndefinedNativeMethod extends RuntimeError {
constructor(private descriptor: string) {

Check warning on line 165 in src/ec-evaluator/errors.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 165 in src/ec-evaluator/errors.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
super()

Check warning on line 166 in src/ec-evaluator/errors.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

public explain() {

Check warning on line 169 in src/ec-evaluator/errors.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
return `Native function ${this.descriptor} has no defined implementation.`

Check warning on line 170 in src/ec-evaluator/errors.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}
}
30 changes: 28 additions & 2 deletions src/ec-evaluator/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,15 @@
searchMainMtdClass,
prependExpConInvIfNeeded,
isStatic,
isNative,
resOverload,
resOverride,
resConOverload,
isNull,
makeNonLocalVarNonParamSimpleNameQualified
makeNonLocalVarNonParamSimpleNameQualified,
getFullyQualifiedDescriptor
} from './utils'
import { natives } from './natives'

type CmdEvaluator = (
command: ControlItem,
Expand Down Expand Up @@ -136,7 +139,7 @@
return stash.peek()
}

const cmdEvaluators: { [type: string]: CmdEvaluator } = {
export const cmdEvaluators: { [type: string]: CmdEvaluator } = {
CompilationUnit: (
command: CompilationUnit,
_environment: Environment,
Expand Down Expand Up @@ -501,6 +504,29 @@
environment.defineVariable(params[i].identifier, params[i].unannType, args[i])
}

// Native function escape hatch
if (closure.mtdOrCon.kind === 'MethodDeclaration' && isNative(closure.mtdOrCon)) {
const nativeFnDescriptor = getFullyQualifiedDescriptor(closure.mtdOrCon)
const nativeFn = natives[nativeFnDescriptor]

if (!nativeFn) {
throw new errors.UndefinedNativeMethod(nativeFnDescriptor)

Check warning on line 513 in src/ec-evaluator/interpreter.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 514 in src/ec-evaluator/interpreter.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

// call foreign fn
nativeFn({ control, stash, environment })

// only because resetInstr demands one, never actually used
const superfluousReturnStatement: ReturnStatement = {
kind: 'ReturnStatement',
exp: { kind: 'Void' }
}

// handle return from native fn
control.push(instr.resetInstr(superfluousReturnStatement))
return
}

// Push method/constructor body.
const body =
closure.mtdOrCon.kind === 'MethodDeclaration'
Expand Down
29 changes: 29 additions & 0 deletions src/ec-evaluator/natives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Control, Environment, Stash } from './components'

/*
Native function escape hatch.

Used for implementing native methods. Allows for purely arbitrary modification to the control, stash, and environment via an external handler function.

All native functions are expected to respect Java method call preconditions and postconditions, with the exception of returning. When a native function is called, it can expect the following.

Preconditions: environment has been initialised for the current function call.

Postconditions: returned result must be pushed onto the top of the stash.

The current implementation automatically injects a return instruction after the external handler function call ends.
*/

export type NativeFunction = ({
control,
stash,
environment
}: {
control: Control
stash: Stash
environment: Environment
}) => void

export const natives: {
[descriptor: string]: NativeFunction
} = {}
9 changes: 9 additions & 0 deletions src/ec-evaluator/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ export const getDescriptor = (mtdOrCon: MethodDeclaration | ConstructorDeclarati
: `${mtdOrCon.constructorDeclarator.identifier}(${mtdOrCon.constructorDeclarator.formalParameterList.map(p => p.unannType).join(',')})`
}

// for native methods (uses new proposed format) with parameter names
// because native functions must retrieve variables from the environment by identifier, this descriptor type also includes parameter names for convenience
export const getFullyQualifiedDescriptor = (mtd: MethodDeclaration): string =>
`${mtd.methodHeader.identifier}(${mtd.methodHeader.formalParameterList.map(p => `${p.unannType} ${p.identifier}`).join(',')}): ${mtd.methodHeader.result}`

export const isQualified = (name: string) => {
return name.includes('.')
}
Expand Down Expand Up @@ -230,6 +235,10 @@ export const isInstance = (fieldOrMtd: FieldDeclaration | MethodDeclaration): bo
return !isStatic(fieldOrMtd)
}

export const isNative = (mtd: MethodDeclaration): boolean => {
return mtd.methodModifier.includes('native')
}

const convertFieldDeclToExpStmtAssmt = (fd: FieldDeclaration): ExpressionStatement => {
const left = `this.${fd.variableDeclaratorList[0].variableDeclaratorId}`
// Fields are always initialized to default value if initializer is absent.
Expand Down
8 changes: 7 additions & 1 deletion src/types/ast/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1294,8 +1294,14 @@
return getIdentifier(ctx.Identifier![0])
}

methodBody(ctx: JavaParser.MethodBodyCtx): AST.MethodBody {
methodBody(ctx: JavaParser.MethodBodyCtx): AST.MethodBody | undefined {
if (ctx.block) return this.visit(ctx.block)

// handle only semicolon i.e. empty block
if (ctx.Semicolon) {
return undefined

Check warning on line 1302 in src/types/ast/extractor.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 1303 in src/types/ast/extractor.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 1303 in src/types/ast/extractor.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

throw new Error('Not implemented')
}

Expand Down
2 changes: 1 addition & 1 deletion src/types/ast/specificationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,7 @@ export type MethodDeclaration = {
kind: 'MethodDeclaration'
methodModifiers: MethodModifier[]
methodHeader: MethodHeader
methodBody: MethodBody
methodBody: MethodBody | undefined
location: Location
}

Expand Down
14 changes: 14 additions & 0 deletions src/types/checker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
BadOperandTypesError,
CannotFindSymbolError,
IncompatibleTypesError,
MissingMethodBodyError,
NotApplicableToExpressionTypeError,
TypeCheckerError,
TypeCheckerInternalError,
Expand Down Expand Up @@ -518,6 +519,19 @@
errors.push(...methodErrors)
break
}

// skip type checking for bodies of native methods (admit empty body)
if (bodyDeclaration.methodModifiers.map(i => i.identifier).includes('native')) {
console.log(bodyDeclaration)

Check warning on line 525 in src/types/checker/index.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
break

Check warning on line 526 in src/types/checker/index.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 527 in src/types/checker/index.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

// empty body is error
if (bodyDeclaration.methodBody === undefined) {
errors.push(new MissingMethodBodyError(bodyDeclaration.location))

Check warning on line 531 in src/types/checker/index.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
break

Check warning on line 532 in src/types/checker/index.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 533 in src/types/checker/index.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

const { errors: checkErrors } = typeCheckBody(bodyDeclaration.methodBody, methodFrame)
if (checkErrors.length > 0) errors.push(...checkErrors)
break
Expand Down
6 changes: 6 additions & 0 deletions src/types/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@
}
}

export class MissingMethodBodyError extends TypeCheckerError {
constructor(location?: Location) {

Check warning on line 122 in src/types/errors.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
super('missing method body', location)

Check warning on line 123 in src/types/errors.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}
}

export class ModifierNotAllowedHereError extends TypeCheckerError {
constructor(location?: Location) {
super('modifier not allowed here', location)
Expand Down
Loading