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:
// 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

View File

@ -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')
}
})