150 lines
3.6 KiB
TypeScript
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 })
|
|
})
|