From 83593b9039f2d122ed9c6f3a15b90f085462e676 Mon Sep 17 00:00:00 2001 From: David Souther Date: Sun, 12 Nov 2023 15:19:22 -0500 Subject: [PATCH] VMTest passes projects 7 and 8 files --- simulator/src/languages/grammars/tst.ohm | 5 +- simulator/src/languages/grammars/tst.ohm.js | 5 +- simulator/src/languages/tst.test.ts | 26 +++ simulator/src/languages/tst.ts | 6 +- simulator/src/test/builder.ts | 7 + simulator/src/test/instruction.ts | 12 ++ simulator/src/test/tst.ts | 3 +- simulator/src/test/vmtst.test.ts | 37 +++++ simulator/src/test/vmtst.ts | 68 +++++++- simulator/src/vm/memory.ts | 77 ++++----- simulator/src/vm/vm.test.ts | 2 - simulator/src/vm/vm.ts | 166 +++++++++++++------- web/src/shell/editor.mock.tsx | 1 - 13 files changed, 296 insertions(+), 119 deletions(-) diff --git a/simulator/src/languages/grammars/tst.ohm b/simulator/src/languages/grammars/tst.ohm index b8407bf37..9645012c3 100644 --- a/simulator/src/languages/grammars/tst.ohm +++ b/simulator/src/languages/grammars/tst.ohm @@ -17,13 +17,13 @@ Tst <: Base { | TstLoadROMOperation TstLoadROMOperation = ROM32K Load FileName - TstFileOperation = FileOperation FileName + TstFileOperation = FileOperation FileName? TstOutputListOperation = "output-list" OutputFormat+ OutputFormat = Name Index? percent FormatStyle wholeDec dot wholeDec dot wholeDec TstSetOperation = Set Name Index? Number Index = OpenSquare wholeDec? CloseSquare Condition = Value CompareOp Value - TstEvalOperation = Eval | Tick | Tock | TickTock + TstEvalOperation = Eval | Tick | Tock | TickTock | VmStep TstOutputOperation = Output TstEchoOperation = Echo String TstClearEchoOperation = ClearEcho @@ -36,6 +36,7 @@ Tst <: Base { Tick = "tick" Tock = "tock" TickTock = "ticktock" + VmStep = "vmstep" Echo = "echo" Repeat = "repeat" ClearEcho = "clear-echo" diff --git a/simulator/src/languages/grammars/tst.ohm.js b/simulator/src/languages/grammars/tst.ohm.js index bbb19cb99..befa5a7ca 100644 --- a/simulator/src/languages/grammars/tst.ohm.js +++ b/simulator/src/languages/grammars/tst.ohm.js @@ -18,13 +18,13 @@ Tst <: Base { | TstLoadROMOperation TstLoadROMOperation = ROM32K Load FileName - TstFileOperation = FileOperation FileName + TstFileOperation = FileOperation FileName? TstOutputListOperation = "output-list" OutputFormat+ OutputFormat = Name Index? percent FormatStyle wholeDec dot wholeDec dot wholeDec TstSetOperation = Set Name Index? Number Index = OpenSquare wholeDec? CloseSquare Condition = Value CompareOp Value - TstEvalOperation = Eval | Tick | Tock | TickTock + TstEvalOperation = Eval | Tick | Tock | TickTock | VmStep TstOutputOperation = Output TstEchoOperation = Echo String TstClearEchoOperation = ClearEcho @@ -37,6 +37,7 @@ Tst <: Base { Tick = "tick" Tock = "tock" TickTock = "ticktock" + VmStep = "vmstep" Echo = "echo" Repeat = "repeat" ClearEcho = "clear-echo" diff --git a/simulator/src/languages/tst.test.ts b/simulator/src/languages/tst.test.ts index 33a0671d3..96a3d9274 100644 --- a/simulator/src/languages/tst.test.ts +++ b/simulator/src/languages/tst.test.ts @@ -1,3 +1,8 @@ +import { + FileSystem, + ObjectFileSystemAdapter, +} from "@davidsouther/jiffies/lib/esm/fs.js"; +import { resetFiles } from "@nand2tetris/projects/index.js"; import { grammar, TST } from "./tst.js"; const NOT_TST = ` @@ -469,3 +474,24 @@ describe("tst language", () => { }); }); }); + +it("loads all project tst files", async () => { + const fs = new FileSystem(new ObjectFileSystemAdapter()); + await resetFiles(fs); + async function check() { + for (const stat of await fs.scandir(".")) { + if (stat.isDirectory()) { + fs.pushd(stat.name); + await check(); + fs.popd(); + } else { + if (stat.name.endsWith("vm_tst")) { + const tst = await fs.readFile(stat.name); + const match = grammar.match(tst); + expect(match).toHaveSucceeded(); + } + } + } + } + await check(); +}); diff --git a/simulator/src/languages/tst.ts b/simulator/src/languages/tst.ts index b3e9aea8c..2f19ce4b9 100644 --- a/simulator/src/languages/tst.ts +++ b/simulator/src/languages/tst.ts @@ -20,7 +20,7 @@ export interface TstSetOperation { } export interface TstEvalOperation { - op: "eval" | "tick" | "tock"; + op: "eval" | "tick" | "tock" | "ticktock" | "vmstep"; } export interface TstOutputOperation { @@ -49,7 +49,7 @@ export interface TstLoadROMOperation { export interface TstFileOperation { op: "load" | "output-file" | "compare-to"; - file: string; + file?: string; } export type TstOperation = @@ -183,7 +183,7 @@ tstSemantics.addAttribute("operation", { TstFileOperation(op, file) { return { op: op.sourceString as TstFileOperation["op"], - file: file.sourceString, + file: file?.sourceString, }; }, }); diff --git a/simulator/src/test/builder.ts b/simulator/src/test/builder.ts index b663fa5c1..677ac81b8 100644 --- a/simulator/src/test/builder.ts +++ b/simulator/src/test/builder.ts @@ -16,6 +16,7 @@ import { TestClearEchoInstruction, TestCompoundInstruction, TestEchoInstruction, + TestLoadInstruction, TestLoadROMInstruction, TestOutputInstruction, TestOutputListInstruction, @@ -24,6 +25,8 @@ import { TestWhileInstruction, } from "./instruction.js"; import { Test } from "./tst.js"; +import { TestVMStepInstruction } from "./vmtst.js"; +import { TestTickTockInstruction } from "./cputst.js"; function isTstLineStatment(line: TstStatement): line is TstLineStatement { return (line as TstLineStatement).ops !== undefined; @@ -50,8 +53,12 @@ function makeInstruction(inst: TstOperation) { return new TestTickInstruction(); case "tock": return new TestTockInstruction(); + case "ticktock": + return new TestTickTockInstruction(); case "eval": return new TestEvalInstruction(); + case "vmstep": + return new TestVMStepInstruction(); case "output": return new TestOutputInstruction(); case "set": diff --git a/simulator/src/test/instruction.ts b/simulator/src/test/instruction.ts index 812d8adbb..75936aa51 100644 --- a/simulator/src/test/instruction.ts +++ b/simulator/src/test/instruction.ts @@ -204,6 +204,18 @@ export class TestLoadROMInstruction implements TestInstruction { } } +export class TestLoadInstruction implements TestInstruction { + constructor(readonly file?: string) {} + + async do(test: Test) { + await test.load(this.file); + } + + *steps() { + yield this; + } +} + export class TestBreakpointInstruction implements TestInstruction { constructor(readonly variable: string, readonly value: number) {} diff --git a/simulator/src/test/tst.ts b/simulator/src/test/tst.ts index bec1a626e..aceb2a7f5 100644 --- a/simulator/src/test/tst.ts +++ b/simulator/src/test/tst.ts @@ -21,7 +21,7 @@ export abstract class Test { return undefined; } - async load(_filename: string): Promise { + async load(_filename?: string): Promise { return undefined; } async compareTo(_filename: string): Promise { @@ -45,7 +45,6 @@ export abstract class Test { } })(this); this._step = this._steps.next(); - this._step; //? this._log = ""; return this; } diff --git a/simulator/src/test/vmtst.test.ts b/simulator/src/test/vmtst.test.ts index e69de29bb..b0ce6c5ef 100644 --- a/simulator/src/test/vmtst.test.ts +++ b/simulator/src/test/vmtst.test.ts @@ -0,0 +1,37 @@ +import { VM_PROJECTS, resetFiles } from "@nand2tetris/projects/index.js"; +import { + FileSystem, + ObjectFileSystemAdapter, +} from "@davidsouther/jiffies/lib/esm/fs.js"; +import { TST } from "../languages/tst.js"; +import { unwrap } from "@davidsouther/jiffies/lib/esm/result.js"; +import { VMTest } from "./vmtst.js"; + +async function prepare(project: "07" | "08", name: string): Promise { + const fs = new FileSystem(new ObjectFileSystemAdapter({})); + await resetFiles(fs); + fs.cd(`/projects/${project}/${name}`); + const vm_tst = await fs.readFile(name + ".vm_tst"); + const tst = unwrap(TST.parse(vm_tst)); + const test = VMTest.from(tst).using(fs); + await test.load(); + return test; +} + +describe("VM Test Runner", () => { + test.each(VM_PROJECTS["07"])("07 VM Test Runner %s", async (name) => { + const test = await prepare("07", name); + + for (let i = 0; i < 100; i++) { + await test.step(); + } + }); + + test.each(VM_PROJECTS["08"])("08 VM Test Runner %s", async (name) => { + const test = await prepare("08", name); + + for (let i = 0; i < 100; i++) { + test.step(); + } + }); +}); diff --git a/simulator/src/test/vmtst.ts b/simulator/src/test/vmtst.ts index 318c1a7c6..0c34f4ef9 100644 --- a/simulator/src/test/vmtst.ts +++ b/simulator/src/test/vmtst.ts @@ -1,11 +1,26 @@ +import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js"; +import { unwrap } from "@davidsouther/jiffies/lib/esm/result.js"; import { RAM } from "../cpu/memory.js"; -import { Vm } from "../vm/vm.js"; +import { Tst } from "../languages/tst.js"; +import { VM } from "../languages/vm.js"; +import { Segment, Vm } from "../vm/vm.js"; +import { fill } from "./builder.js"; import { TestInstruction } from "./instruction.js"; import { Test } from "./tst.js"; export class VMTest extends Test { vm: Vm = new Vm(); + static from(tst: Tst): VMTest { + const test = new VMTest(); + return fill(test, tst); + } + + using(fs: FileSystem): this { + this.fs = fs; + return this; + } + with(vm: Vm) { this.vm = vm; return this; @@ -24,7 +39,16 @@ export class VMTest extends Test { ) { return true; } - return false; + return [ + "argument", + "local", + "static", + "constant", + "this", + "that", + "pointer", + "temp", + ].includes(variable.toLowerCase()); } getVar(variable: string | number, index?: number): number { @@ -40,7 +64,7 @@ export class VMTest extends Test { ) { return this.vm.RAM.get(index); } - return 0; + return this.vm.memory.getSegment(variable as Segment, index ?? 0); } setVar(variable: string, value: number, index?: number): void { @@ -56,11 +80,49 @@ export class VMTest extends Test { ) { this.vm.RAM.set(index, value); } + if (index) { + this.vm.memory.setSegment(variable as Segment, index, value); + } else { + switch (variable.toLowerCase()) { + case "sp": + this.vm.memory.SP = value; + break; + case "arg": + case "argument": + this.vm.memory.ARG = value; + break; + case "lcl": + case "local": + this.vm.memory.LCL = value; + break; + case "this": + this.vm.memory.THIS = value; + break; + case "that": + this.vm.memory.THAT = value; + break; + } + } } vmstep(): void { this.vm.step(); } + + override async load(filename?: string) { + if (filename) { + const file = await this.fs.readFile(filename); + const { instructions } = unwrap(VM.parse(file)); + unwrap(this.vm.load(instructions)); + } else { + for (const file of await this.fs.scandir(".")) { + if (file.isFile() && file.name.endsWith(".vm")) { + await this.load(file.name); + } + } + } + unwrap(this.vm.bootstrap()); + } } export interface VMTestInstruction extends TestInstruction { diff --git a/simulator/src/vm/memory.ts b/simulator/src/vm/memory.ts index 8e3555bee..a1ae2239b 100644 --- a/simulator/src/vm/memory.ts +++ b/simulator/src/vm/memory.ts @@ -14,37 +14,32 @@ export class VmMemory extends RAM { get SP(): number { return this.get(SP); } + set SP(value: number) { + this.set(SP, value); + } get LCL(): number { return this.get(LCL); } + set LCL(value: number) { + this.set(LCL, value); + } get ARG(): number { return this.get(ARG); } + set ARG(value: number) { + this.set(ARG, value); + } get THIS(): number { return this.get(THIS); } + set THIS(value: number) { + this.set(THIS, value); + } get THAT(): number { return this.get(THAT); } - - get state() { - const temps = []; - for (let i = 5; i < 13; i++) { - temps.push(this.get(i)); - } - const internal = []; - for (let i = 13; i < 16; i++) { - internal.push(i); - } - return { - ["0: SP"]: this.SP, - ["1: LCL"]: this.LCL, - ["2: ARG"]: this.ARG, - ["3: THIS"]: this.THIS, - ["4: THAT"]: this.THAT, - TEMPS: temps, - VM: internal, - }; + set THAT(value: number) { + this.set(THAT, value); } get statics() { @@ -55,27 +50,6 @@ export class VmMemory extends RAM { return statics; } - get frame() { - // Arg0 Arg1... RET LCL ARG THIS THAT [SP] - const args = []; - for (let i = this.ARG; i < this.LCL - 5; i++) { - args.push(this.get(i)); - } - const locals = []; - for (let i = this.LCL; i < this.SP; i++) { - locals.push(this.get(i)); - } - const _this = []; - for (let i = 0; i < 5; i++) { - _this.push(this.this(i)); - } - return { - args, - locals, - this: _this, - }; - } - constructor() { super(); this.set(SP, 256); @@ -224,13 +198,14 @@ export class VmMemory extends RAM { const args = [...this.map((_, v) => v, arg, arg + argN)]; const locals = [...this.map((_, v) => v, lcl, lcl + localN)]; const stack = [...this.map((_, v) => v, stk, stk + stackN)]; - // [arg, argN, args]; //? - // [lcl, localN, locals]; //? - // [stk, stackN, stack]; //? + const this_ = [...this.map((_, v) => v, this.THIS, this.THIS + thisN)]; + const that = [...this.map((_, v) => v, this.THIS, this.THIS + thatN)]; return { args: { base: arg, count: argN, values: args }, locals: { base: lcl, count: localN, values: locals }, stack: { base: stk, count: stackN, values: stack }, + this: { base: stk, count: thisN, values: this_ }, + that: { base: stk, count: thatN, values: that }, frame: { RET: this.get(base), LCL: this.LCL, @@ -241,6 +216,22 @@ export class VmMemory extends RAM { }; } + getVmState(staticN = 240) { + const temps = [...this.map((_, v) => v, 5, 13)]; + const internal = [...this.map((_, v) => v, 13, 16)]; + const statics = [...this.map((_, v) => v, 16, 16 + staticN)]; + return { + ["0: SP"]: this.SP, + ["1: LCL"]: this.LCL, + ["2: ARG"]: this.ARG, + ["3: THIS"]: this.THIS, + ["4: THAT"]: this.THAT, + temps, + internal, + static: statics, + }; + } + binOp(fn: (a: number, b: number) => number) { const a = this.get(this.SP - 2); const b = this.get(this.SP - 1); diff --git a/simulator/src/vm/vm.test.ts b/simulator/src/vm/vm.test.ts index da3cdc54d..2a317aed0 100644 --- a/simulator/src/vm/vm.test.ts +++ b/simulator/src/vm/vm.test.ts @@ -307,8 +307,6 @@ test("08 / Simple Function / Simple Function", () => { vm.step(); } - vm.program; //? - const test = vm.read([0, 256]); expect(test).toEqual([257, 12]); }); diff --git a/simulator/src/vm/vm.ts b/simulator/src/vm/vm.ts index 78038f9b9..572bfa64c 100644 --- a/simulator/src/vm/vm.ts +++ b/simulator/src/vm/vm.ts @@ -75,6 +75,8 @@ export interface VmFrame { locals: VmFrameValues; args: VmFrameValues; stack: VmFrameValues; + this: VmFrameValues; + that: VmFrameValues; frame: { RET: number; ARG: number; @@ -122,6 +124,19 @@ const BOOTSTRAP: VmFunction = { ], }; +function BootstrapFor(name: string): VmFunction { + return { + name: "__bootstrap", + nVars: 0, + opBase: 0, + labels: {}, + operations: [ + { op: "function", name: "__bootstrap", nVars: 0 }, + { op: "call", name, nArgs: 0 }, + ], + }; +} + const END_LABEL = "__END"; const SYS_INIT: VmFunction = { name: "Sys.init", @@ -135,14 +150,11 @@ const SYS_INIT: VmFunction = { ], }; -const INITIAL_FNS = [BOOTSTRAP.name, IMPLICIT.name]; - export class Vm { - protected memory = new VmMemory(); - protected functionMap: Record = { - [BOOTSTRAP.name]: BOOTSTRAP, - }; - protected executionStack: VmFunctionInvocation[] = []; + memory = new VmMemory(); + entry = ""; + functionMap: Record = {}; + executionStack: VmFunctionInvocation[] = []; functions: VmFunction[] = []; program: VmOperation[] = []; @@ -177,55 +189,9 @@ export class Vm { static build(instructions: VmInstruction[]): Result { const vm = new Vm(); - - if (instructions[0]?.op !== "function") { - instructions.unshift({ op: "function", name: IMPLICIT.name, nVars: 0 }); - } - - let i = 0; - while (i < instructions.length) { - const buildFn = this.buildFunction(instructions, i); - - if (isErr(buildFn)) - return Err(new Error("Failed to build VM", { cause: Err(buildFn) })); - const [fn, i_] = unwrap(buildFn); - if (vm.functionMap[fn.name]) - throw new Error(`VM Already has a function named ${fn.name}`); - - vm.functionMap[fn.name] = fn; - i = i_; - } - - if (!vm.functionMap[SYS_INIT.name]) { - if (vm.functionMap["main"]) { - // Inject a Sys.init - vm.functionMap[SYS_INIT.name] = SYS_INIT; - } else if (vm.functionMap[IMPLICIT.name]) { - // Use __implicit instead of __bootstrap - delete vm.functionMap[BOOTSTRAP.name]; - } else { - return Err(Error("Could not determine an entry point for VM")); - } - } - - vm.registerStatics(); - - vm.functions = Object.values(vm.functionMap); - vm.functions.sort((a, b) => { - if (INITIAL_FNS.includes(a.name)) return -1; - if (INITIAL_FNS.includes(b.name)) return 1; - return a.name.localeCompare(b.name); - }); - - let offset = 0; - vm.program = vm.functions.reduce((prog, fn) => { - fn.opBase = offset; - offset += fn.operations.length; - return prog.concat(fn.operations); - }, [] as VmOperation[]); - - vm.reset(); - return Ok(vm); + const load = vm.load(instructions); + if (isErr(load)) return load; + return vm.bootstrap(); } private static buildFunction( @@ -348,7 +314,9 @@ export class Vm { get invocation() { const invocation = this.executionStack.at(-1); - if (invocation === undefined) throw new Error("Empty execution stack!"); + if (invocation === undefined) { + throw new Error("Empty execution stack!"); + } return invocation; } @@ -370,12 +338,86 @@ export class Vm { return this.currentFunction.operations[this.invocation.opPtr]; } + load(instructions: VmInstruction[]): Result { + if (instructions[0]?.op !== "function") { + instructions.unshift({ op: "function", name: IMPLICIT.name, nVars: 0 }); + } + + let i = 0; + while (i < instructions.length) { + const buildFn = Vm.buildFunction(instructions, i); + + if (isErr(buildFn)) + return Err(new Error("Failed to build VM", { cause: Err(buildFn) })); + const [fn, i_] = unwrap(buildFn); + if ( + this.functionMap[fn.name] && + this.memory.strict && + fn.name !== IMPLICIT.name + ) { + return Err(new Error(`VM Already has a function named ${fn.name}`)); + } + + this.functionMap[fn.name] = fn; + i = i_; + } + + this.registerStatics(); + + return Ok(this); + } + + bootstrap() { + if (!this.functionMap[SYS_INIT.name] && this.functionMap["main"]) { + this.functionMap[SYS_INIT.name] = SYS_INIT; + // TODO should this be an error from the compiler/OS? + } + + if (this.functionMap[SYS_INIT.name]) { + this.functionMap[BOOTSTRAP.name] = BootstrapFor(SYS_INIT.name); + this.entry = BOOTSTRAP.name; + } else if (this.functionMap[IMPLICIT.name]) { + this.entry = IMPLICIT.name; + } else { + const fnNames = Object.keys(this.functionMap); + if (fnNames.length === 1) { + this.functionMap[BOOTSTRAP.name] = BootstrapFor(fnNames[0]); + this.entry = BOOTSTRAP.name; + } + } + + if (this.functionMap[IMPLICIT.name] && this.functionMap[BOOTSTRAP.name]) { + return Err( + new Error("Cannot use both bootstrap and an implicit function") + ); + } + + if (this.entry === "") { + return Err(Error("Could not determine an entry point for VM")); + } + + this.functions = Object.values(this.functionMap); + this.functions.sort((a, b) => { + if (a.name === this.entry) return -1; + if (a.name === this.entry) return 1; + return 0; // Stable sort otherwise + }); + + let offset = 0; + this.program = this.functions.reduce((prog, fn) => { + fn.opBase = offset; + offset += fn.operations.length; + return prog.concat(fn.operations); + }, [] as VmOperation[]); + + this.reset(); + + return Ok(this); + } + reset() { - const bootstrap = this.functionMap[BOOTSTRAP.name] - ? BOOTSTRAP.name - : "__implicit"; this.executionStack = [ - { function: bootstrap, opPtr: 1, frameBase: 256, nArgs: 0 }, + { function: this.entry, opPtr: 1, frameBase: 256, nArgs: 0 }, ]; this.memory.reset(); this.memory.set(0, 256); @@ -520,6 +562,8 @@ export class Vm { count: frameEnd - frameBase, values: [...this.memory.map((_, v) => v, frameBase, frameEnd)], }, + ["this"]: { base: 0, count: 0, values: [] }, + that: { base: 0, count: 0, values: [] }, frame: { ARG: 0, LCL: 0, diff --git a/web/src/shell/editor.mock.tsx b/web/src/shell/editor.mock.tsx index f578f9a5b..33ec2b5eb 100644 --- a/web/src/shell/editor.mock.tsx +++ b/web/src/shell/editor.mock.tsx @@ -11,7 +11,6 @@ jest.mock("@monaco-editor/react", () => { > ); }); - console.log("mocked fake editor"); return FakeEditor; });