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
|
||||
- [x] MAKE_FUNCTION
|
||||
- [x] CALL
|
||||
- [ ] TAIL_CALL
|
||||
- [x] TAIL_CALL
|
||||
- [x] RETURN
|
||||
|
||||
### Arrays
|
||||
|
|
@ -77,7 +77,7 @@ It's where Shrimp live.
|
|||
|
||||
## Test Status
|
||||
|
||||
✅ **66 tests passing** covering:
|
||||
✅ **70 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)
|
||||
|
|
@ -86,9 +86,10 @@ It's where Shrimp live.
|
|||
- 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 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
|
||||
- TypeScript interop (CALL_NATIVE) with sync and async functions
|
||||
- Native function interop (CALL_NATIVE) with sync and async functions
|
||||
- HALT instruction
|
||||
|
||||
## 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
|
||||
|
||||
🚧 **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
|
||||
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:
|
||||
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