diff --git a/src/ast/utils/astToString.ts b/src/ast/utils/astToString.ts index bf4f38d0..aed541b5 100644 --- a/src/ast/utils/astToString.ts +++ b/src/ast/utils/astToString.ts @@ -1,4 +1,5 @@ -import { Node } from '../types/ast'; +import { NativeDeclaration } from '../../ec-evaluator' +import { Node } from '../types/ast' import { Assignment, BinaryExpression, @@ -10,122 +11,159 @@ import { Literal, LocalVariableDeclarationStatement, MethodInvocation, - ReturnStatement, -} from '../types/blocks-and-statements'; + ReturnStatement +} from '../types/blocks-and-statements' import { ConstructorDeclaration, FieldDeclaration, MethodDeclaration, - NormalClassDeclaration, -} from '../types/classes'; + NormalClassDeclaration +} from '../types/classes' const newline = (line: string): string => { - return `${line}\n`; + return `${line}\n` } const indentLine = (lvl: number, line: string): string => { - return `${' '.repeat(lvl)}${line}`; + return `${' '.repeat(lvl)}${line}` } -const INDENT_SPACES = 2; +const INDENT_SPACES = 2 -export const astToString = (node: Node, indent: number = 0): string => { +export const astToString = (node: Node | NativeDeclaration, indent: number = 0): string => { switch (node.kind) { - case "NormalClassDeclaration": - const c = node as NormalClassDeclaration; - const cModifier = c.classModifier.length > 0 ? c.classModifier.join(" ") + " " : ""; - const cSuperclass = c.sclass ? " extends " + c.sclass : ""; - return newline(indentLine(indent, `${cModifier}class ${c.typeIdentifier}${cSuperclass} {`)) - + `${c.classBody.map(b => newline(astToString(b, indent + INDENT_SPACES))).join("")}` - + `${indentLine(indent, "}")}`; - - case "Block": - const block = node as Block; - return `${newline(indentLine(indent, "{"))}` - + `${block.blockStatements.map(s => newline(astToString(s, indent + INDENT_SPACES))).join("")}` - + `${indentLine(indent, "}")}`; - - case "ConstructorDeclaration": - const con = node as ConstructorDeclaration; - const conModifier = con.constructorModifier.length > 0 - ? con.constructorModifier.join(" ") + " " - : ""; - const conIdentifier = con.constructorDeclarator.identifier; - const conParams = con.constructorDeclarator.formalParameterList.map(p => `${p.unannType} ${p.identifier}`).join(", "); - return newline(indentLine(indent, `${conModifier}${conIdentifier}(${conParams}) {`)) - + `${con.constructorBody.blockStatements.map(s => newline(astToString(s, indent + INDENT_SPACES))).join("")}` - + `${indentLine(indent, "}")}`; - - case "MethodDeclaration": - const mtd = node as MethodDeclaration; - const mtdModifier = mtd.methodModifier.length > 0 ? mtd.methodModifier.join(" ") + " " : ""; - const mtdRes = mtd.methodHeader.result; - const mtdIdentifier = mtd.methodHeader.identifier; - const mtdParams = mtd.methodHeader.formalParameterList.map(p => `${p.unannType} ${p.identifier}`).join(", "); - return newline(indentLine(indent, `${mtdModifier}${mtdRes} ${mtdIdentifier}(${mtdParams}) {`)) - + `${mtd.methodBody.blockStatements.map(s => newline(astToString(s, indent + INDENT_SPACES))).join("")}` - + `${indentLine(indent, "}")}`; - - case "FieldDeclaration": - const field = node as FieldDeclaration; - const fieldModifier = field.fieldModifier.length > 0 ? field.fieldModifier.join(" ") + " " : ""; - const fieldType = field.fieldType; - const fieldIdentifier = field.variableDeclaratorList[0].variableDeclaratorId; + case 'NormalClassDeclaration': + const c = node as NormalClassDeclaration + const cModifier = c.classModifier.length > 0 ? c.classModifier.join(' ') + ' ' : '' + const cSuperclass = c.sclass ? ' extends ' + c.sclass : '' + return ( + newline(indentLine(indent, `${cModifier}class ${c.typeIdentifier}${cSuperclass} {`)) + + `${c.classBody.map(b => newline(astToString(b, indent + INDENT_SPACES))).join('')}` + + `${indentLine(indent, '}')}` + ) + + case 'Block': + const block = node as Block + return ( + `${newline(indentLine(indent, '{'))}` + + `${block.blockStatements.map(s => newline(astToString(s, indent + INDENT_SPACES))).join('')}` + + `${indentLine(indent, '}')}` + ) + + case 'ConstructorDeclaration': + const con = node as ConstructorDeclaration + const conModifier = + con.constructorModifier.length > 0 ? con.constructorModifier.join(' ') + ' ' : '' + const conIdentifier = con.constructorDeclarator.identifier + const conParams = con.constructorDeclarator.formalParameterList + .map(p => `${p.unannType} ${p.identifier}`) + .join(', ') + return ( + newline(indentLine(indent, `${conModifier}${conIdentifier}(${conParams}) {`)) + + `${con.constructorBody.blockStatements.map(s => newline(astToString(s, indent + INDENT_SPACES))).join('')}` + + `${indentLine(indent, '}')}` + ) + + case 'MethodDeclaration': + const normalMtd = node as MethodDeclaration + const normalMtdModifier = + normalMtd.methodModifier.length > 0 ? normalMtd.methodModifier.join(' ') + ' ' : '' + const normalMtdRes = normalMtd.methodHeader.result + const normalMtdIdentifier = normalMtd.methodHeader.identifier + const normalMtdParams = normalMtd.methodHeader.formalParameterList + .map(p => `${p.unannType} ${p.identifier}`) + .join(', ') + return ( + newline( + indentLine( + indent, + `${normalMtdModifier}${normalMtdRes} ${normalMtdIdentifier}(${normalMtdParams}) {` + ) + ) + + `${normalMtd.methodBody.blockStatements.map(s => newline(astToString(s, indent + INDENT_SPACES))).join('')}` + + `${indentLine(indent, '}')}` + ) + + case 'NativeDeclaration': + const nativeMtd = node as NativeDeclaration + const nativeMtdModifier = + nativeMtd.methodModifier.length > 0 ? nativeMtd.methodModifier.join(' ') + ' ' : '' + const nativeMtdRes = nativeMtd.methodHeader.result + const nativeMtdIdentifier = nativeMtd.methodHeader.identifier + const nativeMtdParams = nativeMtd.methodHeader.formalParameterList + .map(p => `${p.unannType} ${p.identifier}`) + .join(', ') + return newline( + indentLine( + indent, + `${nativeMtdModifier}${nativeMtdRes} ${nativeMtdIdentifier}(${nativeMtdParams});` + ) + ) + + case 'FieldDeclaration': + const field = node as FieldDeclaration + const fieldModifier = + field.fieldModifier.length > 0 ? field.fieldModifier.join(' ') + ' ' : '' + const fieldType = field.fieldType + const fieldIdentifier = field.variableDeclaratorList[0].variableDeclaratorId const fieldInit = field.variableDeclaratorList[0].variableInitializer ? ` = ${astToString(field.variableDeclaratorList[0].variableInitializer! as ExpressionName)}` - : ""; - return indentLine(indent, `${fieldModifier}${fieldType} ${fieldIdentifier}${fieldInit};`); + : '' + return indentLine(indent, `${fieldModifier}${fieldType} ${fieldIdentifier}${fieldInit};`) - case "LocalVariableDeclarationStatement": - const localVar = node as LocalVariableDeclarationStatement; - const localVarType = localVar.localVariableType; - const localVarIdentifier = localVar.variableDeclaratorList[0].variableDeclaratorId; + case 'LocalVariableDeclarationStatement': + const localVar = node as LocalVariableDeclarationStatement + const localVarType = localVar.localVariableType + const localVarIdentifier = localVar.variableDeclaratorList[0].variableDeclaratorId const localVarInit = localVar.variableDeclaratorList[0].variableInitializer ? ` = ${astToString(localVar.variableDeclaratorList[0].variableInitializer! as ExpressionName)}` - : ""; - return indentLine(indent, `${localVarType} ${localVarIdentifier}${localVarInit};`); + : '' + return indentLine(indent, `${localVarType} ${localVarIdentifier}${localVarInit};`) - case "ReturnStatement": - const returnStmt = node as ReturnStatement; - const returnExp = returnStmt.exp.kind === "Void" ? "" : ` ${astToString(returnStmt.exp)}`; - return indentLine(indent, `return${returnExp};`); + case 'ReturnStatement': + const returnStmt = node as ReturnStatement + const returnExp = returnStmt.exp.kind === 'Void' ? '' : ` ${astToString(returnStmt.exp)}` + return indentLine(indent, `return${returnExp};`) - case "ExpressionStatement": - const expStmt = node as ExpressionStatement; - return indentLine(indent, `${astToString(expStmt.stmtExp)};`); + case 'ExpressionStatement': + const expStmt = node as ExpressionStatement + return indentLine(indent, `${astToString(expStmt.stmtExp)};`) - case "Assignment": - const assmt = node as Assignment; - return `${(assmt.left as ExpressionName).name} = ${astToString(assmt.right)}`; + case 'Assignment': + const assmt = node as Assignment + return `${(assmt.left as ExpressionName).name} = ${astToString(assmt.right)}` - case "MethodInvocation": - const mtdInv = node as MethodInvocation; - return `${mtdInv.identifier}(${mtdInv.argumentList.map(a => astToString(a)).join(", ")})` + case 'MethodInvocation': + const mtdInv = node as MethodInvocation + return `${mtdInv.identifier}(${mtdInv.argumentList.map(a => astToString(a)).join(', ')})` - case "ClassInstanceCreationExpression": - const classInstanceCreationExp = node as ClassInstanceCreationExpression; - return `new ${classInstanceCreationExp.identifier}(${classInstanceCreationExp.argumentList.map(a => astToString(a))})`; + case 'ClassInstanceCreationExpression': + const classInstanceCreationExp = node as ClassInstanceCreationExpression + return `new ${classInstanceCreationExp.identifier}(${classInstanceCreationExp.argumentList.map(a => astToString(a))})` - case "ExplicitConstructorInvocation": - const expConInv = node as ExplicitConstructorInvocation; - return indentLine(indent, `${expConInv.thisOrSuper}(${expConInv.argumentList.map(a => astToString(a))});`); + case 'ExplicitConstructorInvocation': + const expConInv = node as ExplicitConstructorInvocation + return indentLine( + indent, + `${expConInv.thisOrSuper}(${expConInv.argumentList.map(a => astToString(a))});` + ) - case "BinaryExpression": - const bin = node as BinaryExpression; - return `${astToString(bin.left)} ${bin.operator} ${astToString(bin.right)}`; + case 'BinaryExpression': + const bin = node as BinaryExpression + return `${astToString(bin.left)} ${bin.operator} ${astToString(bin.right)}` - case "ExpressionName": - const exp = node as ExpressionName; - return exp.name; + case 'ExpressionName': + const exp = node as ExpressionName + return exp.name - case "Literal": - const literal = node as Literal; - return `${literal.literalType.value}`; + case 'Literal': + const literal = node as Literal + return `${literal.literalType.value}` - case "Void": - return "void"; + case 'Void': + return 'void' default: - return node.kind; + return node.kind } -} \ No newline at end of file +} diff --git a/src/ec-evaluator/__tests__/__utils__/library.ts b/src/ec-evaluator/__tests__/__utils__/library.ts new file mode 100644 index 00000000..064eb27f --- /dev/null +++ b/src/ec-evaluator/__tests__/__utils__/library.ts @@ -0,0 +1,35 @@ +import { runECEvaluator } from '../..' +import { Result } from '../../types' + +export type LibraryRun = { result: Result; out: string[]; err: string[] } + +/** Runs a program through the public entry point, capturing stdout and stderr. */ +export const run = async (code: string, targetStep?: number): Promise => { + const out: string[] = [] + const err: string[] = [] + const result = await runECEvaluator(code, targetStep, { + stdout: s => out.push(s), + stderr: (type, msg) => err.push(`${type}: ${msg}`) + }) + return { result, out, err } +} + +/** Wraps statements in a `Main.main`, with optional extra top-level classes. */ +export const program = (body: string, classes: string = '') => ` + ${classes} + public class Main { + public static void main(String[] args) { + ${body} + } + }` + +/** Every prefix run must print a prefix of the full run's output: the re-run stepping model. */ +export const expectStableAcrossSteps = async (code: string) => { + const full = await run(code) + expect(full.result.status).toBe('finished') + for (let step = 0; step <= full.result.context.totalSteps; step++) { + const partial = await run(code, step) + expect(partial.out).toEqual(full.out.slice(0, partial.out.length)) + } + return full +} diff --git a/src/ec-evaluator/__tests__/__utils__/utils.ts b/src/ec-evaluator/__tests__/__utils__/utils.ts index cb6ce5ce..2520eb8d 100644 --- a/src/ec-evaluator/__tests__/__utils__/utils.ts +++ b/src/ec-evaluator/__tests__/__utils__/utils.ts @@ -1,26 +1,34 @@ -import { Environment } from "../../components"; -import { STEP_LIMIT } from "../../constants"; -import { ControlItem, Context, StashItem, StructType } from "../../types"; -import { Stack, isNode } from "../../utils"; +import { Environment } from '../../components' +import { STEP_LIMIT } from '../../constants' +import { LFSR } from '../../lib' +import { ControlItem, Context, StashItem, StructType } from '../../types' +import { Stack, isNode } from '../../utils' +import { makeObjectClass } from '../../index' export class StackStub extends Stack { - private trace: T[] = []; + private trace: T[] = [] public push(...items: T[]): void { for (const item of items) { - super.push(item); - this.trace.push(item); + super.push(item) + this.trace.push(item) } - }; + } public getTrace(): T[] { - return this.trace; - }; -}; -export class ControlStub extends StackStub {}; -export class StashStub extends StackStub {}; + return this.trace + } +} + +export class ControlStub extends StackStub {} +export class StashStub extends StackStub {} // TODO make env traceable -export class EnvironmentStub extends Environment {}; +export class EnvironmentStub extends Environment { + constructor() { + super() + this.defineClass('Object', makeObjectClass()) + } +} export const createContextStub = (): Context => ({ errors: [], @@ -28,24 +36,31 @@ export const createContextStub = (): Context => ({ stash: new StashStub(), environment: new EnvironmentStub(), totalSteps: STEP_LIMIT, -}); + interfaces: { + stdout: console.log, + stderr: console.error, + statics: { + lfsr: new LFSR('') + } + } +}) export const getControlItemStr = (i: ControlItem): string => { - return isNode(i) ? i.kind : i.instrType; -}; + return isNode(i) ? i.kind : i.instrType +} export const getStashItemStr = (i: StashItem): string => { - return i.kind === "Literal" - ? i.literalType.value + return i.kind === 'Literal' + ? i.literalType.value : i.kind === StructType.CLOSURE - ? i.mtdOrCon.kind === "MethodDeclaration" - ? i.mtdOrCon.methodHeader.identifier - : i.mtdOrCon.constructorDeclarator.identifier - : i.kind === StructType.VARIABLE - ? i.name - : i.kind === StructType.CLASS - ? i.frame.name - : i.kind === StructType.TYPE - ? i.type - : i.kind; -}; + ? i.mtdOrCon.kind === 'MethodDeclaration' || i.mtdOrCon.kind === 'NativeDeclaration' + ? i.mtdOrCon.methodHeader.identifier + : i.mtdOrCon.constructorDeclarator.identifier + : i.kind === StructType.VARIABLE + ? i.name + : i.kind === StructType.CLASS + ? i.frame.name + : i.kind === StructType.TYPE + ? i.type + : i.kind +} diff --git a/src/ec-evaluator/__tests__/arithmetic.test.ts b/src/ec-evaluator/__tests__/arithmetic.test.ts index 62f8828d..d69bf6a7 100644 --- a/src/ec-evaluator/__tests__/arithmetic.test.ts +++ b/src/ec-evaluator/__tests__/arithmetic.test.ts @@ -30,10 +30,6 @@ it("evaluate LocalVariableDeclarationStatement to a basic arithmetic operation c "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // public class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} @@ -124,10 +120,6 @@ it("evaluate LocalVariableDeclarationStatement to a complex arithmetic operation "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // public class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} @@ -229,10 +221,6 @@ it("evaluate FieldDeclaration to a basic arithmetic expression without brackets "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // public class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {} @@ -323,10 +311,6 @@ it("evaluate FieldDeclaration to a complex arithmetic expression without bracket "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // public class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {} diff --git a/src/ec-evaluator/__tests__/class-decl.test.ts b/src/ec-evaluator/__tests__/class-decl.test.ts index 7723f251..0d3a02a6 100644 --- a/src/ec-evaluator/__tests__/class-decl.test.ts +++ b/src/ec-evaluator/__tests__/class-decl.test.ts @@ -31,10 +31,6 @@ describe("evaluate NormalClassDeclaration correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "ConstructorDeclaration", // Parent() {...} @@ -109,10 +105,6 @@ describe("evaluate FieldDeclaration correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // public class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} @@ -191,10 +183,6 @@ describe("evaluate FieldDeclaration correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // public class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} @@ -274,10 +262,6 @@ describe("evaluate MethodDeclaration correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // public class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} @@ -348,10 +332,6 @@ describe("evaluate MethodDeclaration correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // public class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} @@ -422,10 +402,6 @@ describe("evaluate MethodDeclaration correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // public class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // private int test2(int x) {} diff --git a/src/ec-evaluator/__tests__/class-instance-creation.test.ts b/src/ec-evaluator/__tests__/class-instance-creation.test.ts index e25a4d4a..25e4dcca 100644 --- a/src/ec-evaluator/__tests__/class-instance-creation.test.ts +++ b/src/ec-evaluator/__tests__/class-instance-creation.test.ts @@ -32,10 +32,6 @@ describe("evaluate default constructor", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} @@ -191,10 +187,6 @@ describe("evaluate default constructor", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} @@ -353,10 +345,6 @@ describe("evaluate default constructor", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} @@ -530,10 +518,6 @@ describe("evaluate default constructor", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} @@ -707,10 +691,6 @@ describe("evaluate default constructor", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} diff --git a/src/ec-evaluator/__tests__/constructor-overloading.test.ts b/src/ec-evaluator/__tests__/constructor-overloading.test.ts index ec7d2b9c..544f9d67 100644 --- a/src/ec-evaluator/__tests__/constructor-overloading.test.ts +++ b/src/ec-evaluator/__tests__/constructor-overloading.test.ts @@ -44,10 +44,6 @@ describe("evaluate constructor overloading resolution correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "ConstructorDeclaration", // Parent(Parent p) {...} @@ -265,10 +261,6 @@ describe("evaluate constructor overloading resolution correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} @@ -454,10 +446,6 @@ describe("evaluate constructor overloading resolution correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // Test Test(int x) {...} @@ -644,10 +632,6 @@ describe("evaluate constructor overloading resolution correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} diff --git a/src/ec-evaluator/__tests__/inheritance-instance-field.test.ts b/src/ec-evaluator/__tests__/inheritance-instance-field.test.ts index a4d9563f..18c1632d 100644 --- a/src/ec-evaluator/__tests__/inheritance-instance-field.test.ts +++ b/src/ec-evaluator/__tests__/inheritance-instance-field.test.ts @@ -41,10 +41,6 @@ describe("evaluate instance fields with inheritance", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // void test() {...} @@ -299,10 +295,6 @@ describe("evaluate instance fields with inheritance", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // void test() {...} @@ -568,10 +560,6 @@ describe("evaluate instance fields with inheritance", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "ConstructorDeclaration", // Parent() {...} @@ -788,10 +776,6 @@ describe("evaluate instance fields with inheritance", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "ConstructorDeclaration", // Parent() {...} @@ -1023,10 +1007,6 @@ describe("evaluate instance fields with inheritance", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "ConstructorDeclaration", // Parent() {...} diff --git a/src/ec-evaluator/__tests__/inheritance-static-field.test.ts b/src/ec-evaluator/__tests__/inheritance-static-field.test.ts index f5befa9c..38f2464c 100644 --- a/src/ec-evaluator/__tests__/inheritance-static-field.test.ts +++ b/src/ec-evaluator/__tests__/inheritance-static-field.test.ts @@ -36,10 +36,6 @@ describe("evaluate static fields with inheritance", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "ConstructorDeclaration", // Parent() {...} @@ -149,10 +145,6 @@ describe("evaluate static fields with inheritance", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "ConstructorDeclaration", // Parent() {...} @@ -271,10 +263,6 @@ describe("evaluate static fields with inheritance", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "ConstructorDeclaration", // Parent() {...} @@ -485,10 +473,6 @@ describe("evaluate static fields with inheritance", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "ConstructorDeclaration", // Parent() {...} @@ -708,10 +692,6 @@ describe("evaluate static fields with inheritance", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "ConstructorDeclaration", // Parent() {...} diff --git a/src/ec-evaluator/__tests__/inherited-override.test.ts b/src/ec-evaluator/__tests__/inherited-override.test.ts new file mode 100644 index 00000000..d349be38 --- /dev/null +++ b/src/ec-evaluator/__tests__/inherited-override.test.ts @@ -0,0 +1,63 @@ +import { program, run } from './__utils__/library' + +describe('method overriding through an intermediate class', () => { + it('runs an override inherited from an intermediate class', async () => { + const { result, out } = await run( + program( + `Base b = new C(); int r = b.f(); System.out.println(r);`, + `class Base { public int f() { return 1; } } + class B extends Base { public int f() { return 2; } } + class C extends B {}` + ) + ) + expect(result.status).toBe('finished') + expect(out).toEqual(['2']) + }) + + it('prefers the nearest override', async () => { + const { out } = await run( + program( + `Base b = new C(); int r = b.f(); System.out.println(r);`, + `class Base { public int f() { return 1; } } + class B extends Base { public int f() { return 2; } } + class C extends B { public int f() { return 3; } }` + ) + ) + expect(out).toEqual(['3']) + }) + + it('matches the method name exactly, not as a suffix', async () => { + const { out } = await run( + program( + `Base b = new C(); int r = b.f(); System.out.println(r);`, + `class Base { public int f() { return 1; } } + class B extends Base { public int wolf() { return 9; } } + class C extends B { public int elf() { return 8; } }` + ) + ) + expect(out).toEqual(['1']) + }) + + it('does not override a private method', async () => { + const { out } = await run( + program( + `C c = new C(); int r = c.g(); System.out.println(r);`, + `class A { private int f() { return 1; } public int g() { int r = f(); return r; } } + class B extends A { public int f() { return 2; } } + class C extends B {}` + ) + ) + expect(out).toEqual(['1']) + }) + + it("runs an inherited override of Object's toString", async () => { + const { out } = await run( + program( + `Object o = new Dog(); String s = o.toString(); System.out.println(s);`, + `class Animal { public String toString() { return "animal"; } } + class Dog extends Animal {}` + ) + ) + expect(out).toEqual(['animal']) + }) +}) diff --git a/src/ec-evaluator/__tests__/library/Object.test.ts b/src/ec-evaluator/__tests__/library/Object.test.ts new file mode 100644 index 00000000..ddbad674 --- /dev/null +++ b/src/ec-evaluator/__tests__/library/Object.test.ts @@ -0,0 +1,161 @@ +import { StructType } from '../../types' +import { expectStableAcrossSteps, program, run } from '../__utils__/library' + +const HEX_LABEL = /^([\w.]+)@([0-9a-f]+)$/ + +describe('Object.hashCode', () => { + it('is stable across calls on the same object', async () => { + const { result, out } = await run( + program( + `Dog d = new Dog(); + int a = d.hashCode(); int b = d.hashCode(); + System.out.println(a); System.out.println(b);`, + 'class Dog {}' + ) + ) + expect(result.status).toBe('finished') + expect(out[1]).toBe(out[0]) + }) + + it('is distinct and non-negative across many objects', async () => { + const n = 40 + const body = Array.from( + { length: n }, + (_, i) => `Dog d${i} = new Dog(); int h${i} = d${i}.hashCode(); System.out.println(h${i});` + ).join('\n') + const { result, out } = await run(program(body, 'class Dog {}')) + expect(result.status).toBe('finished') + expect(out).toHaveLength(n) + expect(out.every(h => Number(h) > 0)).toBe(true) + expect(new Set(out).size).toBe(n) + }) + + it('is inherited by subclasses and works on Object itself', async () => { + const { result, out } = await run( + program( + `Dog d = new Dog(); int a = d.hashCode(); System.out.println(a); + Object o = new Object(); int b = o.hashCode(); System.out.println(b);`, + 'class Animal {} class Dog extends Animal {}' + ) + ) + expect(result.status).toBe('finished') + expect(out).toHaveLength(2) + expect(out[0]).not.toBe(out[1]) + }) + + it('gives the same values when the same program is run again', async () => { + const code = program( + `Dog d = new Dog(); Dog e = new Dog(); + int a = d.hashCode(); int b = e.hashCode(); + System.out.println(a); System.out.println(b);`, + 'class Dog {}' + ) + expect((await run(code)).out).toEqual((await run(code)).out) + }) + + it('is deterministic across re-runs to any step', async () => { + const full = await expectStableAcrossSteps( + program( + `Dog d = new Dog(); Dog e = new Dog(); + int h = d.hashCode(); System.out.println(h); + String s = e.toString(); System.out.println(s); + int k = d.hashCode(); System.out.println(k);`, + 'class Dog {}' + ) + ) + expect(full.out).toHaveLength(3) + expect(full.out[2]).toBe(full.out[0]) + }) + + it('can be overridden', async () => { + const { out } = await run( + program( + `Dog d = new Dog(); int h = d.hashCode(); System.out.println(h);`, + 'class Dog { public int hashCode() { return 255; } }' + ) + ) + expect(out).toEqual(['255']) + }) +}) + +describe('Object.toString', () => { + it('is ClassName@hex, with the hex of hashCode', async () => { + const { result, out } = await run( + program( + `Dog d = new Dog(); + int h = d.hashCode(); String s = d.toString(); + System.out.println(h); System.out.println(s);`, + 'class Dog {}' + ) + ) + expect(result.status).toBe('finished') + const [, className, hex] = out[1].match(HEX_LABEL)! + expect(className).toBe('Dog') + expect(parseInt(hex, 16)).toBe(Number(out[0])) + }) + + it('is stable for one object and distinct between objects', async () => { + const { out } = await run( + program( + `Dog d = new Dog(); Dog e = new Dog(); + String a = d.toString(); String b = e.toString(); String c = d.toString(); + System.out.println(a); System.out.println(b); System.out.println(c);`, + 'class Dog {}' + ) + ) + expect(out[0]).toMatch(HEX_LABEL) + expect(out[1]).toMatch(HEX_LABEL) + expect(out[0]).not.toBe(out[1]) + expect(out[2]).toBe(out[0]) + }) + + it('names the runtime class by its binary name, not the declared type', async () => { + const { out } = await run( + program( + `Animal a = new Dog(); String s = a.toString(); System.out.println(s); + Object o = new Dog(); String t = o.toString(); System.out.println(t); + Object p = new Object(); String u = p.toString(); System.out.println(u);`, + 'class Animal {} class Dog extends Animal {}' + ) + ) + expect(out.map(s => s.match(HEX_LABEL)![1])).toEqual(['Dog', 'Dog', 'java.lang.Object']) + }) + + it('can be overridden', async () => { + const { out } = await run( + program( + `Dog d = new Dog(); String s = d.toString(); System.out.println(s);`, + 'class Dog { public String toString() { return "a dog"; } }' + ) + ) + expect(out).toEqual(['a dog']) + }) + + it('returns a string literal with its quotes, like any other string value', async () => { + // the visualizer renders literalType.value verbatim + const code = program(`Dog d = new Dog(); String s = d.toString();`, 'class Dog {}') + const { result } = await run(code) + let found: string | undefined + for (let step = 0; step <= result.context.totalSteps && !found; step++) { + const top = (await run(code, step)).result.context.stash.peek() + if (top?.kind === 'Literal' && top.literalType.value.includes('Dog@')) { + found = top.literalType.value + } + } + expect(found).toMatch(/^"Dog@[0-9a-f]+"$/) + }) + + it('is backed by the memoised hash on the object', async () => { + const { result } = await run( + program(`Dog d = new Dog(); String s = d.toString();`, 'class Dog {}') + ) + const dogs = result.context.environment.objects.filter( + o => o.kind === StructType.OBJECT && o.class.classDecl.typeIdentifier === 'Dog' + ) + expect(dogs).toHaveLength(1) + expect(dogs[0].hashCode).toBeGreaterThan(0) + }) + + // Java's Object.toString calls the (overridable) hashCode(); the native reads the hash directly. + it.todo('uses an overridden hashCode') +}) diff --git a/src/ec-evaluator/__tests__/library/System.test.ts b/src/ec-evaluator/__tests__/library/System.test.ts new file mode 100644 index 00000000..b77d505e --- /dev/null +++ b/src/ec-evaluator/__tests__/library/System.test.ts @@ -0,0 +1,115 @@ +import { runECEvaluator } from '../..' +import { NullPointerException } from '../../errors' +import { expectStableAcrossSteps, program, run } from '../__utils__/library' + +describe('System.out.println', () => { + it('prints a string literal without its quotes', async () => { + const { result, out, err } = await run(program(`System.out.println("hello");`)) + expect(result.status).toBe('finished') + expect(out).toEqual(['hello']) + expect(err).toEqual([]) + }) + + it('decodes escape sequences', async () => { + const { out } = await run(program(`System.out.println("a\\tb\\"c\\\\d\\ne");`)) + expect(out).toEqual(['a\tb"c\\d\ne']) + }) + + it('decodes octal escape sequences', async () => { + const { out } = await run(program(`System.out.println("\\141|\\60|\\0x|\\377|\\400");`)) + expect(out).toEqual(['a|0|\0x|\xff|\x200']) + }) + + it('prints a text block with its incidental indentation stripped', async () => { + // closing delimiter on its own line: it sets the indentation, and the value ends in a newline + const block = '"""\n hello \n world\n\n """' + const { out } = await run(program(`String s = ${block}; System.out.println(s);`)) + expect(out).toEqual(['hello\n world\n\n']) + }) + + it('decodes the space escape, which survives trailing white space stripping', async () => { + const { out } = await run(program(`System.out.println("a\\sb");`)) + expect(out).toEqual(['a b']) + const block = '"""\n x\\s\n """' + const { out: blockOut } = await run(program(`System.out.println(${block});`)) + expect(blockOut).toEqual(['x \n']) + }) + + it('decodes escapes and line continuations in a text block', async () => { + const block = '"""\n a\\tb \\\n c\\""""' + const { out } = await run(program(`System.out.println(${block});`)) + expect(out).toEqual(['a\tb c"']) + }) + + it('prints the empty string', async () => { + const { out } = await run(program(`System.out.println("");`)) + expect(out).toEqual(['']) + }) + + it('prints a String variable', async () => { + const { out } = await run(program(`String s = "x"; System.out.println(s);`)) + expect(out).toEqual(['x']) + }) + + it('prints an int literal and an int variable', async () => { + const { out } = await run(program(`System.out.println(7); int x = 42; System.out.println(x);`)) + expect(out).toEqual(['7', '42']) + }) + + it('prints the result of toString', async () => { + const { out } = await run( + program(`Dog d = new Dog(); String s = d.toString(); System.out.println(s);`, 'class Dog {}') + ) + expect(out[0]).toMatch(/^Dog@[0-9a-f]+$/) + }) + + it('prints in program order', async () => { + const { out } = await run( + program(`System.out.println("a"); System.out.println("b"); System.out.println("c");`) + ) + expect(out).toEqual(['a', 'b', 'c']) + }) + + it('works through a PrintStream variable', async () => { + const { out } = await run(program(`PrintStream p = System.out; p.println("via p");`)) + expect(out).toEqual(['via p']) + }) + + it('throws NullPointerException through a null PrintStream', async () => { + const { result, out } = await run(program(`PrintStream p = null; p.println("x");`)) + expect(result.status).toBe('error') + expect(result.context.errors[0]).toBeInstanceOf(NullPointerException) + expect(out).toEqual([]) + }) + + it('prints only what a partial run reaches', async () => { + const full = await expectStableAcrossSteps( + program(`System.out.println("a"); int x = 1; System.out.println("b");`) + ) + expect(full.out).toEqual(['a', 'b']) + }) + + it('defaults to console.log without io callbacks', async () => { + const log = jest.spyOn(console, 'log').mockImplementation(() => {}) + try { + const result = await runECEvaluator(program(`System.out.println("hi");`)) + expect(result.status).toBe('finished') + expect(log).toHaveBeenCalledWith('hi') + } finally { + log.mockRestore() + } + }) + + // RES_TYPE (overload resolution) types only literals of int/String, names and classes. + it.todo('prints a method call result directly, e.g. println(d.toString())') + it.todo('prints a string concatenation, e.g. println("a" + 1)') + it.todo('prints boolean and char values') +}) + +describe('System.out', () => { + it('is a PrintStream object that exists before the program starts', async () => { + const { result } = await run(program(`int x = 1;`), 0) + const objects = result.context.environment.objects + expect(objects.map(o => o.class.classDecl.typeIdentifier)).toEqual(['PrintStream']) + }) +}) diff --git a/src/ec-evaluator/__tests__/library/loading.test.ts b/src/ec-evaluator/__tests__/library/loading.test.ts new file mode 100644 index 00000000..8fdf4443 --- /dev/null +++ b/src/ec-evaluator/__tests__/library/loading.test.ts @@ -0,0 +1,101 @@ +import { typeCheck } from '../../../types' +import { createContext } from '../..' +import { UndefinedNativeMethod } from '../../errors' +import { LFSR, libraryClasses } from '../../lib' +import { isNode } from '../../utils' +import { program, run } from '../__utils__/library' + +describe('library loading', () => { + it('defines the library classes before the first step', async () => { + const { result } = await run(program(`int x = 1;`), 0) + expect([...result.context.environment.global.frame.keys()]).toEqual([ + 'Object', + 'PrintStream', + 'System' + ]) + }) + + it('does not step through the library: step 0 is the user program', async () => { + const { result } = await run(program(`int x = 1;`), 0) + expect(result.context.totalSteps).toBe(0) + const top = result.context.control.peek() + expect(top && isNode(top) && top.kind).toBe('CompilationUnit') + }) + + it('makes user classes subclasses of the library Object', async () => { + const { result } = await run(program(`int x = 1;`, 'class Dog {}')) + const dog = result.context.environment.getClass('Dog') + expect(dog.superclass?.frame.name).toBe('Object') + }) + + it('library classes type check on their own', () => { + expect(typeCheck(libraryClasses).hasTypeErrors).toBe(false) + }) + + it('type checks user code against the library', () => { + const code = program( + `Dog d = new Dog(); String s = d.toString(); int h = d.hashCode();`, + 'class Dog {}' + ) + expect(typeCheck(libraryClasses, code).hasTypeErrors).toBe(false) + }) + + it('reports type errors through stderr', async () => { + const { result, out, err } = await run(program(`int x = "s";`)) + expect(result.status).toBe('error') + expect(out).toEqual([]) + expect(err).toHaveLength(1) + expect(err[0]).toMatch(/^TypeCheck: .*incompatible types/s) + }) + + it('reports type errors only through the stderr callback', async () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}) + try { + await run(program(`int x = "s";`)) + expect(consoleError).not.toHaveBeenCalled() + } finally { + consoleError.mockRestore() + } + }) + + it('resolves to an error when the type checker throws', async () => { + // the type checker throws 'Not implemented' on records + const { result } = await run('record P(int x) {}\n' + program(`int y = 1;`)) + expect(result.status).toBe('error') + expect(result.context.errors).toHaveLength(1) + }) + + it('creates a context without arguments', () => { + expect(createContext().interfaces.statics.lfsr.next()).toBeGreaterThan(0) + }) + + it('rejects an unregistered native method when its class is declared', async () => { + const { result } = await run(program(``, 'class Dog { native void bark(); }')) + expect(result.status).toBe('error') + expect(result.context.errors[0]).toBeInstanceOf(UndefinedNativeMethod) + }) +}) + +describe('LFSR', () => { + it('is deterministic for a seed', () => { + const a = new LFSR('class Main {}') + const b = new LFSR('class Main {}') + expect(Array.from({ length: 10 }, () => a.next())).toEqual( + Array.from({ length: 10 }, () => b.next()) + ) + }) + + it('does not saturate, whatever the seed', () => { + for (let i = 0; i < 64; i++) { + const lfsr = new LFSR(`program ${i}`) + const draws = Array.from({ length: 100 }, () => lfsr.next()) + expect(draws.every(d => d > 0 && d < 2 ** 31)).toBe(true) + expect(new Set(draws).size).toBe(draws.length) + } + }) + + it('survives the empty seed', () => { + const lfsr = new LFSR('') + expect(lfsr.next()).toBeGreaterThan(0) + }) +}) diff --git a/src/ec-evaluator/__tests__/local-var-assignment.test.ts b/src/ec-evaluator/__tests__/local-var-assignment.test.ts index 471d1eaf..9282f767 100644 --- a/src/ec-evaluator/__tests__/local-var-assignment.test.ts +++ b/src/ec-evaluator/__tests__/local-var-assignment.test.ts @@ -30,10 +30,6 @@ it("evaluate LocalVariableDeclarationStatement without variableInitializer corre "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // public class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} @@ -105,10 +101,6 @@ it("evaluate LocalVariableDeclarationStatement with variableInitializer correctl "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // public class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} @@ -194,10 +186,6 @@ it("evaluate Assignment correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // public class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} @@ -281,10 +269,6 @@ it("evaluate LocalVariableDeclarationStatement with local variable as variableIn "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // public class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} diff --git a/src/ec-evaluator/__tests__/main-mtd.test.ts b/src/ec-evaluator/__tests__/main-mtd.test.ts index 60d9d389..75fb9938 100644 --- a/src/ec-evaluator/__tests__/main-mtd.test.ts +++ b/src/ec-evaluator/__tests__/main-mtd.test.ts @@ -65,10 +65,6 @@ describe("evaluate main method correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class AnotherTest {...} "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // static void main(String[] args) {...} @@ -154,10 +150,6 @@ describe("evaluate main method correctly", () => { "ExpressionStatement", // test.main([""]); "NormalClassDeclaration", // class test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // void test() {...} diff --git a/src/ec-evaluator/__tests__/mtd-overloading.test.ts b/src/ec-evaluator/__tests__/mtd-overloading.test.ts index 73458849..d5ccfd2f 100644 --- a/src/ec-evaluator/__tests__/mtd-overloading.test.ts +++ b/src/ec-evaluator/__tests__/mtd-overloading.test.ts @@ -36,10 +36,6 @@ describe("evaluate method overloading correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // static void test() {...} @@ -171,10 +167,6 @@ describe("evaluate method overloading correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // void Test() {...} @@ -409,10 +401,6 @@ describe("evaluate method overloading correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "ConstructorDeclaration", // Parent() {...} @@ -668,10 +656,6 @@ describe("evaluate method overloading correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "ConstructorDeclaration", // Parent() {...} @@ -928,10 +912,6 @@ describe("evaluate method overloading correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "ConstructorDeclaration", // Parent() {...} @@ -1238,10 +1218,6 @@ describe("evaluate method overloading correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // static void test() {...} diff --git a/src/ec-evaluator/__tests__/mtd-overriding.test.ts b/src/ec-evaluator/__tests__/mtd-overriding.test.ts index 9de98542..5b6a4fbe 100644 --- a/src/ec-evaluator/__tests__/mtd-overriding.test.ts +++ b/src/ec-evaluator/__tests__/mtd-overriding.test.ts @@ -42,10 +42,6 @@ describe("evaluate method overriding correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // void test() {...} @@ -294,10 +290,6 @@ describe("evaluate method overriding correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // static void test() {...} @@ -540,10 +532,6 @@ describe("evaluate method overriding correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // void test(Parent p) {...} @@ -916,10 +904,6 @@ describe("evaluate method overriding correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // void test(Parent p) {...} diff --git a/src/ec-evaluator/__tests__/natives.test.ts b/src/ec-evaluator/__tests__/natives.test.ts index e1d8716d..6dc5aba0 100644 --- a/src/ec-evaluator/__tests__/natives.test.ts +++ b/src/ec-evaluator/__tests__/natives.test.ts @@ -1,4 +1,4 @@ -import { natives } from '../natives' +import { foreigns } from '../natives' import { // ControlStub, // StashStub, @@ -10,10 +10,38 @@ import { parse } from '../../ast/parser' import { evaluate } from '../interpreter' describe('native functions', () => { - it('should invoke external native function', () => { + it('method declaration should dynamically link foreign function', () => { const mockForeignFn = jest.fn() - natives['testNative(): void'] = mockForeignFn + foreigns['C::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) + + const c = context.environment.getClass('C') + c.instanceMethods.find(md => md) + }) + + it('invoke instruction should invoke external native function', () => { + const mockForeignFn = jest.fn() + + foreigns['C::testNative(): void'] = mockForeignFn const programStr = ` class C { @@ -42,7 +70,7 @@ describe('native functions', () => { expect(s).toBe('"Test"') }) - natives['testNative(String s): void'] = foreignFn + foreigns['C::testNative(String s): void'] = foreignFn const programStr = ` class C { @@ -75,8 +103,8 @@ describe('native functions', () => { expect(x).toBe('7') }) - natives['viaControl(): int'] = foreignFn - natives['check(int x): void'] = checkFn + foreigns['C::viaControl(): int'] = foreignFn + foreigns['C::check(int x): void'] = checkFn const programStr = ` class C { diff --git a/src/ec-evaluator/__tests__/qualified-mtd-inv.test.ts b/src/ec-evaluator/__tests__/qualified-mtd-inv.test.ts index d020462c..f9966012 100644 --- a/src/ec-evaluator/__tests__/qualified-mtd-inv.test.ts +++ b/src/ec-evaluator/__tests__/qualified-mtd-inv.test.ts @@ -32,10 +32,6 @@ describe("evaluate simple qualified MethodInvocation correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // static void test() {...} @@ -140,10 +136,6 @@ describe("evaluate simple qualified MethodInvocation correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // static void test() {...} @@ -320,10 +312,6 @@ describe("evaluate simple qualified MethodInvocation correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // void test() {...} @@ -536,10 +524,6 @@ describe("evaluate simple qualified MethodInvocation correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // int test() {...} @@ -729,10 +713,6 @@ describe("evaluate simple qualified MethodInvocation correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // void test() {...} @@ -1019,10 +999,6 @@ describe("evaluate complex qualified MethodInvocation correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // static void test() {...} @@ -1148,10 +1124,6 @@ describe("evaluate complex qualified MethodInvocation correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class AnotherTest {...} "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "MethodDeclaration", // public static void main(String[] args) {...} @@ -1355,10 +1327,6 @@ describe("evaluate complex qualified MethodInvocation correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test extends Parent {...} "NormalClassDeclaration", // class Parent {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", // from NormalClassDeclaration "ConstructorDeclaration", // Parent() {...} diff --git a/src/ec-evaluator/__tests__/qualified-names-this.test.ts b/src/ec-evaluator/__tests__/qualified-names-this.test.ts index 302ff12d..020a5835 100644 --- a/src/ec-evaluator/__tests__/qualified-names-this.test.ts +++ b/src/ec-evaluator/__tests__/qualified-names-this.test.ts @@ -32,10 +32,6 @@ describe("evaluate simple qualified names correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // public static void main(String[] args) {...} @@ -132,10 +128,6 @@ describe("evaluate simple qualified names correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // public static void main(String[] args) {...} @@ -306,10 +298,6 @@ describe("evaluate simple qualified names correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // public static void main(String[] args) {...} @@ -413,10 +401,6 @@ describe("evaluate simple qualified names correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // public static void main(String[] args) {...} @@ -597,10 +581,6 @@ describe("evaluate complex qualified names correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // public static void main(String[] args) {...} diff --git a/src/ec-evaluator/__tests__/simple-mtd-inv.test.ts b/src/ec-evaluator/__tests__/simple-mtd-inv.test.ts index 9c543701..39a938ee 100644 --- a/src/ec-evaluator/__tests__/simple-mtd-inv.test.ts +++ b/src/ec-evaluator/__tests__/simple-mtd-inv.test.ts @@ -32,10 +32,6 @@ describe("evaluate static MethodInvocation correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // static void test() {...} @@ -143,10 +139,6 @@ describe("evaluate static MethodInvocation correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // void test() {...} @@ -362,10 +354,6 @@ describe("evaluate instance MethodInvocation correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // void test() {...} diff --git a/src/ec-evaluator/__tests__/this-keyword.test.ts b/src/ec-evaluator/__tests__/this-keyword.test.ts index 8683a0f0..c452588f 100644 --- a/src/ec-evaluator/__tests__/this-keyword.test.ts +++ b/src/ec-evaluator/__tests__/this-keyword.test.ts @@ -36,10 +36,6 @@ describe("evaluate field access with this keyword correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // void test() {...} @@ -247,10 +243,6 @@ describe("evaluate field access with this keyword correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // void test() {...} @@ -467,10 +459,6 @@ describe("evaluate method invocation with this keyword correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // void test() {...} @@ -686,10 +674,6 @@ describe("evaluate method invocation with this keyword correctly", () => { "ExpressionStatement", // Test.main([""]); "NormalClassDeclaration", // class Test {...} - "NormalClassDeclaration", // class Object {...} - - "Env", // from NormalClassDeclaration - "ConstructorDeclaration", // Object() {...} "Env", "MethodDeclaration", // void test() {...} diff --git a/src/ec-evaluator/index.ts b/src/ec-evaluator/index.ts index 9fbf01b0..c7ea9a0a 100644 --- a/src/ec-evaluator/index.ts +++ b/src/ec-evaluator/index.ts @@ -1,18 +1,43 @@ import { parse } from '../ast/parser' +import { typeCheck } from '../types' import { Control, Environment, Stash } from './components' import { STEP_LIMIT } from './constants' import { RuntimeError } from './errors' import { evaluate } from './interpreter' -import { Context, Error, Finished, Result } from './types' +import { LFSR, libraryClasses } from './lib' +import { Class, Context, Error, Finished, Interfaces, IOCallbacks, Result } from './types' +import { handleSequence } from './utils' export * from './components' export * from './errors' export * from './types' export { isInstr, isNode } from './utils' -export const runECEvaluator = (code: string, targetStep: number = STEP_LIMIT): Promise => { - const context = createContext() +export const runECEvaluator = ( + code: string, + targetStep: number = STEP_LIMIT, + ioCallbacks?: IOCallbacks +): Promise => { + const context = createContext(code, ioCallbacks) + try { + // type checking; typeCheck() may also throw, e.g. on unsupported syntax + const typeCheckResult = typeCheck(libraryClasses, code) + if (typeCheckResult.hasTypeErrors) { + const typeErrMsg = typeCheckResult.errorMsgs.join('\n') + context.interfaces.stderr('TypeCheck', typeErrMsg) + return Promise.resolve({ status: 'error', context } as Error) + } + + // load library + const libraryCompilationUnit = parse(libraryClasses) + context.control.push( + ...handleSequence(libraryCompilationUnit.topLevelClassOrInterfaceDeclarations) + ) + + // step size limit is irrelevant here, just makes injected library classes run to completion + evaluate(context, 100000) + // parse() may throw SyntaxError. const compilationUnit = parse(code) @@ -33,12 +58,36 @@ export const runECEvaluator = (code: string, targetStep: number = STEP_LIMIT): P } } -export const createContext = (): Context => ({ +export const createContext = (code: string = '', ioCallbacks?: IOCallbacks): Context => ({ errors: [], control: new Control(), stash: new Stash(), environment: new Environment(), - totalSteps: STEP_LIMIT + interfaces: initialiseInterfaces(code, ioCallbacks), + + totalSteps: 0 }) + +const initialiseInterfaces = (code: string, ioCallbacks?: IOCallbacks): Interfaces => { + return { + stdout: ioCallbacks?.stdout ?? console.log, + stderr: ioCallbacks?.stderr ?? console.error, + statics: { + lfsr: new LFSR(code) + } + } +} + +export const makeObjectClass = (): Class => { + const context = createContext('') + const objectClassDefinition = 'class Object {}' + const libraryCompilationUnit = parse(objectClassDefinition) + context.control.push( + ...handleSequence(libraryCompilationUnit.topLevelClassOrInterfaceDeclarations) + ) + evaluate(context, STEP_LIMIT) + + return context.environment.getClass('Object') +} diff --git a/src/ec-evaluator/interpreter.ts b/src/ec-evaluator/interpreter.ts index a1119458..b260cd61 100644 --- a/src/ec-evaluator/interpreter.ts +++ b/src/ec-evaluator/interpreter.ts @@ -65,7 +65,9 @@ import { ResConOverloadInstr, ResOverrideInstr, ResTypeContInstr, - StructType + StructType, + NativeDeclaration, + Interfaces } from './types' import { defaultValues, @@ -87,7 +89,7 @@ import { searchMainMtdClass, prependExpConInvIfNeeded, isStatic, - isNative, + isNativeMethodDeclaration, resOverload, resOverride, resConOverload, @@ -95,13 +97,14 @@ import { makeNonLocalVarNonParamSimpleNameQualified, getFullyQualifiedDescriptor } from './utils' -import { natives } from './natives' +import { foreigns } from './natives' type CmdEvaluator = ( command: ControlItem, environment: Environment, control: Control, - stash: Stash + stash: Stash, + interfaces: Interfaces ) => void /** @@ -115,6 +118,7 @@ export const evaluate = ( const environment = context.environment const control = context.control const stash = context.stash + const interfaces = context.interfaces context.totalSteps = 0 @@ -127,9 +131,9 @@ export const evaluate = ( control.pop() if (isNode(command)) { - cmdEvaluators[command.kind](command, environment, control, stash) + cmdEvaluators[command.kind](command, environment, control, stash, interfaces) } else { - cmdEvaluators[command.instrType](command, environment, control, stash) + cmdEvaluators[command.instrType](command, environment, control, stash, interfaces) } command = control.peek() @@ -144,7 +148,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: CompilationUnit, _environment: Environment, control: Control, - _stash: Stash + _stash: Stash, + _interfaces: Interfaces ) => { // Get first class that defines the main method according to program order. const className = searchMainMtdClass(command.topLevelClassOrInterfaceDeclarations) @@ -154,14 +159,14 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { control.push(node.mainMtdInvExpStmtNode(className)) control.push(...handleSequence(command.topLevelClassOrInterfaceDeclarations)) - control.push(node.objClassDeclNode()) }, NormalClassDeclaration: ( command: NormalClassDeclaration, environment: Environment, control: Control, - _stash: Stash + _stash: Stash, + _interfaces: Interfaces ) => { const className = command.typeIdentifier @@ -227,7 +232,13 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { control.push(...handleSequence(staticFields)) }, - Block: (command: Block, environment: Environment, control: Control, _stash: Stash) => { + Block: ( + command: Block, + environment: Environment, + control: Control, + _stash: Stash, + _interfaces: Interfaces + ) => { // Save current environment before extending. control.push(instr.envInstr(environment.current, command)) control.push(...handleSequence(command.blockStatements)) @@ -239,7 +250,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: ConstructorDeclaration, environment: Environment, _control: Control, - _stash: Stash + _stash: Stash, + _interfaces: Interfaces ) => { // Use constructor descriptor as key. const conDescriptor: string = getDescriptor(command) @@ -251,11 +263,34 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: MethodDeclaration, environment: Environment, _control: Control, - _stash: Stash + _stash: Stash, + _interfaces: Interfaces ) => { + let mtdClosure + // Native method handling + if (isNativeMethodDeclaration(command)) { + const FQMtdDesc = getFullyQualifiedDescriptor(environment.current.name, command) + const fn = foreigns[FQMtdDesc] + + if (fn === undefined) { + throw new errors.UndefinedNativeMethod(FQMtdDesc) + } + + // bind foreign function + const natDecl: NativeDeclaration = { + kind: 'NativeDeclaration', + methodModifier: command.methodModifier, + methodHeader: command.methodHeader, + foreignFunction: fn + } + + mtdClosure = struct.closureStruct(natDecl, environment.current) + } else { + mtdClosure = struct.closureStruct(command, environment.current) + } + // Use method descriptor as key. const mtdDescriptor: string = getDescriptor(command) - const mtdClosure = struct.closureStruct(command, environment.current) environment.defineMtdOrCon(mtdDescriptor, mtdClosure) }, @@ -263,7 +298,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: FieldDeclaration, environment: Environment, control: Control, - _stash: Stash + _stash: Stash, + _interfaces: Interfaces ) => { // FieldDeclaration to be evaluated are always static fields. // Instance fields are transformed into Assignment ExpressionStatement inserted into constructors. @@ -288,7 +324,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: LocalVariableDeclarationStatement, environment: Environment, control: Control, - _stash: Stash + _stash: Stash, + _interfaces: Interfaces ) => { const type: LocalVariableType = command.localVariableType const declaration: VariableDeclarator = command.variableDeclaratorList[0] @@ -311,7 +348,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: ExpressionStatement, _environment: Environment, control: Control, - _stash: Stash + _stash: Stash, + _interfaces: Interfaces ) => { control.push(instr.popInstr(command)) control.push(command.stmtExp) @@ -321,7 +359,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: ReturnStatement, _environment: Environment, control: Control, - _stash: Stash + _stash: Stash, + _interfaces: Interfaces ) => { control.push(instr.resetInstr(command)) control.push(command.exp) @@ -337,7 +376,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: MethodInvocation, _environment: Environment, control: Control, - _stash: Stash + _stash: Stash, + _interfaces: Interfaces ) => { const nameParts = command.identifier.split('.') const target = nameParts.splice(0, nameParts.length - 1).join('.') @@ -358,7 +398,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: ClassInstanceCreationExpression, environment: Environment, control: Control, - _stash: Stash + _stash: Stash, + _interfaces: Interfaces ) => { const c: Class = environment.getClass(command.identifier) @@ -374,7 +415,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: ExplicitConstructorInvocation, _environment: Environment, control: Control, - _stash: Stash + _stash: Stash, + _interfaces: Interfaces ) => { control.push(instr.popInstr(command)) control.push(instr.invInstr(command.argumentList.length + 1, command)) @@ -385,11 +427,23 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { control.push(instr.resTypeInstr(node.exprNameNode(command.thisOrSuper, command), command)) }, - Literal: (command: Literal, _environment: Environment, _control: Control, stash: Stash) => { + Literal: ( + command: Literal, + _environment: Environment, + _control: Control, + stash: Stash, + _interfaces: Interfaces + ) => { stash.push(command) }, - Void: (command: Void, _environment: Environment, _control: Control, stash: Stash) => { + Void: ( + command: Void, + _environment: Environment, + _control: Control, + stash: Stash, + _interfaces: Interfaces + ) => { stash.push(command) }, @@ -397,7 +451,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: ExpressionName, _environment: Environment, control: Control, - _stash: Stash + _stash: Stash, + _interfaces: Interfaces ) => { control.push(instr.derefInstr(command)) control.push(instr.evalVarInstr(command.name, command)) @@ -407,7 +462,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: BinaryExpression, _environment: Environment, control: Control, - _stash: Stash + _stash: Stash, + _interfaces: Interfaces ) => { control.push(instr.binOpInstr(command.operator, command)) control.push(command.right) @@ -418,7 +474,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { _command: Instr, _environment: Environment, _control: Control, - stash: Stash + stash: Stash, + _interfaces: Interfaces ) => { stash.pop() }, @@ -427,7 +484,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { _command: AssmtInstr, _environment: Environment, _control: Control, - stash: Stash + stash: Stash, + _interfaces: Interfaces ) => { // value is popped before variable becuase value is evaluated later than variable. const value = stash.pop()! as Literal | Object @@ -443,7 +501,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: BinOpInstr, _environment: Environment, _control: Control, - stash: Stash + stash: Stash, + _interfaces: Interfaces ) => { const right = stash.pop()! as Literal const left = stash.pop()! as Literal @@ -454,7 +513,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: InvInstr, environment: Environment, control: Control, - stash: Stash + stash: Stash, + interfaces: Interfaces ) => { // Save current environment to be restored after method returns. control.push(instr.envInstr(environment.current, command.srcNode)) @@ -471,13 +531,13 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { const closure: Closure = stash.pop()! as Closure const params: FormalParameter[] = - closure.mtdOrCon.kind === 'MethodDeclaration' + closure.mtdOrCon.kind === 'MethodDeclaration' || closure.mtdOrCon.kind === 'NativeDeclaration' ? cloneDeep(closure.mtdOrCon.methodHeader.formalParameterList) : cloneDeep(closure.mtdOrCon.constructorDeclarator.formalParameterList) // Extend env from global frame. - const mtdOrConDescriptor = getDescriptor(closure.mtdOrCon) - environment.extendEnv(environment.global, mtdOrConDescriptor) + const methodDescriptor = getDescriptor(closure.mtdOrCon) + environment.extendEnv(environment.global, methodDescriptor) const isInstanceMtdOrCon = args.length == params.length + 1 if (isInstanceMtdOrCon) { @@ -504,15 +564,8 @@ export 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) - } - + // Native functions + if (closure.mtdOrCon.kind === 'NativeDeclaration') { // only because resetInstr demands one, never actually used const superfluousReturnStatement: ReturnStatement = { kind: 'ReturnStatement', @@ -523,24 +576,24 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { // 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 + // call foreign fn with CSE machine state + closure.mtdOrCon.foreignFunction({ control, stash, environment, interfaces }) + } else { + // Push method/constructor body. + const body = + closure.mtdOrCon.kind === 'MethodDeclaration' + ? closure.mtdOrCon.methodBody + : closure.mtdOrCon.constructorBody + control.push(body) } - - // Push method/constructor body. - const body = - closure.mtdOrCon.kind === 'MethodDeclaration' - ? closure.mtdOrCon.methodBody - : closure.mtdOrCon.constructorBody - control.push(body) }, [InstrType.ENV]: ( command: EnvInstr, environment: Environment, _control: Control, - _stash: Stash + _stash: Stash, + _interfaces: Interfaces ) => { environment.restoreEnv(command.env) }, @@ -549,7 +602,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: ResetInstr, _environment: Environment, control: Control, - _stash: Stash + _stash: Stash, + _interfaces: Interfaces ) => { // Continue popping ControlItem until Marker is found. const next: ControlItem | undefined = control.pop() @@ -562,7 +616,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: EvalVarInstr, environment: Environment, control: Control, - stash: Stash + stash: Stash, + _interfaces: Interfaces ) => { if (isQualified(command.symbol)) { const nameParts = command.symbol.split('.') @@ -579,7 +634,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: ResInstr, environment: Environment, _control: Control, - stash: Stash + stash: Stash, + _interfaces: Interfaces ) => { const varOrClass = stash.pop()! as Variable | Class @@ -648,7 +704,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { _command: DerefInstr, _environment: Environment, _control: Control, - stash: Stash + stash: Stash, + _interfaces: Interfaces ) => { const varOrClass = stash.pop()! as Variable | Class const value = @@ -665,7 +722,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: NewInstr, environment: Environment, _control: Control, - stash: Stash + stash: Stash, + _interfaces: Interfaces ) => { // To be restore after extending curr env to declare instance fields in obj frame. const currEnv = environment.current @@ -715,7 +773,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: ResTypeInstr, environment: Environment, control: Control, - stash: Stash + stash: Stash, + _interfaces: Interfaces ) => { // TODO need to handle all type of arg expressions const value: Expression | Class = command.value @@ -753,7 +812,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: ResTypeContInstr, environment: Environment, _control: Control, - stash: Stash + stash: Stash, + _interfaces: Interfaces ) => { const typeToSearchIn = stash.pop()! as Type @@ -793,7 +853,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: ResOverloadInstr, environment: Environment, control: Control, - stash: Stash + stash: Stash, + _interfaces: Interfaces ) => { // Retrieve arg types in reversed order for method overloading resolution. const argTypes: Type[] = [] @@ -830,7 +891,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { _command: ResOverrideInstr, environment: Environment, _control: Control, - stash: Stash + stash: Stash, + _interfaces: Interfaces ) => { const target = stash.pop()! const overloadResolvedClosure = stash.pop()! as Closure @@ -862,7 +924,8 @@ export const cmdEvaluators: { [type: string]: CmdEvaluator } = { command: ResConOverloadInstr, environment: Environment, _control: Control, - stash: Stash + stash: Stash, + _interfaces: Interfaces ) => { // Retrieve arg types in reversed order for constructor resolution. const argTypes: Type[] = [] diff --git a/src/ec-evaluator/lib.ts b/src/ec-evaluator/lib.ts new file mode 100644 index 00000000..7ffb58ee --- /dev/null +++ b/src/ec-evaluator/lib.ts @@ -0,0 +1,69 @@ +/* + Java library classes + + Injects library classes for use by programs run in the CSEC machine. +*/ + +function fnv1a(s: string) { + const FNV_OFFSET_BASIS = 2166136261 + const FNV_PRIME = 16777619 + + let hash = FNV_OFFSET_BASIS + + for (let i = 0; i < s.length; i++) { + hash ^= s.charCodeAt(i) + hash = Math.imul(hash, FNV_PRIME) + } + + return hash >>> 0 +} + +// 31-bit Fibonacci LFSR. The state is kept non-negative, since a sign bit would be smeared in by +// the shift and the register would saturate to all ones. +export class LFSR { + #state: number + + constructor(init: string) { + this.#state = fnv1a(init) & 0x7fffffff || 1 // all-zero state is a fixed point + } + + next() { + const state = this.#state + const bit = (state ^ (state >>> 1) ^ (state >>> 2) ^ (state >>> 3)) & 1 + this.#state = (state >>> 1) | (bit << 30) + return this.#state + } +} + +export const libraryClasses = ` +class Object { + public native int hashCode(); + + public native String toString(); +} + +class PrintStream { + public native void println(String s); +} + +class System { + public static PrintStream out = new PrintStream(); +} +` + +// Binary names of the library classes, as Class.getName() reports them; user classes are in the +// default package, so their binary name is their simple name. +export const libraryBinaryNames: { [className: string]: string } = { + Object: 'java.lang.Object', + PrintStream: 'java.io.PrintStream', + System: 'java.lang.System' +} + +// const disabled =`class PrintStream extends Object { +// public native void println(String s); +// } + +// class System extends Object { +// public static PrintStream out = new PrintStream(); +// } +// ` diff --git a/src/ec-evaluator/natives.ts b/src/ec-evaluator/natives.ts index 9a188de5..5235d1cc 100644 --- a/src/ec-evaluator/natives.ts +++ b/src/ec-evaluator/natives.ts @@ -1,4 +1,8 @@ +import { Literal } from '../ast/types/blocks-and-statements' import { Control, Environment, Stash } from './components' +import { RuntimeError } from './errors' +import { libraryBinaryNames } from './lib' +import { Interfaces, StashItem, StructType, VarValue } from './types' /* Native function escape hatch. @@ -14,16 +18,116 @@ import { Control, Environment, Stash } from './components' The current implementation automatically injects a return instruction after the external handler function call ends. */ -export type NativeFunction = ({ +export type ForeignFunction = ({ control, stash, - environment + environment, + interfaces }: { control: Control stash: Stash environment: Environment + interfaces: Interfaces }) => void -export const natives: { - [descriptor: string]: NativeFunction -} = {} +export const foreigns: { + [descriptor: string]: ForeignFunction +} = { + 'Object::hashCode(): int': ({ stash, environment, interfaces }) => { + const instance = environment.getVariable('this').value + + const hashCode = getHashCode(instance, interfaces) + + const stashItem: StashItem = { + kind: 'Literal', + literalType: { kind: 'DecimalIntegerLiteral', value: String(hashCode) } + } + + stash.push(stashItem) + }, + + 'PrintStream::println(String s): void': ({ environment, interfaces }) => { + // @ts-expect-error ts(2339): guaranteed valid by type checker + const literal: Literal['literalType'] = environment.getVariable('s').value.literalType + + interfaces.stdout(literal.kind === 'StringLiteral' ? unquote(literal.value) : literal.value) + }, + + 'Object::toString(): String': ({ stash, environment, interfaces }) => { + const instance = environment.getVariable('this').value + + if (instance.kind !== StructType.OBJECT) { + throw new RuntimeError('Call to toString on non-Object') + } + + // as getClass().getName() + const simpleName = instance.class.classDecl.typeIdentifier + const className = libraryBinaryNames[simpleName] ?? simpleName + // unsigned, as Integer.toHexString + const hashCodeToHex = (getHashCode(instance, interfaces) >>> 0).toString(16) + + // string literal values keep their source quotes + const stashItem: StashItem = { + kind: 'Literal', + literalType: { + kind: 'StringLiteral', + value: `"${className}@${hashCodeToHex}"` + } + } + + stash.push(stashItem) + return + } +} + +// Utility functions used by native methods +const ESCAPES: { [c: string]: string } = { + b: '\b', + t: '\t', + n: '\n', + f: '\f', + r: '\r', + s: ' ', + '"': '"', + "'": "'", + '\\': '\\' +} + +// Octal escapes are \0 to \377, matched longest first. A backslash ending a line (text blocks +// only) joins it to the next. +const decodeEscapes = (content: string): string => + content.replace( + /\\(?:([0-3][0-7]{2}|[0-7][0-7]?)|([btnfrs"'\\])|\n)/g, + (_, octal: string, c: string) => + octal ? String.fromCharCode(parseInt(octal, 8)) : c ? ESCAPES[c] : '' + ) + +// Text block content, as JLS 3.10.6: drop the line break after the opening delimiter, normalise +// line breaks, strip the indentation common to the lines and trailing white space, then decode +// escapes. The closing delimiter's line counts towards the indentation even when blank. +const decodeTextBlock = (content: string): string => { + const lines = content.replace(/\r\n?/g, '\n').split('\n').slice(1) + const indent = Math.min( + ...lines + .filter((line, i) => line.trim() !== '' || i === lines.length - 1) + .map(line => line.length - line.trimStart().length) + ) + return decodeEscapes(lines.map(line => line.slice(indent).trimEnd()).join('\n')) +} + +// String literal or text block source token -> its value. +const unquote = (token: string): string => + token.startsWith('"""') ? decodeTextBlock(token.slice(3, -3)) : decodeEscapes(token.slice(1, -1)) + +const getHashCode = (obj: VarValue, interfaces: Interfaces): number => { + if (obj.kind !== StructType.OBJECT) { + // TODO: throw error here + throw new RuntimeError('Attempt to retrieve hashCode from non-Object') + } + + if (obj.hashCode === undefined) { + obj.hashCode = interfaces.statics.lfsr.next() + } + + return obj.hashCode +} diff --git a/src/ec-evaluator/structCreator.ts b/src/ec-evaluator/structCreator.ts index 1943ea4c..1fdd5bd8 100644 --- a/src/ec-evaluator/structCreator.ts +++ b/src/ec-evaluator/structCreator.ts @@ -6,7 +6,18 @@ import { UnannType } from '../ast/types/classes' import { EnvNode } from './components' -import { Name, StructType, Type, VarValue, Variable, Symbol, Object, Class, Closure } from './types' +import { + Name, + StructType, + Type, + VarValue, + Variable, + Symbol, + Object, + Class, + Closure, + NativeDeclaration +} from './types' export const varStruct = (type: UnannType, name: Name, value: VarValue): Variable => ({ kind: StructType.VARIABLE, @@ -27,7 +38,7 @@ export const objStruct = (frame: EnvNode, c: Class): Object => ({ }) export const closureStruct = ( - mtdOrCon: MethodDeclaration | ConstructorDeclaration, + mtdOrCon: MethodDeclaration | ConstructorDeclaration | NativeDeclaration, env: EnvNode ): Closure => ({ kind: StructType.CLOSURE, diff --git a/src/ec-evaluator/types.ts b/src/ec-evaluator/types.ts index 9ce1b6dc..0dbd7b5d 100644 --- a/src/ec-evaluator/types.ts +++ b/src/ec-evaluator/types.ts @@ -4,11 +4,15 @@ import { ConstructorDeclaration, FieldDeclaration, MethodDeclaration, + MethodHeader, + MethodModifier, NormalClassDeclaration, UnannType } from '../ast/types/classes' import { Control, EnvNode, Environment, Stash } from './components' import { SourceError } from './errors' +import { LFSR } from './lib' +import { ForeignFunction } from './natives' export interface Context { errors: SourceError[] @@ -17,9 +21,24 @@ export interface Context { stash: Stash environment: Environment + interfaces: Interfaces + totalSteps: number } +export interface Interfaces { + stdout: (displayString: string) => void + stderr: (errType: 'TypeCheck' | 'Compile' | 'Runtime', errMsg: string) => void + statics: { + lfsr: LFSR + } +} + +export interface IOCallbacks { + stdout?: (displayString: string) => void + stderr?: (errType: 'TypeCheck' | 'Compile' | 'Runtime', errMsg: string) => void +} + /** * Instructions */ @@ -159,11 +178,19 @@ export interface Object { kind: StructType.OBJECT frame: EnvNode class: Class + hashCode?: number +} + +export interface NativeDeclaration { + kind: 'NativeDeclaration' + methodModifier: Array + methodHeader: MethodHeader + foreignFunction: ForeignFunction } export interface Closure { kind: StructType.CLOSURE - mtdOrCon: MethodDeclaration | ConstructorDeclaration + mtdOrCon: MethodDeclaration | ConstructorDeclaration | NativeDeclaration env: EnvNode } @@ -173,9 +200,9 @@ export interface Class { classDecl: NormalClassDeclaration constructors: ConstructorDeclaration[] instanceFields: FieldDeclaration[] - instanceMethods: MethodDeclaration[] + instanceMethods: (MethodDeclaration | NativeDeclaration)[] staticFields: FieldDeclaration[] - staticMethods: MethodDeclaration[] + staticMethods: (MethodDeclaration | NativeDeclaration)[] superclass?: Class } diff --git a/src/ec-evaluator/utils.ts b/src/ec-evaluator/utils.ts index a11e7b16..636e3fdd 100644 --- a/src/ec-evaluator/utils.ts +++ b/src/ec-evaluator/utils.ts @@ -27,7 +27,16 @@ import { nullLitNode, returnThisStmtNode } from './nodeCreator' -import { ControlItem, Context, Instr, Class, Type, Closure, StashItem } from './types' +import { + ControlItem, + Context, + Instr, + Class, + Type, + Closure, + StashItem, + NativeDeclaration +} from './types' /** * Components. @@ -178,17 +187,14 @@ export const defaultValues = new Map([ /** * Name */ -export const getDescriptor = (mtdOrCon: MethodDeclaration | ConstructorDeclaration): string => { - return mtdOrCon.kind === 'MethodDeclaration' - ? `${mtdOrCon.methodHeader.identifier}(${mtdOrCon.methodHeader.formalParameterList.map(p => p.unannType).join(',')})${mtdOrCon.methodHeader.result}` +export const getDescriptor = ( + mtdOrCon: MethodDeclaration | ConstructorDeclaration | NativeDeclaration +): string => { + return mtdOrCon.kind === 'MethodDeclaration' || mtdOrCon.kind === 'NativeDeclaration' + ? `${mtdOrCon.methodHeader.identifier}(${mtdOrCon.methodHeader.formalParameterList.map(p => p.unannType).join(',')}): ${mtdOrCon.methodHeader.result}` : `${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('.') } @@ -236,7 +242,7 @@ export const isInstance = (fieldOrMtd: FieldDeclaration | MethodDeclaration): bo return !isStatic(fieldOrMtd) } -export const isNative = (mtd: MethodDeclaration): boolean => { +export const isNativeMethodDeclaration = (mtd: MethodDeclaration): boolean => { return mtd.methodModifier.includes('native') } @@ -434,8 +440,8 @@ export const resOverload = ( for (const [closureName, closure] of c.frame.frame.entries()) { // Methods contains parantheses and must have return type. if (closureName.includes(mtdName + '(') && closureName[closureName.length - 1] !== ')') { - const params = ((closure as Closure).mtdOrCon as MethodDeclaration).methodHeader - .formalParameterList + const params = ((closure as Closure).mtdOrCon as MethodDeclaration | NativeDeclaration) + .methodHeader.formalParameterList if (argTypes.length != params.length) continue @@ -470,9 +476,11 @@ export const resOverload = ( for (let i = 0; i < argTypes.length; i++) { let mostSpecClosureByParam = appClosures[0] for (const appClosure of appClosures) { - const mostSpecParams = (mostSpecClosureByParam.mtdOrCon as MethodDeclaration).methodHeader + const mostSpecParams = ( + mostSpecClosureByParam.mtdOrCon as MethodDeclaration | NativeDeclaration + ).methodHeader.formalParameterList + const params = (appClosure.mtdOrCon as MethodDeclaration | NativeDeclaration).methodHeader .formalParameterList - const params = (appClosure.mtdOrCon as MethodDeclaration).methodHeader.formalParameterList if (isSubtype(params[i].unannType, mostSpecParams[i].unannType, classStore)) { mostSpecClosureByParam = appClosure } @@ -488,21 +496,31 @@ export const resOverload = ( } export const resOverride = (classToSearchIn: Class, overloadResolvedClosure: Closure): Closure => { - const overloadResolvedMtd = overloadResolvedClosure.mtdOrCon as MethodDeclaration + const overloadResolvedMtd = overloadResolvedClosure.mtdOrCon as + | MethodDeclaration + | NativeDeclaration const name = overloadResolvedMtd.methodHeader.identifier const overloadResolvedClosureParams = overloadResolvedMtd.methodHeader.formalParameterList + // Private methods are not overridden. + if (overloadResolvedMtd.methodModifier.includes('private')) return overloadResolvedClosure + + // The runtime class first, then up its superclasses, so the nearest override wins: a class frame + // holds only the methods its class declares, not inherited ones. const closures: Closure[] = [] - for (const [closureName, closure] of classToSearchIn.frame.frame.entries()) { - // Methods contains parantheses and must have return type. - if (closureName.includes(name + '(') && closureName[closureName.length - 1] !== ')') { - closures.push(closure as Closure) + for (let c: Class | undefined = classToSearchIn; c; c = c.superclass) { + for (const [closureName, closure] of c.frame.frame.entries()) { + // Methods contains parantheses and must have return type. Descriptors start with the name. + if (closureName.startsWith(name + '(') && closureName[closureName.length - 1] !== ')') { + closures.push(closure as Closure) + } } } let overrideResolvedClosure = overloadResolvedClosure for (const closure of closures) { - const params = (closure.mtdOrCon as MethodDeclaration).methodHeader.formalParameterList + const params = (closure.mtdOrCon as MethodDeclaration | NativeDeclaration).methodHeader + .formalParameterList if (overloadResolvedClosureParams.length != params.length) continue @@ -563,3 +581,7 @@ export const resConOverload = ( export const isNull = (stashItem: StashItem) => { return stashItem.kind === 'Literal' && stashItem.literalType.kind === 'NullLiteral' } + +// because native functions must retrieve variables from the environment by identifier, this descriptor type also includes parameter names for convenience +export const getFullyQualifiedDescriptor = (className: string, decl: MethodDeclaration) => + `${className}::${decl.methodHeader.identifier}(${decl.methodHeader.formalParameterList.map(p => `${p.unannType} ${p.identifier}`).join(',')}): ${decl.methodHeader.result}` diff --git a/src/types/checker/__tests__/classes.test.ts b/src/types/checker/__tests__/classes.test.ts index 6a0a6a4e..7d2d4963 100644 --- a/src/types/checker/__tests__/classes.test.ts +++ b/src/types/checker/__tests__/classes.test.ts @@ -1,6 +1,6 @@ import { check } from '..' import { parse } from '../../ast' -import { CyclicInheritanceError, TypeCheckerError } from '../../errors' +import { CannotFindSymbolError, CyclicInheritanceError, TypeCheckerError } from '../../errors' import { Type } from '../../types/type' const testcases: { @@ -96,6 +96,26 @@ const testcases: { `, result: { type: null, errors: [new CyclicInheritanceError()] } }, + { + input: ` + class Object extends Missing {} + + public class Main { + public static void main(String[] args) {} + } + `, + result: { type: null, errors: [new CannotFindSymbolError()] } + }, + { + input: ` + class Object extends Object {} + + public class Main { + public static void main(String[] args) {} + } + `, + result: { type: null, errors: [new CyclicInheritanceError()] } + }, { input: ` class A extends B {} diff --git a/src/types/checker/index.ts b/src/types/checker/index.ts index 396c0446..d1548908 100644 --- a/src/types/checker/index.ts +++ b/src/types/checker/index.ts @@ -56,8 +56,8 @@ export const newResult = ( export const OK_RESULT: Result = newResult(null) -export const check = (node: Node, frame: Frame = Frame.globalFrame()): Result => { - const typeCheckingFrame = frame.newChildFrame() +export const check = (node: Node, frame: Frame = Frame.globalFrame().newChildFrame()): Result => { + const typeCheckingFrame = frame const addClassesResult = addClasses(node, typeCheckingFrame) if (addClassesResult.hasErrors) return addClassesResult const addClassParentsResult = addClassParents(node, typeCheckingFrame) diff --git a/src/types/checker/prechecks.ts b/src/types/checker/prechecks.ts index c0a08ac6..49424cc0 100644 --- a/src/types/checker/prechecks.ts +++ b/src/types/checker/prechecks.ts @@ -200,6 +200,22 @@ export const addClassParents = (node: Node, frame: Frame): Result => { const classType = frame.getType(node.typeIdentifier.identifier, node.typeIdentifier.location) if (classType instanceof Error) return newResult(null, [classType]) if (!(classType instanceof ClassType)) throw new Error('class type should be a ClassImpl') + if (node.typeIdentifier.identifier === 'Object') { + // The declared Object is the root and gets no parent, but its extends clause must still + // name a type. Linking it to a parent could leave a cycle the check below cannot see. + if (node.classExtends) { + const extendsType = frame.getType( + node.classExtends.classType.typeIdentifier.identifier, + node.classExtends.classType.typeIdentifier.location + ) + if (extendsType instanceof Error) return newResult(null, [extendsType]) + if (extendsType === classType) + return newResult(null, [ + new CyclicInheritanceError(node.classExtends.classType.location) + ]) + } + return newResult(null) + } if (node.classExtends) { const extendsType = frame.getType( node.classExtends.classType.typeIdentifier.identifier, @@ -217,7 +233,14 @@ export const addClassParents = (node: Node, frame: Frame): Result => { type = type.getParentClass() } classType.setParentClass(extendsType) + } else { + const objectType = frame.getType('Object', { startOffset: -1, startLine: -1 }) + // if Object is declared explicitly, use that + if (!(objectType instanceof TypeCheckerError)) { + classType.setParentClass(objectType as ObjectClass) + } } + return newResult(classType) } case 'EnumDeclaration': { diff --git a/src/types/index.ts b/src/types/index.ts index c99b1466..420f438f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,7 @@ import { check } from './checker' import { parse } from './ast' import { TypeCheckerError } from './errors' +import { Frame } from './checker/environment' type TypeCheckResult = { hasTypeErrors: boolean; errorMsgs: string[] } @@ -11,16 +12,28 @@ const convertErrorsToReadableMsgs = (program: string, errors: Error[]): string[] }) } -export const typeCheck = (program: string): TypeCheckResult => { - const ast = parse(program) - if (ast instanceof TypeCheckerError) - return { - hasTypeErrors: true, - errorMsgs: convertErrorsToReadableMsgs(program, [ast]) - } - const result = check(ast) +export const typeCheck = (...programs: string[]): TypeCheckResult => { + const frame: Frame = Frame.globalFrame().newChildFrame() + + // fail fast; returns early + for (const p of programs) { + const ast = parse(p) + if (ast instanceof TypeCheckerError) + return { + hasTypeErrors: true, + errorMsgs: convertErrorsToReadableMsgs(p, [ast]) + } + const result = check(ast, frame) + if (result.errors.length > 0) + return { + hasTypeErrors: true, + errorMsgs: convertErrorsToReadableMsgs(p, result.errors) + } + } + + // no errors return { - hasTypeErrors: result.errors.length > 0, - errorMsgs: convertErrorsToReadableMsgs(program, result.errors) + hasTypeErrors: false, + errorMsgs: [] } }