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
**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)
- **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`

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`.
#### 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

131
src/vm.ts
View File

@ -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<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()!
@ -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<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
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<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')
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<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
this.pc = fn.body - 1
this.pc = tailFn.body - 1
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
// 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

View File

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

View File

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

View File

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

View File

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