BREAK
This commit is contained in:
parent
4b3c9e8bfc
commit
8754afb536
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
test("BREAK - exits from nested function call", async () => {
|
||||||
// For now, let's skip this and test BREAK with a simpler approach
|
// BREAK unwinds to the break target (the outer function's frame)
|
||||||
expect(true).toBe(true) // placeholder
|
// 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 - skips to loop start", async () => {
|
test("CONTINUE - throws error when no continue target", async () => {
|
||||||
// CONTINUE requires function call frames with continueAddress set
|
// CONTINUE requires continueAddress to be set on a frame
|
||||||
// This will be tested once we implement CALL/RETURN
|
// Since we have no instruction to set it yet, this should throw
|
||||||
expect(true).toBe(true) // placeholder
|
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')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user