forked from defunkt/ReefVM
tail calls
This commit is contained in:
parent
66c46677e6
commit
45e4c29df4
11
README.md
11
README.md
|
|
@ -54,7 +54,7 @@ It's where Shrimp live.
|
||||||
### Functions
|
### Functions
|
||||||
- [x] MAKE_FUNCTION
|
- [x] MAKE_FUNCTION
|
||||||
- [x] CALL
|
- [x] CALL
|
||||||
- [ ] TAIL_CALL
|
- [x] TAIL_CALL
|
||||||
- [x] RETURN
|
- [x] RETURN
|
||||||
|
|
||||||
### Arrays
|
### Arrays
|
||||||
|
|
@ -77,7 +77,7 @@ It's where Shrimp live.
|
||||||
|
|
||||||
## Test Status
|
## Test Status
|
||||||
|
|
||||||
✅ **66 tests passing** covering:
|
✅ **70 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)
|
||||||
|
|
@ -86,9 +86,10 @@ It's where Shrimp live.
|
||||||
- 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_PUSH, 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
|
- Function operations (MAKE_FUNCTION, CALL, TAIL_CALL, RETURN) with parameter binding
|
||||||
|
- **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
|
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
|
||||||
- TypeScript interop (CALL_NATIVE) with sync and async functions
|
- Native function interop (CALL_NATIVE) with sync and async functions
|
||||||
- HALT instruction
|
- HALT instruction
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
|
|
@ -98,4 +99,4 @@ It's where Shrimp live.
|
||||||
- **Short-circuiting via compiler**: No AND/OR opcodes—compilers use JUMP patterns for proper short-circuit evaluation
|
- **Short-circuiting via compiler**: No AND/OR opcodes—compilers use JUMP patterns for proper short-circuit evaluation
|
||||||
|
|
||||||
🚧 **Still TODO**:
|
🚧 **Still TODO**:
|
||||||
- Advanced function features (TAIL_CALL, variadic params, kwargs, default parameters)
|
- Advanced function features (variadic params, kwargs, named arguments in CALL)
|
||||||
36
src/vm.ts
36
src/vm.ts
|
|
@ -399,6 +399,42 @@ export class VM {
|
||||||
this.pc = fn.body - 1
|
this.pc = fn.body - 1
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case OpCode.TAIL_CALL:
|
||||||
|
const tailArgCount = instruction.operand as number
|
||||||
|
|
||||||
|
const tailArgs: Value[] = []
|
||||||
|
for (let i = 0; i < tailArgCount; i++)
|
||||||
|
tailArgs.unshift(this.stack.pop()!)
|
||||||
|
|
||||||
|
const tailFn = this.stack.pop()!
|
||||||
|
|
||||||
|
if (tailFn.type !== 'function')
|
||||||
|
throw new Error('TAIL_CALL: not a function')
|
||||||
|
|
||||||
|
this.scope = new Scope(tailFn.parentScope)
|
||||||
|
|
||||||
|
// same logic as CALL
|
||||||
|
for (let i = 0; i < tailFn.params.length; i++) {
|
||||||
|
const paramName = tailFn.params[i]!
|
||||||
|
const argValue = tailArgs[i]
|
||||||
|
|
||||||
|
if (argValue !== undefined) {
|
||||||
|
this.scope.set(paramName, argValue)
|
||||||
|
} else if (tailFn.defaults[paramName] !== undefined) {
|
||||||
|
const defaultIdx = tailFn.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 = tailFn.body - 1
|
||||||
|
break
|
||||||
|
|
||||||
case OpCode.RETURN:
|
case OpCode.RETURN:
|
||||||
const returnValue = this.stack.length ? this.stack.pop()! : toValue(null)
|
const returnValue = this.stack.length ? this.stack.pop()! : toValue(null)
|
||||||
|
|
||||||
|
|
|
||||||
229
tests/tail-call.test.ts
Normal file
229
tests/tail-call.test.ts
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
import { test, expect } from "bun:test"
|
||||||
|
import { VM } from "#vm"
|
||||||
|
import { OpCode } from "#opcode"
|
||||||
|
import { toValue } from "#value"
|
||||||
|
|
||||||
|
test("TAIL_CALL - basic tail recursive countdown", async () => {
|
||||||
|
// Tail recursive function that counts down to 0
|
||||||
|
// function countdown(n) {
|
||||||
|
// if (n === 0) return "done"
|
||||||
|
// return countdown(n - 1) // tail call
|
||||||
|
// }
|
||||||
|
const vm = new VM({
|
||||||
|
instructions: [
|
||||||
|
// 0: Setup - create function and call it
|
||||||
|
{ op: OpCode.MAKE_FUNCTION, operand: 0 },
|
||||||
|
{ op: OpCode.STORE, operand: 'countdown' },
|
||||||
|
{ op: OpCode.LOAD, operand: 'countdown' },
|
||||||
|
{ op: OpCode.PUSH, operand: 1 }, // start with 5
|
||||||
|
{ op: OpCode.CALL, operand: 1 },
|
||||||
|
{ op: OpCode.HALT },
|
||||||
|
|
||||||
|
// 6: Function body
|
||||||
|
{ op: OpCode.LOAD, operand: 'n' },
|
||||||
|
{ op: OpCode.PUSH, operand: 2 }, // 0
|
||||||
|
{ op: OpCode.EQ },
|
||||||
|
{ op: OpCode.JUMP_IF_FALSE, operand: 2 }, // if n !== 0, jump to recursive case
|
||||||
|
{ op: OpCode.PUSH, operand: 3 }, // return "done"
|
||||||
|
{ op: OpCode.RETURN },
|
||||||
|
|
||||||
|
// 12: Recursive case (tail call)
|
||||||
|
{ op: OpCode.LOAD, operand: 'countdown' },
|
||||||
|
{ op: OpCode.LOAD, operand: 'n' },
|
||||||
|
{ op: OpCode.PUSH, operand: 4 }, // 1
|
||||||
|
{ op: OpCode.SUB },
|
||||||
|
{ op: OpCode.TAIL_CALL, operand: 1 } // tail call with n-1
|
||||||
|
],
|
||||||
|
constants: [
|
||||||
|
{
|
||||||
|
type: 'function_def',
|
||||||
|
params: ['n'],
|
||||||
|
defaults: {},
|
||||||
|
body: 6,
|
||||||
|
variadic: false,
|
||||||
|
kwargs: false
|
||||||
|
},
|
||||||
|
toValue(5),
|
||||||
|
toValue(0),
|
||||||
|
toValue('done'),
|
||||||
|
toValue(1)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'done' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("TAIL_CALL - tail recursive sum with accumulator", async () => {
|
||||||
|
// function sum(n, acc = 0) {
|
||||||
|
// if (n === 0) return acc
|
||||||
|
// return sum(n - 1, acc + n) // tail call
|
||||||
|
// }
|
||||||
|
const vm = new VM({
|
||||||
|
instructions: [
|
||||||
|
// 0: Setup
|
||||||
|
{ op: OpCode.MAKE_FUNCTION, operand: 0 },
|
||||||
|
{ op: OpCode.STORE, operand: 'sum' },
|
||||||
|
{ op: OpCode.LOAD, operand: 'sum' },
|
||||||
|
{ op: OpCode.PUSH, operand: 1 }, // n = 10
|
||||||
|
{ op: OpCode.PUSH, operand: 2 }, // acc = 0
|
||||||
|
{ op: OpCode.CALL, operand: 2 },
|
||||||
|
{ op: OpCode.HALT },
|
||||||
|
|
||||||
|
// 7: Function body
|
||||||
|
{ op: OpCode.LOAD, operand: 'n' },
|
||||||
|
{ op: OpCode.PUSH, operand: 2 }, // 0
|
||||||
|
{ op: OpCode.EQ },
|
||||||
|
{ op: OpCode.JUMP_IF_FALSE, operand: 2 },
|
||||||
|
{ op: OpCode.LOAD, operand: 'acc' },
|
||||||
|
{ op: OpCode.RETURN },
|
||||||
|
|
||||||
|
// 13: Recursive case
|
||||||
|
{ op: OpCode.LOAD, operand: 'sum' },
|
||||||
|
{ op: OpCode.LOAD, operand: 'n' },
|
||||||
|
{ op: OpCode.PUSH, operand: 3 }, // 1
|
||||||
|
{ op: OpCode.SUB },
|
||||||
|
{ op: OpCode.LOAD, operand: 'acc' },
|
||||||
|
{ op: OpCode.LOAD, operand: 'n' },
|
||||||
|
{ op: OpCode.ADD },
|
||||||
|
{ op: OpCode.TAIL_CALL, operand: 2 }
|
||||||
|
],
|
||||||
|
constants: [
|
||||||
|
{
|
||||||
|
type: 'function_def',
|
||||||
|
params: ['n', 'acc'],
|
||||||
|
defaults: {},
|
||||||
|
body: 7,
|
||||||
|
variadic: false,
|
||||||
|
kwargs: false
|
||||||
|
},
|
||||||
|
toValue(10),
|
||||||
|
toValue(0),
|
||||||
|
toValue(1)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'number', value: 55 }) // sum of 1..10
|
||||||
|
})
|
||||||
|
|
||||||
|
test("TAIL_CALL - doesn't overflow stack with deep recursion", async () => {
|
||||||
|
// This would overflow the stack with regular CALL
|
||||||
|
// but should work fine with TAIL_CALL
|
||||||
|
const vm = new VM({
|
||||||
|
instructions: [
|
||||||
|
// 0: Setup
|
||||||
|
{ op: OpCode.MAKE_FUNCTION, operand: 0 },
|
||||||
|
{ op: OpCode.STORE, operand: 'deep' },
|
||||||
|
{ op: OpCode.LOAD, operand: 'deep' },
|
||||||
|
{ op: OpCode.PUSH, operand: 1 }, // 10000 iterations
|
||||||
|
{ op: OpCode.CALL, operand: 1 },
|
||||||
|
{ op: OpCode.HALT },
|
||||||
|
|
||||||
|
// 6: Function body
|
||||||
|
{ op: OpCode.LOAD, operand: 'n' },
|
||||||
|
{ op: OpCode.PUSH, operand: 2 }, // 0
|
||||||
|
{ op: OpCode.LTE },
|
||||||
|
{ op: OpCode.JUMP_IF_FALSE, operand: 2 },
|
||||||
|
{ op: OpCode.PUSH, operand: 3 }, // "success"
|
||||||
|
{ op: OpCode.RETURN },
|
||||||
|
|
||||||
|
// 12: Tail recursive case
|
||||||
|
{ op: OpCode.LOAD, operand: 'deep' },
|
||||||
|
{ op: OpCode.LOAD, operand: 'n' },
|
||||||
|
{ op: OpCode.PUSH, operand: 4 }, // 1
|
||||||
|
{ op: OpCode.SUB },
|
||||||
|
{ op: OpCode.TAIL_CALL, operand: 1 }
|
||||||
|
],
|
||||||
|
constants: [
|
||||||
|
{
|
||||||
|
type: 'function_def',
|
||||||
|
params: ['n'],
|
||||||
|
defaults: {},
|
||||||
|
body: 6,
|
||||||
|
variadic: false,
|
||||||
|
kwargs: false
|
||||||
|
},
|
||||||
|
toValue(10000), // This would overflow with regular recursion
|
||||||
|
toValue(0),
|
||||||
|
toValue('success'),
|
||||||
|
toValue(1)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'success' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("TAIL_CALL - tail call to different function", async () => {
|
||||||
|
// TAIL_CALL can call a different function (mutual recursion)
|
||||||
|
// function even(n) { return n === 0 ? true : odd(n - 1) }
|
||||||
|
// function odd(n) { return n === 0 ? false : even(n - 1) }
|
||||||
|
const vm = new VM({
|
||||||
|
instructions: [
|
||||||
|
// 0: Setup both functions
|
||||||
|
{ op: OpCode.MAKE_FUNCTION, operand: 0 }, // even
|
||||||
|
{ op: OpCode.STORE, operand: 'even' },
|
||||||
|
{ op: OpCode.MAKE_FUNCTION, operand: 1 }, // odd
|
||||||
|
{ op: OpCode.STORE, operand: 'odd' },
|
||||||
|
{ op: OpCode.LOAD, operand: 'even' },
|
||||||
|
{ op: OpCode.PUSH, operand: 2 }, // test with 7
|
||||||
|
{ op: OpCode.CALL, operand: 1 },
|
||||||
|
{ op: OpCode.HALT },
|
||||||
|
|
||||||
|
// 8: even function body
|
||||||
|
{ op: OpCode.LOAD, operand: 'n' },
|
||||||
|
{ op: OpCode.PUSH, operand: 3 }, // 0
|
||||||
|
{ op: OpCode.EQ },
|
||||||
|
{ op: OpCode.JUMP_IF_FALSE, operand: 2 },
|
||||||
|
{ op: OpCode.PUSH, operand: 4 }, // true
|
||||||
|
{ op: OpCode.RETURN },
|
||||||
|
// Tail call to odd
|
||||||
|
{ op: OpCode.LOAD, operand: 'odd' },
|
||||||
|
{ op: OpCode.LOAD, operand: 'n' },
|
||||||
|
{ op: OpCode.PUSH, operand: 5 }, // 1
|
||||||
|
{ op: OpCode.SUB },
|
||||||
|
{ op: OpCode.TAIL_CALL, operand: 1 },
|
||||||
|
|
||||||
|
// 19: odd function body
|
||||||
|
{ op: OpCode.LOAD, operand: 'n' },
|
||||||
|
{ op: OpCode.PUSH, operand: 3 }, // 0
|
||||||
|
{ op: OpCode.EQ },
|
||||||
|
{ op: OpCode.JUMP_IF_FALSE, operand: 2 },
|
||||||
|
{ op: OpCode.PUSH, operand: 6 }, // false
|
||||||
|
{ op: OpCode.RETURN },
|
||||||
|
// Tail call to even
|
||||||
|
{ op: OpCode.LOAD, operand: 'even' },
|
||||||
|
{ op: OpCode.LOAD, operand: 'n' },
|
||||||
|
{ op: OpCode.PUSH, operand: 5 }, // 1
|
||||||
|
{ op: OpCode.SUB },
|
||||||
|
{ op: OpCode.TAIL_CALL, operand: 1 }
|
||||||
|
],
|
||||||
|
constants: [
|
||||||
|
{
|
||||||
|
type: 'function_def',
|
||||||
|
params: ['n'],
|
||||||
|
defaults: {},
|
||||||
|
body: 8, // even body
|
||||||
|
variadic: false,
|
||||||
|
kwargs: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function_def',
|
||||||
|
params: ['n'],
|
||||||
|
defaults: {},
|
||||||
|
body: 19, // odd body
|
||||||
|
variadic: false,
|
||||||
|
kwargs: false
|
||||||
|
},
|
||||||
|
toValue(7),
|
||||||
|
toValue(0),
|
||||||
|
toValue(true),
|
||||||
|
toValue(1),
|
||||||
|
toValue(false)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'boolean', value: false }) // 7 is odd
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user