update tests

This commit is contained in:
Chris Wanstrath 2025-10-05 19:56:56 -07:00
parent 8198c555ac
commit 4b3c9e8bfc
3 changed files with 146 additions and 292 deletions

View File

@ -17,12 +17,15 @@ export type Constant =
// //
// Parse bytecode from human-readable string format. // Parse bytecode from human-readable string format.
// Operand types are determined by prefix: // Operand types are determined by prefix/literal:
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3) // #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
// name -> variable/function name (e.g., LOAD x, CALL_NATIVE add) // name -> variable/function name (e.g., LOAD x, CALL_NATIVE add)
// 42 -> number constant (e.g., PUSH 42) // 42 -> number constant (e.g., PUSH 42)
// "str" -> string constant (e.g., PUSH "hello") // "str" -> string constant (e.g., PUSH "hello")
// 'str' -> string constant (e.g., PUSH 'hello') // 'str' -> string constant (e.g., PUSH 'hello')
// true -> boolean constant (e.g., PUSH true)
// false -> boolean constant (e.g., PUSH false)
// null -> null constant (e.g., PUSH null)
// //
// Function definitions: // Function definitions:
// MAKE_FUNCTION (x y) #7 -> basic function // MAKE_FUNCTION (x y) #7 -> basic function
@ -156,6 +159,16 @@ export function toBytecode(str: string): Bytecode /* throws */ {
bytecode.constants.push(toValue(parseFloat(operand))) bytecode.constants.push(toValue(parseFloat(operand)))
operandValue = bytecode.constants.length - 1 operandValue = bytecode.constants.length - 1
} else if (operand === 'true' || operand === 'false') {
// boolean
bytecode.constants.push(toValue(operand === 'true'))
operandValue = bytecode.constants.length - 1
} else if (operand === 'null') {
// null
bytecode.constants.push(toValue(null))
operandValue = bytecode.constants.length - 1
} else if (/^[a-zA-Z_].*$/.test(operand)) { } else if (/^[a-zA-Z_].*$/.test(operand)) {
// variable // variable
operandValue = operand operandValue = operand

View File

@ -1,25 +1,13 @@
import { test, expect } from "bun:test" import { test, expect } from "bun:test"
import { toBytecode } from "#bytecode"
import { VM } from "#vm" import { VM } from "#vm"
import { OpCode } from "#opcode"
test("MAKE_FUNCTION - creates function with captured scope", async () => { test("MAKE_FUNCTION - creates function with captured scope", async () => {
const vm = new VM({ const bytecode = toBytecode(`
instructions: [ MAKE_FUNCTION () #999
{ op: OpCode.MAKE_FUNCTION, operand: 0 } `)
],
constants: [
{
type: 'function_def',
params: [],
defaults: {},
body: 999,
variadic: false,
kwargs: false
}
]
})
const result = await vm.run() const result = await new VM(bytecode).run()
expect(result.type).toBe('function') expect(result.type).toBe('function')
if (result.type === 'function') { if (result.type === 'function') {
expect(result.body).toBe(999) expect(result.body).toBe(999)
@ -28,97 +16,45 @@ test("MAKE_FUNCTION - creates function with captured scope", async () => {
}) })
test("CALL and RETURN - basic function call", async () => { test("CALL and RETURN - basic function call", async () => {
// Function that returns 42 const bytecode = toBytecode(`
const vm = new VM({ MAKE_FUNCTION () #3
instructions: [ CALL #0
// 0: Create and call the function HALT
{ op: OpCode.MAKE_FUNCTION, operand: 0 }, PUSH 42
{ op: OpCode.CALL, operand: 0 }, // call with 0 args RETURN
{ op: OpCode.HALT }, `)
// 3: Function body (starts here, address = 3) const result = await new VM(bytecode).run()
{ 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 }) expect(result).toEqual({ type: 'number', value: 42 })
}) })
test("CALL and RETURN - function with one parameter", async () => { test("CALL and RETURN - function with one parameter", async () => {
// Function that returns its parameter const bytecode = toBytecode(`
const vm = new VM({ MAKE_FUNCTION (x) #4
instructions: [ PUSH 100
// 0: Push argument and call function CALL #1
{ op: OpCode.MAKE_FUNCTION, operand: 0 }, HALT
{ op: OpCode.PUSH, operand: 1 }, // argument: 100 LOAD x
{ op: OpCode.CALL, operand: 1 }, // call with 1 arg RETURN
{ op: OpCode.HALT }, `)
// 4: Function body const result = await new VM(bytecode).run()
{ 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 }) expect(result).toEqual({ type: 'number', value: 100 })
}) })
test("CALL and RETURN - function with two parameters", async () => { test("CALL and RETURN - function with two parameters", async () => {
// Function that adds two parameters const bytecode = toBytecode(`
const vm = new VM({ MAKE_FUNCTION (a b) #5
instructions: [ PUSH 10
// 0: Push arguments and call function PUSH 20
{ op: OpCode.MAKE_FUNCTION, operand: 0 }, CALL #2
{ op: OpCode.PUSH, operand: 1 }, // arg1: 10 HALT
{ op: OpCode.PUSH, operand: 2 }, // arg2: 20 LOAD a
{ op: OpCode.CALL, operand: 2 }, // call with 2 args LOAD b
{ op: OpCode.HALT }, ADD
RETURN
`)
// 5: Function body const result = await new VM(bytecode).run()
{ 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 }) expect(result).toEqual({ type: 'number', value: 30 })
}) })

View File

@ -1,7 +1,6 @@
import { test, expect } from "bun:test" import { test, expect } from "bun:test"
import { toBytecode } from "#bytecode"
import { VM } from "#vm" import { VM } from "#vm"
import { OpCode } from "#opcode"
import { toValue } from "#value"
test("TAIL_CALL - basic tail recursive countdown", async () => { test("TAIL_CALL - basic tail recursive countdown", async () => {
// Tail recursive function that counts down to 0 // Tail recursive function that counts down to 0
@ -9,48 +8,27 @@ test("TAIL_CALL - basic tail recursive countdown", async () => {
// if (n === 0) return "done" // if (n === 0) return "done"
// return countdown(n - 1) // tail call // return countdown(n - 1) // tail call
// } // }
const vm = new VM({ const bytecode = toBytecode(`
instructions: [ MAKE_FUNCTION (n) #6
// 0: Setup - create function and call it STORE countdown
{ op: OpCode.MAKE_FUNCTION, operand: 0 }, LOAD countdown
{ op: OpCode.STORE, operand: 'countdown' }, PUSH 5
{ op: OpCode.LOAD, operand: 'countdown' }, CALL #1
{ op: OpCode.PUSH, operand: 1 }, // start with 5 HALT
{ op: OpCode.CALL, operand: 1 }, LOAD n
{ op: OpCode.HALT }, PUSH 0
EQ
JUMP_IF_FALSE #2
PUSH "done"
RETURN
LOAD countdown
LOAD n
PUSH 1
SUB
TAIL_CALL #1
`)
// 6: Function body const result = await new VM(bytecode).run()
{ 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' }) expect(result).toEqual({ type: 'string', value: 'done' })
}) })
@ -59,99 +37,58 @@ test("TAIL_CALL - tail recursive sum with accumulator", async () => {
// if (n === 0) return acc // if (n === 0) return acc
// return sum(n - 1, acc + n) // tail call // return sum(n - 1, acc + n) // tail call
// } // }
const vm = new VM({ const bytecode = toBytecode(`
instructions: [ MAKE_FUNCTION (n acc) #7
// 0: Setup STORE sum
{ op: OpCode.MAKE_FUNCTION, operand: 0 }, LOAD sum
{ op: OpCode.STORE, operand: 'sum' }, PUSH 10
{ op: OpCode.LOAD, operand: 'sum' }, PUSH 0
{ op: OpCode.PUSH, operand: 1 }, // n = 10 CALL #2
{ op: OpCode.PUSH, operand: 2 }, // acc = 0 HALT
{ op: OpCode.CALL, operand: 2 }, LOAD n
{ op: OpCode.HALT }, PUSH 0
EQ
JUMP_IF_FALSE #2
LOAD acc
RETURN
LOAD sum
LOAD n
PUSH 1
SUB
LOAD acc
LOAD n
ADD
TAIL_CALL #2
`)
// 7: Function body const result = await new VM(bytecode).run()
{ 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 expect(result).toEqual({ type: 'number', value: 55 }) // sum of 1..10
}) })
test("TAIL_CALL - doesn't overflow stack with deep recursion", async () => { test("TAIL_CALL - doesn't overflow stack with deep recursion", async () => {
// This would overflow the stack with regular CALL // This would overflow the stack with regular CALL
// but should work fine with TAIL_CALL // but should work fine with TAIL_CALL
const vm = new VM({ const bytecode = toBytecode(`
instructions: [ MAKE_FUNCTION (n) #6
// 0: Setup STORE deep
{ op: OpCode.MAKE_FUNCTION, operand: 0 }, LOAD deep
{ op: OpCode.STORE, operand: 'deep' }, PUSH 10000
{ op: OpCode.LOAD, operand: 'deep' }, CALL #1
{ op: OpCode.PUSH, operand: 1 }, // 10000 iterations HALT
{ op: OpCode.CALL, operand: 1 }, LOAD n
{ op: OpCode.HALT }, PUSH 0
LTE
JUMP_IF_FALSE #2
PUSH "success"
RETURN
LOAD deep
LOAD n
PUSH 1
SUB
TAIL_CALL #1
`)
// 6: Function body const result = await new VM(bytecode).run()
{ 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' }) expect(result).toEqual({ type: 'string', value: 'success' })
}) })
@ -159,71 +96,39 @@ test("TAIL_CALL - tail call to different function", async () => {
// TAIL_CALL can call a different function (mutual recursion) // TAIL_CALL can call a different function (mutual recursion)
// function even(n) { return n === 0 ? true : odd(n - 1) } // function even(n) { return n === 0 ? true : odd(n - 1) }
// function odd(n) { return n === 0 ? false : even(n - 1) } // function odd(n) { return n === 0 ? false : even(n - 1) }
const vm = new VM({ const bytecode = toBytecode(`
instructions: [ MAKE_FUNCTION (n) #8
// 0: Setup both functions STORE even
{ op: OpCode.MAKE_FUNCTION, operand: 0 }, // even MAKE_FUNCTION (n) #19
{ op: OpCode.STORE, operand: 'even' }, STORE odd
{ op: OpCode.MAKE_FUNCTION, operand: 1 }, // odd LOAD even
{ op: OpCode.STORE, operand: 'odd' }, PUSH 7
{ op: OpCode.LOAD, operand: 'even' }, CALL #1
{ op: OpCode.PUSH, operand: 2 }, // test with 7 HALT
{ op: OpCode.CALL, operand: 1 }, LOAD n
{ op: OpCode.HALT }, PUSH 0
EQ
JUMP_IF_FALSE #2
PUSH true
RETURN
LOAD odd
LOAD n
PUSH 1
SUB
TAIL_CALL #1
LOAD n
PUSH 0
EQ
JUMP_IF_FALSE #2
PUSH false
RETURN
LOAD even
LOAD n
PUSH 1
SUB
TAIL_CALL #1
`)
// 8: even function body const result = await new VM(bytecode).run()
{ 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 expect(result).toEqual({ type: 'boolean', value: false }) // 7 is odd
}) })