From b1ce1927022d374198441afafeb27199fc8a052a Mon Sep 17 00:00:00 2001 From: eunhhu Date: Thu, 24 Sep 2026 17:14:54 +0100 Subject: Establish native micro runtime checkpoint --- prototypes/workbench/compiler.test.mjs | 240 +++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 prototypes/workbench/compiler.test.mjs (limited to 'prototypes/workbench/compiler.test.mjs') diff --git a/prototypes/workbench/compiler.test.mjs b/prototypes/workbench/compiler.test.mjs new file mode 100644 index 0000000..89b8c6c --- /dev/null +++ b/prototypes/workbench/compiler.test.mjs @@ -0,0 +1,240 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { compile, LIMITS } from './web/compiler.mjs'; + +function diagnostic(source, pattern, line, column) { + assert.throws(() => compile(source), error => { + assert.match(error.message, pattern); + assert.ok(Number.isInteger(error.line) && error.line > 0); + assert.ok(Number.isInteger(error.column) && error.column > 0); + if (line !== undefined) assert.equal(error.line, line); + if (column !== undefined) assert.equal(error.column, column); + return true; + }); +} + +// A small independent evaluator checks compiler lowering. The product executes +// these instructions in Zig; the VM has its own native and integration checks. +function evaluate(program, start, length, values, dt = 0) { + const stack = []; + for (let pc = start; pc < start + length; pc++) { + const instruction = program.instructions[pc]; + const { op, arg, value } = instruction; + if (op === 0) stack.push(value); + else if (op === 1) stack.push(values[arg]); + else if (op === 2) stack.push(dt); + else if (op === 7) stack.push(-stack.pop()); + else if (op === 16) stack.push(Number(!stack.pop())); + else if (op === 19) { + const high = stack.pop(), low = stack.pop(), x = stack.pop(); + assert.ok(low <= high); stack.push(Math.min(Math.max(x, low), high)); + } else if (op === 20) values[arg] = stack.pop(); + else if (op === 21) { if (!stack.pop()) pc = arg - 1; } + else { + const right = stack.pop(), left = stack.pop(); + const operations = { + 3: () => left + right, 4: () => left - right, 5: () => left * right, + 6: () => left / right, 8: () => Number(left < right), + 9: () => Number(left <= right), 10: () => Number(left > right), + 11: () => Number(left >= right), 12: () => Number(left === right), + 13: () => Number(left !== right), 14: () => Number(Boolean(left) && Boolean(right)), + 15: () => Number(Boolean(left) || Boolean(right)), + 17: () => Math.min(left, right), 18: () => Math.max(left, right), + }; + assert.ok(operations[op], `Unknown opcode ${op}`); + stack.push(operations[op]()); + } + assert.ok(stack.every(Number.isFinite)); + } + return stack; +} + +function dispatch(program, event, values, dt = 0) { + for (const handler of program.handlers.filter(handler => handler.event === event)) { + assert.deepEqual(evaluate(program, handler.start, handler.length, values, dt), []); + } +} + +test('a blank document is an empty program with no implied behavior', () => { + const result = compile(''); + for (const field of ['states', 'inputs', 'views', 'instructions', 'handlers']) assert.deepEqual(result[field], []); + assert.equal(result.metrics.sourceBytes, 0); + assert.equal(result.metrics.maxStack, 0); + assert.deepEqual(compile('# comment only\n;').instructions, []); +}); + +test('forward references and separate state/input namespaces preserve declaration IDs', () => { + const program = compile('on tap.down { second = tap + first }\ntext label(0,0,second)\nstate first = -2\nstate tap = +3\ninput tap = Space\nstate second = .5e2'); + assert.deepEqual(program.states.map(state => state.name), ['first', 'tap', 'second']); + assert.equal(program.handlers[0].event, 1); + const values = program.states.map(state => state.initial); + dispatch(program, 1, values); + assert.deepEqual(values, [-2, 3, 1]); + const binding = program.views[0].bindings.value; + assert.deepEqual(evaluate(program, binding.start, binding.length, values), [1]); +}); + +test('ordered assignments, repeated handlers, and condition evaluation once', () => { + const program = compile(`state x = 1 +input tap = Enter +on tap.down { + if x == 1 { + x = 0 + x = x + 5 + } +} +on tap.down { x = x * 2 } +on tap.up { x = x - 1 }`); + const values = [1]; + dispatch(program, 1, values); + assert.equal(values[0], 10); + dispatch(program, 2, values); + assert.equal(values[0], 9); + const branch = program.instructions.find(instruction => instruction.op === 21); + assert.equal(branch.arg, program.handlers[0].start + program.handlers[0].length); +}); + +test('nested branches use absolute forward targets and skip correct bodies', () => { + const program = compile(`state x = 0 +text label(0, 0, x) +on frame { + if x > 0 { + x = 8 + if x == 8 { x = 9 } + } + x = x + 1 +}`); + const values = [0]; + dispatch(program, 0, values, 1 / 60); assert.equal(values[0], 1); + dispatch(program, 0, values, 1 / 60); assert.equal(values[0], 10); + const handler = program.handlers[0]; + program.instructions.forEach((instruction, pc) => { + if (instruction.op === 21) assert.ok(instruction.arg > pc && instruction.arg <= handler.start + handler.length); + }); +}); + +test('operator precedence, associativity, unary and builtin function lowering', () => { + const expressions = new Map([ + ['2 + 3 * 4', 14], ['8 - 3 - 2', 3], ['12 / 3 / 2', 2], + ['-2 * -3', 6], ['!0 + !4', 1], ['1 || 0 && 0', 1], + ['1 < 2 && 2 <= 2 && 4 > 3 && 4 >= 4', 1], + ['3 == 3 && 3 != 4', 1], ['clamp(max(3, 9) - min(4, 7), 0, 4)', 4], + ]); + for (const [source, expected] of expressions) { + const program = compile(`state x = 0\non frame { x = ${source} }`); + const values = [0]; dispatch(program, 0, values, 1 / 60); + assert.equal(values[0], expected, source); + } + const eager = compile('state x = 0\non frame { x = 1 || x / 0 }'); + assert.ok(eager.instructions.some(instruction => instruction.op === 6)); + assert.ok(!eager.instructions.some(instruction => instruction.op === 21)); +}); + +test('multiline parentheses, scientific literals, comments, and string escapes', () => { + const program = compile(`state x = 1e-2 # comment +box tile( + (x + + 2), 0, max( + 1, 2), 4, + "#AABBCC") +text label(0, 0, x, "# count: \\\"value\\\" \\n") +on frame { x = x + dt }`); + assert.equal(program.views[0].color, '#AABBCC'); + assert.equal(program.views[1].prefix, '# count: "value" \n'); + const values = [.01]; dispatch(program, 0, values, 1 / 60); + assert.equal(values[0], .01 + 1 / 60); + const binding = program.views[0].bindings.x; + assert.deepEqual(evaluate(program, binding.start, binding.length, values), [values[0] + 2]); +}); + +test('all instruction fields are numeric and carry original source line', () => { + const program = compile('state x = 0\non frame {\n x = x + dt\n}'); + for (const instruction of program.instructions) { + assert.deepEqual(Object.keys(instruction), ['op', 'arg', 'value', 'line']); + assert.ok(Object.values(instruction).every(Number.isFinite)); + assert.equal(instruction.line, 3); + } +}); + +test('contextual declaration words remain valid names outside reserved builtins', () => { + const program = compile('state if = 1\nstate state = 2\non frame { if = if + state }'); + const values = [1, 2]; dispatch(program, 0, values, 1 / 60); + assert.deepEqual(values, [3, 2]); +}); + +test('unknown and reserved symbols have useful one-based locations', () => { + diagnostic('state x = 0\non frame {\n x = missing\n}', /Unknown state missing/, 3, 7); + diagnostic('on missing.down {}', /Unknown input missing/, 1, 4); + diagnostic('on frame { absent = 1 }', /Unknown state absent/, 1, 12); + for (const name of ['frame', 'dt', 'min', 'max', 'clamp']) { + diagnostic(`state ${name} = 0`, /Reserved name/, 1, 7); + diagnostic(`input ${name} = Space`, /Reserved name/); + } + diagnostic('text clock(0, 0, dt)', /dt is only available in handlers/); +}); + +test('duplicates, unsupported keys, initializers, and invalid syntax are rejected', () => { + const invalid = [ + ['state x=0\nstate x=1', /Duplicate state/], + ['input tap=Space\ninput tap=Enter', /Duplicate input/], + ['input tap=Space\ninput other=Space', /Duplicate keyboard/], + ['box a(0,0,1,1,"#000000")\ncircle a(0,0,1,"#000000")', /Duplicate view/], + ['input tap=Escape', /Unsupported keyboard/], + ['state x=1+2', /Expected newline/], + ['state x=1e999', /finite/], + ['state x=NaN', /numeric state initializer/], + ['on tap.click {}', /Expected down or up/], + ['on frame.down {}', /Expected {/], + ['state x=0\non frame { x = min(1) }', /min expects 2/], + ['state x=0\non frame { x = clamp(1,2) }', /clamp expects 3/], + ['state x=0\non frame { x = 1 +\n2 }', /Expected a number/], + ['text a(0,0,1,"broken)', /Unterminated string/], + ['text a(0,0,1,"bad\\q")', /Unsupported string escape/], + ['box a(0,0,1,1,"blue")', /Color must use/], + ['state x=0 @', /Unexpected character/], + ['on frame {', /assignment, if, or closing brace/], + ['state x = 0 state y = 1', /Expected newline/], + ]; + for (const [source, pattern] of invalid) diagnostic(source, pattern); +}); + +test('document, declaration, instruction, and nesting limits reject overflow', () => { + diagnostic('#' + '한'.repeat(11000), /32768 UTF-8 bytes/); + diagnostic(Array.from({ length: 33 }, (_, i) => `state s${i}=0`).join('\n'), /32 states/); + diagnostic(Array.from({ length: 9 }, (_, i) => `input i${i}=Digit${i}`).join('\n'), /8 inputs/); + diagnostic(Array.from({ length: 25 }, (_, i) => `text t${i}(0,0,0)`).join('\n'), /24 views/); + diagnostic('on frame {}\n'.repeat(65), /64 handlers/); + diagnostic(`state x=0\non frame {\n${'x=1\n'.repeat(257)}}`, /512 instructions/); + assert.equal(compile(`state x=0\non frame {\n${'x=1\n'.repeat(256)}}`).instructions.length, LIMITS.instructions); + diagnostic(`state x=0\non frame { x=${'('.repeat(65)}1${')'.repeat(65)} }`, /nesting exceeds 64/); + diagnostic(`on frame { ${'if 1 { '.repeat(65)}${'}\n'.repeat(66)}`, /nesting exceeds 64/); + // Right-nested clamp's two waiting operands exceed the stack before its depth limit. + let deep = '0'; + for (let i = 0; i < 33; i++) deep = `clamp(0,0,${deep})`; + diagnostic(`text stack(0,0,${deep})`, /stack exceeds 64/); +}); + +test('every supported input maps to a bounded ordered event ID', () => { + const keys = ['Space', 'Enter', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'KeyZ', 'Digit9']; + const program = compile(keys.map((key, i) => `input i${i}=${key}\non i${i}.down {}\non i${i}.up {}`).join('\n')); + assert.deepEqual(program.handlers.map(handler => handler.event), Array.from({ length: 16 }, (_, i) => i + 1)); +}); + +test('optional examples compile and charge behavior is entirely authored', async () => { + for (const name of ['counter', 'charge', 'toggle']) { + const program = compile(await readFile(new URL(`./examples/${name}.pure`, import.meta.url), 'utf8')); + assert.ok(program.instructions.length <= LIMITS.instructions); + assert.ok(program.views.length > 0); + if (name === 'charge') { + const values = program.states.map(state => state.initial); + dispatch(program, 1, values); + for (let frame = 0; frame < 90; frame++) dispatch(program, 0, values, 1 / 60); + assert.equal(values[0], 1); assert.equal(values[1], 1); assert.equal(values[2], 240); + dispatch(program, 2, values); assert.deepEqual(values, [0, 0, 240, -460]); + dispatch(program, 0, values, 1 / 60); assert.ok(values[2] < 240); + for (let frame = 0; frame < 120; frame++) dispatch(program, 0, values, 1 / 60); + assert.equal(values[2], 240); assert.equal(values[3], 0); + } + } +}); -- cgit v1.2.3