Compare commits

...

2 Commits

Author SHA1 Message Date
b58f848a65 convert native exceptions to shrimp exceptions 2025-10-29 15:07:59 -07:00
3647159286 remove old imports 2025-10-29 14:57:28 -07:00
2 changed files with 191 additions and 4 deletions

View File

@ -5,7 +5,7 @@ import { OpCode } from "./opcode"
import { Scope } from "./scope" import { Scope } from "./scope"
import type { Value, NativeFunction, TypeScriptFunction } from "./value" import type { Value, NativeFunction, TypeScriptFunction } from "./value"
import { toValue, toNumber, isTrue, isEqual, toString, fromValue, fnFromValue } from "./value" import { toValue, toNumber, isTrue, isEqual, toString, fromValue, fnFromValue } from "./value"
import { extractParamInfo, wrapNative, isWrapped, getOriginalFunction } from "./function" import { extractParamInfo, getOriginalFunction } from "./function"
export class VM { export class VM {
pc = 0 pc = 0
@ -562,9 +562,37 @@ export class VM {
} }
// Call the native function with bound args // Call the native function with bound args
const result = await fn.fn.call(this, ...nativeArgs) try {
this.stack.push(result) const result = await fn.fn.call(this, ...nativeArgs)
break this.stack.push(result)
break
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const errorValue = toValue(errorMessage)
// no exception handlers, let it crash
if (this.exceptionHandlers.length === 0) {
throw new Error(`Uncaught exception in native function: ${errorMessage}`)
}
// use existing THROW logic
const throwHandler = this.exceptionHandlers.pop()!
while (this.callStack.length > throwHandler.callStackDepth)
this.callStack.pop()
this.scope = throwHandler.scope
this.stack.push(errorValue)
// Jump to `finally` if present, otherwise jump to `catch`
const targetAddress = throwHandler.finallyAddress !== undefined
? throwHandler.finallyAddress
: throwHandler.catchAddress
// subtract 1 because pc will be incremented
this.pc = targetAddress - 1
break
}
} }
if (fn.type !== 'function') if (fn.type !== 'function')

View File

@ -2202,3 +2202,162 @@ test("raw flag - raw function can return any Value type", async () => {
} }
} }
}) })
test('native function error caught by try/catch - direct call', async () => {
const bytecode = toBytecode(`
PUSH_TRY .catch
LOAD failing_fn
PUSH 0
PUSH 0
CALL
POP_TRY
JUMP .end
.catch:
STORE err
PUSH 'caught: '
LOAD err
STR_CONCAT #2
JUMP .end
.end:
HALT
`)
const vm = new VM(bytecode)
vm.set('failing_fn', () => {
throw new Error('native error')
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'caught: native error' })
})
test('native function error caught by try/catch - inside reef function', async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION () .try_body
STORE call_native
JUMP .after_try_body
.try_body:
LOAD failing_fn
PUSH 0
PUSH 0
CALL
RETURN
.after_try_body:
PUSH_TRY .catch
LOAD call_native
PUSH 0
PUSH 0
CALL
POP_TRY
JUMP .end
.catch:
STORE err
PUSH 'caught: '
LOAD err
STR_CONCAT #2
JUMP .end
.end:
HALT
`)
const vm = new VM(bytecode)
vm.set('failing_fn', () => {
throw new Error('native error')
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'caught: native error' })
})
test('async native function error caught by try/catch', async () => {
const bytecode = toBytecode(`
PUSH_TRY .catch
LOAD async_fail
PUSH 0
PUSH 0
CALL
POP_TRY
JUMP .end
.catch:
STORE err
PUSH 'async caught: '
LOAD err
STR_CONCAT #2
JUMP .end
.end:
HALT
`)
const vm = new VM(bytecode)
vm.set('async_fail', async () => {
await new Promise(resolve => setTimeout(resolve, 1))
throw new Error('async error')
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'async caught: async error' })
})
test('native function error with finally block', async () => {
const bytecode = toBytecode(`
PUSH 0
STORE cleanup_count
PUSH_TRY .catch
PUSH_FINALLY .finally
LOAD failing_fn
PUSH 0
PUSH 0
CALL
POP_TRY
JUMP .finally
.catch:
STORE err
PUSH 'error handled'
JUMP .finally
.finally:
LOAD cleanup_count
PUSH 1
ADD
STORE cleanup_count
HALT
`)
const vm = new VM(bytecode)
vm.set('failing_fn', () => {
throw new Error('native error')
})
await vm.run()
const cleanupCount = vm.scope.get('cleanup_count')
expect(cleanupCount).toEqual({ type: 'number', value: 1 })
})
test('uncaught native function error crashes VM', async () => {
const bytecode = toBytecode(`
LOAD failing_fn
PUSH 0
PUSH 0
CALL
HALT
`)
const vm = new VM(bytecode)
vm.set('failing_fn', () => {
throw new Error('uncaught error')
})
await expect(vm.run()).rejects.toThrow('Uncaught exception in native function: uncaught error')
})