diff --git a/README.md b/README.md index ccade6b..d2081f2 100644 --- a/README.md +++ b/README.md @@ -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) \ No newline at end of file +- Advanced function features (kwargs, named arguments in CALL) \ No newline at end of file diff --git a/src/vm.ts b/src/vm.ts index b9c551b..44a0fcd 100644 --- a/src/vm.ts +++ b/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) diff --git a/tests/functions.test.ts b/tests/functions.test.ts index 929b953..22726b2 100644 --- a/tests/functions.test.ts +++ b/tests/functions.test.ts @@ -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 } + ] + }) +})