From bdb540269ef2a05f58b8a92fbaa5042e9fe4b9a0 Mon Sep 17 00:00:00 2001 From: mattcce Date: Mon, 16 Jun 2025 23:58:46 +0800 Subject: [PATCH 1/6] native methods --- src/ec-evaluator/__tests__/natives.test.ts | 67 ++++++++++++++++++++++ src/ec-evaluator/errors.ts | 10 ++++ src/ec-evaluator/interpreter.ts | 29 +++++++++- src/ec-evaluator/natives.ts | 29 ++++++++++ src/ec-evaluator/nodeCreator.ts | 38 +++++++++++- src/ec-evaluator/utils.ts | 9 +++ 6 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 src/ec-evaluator/__tests__/natives.test.ts create mode 100644 src/ec-evaluator/natives.ts diff --git a/src/ec-evaluator/__tests__/natives.test.ts b/src/ec-evaluator/__tests__/natives.test.ts new file mode 100644 index 00000000..f8979c10 --- /dev/null +++ b/src/ec-evaluator/__tests__/natives.test.ts @@ -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) + }) +}) 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..27e6173e 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,28 @@ 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 nativeFn = natives[getFullyQualifiedDescriptor(closure.mtdOrCon)] + + if (!nativeFn) { + throw new errors.UndefinedNativeMethod(nativeFn) + } + + // 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' 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/nodeCreator.ts b/src/ec-evaluator/nodeCreator.ts index 05d9c3c2..6c62247e 100644 --- a/src/ec-evaluator/nodeCreator.ts +++ b/src/ec-evaluator/nodeCreator.ts @@ -138,5 +138,41 @@ export const objClassDeclNode = (): ClassDeclaration => ({ kind: 'NormalClassDeclaration', classModifier: [], typeIdentifier: OBJECT_CLASS, - classBody: [] + classBody: [ + { + kind: 'FieldDeclaration', + fieldModifier: ['private'], + fieldType: 'int', + variableDeclaratorList: [ + { + kind: 'VariableDeclarator', + variableDeclaratorId: 'hash', + variableInitializer: { + kind: 'Literal', + literalType: { + kind: 'DecimalIntegerLiteral', + value: String(Math.floor(Math.random() * Math.pow(2, 32))) + } + } + } + ] + }, + { + kind: 'MethodDeclaration', + methodModifier: ['public'], + methodHeader: { result: 'int', identifier: 'hashCode', formalParameterList: [] }, + methodBody: { + kind: 'Block', + blockStatements: [ + { + kind: 'ReturnStatement', + exp: { + kind: 'ExpressionName', + name: 'this.hash' + } + } + ] + } + } + ] }) 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. From 7cf369d8198ffe9133b5c808392e314282379d3f Mon Sep 17 00:00:00 2001 From: mattcce Date: Tue, 17 Jun 2025 00:52:33 +0800 Subject: [PATCH 2/6] display native function descriptor when unimplemented --- src/ec-evaluator/interpreter.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ec-evaluator/interpreter.ts b/src/ec-evaluator/interpreter.ts index 27e6173e..f8cf11d7 100644 --- a/src/ec-evaluator/interpreter.ts +++ b/src/ec-evaluator/interpreter.ts @@ -506,10 +506,11 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { // Native function escape hatch if (closure.mtdOrCon.kind === 'MethodDeclaration' && isNative(closure.mtdOrCon)) { - const nativeFn = natives[getFullyQualifiedDescriptor(closure.mtdOrCon)] + const nativeFnDescriptor = getFullyQualifiedDescriptor(closure.mtdOrCon) + const nativeFn = natives[nativeFnDescriptor] if (!nativeFn) { - throw new errors.UndefinedNativeMethod(nativeFn) + throw new errors.UndefinedNativeMethod(nativeFnDescriptor) } // call foreign fn From c695529e551a0bef93b614b051f58489a0a5c223 Mon Sep 17 00:00:00 2001 From: mattcce Date: Tue, 17 Jun 2025 13:15:37 +0800 Subject: [PATCH 3/6] native method handling for type-checker --- src/types/ast/extractor.ts | 8 +++++++- src/types/ast/specificationTypes.ts | 2 +- src/types/checker/index.ts | 14 ++++++++++++++ src/types/errors.ts | 6 ++++++ 4 files changed, 28 insertions(+), 2 deletions(-) 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/index.ts b/src/types/checker/index.ts index e9147bcc..69dd4485 100644 --- a/src/types/checker/index.ts +++ b/src/types/checker/index.ts @@ -8,6 +8,7 @@ import { CannotFindSymbolError, IncompatibleTypesError, MethodCannotBeAppliedError, + MissingMethodBodyError, NotApplicableToExpressionTypeError, TypeCheckerError, TypeCheckerInternalError, @@ -630,6 +631,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 diff --git a/src/types/errors.ts b/src/types/errors.ts index b1dc889f..f06ac7a5 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -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) From 86a2b1d164dcbbcfa51f8365c149f875c47ca425 Mon Sep 17 00:00:00 2001 From: mattcce Date: Tue, 17 Jun 2025 13:22:32 +0800 Subject: [PATCH 4/6] revert Object class declaration --- src/ec-evaluator/nodeCreator.ts | 38 +-------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/src/ec-evaluator/nodeCreator.ts b/src/ec-evaluator/nodeCreator.ts index 6c62247e..05d9c3c2 100644 --- a/src/ec-evaluator/nodeCreator.ts +++ b/src/ec-evaluator/nodeCreator.ts @@ -138,41 +138,5 @@ export const objClassDeclNode = (): ClassDeclaration => ({ kind: 'NormalClassDeclaration', classModifier: [], typeIdentifier: OBJECT_CLASS, - classBody: [ - { - kind: 'FieldDeclaration', - fieldModifier: ['private'], - fieldType: 'int', - variableDeclaratorList: [ - { - kind: 'VariableDeclarator', - variableDeclaratorId: 'hash', - variableInitializer: { - kind: 'Literal', - literalType: { - kind: 'DecimalIntegerLiteral', - value: String(Math.floor(Math.random() * Math.pow(2, 32))) - } - } - } - ] - }, - { - kind: 'MethodDeclaration', - methodModifier: ['public'], - methodHeader: { result: 'int', identifier: 'hashCode', formalParameterList: [] }, - methodBody: { - kind: 'Block', - blockStatements: [ - { - kind: 'ReturnStatement', - exp: { - kind: 'ExpressionName', - name: 'this.hash' - } - } - ] - } - } - ] + classBody: [] }) From 1734ed58186127508d19e5952727edc1ae7eb343 Mon Sep 17 00:00:00 2001 From: hj235 Date: Wed, 14 Oct 2026 02:35:33 +0800 Subject: [PATCH 5/6] Handle bodyless methods in enums; drop type checker console.log Address the Codex review of this PR: - A native method declared in an enum crashed typeCheck, because the extractor now yields an undefined body for `;` and only the class branch of the checker handled that. Give the EnumDeclaration branch the same native / MissingMethodBodyError handling. - Remove the leftover console.log of native method declarations. --- .../__tests__/methodDeclaration.test.ts | 29 +++++++++++++++++++ src/types/checker/index.ts | 17 ++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/types/checker/__tests__/methodDeclaration.test.ts b/src/types/checker/__tests__/methodDeclaration.test.ts index e5eb4ee3..a5752b2d 100644 --- a/src/types/checker/__tests__/methodDeclaration.test.ts +++ b/src/types/checker/__tests__/methodDeclaration.test.ts @@ -1,5 +1,6 @@ import { IllegalCombinationOfModifiersError, + MissingMethodBodyError, RepeatedModifierError, TypeCheckerError } from '../../errors' @@ -41,6 +42,34 @@ const testcases: { protected private void illegalProtectedPrivateModifiers() {} `, result: { type: null, errors: [new IllegalCombinationOfModifiersError()] } + }, + { + input: ` + public native void nativeMethodWithoutBody(); + `, + result: { type: null, errors: [] } + }, + { + input: ` + public void methodWithoutBody(); + `, + result: { type: null, errors: [new MissingMethodBodyError()] } + }, + { + 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 69dd4485..03e3012a 100644 --- a/src/types/checker/index.ts +++ b/src/types/checker/index.ts @@ -634,7 +634,6 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R // skip type checking for bodies of native methods (admit empty body) if (bodyDeclaration.methodModifiers.map(i => i.identifier).includes('native')) { - console.log(bodyDeclaration) break } @@ -742,6 +741,22 @@ 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 as any).methodModifiers + .map((i: any) => i.identifier) + .includes('native') + ) { + 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 From 3b3abbbd38fb3decf81dd3b9b6a9a698528df1e8 Mon Sep 17 00:00:00 2001 From: hj235 Date: Wed, 14 Oct 2026 17:28:12 +0800 Subject: [PATCH 6/6] Run native handler control items; reject native methods with a body Address the second Codex review: - Push the reset instruction before calling the native handler, so control items the handler pushes run instead of being discarded by RESET. - Reject native methods that declare a body, in classes and enums, with NativeMethodHasBodyError (as in #79). --- src/ec-evaluator/__tests__/natives.test.ts | 36 +++++++++++++++++++ src/ec-evaluator/interpreter.ts | 7 ++-- .../__tests__/methodDeclaration.test.ts | 15 ++++++++ src/types/checker/index.ts | 11 ++++-- src/types/errors.ts | 6 ++++ 5 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/ec-evaluator/__tests__/natives.test.ts b/src/ec-evaluator/__tests__/natives.test.ts index f8979c10..e1d8716d 100644 --- a/src/ec-evaluator/__tests__/natives.test.ts +++ b/src/ec-evaluator/__tests__/natives.test.ts @@ -64,4 +64,40 @@ describe('native functions', () => { 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/interpreter.ts b/src/ec-evaluator/interpreter.ts index f8cf11d7..a1119458 100644 --- a/src/ec-evaluator/interpreter.ts +++ b/src/ec-evaluator/interpreter.ts @@ -513,9 +513,6 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { 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', @@ -523,7 +520,11 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { } // 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 } diff --git a/src/types/checker/__tests__/methodDeclaration.test.ts b/src/types/checker/__tests__/methodDeclaration.test.ts index a5752b2d..3c073de8 100644 --- a/src/types/checker/__tests__/methodDeclaration.test.ts +++ b/src/types/checker/__tests__/methodDeclaration.test.ts @@ -1,6 +1,7 @@ import { IllegalCombinationOfModifiersError, MissingMethodBodyError, + NativeMethodHasBodyError, RepeatedModifierError, TypeCheckerError } from '../../errors' @@ -49,12 +50,26 @@ const testcases: { `, 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) { diff --git a/src/types/checker/index.ts b/src/types/checker/index.ts index 03e3012a..396c0446 100644 --- a/src/types/checker/index.ts +++ b/src/types/checker/index.ts @@ -9,6 +9,7 @@ import { IncompatibleTypesError, MethodCannotBeAppliedError, MissingMethodBodyError, + NativeMethodHasBodyError, NotApplicableToExpressionTypeError, TypeCheckerError, TypeCheckerInternalError, @@ -632,8 +633,11 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R break } - // skip type checking for bodies of native methods (admit empty body) + // 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 } @@ -742,12 +746,15 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R break } - // skip type checking for bodies of native methods (admit empty body) + // 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 } diff --git a/src/types/errors.ts b/src/types/errors.ts index f06ac7a5..da235cdd 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -130,6 +130,12 @@ export class ModifierNotAllowedHereError extends TypeCheckerError { } } +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)