From 0270424f9be96f39c767a6dd8aace0e55b89fbf3 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sun, 5 Oct 2025 21:36:48 -0700 Subject: [PATCH] update spec --- SPEC.md | 83 ++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 31 deletions(-) diff --git a/SPEC.md b/SPEC.md index e8dba5e..43f5149 100644 --- a/SPEC.md +++ b/SPEC.md @@ -38,8 +38,8 @@ type Value = | { type: 'string', value: string } | { type: 'array', value: Value[] } | { type: 'dict', value: Map } - | { type: 'function', params: string[], defaults: Record, - body: number, parentScope: Scope, variadic: boolean, kwargs: boolean } + | { type: 'function', params: string[], defaults: Record, + 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, - 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