forked from defunkt/ReefVM
finally blocks
This commit is contained in:
parent
2da24ccd32
commit
0a4e6ceef6
|
|
@ -47,6 +47,7 @@ It's where Shrimp live.
|
||||||
|
|
||||||
### Exception Handling
|
### Exception Handling
|
||||||
- [x] PUSH_TRY
|
- [x] PUSH_TRY
|
||||||
|
- [x] PUSH_FINALLY
|
||||||
- [x] POP_TRY
|
- [x] POP_TRY
|
||||||
- [x] THROW
|
- [x] THROW
|
||||||
|
|
||||||
|
|
@ -76,7 +77,7 @@ It's where Shrimp live.
|
||||||
|
|
||||||
## Test Status
|
## Test Status
|
||||||
|
|
||||||
✅ **53 tests passing** covering:
|
✅ **58 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,7 +87,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
|
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
|
||||||
- HALT instruction
|
- HALT instruction
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
|
|
|
||||||
30
SPEC.md
30
SPEC.md
|
|
@ -109,6 +109,7 @@ type CallFrame = {
|
||||||
```typescript
|
```typescript
|
||||||
type ExceptionHandler = {
|
type ExceptionHandler = {
|
||||||
catchAddress: number // Where to jump on exception
|
catchAddress: number // Where to jump on exception
|
||||||
|
finallyAddress?: number // Where to jump for finally block (always runs)
|
||||||
callStackDepth: number // Call stack depth when handler pushed
|
callStackDepth: number // Call stack depth when handler pushed
|
||||||
scope: Scope // Scope to restore in catch block
|
scope: Scope // Scope to restore in catch block
|
||||||
}
|
}
|
||||||
|
|
@ -263,18 +264,36 @@ end:
|
||||||
### Exception Handling
|
### Exception Handling
|
||||||
|
|
||||||
#### PUSH_TRY
|
#### PUSH_TRY
|
||||||
**Operand**: Catch block address (number)
|
**Operand**: Catch block offset (number)
|
||||||
**Effect**: Push exception handler
|
**Effect**: Push exception handler
|
||||||
**Stack**: No change
|
**Stack**: No change
|
||||||
|
|
||||||
Registers a try block. If THROW occurs before POP_TRY, execution jumps to catch address.
|
Registers a try block. If THROW occurs before POP_TRY, execution jumps to catch address.
|
||||||
|
|
||||||
|
#### PUSH_FINALLY
|
||||||
|
**Operand**: Finally block offset (number)
|
||||||
|
**Effect**: Add finally address to most recent exception handler
|
||||||
|
**Stack**: No change
|
||||||
|
**Errors**: Throws if no exception handler to modify
|
||||||
|
|
||||||
|
Adds a finally block to the current try/catch. The finally block will execute whether an exception is thrown or not.
|
||||||
|
|
||||||
#### POP_TRY
|
#### POP_TRY
|
||||||
**Operand**: None
|
**Operand**: None
|
||||||
**Effect**: Pop exception handler (try block completed without exception)
|
**Effect**: Pop exception handler (try block completed without exception)
|
||||||
**Stack**: No change
|
**Stack**: No change
|
||||||
**Errors**: Throws if no handler to pop
|
**Errors**: Throws if no handler to pop
|
||||||
|
|
||||||
|
**Behavior**:
|
||||||
|
1. Pop exception handler
|
||||||
|
2. If handler has `finallyAddress`, jump there
|
||||||
|
3. Otherwise continue to next instruction
|
||||||
|
|
||||||
|
**Notes**:
|
||||||
|
- The VM ensures finally runs when try completes normally
|
||||||
|
- The compiler must ensure catch blocks jump to finally when present
|
||||||
|
- Finally blocks should end with normal control flow (no special terminator needed)
|
||||||
|
|
||||||
#### THROW
|
#### THROW
|
||||||
**Operand**: None
|
**Operand**: None
|
||||||
**Effect**: Throw exception with error value from stack
|
**Effect**: Throw exception with error value from stack
|
||||||
|
|
@ -288,6 +307,7 @@ Registers a try block. If THROW occurs before POP_TRY, execution jumps to catch
|
||||||
5. Restore handler's scope
|
5. Restore handler's scope
|
||||||
6. Push error value back onto stack
|
6. Push error value back onto stack
|
||||||
7. Jump to handler's catch address
|
7. Jump to handler's catch address
|
||||||
|
8. **Note**: After catch block executes, compiler must jump to finally if present
|
||||||
|
|
||||||
### Function Operations
|
### Function Operations
|
||||||
|
|
||||||
|
|
@ -495,10 +515,10 @@ skip_body:
|
||||||
|
|
||||||
### Try-Catch
|
### Try-Catch
|
||||||
```
|
```
|
||||||
PUSH_TRY N # catch is N instructions ahead
|
PUSH_TRY catch_label
|
||||||
# try block
|
# try block
|
||||||
POP_TRY
|
POP_TRY
|
||||||
JUMP M # skip catch block
|
JUMP end_label
|
||||||
catch_label:
|
catch_label:
|
||||||
STORE 'errorVar' # Error is on stack
|
STORE 'errorVar' # Error is on stack
|
||||||
# catch block
|
# catch block
|
||||||
|
|
@ -597,6 +617,8 @@ All of these should throw errors:
|
||||||
- THROW unwinds call stack to handler's depth, not just to handler
|
- THROW unwinds call stack to handler's depth, not just to handler
|
||||||
- Exception handlers form a stack (nested try blocks)
|
- Exception handlers form a stack (nested try blocks)
|
||||||
- Error value on stack is available in catch block via STORE
|
- Error value on stack is available in catch block via STORE
|
||||||
|
- Finally blocks always execute, even if there's a return/break in try or catch
|
||||||
|
- Finally executes after try (if no exception) or after catch (if exception)
|
||||||
|
|
||||||
## VM Initialization
|
## VM Initialization
|
||||||
|
|
||||||
|
|
@ -616,7 +638,7 @@ const result = await vm.execute()
|
||||||
2. **Type coercion** for arithmetic, comparison, and logical ops
|
2. **Type coercion** for arithmetic, comparison, and logical ops
|
||||||
3. **Scope chain** resolution (local, parent, global)
|
3. **Scope chain** resolution (local, parent, global)
|
||||||
4. **Call frames** (nested calls, return values)
|
4. **Call frames** (nested calls, return values)
|
||||||
5. **Exception handling** (nested try blocks, unwinding)
|
5. **Exception handling** (nested try blocks, unwinding, finally blocks)
|
||||||
6. **Break/continue** (nested functions, iterator pattern)
|
6. **Break/continue** (nested functions, iterator pattern)
|
||||||
7. **Closures** (capturing variables, multiple nesting levels)
|
7. **Closures** (capturing variables, multiple nesting levels)
|
||||||
8. **Tail calls** (self-recursive, mutual recursion)
|
8. **Tail calls** (self-recursive, mutual recursion)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Scope } from "./scope"
|
||||||
|
|
||||||
export type ExceptionHandler = {
|
export type ExceptionHandler = {
|
||||||
catchAddress: number // Where to jump when exception is caught
|
catchAddress: number // Where to jump when exception is caught
|
||||||
|
finallyAddress?: number // Where to jump for `finally` block
|
||||||
callStackDepth: number // Call stack depth when handler was pushed
|
callStackDepth: number // Call stack depth when handler was pushed
|
||||||
scope: Scope // Scope to restore when catching
|
scope: Scope // Scope to restore when catching
|
||||||
}
|
}
|
||||||
|
|
@ -35,6 +35,7 @@ export enum OpCode {
|
||||||
|
|
||||||
// exception handling
|
// exception handling
|
||||||
PUSH_TRY,
|
PUSH_TRY,
|
||||||
|
PUSH_FINALLY,
|
||||||
POP_TRY,
|
POP_TRY,
|
||||||
THROW,
|
THROW,
|
||||||
|
|
||||||
|
|
|
||||||
23
src/vm.ts
23
src/vm.ts
|
|
@ -186,6 +186,14 @@ export class VM {
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case OpCode.PUSH_FINALLY:
|
||||||
|
const finallyAddress = instruction.operand as number
|
||||||
|
if (this.exceptionHandlers.length === 0)
|
||||||
|
throw new Error('PUSH_FINALLY: no exception handler to modify')
|
||||||
|
|
||||||
|
this.exceptionHandlers[this.exceptionHandlers.length - 1]!.finallyAddress = finallyAddress
|
||||||
|
break
|
||||||
|
|
||||||
case OpCode.POP_TRY:
|
case OpCode.POP_TRY:
|
||||||
if (this.exceptionHandlers.length === 0)
|
if (this.exceptionHandlers.length === 0)
|
||||||
throw new Error('POP_TRY: no exception handler to pop')
|
throw new Error('POP_TRY: no exception handler to pop')
|
||||||
|
|
@ -201,17 +209,22 @@ export class VM {
|
||||||
throw new Error(`Uncaught exception: ${errorMsg}`)
|
throw new Error(`Uncaught exception: ${errorMsg}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handler = this.exceptionHandlers.pop()!
|
const throwHandler = this.exceptionHandlers.pop()!
|
||||||
|
|
||||||
while (this.callStack.length > handler.callStackDepth)
|
while (this.callStack.length > throwHandler.callStackDepth)
|
||||||
this.callStack.pop()
|
this.callStack.pop()
|
||||||
|
|
||||||
this.scope = handler.scope
|
this.scope = throwHandler.scope
|
||||||
|
|
||||||
this.stack.push(errorValue)
|
this.stack.push(errorValue)
|
||||||
|
|
||||||
// subtract 1 because pc was incremented
|
// Jump to finally if present, otherwise jump to catch
|
||||||
this.pc = handler.catchAddress - 1
|
const targetAddress = throwHandler.finallyAddress !== undefined
|
||||||
|
? throwHandler.finallyAddress
|
||||||
|
: throwHandler.catchAddress
|
||||||
|
|
||||||
|
// subtract 1 because pc will be incremented
|
||||||
|
this.pc = targetAddress - 1
|
||||||
break
|
break
|
||||||
|
|
||||||
case OpCode.MAKE_ARRAY:
|
case OpCode.MAKE_ARRAY:
|
||||||
|
|
|
||||||
|
|
@ -172,3 +172,144 @@ test("POP_TRY - error when no handler to pop", async () => {
|
||||||
|
|
||||||
await expect(vm.run()).rejects.toThrow('POP_TRY: no exception handler to pop')
|
await expect(vm.run()).rejects.toThrow('POP_TRY: no exception handler to pop')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("PUSH_FINALLY - finally executes after successful try", async () => {
|
||||||
|
// Try block completes normally, compiler jumps to finally
|
||||||
|
const vm = new VM({
|
||||||
|
instructions: [
|
||||||
|
{ op: OpCode.PUSH_TRY, operand: 8 }, // catch at 8
|
||||||
|
{ op: OpCode.PUSH_FINALLY, operand: 9 }, // finally at 9
|
||||||
|
{ op: OpCode.PUSH, operand: 0 }, // push 10
|
||||||
|
{ op: OpCode.STORE, operand: 'x' },
|
||||||
|
{ op: OpCode.POP_TRY },
|
||||||
|
{ op: OpCode.JUMP, operand: 3 }, // compiler jumps to finally (5->6, +3 = 9)
|
||||||
|
|
||||||
|
// Not executed
|
||||||
|
{ op: OpCode.HALT },
|
||||||
|
{ op: OpCode.HALT },
|
||||||
|
|
||||||
|
// Catch block (instruction 8) - not executed
|
||||||
|
{ op: OpCode.HALT },
|
||||||
|
|
||||||
|
// Finally block (instruction 9)
|
||||||
|
{ op: OpCode.PUSH, operand: 1 }, // push 100
|
||||||
|
{ op: OpCode.LOAD, operand: 'x' }, // load 10
|
||||||
|
{ op: OpCode.ADD }, // 110
|
||||||
|
{ op: OpCode.HALT }
|
||||||
|
],
|
||||||
|
constants: [
|
||||||
|
toValue(10),
|
||||||
|
toValue(100)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'number', value: 110 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("PUSH_FINALLY - finally executes after exception", async () => {
|
||||||
|
// Try block throws, THROW jumps to finally (skipping catch)
|
||||||
|
const vm = new VM({
|
||||||
|
instructions: [
|
||||||
|
{ op: OpCode.PUSH_TRY, operand: 5 }, // catch at 5
|
||||||
|
{ op: OpCode.PUSH_FINALLY, operand: 7 }, // finally at 7
|
||||||
|
{ op: OpCode.PUSH, operand: 0 }, // push error
|
||||||
|
{ op: OpCode.THROW }, // throw (jumps to finally, not catch!)
|
||||||
|
{ op: OpCode.HALT }, // not executed
|
||||||
|
|
||||||
|
// Catch block (instruction 5) - skipped because finally is present
|
||||||
|
{ op: OpCode.STORE, operand: 'err' },
|
||||||
|
{ op: OpCode.HALT },
|
||||||
|
|
||||||
|
// Finally block (instruction 7) - error is still on stack
|
||||||
|
{ op: OpCode.POP }, // discard error
|
||||||
|
{ op: OpCode.PUSH, operand: 1 }, // push "finally ran"
|
||||||
|
{ op: OpCode.HALT }
|
||||||
|
],
|
||||||
|
constants: [
|
||||||
|
toValue('error'),
|
||||||
|
toValue('finally ran')
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'finally ran' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("PUSH_FINALLY - finally without catch", async () => {
|
||||||
|
// Try-finally without catch (compiler generates jump to finally)
|
||||||
|
const vm = new VM({
|
||||||
|
instructions: [
|
||||||
|
{ op: OpCode.PUSH_TRY, operand: 7 }, // catch at 7 (dummy)
|
||||||
|
{ op: OpCode.PUSH_FINALLY, operand: 7 }, // finally at 7
|
||||||
|
{ op: OpCode.PUSH, operand: 0 }, // push 42
|
||||||
|
{ op: OpCode.STORE, operand: 'x' },
|
||||||
|
{ op: OpCode.POP_TRY },
|
||||||
|
{ op: OpCode.JUMP, operand: 1 }, // compiler jumps to finally
|
||||||
|
{ op: OpCode.HALT }, // skipped
|
||||||
|
|
||||||
|
// Finally block (instruction 7)
|
||||||
|
{ op: OpCode.LOAD, operand: 'x' }, // load 42
|
||||||
|
{ op: OpCode.PUSH, operand: 1 }, // push 10
|
||||||
|
{ op: OpCode.ADD }, // 52
|
||||||
|
{ op: OpCode.HALT }
|
||||||
|
],
|
||||||
|
constants: [
|
||||||
|
toValue(42),
|
||||||
|
toValue(10)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'number', value: 52 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("PUSH_FINALLY - nested try-finally blocks", async () => {
|
||||||
|
// Nested try-finally blocks with compiler-generated jumps
|
||||||
|
const vm = new VM({
|
||||||
|
instructions: [
|
||||||
|
{ op: OpCode.PUSH_TRY, operand: 11 }, // outer catch at 11
|
||||||
|
{ op: OpCode.PUSH_FINALLY, operand: 14 }, // outer finally at 14
|
||||||
|
{ op: OpCode.PUSH_TRY, operand: 9 }, // inner catch at 9
|
||||||
|
{ op: OpCode.PUSH_FINALLY, operand: 10 }, // inner finally at 10
|
||||||
|
{ op: OpCode.PUSH, operand: 0 }, // push 1
|
||||||
|
{ op: OpCode.POP_TRY }, // inner pop (instruction 5)
|
||||||
|
{ op: OpCode.JUMP, operand: 3 }, // jump to inner finally (6->7, +3 = 10)
|
||||||
|
{ op: OpCode.HALT }, // skipped
|
||||||
|
{ op: OpCode.HALT }, // skipped
|
||||||
|
|
||||||
|
// Inner catch (instruction 9) - not executed
|
||||||
|
{ op: OpCode.HALT },
|
||||||
|
|
||||||
|
// Inner finally (instruction 10)
|
||||||
|
{ op: OpCode.PUSH, operand: 1 }, // push 10
|
||||||
|
{ op: OpCode.POP_TRY }, // outer pop (instruction 11)
|
||||||
|
{ op: OpCode.JUMP, operand: 1 }, // jump to outer finally (12->13, +1 = 14)
|
||||||
|
|
||||||
|
// Outer catch (instruction 13) - not executed
|
||||||
|
{ op: OpCode.HALT },
|
||||||
|
|
||||||
|
// Outer finally (instruction 14)
|
||||||
|
{ op: OpCode.ADD }, // 1 + 10 = 11
|
||||||
|
{ op: OpCode.HALT }
|
||||||
|
],
|
||||||
|
constants: [
|
||||||
|
toValue(1),
|
||||||
|
toValue(10)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'number', value: 11 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("PUSH_FINALLY - error when no handler", async () => {
|
||||||
|
const vm = new VM({
|
||||||
|
instructions: [
|
||||||
|
{ op: OpCode.PUSH_FINALLY, operand: 5 }
|
||||||
|
],
|
||||||
|
constants: []
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(vm.run()).rejects.toThrow('PUSH_FINALLY: no exception handler to modify')
|
||||||
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user