convert native exceptions to shrimp exceptions

This commit is contained in:
Chris Wanstrath 2025-10-29 15:07:59 -07:00
parent 3647159286
commit b58f848a65
2 changed files with 190 additions and 3 deletions

View File

@ -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')
})