diff --git a/src/vm.ts b/src/vm.ts index fcd8db0..30e1267 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -562,9 +562,37 @@ export class VM { } // Call the native function with bound args - const result = await fn.fn.call(this, ...nativeArgs) - this.stack.push(result) - break + try { + const result = await fn.fn.call(this, ...nativeArgs) + 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') diff --git a/tests/native.test.ts b/tests/native.test.ts index 4b43e7c..410edf3 100644 --- a/tests/native.test.ts +++ b/tests/native.test.ts @@ -2201,4 +2201,163 @@ test("raw flag - raw function can return any Value type", async () => { expect(numbers.value).toEqual([toValue(1), toValue(2), toValue(3)]) } } +}) + +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') }) \ No newline at end of file