update spec

This commit is contained in:
Chris Wanstrath 2025-10-05 21:36:48 -07:00
parent 8c187a89aa
commit 0270424f9b

83
SPEC.md
View File

@ -38,8 +38,8 @@ type Value =
| { type: 'string', value: string }
| { type: 'array', value: Value[] }
| { type: 'dict', value: Map<string, Value> }
| { type: 'function', params: string[], defaults: Record<string, Value>,
body: number, parentScope: Scope, variadic: boolean, kwargs: boolean }
| { type: 'function', params: string[], defaults: Record<string, number>,
body: number, parentScope: Scope, variadic: boolean, named: boolean }
```
### Type Coercion
@ -67,7 +67,7 @@ type Instruction = {
type Constant =
| Value
| { type: 'function_def', params: string[], defaults: Record<string, number>,
body: number, variadic: boolean, kwargs: boolean }
body: number, variadic: boolean, named: boolean }
```
## Scope Chain
@ -269,19 +269,19 @@ Registers a try block. If THROW occurs before POP_TRY, execution jumps to catch
Adds a finally block to the current try/catch. The finally block will execute whether an exception is thrown or not.
#### POP_TRY
**Operand**: None
**Effect**: Pop exception handler (try block completed without exception)
**Stack**: No change
**Operand**: None
**Effect**: Pop exception handler (try block completed without exception)
**Stack**: No change
**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
2. 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
- The VM does NOT automatically jump to finally blocks on POP_TRY
- The compiler must explicitly generate JUMP instructions to finally blocks when the try block completes normally
- The compiler must ensure catch blocks also jump to finally when present
- Finally blocks should end with normal control flow (no special terminator needed)
#### THROW
@ -296,8 +296,12 @@ Adds a finally block to the current try/catch. The finally block will execute wh
4. Unwind call stack to handler's depth
5. Restore handler's scope
6. Push error value back onto stack
7. Jump to handler's catch address
8. **Note**: After catch block executes, compiler must jump to finally if present
7. If handler has `finallyAddress`, jump there; otherwise jump to `catchAddress`
**Notes**:
- When THROW jumps to finally (if present), the error value remains on stack for the finally block
- The compiler must structure catch/finally blocks appropriately to handle the error value
- If finally is present, the catch block is typically entered via a jump from the finally block or through explicit compiler-generated control flow
### Function Operations
@ -310,8 +314,8 @@ The constant must be a `function_def` with:
- `params`: Parameter names
- `defaults`: Map of param names to constant indices for default values
- `body`: Instruction address of function body
- `variadic`: If true, last param collects remaining positional args as array
- `kwargs`: If true, last param collects all named args as dict
- `variadic`: If true, second-to-last param (if `named` is also true) or last param collects remaining positional args as array
- `named`: If true, last param collects unmatched named args as dict
The created function captures `currentScope` as its `parentScope`.
@ -332,7 +336,7 @@ The created function captures `currentScope` as its `parentScope`.
9. Bind parameters:
- For regular functions: bind params by position, then by name, then defaults, then null
- For variadic functions: bind fixed params, collect rest into array
- For kwargs functions: bind fixed params by position/name, collect unmatched named args into dict
- For functions with `named: true`: bind fixed params by position/name, collect unmatched named args into dict
10. Set currentScope to new scope
11. Jump to function body
@ -344,8 +348,8 @@ The created function captures `currentScope` as its `parentScope`.
**Named Args Handling**:
- Named args that match fixed parameter names are bound to those params
- Remaining named args (that don't match any fixed param) are collected into `@kwargs` dict
- This allows flexible calling: `fn(x=10, y=20, extra=30)` where `extra` goes to kwargs
- If the function has `named: true`, remaining named args (that don't match any fixed param) are collected into the last parameter as a dict
- This allows flexible calling: `fn(x=10, y=20, extra=30)` where `extra` goes to the named args dict
**Errors**: Throws if top of stack is not a function
@ -510,14 +514,30 @@ skip_body:
### Try-Catch
```
PUSH_TRY catch_label
# try block
PUSH_TRY #3 ; Jump to catch block (3 instructions ahead)
; try block
POP_TRY
JUMP end_label
catch_label:
STORE 'errorVar' # Error is on stack
# catch block
end_label:
JUMP #2 ; Jump past catch block
; catch:
STORE 'errorVar' ; Error is on stack
; catch block
; end:
```
### Try-Catch-Finally
```
PUSH_TRY #4 ; Jump to catch block (4 instructions ahead)
PUSH_FINALLY #7 ; Jump to finally block (7 instructions ahead)
; try block
POP_TRY
JUMP #5 ; Jump to finally
; catch:
STORE 'errorVar' ; Error is on stack
; catch block
JUMP #2 ; Jump to finally
; finally:
; finally block (executes in both cases)
; end:
```
### Named Function Call
@ -601,7 +621,7 @@ All of these should throw errors:
### Function Parameter Binding
- Missing positional args → use named args → use defaults → use null
- Extra positional args → collected by variadic parameter or ignored
- Extra named args → collected by kwargs parameter or ignored
- Extra named args → collected by named args parameter (if `named: true`) or ignored
- Named arg matching is case-sensitive
### Tail Call Optimization
@ -615,11 +635,12 @@ All of these should throw errors:
- CONTINUE is implemented by the compiler using JUMPs
### Exception Unwinding
- THROW unwinds call stack to handler's depth, not just to handler
- THROW unwinds call stack to handler's depth
- Exception handlers form a stack (nested try blocks)
- 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)
- Error value on stack is available in catch/finally blocks
- When THROW occurs and handler has finallyAddress, VM jumps to finally first
- Compiler is responsible for structuring control flow so finally executes in all cases
- Finally typically executes after try (if no exception) or after catch (if exception), but control flow is compiler-managed
## VM Initialization
@ -643,7 +664,7 @@ const result = await vm.execute()
6. **Break/continue** (nested functions, iterator pattern)
7. **Closures** (capturing variables, multiple nesting levels)
8. **Tail calls** (self-recursive, mutual recursion)
9. **Parameter binding** (positional, named, defaults, variadic, kwargs, combinations)
9. **Parameter binding** (positional, named, defaults, variadic, named args collection, combinations)
10. **Array/dict operations** (creation, access, mutation)
11. **Error conditions** (all error cases listed above)
12. **Edge cases** (empty stack, null values, shadowing, etc.)
@ -655,7 +676,7 @@ const result = await vm.execute()
3. **Closure examples** (counters, adder factories)
4. **Exception examples** (try/catch/throw chains)
5. **Complex scope** (deeply nested functions)
6. **Mixed features** (variadic + defaults + kwargs)
6. **Mixed features** (variadic + defaults + named args)
### Property-Based Tests Should Cover