rest args

This commit is contained in:
Chris Wanstrath 2025-10-05 20:52:48 -07:00
parent d7402d8ebc
commit e75d119ba8
3 changed files with 156 additions and 17 deletions

View File

@ -77,7 +77,7 @@ It's where Shrimp live.
## Test Status
**70 tests passing** covering:
**83 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,6 +87,7 @@ 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
- **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
@ -97,6 +98,7 @@ It's where Shrimp live.
- **Relative jumps**: All JUMP instructions use PC-relative offsets instead of absolute addresses, making bytecode position-independent
- **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 (variadic params, kwargs, named arguments in CALL)
- Advanced function features (kwargs, named arguments in CALL)

View File

@ -332,7 +332,7 @@ export class VM {
})
break
case OpCode.CALL:
case OpCode.CALL: {
const argCount = instruction.operand as number
const args: Value[] = []
@ -355,7 +355,8 @@ export class VM {
this.scope = new Scope(fn.parentScope)
for (let i = 0; i < fn.params.length; i++) {
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]
@ -372,33 +373,40 @@ export class VM {
}
}
if (fn.variadic) {
const variadicParamName = fn.params[fn.params.length - 1]!
const remainingArgs = args.slice(fixedParamCount)
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs })
}
// subtract 1 because pc was incremented
this.pc = fn.body - 1
break
}
case OpCode.TAIL_CALL:
case OpCode.TAIL_CALL: {
const tailArgCount = instruction.operand as number
const tailArgs: Value[] = []
const args: Value[] = []
for (let i = 0; i < tailArgCount; i++)
tailArgs.unshift(this.stack.pop()!)
args.unshift(this.stack.pop()!)
const tailFn = this.stack.pop()!
const fn = this.stack.pop()!
if (tailFn.type !== 'function')
if (fn.type !== 'function')
throw new Error('TAIL_CALL: not a function')
this.scope = new Scope(tailFn.parentScope)
this.scope = new Scope(fn.parentScope)
// same logic as CALL
for (let i = 0; i < tailFn.params.length; i++) {
const paramName = tailFn.params[i]!
const argValue = tailArgs[i]
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]
if (argValue !== undefined) {
this.scope.set(paramName, argValue)
} else if (tailFn.defaults[paramName] !== undefined) {
const defaultIdx = tailFn.defaults[paramName]!
} else if (fn.defaults[paramName] !== undefined) {
const defaultIdx = fn.defaults[paramName]!
const defaultValue = this.constants[defaultIdx]!
if (defaultValue.type === 'function_def')
throw new Error('Default value cannot be a function definition')
@ -408,9 +416,16 @@ export class VM {
}
}
if (fn.variadic) {
const variadicParamName = fn.params[fn.params.length - 1]!
const remainingArgs = args.slice(fixedParamCount)
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs })
}
// subtract 1 because PC was incremented
this.pc = tailFn.body - 1
this.pc = fn.body - 1
break
}
case OpCode.RETURN:
const returnValue = this.stack.length ? this.stack.pop()! : toValue(null)

View File

@ -58,3 +58,125 @@ test("CALL and RETURN - function with two parameters", async () => {
const result = await new VM(bytecode).run()
expect(result).toEqual({ type: 'number', value: 30 })
})
test("CALL - variadic function with no fixed params", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (...args) #6
PUSH 1
PUSH 2
PUSH 3
CALL #3
HALT
LOAD args
RETURN
`)
const result = await new VM(bytecode).run()
expect(result).toEqual({
type: 'array',
value: [
{ type: 'number', value: 1 },
{ type: 'number', value: 2 },
{ type: 'number', value: 3 }
]
})
})
test("CALL - variadic function with one fixed param", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (x ...rest) #6
PUSH 10
PUSH 20
PUSH 30
CALL #3
HALT
LOAD rest
RETURN
`)
const result = await new VM(bytecode).run()
// x should be 10, rest should be [20, 30]
expect(result).toEqual({
type: 'array',
value: [
{ type: 'number', value: 20 },
{ type: 'number', value: 30 }
]
})
})
test("CALL - variadic function with two fixed params", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (a b ...rest) #7
PUSH 1
PUSH 2
PUSH 3
PUSH 4
CALL #4
HALT
LOAD rest
RETURN
`)
const result = await new VM(bytecode).run()
// a=1, b=2, rest=[3, 4]
expect(result).toEqual({
type: 'array',
value: [
{ type: 'number', value: 3 },
{ type: 'number', value: 4 }
]
})
})
test("CALL - variadic function with no extra args", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (x ...rest) #4
PUSH 10
CALL #1
HALT
LOAD rest
RETURN
`)
const result = await new VM(bytecode).run()
// rest should be empty array
expect(result).toEqual({ type: 'array', value: [] })
})
test("CALL - variadic function with defaults on fixed params", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (x=5 ...rest) #3
CALL #0
HALT
LOAD x
RETURN
`)
const result = await new VM(bytecode).run()
// x should use default value 5
expect(result).toEqual({ type: 'number', value: 5 })
})
test("TAIL_CALL - variadic function", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (x ...rest) #6
PUSH 1
PUSH 2
PUSH 3
CALL #3
HALT
LOAD rest
RETURN
`)
const result = await new VM(bytecode).run()
// Should return the rest array [2, 3]
expect(result).toEqual({
type: 'array',
value: [
{ type: 'number', value: 2 },
{ type: 'number', value: 3 }
]
})
})