forked from defunkt/ReefVM
210 lines
4.7 KiB
TypeScript
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,
|
|
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 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')
|
|
})
|