convert tests

This commit is contained in:
Chris Wanstrath 2025-10-05 18:54:26 -07:00
parent 2a280a10b3
commit 66c46677e6
2 changed files with 182 additions and 320 deletions

View File

@ -1,130 +1,84 @@
import { test, expect } from "bun:test" import { test, expect } from "bun:test"
import { VM } from "#vm" import { toBytecode } from "#bytecode"
import { OpCode } from "#opcode" import { run } from "#index"
import { toValue } from "#value"
test("PUSH_TRY and POP_TRY - no exception thrown", async () => { test("PUSH_TRY and POP_TRY - no exception thrown", async () => {
// Try block that completes successfully // Try block that completes successfully
const vm = new VM({ const str = `
instructions: [ PUSH_TRY #5
{ op: OpCode.PUSH_TRY, operand: 5 }, // catch at instruction 5 PUSH 42
{ op: OpCode.PUSH, operand: 0 }, // push 42 PUSH 10
{ op: OpCode.PUSH, operand: 1 }, // push 10 ADD
{ op: OpCode.ADD }, POP_TRY
{ op: OpCode.POP_TRY }, // pop handler (no exception) HALT
{ op: OpCode.HALT }, PUSH 999
HALT
// Catch block (not executed) `
{ op: OpCode.PUSH, operand: 2 }, // push 999 expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 })
{ op: OpCode.HALT }
],
constants: [
toValue(42),
toValue(10),
toValue(999)
]
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 52 })
}) })
test("THROW - catch exception with error value", async () => { test("THROW - catch exception with error value", async () => {
// Try block that throws an exception // Try block that throws an exception
const vm = new VM({ const str = `
instructions: [ PUSH_TRY #5
{ op: OpCode.PUSH_TRY, operand: 5 }, // catch at instruction 5 PUSH "error occurred"
{ op: OpCode.PUSH, operand: 0 }, // push error message THROW
{ op: OpCode.THROW }, // throw exception PUSH 999
{ op: OpCode.PUSH, operand: 1 }, // not executed HALT
{ op: OpCode.HALT }, HALT
`
// Catch block (instruction 5) expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error occurred' })
{ 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' })
}) })
test("THROW - uncaught exception throws JS error", async () => { test("THROW - uncaught exception throws JS error", async () => {
const vm = new VM({ const str = `
instructions: [ PUSH "something went wrong"
{ op: OpCode.PUSH, operand: 0 }, THROW
{ op: OpCode.THROW } `
], await expect(run(toBytecode(str))).rejects.toThrow('Uncaught exception: something went wrong')
constants: [
toValue('something went wrong')
]
})
await expect(vm.run()).rejects.toThrow('Uncaught exception: something went wrong')
}) })
test("THROW - exception with nested try blocks", async () => { test("THROW - exception with nested try blocks", async () => {
// Nested try blocks, inner one catches // Nested try blocks, inner one catches
const vm = new VM({ const str = `
instructions: [ PUSH_TRY #10
{ op: OpCode.PUSH_TRY, operand: 10 }, // outer catch at 10 PUSH_TRY #6
{ op: OpCode.PUSH_TRY, operand: 6 }, // inner catch at 6 PUSH "inner error"
{ op: OpCode.PUSH, operand: 0 }, // push 'inner error' THROW
{ op: OpCode.THROW }, // throw PUSH 999
{ op: OpCode.PUSH, operand: 1 }, // not executed HALT
{ op: OpCode.HALT }, STORE err
POP_TRY
// Inner catch block (instruction 6) LOAD err
{ op: OpCode.STORE, operand: 'err' }, HALT
{ op: OpCode.POP_TRY }, // pop outer handler PUSH "outer error"
{ op: OpCode.LOAD, operand: 'err' }, HALT
{ op: OpCode.HALT }, `
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'inner error' })
// 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' })
}) })
test("THROW - exception skips outer handler", async () => { test("THROW - exception skips outer handler", async () => {
// Nested try blocks, inner doesn't catch, outer does // Nested try blocks, inner doesn't catch, outer does
const vm = new VM({ const str = `
instructions: [ PUSH_TRY #8
{ op: OpCode.PUSH_TRY, operand: 8 }, // outer catch at 8 PUSH_TRY #6
{ op: OpCode.PUSH_TRY, operand: 6 }, // inner catch at 6 PUSH "error message"
{ op: OpCode.PUSH, operand: 0 }, // push error THROW
{ op: OpCode.THROW }, // throw HALT
{ op: OpCode.HALT }, THROW
HALT
// Inner catch block (instruction 6) - re-throws HALT
{ op: OpCode.THROW }, // re-throw the error `
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error message' })
// 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' })
}) })
test("THROW - exception unwinds call stack", async () => { test("THROW - exception unwinds call stack", async () => {
// Function that throws, caller has try/catch // 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({ const vm = new VM({
instructions: [ instructions: [
// 0: Main code with try block // 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 () => { test("POP_TRY - error when no handler to pop", async () => {
const vm = new VM({ const str = `
instructions: [ POP_TRY
{ op: OpCode.POP_TRY } `
], await expect(run(toBytecode(str))).rejects.toThrow('POP_TRY: no exception handler to pop')
constants: []
})
await expect(vm.run()).rejects.toThrow('POP_TRY: no exception handler to pop')
}) })
test("PUSH_FINALLY - finally executes after successful try", async () => { test("PUSH_FINALLY - finally executes after successful try", async () => {
// Try block completes normally, compiler jumps to finally // Try block completes normally, compiler jumps to finally
const vm = new VM({ const str = `
instructions: [ PUSH_TRY #8
{ op: OpCode.PUSH_TRY, operand: 8 }, // catch at 8 PUSH_FINALLY #9
{ op: OpCode.PUSH_FINALLY, operand: 9 }, // finally at 9 PUSH 10
{ op: OpCode.PUSH, operand: 0 }, // push 10 STORE x
{ op: OpCode.STORE, operand: 'x' }, POP_TRY
{ op: OpCode.POP_TRY }, JUMP #3
{ op: OpCode.JUMP, operand: 3 }, // compiler jumps to finally (5->6, +3 = 9) HALT
HALT
// Not executed HALT
{ op: OpCode.HALT }, PUSH 100
{ op: OpCode.HALT }, LOAD x
ADD
// Catch block (instruction 8) - not executed HALT
{ op: OpCode.HALT }, `
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 110 })
// 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 })
}) })
test("PUSH_FINALLY - finally executes after exception", async () => { test("PUSH_FINALLY - finally executes after exception", async () => {
// Try block throws, THROW jumps to finally (skipping catch) // Try block throws, THROW jumps to finally (skipping catch)
const vm = new VM({ const str = `
instructions: [ PUSH_TRY #5
{ op: OpCode.PUSH_TRY, operand: 5 }, // catch at 5 PUSH_FINALLY #7
{ op: OpCode.PUSH_FINALLY, operand: 7 }, // finally at 7 PUSH "error"
{ op: OpCode.PUSH, operand: 0 }, // push error THROW
{ op: OpCode.THROW }, // throw (jumps to finally, not catch!) HALT
{ op: OpCode.HALT }, // not executed STORE err
HALT
// Catch block (instruction 5) - skipped because finally is present POP
{ op: OpCode.STORE, operand: 'err' }, PUSH "finally ran"
{ op: OpCode.HALT }, HALT
`
// Finally block (instruction 7) - error is still on stack expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'finally ran' })
{ 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' })
}) })
test("PUSH_FINALLY - finally without catch", async () => { test("PUSH_FINALLY - finally without catch", async () => {
// Try-finally without catch (compiler generates jump to finally) // Try-finally without catch (compiler generates jump to finally)
const vm = new VM({ const str = `
instructions: [ PUSH_TRY #7
{ op: OpCode.PUSH_TRY, operand: 7 }, // catch at 7 (dummy) PUSH_FINALLY #7
{ op: OpCode.PUSH_FINALLY, operand: 7 }, // finally at 7 PUSH 42
{ op: OpCode.PUSH, operand: 0 }, // push 42 STORE x
{ op: OpCode.STORE, operand: 'x' }, POP_TRY
{ op: OpCode.POP_TRY }, JUMP #1
{ op: OpCode.JUMP, operand: 1 }, // compiler jumps to finally HALT
{ op: OpCode.HALT }, // skipped LOAD x
PUSH 10
// Finally block (instruction 7) ADD
{ op: OpCode.LOAD, operand: 'x' }, // load 42 HALT
{ op: OpCode.PUSH, operand: 1 }, // push 10 `
{ op: OpCode.ADD }, // 52 expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 })
{ op: OpCode.HALT }
],
constants: [
toValue(42),
toValue(10)
]
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 52 })
}) })
test("PUSH_FINALLY - nested try-finally blocks", async () => { test("PUSH_FINALLY - nested try-finally blocks", async () => {
// Nested try-finally blocks with compiler-generated jumps // Nested try-finally blocks with compiler-generated jumps
const vm = new VM({ const str = `
instructions: [ PUSH_TRY #11
{ op: OpCode.PUSH_TRY, operand: 11 }, // outer catch at 11 PUSH_FINALLY #14
{ op: OpCode.PUSH_FINALLY, operand: 14 }, // outer finally at 14 PUSH_TRY #9
{ op: OpCode.PUSH_TRY, operand: 9 }, // inner catch at 9 PUSH_FINALLY #10
{ op: OpCode.PUSH_FINALLY, operand: 10 }, // inner finally at 10 PUSH 1
{ op: OpCode.PUSH, operand: 0 }, // push 1 POP_TRY
{ op: OpCode.POP_TRY }, // inner pop (instruction 5) JUMP #3
{ op: OpCode.JUMP, operand: 3 }, // jump to inner finally (6->7, +3 = 10) HALT
{ op: OpCode.HALT }, // skipped HALT
{ op: OpCode.HALT }, // skipped HALT
PUSH 10
// Inner catch (instruction 9) - not executed POP_TRY
{ op: OpCode.HALT }, JUMP #1
HALT
// Inner finally (instruction 10) ADD
{ op: OpCode.PUSH, operand: 1 }, // push 10 HALT
{ op: OpCode.POP_TRY }, // outer pop (instruction 11) `
{ op: OpCode.JUMP, operand: 1 }, // jump to outer finally (12->13, +1 = 14) expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 11 })
// 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 })
}) })
test("PUSH_FINALLY - error when no handler", async () => { test("PUSH_FINALLY - error when no handler", async () => {
const vm = new VM({ const str = `
instructions: [ PUSH_FINALLY #5
{ op: OpCode.PUSH_FINALLY, operand: 5 } `
], await expect(run(toBytecode(str))).rejects.toThrow('PUSH_FINALLY: no exception handler to modify')
constants: []
})
await expect(vm.run()).rejects.toThrow('PUSH_FINALLY: no exception handler to modify')
}) })

View File

@ -1,23 +1,18 @@
import { test, expect } from "bun:test" import { test, expect } from "bun:test"
import { VM } from "#vm" import { VM } from "#vm"
import { OpCode } from "#opcode" import { toBytecode } from "#bytecode"
import { toValue, toNumber, toString } from "#value" import { toValue, toNumber, toString } from "#value"
test("CALL_NATIVE - basic function call", async () => { test("CALL_NATIVE - basic function call", async () => {
const vm = new VM({ const bytecode = toBytecode(`
instructions: [ PUSH 5
{ op: OpCode.PUSH, operand: 0 }, // push 5 PUSH 10
{ op: OpCode.PUSH, operand: 1 }, // push 10 CALL_NATIVE add
{ op: OpCode.CALL_NATIVE, operand: 'add' }, // call TypeScript 'add' `)
{ op: OpCode.HALT }
],
constants: [
toValue(5),
toValue(10)
]
})
// Register a TypeScript function const vm = new VM(bytecode)
// Register a native function
vm.registerFunction('add', (a, b) => { vm.registerFunction('add', (a, b) => {
return toValue(toNumber(a) + toNumber(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 () => { test("CALL_NATIVE - function with string manipulation", async () => {
const vm = new VM({ const bytecode = toBytecode(`
instructions: [ PUSH "hello"
{ op: OpCode.PUSH, operand: 0 }, // push "hello" PUSH "world"
{ op: OpCode.PUSH, operand: 1 }, // push "world" CALL_NATIVE concat
{ op: OpCode.CALL_NATIVE, operand: 'concat' }, // call TypeScript 'concat' `)
{ op: OpCode.HALT }
], const vm = new VM(bytecode)
constants: [
toValue("hello"),
toValue("world")
]
})
vm.registerFunction('concat', (a, b) => { vm.registerFunction('concat', (a, b) => {
const aStr = a.type === 'string' ? a.value : toString(a) 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 () => { test("CALL_NATIVE - async function", async () => {
const vm = new VM({ const bytecode = toBytecode(`
instructions: [ PUSH 42
{ op: OpCode.PUSH, operand: 0 }, // push 42 CALL_NATIVE asyncDouble
{ op: OpCode.CALL_NATIVE, operand: 'asyncDouble' }, // call async TypeScript function `)
{ op: OpCode.HALT }
], const vm = new VM(bytecode)
constants: [
toValue(42)
]
})
vm.registerFunction('asyncDouble', async (a) => { vm.registerFunction('asyncDouble', async (a) => {
// Simulate async operation // Simulate async operation
@ -73,13 +59,11 @@ test("CALL_NATIVE - async function", async () => {
}) })
test("CALL_NATIVE - function with no arguments", async () => { test("CALL_NATIVE - function with no arguments", async () => {
const vm = new VM({ const bytecode = toBytecode(`
instructions: [ CALL_NATIVE getAnswer
{ op: OpCode.CALL_NATIVE, operand: 'getAnswer' }, // call with empty stack `)
{ op: OpCode.HALT }
], const vm = new VM(bytecode)
constants: []
})
vm.registerFunction('getAnswer', () => { vm.registerFunction('getAnswer', () => {
return toValue(42) return toValue(42)
@ -90,20 +74,14 @@ test("CALL_NATIVE - function with no arguments", async () => {
}) })
test("CALL_NATIVE - function with multiple arguments", async () => { test("CALL_NATIVE - function with multiple arguments", async () => {
const vm = new VM({ const bytecode = toBytecode(`
instructions: [ PUSH 2
{ op: OpCode.PUSH, operand: 0 }, // push 2 PUSH 3
{ op: OpCode.PUSH, operand: 1 }, // push 3 PUSH 4
{ op: OpCode.PUSH, operand: 2 }, // push 4 CALL_NATIVE sum
{ op: OpCode.CALL_NATIVE, operand: 'sum' }, // call TypeScript 'sum' `)
{ op: OpCode.HALT }
], const vm = new VM(bytecode)
constants: [
toValue(2),
toValue(3),
toValue(4)
]
})
vm.registerFunction('sum', (...args) => { vm.registerFunction('sum', (...args) => {
const total = args.reduce((acc, val) => acc + toNumber(val), 0) 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 () => { test("CALL_NATIVE - function returns array", async () => {
const vm = new VM({ const bytecode = toBytecode(`
instructions: [ PUSH 3
{ op: OpCode.PUSH, operand: 0 }, // push 3 CALL_NATIVE makeRange
{ op: OpCode.CALL_NATIVE, operand: 'makeRange' }, // call TypeScript 'makeRange' `)
{ op: OpCode.HALT }
], const vm = new VM(bytecode)
constants: [
toValue(3)
]
})
vm.registerFunction('makeRange', (n) => { vm.registerFunction('makeRange', (n) => {
const count = toNumber(n) const count = toNumber(n)
@ -148,30 +122,24 @@ test("CALL_NATIVE - function returns array", async () => {
}) })
test("CALL_NATIVE - function not found", async () => { test("CALL_NATIVE - function not found", async () => {
const vm = new VM({ const bytecode = toBytecode(`
instructions: [ CALL_NATIVE nonexistent
{ op: OpCode.CALL_NATIVE, operand: 'nonexistent' } `)
],
constants: [] const vm = new VM(bytecode)
})
await expect(vm.run()).rejects.toThrow('CALL_NATIVE: function not found: nonexistent') await expect(vm.run()).rejects.toThrow('CALL_NATIVE: function not found: nonexistent')
}) })
test("CALL_NATIVE - using result in subsequent operations", async () => { test("CALL_NATIVE - using result in subsequent operations", async () => {
const vm = new VM({ const bytecode = toBytecode(`
instructions: [ PUSH 5
{ op: OpCode.PUSH, operand: 0 }, // push 5 CALL_NATIVE triple
{ op: OpCode.CALL_NATIVE, operand: 'triple' }, // call TypeScript 'triple' -> 15 PUSH 10
{ op: OpCode.PUSH, operand: 1 }, // push 10 ADD
{ op: OpCode.ADD }, // 15 + 10 = 25 `)
{ op: OpCode.HALT }
], const vm = new VM(bytecode)
constants: [
toValue(5),
toValue(10)
]
})
vm.registerFunction('triple', (n) => { vm.registerFunction('triple', (n) => {
return toValue(toNumber(n) * 3) return toValue(toNumber(n) * 3)