This commit is contained in:
Chris Wanstrath 2025-10-05 20:08:00 -07:00
parent 4b3c9e8bfc
commit 8754afb536
2 changed files with 55 additions and 61 deletions

View File

@ -148,14 +148,17 @@ export class VM {
case OpCode.BREAK: case OpCode.BREAK:
// Unwind call stack until we find a frame marked as break target // Unwind call stack until we find a frame marked as break target
let foundBreakTarget = false
while (this.callStack.length) { while (this.callStack.length) {
const frame = this.callStack.pop()! const frame = this.callStack.pop()!
this.scope = frame.returnScope this.scope = frame.returnScope
this.pc = frame.returnAddress this.pc = frame.returnAddress
if (frame.isBreakTarget) if (frame.isBreakTarget) {
foundBreakTarget = true
break break
} }
if (this.callStack.length === 0) }
if (!foundBreakTarget)
throw new Error('BREAK: no break target found') throw new Error('BREAK: no break target found')
break break

View File

@ -517,66 +517,57 @@ test("DICT_HAS - checks key missing", async () => {
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false }) expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
}) })
test("BREAK - exits from iterator function", async () => { test("BREAK - throws error when no break target", async () => {
// Simulate a loop with iterator pattern: function calls can be broken out of // BREAK requires a break target frame on the call stack
// We'll need to manually construct bytecode since we need CALL/RETURN // A single function call has no previous frame to mark as break target
const { VM } = await import("#vm") const bytecode = toBytecode(`
const { OpCode } = await import("#opcode") MAKE_FUNCTION () #3
const { toValue } = await import("#value") CALL #0
HALT
BREAK
`)
const vm = new VM({ try {
instructions: [ await run(bytecode)
// 0: Push initial value expect(true).toBe(false) // Should not reach here
{ op: OpCode.PUSH, operand: 0 }, // counter = 0 } catch (e: any) {
{ op: OpCode.STORE, operand: 'counter' }, expect(e.message).toContain('no break target found')
// 2: Create a simple "iterator" function
{ op: OpCode.MAKE_FUNCTION, operand: 0 },
{ op: OpCode.STORE, operand: 'iter' },
{ op: OpCode.JUMP, operand: 7 }, // skip function body
// 5: Function body (will be called in a loop)
{ op: OpCode.LOAD, operand: 'counter' },
{ op: OpCode.PUSH, operand: 1 }, // index 1 = number 5
{ op: OpCode.GTE },
{ op: OpCode.JUMP_IF_TRUE, operand: 1 }, // if counter >= 5, break
{ op: OpCode.BREAK },
{ op: OpCode.LOAD, operand: 'counter' },
{ op: OpCode.PUSH, operand: 2 }, // index 2 = number 1
{ op: OpCode.ADD },
{ op: OpCode.STORE, operand: 'counter' },
{ op: OpCode.RETURN },
// 12: Main code - call iterator in a loop
{ op: OpCode.LOAD, operand: 'iter' },
{ op: OpCode.CALL, operand: 0 }, // Call with 0 args
{ op: OpCode.JUMP, operand: -2 }, // loop back
// After break, we end up here
{ op: OpCode.LOAD, operand: 'counter' },
],
constants: [
toValue(0), // index 0
toValue(5), // index 1
toValue(1), // index 2
{ // index 3 - function definition
type: 'function_def',
params: [],
defaults: {},
body: 5,
variadic: false,
kwargs: false
} }
]
})
// Note: This test would require CALL/RETURN to be implemented
// For now, let's skip this and test BREAK with a simpler approach
expect(true).toBe(true) // placeholder
}) })
test("CONTINUE - skips to loop start", async () => { test("BREAK - exits from nested function call", async () => {
// CONTINUE requires function call frames with continueAddress set // BREAK unwinds to the break target (the outer function's frame)
// This will be tested once we implement CALL/RETURN // Main calls outer, outer calls inner, inner BREAKs back to outer's caller (main)
expect(true).toBe(true) // placeholder const bytecode = toBytecode(`
MAKE_FUNCTION () #4
CALL #0
PUSH 42
HALT
MAKE_FUNCTION () #7
CALL #0
PUSH 99
RETURN
BREAK
`)
const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 42 })
})
test("CONTINUE - throws error when no continue target", async () => {
// CONTINUE requires continueAddress to be set on a frame
// Since we have no instruction to set it yet, this should throw
const bytecode = toBytecode(`
MAKE_FUNCTION () #3
CALL #0
HALT
CONTINUE
`)
try {
await run(bytecode)
expect(true).toBe(false) // Should not reach here
} catch (e: any) {
expect(e.message).toContain('no continue target found')
}
}) })