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 @@ export class NoMainMtdError extends RuntimeError {
return `public static void main(String[] args) is not defined in any class.`
}
}

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

public explain() {
return `Native function ${this.descriptor} has no defined implementation.`
}
}
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 @@ import {
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 @@ export const evaluate = (
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 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = {
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)
}

// 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 @@ class AstExtractor extends BaseJavaCstVisitor {
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
}

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 @@ import {
BadOperandTypesError,
CannotFindSymbolError,
IncompatibleTypesError,
MissingMethodBodyError,
NotApplicableToExpressionTypeError,
TypeCheckerError,
TypeCheckerInternalError,
Expand Down Expand Up @@ -518,6 +519,19 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R
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)
break
}

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

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 MethodCannotBeAppliedError extends TypeCheckerError {
}
}

export class MissingMethodBodyError extends TypeCheckerError {
constructor(location?: Location) {
super('missing method body', location)
}
}

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