Skip to content

Commit b746261

Browse files
committed
native methods
1 parent 6b92414 commit b746261

File tree

7 files changed

+557
-203
lines changed

7 files changed

+557
-203
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { natives } from '../natives'
2+
import {
3+
// ControlStub,
4+
// StashStub,
5+
createContextStub
6+
// getControlItemStr,
7+
// getStashItemStr
8+
} from './__utils__/utils'
9+
import { parse } from '../../ast/parser'
10+
import { evaluate } from '../interpreter'
11+
12+
describe('native functions', () => {
13+
it('should invoke external native function', () => {
14+
const mockForeignFn = jest.fn()
15+
16+
natives['testNative(): void'] = mockForeignFn
17+
18+
const programStr = `
19+
class C {
20+
public native void testNative();
21+
22+
public static void main(String[] args) {
23+
C c = new C();
24+
c.testNative();
25+
}
26+
}`
27+
28+
const compilationUnit = parse(programStr)
29+
expect(compilationUnit).toBeTruthy()
30+
31+
const context = createContextStub()
32+
context.control.push(compilationUnit!)
33+
34+
evaluate(context)
35+
36+
expect(mockForeignFn.mock.calls).toHaveLength(1)
37+
})
38+
39+
it('should invoke external native function with correct environment', () => {
40+
const foreignFn = jest.fn(({ environment }) => {
41+
const s = environment.getVariable('s').value.literalType.value
42+
expect(s).toBe('"Test"')
43+
})
44+
45+
natives['testNative(String s): void'] = foreignFn
46+
47+
const programStr = `
48+
class C {
49+
public native void testNative(String s);
50+
51+
public static void main(String[] args) {
52+
C c = new C();
53+
c.testNative("Test");
54+
}
55+
}`
56+
57+
const compilationUnit = parse(programStr)
58+
expect(compilationUnit).toBeTruthy()
59+
60+
const context = createContextStub()
61+
context.control.push(compilationUnit!)
62+
63+
evaluate(context)
64+
65+
expect(foreignFn.mock.calls).toHaveLength(1)
66+
})
67+
})

src/ec-evaluator/errors.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,13 @@ export class NoMainMtdError extends RuntimeError {
160160
return `public static void main(String[] args) is not defined in any class.`
161161
}
162162
}
163+
164+
export class UndefinedNativeMethod extends RuntimeError {
165+
constructor(private descriptor: string) {
166+
super()
167+
}
168+
169+
public explain() {
170+
return `Native function ${this.descriptor} has no defined implementation.`
171+
}
172+
}

src/ec-evaluator/interpreter.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,15 @@ import {
8787
searchMainMtdClass,
8888
prependExpConInvIfNeeded,
8989
isStatic,
90+
isNative,
9091
resOverload,
9192
resOverride,
9293
resConOverload,
9394
isNull,
94-
makeNonLocalVarNonParamSimpleNameQualified
95+
makeNonLocalVarNonParamSimpleNameQualified,
96+
getFullyQualifiedDescriptor
9597
} from './utils'
98+
import { natives } from './natives'
9699

97100
type CmdEvaluator = (
98101
command: ControlItem,
@@ -136,7 +139,7 @@ export const evaluate = (
136139
return stash.peek()
137140
}
138141

139-
const cmdEvaluators: { [type: string]: CmdEvaluator } = {
142+
export const cmdEvaluators: { [type: string]: CmdEvaluator } = {
140143
CompilationUnit: (
141144
command: CompilationUnit,
142145
_environment: Environment,
@@ -501,6 +504,28 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = {
501504
environment.defineVariable(params[i].identifier, params[i].unannType, args[i])
502505
}
503506

507+
// Native function escape hatch
508+
if (closure.mtdOrCon.kind === 'MethodDeclaration' && isNative(closure.mtdOrCon)) {
509+
const nativeFn = natives[getFullyQualifiedDescriptor(closure.mtdOrCon)]
510+
511+
if (!nativeFn) {
512+
throw new errors.UndefinedNativeMethod(nativeFn)
513+
}
514+
515+
// call foreign fn
516+
nativeFn({ control, stash, environment })
517+
518+
// only because resetInstr demands one, never actually used
519+
const superfluousReturnStatement: ReturnStatement = {
520+
kind: 'ReturnStatement',
521+
exp: { kind: 'Void' }
522+
}
523+
524+
// handle return from native fn
525+
control.push(instr.resetInstr(superfluousReturnStatement))
526+
return
527+
}
528+
504529
// Push method/constructor body.
505530
const body =
506531
closure.mtdOrCon.kind === 'MethodDeclaration'

src/ec-evaluator/natives.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Control, Environment, Stash } from './components'
2+
3+
/*
4+
Native function escape hatch.
5+
6+
Used for implementing native methods. Allows for purely arbitrary modification to the control, stash, and environment via an external handler function.
7+
8+
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.
9+
10+
Preconditions: environment has been initialised for the current function call.
11+
12+
Postconditions: returned result must be pushed onto the top of the stash.
13+
14+
The current implementation automatically injects a return instruction after the external handler function call ends.
15+
*/
16+
17+
export type NativeFunction = ({
18+
control,
19+
stash,
20+
environment
21+
}: {
22+
control: Control
23+
stash: Stash
24+
environment: Environment
25+
}) => void
26+
27+
export const natives: {
28+
[descriptor: string]: NativeFunction
29+
} = {}

src/ec-evaluator/nodeCreator.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,5 +138,41 @@ export const objClassDeclNode = (): ClassDeclaration => ({
138138
kind: 'NormalClassDeclaration',
139139
classModifier: [],
140140
typeIdentifier: OBJECT_CLASS,
141-
classBody: []
141+
classBody: [
142+
{
143+
kind: 'FieldDeclaration',
144+
fieldModifier: ['private'],
145+
fieldType: 'int',
146+
variableDeclaratorList: [
147+
{
148+
kind: 'VariableDeclarator',
149+
variableDeclaratorId: 'hash',
150+
variableInitializer: {
151+
kind: 'Literal',
152+
literalType: {
153+
kind: 'DecimalIntegerLiteral',
154+
value: String(Math.floor(Math.random() * Math.pow(2, 32)))
155+
}
156+
}
157+
}
158+
]
159+
},
160+
{
161+
kind: 'MethodDeclaration',
162+
methodModifier: ['public'],
163+
methodHeader: { result: 'int', identifier: 'hashCode', formalParameterList: [] },
164+
methodBody: {
165+
kind: 'Block',
166+
blockStatements: [
167+
{
168+
kind: 'ReturnStatement',
169+
exp: {
170+
kind: 'ExpressionName',
171+
name: 'this.hash'
172+
}
173+
}
174+
]
175+
}
176+
}
177+
]
142178
})

src/ec-evaluator/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ export const getDescriptor = (mtdOrCon: MethodDeclaration | ConstructorDeclarati
183183
: `${mtdOrCon.constructorDeclarator.identifier}(${mtdOrCon.constructorDeclarator.formalParameterList.map(p => p.unannType).join(',')})`
184184
}
185185

186+
// for native methods (uses new proposed format) with parameter names
187+
// because native functions must retrieve variables from the environment by identifier, this descriptor type also includes parameter names for convenience
188+
export const getFullyQualifiedDescriptor = (mtd: MethodDeclaration): string =>
189+
`${mtd.methodHeader.identifier}(${mtd.methodHeader.formalParameterList.map(p => `${p.unannType} ${p.identifier}`).join(',')}): ${mtd.methodHeader.result}`
190+
186191
export const isQualified = (name: string) => {
187192
return name.includes('.')
188193
}
@@ -230,6 +235,10 @@ export const isInstance = (fieldOrMtd: FieldDeclaration | MethodDeclaration): bo
230235
return !isStatic(fieldOrMtd)
231236
}
232237

238+
export const isNative = (mtd: MethodDeclaration): boolean => {
239+
return mtd.methodModifier.includes('native')
240+
}
241+
233242
const convertFieldDeclToExpStmtAssmt = (fd: FieldDeclaration): ExpressionStatement => {
234243
const left = `this.${fd.variableDeclaratorList[0].variableDeclaratorId}`
235244
// Fields are always initialized to default value if initializer is absent.

0 commit comments

Comments
 (0)