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
|
- [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)
|
||||||
39
src/vm.ts
39
src/vm.ts
|
|
@ -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
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