diff --git a/src/ec-evaluator/__tests__/natives.test.ts b/src/ec-evaluator/__tests__/natives.test.ts new file mode 100644 index 00000000..e1d8716d --- /dev/null +++ b/src/ec-evaluator/__tests__/natives.test.ts @@ -0,0 +1,103 @@ +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) + }) + + it('should run control items pushed by external native function', () => { + const foreignFn = jest.fn(({ control }) => { + control.push({ kind: 'Literal', literalType: { kind: 'DecimalIntegerLiteral', value: '7' } }) + }) + + const checkFn = jest.fn(({ environment }) => { + const x = environment.getVariable('x').value.literalType.value + expect(x).toBe('7') + }) + + natives['viaControl(): int'] = foreignFn + natives['check(int x): void'] = checkFn + + const programStr = ` + class C { + public static native int viaControl(); + public static native void check(int x); + + public static void main(String[] args) { + int x = viaControl(); + check(x); + } + }` + + const compilationUnit = parse(programStr) + expect(compilationUnit).toBeTruthy() + + const context = createContextStub() + context.control.push(compilationUnit!) + + evaluate(context) + + expect(foreignFn.mock.calls).toHaveLength(1) + expect(checkFn.mock.calls).toHaveLength(1) + }) +}) diff --git a/src/ec-evaluator/errors.ts b/src/ec-evaluator/errors.ts index 495b3a00..e0d5d4a1 100644 --- a/src/ec-evaluator/errors.ts +++ b/src/ec-evaluator/errors.ts @@ -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.` + } +} diff --git a/src/ec-evaluator/interpreter.ts b/src/ec-evaluator/interpreter.ts index fa8c5481..a1119458 100644 --- a/src/ec-evaluator/interpreter.ts +++ b/src/ec-evaluator/interpreter.ts @@ -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, @@ -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, @@ -501,6 +504,30 @@ 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) + } + + // only because resetInstr demands one, never actually used + const superfluousReturnStatement: ReturnStatement = { + kind: 'ReturnStatement', + exp: { kind: 'Void' } + } + + // handle return from native fn + // pushed before the call, so control items pushed by the foreign fn run before the reset + control.push(instr.resetInstr(superfluousReturnStatement)) + + // call foreign fn + nativeFn({ control, stash, environment }) + return + } + // Push method/constructor body. const body = closure.mtdOrCon.kind === 'MethodDeclaration' diff --git a/src/ec-evaluator/natives.ts b/src/ec-evaluator/natives.ts new file mode 100644 index 00000000..9a188de5 --- /dev/null +++ b/src/ec-evaluator/natives.ts @@ -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 +} = {} diff --git a/src/ec-evaluator/utils.ts b/src/ec-evaluator/utils.ts index 05642a20..a11e7b16 100644 --- a/src/ec-evaluator/utils.ts +++ b/src/ec-evaluator/utils.ts @@ -184,6 +184,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('.') } @@ -231,6 +236,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. diff --git a/src/types/ast/extractor.ts b/src/types/ast/extractor.ts index ff7a0431..b081dc2b 100644 --- a/src/types/ast/extractor.ts +++ b/src/types/ast/extractor.ts @@ -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') } diff --git a/src/types/ast/specificationTypes.ts b/src/types/ast/specificationTypes.ts index 70bd0600..f3d1f3a6 100644 --- a/src/types/ast/specificationTypes.ts +++ b/src/types/ast/specificationTypes.ts @@ -742,7 +742,7 @@ export type MethodDeclaration = { kind: 'MethodDeclaration' methodModifiers: MethodModifier[] methodHeader: MethodHeader - methodBody: MethodBody + methodBody: MethodBody | undefined location: Location } diff --git a/src/types/checker/__tests__/methodDeclaration.test.ts b/src/types/checker/__tests__/methodDeclaration.test.ts index e5eb4ee3..3c073de8 100644 --- a/src/types/checker/__tests__/methodDeclaration.test.ts +++ b/src/types/checker/__tests__/methodDeclaration.test.ts @@ -1,5 +1,7 @@ import { IllegalCombinationOfModifiersError, + MissingMethodBodyError, + NativeMethodHasBodyError, RepeatedModifierError, TypeCheckerError } from '../../errors' @@ -41,6 +43,48 @@ const testcases: { protected private void illegalProtectedPrivateModifiers() {} `, result: { type: null, errors: [new IllegalCombinationOfModifiersError()] } + }, + { + input: ` + public native void nativeMethodWithoutBody(); + `, + result: { type: null, errors: [] } + }, + { + input: ` + public native void nativeMethodWithBody() {} + `, + result: { type: null, errors: [new NativeMethodHasBodyError()] } + }, + { + input: ` + public void methodWithoutBody(); + `, + result: { type: null, errors: [new MissingMethodBodyError()] } + }, + { + input: ` + public static void main(String[] args) { + enum Color { RED; native void nativeMethodWithBody() {} } + } + `, + result: { type: null, errors: [new NativeMethodHasBodyError()] } + }, + { + input: ` + public static void main(String[] args) { + enum Color { RED; native void nativeMethodWithoutBody(); } + } + `, + result: { type: null, errors: [] } + }, + { + input: ` + public static void main(String[] args) { + enum Color { RED; void methodWithoutBody(); } + } + `, + result: { type: null, errors: [new MissingMethodBodyError()] } } ] diff --git a/src/types/checker/index.ts b/src/types/checker/index.ts index e9147bcc..396c0446 100644 --- a/src/types/checker/index.ts +++ b/src/types/checker/index.ts @@ -8,6 +8,8 @@ import { CannotFindSymbolError, IncompatibleTypesError, MethodCannotBeAppliedError, + MissingMethodBodyError, + NativeMethodHasBodyError, NotApplicableToExpressionTypeError, TypeCheckerError, TypeCheckerInternalError, @@ -630,6 +632,21 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R errors.push(...methodErrors) break } + + // native methods (admit empty body) + if (bodyDeclaration.methodModifiers.map(i => i.identifier).includes('native')) { + if (bodyDeclaration.methodBody !== undefined) { + errors.push(new NativeMethodHasBodyError(bodyDeclaration.location)) + } + 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 @@ -728,6 +745,25 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R errors.push(...methodErrors) break } + + // native methods (admit empty body) + if ( + (bodyDeclaration as any).methodModifiers + .map((i: any) => i.identifier) + .includes('native') + ) { + if ((bodyDeclaration as any).methodBody !== undefined) { + errors.push(new NativeMethodHasBodyError((bodyDeclaration as any).location)) + } + break + } + + // empty body is error + if ((bodyDeclaration as any).methodBody === undefined) { + errors.push(new MissingMethodBodyError((bodyDeclaration as any).location)) + break + } + const { errors: checkErrors } = typeCheckBody( (bodyDeclaration as any).methodBody, methodFrame diff --git a/src/types/errors.ts b/src/types/errors.ts index b1dc889f..da235cdd 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -118,12 +118,24 @@ 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) } } +export class NativeMethodHasBodyError extends TypeCheckerError { + constructor(location?: Location) { + super('native methods cannot have a body', location) + } +} + export class NotApplicableToExpressionTypeError extends TypeCheckerError { constructor(location?: Location) { super('not applicable to expression type', location)