try / catch / throw
This commit is contained in:
parent
25ed12b3ce
commit
2da24ccd32
10
README.md
10
README.md
|
|
@ -46,9 +46,9 @@ It's where Shrimp live.
|
|||
- [x] CONTINUE
|
||||
|
||||
### Exception Handling
|
||||
- [ ] PUSH_TRY
|
||||
- [ ] POP_TRY
|
||||
- [ ] THROW
|
||||
- [x] PUSH_TRY
|
||||
- [x] POP_TRY
|
||||
- [x] THROW
|
||||
|
||||
### Functions
|
||||
- [x] MAKE_FUNCTION
|
||||
|
|
@ -76,7 +76,7 @@ It's where Shrimp live.
|
|||
|
||||
## Test Status
|
||||
|
||||
✅ **46 tests passing** covering:
|
||||
✅ **53 tests passing** covering:
|
||||
- All stack operations (PUSH, POP, DUP)
|
||||
- All arithmetic operations (ADD, SUB, MUL, DIV, MOD)
|
||||
- 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 dictionary operations (MAKE_DICT, DICT_GET, DICT_SET, DICT_HAS)
|
||||
- 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
|
||||
|
||||
## 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
|
||||
|
||||
🚧 **Still TODO**:
|
||||
- Exception handling (PUSH_TRY, POP_TRY, THROW)
|
||||
- Advanced function features (TAIL_CALL, variadic params, kwargs, default parameters)
|
||||
- TypeScript interop (CALL_TYPESCRIPT)
|
||||
39
src/vm.ts
39
src/vm.ts
|
|
@ -141,7 +141,7 @@ export class VM {
|
|||
|
||||
case OpCode.BREAK:
|
||||
// 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()!
|
||||
this.scope = frame.returnScope
|
||||
this.pc = frame.returnAddress
|
||||
|
|
@ -177,6 +177,43 @@ export class VM {
|
|||
this.pc = continueFrame.continueAddress! - 1 // -1 because PC will be incremented
|
||||
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:
|
||||
const arraySize = instruction.operand as number
|
||||
const items: Value[] = []
|
||||
|
|
|
|||
174
tests/exceptions.test.ts
Normal file
174
tests/exceptions.test.ts
Normal 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')
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user