From 25ed12b3ce1a06829bfb3101135fd2c38aafd723 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sun, 5 Oct 2025 15:47:34 -0700 Subject: [PATCH] CALL / RETURN / MAKE_FUNCTION --- README.md | 17 +++--- src/value.ts | 2 +- src/vm.ts | 92 ++++++++++++++++++++++++++++- tests/functions.test.ts | 124 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 224 insertions(+), 11 deletions(-) create mode 100644 tests/functions.test.ts diff --git a/README.md b/README.md index da9be0b..6bdae6c 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,10 @@ It's where Shrimp live. - [ ] THROW ### Functions -- [ ] MAKE_FUNCTION -- [ ] CALL +- [x] MAKE_FUNCTION +- [x] CALL - [ ] TAIL_CALL -- [ ] RETURN +- [x] RETURN ### Arrays - [x] MAKE_ARRAY @@ -76,15 +76,16 @@ It's where Shrimp live. ## Test Status -✅ **40 tests passing** covering: +✅ **46 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) - Logical operations (NOT, AND/OR patterns with short-circuiting) - Variable operations (LOAD, STORE) - Control flow with **relative jumps** (JUMP, JUMP_IF_FALSE, JUMP_IF_TRUE, BREAK, CONTINUE) -- All array operations (MAKE_ARRAY, ARRAY_GET, ARRAY_SET, 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) +- Basic function operations (MAKE_FUNCTION, CALL, RETURN) with parameter binding - HALT instruction ## Design Decisions @@ -95,7 +96,5 @@ It's where Shrimp live. 🚧 **Still TODO**: - Exception handling (PUSH_TRY, POP_TRY, THROW) -- Function operations (MAKE_FUNCTION, CALL, TAIL_CALL, RETURN) -- TypeScript interop (CALL_TYPESCRIPT) - -**Note**: BREAK and CONTINUE are implemented but need CALL/RETURN to be properly tested with the iterator pattern. \ No newline at end of file +- Advanced function features (TAIL_CALL, variadic params, kwargs, default parameters) +- TypeScript interop (CALL_TYPESCRIPT) \ No newline at end of file diff --git a/src/value.ts b/src/value.ts index 7972be2..cf27641 100644 --- a/src/value.ts +++ b/src/value.ts @@ -10,7 +10,7 @@ export type Value = | { type: 'function', params: string[], - defaults: Record, + defaults: Record, // indices into constants body: number, parentScope: Scope, variadic: boolean, diff --git a/src/vm.ts b/src/vm.ts index bbc12ff..40c4c78 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -110,8 +110,10 @@ export class VM { case OpCode.LOAD: const varName = instruction.operand as string const value = this.scope.get(varName) + if (value === undefined) throw new Error(`Undefined variable: ${varName}`) + this.stack.push(value) break @@ -178,8 +180,10 @@ export class VM { case OpCode.MAKE_ARRAY: const arraySize = instruction.operand as number const items: Value[] = [] + for (let i = 0; i < arraySize; i++) items.unshift(this.stack.pop()!) + this.stack.push({ type: 'array', value: items }) break @@ -224,47 +228,133 @@ export class VM { case OpCode.ARRAY_LEN: const lenArray = this.stack.pop()! + if (lenArray.type !== 'array') throw new Error('ARRAY_LEN: not an array') + this.stack.push({ type: 'number', value: lenArray.value.length }) break case OpCode.MAKE_DICT: const dictPairs = instruction.operand as number const dict = new Map() + for (let i = 0; i < dictPairs; i++) { const value = this.stack.pop()! const key = this.stack.pop()! dict.set(toString(key), value) } + this.stack.push({ type: 'dict', value: dict }) break case OpCode.DICT_GET: const getKey = this.stack.pop()! const getDict = this.stack.pop()! + if (getDict.type !== 'dict') throw new Error('DICT_GET: not a dict') - this.stack.push(getDict.value.get(toString(getKey)) || { type: 'null', value: null }) + + this.stack.push(getDict.value.get(toString(getKey)) || toValue(null)) break case OpCode.DICT_SET: const dictSetValue = this.stack.pop()! const dictSetKey = this.stack.pop()! const dictSet = this.stack.pop()! + if (dictSet.type !== 'dict') throw new Error('DICT_SET: not a dict') + dictSet.value.set(toString(dictSetKey), dictSetValue) break case OpCode.DICT_HAS: const hasKey = this.stack.pop()! const hasDict = this.stack.pop()! + if (hasDict.type !== 'dict') throw new Error('DICT_HAS: not a dict') + this.stack.push({ type: 'boolean', value: hasDict.value.has(toString(hasKey)) }) break + case OpCode.MAKE_FUNCTION: + const fnDefIdx = instruction.operand as number + const fnDef = this.constants[fnDefIdx] + + if (!fnDef || fnDef.type !== 'function_def') + throw new Error(`Invalid function definition index: ${fnDefIdx}`) + + this.stack.push({ + type: 'function', + params: fnDef.params, + defaults: fnDef.defaults, + body: fnDef.body, + variadic: fnDef.variadic, + kwargs: fnDef.kwargs, + parentScope: this.scope + }) + break + + case OpCode.CALL: + const argCount = instruction.operand as number + + const args: Value[] = [] + for (let i = 0; i < argCount; i++) + args.unshift(this.stack.pop()!) + + const fn = this.stack.pop()! + + if (fn.type !== 'function') + throw new Error('CALL: not a function') + + if (this.callStack.length > 0) + this.callStack[this.callStack.length - 1]!.isBreakTarget = true + + this.callStack.push({ + returnAddress: this.pc, + returnScope: this.scope, + isBreakTarget: false, + continueAddress: undefined + }) + + this.scope = new Scope(fn.parentScope) + + for (let i = 0; i < fn.params.length; i++) { + const paramName = fn.params[i]! + const argValue = args[i] + + if (argValue !== undefined) { + this.scope.set(paramName, argValue) + } 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') + this.scope.set(paramName, defaultValue) + } else { + this.scope.set(paramName, toValue(null)) + } + } + + // subtract 1 because pc was incremented + this.pc = fn.body - 1 + break + + case OpCode.RETURN: + const returnValue = this.stack.length ? this.stack.pop()! : toValue(null) + + const frame = this.callStack.pop() + if (!frame) + throw new Error('RETURN: no call frame to return from') + + this.scope = frame.returnScope + this.pc = frame.returnAddress + + this.stack.push(returnValue) + break + default: throw `Unknown op: ${instruction.op}` } diff --git a/tests/functions.test.ts b/tests/functions.test.ts new file mode 100644 index 0000000..a8715ce --- /dev/null +++ b/tests/functions.test.ts @@ -0,0 +1,124 @@ +import { test, expect } from "bun:test" +import { VM } from "#vm" +import { OpCode } from "#opcode" + +test("MAKE_FUNCTION - creates function with captured scope", async () => { + const vm = new VM({ + instructions: [ + { op: OpCode.MAKE_FUNCTION, operand: 0 } + ], + constants: [ + { + type: 'function_def', + params: [], + defaults: {}, + body: 999, + variadic: false, + kwargs: false + } + ] + }) + + const result = await vm.run() + expect(result.type).toBe('function') + if (result.type === 'function') { + expect(result.body).toBe(999) + expect(result.params).toEqual([]) + } +}) + +test("CALL and RETURN - basic function call", async () => { + // Function that returns 42 + const vm = new VM({ + instructions: [ + // 0: Create and call the function + { op: OpCode.MAKE_FUNCTION, operand: 0 }, + { op: OpCode.CALL, operand: 0 }, // call with 0 args + { op: OpCode.HALT }, + + // 3: Function body (starts here, address = 3) + { op: OpCode.PUSH, operand: 1 }, // push 42 + { op: OpCode.RETURN } + ], + constants: [ + { + type: 'function_def', + params: [], + defaults: {}, + body: 3, // function body starts at instruction 3 + variadic: false, + kwargs: false + }, + { type: 'number', value: 42 } + ] + }) + + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 42 }) +}) + +test("CALL and RETURN - function with one parameter", async () => { + // Function that returns its parameter + const vm = new VM({ + instructions: [ + // 0: Push argument and call function + { op: OpCode.MAKE_FUNCTION, operand: 0 }, + { op: OpCode.PUSH, operand: 1 }, // argument: 100 + { op: OpCode.CALL, operand: 1 }, // call with 1 arg + { op: OpCode.HALT }, + + // 4: Function body + { op: OpCode.LOAD, operand: 'x' }, // load parameter x + { op: OpCode.RETURN } + ], + constants: [ + { + type: 'function_def', + params: ['x'], + defaults: {}, + body: 4, + variadic: false, + kwargs: false + }, + { type: 'number', value: 100 } + ] + }) + + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 100 }) +}) + +test("CALL and RETURN - function with two parameters", async () => { + // Function that adds two parameters + const vm = new VM({ + instructions: [ + // 0: Push arguments and call function + { op: OpCode.MAKE_FUNCTION, operand: 0 }, + { op: OpCode.PUSH, operand: 1 }, // arg1: 10 + { op: OpCode.PUSH, operand: 2 }, // arg2: 20 + { op: OpCode.CALL, operand: 2 }, // call with 2 args + { op: OpCode.HALT }, + + // 5: Function body + { op: OpCode.LOAD, operand: 'a' }, + { op: OpCode.LOAD, operand: 'b' }, + { op: OpCode.ADD }, + { op: OpCode.RETURN } + ], + constants: [ + { + type: 'function_def', + params: ['a', 'b'], + defaults: {}, + body: 5, + variadic: false, + kwargs: false + }, + { type: 'number', value: 10 }, + { type: 'number', value: 20 } + ] + }) + + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 30 }) +})