Skip to content
Merged
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
103 changes: 103 additions & 0 deletions src/ec-evaluator/__tests__/natives.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
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.`
}
}
31 changes: 29 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,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))
Comment thread
hj235 marked this conversation as resolved.
Resolved

// call foreign fn
nativeFn({ control, stash, environment })
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
} = {}
Comment thread
hj235 marked this conversation as resolved.
Resolved
9 changes: 9 additions & 0 deletions src/ec-evaluator/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
Comment thread
hj235 marked this conversation as resolved.
Resolved

export const isQualified = (name: string) => {
return name.includes('.')
}
Expand Down Expand Up @@ -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.
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
Comment thread
hj235 marked this conversation as resolved.
Resolved
}

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
44 changes: 44 additions & 0 deletions src/types/checker/__tests__/methodDeclaration.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
IllegalCombinationOfModifiersError,
MissingMethodBodyError,
NativeMethodHasBodyError,
RepeatedModifierError,
TypeCheckerError
} from '../../errors'
Expand Down Expand Up @@ -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()] }
}
]

Expand Down
36 changes: 36 additions & 0 deletions src/types/checker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
CannotFindSymbolError,
IncompatibleTypesError,
MethodCannotBeAppliedError,
MissingMethodBodyError,
NativeMethodHasBodyError,
NotApplicableToExpressionTypeError,
TypeCheckerError,
TypeCheckerInternalError,
Expand Down Expand Up @@ -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))
Comment thread
hj235 marked this conversation as resolved.
Resolved
break
}

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