ReefVM/tests/repl.test.ts

150 lines
3.6 KiB
TypeScript

import { test, expect } from "bun:test"
import { VM, toBytecode } from "#reef"
test("REPL mode - demonstrates PC reset problem", async () => {
// Track how many times each line executes
let line1Count = 0
let line2Count = 0
// Line 1: Set x = 5, track execution
const line1 = toBytecode([
["LOAD", "trackLine1"],
["PUSH", 0],
["PUSH", 0],
["CALL"],
["POP"],
["PUSH", 5],
["STORE", "x"]
])
const vm = new VM(line1, {
trackLine1: () => { line1Count++; return null },
trackLine2: () => { line2Count++; return null }
})
await vm.run()
expect(vm.scope.get("x")).toEqual({ type: "number", value: 5 })
expect(line1Count).toBe(1)
expect(line2Count).toBe(0)
// Line 2: Track execution, load x
const line2 = toBytecode([
["LOAD", "trackLine2"],
["PUSH", 0],
["PUSH", 0],
["CALL"],
["POP"],
["LOAD", "x"]
])
// Append line2 bytecode to VM (what a REPL would do)
vm.instructions.push(...line2.instructions)
for (const constant of line2.constants) {
if (!vm.constants.includes(constant)) {
vm.constants.push(constant)
}
}
// Current behavior: run() resets PC to 0, re-executing everything
await vm.run()
// PROBLEM: Line 1 ran AGAIN (count is now 2, not 1)
// This is the issue for REPL - side effects run multiple times
expect(line1Count).toBe(2) // Ran twice! Should still be 1
expect(line2Count).toBe(1) // This is correct
// What we WANT for REPL:
// - Only execute the NEW bytecode (line 2)
// - line1Count should stay at 1
// - line2Count should be 1
})
test("REPL mode - continue() executes only new bytecode", async () => {
// Track how many times each line executes
let line1Count = 0
let line2Count = 0
// Line 1: Set x = 5, track execution
const line1 = toBytecode([
["LOAD", "trackLine1"],
["PUSH", 0],
["PUSH", 0],
["CALL"],
["POP"],
["PUSH", 5],
["STORE", "x"]
])
const vm = new VM(line1, {
trackLine1: () => { line1Count++; return null },
trackLine2: () => { line2Count++; return null }
})
await vm.run()
expect(vm.scope.get("x")).toEqual({ type: "number", value: 5 })
expect(line1Count).toBe(1)
expect(line2Count).toBe(0)
// Line 2: Track execution, load x and add 10
const line2 = toBytecode([
["LOAD", "trackLine2"],
["PUSH", 0],
["PUSH", 0],
["CALL"],
["POP"],
["LOAD", "x"],
["PUSH", 10],
["ADD"]
])
// Append line2 bytecode to VM using the helper method
vm.appendBytecode(line2)
// SOLUTION: Use continue() instead of run() to resume from current PC
await vm.continue()
const result = vm.stack[vm.stack.length - 1]
// SUCCESS: Line 1 only ran once, line 2 ran once
expect(line1Count).toBe(1) // Still 1! Side effect didn't re-run
expect(line2Count).toBe(1) // Ran once as expected
expect(result).toEqual({ type: "number", value: 15 }) // 5 + 10
})
test("REPL mode - function calls work across chunks", async () => {
const vm = new VM(toBytecode([]))
await vm.run()
// Line 1: Define a function
const line1 = toBytecode([
["MAKE_FUNCTION", ["x"], ".body"],
["STORE", "add1"],
["JUMP", ".end"],
[".body:"],
["LOAD", "x"],
["PUSH", 1],
["ADD"],
["RETURN"],
[".end:"]
])
vm.appendBytecode(line1)
await vm.continue()
expect(vm.scope.get("add1")?.type).toBe("function")
// Line 2: Call the function
const line2 = toBytecode([
["LOAD", "add1"],
["PUSH", 10],
["PUSH", 1],
["PUSH", 0],
["CALL"]
])
vm.appendBytecode(line2)
const result = await vm.continue()
expect(result).toEqual({ type: "number", value: 11 })
})