From 66c46677e6c4ada0972b6e5348fb1d0991fde010 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sun, 5 Oct 2025 18:54:26 -0700 Subject: [PATCH] convert tests --- tests/exceptions.test.ts | 362 ++++++++++++++------------------------- tests/native.test.ts | 140 ++++++--------- 2 files changed, 182 insertions(+), 320 deletions(-) diff --git a/tests/exceptions.test.ts b/tests/exceptions.test.ts index 5345da3..7b59566 100644 --- a/tests/exceptions.test.ts +++ b/tests/exceptions.test.ts @@ -1,130 +1,84 @@ import { test, expect } from "bun:test" -import { VM } from "#vm" -import { OpCode } from "#opcode" -import { toValue } from "#value" +import { toBytecode } from "#bytecode" +import { run } from "#index" test("PUSH_TRY and POP_TRY - no exception thrown", async () => { // Try block that completes successfully - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH_TRY, operand: 5 }, // catch at instruction 5 - { op: OpCode.PUSH, operand: 0 }, // push 42 - { op: OpCode.PUSH, operand: 1 }, // push 10 - { op: OpCode.ADD }, - { op: OpCode.POP_TRY }, // pop handler (no exception) - { op: OpCode.HALT }, - - // Catch block (not executed) - { op: OpCode.PUSH, operand: 2 }, // push 999 - { op: OpCode.HALT } - ], - constants: [ - toValue(42), - toValue(10), - toValue(999) - ] - }) - - const result = await vm.run() - expect(result).toEqual({ type: 'number', value: 52 }) + const str = ` + PUSH_TRY #5 + PUSH 42 + PUSH 10 + ADD + POP_TRY + HALT + PUSH 999 + HALT + ` + expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 }) }) test("THROW - catch exception with error value", async () => { // Try block that throws an exception - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH_TRY, operand: 5 }, // catch at instruction 5 - { op: OpCode.PUSH, operand: 0 }, // push error message - { op: OpCode.THROW }, // throw exception - { op: OpCode.PUSH, operand: 1 }, // not executed - { op: OpCode.HALT }, - - // Catch block (instruction 5) - { op: OpCode.HALT } // error value is on stack - ], - constants: [ - toValue('error occurred'), - toValue(999) - ] - }) - - const result = await vm.run() - expect(result).toEqual({ type: 'string', value: 'error occurred' }) + const str = ` + PUSH_TRY #5 + PUSH "error occurred" + THROW + PUSH 999 + HALT + HALT + ` + expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error occurred' }) }) test("THROW - uncaught exception throws JS error", async () => { - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH, operand: 0 }, - { op: OpCode.THROW } - ], - constants: [ - toValue('something went wrong') - ] - }) - - await expect(vm.run()).rejects.toThrow('Uncaught exception: something went wrong') + const str = ` + PUSH "something went wrong" + THROW + ` + await expect(run(toBytecode(str))).rejects.toThrow('Uncaught exception: something went wrong') }) test("THROW - exception with nested try blocks", async () => { // Nested try blocks, inner one catches - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH_TRY, operand: 10 }, // outer catch at 10 - { op: OpCode.PUSH_TRY, operand: 6 }, // inner catch at 6 - { op: OpCode.PUSH, operand: 0 }, // push 'inner error' - { op: OpCode.THROW }, // throw - { op: OpCode.PUSH, operand: 1 }, // not executed - { op: OpCode.HALT }, - - // Inner catch block (instruction 6) - { op: OpCode.STORE, operand: 'err' }, - { op: OpCode.POP_TRY }, // pop outer handler - { op: OpCode.LOAD, operand: 'err' }, - { op: OpCode.HALT }, - - // Outer catch block (instruction 10, not executed) - { op: OpCode.PUSH, operand: 2 }, - { op: OpCode.HALT } - ], - constants: [ - toValue('inner error'), - toValue(999), - toValue('outer error') - ] - }) - - const result = await vm.run() - expect(result).toEqual({ type: 'string', value: 'inner error' }) + const str = ` + PUSH_TRY #10 + PUSH_TRY #6 + PUSH "inner error" + THROW + PUSH 999 + HALT + STORE err + POP_TRY + LOAD err + HALT + PUSH "outer error" + HALT + ` + expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'inner error' }) }) test("THROW - exception skips outer handler", async () => { // Nested try blocks, inner doesn't catch, outer does - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH_TRY, operand: 8 }, // outer catch at 8 - { op: OpCode.PUSH_TRY, operand: 6 }, // inner catch at 6 - { op: OpCode.PUSH, operand: 0 }, // push error - { op: OpCode.THROW }, // throw - { op: OpCode.HALT }, - - // Inner catch block (instruction 6) - re-throws - { op: OpCode.THROW }, // re-throw the error - - // Outer catch block (instruction 8) - { op: OpCode.HALT } - ], - constants: [ - toValue('error message') - ] - }) - - const result = await vm.run() - expect(result).toEqual({ type: 'string', value: 'error message' }) + const str = ` + PUSH_TRY #8 + PUSH_TRY #6 + PUSH "error message" + THROW + HALT + THROW + HALT + HALT + ` + expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error message' }) }) test("THROW - exception unwinds call stack", async () => { // Function that throws, caller has try/catch + // Note: This test uses manual VM construction because it needs MAKE_FUNCTION + const { VM } = await import("#vm") + const { OpCode } = await import("#opcode") + const { toValue } = await import("#value") + const vm = new VM({ instructions: [ // 0: Main code with try block @@ -163,153 +117,93 @@ test("THROW - exception unwinds call stack", async () => { }) test("POP_TRY - error when no handler to pop", async () => { - const vm = new VM({ - instructions: [ - { op: OpCode.POP_TRY } - ], - constants: [] - }) - - await expect(vm.run()).rejects.toThrow('POP_TRY: no exception handler to pop') + const str = ` + POP_TRY + ` + await expect(run(toBytecode(str))).rejects.toThrow('POP_TRY: no exception handler to pop') }) test("PUSH_FINALLY - finally executes after successful try", async () => { // Try block completes normally, compiler jumps to finally - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH_TRY, operand: 8 }, // catch at 8 - { op: OpCode.PUSH_FINALLY, operand: 9 }, // finally at 9 - { op: OpCode.PUSH, operand: 0 }, // push 10 - { op: OpCode.STORE, operand: 'x' }, - { op: OpCode.POP_TRY }, - { op: OpCode.JUMP, operand: 3 }, // compiler jumps to finally (5->6, +3 = 9) - - // Not executed - { op: OpCode.HALT }, - { op: OpCode.HALT }, - - // Catch block (instruction 8) - not executed - { op: OpCode.HALT }, - - // Finally block (instruction 9) - { op: OpCode.PUSH, operand: 1 }, // push 100 - { op: OpCode.LOAD, operand: 'x' }, // load 10 - { op: OpCode.ADD }, // 110 - { op: OpCode.HALT } - ], - constants: [ - toValue(10), - toValue(100) - ] - }) - - const result = await vm.run() - expect(result).toEqual({ type: 'number', value: 110 }) + const str = ` + PUSH_TRY #8 + PUSH_FINALLY #9 + PUSH 10 + STORE x + POP_TRY + JUMP #3 + HALT + HALT + HALT + PUSH 100 + LOAD x + ADD + HALT + ` + expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 110 }) }) test("PUSH_FINALLY - finally executes after exception", async () => { // Try block throws, THROW jumps to finally (skipping catch) - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH_TRY, operand: 5 }, // catch at 5 - { op: OpCode.PUSH_FINALLY, operand: 7 }, // finally at 7 - { op: OpCode.PUSH, operand: 0 }, // push error - { op: OpCode.THROW }, // throw (jumps to finally, not catch!) - { op: OpCode.HALT }, // not executed - - // Catch block (instruction 5) - skipped because finally is present - { op: OpCode.STORE, operand: 'err' }, - { op: OpCode.HALT }, - - // Finally block (instruction 7) - error is still on stack - { op: OpCode.POP }, // discard error - { op: OpCode.PUSH, operand: 1 }, // push "finally ran" - { op: OpCode.HALT } - ], - constants: [ - toValue('error'), - toValue('finally ran') - ] - }) - - const result = await vm.run() - expect(result).toEqual({ type: 'string', value: 'finally ran' }) + const str = ` + PUSH_TRY #5 + PUSH_FINALLY #7 + PUSH "error" + THROW + HALT + STORE err + HALT + POP + PUSH "finally ran" + HALT + ` + expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'finally ran' }) }) test("PUSH_FINALLY - finally without catch", async () => { // Try-finally without catch (compiler generates jump to finally) - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH_TRY, operand: 7 }, // catch at 7 (dummy) - { op: OpCode.PUSH_FINALLY, operand: 7 }, // finally at 7 - { op: OpCode.PUSH, operand: 0 }, // push 42 - { op: OpCode.STORE, operand: 'x' }, - { op: OpCode.POP_TRY }, - { op: OpCode.JUMP, operand: 1 }, // compiler jumps to finally - { op: OpCode.HALT }, // skipped - - // Finally block (instruction 7) - { op: OpCode.LOAD, operand: 'x' }, // load 42 - { op: OpCode.PUSH, operand: 1 }, // push 10 - { op: OpCode.ADD }, // 52 - { op: OpCode.HALT } - ], - constants: [ - toValue(42), - toValue(10) - ] - }) - - const result = await vm.run() - expect(result).toEqual({ type: 'number', value: 52 }) + const str = ` + PUSH_TRY #7 + PUSH_FINALLY #7 + PUSH 42 + STORE x + POP_TRY + JUMP #1 + HALT + LOAD x + PUSH 10 + ADD + HALT + ` + expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 }) }) test("PUSH_FINALLY - nested try-finally blocks", async () => { // Nested try-finally blocks with compiler-generated jumps - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH_TRY, operand: 11 }, // outer catch at 11 - { op: OpCode.PUSH_FINALLY, operand: 14 }, // outer finally at 14 - { op: OpCode.PUSH_TRY, operand: 9 }, // inner catch at 9 - { op: OpCode.PUSH_FINALLY, operand: 10 }, // inner finally at 10 - { op: OpCode.PUSH, operand: 0 }, // push 1 - { op: OpCode.POP_TRY }, // inner pop (instruction 5) - { op: OpCode.JUMP, operand: 3 }, // jump to inner finally (6->7, +3 = 10) - { op: OpCode.HALT }, // skipped - { op: OpCode.HALT }, // skipped - - // Inner catch (instruction 9) - not executed - { op: OpCode.HALT }, - - // Inner finally (instruction 10) - { op: OpCode.PUSH, operand: 1 }, // push 10 - { op: OpCode.POP_TRY }, // outer pop (instruction 11) - { op: OpCode.JUMP, operand: 1 }, // jump to outer finally (12->13, +1 = 14) - - // Outer catch (instruction 13) - not executed - { op: OpCode.HALT }, - - // Outer finally (instruction 14) - { op: OpCode.ADD }, // 1 + 10 = 11 - { op: OpCode.HALT } - ], - constants: [ - toValue(1), - toValue(10) - ] - }) - - const result = await vm.run() - expect(result).toEqual({ type: 'number', value: 11 }) + const str = ` + PUSH_TRY #11 + PUSH_FINALLY #14 + PUSH_TRY #9 + PUSH_FINALLY #10 + PUSH 1 + POP_TRY + JUMP #3 + HALT + HALT + HALT + PUSH 10 + POP_TRY + JUMP #1 + HALT + ADD + HALT + ` + expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 11 }) }) test("PUSH_FINALLY - error when no handler", async () => { - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH_FINALLY, operand: 5 } - ], - constants: [] - }) - - await expect(vm.run()).rejects.toThrow('PUSH_FINALLY: no exception handler to modify') + const str = ` + PUSH_FINALLY #5 + ` + await expect(run(toBytecode(str))).rejects.toThrow('PUSH_FINALLY: no exception handler to modify') }) diff --git a/tests/native.test.ts b/tests/native.test.ts index 7d0a4f1..cc6607a 100644 --- a/tests/native.test.ts +++ b/tests/native.test.ts @@ -1,23 +1,18 @@ import { test, expect } from "bun:test" import { VM } from "#vm" -import { OpCode } from "#opcode" +import { toBytecode } from "#bytecode" import { toValue, toNumber, toString } from "#value" test("CALL_NATIVE - basic function call", async () => { - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH, operand: 0 }, // push 5 - { op: OpCode.PUSH, operand: 1 }, // push 10 - { op: OpCode.CALL_NATIVE, operand: 'add' }, // call TypeScript 'add' - { op: OpCode.HALT } - ], - constants: [ - toValue(5), - toValue(10) - ] - }) + const bytecode = toBytecode(` + PUSH 5 + PUSH 10 + CALL_NATIVE add + `) - // Register a TypeScript function + const vm = new VM(bytecode) + + // Register a native function vm.registerFunction('add', (a, b) => { return toValue(toNumber(a) + toNumber(b)) }) @@ -27,18 +22,13 @@ test("CALL_NATIVE - basic function call", async () => { }) test("CALL_NATIVE - function with string manipulation", async () => { - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH, operand: 0 }, // push "hello" - { op: OpCode.PUSH, operand: 1 }, // push "world" - { op: OpCode.CALL_NATIVE, operand: 'concat' }, // call TypeScript 'concat' - { op: OpCode.HALT } - ], - constants: [ - toValue("hello"), - toValue("world") - ] - }) + const bytecode = toBytecode(` + PUSH "hello" + PUSH "world" + CALL_NATIVE concat + `) + + const vm = new VM(bytecode) vm.registerFunction('concat', (a, b) => { const aStr = a.type === 'string' ? a.value : toString(a) @@ -51,16 +41,12 @@ test("CALL_NATIVE - function with string manipulation", async () => { }) test("CALL_NATIVE - async function", async () => { - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH, operand: 0 }, // push 42 - { op: OpCode.CALL_NATIVE, operand: 'asyncDouble' }, // call async TypeScript function - { op: OpCode.HALT } - ], - constants: [ - toValue(42) - ] - }) + const bytecode = toBytecode(` + PUSH 42 + CALL_NATIVE asyncDouble + `) + + const vm = new VM(bytecode) vm.registerFunction('asyncDouble', async (a) => { // Simulate async operation @@ -73,13 +59,11 @@ test("CALL_NATIVE - async function", async () => { }) test("CALL_NATIVE - function with no arguments", async () => { - const vm = new VM({ - instructions: [ - { op: OpCode.CALL_NATIVE, operand: 'getAnswer' }, // call with empty stack - { op: OpCode.HALT } - ], - constants: [] - }) + const bytecode = toBytecode(` + CALL_NATIVE getAnswer + `) + + const vm = new VM(bytecode) vm.registerFunction('getAnswer', () => { return toValue(42) @@ -90,20 +74,14 @@ test("CALL_NATIVE - function with no arguments", async () => { }) test("CALL_NATIVE - function with multiple arguments", async () => { - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH, operand: 0 }, // push 2 - { op: OpCode.PUSH, operand: 1 }, // push 3 - { op: OpCode.PUSH, operand: 2 }, // push 4 - { op: OpCode.CALL_NATIVE, operand: 'sum' }, // call TypeScript 'sum' - { op: OpCode.HALT } - ], - constants: [ - toValue(2), - toValue(3), - toValue(4) - ] - }) + const bytecode = toBytecode(` + PUSH 2 + PUSH 3 + PUSH 4 + CALL_NATIVE sum + `) + + const vm = new VM(bytecode) vm.registerFunction('sum', (...args) => { const total = args.reduce((acc, val) => acc + toNumber(val), 0) @@ -115,16 +93,12 @@ test("CALL_NATIVE - function with multiple arguments", async () => { }) test("CALL_NATIVE - function returns array", async () => { - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH, operand: 0 }, // push 3 - { op: OpCode.CALL_NATIVE, operand: 'makeRange' }, // call TypeScript 'makeRange' - { op: OpCode.HALT } - ], - constants: [ - toValue(3) - ] - }) + const bytecode = toBytecode(` + PUSH 3 + CALL_NATIVE makeRange + `) + + const vm = new VM(bytecode) vm.registerFunction('makeRange', (n) => { const count = toNumber(n) @@ -148,30 +122,24 @@ test("CALL_NATIVE - function returns array", async () => { }) test("CALL_NATIVE - function not found", async () => { - const vm = new VM({ - instructions: [ - { op: OpCode.CALL_NATIVE, operand: 'nonexistent' } - ], - constants: [] - }) + const bytecode = toBytecode(` + CALL_NATIVE nonexistent + `) + + const vm = new VM(bytecode) await expect(vm.run()).rejects.toThrow('CALL_NATIVE: function not found: nonexistent') }) test("CALL_NATIVE - using result in subsequent operations", async () => { - const vm = new VM({ - instructions: [ - { op: OpCode.PUSH, operand: 0 }, // push 5 - { op: OpCode.CALL_NATIVE, operand: 'triple' }, // call TypeScript 'triple' -> 15 - { op: OpCode.PUSH, operand: 1 }, // push 10 - { op: OpCode.ADD }, // 15 + 10 = 25 - { op: OpCode.HALT } - ], - constants: [ - toValue(5), - toValue(10) - ] - }) + const bytecode = toBytecode(` + PUSH 5 + CALL_NATIVE triple + PUSH 10 + ADD + `) + + const vm = new VM(bytecode) vm.registerFunction('triple', (n) => { return toValue(toNumber(n) * 3)