import { FileSystem, ObjectFileSystemAdapter, } from "@davidsouther/jiffies/lib/esm/fs.js"; import { resetFiles } from "@nand2tetris/projects/full.js"; import { grammar, TST } from "./tst.js"; const NOT_TST = ` output-list in%B3.1.3 out%B3.1.3; set in 0, eval, output; set in 1, eval, output;`; const BIT_TST = ` output-list time%S1.4.1 in%B2.1.2 load%B2.1.2 out%B2.1.2; set in 0, set load 0, tick, output; tock, output; set in 0, set load 1, eval, output; `; const MEM_TST = ` output-list time%S1.2.1 in%B2.1.2; set in -32123, tick, output; `; const MEM_REPEAT = ` repeat 14 { eval, output; } `; const INDEF_REPEAT = ` repeat { eval, output; } `; const COND_WHILE = `while out <> 89 { eval; }`; describe("tst language", () => { it("parses an output format", () => { const match = grammar.match("a%B3.1.3", "OutputFormat"); expect(match).toHaveSucceeded(); expect(TST.semantics(match).format).toStrictEqual({ id: "a", builtin: false, address: -1, format: { style: "B", width: 1, lpad: 3, rpad: 3, }, }); }); it("parses an output list", () => { const match = grammar.match( "output-list a%B1.1.1 out%X2.3.4", "TstOutputListOperation", ); expect(match).toHaveSucceeded(); expect(TST.semantics(match).operation).toStrictEqual({ op: "output-list", spec: [ { id: "a", builtin: false, address: -1, format: { style: "B", width: 1, lpad: 1, rpad: 1 }, }, { id: "out", builtin: false, address: -1, format: { style: "X", width: 3, lpad: 2, rpad: 4 }, }, ], }); }); it("parses an output list with junk", () => { const match = grammar.match( "\n/// A list\noutput-list a%B1.1.1 /* the output */ out%X2.3.4", "TstOutputListOperation", ); expect(match).toHaveSucceeded(); expect(TST.semantics(match).operation).toStrictEqual({ op: "output-list", spec: [ { id: "a", builtin: false, address: -1, format: { style: "B", width: 1, lpad: 1, rpad: 1 }, }, { id: "out", builtin: false, address: -1, format: { style: "X", width: 3, lpad: 2, rpad: 4 }, }, ], }); }); it("parses an output list with builtins", () => { const match = grammar.match( "output-list PC[]%D0.4.0 RAM16K[0]%D1.7.1", "TstOutputListOperation", ); expect(match).toHaveSucceeded(); expect(TST.semantics(match).operation).toStrictEqual({ op: "output-list", spec: [ { id: "PC", builtin: true, address: -1, format: { style: "D", width: 4, lpad: 0, rpad: 0 }, }, { id: "RAM16K", builtin: true, address: 0, format: { style: "D", width: 7, lpad: 1, rpad: 1 }, }, ], }); }); it("parses file ops", () => { const match = grammar.match( "load A.hdl, output-file A.out, compare-to A.cmp, output-list a%B1.1.1;", ); expect(match).toHaveSucceeded(); }); it("parses a single set", () => { const match = grammar.match("set a 0", "TstSetOperation"); expect(match).toHaveSucceeded(); expect(TST.semantics(match).operation).toEqual({ op: "set", id: "a", value: 0, }); }); it("parses simple multiline", () => { const match = grammar.match("eval;\n\neval;\n\n"); expect(match).toHaveSucceeded(); expect(TST.semantics(match).tst).toEqual({ lines: [ { op: { op: "eval" }, separator: ";", span: { start: 0, end: 5, line: 1 }, }, { op: { op: "eval" }, separator: ";", span: { start: 7, end: 12, line: 3 }, }, ], }); }); it("parses a test file", () => { const match = grammar.match(NOT_TST); expect(match).toHaveSucceeded(); expect(TST.semantics(match).tst).toEqual({ lines: [ { op: { op: "output-list", spec: [ { id: "in", builtin: false, address: -1, format: { style: "B", width: 1, lpad: 3, rpad: 3 }, }, { id: "out", builtin: false, address: -1, format: { style: "B", width: 1, lpad: 3, rpad: 3 }, }, ], }, separator: ";", span: { line: 2, start: 1, end: 34 }, }, { op: { op: "set", id: "in", value: 0 }, separator: ",", span: { line: 4, start: 36, end: 45 }, }, { op: { op: "eval" }, separator: ",", span: { line: 4, start: 46, end: 51 }, }, { op: { op: "output" }, separator: ";", span: { line: 4, start: 52, end: 59 }, }, { op: { op: "set", id: "in", value: 1 }, separator: ",", span: { line: 5, start: 60, end: 69 }, }, { op: { op: "eval" }, separator: ",", span: { line: 5, start: 70, end: 75 }, }, { op: { op: "output" }, separator: ";", span: { line: 5, start: 76, end: 83 }, }, ], }); }); it("parses a clocked test file", () => { const match = grammar.match(BIT_TST); expect(match).toHaveSucceeded(); expect(TST.semantics(match).tst).toEqual({ lines: [ { op: { op: "output-list", spec: [ { id: "time", builtin: false, address: -1, format: { style: "S", width: 4, lpad: 1, rpad: 1 }, }, { id: "in", builtin: false, address: -1, format: { style: "B", width: 1, lpad: 2, rpad: 2 }, }, { id: "load", builtin: false, address: -1, format: { style: "B", width: 1, lpad: 2, rpad: 2 }, }, { id: "out", builtin: false, address: -1, format: { style: "B", width: 1, lpad: 2, rpad: 2 }, }, ], }, separator: ";", span: { line: 2, start: 1, end: 58 }, }, { op: { op: "set", id: "in", value: 0 }, separator: ",", span: { line: 3, start: 59, end: 68 }, }, { op: { op: "set", id: "load", value: 0 }, separator: ",", span: { line: 3, start: 69, end: 80 }, }, { op: { op: "tick" }, separator: ",", span: { line: 3, start: 81, end: 86 }, }, { op: { op: "output" }, separator: ";", span: { line: 3, start: 87, end: 94 }, }, { op: { op: "tock" }, separator: ",", span: { line: 3, start: 95, end: 100 }, }, { op: { op: "output" }, separator: ";", span: { line: 3, start: 101, end: 108 }, }, { op: { op: "set", id: "in", value: 0 }, separator: ",", span: { line: 4, start: 109, end: 118 }, }, { op: { op: "set", id: "load", value: 1 }, separator: ",", span: { line: 4, start: 119, end: 130 }, }, { op: { op: "eval" }, separator: ",", span: { line: 4, start: 131, end: 136 }, }, { op: { op: "output" }, separator: ";", span: { line: 4, start: 137, end: 144 }, }, ], }); }); it("parses a test file with negative integers", () => { const match = grammar.match(MEM_TST); expect(match).toHaveSucceeded(); expect(TST.semantics(match).tst).toEqual({ lines: [ // output-list time%S1.2.1 in%B2.1.2; { op: { op: "output-list", spec: [ { id: "time", builtin: false, address: -1, format: { style: "S", width: 2, lpad: 1, rpad: 1 }, }, { id: "in", builtin: false, address: -1, format: { style: "B", width: 1, lpad: 2, rpad: 2 }, }, ], }, separator: ";", span: { start: 1, end: 35, line: 2, }, }, // set in -32123, tick, output; { op: { op: "set", id: "in", value: 33413 /* unsigned */ }, separator: ",", span: { line: 3, start: 36, end: 50 }, }, { op: { op: "tick" }, separator: ",", span: { line: 3, start: 51, end: 56 }, }, { op: { op: "output" }, separator: ";", span: { line: 3, start: 57, end: 64 }, }, ], }); }); it("repeats blocks", () => { const match = grammar.match(MEM_REPEAT); expect(match).toHaveSucceeded(); expect(TST.semantics(match).tst).toEqual({ lines: [ { count: 14, statements: [ { op: { op: "eval" }, separator: ",", span: { line: 3, start: 15, end: 20 }, }, { op: { op: "output" }, separator: ";", span: { line: 3, start: 21, end: 28 }, }, ], span: { start: 1, end: 30, line: 2, }, }, ], }); }); it("repeats indefinitely", () => { const match = grammar.match(INDEF_REPEAT); expect(match).toHaveSucceeded(); expect(TST.semantics(match).tst).toEqual({ lines: [ { count: -1, span: { start: 1, end: 27, line: 2, }, statements: [ { op: { op: "eval" }, separator: ",", span: { line: 3, start: 12, end: 17 }, }, { op: { op: "output" }, separator: ";", span: { line: 3, start: 18, end: 25 }, }, ], }, ], }); }); it("loops with a condition", () => { const match = grammar.match(COND_WHILE); expect(match).toHaveSucceeded(); expect(TST.semantics(match).tst).toEqual({ lines: [ { span: { start: 0, end: 27, line: 1, }, condition: { op: "<>", left: "out", right: 89, }, statements: [ { op: { op: "eval" }, separator: ";", span: { start: 20, end: 25, line: 2, }, }, ], }, ], }); }); it("loads ROMs", () => { const match = grammar.match(`ROM32K load Max.hack;`); expect(match).toHaveSucceeded(); expect(TST.semantics(match).tst).toEqual({ lines: [ { span: { start: 0, end: 21, line: 1, }, op: { op: "loadRom", file: "Max.hack" }, separator: ";", }, ], }); }); }); 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(); });