CALL / RETURN / MAKE_FUNCTION

This commit is contained in:
Chris Wanstrath 2025-10-05 15:47:34 -07:00
parent 6f6ddcea89
commit 25ed12b3ce
4 changed files with 224 additions and 11 deletions

View File

@ -51,10 +51,10 @@ It's where Shrimp live.
- [ ] THROW - [ ] THROW
### Functions ### Functions
- [ ] MAKE_FUNCTION - [x] MAKE_FUNCTION
- [ ] CALL - [x] CALL
- [ ] TAIL_CALL - [ ] TAIL_CALL
- [ ] RETURN - [x] RETURN
### Arrays ### Arrays
- [x] MAKE_ARRAY - [x] MAKE_ARRAY
@ -76,15 +76,16 @@ It's where Shrimp live.
## Test Status ## Test Status
**40 tests passing** covering: **46 tests passing** covering:
- All stack operations (PUSH, POP, DUP) - All stack operations (PUSH, POP, DUP)
- All arithmetic operations (ADD, SUB, MUL, DIV, MOD) - All arithmetic operations (ADD, SUB, MUL, DIV, MOD)
- All comparison operations (EQ, NEQ, LT, GT, LTE, GTE) - All comparison operations (EQ, NEQ, LT, GT, LTE, GTE)
- Logical operations (NOT, AND/OR patterns with short-circuiting) - Logical operations (NOT, AND/OR patterns with short-circuiting)
- Variable operations (LOAD, STORE) - Variable operations (LOAD, STORE)
- Control flow with **relative jumps** (JUMP, JUMP_IF_FALSE, JUMP_IF_TRUE, BREAK, CONTINUE) - 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) - All dictionary operations (MAKE_DICT, DICT_GET, DICT_SET, DICT_HAS)
- Basic function operations (MAKE_FUNCTION, CALL, RETURN) with parameter binding
- HALT instruction - HALT instruction
## Design Decisions ## Design Decisions
@ -95,7 +96,5 @@ It's where Shrimp live.
🚧 **Still TODO**: 🚧 **Still TODO**:
- Exception handling (PUSH_TRY, POP_TRY, THROW) - Exception handling (PUSH_TRY, POP_TRY, THROW)
- Function operations (MAKE_FUNCTION, CALL, TAIL_CALL, RETURN) - Advanced function features (TAIL_CALL, variadic params, kwargs, default parameters)
- TypeScript interop (CALL_TYPESCRIPT) - TypeScript interop (CALL_TYPESCRIPT)
**Note**: BREAK and CONTINUE are implemented but need CALL/RETURN to be properly tested with the iterator pattern.

View File

@ -10,7 +10,7 @@ export type Value =
| { | {
type: 'function', type: 'function',
params: string[], params: string[],
defaults: Record<string, Value>, defaults: Record<string, number>, // indices into constants
body: number, body: number,
parentScope: Scope, parentScope: Scope,
variadic: boolean, variadic: boolean,

View File

@ -110,8 +110,10 @@ export class VM {
case OpCode.LOAD: case OpCode.LOAD:
const varName = instruction.operand as string const varName = instruction.operand as string
const value = this.scope.get(varName) const value = this.scope.get(varName)
if (value === undefined) if (value === undefined)
throw new Error(`Undefined variable: ${varName}`) throw new Error(`Undefined variable: ${varName}`)
this.stack.push(value) this.stack.push(value)
break break
@ -178,8 +180,10 @@ export class VM {
case OpCode.MAKE_ARRAY: case OpCode.MAKE_ARRAY:
const arraySize = instruction.operand as number const arraySize = instruction.operand as number
const items: Value[] = [] const items: Value[] = []
for (let i = 0; i < arraySize; i++) for (let i = 0; i < arraySize; i++)
items.unshift(this.stack.pop()!) items.unshift(this.stack.pop()!)
this.stack.push({ type: 'array', value: items }) this.stack.push({ type: 'array', value: items })
break break
@ -224,47 +228,133 @@ export class VM {
case OpCode.ARRAY_LEN: case OpCode.ARRAY_LEN:
const lenArray = this.stack.pop()! const lenArray = this.stack.pop()!
if (lenArray.type !== 'array') if (lenArray.type !== 'array')
throw new Error('ARRAY_LEN: not an array') throw new Error('ARRAY_LEN: not an array')
this.stack.push({ type: 'number', value: lenArray.value.length }) this.stack.push({ type: 'number', value: lenArray.value.length })
break break
case OpCode.MAKE_DICT: case OpCode.MAKE_DICT:
const dictPairs = instruction.operand as number const dictPairs = instruction.operand as number
const dict = new Map<string, Value>() const dict = new Map<string, Value>()
for (let i = 0; i < dictPairs; i++) { for (let i = 0; i < dictPairs; i++) {
const value = this.stack.pop()! const value = this.stack.pop()!
const key = this.stack.pop()! const key = this.stack.pop()!
dict.set(toString(key), value) dict.set(toString(key), value)
} }
this.stack.push({ type: 'dict', value: dict }) this.stack.push({ type: 'dict', value: dict })
break break
case OpCode.DICT_GET: case OpCode.DICT_GET:
const getKey = this.stack.pop()! const getKey = this.stack.pop()!
const getDict = this.stack.pop()! const getDict = this.stack.pop()!
if (getDict.type !== 'dict') if (getDict.type !== 'dict')
throw new Error('DICT_GET: not a 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 break
case OpCode.DICT_SET: case OpCode.DICT_SET:
const dictSetValue = this.stack.pop()! const dictSetValue = this.stack.pop()!
const dictSetKey = this.stack.pop()! const dictSetKey = this.stack.pop()!
const dictSet = this.stack.pop()! const dictSet = this.stack.pop()!
if (dictSet.type !== 'dict') if (dictSet.type !== 'dict')
throw new Error('DICT_SET: not a dict') throw new Error('DICT_SET: not a dict')
dictSet.value.set(toString(dictSetKey), dictSetValue) dictSet.value.set(toString(dictSetKey), dictSetValue)
break break
case OpCode.DICT_HAS: case OpCode.DICT_HAS:
const hasKey = this.stack.pop()! const hasKey = this.stack.pop()!
const hasDict = this.stack.pop()! const hasDict = this.stack.pop()!
if (hasDict.type !== 'dict') if (hasDict.type !== 'dict')
throw new Error('DICT_HAS: not a dict') throw new Error('DICT_HAS: not a dict')
this.stack.push({ type: 'boolean', value: hasDict.value.has(toString(hasKey)) }) this.stack.push({ type: 'boolean', value: hasDict.value.has(toString(hasKey)) })
break 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: default:
throw `Unknown op: ${instruction.op}` throw `Unknown op: ${instruction.op}`
} }

124
tests/functions.test.ts Normal file
View File

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