diff --git a/src/vm.ts b/src/vm.ts index 1c3bc34..0bd522c 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -148,14 +148,17 @@ export class VM { case OpCode.BREAK: // Unwind call stack until we find a frame marked as break target + let foundBreakTarget = false while (this.callStack.length) { const frame = this.callStack.pop()! this.scope = frame.returnScope this.pc = frame.returnAddress - if (frame.isBreakTarget) + if (frame.isBreakTarget) { + foundBreakTarget = true break + } } - if (this.callStack.length === 0) + if (!foundBreakTarget) throw new Error('BREAK: no break target found') break diff --git a/tests/basic.test.ts b/tests/basic.test.ts index dc1d898..4d81f7f 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -517,66 +517,57 @@ test("DICT_HAS - checks key missing", async () => { expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false }) }) -test("BREAK - exits from iterator function", async () => { - // Simulate a loop with iterator pattern: function calls can be broken out of - // We'll need to manually construct bytecode since we need CALL/RETURN - const { VM } = await import("#vm") - const { OpCode } = await import("#opcode") - const { toValue } = await import("#value") +test("BREAK - throws error when no break target", async () => { + // BREAK requires a break target frame on the call stack + // A single function call has no previous frame to mark as break target + const bytecode = toBytecode(` + MAKE_FUNCTION () #3 + CALL #0 + HALT + BREAK + `) - const vm = new VM({ - instructions: [ - // 0: Push initial value - { op: OpCode.PUSH, operand: 0 }, // counter = 0 - { op: OpCode.STORE, operand: 'counter' }, - - // 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 + try { + await run(bytecode) + expect(true).toBe(false) // Should not reach here + } catch (e: any) { + expect(e.message).toContain('no break target found') + } }) -test("CONTINUE - skips to loop start", async () => { - // CONTINUE requires function call frames with continueAddress set - // This will be tested once we implement CALL/RETURN - expect(true).toBe(true) // placeholder +test("BREAK - exits from nested function call", async () => { + // BREAK unwinds to the break target (the outer function's frame) + // Main calls outer, outer calls inner, inner BREAKs back to outer's caller (main) + 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') + } })