From db4f332472a113dca7f29f5e09d440ee4440c830 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sun, 5 Oct 2025 21:07:36 -0700 Subject: [PATCH] variadic and named args --- README.md | 11 +- SPEC.md | 43 ++++---- src/vm.ts | 131 +++++++++++++++++----- tests/basic.test.ts | 18 ++- tests/bytecode.test.ts | 40 ++++--- tests/exceptions.test.ts | 17 +-- tests/functions.test.ts | 231 ++++++++++++++++++++++++++++++++++++--- tests/tail-call.test.ts | 46 +++++--- 8 files changed, 426 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index d2081f2..2457833 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ It's where Shrimp live. ## Test Status -✅ **83 tests passing** covering: +✅ **91 tests passing** covering: - All stack operations (PUSH, POP, DUP) - All arithmetic operations (ADD, SUB, MUL, DIV, MOD) - All comparison operations (EQ, NEQ, LT, GT, LTE, GTE) @@ -87,7 +87,9 @@ It's where Shrimp live. - All array operations (MAKE_ARRAY, ARRAY_GET, ARRAY_SET, ARRAY_PUSH, ARRAY_LEN) - All dictionary operations (MAKE_DICT, DICT_GET, DICT_SET, DICT_HAS) - Function operations (MAKE_FUNCTION, CALL, TAIL_CALL, RETURN) with parameter binding -- **Variadic functions** with positional rest parameters +- **Variadic functions** with positional rest parameters (`...rest`) +- **Named arguments (kwargs)** that collect unmatched named args into a dict (`@kwargs`) +- **Mixed positional and named arguments** with proper priority binding - **Tail call optimization** with unbounded recursion (10,000+ iterations without stack overflow) - Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding - Native function interop (CALL_NATIVE) with sync and async functions @@ -99,6 +101,5 @@ It's where Shrimp live. - **Simple truthiness**: Only `null` and `false` are falsy (unlike JavaScript where `0`, `""`, etc. are also falsy) - **Short-circuiting via compiler**: No AND/OR opcodes—compilers use JUMP patterns for proper short-circuit evaluation - **Variadic parameters**: Functions can collect remaining positional arguments into an array using `...rest` syntax - -🚧 **Still TODO**: -- Advanced function features (kwargs, named arguments in CALL) \ No newline at end of file +- **Named parameters (kwargs)**: Functions can collect unmatched named arguments into a dict using `@kwargs` syntax +- **Argument binding priority**: Named args can bind to regular params first, with unmatched ones going to `@kwargs` \ No newline at end of file diff --git a/SPEC.md b/SPEC.md index 30e2769..20378bd 100644 --- a/SPEC.md +++ b/SPEC.md @@ -316,38 +316,43 @@ The constant must be a `function_def` with: The created function captures `currentScope` as its `parentScope`. #### CALL -**Operand**: Either: -- Number: positional argument count -- Object: `{ positional: number, named: number }` +**Operand**: None -**Stack**: [fn, arg1, arg2, ..., name1, val1, name2, val2, ...] → [returnValue] +**Stack**: [fn, arg1, arg2, ..., name1, val1, name2, val2, ..., positionalCount, namedCount] → [returnValue] **Behavior**: -1. Pop function from stack -2. Pop named arguments (name/value pairs) according to operand -3. Pop positional arguments according to operand -4. Mark current frame (if exists) as break target (`isBreakTarget = true`) -5. Push new call frame with current PC and scope -6. Create new scope with function's parentScope as parent -7. Bind parameters: +1. Pop namedCount from stack (top of stack) +2. Pop positionalCount from stack +3. Pop named arguments (name/value pairs) from stack +4. Pop positional arguments from stack +5. Pop function from stack +6. Mark current frame (if exists) as break target (`isBreakTarget = true`) +7. Push new call frame with current PC and scope +8. Create new scope with function's parentScope as parent +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, collect named args into dict -8. Set currentScope to new scope -9. Jump to function body + - For kwargs functions: bind fixed params by position/name, collect unmatched named args into dict +10. Set currentScope to new scope +11. Jump to function body -**Parameter Binding Priority**: -1. Named argument (if provided) +**Parameter Binding Priority** (for fixed params): +1. Named argument (if provided and matches param name) 2. Positional argument (if provided) 3. Default value (if defined) 4. Null +**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 + **Errors**: Throws if top of stack is not a function #### TAIL_CALL -**Operand**: Same as CALL -**Effect**: Same as CALL, but reuses current call frame -**Stack**: Same as CALL +**Operand**: None +**Effect**: Same as CALL, but reuses current call frame +**Stack**: [fn, arg1, arg2, ..., name1, val1, name2, val2, ..., positionalCount, namedCount] → [returnValue] **Behavior**: Identical to CALL except: - Does NOT push a new call frame diff --git a/src/vm.ts b/src/vm.ts index 44a0fcd..eb5d859 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -333,11 +333,30 @@ export class VM { break case OpCode.CALL: { - const argCount = instruction.operand as number + // Pop named count from stack (top) + const namedCount = toNumber(this.stack.pop()!) - const args: Value[] = [] - for (let i = 0; i < argCount; i++) - args.unshift(this.stack.pop()!) + // Pop positional count from stack + const positionalCount = toNumber(this.stack.pop()!) + + // Pop named arguments (name-value pairs) from stack + // Stack has: ... key1 value1 key2 value2 (top) + // So we pop value2, key2, value1, key1 + const namedArgs = new Map() + const namedPairs: Array<{ key: string; value: Value }> = [] + for (let i = 0; i < namedCount; i++) { + const value = this.stack.pop()! + const key = this.stack.pop()! + namedPairs.unshift({ key: toString(key), value }) + } + for (const pair of namedPairs) { + namedArgs.set(pair.key, pair.value) + } + + // Pop positional arguments from stack + const positionalArgs: Value[] = [] + for (let i = 0; i < positionalCount; i++) + positionalArgs.unshift(this.stack.pop()!) const fn = this.stack.pop()! @@ -355,13 +374,21 @@ export class VM { this.scope = new Scope(fn.parentScope) - const fixedParamCount = fn.variadic ? fn.params.length - 1 : fn.params.length + // Determine how many params are fixed (excluding variadic and named) + let fixedParamCount = fn.params.length + if (fn.variadic) fixedParamCount-- + if (fn.named) fixedParamCount-- + + // Bind fixed parameters using priority: named arg > positional arg > default > null for (let i = 0; i < fixedParamCount; i++) { const paramName = fn.params[i]! - const argValue = args[i] - if (argValue !== undefined) { - this.scope.set(paramName, argValue) + // Check if named argument was provided for this param + if (namedArgs.has(paramName)) { + this.scope.set(paramName, namedArgs.get(paramName)!) + namedArgs.delete(paramName) // Remove from named args so it won't go to kwargs + } else if (positionalArgs[i] !== undefined) { + this.scope.set(paramName, positionalArgs[i]!) } else if (fn.defaults[paramName] !== undefined) { const defaultIdx = fn.defaults[paramName]! const defaultValue = this.constants[defaultIdx]! @@ -373,40 +400,75 @@ export class VM { } } + // Handle variadic parameter (collect remaining positional args) if (fn.variadic) { - const variadicParamName = fn.params[fn.params.length - 1]! - const remainingArgs = args.slice(fixedParamCount) + const variadicParamName = fn.params[fn.params.length - (fn.named ? 2 : 1)]! + const remainingArgs = positionalArgs.slice(fixedParamCount) this.scope.set(variadicParamName, { type: 'array', value: remainingArgs }) } + // Handle named parameter (collect remaining named args that didn't match params) + if (fn.named) { + const namedParamName = fn.params[fn.params.length - 1]! + const kwargsDict = new Map() + for (const [key, value] of namedArgs) { + kwargsDict.set(key, value) + } + this.scope.set(namedParamName, { type: 'dict', value: kwargsDict }) + } + // subtract 1 because pc was incremented this.pc = fn.body - 1 break } case OpCode.TAIL_CALL: { - const tailArgCount = instruction.operand as number + // Pop named count from stack (top) + const tailNamedCount = toNumber(this.stack.pop()!) - const args: Value[] = [] - for (let i = 0; i < tailArgCount; i++) - args.unshift(this.stack.pop()!) + // Pop positional count from stack + const tailPositionalCount = toNumber(this.stack.pop()!) - const fn = this.stack.pop()! + // Pop named arguments (name-value pairs) from stack + const tailNamedArgs = new Map() + const tailNamedPairs: Array<{ key: string; value: Value }> = [] + for (let i = 0; i < tailNamedCount; i++) { + const value = this.stack.pop()! + const key = this.stack.pop()! + tailNamedPairs.unshift({ key: toString(key), value }) + } + for (const pair of tailNamedPairs) { + tailNamedArgs.set(pair.key, pair.value) + } - if (fn.type !== 'function') + // Pop positional arguments from stack + const tailPositionalArgs: Value[] = [] + for (let i = 0; i < tailPositionalCount; i++) + tailPositionalArgs.unshift(this.stack.pop()!) + + const tailFn = this.stack.pop()! + + if (tailFn.type !== 'function') throw new Error('TAIL_CALL: not a function') - this.scope = new Scope(fn.parentScope) + this.scope = new Scope(tailFn.parentScope) - const fixedParamCount = fn.variadic ? fn.params.length - 1 : fn.params.length - for (let i = 0; i < fixedParamCount; i++) { - const paramName = fn.params[i]! - const argValue = args[i] + // Determine how many params are fixed (excluding variadic and named) + let tailFixedParamCount = tailFn.params.length + if (tailFn.variadic) tailFixedParamCount-- + if (tailFn.named) tailFixedParamCount-- - if (argValue !== undefined) { - this.scope.set(paramName, argValue) - } else if (fn.defaults[paramName] !== undefined) { - const defaultIdx = fn.defaults[paramName]! + // Bind fixed parameters + for (let i = 0; i < tailFixedParamCount; i++) { + const paramName = tailFn.params[i]! + + if (tailNamedArgs.has(paramName)) { + this.scope.set(paramName, tailNamedArgs.get(paramName)!) + tailNamedArgs.delete(paramName) + } else if (tailPositionalArgs[i] !== undefined) { + this.scope.set(paramName, tailPositionalArgs[i]!) + } else if (tailFn.defaults[paramName] !== undefined) { + const defaultIdx = tailFn.defaults[paramName]! const defaultValue = this.constants[defaultIdx]! if (defaultValue.type === 'function_def') throw new Error('Default value cannot be a function definition') @@ -416,14 +478,25 @@ export class VM { } } - if (fn.variadic) { - const variadicParamName = fn.params[fn.params.length - 1]! - const remainingArgs = args.slice(fixedParamCount) + // Handle variadic parameter + if (tailFn.variadic) { + const variadicParamName = tailFn.params[tailFn.params.length - (tailFn.named ? 2 : 1)]! + const remainingArgs = tailPositionalArgs.slice(tailFixedParamCount) this.scope.set(variadicParamName, { type: 'array', value: remainingArgs }) } + // Handle named parameter + if (tailFn.named) { + const namedParamName = tailFn.params[tailFn.params.length - 1]! + const kwargsDict = new Map() + for (const [key, value] of tailNamedArgs) { + kwargsDict.set(key, value) + } + this.scope.set(namedParamName, { type: 'dict', value: kwargsDict }) + } + // subtract 1 because PC was incremented - this.pc = fn.body - 1 + this.pc = tailFn.body - 1 break } diff --git a/tests/basic.test.ts b/tests/basic.test.ts index 4c164ec..0616df0 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -521,8 +521,10 @@ test("BREAK - throws error when no break target", async () => { // BREAK requires a break target frame on the call stack // A single function call has no previous frame to mark as break target const bytecode = toBytecode(` - MAKE_FUNCTION () #3 - CALL #0 + MAKE_FUNCTION () #5 + PUSH 0 + PUSH 0 + CALL HALT BREAK `) @@ -539,12 +541,16 @@ test("BREAK - exits from nested function call", async () => { // BREAK unwinds to the break target (the outer function's frame) // Main calls outer, outer calls inner, inner BREAKs back to outer's caller (main) const bytecode = toBytecode(` - MAKE_FUNCTION () #4 - CALL #0 + MAKE_FUNCTION () #6 + PUSH 0 + PUSH 0 + CALL PUSH 42 HALT - MAKE_FUNCTION () #7 - CALL #0 + MAKE_FUNCTION () #11 + PUSH 0 + PUSH 0 + CALL PUSH 99 RETURN BREAK diff --git a/tests/bytecode.test.ts b/tests/bytecode.test.ts index bf81867..b77482e 100644 --- a/tests/bytecode.test.ts +++ b/tests/bytecode.test.ts @@ -25,8 +25,10 @@ test("string compilation", () => { test("MAKE_FUNCTION - basic function", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION () #3 - CALL #0 + MAKE_FUNCTION () #5 + PUSH 0 + PUSH 0 + CALL HALT PUSH 42 RETURN @@ -39,10 +41,12 @@ test("MAKE_FUNCTION - basic function", async () => { test("MAKE_FUNCTION - function with parameters", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (x y) #5 + MAKE_FUNCTION (x y) #7 PUSH 10 PUSH 20 - CALL #2 + PUSH 2 + PUSH 0 + CALL HALT LOAD x LOAD y @@ -57,9 +61,11 @@ test("MAKE_FUNCTION - function with parameters", async () => { test("MAKE_FUNCTION - function with default parameters", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (x y=100) #4 + MAKE_FUNCTION (x y=100) #6 PUSH 10 - CALL #1 + PUSH 1 + PUSH 0 + CALL HALT LOAD x LOAD y @@ -74,11 +80,13 @@ test("MAKE_FUNCTION - function with default parameters", async () => { test("MAKE_FUNCTION - tail recursive countdown", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (n) #6 + MAKE_FUNCTION (n) #8 STORE countdown LOAD countdown PUSH 5 - CALL #1 + PUSH 1 + PUSH 0 + CALL HALT LOAD n PUSH 0 @@ -90,7 +98,9 @@ test("MAKE_FUNCTION - tail recursive countdown", async () => { LOAD n PUSH 1 SUB - TAIL_CALL #1 + PUSH 1 + PUSH 0 + TAIL_CALL `) const vm = new VM(bytecode) @@ -100,8 +110,10 @@ test("MAKE_FUNCTION - tail recursive countdown", async () => { test("MAKE_FUNCTION - multiple default values", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (a=1 b=2 c=3) #3 - CALL #0 + MAKE_FUNCTION (a=1 b=2 c=3) #5 + PUSH 0 + PUSH 0 + CALL HALT LOAD a LOAD b @@ -118,8 +130,10 @@ test("MAKE_FUNCTION - multiple default values", async () => { test("MAKE_FUNCTION - default with string", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (name="World") #3 - CALL #0 + MAKE_FUNCTION (name="World") #5 + PUSH 0 + PUSH 0 + CALL HALT LOAD name RETURN diff --git a/tests/exceptions.test.ts b/tests/exceptions.test.ts index abc466c..bedb92a 100644 --- a/tests/exceptions.test.ts +++ b/tests/exceptions.test.ts @@ -82,19 +82,21 @@ test("THROW - exception unwinds call stack", async () => { const vm = new VM({ instructions: [ // 0: Main code with try block - { op: OpCode.PUSH_TRY, operand: 6 }, + { op: OpCode.PUSH_TRY, operand: 8 }, { op: OpCode.MAKE_FUNCTION, operand: 0 }, - { op: OpCode.CALL, operand: 0 }, + { op: OpCode.PUSH, operand: 3 }, // positionalCount = 0 + { op: OpCode.PUSH, operand: 3 }, // namedCount = 0 + { op: OpCode.CALL }, { op: OpCode.POP_TRY }, { op: OpCode.HALT }, - // 5: Not executed + // 7: Not executed { op: OpCode.PUSH, operand: 2 }, - // 6: Catch block + // 8: Catch block { op: OpCode.HALT }, // error value on stack - // 7: Function body (throws) + // 9: Function body (throws) { op: OpCode.PUSH, operand: 1 }, { op: OpCode.THROW } ], @@ -103,12 +105,13 @@ test("THROW - exception unwinds call stack", async () => { type: 'function_def', params: [], defaults: {}, - body: 7, + body: 9, variadic: false, named: false }, toValue('function error'), - toValue(999) + toValue(999), + toValue(0) // constant for 0 ] }) diff --git a/tests/functions.test.ts b/tests/functions.test.ts index 22726b2..a905cc9 100644 --- a/tests/functions.test.ts +++ b/tests/functions.test.ts @@ -17,8 +17,10 @@ test("MAKE_FUNCTION - creates function with captured scope", async () => { test("CALL and RETURN - basic function call", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION () #3 - CALL #0 + MAKE_FUNCTION () #5 + PUSH 0 + PUSH 0 + CALL HALT PUSH 42 RETURN @@ -30,9 +32,11 @@ test("CALL and RETURN - basic function call", async () => { test("CALL and RETURN - function with one parameter", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (x) #4 + MAKE_FUNCTION (x) #6 PUSH 100 - CALL #1 + PUSH 1 + PUSH 0 + CALL HALT LOAD x RETURN @@ -44,10 +48,12 @@ test("CALL and RETURN - function with one parameter", async () => { test("CALL and RETURN - function with two parameters", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (a b) #5 + MAKE_FUNCTION (a b) #7 PUSH 10 PUSH 20 - CALL #2 + PUSH 2 + PUSH 0 + CALL HALT LOAD a LOAD b @@ -61,11 +67,13 @@ test("CALL and RETURN - function with two parameters", async () => { test("CALL - variadic function with no fixed params", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (...args) #6 + MAKE_FUNCTION (...args) #8 PUSH 1 PUSH 2 PUSH 3 - CALL #3 + PUSH 3 + PUSH 0 + CALL HALT LOAD args RETURN @@ -84,11 +92,13 @@ test("CALL - variadic function with no fixed params", async () => { test("CALL - variadic function with one fixed param", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (x ...rest) #6 + MAKE_FUNCTION (x ...rest) #8 PUSH 10 PUSH 20 PUSH 30 - CALL #3 + PUSH 3 + PUSH 0 + CALL HALT LOAD rest RETURN @@ -107,12 +117,14 @@ test("CALL - variadic function with one fixed param", async () => { test("CALL - variadic function with two fixed params", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (a b ...rest) #7 + MAKE_FUNCTION (a b ...rest) #9 PUSH 1 PUSH 2 PUSH 3 PUSH 4 - CALL #4 + PUSH 4 + PUSH 0 + CALL HALT LOAD rest RETURN @@ -131,9 +143,11 @@ test("CALL - variadic function with two fixed params", async () => { test("CALL - variadic function with no extra args", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (x ...rest) #4 + MAKE_FUNCTION (x ...rest) #6 PUSH 10 - CALL #1 + PUSH 1 + PUSH 0 + CALL HALT LOAD rest RETURN @@ -146,8 +160,10 @@ test("CALL - variadic function with no extra args", async () => { test("CALL - variadic function with defaults on fixed params", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (x=5 ...rest) #3 - CALL #0 + MAKE_FUNCTION (x=5 ...rest) #5 + PUSH 0 + PUSH 0 + CALL HALT LOAD x RETURN @@ -160,11 +176,13 @@ test("CALL - variadic function with defaults on fixed params", async () => { test("TAIL_CALL - variadic function", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (x ...rest) #6 + MAKE_FUNCTION (x ...rest) #8 PUSH 1 PUSH 2 PUSH 3 - CALL #3 + PUSH 3 + PUSH 0 + CALL HALT LOAD rest RETURN @@ -180,3 +198,180 @@ test("TAIL_CALL - variadic function", async () => { ] }) }) + +test("CALL - named args function with no fixed params", async () => { + const bytecode = toBytecode(` + MAKE_FUNCTION (@kwargs) #9 + PUSH "name" + PUSH "Bob" + PUSH "age" + PUSH 50 + PUSH 0 + PUSH 2 + CALL + HALT + LOAD kwargs + RETURN + `) + + const result = await new VM(bytecode).run() + expect(result.type).toBe('dict') + if (result.type === 'dict') { + expect(result.value.get('name')).toEqual({ type: 'string', value: 'Bob' }) + expect(result.value.get('age')).toEqual({ type: 'number', value: 50 }) + } +}) + +test("CALL - named args function with one fixed param", async () => { + const bytecode = toBytecode(` + MAKE_FUNCTION (x @kwargs) #8 + PUSH 10 + PUSH "name" + PUSH "Alice" + PUSH 1 + PUSH 1 + CALL + HALT + LOAD kwargs + RETURN + `) + + const result = await new VM(bytecode).run() + expect(result.type).toBe('dict') + if (result.type === 'dict') { + expect(result.value.get('name')).toEqual({ type: 'string', value: 'Alice' }) + expect(result.value.size).toBe(1) + } +}) + +test("CALL - named args with matching param name should bind to param not kwargs", async () => { + const bytecode = toBytecode(` + MAKE_FUNCTION (name @kwargs) #8 + PUSH "Bob" + PUSH "age" + PUSH 50 + PUSH 1 + PUSH 1 + CALL + HALT + LOAD name + RETURN + `) + + const result = await new VM(bytecode).run() + // name should be bound as regular param, not collected in kwargs + expect(result).toEqual({ type: 'string', value: 'Bob' }) +}) + +test("CALL - named args that match param names should not be in kwargs", async () => { + const bytecode = toBytecode(` + MAKE_FUNCTION (name age @kwargs) #9 + PUSH "name" + PUSH "Bob" + PUSH "city" + PUSH "NYC" + PUSH 0 + PUSH 2 + CALL + HALT + LOAD kwargs + RETURN + `) + + const result = await new VM(bytecode).run() + expect(result.type).toBe('dict') + if (result.type === 'dict') { + // Only city should be in kwargs, name should be bound to param + expect(result.value.get('city')).toEqual({ type: 'string', value: 'NYC' }) + expect(result.value.has('name')).toBe(false) + expect(result.value.size).toBe(1) + } +}) + +test("CALL - mixed variadic and named args", async () => { + const bytecode = toBytecode(` + MAKE_FUNCTION (x ...rest @kwargs) #10 + PUSH 1 + PUSH 2 + PUSH 3 + PUSH "name" + PUSH "Bob" + PUSH 3 + PUSH 1 + CALL + HALT + LOAD rest + RETURN + `) + + const result = await new VM(bytecode).run() + // rest should have [2, 3] + expect(result).toEqual({ + type: 'array', + value: [ + { type: 'number', value: 2 }, + { type: 'number', value: 3 } + ] + }) +}) + +test("CALL - mixed variadic and named args, check kwargs", async () => { + const bytecode = toBytecode(` + MAKE_FUNCTION (x ...rest @kwargs) #10 + PUSH 1 + PUSH 2 + PUSH 3 + PUSH "name" + PUSH "Bob" + PUSH 3 + PUSH 1 + CALL + HALT + LOAD kwargs + RETURN + `) + + const result = await new VM(bytecode).run() + expect(result.type).toBe('dict') + if (result.type === 'dict') { + expect(result.value.get('name')).toEqual({ type: 'string', value: 'Bob' }) + } +}) + +test("CALL - named args with no extra named args", async () => { + const bytecode = toBytecode(` + MAKE_FUNCTION (x @kwargs) #6 + PUSH 10 + PUSH 1 + PUSH 0 + CALL + HALT + LOAD kwargs + RETURN + `) + + const result = await new VM(bytecode).run() + // kwargs should be empty dict + expect(result.type).toBe('dict') + if (result.type === 'dict') { + expect(result.value.size).toBe(0) + } +}) + +test("CALL - named args with defaults on fixed params", async () => { + const bytecode = toBytecode(` + MAKE_FUNCTION (x=5 @kwargs) #7 + PUSH "name" + PUSH "Alice" + PUSH 0 + PUSH 1 + CALL + HALT + LOAD x + RETURN + `) + + const result = await new VM(bytecode).run() + // x should use default value 5 + expect(result).toEqual({ type: 'number', value: 5 }) +}) diff --git a/tests/tail-call.test.ts b/tests/tail-call.test.ts index 4258261..76bbfcf 100644 --- a/tests/tail-call.test.ts +++ b/tests/tail-call.test.ts @@ -9,11 +9,13 @@ test("TAIL_CALL - basic tail recursive countdown", async () => { // return countdown(n - 1) // tail call // } const bytecode = toBytecode(` - MAKE_FUNCTION (n) #6 + MAKE_FUNCTION (n) #8 STORE countdown LOAD countdown PUSH 5 - CALL #1 + PUSH 1 + PUSH 0 + CALL HALT LOAD n PUSH 0 @@ -25,7 +27,9 @@ test("TAIL_CALL - basic tail recursive countdown", async () => { LOAD n PUSH 1 SUB - TAIL_CALL #1 + PUSH 1 + PUSH 0 + TAIL_CALL `) const result = await new VM(bytecode).run() @@ -38,12 +42,14 @@ test("TAIL_CALL - tail recursive sum with accumulator", async () => { // return sum(n - 1, acc + n) // tail call // } const bytecode = toBytecode(` - MAKE_FUNCTION (n acc) #7 + MAKE_FUNCTION (n acc) #9 STORE sum LOAD sum PUSH 10 PUSH 0 - CALL #2 + PUSH 2 + PUSH 0 + CALL HALT LOAD n PUSH 0 @@ -58,7 +64,9 @@ test("TAIL_CALL - tail recursive sum with accumulator", async () => { LOAD acc LOAD n ADD - TAIL_CALL #2 + PUSH 2 + PUSH 0 + TAIL_CALL `) const result = await new VM(bytecode).run() @@ -69,11 +77,13 @@ test("TAIL_CALL - doesn't overflow stack with deep recursion", async () => { // This would overflow the stack with regular CALL // but should work fine with TAIL_CALL const bytecode = toBytecode(` - MAKE_FUNCTION (n) #6 + MAKE_FUNCTION (n) #8 STORE deep LOAD deep PUSH 10000 - CALL #1 + PUSH 1 + PUSH 0 + CALL HALT LOAD n PUSH 0 @@ -85,7 +95,9 @@ test("TAIL_CALL - doesn't overflow stack with deep recursion", async () => { LOAD n PUSH 1 SUB - TAIL_CALL #1 + PUSH 1 + PUSH 0 + TAIL_CALL `) const result = await new VM(bytecode).run() @@ -97,13 +109,15 @@ test("TAIL_CALL - tail call to different function", async () => { // function even(n) { return n === 0 ? true : odd(n - 1) } // function odd(n) { return n === 0 ? false : even(n - 1) } const bytecode = toBytecode(` - MAKE_FUNCTION (n) #8 + MAKE_FUNCTION (n) #10 STORE even - MAKE_FUNCTION (n) #19 + MAKE_FUNCTION (n) #23 STORE odd LOAD even PUSH 7 - CALL #1 + PUSH 1 + PUSH 0 + CALL HALT LOAD n PUSH 0 @@ -115,7 +129,9 @@ test("TAIL_CALL - tail call to different function", async () => { LOAD n PUSH 1 SUB - TAIL_CALL #1 + PUSH 1 + PUSH 0 + TAIL_CALL LOAD n PUSH 0 EQ @@ -126,7 +142,9 @@ test("TAIL_CALL - tail call to different function", async () => { LOAD n PUSH 1 SUB - TAIL_CALL #1 + PUSH 1 + PUSH 0 + TAIL_CALL `) const result = await new VM(bytecode).run()