diff --git a/src/bytecode.ts b/src/bytecode.ts index ae8b190..f98b96b 100644 --- a/src/bytecode.ts +++ b/src/bytecode.ts @@ -17,12 +17,15 @@ export type Constant = // // 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) // name -> variable/function name (e.g., LOAD x, CALL_NATIVE add) // 42 -> number constant (e.g., PUSH 42) // "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: // MAKE_FUNCTION (x y) #7 -> basic function @@ -156,6 +159,16 @@ export function toBytecode(str: string): Bytecode /* throws */ { bytecode.constants.push(toValue(parseFloat(operand))) 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)) { // variable operandValue = operand diff --git a/tests/functions.test.ts b/tests/functions.test.ts index a8715ce..929b953 100644 --- a/tests/functions.test.ts +++ b/tests/functions.test.ts @@ -1,25 +1,13 @@ import { test, expect } from "bun:test" +import { toBytecode } from "#bytecode" 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 bytecode = toBytecode(` + MAKE_FUNCTION () #999 + `) - const result = await vm.run() + const result = await new VM(bytecode).run() expect(result.type).toBe('function') if (result.type === 'function') { 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 () => { - // 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 }, + const bytecode = toBytecode(` + MAKE_FUNCTION () #3 + CALL #0 + HALT + PUSH 42 + RETURN + `) - // 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() + const result = await new VM(bytecode).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 }, + const bytecode = toBytecode(` + MAKE_FUNCTION (x) #4 + PUSH 100 + CALL #1 + HALT + LOAD x + RETURN + `) - // 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() + const result = await new VM(bytecode).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 }, + const bytecode = toBytecode(` + MAKE_FUNCTION (a b) #5 + PUSH 10 + PUSH 20 + CALL #2 + HALT + LOAD a + LOAD b + ADD + RETURN + `) - // 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() + const result = await new VM(bytecode).run() expect(result).toEqual({ type: 'number', value: 30 }) }) diff --git a/tests/tail-call.test.ts b/tests/tail-call.test.ts index e1d463e..4258261 100644 --- a/tests/tail-call.test.ts +++ b/tests/tail-call.test.ts @@ -1,7 +1,6 @@ import { test, expect } from "bun:test" +import { toBytecode } from "#bytecode" 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 @@ -9,48 +8,27 @@ test("TAIL_CALL - basic tail recursive countdown", async () => { // 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 }, + const bytecode = toBytecode(` + MAKE_FUNCTION (n) #6 + STORE countdown + LOAD countdown + PUSH 5 + CALL #1 + HALT + LOAD n + PUSH 0 + EQ + JUMP_IF_FALSE #2 + PUSH "done" + RETURN + LOAD countdown + LOAD n + PUSH 1 + SUB + TAIL_CALL #1 + `) - // 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() + const result = await new VM(bytecode).run() 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 // 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 }, + const bytecode = toBytecode(` + MAKE_FUNCTION (n acc) #7 + STORE sum + LOAD sum + PUSH 10 + PUSH 0 + CALL #2 + HALT + LOAD n + 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 - { 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() + const result = await new VM(bytecode).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 }, + const bytecode = toBytecode(` + MAKE_FUNCTION (n) #6 + STORE deep + LOAD deep + PUSH 10000 + CALL #1 + HALT + LOAD n + PUSH 0 + LTE + JUMP_IF_FALSE #2 + PUSH "success" + RETURN + LOAD deep + LOAD n + PUSH 1 + SUB + TAIL_CALL #1 + `) - // 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() + const result = await new VM(bytecode).run() 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) // 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 }, + const bytecode = toBytecode(` + MAKE_FUNCTION (n) #8 + STORE even + MAKE_FUNCTION (n) #19 + STORE odd + LOAD even + PUSH 7 + CALL #1 + HALT + LOAD n + 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 - { 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() + const result = await new VM(bytecode).run() expect(result).toEqual({ type: 'boolean', value: false }) // 7 is odd })