variadic and named args

This commit is contained in:
Chris Wanstrath 2025-10-05 21:07:36 -07:00
parent e75d119ba8
commit db4f332472
8 changed files with 426 additions and 111 deletions

View File

@ -77,7 +77,7 @@ It's where Shrimp live.
## Test Status ## Test Status
**83 tests passing** covering: **91 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)
@ -87,7 +87,9 @@ 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)
- Function operations (MAKE_FUNCTION, CALL, TAIL_CALL, RETURN) with parameter binding - 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) - **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 - 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 - 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) - **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 - **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 - **Variadic parameters**: Functions can collect remaining positional arguments into an array using `...rest` syntax
- **Named parameters (kwargs)**: Functions can collect unmatched named arguments into a dict using `@kwargs` syntax
🚧 **Still TODO**: - **Argument binding priority**: Named args can bind to regular params first, with unmatched ones going to `@kwargs`
- Advanced function features (kwargs, named arguments in CALL)

43
SPEC.md
View File

@ -316,38 +316,43 @@ The constant must be a `function_def` with:
The created function captures `currentScope` as its `parentScope`. The created function captures `currentScope` as its `parentScope`.
#### CALL #### CALL
**Operand**: Either: **Operand**: None
- Number: positional argument count
- Object: `{ positional: number, named: number }`
**Stack**: [fn, arg1, arg2, ..., name1, val1, name2, val2, ...] → [returnValue] **Stack**: [fn, arg1, arg2, ..., name1, val1, name2, val2, ..., positionalCount, namedCount] → [returnValue]
**Behavior**: **Behavior**:
1. Pop function from stack 1. Pop namedCount from stack (top of stack)
2. Pop named arguments (name/value pairs) according to operand 2. Pop positionalCount from stack
3. Pop positional arguments according to operand 3. Pop named arguments (name/value pairs) from stack
4. Mark current frame (if exists) as break target (`isBreakTarget = true`) 4. Pop positional arguments from stack
5. Push new call frame with current PC and scope 5. Pop function from stack
6. Create new scope with function's parentScope as parent 6. Mark current frame (if exists) as break target (`isBreakTarget = true`)
7. Bind parameters: 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 regular functions: bind params by position, then by name, then defaults, then null
- For variadic functions: bind fixed params, collect rest into array - For variadic functions: bind fixed params, collect rest into array
- For kwargs functions: bind fixed params, collect named args into dict - For kwargs functions: bind fixed params by position/name, collect unmatched named args into dict
8. Set currentScope to new scope 10. Set currentScope to new scope
9. Jump to function body 11. Jump to function body
**Parameter Binding Priority**: **Parameter Binding Priority** (for fixed params):
1. Named argument (if provided) 1. Named argument (if provided and matches param name)
2. Positional argument (if provided) 2. Positional argument (if provided)
3. Default value (if defined) 3. Default value (if defined)
4. Null 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 **Errors**: Throws if top of stack is not a function
#### TAIL_CALL #### TAIL_CALL
**Operand**: Same as CALL **Operand**: None
**Effect**: Same as CALL, but reuses current call frame **Effect**: Same as CALL, but reuses current call frame
**Stack**: Same as CALL **Stack**: [fn, arg1, arg2, ..., name1, val1, name2, val2, ..., positionalCount, namedCount] → [returnValue]
**Behavior**: Identical to CALL except: **Behavior**: Identical to CALL except:
- Does NOT push a new call frame - Does NOT push a new call frame

131
src/vm.ts
View File

@ -333,11 +333,30 @@ export class VM {
break break
case OpCode.CALL: { case OpCode.CALL: {
const argCount = instruction.operand as number // Pop named count from stack (top)
const namedCount = toNumber(this.stack.pop()!)
const args: Value[] = [] // Pop positional count from stack
for (let i = 0; i < argCount; i++) const positionalCount = toNumber(this.stack.pop()!)
args.unshift(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<string, Value>()
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()! const fn = this.stack.pop()!
@ -355,13 +374,21 @@ export class VM {
this.scope = new Scope(fn.parentScope) 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++) { for (let i = 0; i < fixedParamCount; i++) {
const paramName = fn.params[i]! const paramName = fn.params[i]!
const argValue = args[i]
if (argValue !== undefined) { // Check if named argument was provided for this param
this.scope.set(paramName, argValue) 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) { } else if (fn.defaults[paramName] !== undefined) {
const defaultIdx = fn.defaults[paramName]! const defaultIdx = fn.defaults[paramName]!
const defaultValue = this.constants[defaultIdx]! const defaultValue = this.constants[defaultIdx]!
@ -373,40 +400,75 @@ export class VM {
} }
} }
// Handle variadic parameter (collect remaining positional args)
if (fn.variadic) { if (fn.variadic) {
const variadicParamName = fn.params[fn.params.length - 1]! const variadicParamName = fn.params[fn.params.length - (fn.named ? 2 : 1)]!
const remainingArgs = args.slice(fixedParamCount) const remainingArgs = positionalArgs.slice(fixedParamCount)
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs }) 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<string, Value>()
for (const [key, value] of namedArgs) {
kwargsDict.set(key, value)
}
this.scope.set(namedParamName, { type: 'dict', value: kwargsDict })
}
// subtract 1 because pc was incremented // subtract 1 because pc was incremented
this.pc = fn.body - 1 this.pc = fn.body - 1
break break
} }
case OpCode.TAIL_CALL: { 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[] = [] // Pop positional count from stack
for (let i = 0; i < tailArgCount; i++) const tailPositionalCount = toNumber(this.stack.pop()!)
args.unshift(this.stack.pop()!)
const fn = this.stack.pop()! // Pop named arguments (name-value pairs) from stack
const tailNamedArgs = new Map<string, Value>()
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') 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 // Determine how many params are fixed (excluding variadic and named)
for (let i = 0; i < fixedParamCount; i++) { let tailFixedParamCount = tailFn.params.length
const paramName = fn.params[i]! if (tailFn.variadic) tailFixedParamCount--
const argValue = args[i] if (tailFn.named) tailFixedParamCount--
if (argValue !== undefined) { // Bind fixed parameters
this.scope.set(paramName, argValue) for (let i = 0; i < tailFixedParamCount; i++) {
} else if (fn.defaults[paramName] !== undefined) { const paramName = tailFn.params[i]!
const defaultIdx = fn.defaults[paramName]!
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]! const defaultValue = this.constants[defaultIdx]!
if (defaultValue.type === 'function_def') if (defaultValue.type === 'function_def')
throw new Error('Default value cannot be a function definition') throw new Error('Default value cannot be a function definition')
@ -416,14 +478,25 @@ export class VM {
} }
} }
if (fn.variadic) { // Handle variadic parameter
const variadicParamName = fn.params[fn.params.length - 1]! if (tailFn.variadic) {
const remainingArgs = args.slice(fixedParamCount) const variadicParamName = tailFn.params[tailFn.params.length - (tailFn.named ? 2 : 1)]!
const remainingArgs = tailPositionalArgs.slice(tailFixedParamCount)
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs }) 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<string, Value>()
for (const [key, value] of tailNamedArgs) {
kwargsDict.set(key, value)
}
this.scope.set(namedParamName, { type: 'dict', value: kwargsDict })
}
// subtract 1 because PC was incremented // subtract 1 because PC was incremented
this.pc = fn.body - 1 this.pc = tailFn.body - 1
break break
} }

View File

@ -521,8 +521,10 @@ test("BREAK - throws error when no break target", async () => {
// BREAK requires a break target frame on the call stack // BREAK requires a break target frame on the call stack
// A single function call has no previous frame to mark as break target // A single function call has no previous frame to mark as break target
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION () #3 MAKE_FUNCTION () #5
CALL #0 PUSH 0
PUSH 0
CALL
HALT HALT
BREAK BREAK
`) `)
@ -539,12 +541,16 @@ test("BREAK - exits from nested function call", async () => {
// BREAK unwinds to the break target (the outer function's frame) // 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) // Main calls outer, outer calls inner, inner BREAKs back to outer's caller (main)
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION () #4 MAKE_FUNCTION () #6
CALL #0 PUSH 0
PUSH 0
CALL
PUSH 42 PUSH 42
HALT HALT
MAKE_FUNCTION () #7 MAKE_FUNCTION () #11
CALL #0 PUSH 0
PUSH 0
CALL
PUSH 99 PUSH 99
RETURN RETURN
BREAK BREAK

View File

@ -25,8 +25,10 @@ test("string compilation", () => {
test("MAKE_FUNCTION - basic function", async () => { test("MAKE_FUNCTION - basic function", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION () #3 MAKE_FUNCTION () #5
CALL #0 PUSH 0
PUSH 0
CALL
HALT HALT
PUSH 42 PUSH 42
RETURN RETURN
@ -39,10 +41,12 @@ test("MAKE_FUNCTION - basic function", async () => {
test("MAKE_FUNCTION - function with parameters", async () => { test("MAKE_FUNCTION - function with parameters", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (x y) #5 MAKE_FUNCTION (x y) #7
PUSH 10 PUSH 10
PUSH 20 PUSH 20
CALL #2 PUSH 2
PUSH 0
CALL
HALT HALT
LOAD x LOAD x
LOAD y LOAD y
@ -57,9 +61,11 @@ test("MAKE_FUNCTION - function with parameters", async () => {
test("MAKE_FUNCTION - function with default parameters", async () => { test("MAKE_FUNCTION - function with default parameters", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (x y=100) #4 MAKE_FUNCTION (x y=100) #6
PUSH 10 PUSH 10
CALL #1 PUSH 1
PUSH 0
CALL
HALT HALT
LOAD x LOAD x
LOAD y LOAD y
@ -74,11 +80,13 @@ test("MAKE_FUNCTION - function with default parameters", async () => {
test("MAKE_FUNCTION - tail recursive countdown", async () => { test("MAKE_FUNCTION - tail recursive countdown", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (n) #6 MAKE_FUNCTION (n) #8
STORE countdown STORE countdown
LOAD countdown LOAD countdown
PUSH 5 PUSH 5
CALL #1 PUSH 1
PUSH 0
CALL
HALT HALT
LOAD n LOAD n
PUSH 0 PUSH 0
@ -90,7 +98,9 @@ test("MAKE_FUNCTION - tail recursive countdown", async () => {
LOAD n LOAD n
PUSH 1 PUSH 1
SUB SUB
TAIL_CALL #1 PUSH 1
PUSH 0
TAIL_CALL
`) `)
const vm = new VM(bytecode) const vm = new VM(bytecode)
@ -100,8 +110,10 @@ test("MAKE_FUNCTION - tail recursive countdown", async () => {
test("MAKE_FUNCTION - multiple default values", async () => { test("MAKE_FUNCTION - multiple default values", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (a=1 b=2 c=3) #3 MAKE_FUNCTION (a=1 b=2 c=3) #5
CALL #0 PUSH 0
PUSH 0
CALL
HALT HALT
LOAD a LOAD a
LOAD b LOAD b
@ -118,8 +130,10 @@ test("MAKE_FUNCTION - multiple default values", async () => {
test("MAKE_FUNCTION - default with string", async () => { test("MAKE_FUNCTION - default with string", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (name="World") #3 MAKE_FUNCTION (name="World") #5
CALL #0 PUSH 0
PUSH 0
CALL
HALT HALT
LOAD name LOAD name
RETURN RETURN

View File

@ -82,19 +82,21 @@ test("THROW - exception unwinds call stack", async () => {
const vm = new VM({ const vm = new VM({
instructions: [ instructions: [
// 0: Main code with try block // 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.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.POP_TRY },
{ op: OpCode.HALT }, { op: OpCode.HALT },
// 5: Not executed // 7: Not executed
{ op: OpCode.PUSH, operand: 2 }, { op: OpCode.PUSH, operand: 2 },
// 6: Catch block // 8: Catch block
{ op: OpCode.HALT }, // error value on stack { op: OpCode.HALT }, // error value on stack
// 7: Function body (throws) // 9: Function body (throws)
{ op: OpCode.PUSH, operand: 1 }, { op: OpCode.PUSH, operand: 1 },
{ op: OpCode.THROW } { op: OpCode.THROW }
], ],
@ -103,12 +105,13 @@ test("THROW - exception unwinds call stack", async () => {
type: 'function_def', type: 'function_def',
params: [], params: [],
defaults: {}, defaults: {},
body: 7, body: 9,
variadic: false, variadic: false,
named: false named: false
}, },
toValue('function error'), toValue('function error'),
toValue(999) toValue(999),
toValue(0) // constant for 0
] ]
}) })

View File

@ -17,8 +17,10 @@ test("MAKE_FUNCTION - creates function with captured scope", async () => {
test("CALL and RETURN - basic function call", async () => { test("CALL and RETURN - basic function call", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION () #3 MAKE_FUNCTION () #5
CALL #0 PUSH 0
PUSH 0
CALL
HALT HALT
PUSH 42 PUSH 42
RETURN RETURN
@ -30,9 +32,11 @@ test("CALL and RETURN - basic function call", async () => {
test("CALL and RETURN - function with one parameter", async () => { test("CALL and RETURN - function with one parameter", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (x) #4 MAKE_FUNCTION (x) #6
PUSH 100 PUSH 100
CALL #1 PUSH 1
PUSH 0
CALL
HALT HALT
LOAD x LOAD x
RETURN RETURN
@ -44,10 +48,12 @@ test("CALL and RETURN - function with one parameter", async () => {
test("CALL and RETURN - function with two parameters", async () => { test("CALL and RETURN - function with two parameters", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (a b) #5 MAKE_FUNCTION (a b) #7
PUSH 10 PUSH 10
PUSH 20 PUSH 20
CALL #2 PUSH 2
PUSH 0
CALL
HALT HALT
LOAD a LOAD a
LOAD b LOAD b
@ -61,11 +67,13 @@ test("CALL and RETURN - function with two parameters", async () => {
test("CALL - variadic function with no fixed params", async () => { test("CALL - variadic function with no fixed params", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (...args) #6 MAKE_FUNCTION (...args) #8
PUSH 1 PUSH 1
PUSH 2 PUSH 2
PUSH 3 PUSH 3
CALL #3 PUSH 3
PUSH 0
CALL
HALT HALT
LOAD args LOAD args
RETURN RETURN
@ -84,11 +92,13 @@ test("CALL - variadic function with no fixed params", async () => {
test("CALL - variadic function with one fixed param", async () => { test("CALL - variadic function with one fixed param", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (x ...rest) #6 MAKE_FUNCTION (x ...rest) #8
PUSH 10 PUSH 10
PUSH 20 PUSH 20
PUSH 30 PUSH 30
CALL #3 PUSH 3
PUSH 0
CALL
HALT HALT
LOAD rest LOAD rest
RETURN RETURN
@ -107,12 +117,14 @@ test("CALL - variadic function with one fixed param", async () => {
test("CALL - variadic function with two fixed params", async () => { test("CALL - variadic function with two fixed params", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (a b ...rest) #7 MAKE_FUNCTION (a b ...rest) #9
PUSH 1 PUSH 1
PUSH 2 PUSH 2
PUSH 3 PUSH 3
PUSH 4 PUSH 4
CALL #4 PUSH 4
PUSH 0
CALL
HALT HALT
LOAD rest LOAD rest
RETURN RETURN
@ -131,9 +143,11 @@ test("CALL - variadic function with two fixed params", async () => {
test("CALL - variadic function with no extra args", async () => { test("CALL - variadic function with no extra args", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (x ...rest) #4 MAKE_FUNCTION (x ...rest) #6
PUSH 10 PUSH 10
CALL #1 PUSH 1
PUSH 0
CALL
HALT HALT
LOAD rest LOAD rest
RETURN RETURN
@ -146,8 +160,10 @@ test("CALL - variadic function with no extra args", async () => {
test("CALL - variadic function with defaults on fixed params", async () => { test("CALL - variadic function with defaults on fixed params", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (x=5 ...rest) #3 MAKE_FUNCTION (x=5 ...rest) #5
CALL #0 PUSH 0
PUSH 0
CALL
HALT HALT
LOAD x LOAD x
RETURN RETURN
@ -160,11 +176,13 @@ test("CALL - variadic function with defaults on fixed params", async () => {
test("TAIL_CALL - variadic function", async () => { test("TAIL_CALL - variadic function", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (x ...rest) #6 MAKE_FUNCTION (x ...rest) #8
PUSH 1 PUSH 1
PUSH 2 PUSH 2
PUSH 3 PUSH 3
CALL #3 PUSH 3
PUSH 0
CALL
HALT HALT
LOAD rest LOAD rest
RETURN 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 })
})

View File

@ -9,11 +9,13 @@ test("TAIL_CALL - basic tail recursive countdown", async () => {
// return countdown(n - 1) // tail call // return countdown(n - 1) // tail call
// } // }
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (n) #6 MAKE_FUNCTION (n) #8
STORE countdown STORE countdown
LOAD countdown LOAD countdown
PUSH 5 PUSH 5
CALL #1 PUSH 1
PUSH 0
CALL
HALT HALT
LOAD n LOAD n
PUSH 0 PUSH 0
@ -25,7 +27,9 @@ test("TAIL_CALL - basic tail recursive countdown", async () => {
LOAD n LOAD n
PUSH 1 PUSH 1
SUB SUB
TAIL_CALL #1 PUSH 1
PUSH 0
TAIL_CALL
`) `)
const result = await new VM(bytecode).run() 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 // return sum(n - 1, acc + n) // tail call
// } // }
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (n acc) #7 MAKE_FUNCTION (n acc) #9
STORE sum STORE sum
LOAD sum LOAD sum
PUSH 10 PUSH 10
PUSH 0 PUSH 0
CALL #2 PUSH 2
PUSH 0
CALL
HALT HALT
LOAD n LOAD n
PUSH 0 PUSH 0
@ -58,7 +64,9 @@ test("TAIL_CALL - tail recursive sum with accumulator", async () => {
LOAD acc LOAD acc
LOAD n LOAD n
ADD ADD
TAIL_CALL #2 PUSH 2
PUSH 0
TAIL_CALL
`) `)
const result = await new VM(bytecode).run() 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 // This would overflow the stack with regular CALL
// but should work fine with TAIL_CALL // but should work fine with TAIL_CALL
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (n) #6 MAKE_FUNCTION (n) #8
STORE deep STORE deep
LOAD deep LOAD deep
PUSH 10000 PUSH 10000
CALL #1 PUSH 1
PUSH 0
CALL
HALT HALT
LOAD n LOAD n
PUSH 0 PUSH 0
@ -85,7 +95,9 @@ test("TAIL_CALL - doesn't overflow stack with deep recursion", async () => {
LOAD n LOAD n
PUSH 1 PUSH 1
SUB SUB
TAIL_CALL #1 PUSH 1
PUSH 0
TAIL_CALL
`) `)
const result = await new VM(bytecode).run() 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 even(n) { return n === 0 ? true : odd(n - 1) }
// function odd(n) { return n === 0 ? false : even(n - 1) } // function odd(n) { return n === 0 ? false : even(n - 1) }
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (n) #8 MAKE_FUNCTION (n) #10
STORE even STORE even
MAKE_FUNCTION (n) #19 MAKE_FUNCTION (n) #23
STORE odd STORE odd
LOAD even LOAD even
PUSH 7 PUSH 7
CALL #1 PUSH 1
PUSH 0
CALL
HALT HALT
LOAD n LOAD n
PUSH 0 PUSH 0
@ -115,7 +129,9 @@ test("TAIL_CALL - tail call to different function", async () => {
LOAD n LOAD n
PUSH 1 PUSH 1
SUB SUB
TAIL_CALL #1 PUSH 1
PUSH 0
TAIL_CALL
LOAD n LOAD n
PUSH 0 PUSH 0
EQ EQ
@ -126,7 +142,9 @@ test("TAIL_CALL - tail call to different function", async () => {
LOAD n LOAD n
PUSH 1 PUSH 1
SUB SUB
TAIL_CALL #1 PUSH 1
PUSH 0
TAIL_CALL
`) `)
const result = await new VM(bytecode).run() const result = await new VM(bytecode).run()