finally blocks

This commit is contained in:
Chris Wanstrath 2025-10-05 17:59:02 -07:00
parent 2da24ccd32
commit 0a4e6ceef6
6 changed files with 193 additions and 14 deletions

View File

@ -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
View File

@ -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)

View File

@ -1,7 +1,8 @@
import { Scope } from "./scope" 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
callStackDepth: number // Call stack depth when handler was pushed finallyAddress?: number // Where to jump for `finally` block
scope: Scope // Scope to restore when catching callStackDepth: number // Call stack depth when handler was pushed
scope: Scope // Scope to restore when catching
} }

View File

@ -35,6 +35,7 @@ export enum OpCode {
// exception handling // exception handling
PUSH_TRY, PUSH_TRY,
PUSH_FINALLY,
POP_TRY, POP_TRY,
THROW, THROW,

View File

@ -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:

View File

@ -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')
})