ReefVM/tests/exceptions.test.ts
2025-10-05 20:43:17 -07:00

210 lines
4.7 KiB
TypeScript

import { test, expect } from "bun:test"
import { toBytecode } from "#bytecode"
import { run } from "#index"
test("PUSH_TRY and POP_TRY - no exception thrown", async () => {
// Try block that completes successfully
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 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 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 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 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
{ 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,
named: 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 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 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 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 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 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 str = `
PUSH_FINALLY #5
`
await expect(run(toBytecode(str))).rejects.toThrow('PUSH_FINALLY: no exception handler to modify')
})