Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions crates/zapcode-core/src/compiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ struct Compiler {
struct LoopInfo {
break_patches: Vec<usize>,
continue_patches: Vec<usize>,
// A `switch` participates in `break` targeting (break exits the switch) but
// NOT `continue` — a `continue` inside a switch targets the enclosing loop.
is_switch: bool,
}

impl Compiler {
Expand Down Expand Up @@ -222,6 +225,7 @@ impl Compiler {
self.loop_stack.push(LoopInfo {
break_patches: Vec::new(),
continue_patches: Vec::new(),
is_switch: false,
});

self.compile_expr(test)?;
Expand All @@ -248,6 +252,7 @@ impl Compiler {
self.loop_stack.push(LoopInfo {
break_patches: Vec::new(),
continue_patches: Vec::new(),
is_switch: false,
});

for s in body {
Expand Down Expand Up @@ -282,6 +287,7 @@ impl Compiler {
self.loop_stack.push(LoopInfo {
break_patches: Vec::new(),
continue_patches: Vec::new(),
is_switch: false,
});

let exit_jump = if let Some(test) = test {
Expand Down Expand Up @@ -329,6 +335,7 @@ impl Compiler {
self.loop_stack.push(LoopInfo {
break_patches: Vec::new(),
continue_patches: Vec::new(),
is_switch: false,
});

self.emit(Instruction::Dup);
Expand Down Expand Up @@ -413,15 +420,28 @@ impl Compiler {
}
}
Statement::Break { .. } => {
// `break` exits the nearest loop OR switch.
let idx = self.emit(Instruction::Jump(0));
if let Some(loop_info) = self.loop_stack.last_mut() {
loop_info.break_patches.push(idx);
match self.loop_stack.last_mut() {
Some(loop_info) => loop_info.break_patches.push(idx),
None => {
return Err(ZapcodeError::CompileError(
"illegal break statement (not inside a loop or switch)".to_string(),
))
}
}
}
Statement::Continue { .. } => {
// `continue` targets the nearest enclosing *loop*, skipping any
// switch frames in between (a switch is break-only).
let idx = self.emit(Instruction::Jump(0));
if let Some(loop_info) = self.loop_stack.last_mut() {
loop_info.continue_patches.push(idx);
match self.loop_stack.iter_mut().rev().find(|l| !l.is_switch) {
Some(loop_info) => loop_info.continue_patches.push(idx),
None => {
return Err(ZapcodeError::CompileError(
"illegal continue statement (not inside a loop)".to_string(),
))
}
}
}
Statement::FunctionDecl { func_index, .. } => {
Expand Down Expand Up @@ -487,6 +507,14 @@ impl Compiler {

let jump_end = self.emit(Instruction::Jump(0));

// A `break` inside a case must exit the switch. Register a
// switch frame so `break` jumps here (and `continue` skips it).
self.loop_stack.push(LoopInfo {
break_patches: Vec::new(),
continue_patches: Vec::new(),
is_switch: true,
});

// Compile case bodies
let mut body_starts = Vec::new();
for case in cases {
Expand All @@ -499,6 +527,12 @@ impl Compiler {
let end = self.current_offset();
self.emit(Instruction::Pop); // pop discriminant

// `break` targets the Pop, so it also cleans up the discriminant.
let switch_frame = self.loop_stack.pop().expect("switch frame present");
for patch in switch_frame.break_patches {
self.patch_jump(patch, end);
}

// Patch jumps
for (i, &jump) in case_jumps.iter().enumerate() {
if jump != 0 {
Expand Down
36 changes: 36 additions & 0 deletions crates/zapcode-core/tests/control_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,39 @@ fn test_nullish_coalescing_defined() {
let result = eval_ts("0 ?? 42").unwrap();
assert_eq!(result, Value::Int(0));
}

// ── switch (regression: bare `break` looped forever → allocation blowup) ─────

#[test]
fn test_switch_basic_match_and_break() {
let r = eval_ts(
"let r = 'none'; switch (2) { case 1: r = 'one'; break; case 2: r = 'two'; break; } r",
)
.unwrap();
assert_eq!(r, Value::String("two".into()));
}

#[test]
fn test_switch_default() {
let r = eval_ts("let r = 'x'; switch (9) { case 1: r = 'one'; break; default: r = 'def'; } r")
.unwrap();
assert_eq!(r, Value::String("def".into()));
}

#[test]
fn test_switch_fallthrough() {
let r = eval_ts(
"let r = 0; switch (1) { case 1: r += 1; case 2: r += 10; break; case 3: r += 100; } r",
)
.unwrap();
assert_eq!(r, Value::Int(11));
}

#[test]
fn test_switch_break_inside_loop_breaks_switch_only() {
// break exits the switch (not the loop): n=1 adds 1, n=2 hits default (+100);
// the loop still runs both iterations and the trailing `t += 1000` each time.
let src = "let t = 0; for (const n of [1, 2]) { switch (n) { case 1: t += 1; break; default: t += 100; } t += 1000; } t";
let r = eval_ts(src).unwrap();
assert_eq!(r, Value::Int(2101));
}