rest args
This commit is contained in:
parent
d7402d8ebc
commit
e75d119ba8
|
|
@ -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)
|
||||
45
src/vm.ts
45
src/vm.ts
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
]
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user