CALL / RETURN / MAKE_FUNCTION
This commit is contained in:
parent
6f6ddcea89
commit
25ed12b3ce
15
README.md
15
README.md
|
|
@ -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.
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
92
src/vm.ts
92
src/vm.ts
|
|
@ -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
124
tests/functions.test.ts
Normal 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 })
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user