try / catch / throw

This commit is contained in:
Chris Wanstrath 2025-10-05 15:50:37 -07:00
parent 25ed12b3ce
commit 2da24ccd32
3 changed files with 217 additions and 6 deletions

View File

@ -46,9 +46,9 @@ It's where Shrimp live.
- [x] CONTINUE - [x] CONTINUE
### Exception Handling ### Exception Handling
- [ ] PUSH_TRY - [x] PUSH_TRY
- [ ] POP_TRY - [x] POP_TRY
- [ ] THROW - [x] THROW
### Functions ### Functions
- [x] MAKE_FUNCTION - [x] MAKE_FUNCTION
@ -76,7 +76,7 @@ It's where Shrimp live.
## Test Status ## Test Status
**46 tests passing** covering: **53 tests passing** covering:
- All stack operations (PUSH, POP, DUP) - All stack operations (PUSH, POP, DUP)
- All arithmetic operations (ADD, SUB, MUL, DIV, MOD) - All arithmetic operations (ADD, SUB, MUL, DIV, MOD)
- All comparison operations (EQ, NEQ, LT, GT, LTE, GTE) - All comparison operations (EQ, NEQ, LT, GT, LTE, GTE)
@ -86,6 +86,7 @@ It's where Shrimp live.
- All array operations (MAKE_ARRAY, ARRAY_GET, ARRAY_SET, ARRAY_PUSH, ARRAY_LEN) - All array operations (MAKE_ARRAY, ARRAY_GET, ARRAY_SET, ARRAY_PUSH, ARRAY_LEN)
- All dictionary operations (MAKE_DICT, DICT_GET, DICT_SET, DICT_HAS) - All dictionary operations (MAKE_DICT, DICT_GET, DICT_SET, DICT_HAS)
- Basic function operations (MAKE_FUNCTION, CALL, RETURN) with parameter binding - Basic function operations (MAKE_FUNCTION, CALL, RETURN) with parameter binding
- Exception handling (PUSH_TRY, POP_TRY, THROW) with nested try blocks and call stack unwinding
- HALT instruction - HALT instruction
## Design Decisions ## Design Decisions
@ -95,6 +96,5 @@ It's where Shrimp live.
- **Short-circuiting via compiler**: No AND/OR opcodes—compilers use JUMP patterns for proper short-circuit evaluation - **Short-circuiting via compiler**: No AND/OR opcodes—compilers use JUMP patterns for proper short-circuit evaluation
🚧 **Still TODO**: 🚧 **Still TODO**:
- Exception handling (PUSH_TRY, POP_TRY, THROW)
- Advanced function features (TAIL_CALL, variadic params, kwargs, default parameters) - Advanced function features (TAIL_CALL, variadic params, kwargs, default parameters)
- TypeScript interop (CALL_TYPESCRIPT) - TypeScript interop (CALL_TYPESCRIPT)

View File

@ -141,7 +141,7 @@ export class VM {
case OpCode.BREAK: case OpCode.BREAK:
// Unwind call stack until we find a frame marked as break target // Unwind call stack until we find a frame marked as break target
while (this.callStack.length > 0) { while (this.callStack.length) {
const frame = this.callStack.pop()! const frame = this.callStack.pop()!
this.scope = frame.returnScope this.scope = frame.returnScope
this.pc = frame.returnAddress this.pc = frame.returnAddress
@ -177,6 +177,43 @@ export class VM {
this.pc = continueFrame.continueAddress! - 1 // -1 because PC will be incremented this.pc = continueFrame.continueAddress! - 1 // -1 because PC will be incremented
break break
case OpCode.PUSH_TRY:
const catchAddress = instruction.operand as number
this.exceptionHandlers.push({
catchAddress,
callStackDepth: this.callStack.length,
scope: this.scope
})
break
case OpCode.POP_TRY:
if (this.exceptionHandlers.length === 0)
throw new Error('POP_TRY: no exception handler to pop')
this.exceptionHandlers.pop()
break
case OpCode.THROW:
const errorValue = this.stack.pop()!
if (this.exceptionHandlers.length === 0) {
const errorMsg = toString(errorValue)
throw new Error(`Uncaught exception: ${errorMsg}`)
}
const handler = this.exceptionHandlers.pop()!
while (this.callStack.length > handler.callStackDepth)
this.callStack.pop()
this.scope = handler.scope
this.stack.push(errorValue)
// subtract 1 because pc was incremented
this.pc = handler.catchAddress - 1
break
case OpCode.MAKE_ARRAY: case OpCode.MAKE_ARRAY:
const arraySize = instruction.operand as number const arraySize = instruction.operand as number
const items: Value[] = [] const items: Value[] = []

174
tests/exceptions.test.ts Normal file
View File

@ -0,0 +1,174 @@
import { test, expect } from "bun:test"
import { VM } from "#vm"
import { OpCode } from "#opcode"
import { toValue } from "#value"
test("PUSH_TRY and POP_TRY - no exception thrown", async () => {
// Try block that completes successfully
const vm = new VM({
instructions: [
{ op: OpCode.PUSH_TRY, operand: 5 }, // catch at instruction 5
{ op: OpCode.PUSH, operand: 0 }, // push 42
{ op: OpCode.PUSH, operand: 1 }, // push 10
{ op: OpCode.ADD },
{ op: OpCode.POP_TRY }, // pop handler (no exception)
{ op: OpCode.HALT },
// Catch block (not executed)
{ op: OpCode.PUSH, operand: 2 }, // push 999
{ op: OpCode.HALT }
],
constants: [
toValue(42),
toValue(10),
toValue(999)
]
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 52 })
})
test("THROW - catch exception with error value", async () => {
// Try block that throws an exception
const vm = new VM({
instructions: [
{ op: OpCode.PUSH_TRY, operand: 5 }, // catch at instruction 5
{ op: OpCode.PUSH, operand: 0 }, // push error message
{ op: OpCode.THROW }, // throw exception
{ op: OpCode.PUSH, operand: 1 }, // not executed
{ op: OpCode.HALT },
// Catch block (instruction 5)
{ op: OpCode.HALT } // error value is on stack
],
constants: [
toValue('error occurred'),
toValue(999)
]
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'error occurred' })
})
test("THROW - uncaught exception throws JS error", async () => {
const vm = new VM({
instructions: [
{ op: OpCode.PUSH, operand: 0 },
{ op: OpCode.THROW }
],
constants: [
toValue('something went wrong')
]
})
await expect(vm.run()).rejects.toThrow('Uncaught exception: something went wrong')
})
test("THROW - exception with nested try blocks", async () => {
// Nested try blocks, inner one catches
const vm = new VM({
instructions: [
{ op: OpCode.PUSH_TRY, operand: 10 }, // outer catch at 10
{ op: OpCode.PUSH_TRY, operand: 6 }, // inner catch at 6
{ op: OpCode.PUSH, operand: 0 }, // push 'inner error'
{ op: OpCode.THROW }, // throw
{ op: OpCode.PUSH, operand: 1 }, // not executed
{ op: OpCode.HALT },
// Inner catch block (instruction 6)
{ op: OpCode.STORE, operand: 'err' },
{ op: OpCode.POP_TRY }, // pop outer handler
{ op: OpCode.LOAD, operand: 'err' },
{ op: OpCode.HALT },
// Outer catch block (instruction 10, not executed)
{ op: OpCode.PUSH, operand: 2 },
{ op: OpCode.HALT }
],
constants: [
toValue('inner error'),
toValue(999),
toValue('outer error')
]
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'inner error' })
})
test("THROW - exception skips outer handler", async () => {
// Nested try blocks, inner doesn't catch, outer does
const vm = new VM({
instructions: [
{ op: OpCode.PUSH_TRY, operand: 8 }, // outer catch at 8
{ op: OpCode.PUSH_TRY, operand: 6 }, // inner catch at 6
{ op: OpCode.PUSH, operand: 0 }, // push error
{ op: OpCode.THROW }, // throw
{ op: OpCode.HALT },
// Inner catch block (instruction 6) - re-throws
{ op: OpCode.THROW }, // re-throw the error
// Outer catch block (instruction 8)
{ op: OpCode.HALT }
],
constants: [
toValue('error message')
]
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'error message' })
})
test("THROW - exception unwinds call stack", async () => {
// Function that throws, caller has try/catch
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 vm = new VM({
instructions: [
{ op: OpCode.POP_TRY }
],
constants: []
})
await expect(vm.run()).rejects.toThrow('POP_TRY: no exception handler to pop')
})