ReefVM/tests/exceptions.test.ts
2025-10-05 17:59:02 -07:00

316 lines
9.8 KiB
TypeScript

import { test, expect } from "bun:test"
import { VM } from "#vm"
import { OpCode } from "#opcode"
import { toValue } from "#value"
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 })
})
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' })
})
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')
})
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' })
})
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' })
})
test("THROW - exception unwinds call stack", async () => {
// Function that throws, caller has try/catch
const vm = new VM({
instructions: [
// 0: Main code with try block
{ op: OpCode.PUSH_TRY, operand: 6 },
{ op: OpCode.MAKE_FUNCTION, operand: 0 },
{ op: OpCode.CALL, operand: 0 },
{ op: OpCode.POP_TRY },
{ op: OpCode.HALT },
// 5: Not executed
{ op: OpCode.PUSH, operand: 2 },
// 6: Catch block
{ op: OpCode.HALT }, // error value on stack
// 7: Function body (throws)
{ op: OpCode.PUSH, operand: 1 },
{ op: OpCode.THROW }
],
constants: [
{
type: 'function_def',
params: [],
defaults: {},
body: 7,
variadic: false,
kwargs: false
},
toValue('function error'),
toValue(999)
]
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'function error' })
})
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')
})
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 })
})
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' })
})
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 })
})
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 })
})
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')
})