From 8d0ac60fe4d8d19326e10730941f038d94ced4f3 Mon Sep 17 00:00:00 2001 From: Ian Davis Date: Fri, 10 Jul 2026 13:51:54 -0700 Subject: [PATCH 01/40] Initial break and continue support --- Cargo.lock | 2 + samples/language/BreakAndContinue.qs | 46 + source/compiler/qsc/src/codegen/tests.rs | 232 +++ source/compiler/qsc/src/interpret.rs | 36 +- .../qsc/src/interpret/debugger_tests.rs | 585 +++++++ source/compiler/qsc/src/interpret/tests.rs | 1366 +++++++++++++++ source/compiler/qsc_ast/src/ast.rs | 8 +- source/compiler/qsc_ast/src/mut_visit.rs | 6 +- source/compiler/qsc_ast/src/visit.rs | 6 +- source/compiler/qsc_codegen/src/qsharp.rs | 6 + source/compiler/qsc_eval/src/lib.rs | 10 + .../normalize/tests/regression_and_depth.rs | 2 +- source/compiler/qsc_frontend/src/lower.rs | 2 + .../compiler/qsc_frontend/src/lower/tests.rs | 111 ++ .../compiler/qsc_frontend/src/typeck/rules.rs | 4 + .../compiler/qsc_frontend/src/typeck/tests.rs | 138 ++ source/compiler/qsc_hir/src/hir.rs | 8 +- source/compiler/qsc_hir/src/mut_visit.rs | 5 +- source/compiler/qsc_hir/src/visit.rs | 5 +- source/compiler/qsc_lowerer/src/lib.rs | 2 + .../qsc_openqasm_compiler/src/ast_builder.rs | 20 + .../qsc_openqasm_compiler/src/compiler.rs | 23 +- .../compiler/qsc_openqasm_compiler/src/lib.rs | 12 +- .../src/tests/compiler_errors.rs | 50 +- .../openqasm_compiler_errors_test.qasm | 14 +- .../src/tests/statement.rs | 1 + .../src/tests/statement/break_continue.rs | 299 ++++ .../qsc_parse/src/completion/word_kinds.rs | 2 + source/compiler/qsc_parse/src/expr.rs | 4 + source/compiler/qsc_parse/src/expr/tests.rs | 135 ++ source/compiler/qsc_parse/src/keyword.rs | 8 +- source/compiler/qsc_parse/src/stmt/tests.rs | 47 + source/compiler/qsc_passes/Cargo.toml | 2 + .../compiler/qsc_passes/src/capabilitiesck.rs | 2 +- .../src/capabilitiesck/tests_adaptive.rs | 94 +- .../tests_adaptive_plus_integers.rs | 4 +- ...tests_adaptive_plus_integers_and_floats.rs | 4 +- .../src/capabilitiesck/tests_base.rs | 4 +- .../src/capabilitiesck/tests_common.rs | 75 +- source/compiler/qsc_passes/src/common.rs | 81 +- .../qsc_passes/src/conjugate_invert.rs | 55 +- .../qsc_passes/src/conjugate_invert/tests.rs | 147 +- .../qsc_passes/src/index_assignment/tests.rs | 24 +- source/compiler/qsc_passes/src/lib.rs | 95 +- source/compiler/qsc_passes/src/logic_sep.rs | 27 +- .../qsc_passes/src/logic_sep/tests.rs | 125 ++ .../compiler/qsc_passes/src/loop_control.rs | 115 ++ .../qsc_passes/src/loop_control/tests.rs | 519 ++++++ .../compiler/qsc_passes/src/loop_normalize.rs | 836 +++++++++ .../qsc_passes/src/loop_normalize/tests.rs | 1451 ++++++++++++++++ .../qsc_passes/src/loop_unification.rs | 1126 +++++++++++- .../loop_unification/break_continue_tests.rs | 1513 +++++++++++++++++ .../src/loop_unification/test_utils.rs | 77 + .../qsc_passes/src/loop_unification/tests.rs | 686 ++------ source/compiler/qsc_passes/src/qsharp_gen.rs | 887 ++++++++++ .../src/replace_qubit_allocation/tests.rs | 34 +- .../compiler/qsc_passes/src/spec_gen/tests.rs | 103 +- source/jupyterlab/src/index.ts | 2 +- source/playground/src/openqasm-language.ts | 2 + source/qdk_openqasm_parser/src/lib.rs | 14 +- source/qdk_openqasm_parser/src/parser.rs | 2 +- source/qdk_openqasm_parser/src/semantic.rs | 2 +- .../src/semantic/lowerer.rs | 11 +- .../semantic/tests/statements/break_stmt.rs | 37 + .../tests/statements/continue_stmt.rs | 37 + source/samples_test/src/tests/language.rs | 6 + source/vscode/ai/qdk-programming/openqasm.md | 13 +- source/vscode/ai/qdk-programming/qsharp.md | 17 +- .../vscode/syntaxes/openqasm.tmLanguage.json | 2 +- source/vscode/syntaxes/qsharp.tmLanguage.json | 2 +- 70 files changed, 10653 insertions(+), 775 deletions(-) create mode 100644 samples/language/BreakAndContinue.qs create mode 100644 source/compiler/qsc_openqasm_compiler/src/tests/statement/break_continue.rs create mode 100644 source/compiler/qsc_passes/src/loop_control.rs create mode 100644 source/compiler/qsc_passes/src/loop_control/tests.rs create mode 100644 source/compiler/qsc_passes/src/loop_normalize.rs create mode 100644 source/compiler/qsc_passes/src/loop_normalize/tests.rs create mode 100644 source/compiler/qsc_passes/src/loop_unification/break_continue_tests.rs create mode 100644 source/compiler/qsc_passes/src/loop_unification/test_utils.rs create mode 100644 source/compiler/qsc_passes/src/qsharp_gen.rs diff --git a/Cargo.lock b/Cargo.lock index b0ebe7963c4..5873ed596b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2315,10 +2315,12 @@ dependencies = [ "expect-test", "indoc", "miette", + "num-bigint", "qsc", "qsc_data_structures", "qsc_eval", "qsc_fir", + "qsc_formatter", "qsc_frontend", "qsc_hir", "qsc_lowerer", diff --git a/samples/language/BreakAndContinue.qs b/samples/language/BreakAndContinue.qs new file mode 100644 index 00000000000..31655c5b2a8 --- /dev/null +++ b/samples/language/BreakAndContinue.qs @@ -0,0 +1,46 @@ +// # Sample +// Break and Continue +// +// # Description +// The `break` statement immediately exits the innermost enclosing loop. +// The `continue` statement skips the rest of the current iteration and +// proceeds to the next one. Both can be used in the bodies of `for`, +// `while`, and `repeat`-`until` loops. They cannot be used in loop +// conditions or in `repeat`-`until` fixup blocks. + +function Main() : (Int, Int, Int) { + // Use `break` to stop a loop early. Here we find the first integer whose + // square exceeds 50 and then stop searching. + mutable firstOverFifty = 0; + for n in 1..100 { + if n * n > 50 { + firstOverFifty = n; + break; + } + } + + // Use `continue` to skip selected iterations. Here we sum the numbers from + // 1 to 20, skipping every multiple of 3. + mutable sumWithoutMultiplesOfThree = 0; + for n in 1..20 { + if n % 3 == 0 { + continue; + } + sumWithoutMultiplesOfThree += n; + } + + // `break` and `continue` also work in `while` loops. Here we count how many + // times 1 can be doubled before the result exceeds 1000. + mutable doublings = 0; + mutable value = 1; + while true { + value *= 2; + if value <= 1000 { + doublings += 1; + continue; + } + break; + } + + return (firstOverFifty, sumWithoutMultiplesOfThree, doublings); +} diff --git a/source/compiler/qsc/src/codegen/tests.rs b/source/compiler/qsc/src/codegen/tests.rs index 4bb6d037c97..7755c85668a 100644 --- a/source/compiler/qsc/src/codegen/tests.rs +++ b/source/compiler/qsc/src/codegen/tests.rs @@ -5388,3 +5388,235 @@ fn chemistry_like_iqpe_with_udt_capture_closure_generates_base_profile_qir() { "expected threaded Controlled X capture in QIR:\n{qir}" ); } + +// `break`/`continue` are desugared to flags in HIR, so codegen sees only +// ordinary `while` loops. These tests confirm the desugared programs lower all +// the way to QIR/RIR with no residual early-exit constructs, and that +// classically-conditioned `break`/`continue` needs no capability beyond the +// Base profile. + +#[test] +fn break_and_continue_in_for_loop_generates_qir_on_base_profile() { + let source = r#" + namespace Test { + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + for i in 0..10 { + if i == 5 { + break; + } + if i % 2 == 0 { + continue; + } + X(q); + } + Reset(q); + } + } + "#; + + // Classical conditions keep the loop static, so the most restrictive Base + // profile suffices: no new capability is required for `break`/`continue`. + let qir = compile_source_to_qir(source, TargetCapabilityFlags::empty()); + + // `continue` skips even i and `break` stops at i == 5, so X is applied for + // i = 1 and i = 3 only. Match call sites, not the trailing `declare`. + assert_eq!( + qir.matches("call void @__quantum__qis__x__body").count(), + 2, + "expected exactly two X gates from odd i < 5:\n{qir}" + ); +} + +#[test] +fn while_and_repeat_with_break_continue_generate_qir_on_base_profile() { + let source = r#" + namespace Test { + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + mutable i = 0; + while i < 100 { + set i += 1; + if i == 3 { + break; + } + X(q); + } + mutable j = 0; + repeat { + set j += 1; + if j % 2 == 0 { + continue; + } + H(q); + } until j >= 5; + Reset(q); + } + } + "#; + + let qir = compile_source_to_qir(source, TargetCapabilityFlags::empty()); + + // while: X for i = 1, 2 then break at i == 3 -> 2 X gates. + assert_eq!( + qir.matches("call void @__quantum__qis__x__body").count(), + 2, + "expected two X gates before the while `break`:\n{qir}" + ); + // repeat: `continue` skips even j, so H fires for odd j = 1, 3, 5 -> 3 H gates. + assert_eq!( + qir.matches("call void @__quantum__qis__h__body").count(), + 3, + "expected three H gates for odd j via repeat `continue`:\n{qir}" + ); +} + +#[test] +fn return_and_break_in_loop_composes_with_return_unify() { + // A loop containing both a `return` and a `break`. The `break` desugar in HIR + // `loop_unification` rewrites the loop condition to `(not .broke_) and cond`; + // `return_unify` in FIR then wraps it again as + // `(not __has_returned) and ((not .broke_) and cond)`. Because the synthetic + // break flag `.broke_` uses a distinct name from the return flags, the + // `check_no_flag_writes_in_operand_position` invariant, which scans only the + // return-flag names, never fires on it. Successful QIR generation therefore + // demonstrates the two flag lowerings compose cleanly with no invariant + // conflict and no residual early exit. + let source = r#" + namespace Test { + @EntryPoint() + operation Main() : Int { + use q = Qubit(); + mutable total = 0; + for i in 0..20 { + if i == 10 { + break; + } + X(q); + set total += i; + if total > 15 { + return total; + } + } + Reset(q); + total + } + } + "#; + + let qir = compile_source_to_qir( + source, + TargetCapabilityFlags::Adaptive | TargetCapabilityFlags::IntegerComputations, + ); + + // The early `return` fires at i == 6, where total = 21 > 15, after X on i = 0..6. + assert_eq!( + qir.matches("call void @__quantum__qis__x__body").count(), + 7, + "expected seven X gates before the early return:\n{qir}" + ); + assert!( + qir.contains("__quantum__rt__int_record_output(i64 21"), + "expected the composed return value 21 to be recorded:\n{qir}" + ); +} + +#[test] +fn break_continue_loops_lower_to_rir_without_early_exit() { + // Exercises the RIR lowering path, a superset of the checks QIR relies on, + // for every loop form carrying `break`/`continue`, confirming no residual + // `break`/`continue`/early-exit construct survives into RIR. + let source = r#" + namespace Test { + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + for i in 0..6 { + if i == 4 { + break; + } + if i == 1 { + continue; + } + X(q); + } + mutable k = 0; + repeat { + set k += 1; + if k == 2 { + continue; + } + if k == 5 { + break; + } + Y(q); + } until k >= 8; + Reset(q); + } + } + "#; + + let rir = compile_source_to_rir(source, TargetCapabilityFlags::empty()); + let rir_text = rir.join("\n"); + // Both loop bodies' quantum ops survive into the RIR callable table, and the + // fully-desugared program lowers to a clean single-return body; no residual + // early-exit construct could survive, because the lowerer panics on any + // leftover `break`/`continue` and `return_unify` asserts no leftover `Return`. + assert!( + rir_text.contains("name: __quantum__qis__x__body"), + "expected the X gate callable to survive into RIR:\n{rir_text}" + ); + assert!( + rir_text.contains("name: __quantum__qis__y__body"), + "expected the Y gate callable to survive into RIR:\n{rir_text}" + ); + assert!( + rir_text.contains("Return Integer(0)"), + "expected the desugared program to lower to a single clean return:\n{rir_text}" + ); +} + +#[test] +fn bare_operand_break_lowers_to_qir_and_skips_eager_consumer() { + // A bare-operand `break` buried in a call argument such as `Sink(q, break)` is + // hoisted to statement position and desugared to flags, so the whole + // HIR -> FIR -> RIR -> QIR pipeline lowers with no residual early-exit node, + // since the lowerer panics on any leftover `break`. The eager consumer `Sink` + // applies an `H` gate, so a successful lowering with no `H` in the QIR + // proves the break fires before the argument is consumed. Previously the + // buried break was left in operand position, which would instead run + // `Sink`, emitting `H`, on the break iteration. + let source = r#" + namespace Test { + operation Sink(q : Qubit, x : Int) : Int { H(q); x } + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + for i in 0..10 { + if i == 3 { + let _ = Sink(q, break); + } + X(q); + } + Reset(q); + } + } + "#; + + // Classical loop and condition keep the program static, so the Base profile + // suffices. `X(q)` runs for i = 0, 1, 2 before the guard; on i == 3 the break + // fires first, so the trailing `X(q)` is skipped and the loop exits. That + // gives exactly three X gates, and no H gate because `Sink` never runs. + let qir = compile_source_to_qir(source, TargetCapabilityFlags::empty()); + assert_eq!( + qir.matches("call void @__quantum__qis__x__body").count(), + 3, + "expected exactly three X gates before the operand-position break:\n{qir}" + ); + assert!( + !qir.contains("call void @__quantum__qis__h__body"), + "expected no H gate: the eager consumer must not run on the break path:\n{qir}" + ); +} diff --git a/source/compiler/qsc/src/interpret.rs b/source/compiler/qsc/src/interpret.rs index f476fa06f5e..7bd26b82ede 100644 --- a/source/compiler/qsc/src/interpret.rs +++ b/source/compiler/qsc/src/interpret.rs @@ -41,7 +41,7 @@ use qsc_data_structures::{ error::WithSource, functors::FunctorApp, language_features::LanguageFeatures, - line_column::{Encoding, Range}, + line_column::{Encoding, Position, Range}, source::{Source, SourceMap}, span::Span, target::{Profile, TargetCapabilityFlags}, @@ -2068,7 +2068,7 @@ pub struct BreakpointSpan { } struct BreakpointCollector<'a> { - statements: FxHashMap, + statements: FxHashMap, sources: &'a SourceMap, offset: u32, package: &'a Package, @@ -2107,13 +2107,17 @@ impl<'a> BreakpointCollector<'a> { id: stmt.id.into(), range, }; - // Keep the first statement seen for a source range so UI clients get - // one stable, hittable breakpoint per visual location. - // Multiple HIR passes (ReplaceQubitAllocation, LoopUni, - // conjugate_invert, spec_gen) generate statements sharing the same - // source span. The lowerer maps these 1:1 into FIR, so deduplication - // is needed here. - self.statements.entry(range).or_insert(bps); + // Keep the first statement seen for a given start position so UI + // clients get one stable, hittable breakpoint per visual location. + // Multiple HIR passes, including ReplaceQubitAllocation, LoopUni, + // conjugate_invert, and spec_gen, generate statements that share a + // start position but differ in span. For example, the qubit-release + // desugar wraps `return e;` in a block whose inner `return e` keeps + // the return-expression span, one column shorter than the outer + // statement's `return e;` span. Keying on the start position + // collapses such overlapping locations onto the outer statement + // seen first, so a single source line maps to a single breakpoint. + self.statements.entry(range.start).or_insert(bps); } } } @@ -2123,11 +2127,21 @@ impl<'a> Visitor<'a> for BreakpointCollector<'a> { fn visit_stmt(&mut self, stmt: StmtId) { let stmt_res = self.get_stmt(stmt); match stmt_res.kind { - fir::StmtKind::Expr(expr) | fir::StmtKind::Local(_, _, expr) => { + // Recurse into the statement's expression so that nested statements + // become individually breakpointable. `Semi` is included alongside + // `Expr`/`Local` because block-like statements written with a + // trailing semicolon, notably a desugared `repeat ... until cond;` + // that is always a `Semi`, carry their loop body inside this + // expression. Recursing into a simple `Semi` such as `foo();` or + // `set x = 1;` adds no breakpoints, since `walk_expr` only surfaces + // breakpoints when it re-enters a nested statement. + fir::StmtKind::Expr(expr) + | fir::StmtKind::Local(_, _, expr) + | fir::StmtKind::Semi(expr) => { self.add_stmt(stmt_res); visit::walk_expr(self, expr); } - fir::StmtKind::Item(_) | fir::StmtKind::Semi(_) => { + fir::StmtKind::Item(_) => { self.add_stmt(stmt_res); } } diff --git a/source/compiler/qsc/src/interpret/debugger_tests.rs b/source/compiler/qsc/src/interpret/debugger_tests.rs index 97f90f6ef40..77bd0b9c00c 100644 --- a/source/compiler/qsc/src/interpret/debugger_tests.rs +++ b/source/compiler/qsc/src/interpret/debugger_tests.rs @@ -93,6 +93,52 @@ fn expect_out(debugger: &mut Debugger) { } } +/// Converts a line/column position to a byte offset in `source`. The test +/// sources here are ASCII, so a UTF-8 column equals a byte column. +fn offset_of(source: &str, pos: crate::line_column::Position) -> usize { + let target_line = pos.line as usize; + let mut offset = 0; + for (line_idx, line) in source.split_inclusive('\n').enumerate() { + if line_idx == target_line { + return offset + pos.column as usize; + } + offset += line.len(); + } + offset +} + +/// Renders a breakpoint as `startLine:startCol-endLine:endCol ""` +/// so a snapshot shows exactly which user statement each breakpoint maps to. +/// Any synthetic desugar node would surface here as a `0:0-0:0` entry, so the +/// snapshot doubles as an assertion that no synthetic guard is breakpointable. +fn format_breakpoint(source: &str, bp: &crate::interpret::BreakpointSpan) -> String { + let start = offset_of(source, bp.range.start); + let end = offset_of(source, bp.range.end); + let snippet = source[start..end].replace('\n', " "); + format!( + "{}:{}-{}:{} {snippet:?}", + bp.range.start.line, bp.range.start.column, bp.range.end.line, bp.range.end.column, + ) +} + +/// Finds the breakpoint whose trimmed source text matches `text` and returns +/// its statement id, so a test can set a breakpoint on a specific user +/// statement, such as the `break;`, and verify it is hittable. +fn breakpoint_id_for_text(debugger: &Debugger, path: &str, source: &str, text: &str) -> StmtId { + debugger + .get_breakpoints(path) + .into_iter() + .find(|bp| { + let start = offset_of(source, bp.range.start); + let end = offset_of(source, bp.range.end); + source[start..end].trim() == text + }) + .map_or_else( + || panic!("no breakpoint matching {text:?}"), + |bp| bp.id.into(), + ) +} + #[cfg(test)] mod given_debugger { use super::*; @@ -283,4 +329,543 @@ mod given_debugger { Ok(()) } } + + // Stepping and breakpoint behavior for loops, including those carrying + // `break`/`continue`. The desugar runs in `loop_unification`, so its + // synthetic nodes are debugger-visible; these tests confirm the steppable-span + // discipline: the flag-set replacing `break;`/`continue;` keeps the keyword + // span and is breakpointable, guarded user statements keep their own spans, and + // the synthetic guard `if`s (which carry `Span::default()`) are never surfaced + // as breakpoints. + #[cfg(test)] + mod loops_with_break_continue { + use super::*; + use expect_test::expect; + use qsc_data_structures::{source::SourceMap, target::TargetCapabilityFlags}; + + static FOR_BREAK_SOURCE: &str = r#"namespace Test { + @EntryPoint() + operation Main() : Int { + mutable total = 0; + for i in 0..10 { + if i == 3 { + break; + } + set total = total + i; + } + total + } +}"#; + + static WHILE_BREAK_SOURCE: &str = r#"namespace Test { + @EntryPoint() + operation Main() : Int { + mutable i = 0; + while i < 10 { + if i == 4 { + break; + } + set i = i + 1; + } + i + } +}"#; + + static REPEAT_CONTINUE_SOURCE: &str = r#"namespace Test { + @EntryPoint() + operation Main() : Int { + mutable i = 0; + mutable total = 0; + repeat { + set i = i + 1; + if i % 2 == 0 { + continue; + } + set total = total + i; + } until i >= 5; + total + } +}"#; + + static FOR_CONTINUE_SOURCE: &str = r#"namespace Test { + @EntryPoint() + operation Main() : Int { + mutable total = 0; + for i in 0..5 { + if i % 2 == 0 { + continue; + } + set total = total + i; + } + total + } +}"#; + + static NESTED_CONTROL_SOURCE: &str = r#"namespace Test { + @EntryPoint() + operation Main() : Int { + mutable outer = 0; + while outer < 2 { + set outer = outer + 1; + for inner in 1..3 { + if inner == 2 { + break; + } + } + continue; + } + outer + } +}"#; + + static PLAIN_REPEAT_SOURCE: &str = r#"namespace Test { + @EntryPoint() + operation Main() : Int { + mutable i = 0; + repeat { + set i = i + 1; + set i = i + 10; + } until i >= 5; + i + } +}"#; + + fn make_debugger(source: &str) -> Debugger { + let sources = SourceMap::new([("test".into(), source.into())], None); + let (std_id, store) = + crate::compile::package_store_with_stdlib(TargetCapabilityFlags::all()); + Debugger::new( + sources, + TargetCapabilityFlags::all(), + Encoding::Utf8, + LanguageFeatures::default(), + store, + &[(std_id, None)], + ) + .expect("debugger should be created") + } + + fn rendered_breakpoints(debugger: &Debugger, source: &str) -> String { + debugger + .get_breakpoints("test") + .iter() + .map(|bp| format_breakpoint(source, bp)) + .collect::>() + .join("\n") + } + + #[test] + fn for_break_breakpoints_map_to_user_statements() { + let debugger = make_debugger(FOR_BREAK_SOURCE); + // Every breakpoint maps to a real user statement, including `break;`. + // No `0:0-0:0` entry appears, so no synthetic guard is breakpointable. + expect![[r#" + 3:8-3:26 "mutable total = 0;" + 4:8-9:9 "for i in 0..10 { if i == 3 { break; } set total = total + i; }" + 4:12-4:13 "i" + 4:17-4:22 "0..10" + 5:12-7:13 "if i == 3 { break; }" + 6:16-6:21 "break" + 8:12-8:34 "set total = total + i;" + 10:8-10:13 "total""#]] + .assert_eq(&rendered_breakpoints(&debugger, FOR_BREAK_SOURCE)); + } + + #[test] + fn for_break_stepping_lands_on_break_statement() { + let mut debugger = make_debugger(FOR_BREAK_SOURCE); + // Setting only the `break;` breakpoint and running hits it, proving the + // flag-set replacing `break;` is a reachable, steppable location. The + // steppable span is the `break` keyword itself, with no trailing `;`. + let break_id = breakpoint_id_for_text(&debugger, "test", FOR_BREAK_SOURCE, "break"); + expect_bp(&mut debugger, &[break_id], break_id); + } + + #[test] + fn stepping_through_for_break_loop_never_reports_inverted_span() { + let mut debugger = make_debugger(FOR_BREAK_SOURCE); + // Run to the first user statement, then step `Next` through the whole + // loop. Every reported location must be a well-formed forward span: + // `start <= end`. Before the fix, stepping over a synthetic guard + // block's exit computed `block.span.hi - 1` on a `Span::default()` + // block, underflowing the `u32` offset to `u32::MAX` and reporting an + // end-of-file location with `start > end`. + let ids = get_breakpoint_ids(&debugger, "test"); + expect_bp(&mut debugger, &ids, ids[0]); + loop { + let (result, _) = step_next(&mut debugger, &[]); + if let Some(frame) = debugger.get_stack_frames().last() { + let r = &frame.location.range; + assert!( + (r.start.line, r.start.column) <= (r.end.line, r.end.column), + "inverted step location {:?}..{:?}", + r.start, + r.end + ); + } + match result { + Ok(StepResult::Return(_)) => break, + Ok(_) => {} + Err(e) => panic!("unexpected error while stepping: {e:?}"), + } + } + } + + #[test] + fn for_break_guarded_statement_retains_breakpoint() { + let mut debugger = make_debugger(FOR_BREAK_SOURCE); + // The statement after `break;` is wrapped in a synthetic guard, yet it + // keeps its own breakpoint and is hit on the pre-break iterations. + let guarded_id = breakpoint_id_for_text( + &debugger, + "test", + FOR_BREAK_SOURCE, + "set total = total + i;", + ); + expect_bp(&mut debugger, &[guarded_id], guarded_id); + } + + #[test] + fn while_break_breakpoints_map_to_user_statements() { + let debugger = make_debugger(WHILE_BREAK_SOURCE); + expect![[r#" + 3:8-3:22 "mutable i = 0;" + 4:8-9:9 "while i < 10 { if i == 4 { break; } set i = i + 1; }" + 5:12-7:13 "if i == 4 { break; }" + 6:16-6:21 "break" + 8:12-8:26 "set i = i + 1;" + 10:8-10:9 "i""#]] + .assert_eq(&rendered_breakpoints(&debugger, WHILE_BREAK_SOURCE)); + } + + #[test] + fn while_break_stepping_lands_on_break_statement() { + let mut debugger = make_debugger(WHILE_BREAK_SOURCE); + let break_id = breakpoint_id_for_text(&debugger, "test", WHILE_BREAK_SOURCE, "break"); + expect_bp(&mut debugger, &[break_id], break_id); + } + + #[test] + fn repeat_continue_breakpoints_map_to_user_statements() { + let debugger = make_debugger(REPEAT_CONTINUE_SOURCE); + // Even though a `repeat ... until cond;` is a `Semi` statement, its + // body statements are individually breakpointable, mirroring the + // `for`/`while` cases. The `continue;` maps to the `continue` + // keyword span, the statement after it keeps its own breakpoint, and + // no synthetic guard surfaces as a `0:0-0:0` breakpoint. + expect![[r#" + 3:8-3:22 "mutable i = 0;" + 4:8-4:26 "mutable total = 0;" + 5:8-11:23 "repeat { set i = i + 1; if i % 2 == 0 { continue; } set total = total + i; } until i >= 5;" + 6:12-6:26 "set i = i + 1;" + 7:12-9:13 "if i % 2 == 0 { continue; }" + 8:16-8:24 "continue" + 10:12-10:34 "set total = total + i;" + 11:16-11:22 "i >= 5" + 12:8-12:13 "total""#]] + .assert_eq(&rendered_breakpoints(&debugger, REPEAT_CONTINUE_SOURCE)); + } + + #[test] + fn repeat_breakpoints_map_to_user_statements() { + let debugger = make_debugger(PLAIN_REPEAT_SOURCE); + // A plain `repeat ... until cond;` with no `break`/`continue` is a `Semi` + // statement, yet each body statement is individually breakpointable, + // just like the `for`/`while` cases. The synthetic `while` wrapper + // introduced by the loop desugar carries `Span::default()`, so it never + // surfaces as a `0:0-0:0` breakpoint. + expect![[r#" + 3:8-3:22 "mutable i = 0;" + 4:8-7:23 "repeat { set i = i + 1; set i = i + 10; } until i >= 5;" + 5:12-5:26 "set i = i + 1;" + 6:12-6:27 "set i = i + 10;" + 7:16-7:22 "i >= 5" + 8:8-8:9 "i""#]].assert_eq(&rendered_breakpoints(&debugger, PLAIN_REPEAT_SOURCE)); + } + + #[test] + fn for_continue_breakpoints_map_to_user_statements() { + let debugger = make_debugger(FOR_CONTINUE_SOURCE); + // The `continue;` maps to the `continue` keyword span and the statement + // after it keeps its own breakpoint; no synthetic guard surfaces. + expect![[r#" + 3:8-3:26 "mutable total = 0;" + 4:8-9:9 "for i in 0..5 { if i % 2 == 0 { continue; } set total = total + i; }" + 4:12-4:13 "i" + 4:17-4:21 "0..5" + 5:12-7:13 "if i % 2 == 0 { continue; }" + 6:16-6:24 "continue" + 8:12-8:34 "set total = total + i;" + 10:8-10:13 "total""#]] + .assert_eq(&rendered_breakpoints(&debugger, FOR_CONTINUE_SOURCE)); + } + + #[test] + fn for_continue_stepping_lands_on_continue_statement() { + let mut debugger = make_debugger(FOR_CONTINUE_SOURCE); + // Setting only the `continue;` breakpoint and running hits it. The + // steppable span is the `continue` keyword itself, with no trailing `;`. + let continue_id = + breakpoint_id_for_text(&debugger, "test", FOR_CONTINUE_SOURCE, "continue"); + expect_bp(&mut debugger, &[continue_id], continue_id); + } + + #[test] + fn nested_control_breakpoints_keep_distinct_keyword_spans() { + let debugger = make_debugger(NESTED_CONTROL_SOURCE); + expect![[r#" + 3:8-3:26 "mutable outer = 0;" + 4:8-12:9 "while outer < 2 { set outer = outer + 1; for inner in 1..3 { if inner == 2 { break; } } continue; }" + 5:12-5:34 "set outer = outer + 1;" + 6:12-10:13 "for inner in 1..3 { if inner == 2 { break; } }" + 6:16-6:21 "inner" + 6:25-6:29 "1..3" + 7:16-9:17 "if inner == 2 { break; }" + 8:20-8:25 "break" + 11:12-11:20 "continue" + 13:8-13:13 "outer""#]].assert_eq(&rendered_breakpoints(&debugger, NESTED_CONTROL_SOURCE)); + } + + #[test] + fn for_continue_guarded_statement_retains_breakpoint() { + let mut debugger = make_debugger(FOR_CONTINUE_SOURCE); + // The statement after `continue;` runs on the odd iterations, so its + // retained breakpoint is hittable. + let guarded_id = breakpoint_id_for_text( + &debugger, + "test", + FOR_CONTINUE_SOURCE, + "set total = total + i;", + ); + expect_bp(&mut debugger, &[guarded_id], guarded_id); + } + + static OPERAND_BREAK_SOURCE: &str = r#"namespace Test { + operation Foo(q : Qubit) : Int { 5 } + @EntryPoint() + operation Main() : Int { + use q = Qubit(); + mutable total = 0; + for i in 0..10 { + set total = total + Foo(if i == 2 { break } else { q }); + } + total + } +}"#; + + #[test] + fn operand_break_breakpoints_map_to_user_statements() { + let debugger = make_debugger(OPERAND_BREAK_SOURCE); + // The `break` sits in operand position inside `Foo(if i == 2 { break } + // else { q })`. Because the operand is `Qubit`-typed with no classical + // default, the desugar lifts it to a synthetic array-backed temp and + // guards the consuming `set` statement. Every synthetic node the lift + // and guard introduce carries `Span::default()`, so none surfaces as a + // `0:0-0:0` breakpoint; only real user statements are breakpointable, + // including the `break` keyword at its own span. + expect![[r#" + 1:37-1:38 "5" + 4:8-4:24 "use q = Qubit();" + 5:8-5:26 "mutable total = 0;" + 6:8-8:9 "for i in 0..10 { set total = total + Foo(if i == 2 { break } else { q }); }" + 6:12-6:13 "i" + 6:17-6:22 "0..10" + 7:12-7:68 "set total = total + Foo(if i == 2 { break } else { q });" + 7:48-7:53 "break" + 7:63-7:64 "q" + 9:8-9:13 "total""#]] + .assert_eq(&rendered_breakpoints(&debugger, OPERAND_BREAK_SOURCE)); + } + + #[test] + fn operand_break_stepping_lands_on_break_statement() { + let mut debugger = make_debugger(OPERAND_BREAK_SOURCE); + // Setting only the operand-position `break` breakpoint and running hits + // it, proving the lifted, guarded desugar keeps the user's `break` + // keyword as a reachable, steppable location; the synthetic temp and + // guard nodes carry `Span::default()` and are never stepped onto. + let break_id = breakpoint_id_for_text(&debugger, "test", OPERAND_BREAK_SOURCE, "break"); + expect_bp(&mut debugger, &[break_id], break_id); + } + + static REPEAT_FIXUP_SOURCE: &str = r#"namespace Test { + @EntryPoint() + operation Main() : Int { + mutable i = 0; + repeat { + set i = i + 1; + } until i >= 5 + fixup { + set i = i * 2; + } + i + } +}"#; + + #[test] + fn repeat_fixup_breakpoints_map_to_user_statements() { + let debugger = make_debugger(REPEAT_FIXUP_SOURCE); + // A `repeat ... until cond fixup { ... }` with no break/continue desugars + // to a `while` whose tail runs the `until` update and a synthetic + // `if .continue_cond_ { fixup }`. The tail update carries the `until` + // condition span and the fixup guard carries the fixup block span, so + // both surface at real user spans; every other synthetic node the + // desugar introduces carries `Span::default()` and never surfaces as a + // `0:0-0:0` breakpoint. + expect![[r#" + 3:8-3:22 "mutable i = 0;" + 4:8-9:9 "repeat { set i = i + 1; } until i >= 5 fixup { set i = i * 2; }" + 5:12-5:26 "set i = i + 1;" + 6:16-6:22 "i >= 5" + 7:14-9:9 "{ set i = i * 2; }" + 8:12-8:26 "set i = i * 2;" + 10:8-10:9 "i""#]].assert_eq(&rendered_breakpoints(&debugger, REPEAT_FIXUP_SOURCE)); + } + + static REPEAT_BREAK_FIXUP_SOURCE: &str = r#"namespace Test { + @EntryPoint() + operation Main() : Int { + mutable i = 0; + repeat { + set i = i + 1; + if i == 3 { + break; + } + } until i >= 5 + fixup { + set i = i * 2; + } + i + } +}"#; + + #[test] + fn repeat_break_fixup_breakpoints_map_to_user_statements() { + let debugger = make_debugger(REPEAT_BREAK_FIXUP_SOURCE); + // A `repeat` carrying a `break` and a `fixup`: the tail, the `until` + // update and the `if .continue_cond_ { fixup }`, is wrapped in a + // `break`-guarded block so it is skipped on the breaking iteration. + // The `break` keyword, the `until` update span, and the fixup block + // span all remain breakpointable; the `break` guard and flag scaffold + // carry `Span::default()` and never surface. + expect![[r#" + 3:8-3:22 "mutable i = 0;" + 4:8-12:9 "repeat { set i = i + 1; if i == 3 { break; } } until i >= 5 fixup { set i = i * 2; }" + 5:12-5:26 "set i = i + 1;" + 6:12-8:13 "if i == 3 { break; }" + 7:16-7:21 "break" + 9:16-9:22 "i >= 5" + 10:14-12:9 "{ set i = i * 2; }" + 11:12-11:26 "set i = i * 2;" + 13:8-13:9 "i""#]].assert_eq(&rendered_breakpoints(&debugger, REPEAT_BREAK_FIXUP_SOURCE)); + } + + static FOR_ARRAY_BREAK_SOURCE: &str = r#"namespace Test { + @EntryPoint() + operation Main() : Int { + mutable total = 0; + for x in [1, 2, 3, 4, 5] { + if x == 3 { + break; + } + set total = total + x; + } + total + } +}"#; + + #[test] + fn for_array_break_breakpoints_map_to_user_statements() { + let debugger = make_debugger(FOR_ARRAY_BREAK_SOURCE); + // Iterating an array desugars through a distinct shape from a range + // `for`: `.array_id_`/`.len_id_` captures and an index-driven `while`. The + // loop variable pattern and the iterable keep their spans, the `break` + // keyword keeps its span, and the array/length captures and guard + // scaffold carry `Span::default()`, so no synthetic `0:0-0:0` + // breakpoint surfaces. + expect![[r#" + 3:8-3:26 "mutable total = 0;" + 4:8-9:9 "for x in [1, 2, 3, 4, 5] { if x == 3 { break; } set total = total + x; }" + 4:12-4:13 "x" + 4:17-4:32 "[1, 2, 3, 4, 5]" + 5:12-7:13 "if x == 3 { break; }" + 6:16-6:21 "break" + 8:12-8:34 "set total = total + x;" + 10:8-10:13 "total""#]].assert_eq(&rendered_breakpoints(&debugger, FOR_ARRAY_BREAK_SOURCE)); + } + + static WHILE_CONTINUE_SOURCE: &str = r#"namespace Test { + @EntryPoint() + operation Main() : Int { + mutable i = 0; + mutable total = 0; + while i < 10 { + set i = i + 1; + if i % 2 == 0 { + continue; + } + set total = total + i; + } + total + } +}"#; + + #[test] + fn while_continue_breakpoints_map_to_user_statements() { + let debugger = make_debugger(WHILE_CONTINUE_SOURCE); + // A continue-only `while` needs no `.broke_` flag, so the desugar keeps + // it as a bare `while` (no wrapping block) and simply resets `.cont_` + // per iteration. The `while`, its body statements, and the `continue` + // keyword keep their spans; the `.cont_` reset and the guard `if` carry + // `Span::default()`, so no synthetic breakpoint surfaces. + expect![[r#" + 3:8-3:22 "mutable i = 0;" + 4:8-4:26 "mutable total = 0;" + 5:8-11:9 "while i < 10 { set i = i + 1; if i % 2 == 0 { continue; } set total = total + i; }" + 6:12-6:26 "set i = i + 1;" + 7:12-9:13 "if i % 2 == 0 { continue; }" + 8:16-8:24 "continue" + 10:12-10:34 "set total = total + i;" + 12:8-12:13 "total""#]].assert_eq(&rendered_breakpoints(&debugger, WHILE_CONTINUE_SOURCE)); + } + + static FOR_BREAK_CONTINUE_SOURCE: &str = r#"namespace Test { + @EntryPoint() + operation Main() : Int { + mutable total = 0; + for i in 0..10 { + if i % 2 == 0 { + continue; + } + if i == 7 { + break; + } + set total = total + i; + } + total + } +}"#; + + #[test] + fn for_break_continue_breakpoints_map_to_user_statements() { + let debugger = make_debugger(FOR_BREAK_CONTINUE_SOURCE); + // A `for` carrying both a `break` and a `continue` mints both flags and + // guards trailing statements with `not .broke_ and not .cont_`. Both + // keywords, both `if`s, and the trailing `set` keep their spans; every + // two-flag guard node carries `Span::default()`, so no synthetic + // breakpoint surfaces. + expect![[r#" + 3:8-3:26 "mutable total = 0;" + 4:8-12:9 "for i in 0..10 { if i % 2 == 0 { continue; } if i == 7 { break; } set total = total + i; }" + 4:12-4:13 "i" + 4:17-4:22 "0..10" + 5:12-7:13 "if i % 2 == 0 { continue; }" + 6:16-6:24 "continue" + 8:12-10:13 "if i == 7 { break; }" + 9:16-9:21 "break" + 11:12-11:34 "set total = total + i;" + 13:8-13:13 "total""#]] + .assert_eq(&rendered_breakpoints(&debugger, FOR_BREAK_CONTINUE_SOURCE)); + } + } } diff --git a/source/compiler/qsc/src/interpret/tests.rs b/source/compiler/qsc/src/interpret/tests.rs index 0300cb2a8ec..0ee07c2f8f3 100644 --- a/source/compiler/qsc/src/interpret/tests.rs +++ b/source/compiler/qsc/src/interpret/tests.rs @@ -3668,4 +3668,1370 @@ mod given_interpreter { } } } + + // End-to-end evaluation of `break`/`continue` in every loop form, plus qubit + // release regressions. These run the full default-pass pipeline, which + // includes the loop normalization and unification desugar, and then simulate + // the program, so each test exercises the flag desugar all the way to a + // concrete runtime value. The qubit tests confirm that the desugar, which + // introduces no real early exit, leaves natural end-of-block qubit release + // intact for every `break`/`continue` configuration. + mod loops_with_break_continue { + use super::*; + use expect_test::expect; + use indoc::indoc; + + #[test] + fn break_in_qubit_initializer_skips_later_initializers() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation BreakInQubitInitializer() : Int { + mutable effects = 0; + for _ in 0..1 { + use (first, second) = ( + Qubit[if true { break } else { 1 }], + Qubit[{ effects += 1; 1 }] + ); + } + effects + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + let (result, output) = run(&mut interpreter, "BreakInQubitInitializer()"); + is_only_value(&result, &output, &Value::Int(0)); + } + + #[test] + fn continue_in_borrow_initializer_skips_later_initializers() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation ContinueInBorrowInitializer() : Int { + mutable effects = 0; + for i in 0..2 { + borrow (first, second) = ( + Qubit[if i == 1 { continue } else { 1 }], + Qubit[{ effects += 1; 1 }] + ); + } + effects + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + let (result, output) = run(&mut interpreter, "ContinueInBorrowInitializer()"); + is_only_value(&result, &output, &Value::Int(2)); + } + + #[test] + fn for_range_accumulates_until_break() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation ForRangeBreak() : Int { + mutable total = 0; + for i in 0..10 { + if i == 5 { + break; + } + total += i; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // Iterations i = 0..4 run before `break` at i == 5: 0+1+2+3+4 = 10. + let (result, output) = run(&mut interpreter, "ForRangeBreak()"); + is_only_value(&result, &output, &Value::Int(10)); + } + + #[test] + fn descending_for_range_break_stops_without_stepping() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation DescendingRangeBreak() : Int { + mutable total = 0; + for i in 5..-1..0 { + if i == 3 { + break; + } + total = total * 10 + i; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + let (result, output) = run(&mut interpreter, "DescendingRangeBreak()"); + is_only_value(&result, &output, &Value::Int(54)); + } + + #[test] + fn descending_for_range_continue_still_steps() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation DescendingRangeContinue() : Int { + mutable total = 0; + for i in 5..-1..0 { + if i == 3 { + continue; + } + total = total * 10 + i; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + let (result, output) = run(&mut interpreter, "DescendingRangeContinue()"); + is_only_value(&result, &output, &Value::Int(54_210)); + } + + #[test] + fn for_array_skips_with_continue() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation ForArrayContinue() : Int { + mutable total = 0; + for x in [1, 2, 3, 4, 5] { + if x % 2 == 0 { + continue; + } + total += x; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // `continue` skips the even elements: 1 + 3 + 5 = 9. + let (result, output) = run(&mut interpreter, "ForArrayContinue()"); + is_only_value(&result, &output, &Value::Int(9)); + } + + #[test] + fn for_array_stops_with_break() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation ForArrayBreak() : Int { + mutable total = 0; + for x in [1, 2, 3, 4, 5] { + if x == 3 { + break; + } + total += x; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + let (result, output) = run(&mut interpreter, "ForArrayBreak()"); + is_only_value(&result, &output, &Value::Int(3)); + } + + #[test] + fn while_counter_with_break() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation WhileBreak() : Int { + mutable i = 0; + mutable total = 0; + while i < 100 { + if total > 10 { + break; + } + total += i; + i += 1; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // total grows 0,1,3,6,10,15; once it exceeds 10, at 15, the loop breaks. + let (result, output) = run(&mut interpreter, "WhileBreak()"); + is_only_value(&result, &output, &Value::Int(15)); + } + + #[test] + fn while_continue_rechecks_condition() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation WhileContinue() : Int { + mutable i = 0; + mutable total = 0; + while i < 5 { + i += 1; + if i % 2 == 0 { + continue; + } + total += i; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + let (result, output) = run(&mut interpreter, "WhileContinue()"); + is_only_value(&result, &output, &Value::Int(9)); + } + + #[test] + fn repeat_until_with_continue_is_do_while() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation RepeatContinue() : Int { + mutable i = 0; + mutable total = 0; + repeat { + i += 1; + if i % 2 == 0 { + continue; + } + total += i; + } until i >= 5; + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // `continue` in `repeat` proceeds to the `until` check, like a do-while. + // The odd values 1, 3, 5 accumulate before `until i >= 5` is satisfied. + let (result, output) = run(&mut interpreter, "RepeatContinue()"); + is_only_value(&result, &output, &Value::Int(9)); + } + + #[test] + fn repeat_break_skips_condition_and_fixup() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation RepeatBreak() : Int { + mutable i = 0; + mutable fixups = 0; + repeat { + i += 1; + if i == 3 { + break; + } + } until false + fixup { + fixups += 1; + } + i * 10 + fixups + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + let (result, output) = run(&mut interpreter, "RepeatBreak()"); + is_only_value(&result, &output, &Value::Int(32)); + } + + #[test] + fn nested_loops_break_affects_inner_only() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation NestedBreakInner() : Int { + mutable total = 0; + for i in 1..3 { + for j in 1..10 { + if j == 3 { + break; + } + total += j; + } + total += 100; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // The inner `break` exits only the inner loop: each of the 3 outer + // iterations adds (1 + 2) + 100 = 103, so 3 * 103 = 309. + let (result, output) = run(&mut interpreter, "NestedBreakInner()"); + is_only_value(&result, &output, &Value::Int(309)); + } + + #[test] + fn mixed_nested_loops_keep_independent_control_targets() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation MixedNestedControl() : Int { + mutable outer = 0; + mutable total = 0; + while outer < 3 { + outer += 1; + for inner in 1..4 { + if inner == 2 { + continue; + } + if inner == 4 { + break; + } + total += inner; + } + if outer == 2 { + continue; + } + total += 10; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + let (result, output) = run(&mut interpreter, "MixedNestedControl()"); + is_only_value(&result, &output, &Value::Int(32)); + } + + #[test] + fn outer_break_after_inner_loop_exits_outer_loop() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation OuterBreakAfterInner() : Int { + mutable outer = 0; + mutable total = 0; + while outer < 5 { + outer += 1; + for inner in 1..3 { + if inner == 2 { + break; + } + total += inner; + } + if outer == 2 { + break; + } + total += 10; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + let (result, output) = run(&mut interpreter, "OuterBreakAfterInner()"); + is_only_value(&result, &output, &Value::Int(12)); + } + + #[test] + fn nested_repeat_control_preserves_until_and_fixup_semantics() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation NestedRepeatControl() : Int { + mutable total = 0; + mutable fixups = 0; + for outer in 1..2 { + mutable inner = 0; + repeat { + inner += 1; + if outer == 1 and inner == 2 { + continue; + } + if outer == 2 and inner == 2 { + break; + } + total += 1; + } until inner >= 3 + fixup { + fixups += 1; + } + } + total * 10 + fixups + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + let (result, output) = run(&mut interpreter, "NestedRepeatControl()"); + is_only_value(&result, &output, &Value::Int(33)); + } + + #[test] + fn nested_array_loops_keep_independent_indices_and_flags() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation NestedArrayControl() : Int { + mutable total = 0; + for outer in [1, 2, 3] { + for inner in [1, 2, 3, 4] { + if inner == 2 { + continue; + } + if inner == 4 { + break; + } + total += inner; + } + if outer == 2 { + break; + } + total += 100; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + let (result, output) = run(&mut interpreter, "NestedArrayControl()"); + is_only_value(&result, &output, &Value::Int(108)); + } + + #[test] + fn three_level_nested_control_targets_each_owning_loop() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation ThreeLevelNestedControl() : Int { + mutable outer = 0; + mutable total = 0; + while outer < 4 { + outer += 1; + for middle in 1..3 { + mutable inner = 0; + repeat { + inner += 1; + if inner == 2 { + continue; + } + if inner == 3 { + break; + } + total += 1; + } until false; + if middle == 2 { + continue; + } + if middle == 3 { + break; + } + total += 10; + } + if outer == 2 { + continue; + } + if outer == 3 { + break; + } + total += 100; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + let (result, output) = run(&mut interpreter, "ThreeLevelNestedControl()"); + is_only_value(&result, &output, &Value::Int(139)); + } + + #[test] + fn loop_mixing_return_and_break() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation ReturnAndBreak(threshold : Int) : Int { + mutable total = 0; + for i in 0..10 { + if i == 8 { + break; + } + total += i; + if total > threshold { + return total; + } + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // High threshold: `return` never fires, `break` at i == 8 leaves + // total = 0+1+...+7 = 28, returned by the trailing expression. + let (result, output) = run(&mut interpreter, "ReturnAndBreak(100)"); + is_only_value(&result, &output, &Value::Int(28)); + // Low threshold: `return` fires first, with total = 6 > 5 at i == 3. + let (result, output) = run(&mut interpreter, "ReturnAndBreak(5)"); + is_only_value(&result, &output, &Value::Int(6)); + } + + #[test] + fn break_skips_remaining_body_statements() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation BreakSkipsRest() : Int { + mutable total = 0; + for i in 0..10 { + total += 1; + if i == 3 { + break; + } + total += 100; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // i = 0,1,2 each add 1 + 100 = 101; i = 3 adds 1 then breaks before the + // guarded `set total += 100`, so 3*101 + 1 = 304. + let (result, output) = run(&mut interpreter, "BreakSkipsRest()"); + is_only_value(&result, &output, &Value::Int(304)); + } + + #[test] + fn continue_runs_loop_step_and_skips_rest() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation ContinueSkipsRest() : Int { + mutable total = 0; + for i in 0..4 { + total += 1; + if i % 2 == 0 { + continue; + } + total += 100; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // All 5 iterations run `set total += 1`, since the loop step always + // advances, but `continue` on even i skips `set total += 100`; it runs + // only for i = 1 and i = 3: 5 + 2*100 = 205. + let (result, output) = run(&mut interpreter, "ContinueSkipsRest()"); + is_only_value(&result, &output, &Value::Int(205)); + } + + #[test] + fn qubit_use_inside_loop_body_with_break_releases() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation UseQInLoopBreak() : Int { + mutable count = 0; + for i in 0..5 { + use q = Qubit(); + X(q); + Reset(q); + if i == 2 { + break; + } + count += 1; + } + count + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // Each iteration allocates, uses, resets, and releases its own qubit. + // A clean run to 2 proves no double-release and no leak across `break`. + let (result, output) = run(&mut interpreter, "UseQInLoopBreak()"); + is_only_value(&result, &output, &Value::Int(2)); + } + + #[test] + fn qubit_use_inside_loop_body_with_continue_releases() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation UseQInLoopContinue() : Int { + mutable count = 0; + for i in 0..5 { + use q = Qubit(); + X(q); + Reset(q); + if i % 2 == 0 { + continue; + } + count += 1; + } + count + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // All 6 iterations allocate/reset/release their qubit; `continue` skips + // only the counter bump, incremented for odd i = 1,3,5 -> 3. + let (result, output) = run(&mut interpreter, "UseQInLoopContinue()"); + is_only_value(&result, &output, &Value::Int(3)); + } + + #[test] + fn qubit_use_outside_loop_with_break_releases() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation UseQOutsideLoopBreak() : Int { + use q = Qubit(); + mutable steps = 0; + for x in 0..10 { + if x == 3 { + break; + } + steps += 1; + } + X(q); + let r = MResetZ(q); + steps + (r == One ? 100 | 0) + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // The loop breaks at x == 3, with steps = 3, and falls through to the + // enclosing block; the outer qubit is used and released only after the + // loop. Clean run: 3 + 100 = 103. + let (result, output) = run(&mut interpreter, "UseQOutsideLoopBreak()"); + is_only_value(&result, &output, &Value::Int(103)); + } + + #[test] + fn qubit_borrow_inside_loop_with_break_releases() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation BorrowInLoopBreak() : Int { + mutable count = 0; + for i in 0..5 { + borrow b = Qubit(); + if i == 2 { + break; + } + count += 1; + } + count + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // The borrowed qubit is returned at the end of each loop body even on + // the `break` iteration. Clean run to 2. + let (result, output) = run(&mut interpreter, "BorrowInLoopBreak()"); + is_only_value(&result, &output, &Value::Int(2)); + } + + #[test] + fn break_path_still_releases_qubit() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation BreakLeavesQubitDirty() : Unit { + for i in 0..5 { + use q = Qubit(); + X(q); + if i == 2 { + break; + } + Reset(q); + } + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // On the `break` iteration the guarded `Reset(q)` is skipped, so the + // qubit is still in |1> when end-of-block release fires. The release + // therefore reports a non-zero qubit, proving release runs on the + // `break` path rather than leaking the qubit. + let (result, output) = run(&mut interpreter, "BreakLeavesQubitDirty()"); + is_only_error( + &result, + &output, + &expect![[r#" + runtime error: Qubit0 released while not in |0⟩ state + Qubit0 [line_0] [use q = Qubit();] + "#]], + ); + } + + #[test] + fn runtime_error_in_guarded_statement_points_at_user_statement() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation GuardedRuntimeError() : Int { + let data = [1, 2, 3]; + mutable total = 0; + for i in 0..10 { + if i == 0 { + continue; + } + total = total + data[i]; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // `set total = total + data[i]` runs guarded after `continue`. At i == 3 + // the index is out of range; the runtime error points at the user + // expression `data[i]`, not at the synthetic guard, which has no span. + let (result, output) = run(&mut interpreter, "GuardedRuntimeError()"); + is_only_error( + &result, + &output, + &expect![[r#" + runtime error: index out of range: 3 + out of range [line_0] [i] + "#]], + ); + } + + #[test] + fn operand_block_break_qubit_array_backed_is_sound() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation Foo(q : Qubit) : Int { 5 } + operation OperandBreakQubit() : Int { + use q = Qubit(); + mutable total = 0; + for i in 0..10 { + total += Foo(if i == 2 { break } else { q }); + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // The operand `if i == 2 { break } else { q }` is `Qubit`-typed and has + // no classical default, so it is array-backed as `Qubit[]`. On i = 0, 1 + // it yields `q`, so `Foo(q) = 5` accumulates to 10. On i == 2 the break + // fires: the temp holds the empty-array default `[]`, but the guarded + // read `.operand_tmp_[0]` is never evaluated, so no index-out-of-range + // error occurs and the loop exits with total = 10. An unsound, unguarded + // read would instead fault on `[][0]`. + let (result, output) = run(&mut interpreter, "OperandBreakQubit()"); + is_only_value(&result, &output, &Value::Int(10)); + } + + #[test] + fn operand_block_break_arrow_array_backed_is_sound() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation Flip(q : Qubit) : Unit { X(q); } + operation OperandBreakArrow() : Int { + use q = Qubit(); + mutable count = 0; + for i in 0..10 { + (if i == 3 { break } else { Flip })(q); + Reset(q); + count += 1; + } + count + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // The callee operand `if i == 3 { break } else { Flip }` is arrow-typed + // (`Qubit => Unit`), which has no classical default; it is array-backed + // as `(Qubit => Unit)[]`. Previously such operands were rejected by the + // desugar. On i = 0, 1, 2 the operation runs and count reaches 3; on + // i == 3 the break fires and the guarded call `.operand_tmp_[0](q)` + // on the empty-array temp is never evaluated, so the loop exits with count = 3. + let (result, output) = run(&mut interpreter, "OperandBreakArrow()"); + is_only_value(&result, &output, &Value::Int(3)); + } + + #[test] + fn operand_block_break_udt_array_backed_is_sound() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + struct Boxed { Value : Int } + function Unbox(b : Boxed) : Int { b.Value } + operation OperandBreakUdt() : Int { + mutable total = 0; + for i in 0..10 { + total += Unbox(if i == 2 { break } else { new Boxed { Value = i + 1 } }); + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // The operand `if i == 2 { break } else { new Boxed { Value = i + 1 } }` + // is a user-defined type with no classical default; it is array-backed as + // `Boxed[]`. Previously the two passes disagreed and it was rejected. On + // i = 0, 1 it unwraps to 1 and 2, so total = 3; on i == 2 the break fires + // and the guarded read `.operand_tmp_[0]` on the empty-array temp is never + // evaluated, so the loop exits with total = 3. + let (result, output) = run(&mut interpreter, "OperandBreakUdt()"); + is_only_value(&result, &output, &Value::Int(3)); + } + + #[test] + fn operand_bare_break_skips_eager_consumer() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {r#" + operation Sink(x : Int) : Int { Message("BUG"); x } + operation BareOperandBreak() : Int { + mutable total = 0; + for i in 0..10 { + total += i; + if i == 2 { + total += Sink(break); + } + } + total + } + "#}, + ); + is_only_value(&result, &output, &Value::unit()); + // The divergent operand `break` is hoisted out of `Sink(break)`, so the + // eager consumer `Sink` never runs: iterations i = 0, 1, 2 accumulate + // 0 + 1 + 2 = 3 before the break at i == 2 fires, and the guarded + // `set total += Sink(...)`, which would emit "BUG", is skipped. An + // unguarded rewrite would instead call `Sink(0)`, printing "BUG", and + // leave total = 3 + 0. The empty output asserts `Sink` did not run. + let (result, output) = run(&mut interpreter, "BareOperandBreak()"); + is_only_value(&result, &output, &Value::Int(3)); + } + + #[test] + fn operand_bare_continue_skips_eager_consumer() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {r#" + operation Sink(x : Int) : Int { Message("BUG"); x } + operation BareOperandContinue() : Int { + mutable total = 0; + for i in 0..3 { + if i == 2 { + total += Sink(continue); + } + total += i; + } + total + } + "#}, + ); + is_only_value(&result, &output, &Value::unit()); + // The divergent operand `continue` is hoisted out of `Sink(continue)`, + // so `Sink` never runs. The inclusive range `0..3` iterates i = 0, 1, + // 2, 3. On i == 2 the continue skips the rest of that iteration, both + // the guarded `set total += Sink(...)` and the trailing `set total += + // i`, but does not exit the loop, so `set total += i` runs for i = 0, + // 1, 3 -> 0 + 1 + 3 = 4. Unlike the break analogue, which would stop + // the loop at total = 1, the loop continues. The empty output asserts + // `Sink` did not run. + let (result, output) = run(&mut interpreter, "BareOperandContinue()"); + is_only_value(&result, &output, &Value::Int(4)); + } + + #[test] + fn operand_tuple_element_break_skips_consumer() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {r#" + operation Sink(x : Int) : Int { Message("BUG"); x } + operation TupleOperandBreak() : Int { + mutable total = 0; + for i in 0..10 { + total += i; + if i == 2 { + let (a, _) = (Sink(break), 0); + total += a; + } + } + total + } + "#}, + ); + is_only_value(&result, &output, &Value::unit()); + // The `break` is buried in a tuple-element operand, `(Sink(break), + // 0)`, whose value is destructured. The N-ary tuple lift hoists the + // divergent operand, so the eager consumer `Sink` never runs and the + // tuple is never built: iterations i = 0, 1, 2 accumulate 0 + 1 + 2 = + // 3 before the break fires, and the guarded binding and its use are + // skipped. The empty output asserts `Sink` did not run. + let (result, output) = run(&mut interpreter, "TupleOperandBreak()"); + is_only_value(&result, &output, &Value::Int(3)); + } + + #[test] + fn operand_array_element_continue_skips_consumer() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {r#" + operation Sink(x : Int) : Int { Message("BUG"); x } + operation ArrayOperandContinue() : Int { + mutable total = 0; + for i in 0..3 { + if i == 2 { + total += [Sink(continue), 0][0]; + } + total += i; + } + total + } + "#}, + ); + is_only_value(&result, &output, &Value::unit()); + // The `continue` is buried in an array-element operand nested inside + // an index, `[Sink(continue), 0][0]`. The N-ary array lift hoists the + // divergent operand, so `Sink` never runs and the array is never + // built. On i == 2 the continue skips the rest of that iteration but + // does not exit the loop, so `set total += i` runs for i = 0, 1, 3 -> + // 0 + 1 + 3 = 4. The empty output asserts `Sink` did not run. + let (result, output) = run(&mut interpreter, "ArrayOperandContinue()"); + is_only_value(&result, &output, &Value::Int(4)); + } + + #[test] + fn operand_assign_break_retains_prior_value() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation AssignOperandBreak() : Int { + mutable x = 0; + for i in 0..10 { + x = i * 10; + if i == 2 { + x = break; + } + } + x + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // `set x = break` is hoisted so the assignment is guarded away on the + // break iteration: x holds its pre-break value 20, from `set x = i * 10` + // at i == 2, when the loop exits. An unguarded rewrite would assign the + // classical default `set x = 0`, yielding 0 instead of 20. + let (result, output) = run(&mut interpreter, "AssignOperandBreak()"); + is_only_value(&result, &output, &Value::Int(20)); + } + + #[test] + fn operand_index_break_avoids_out_of_range() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation IndexOperandBreak() : Int { + let arr = [10, 20, 30]; + mutable total = 0; + for i in 0..10 { + total += i; + if i == 2 { + total += arr[break]; + } + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // `arr[break]` is only reached at i == 2, where the divergent index + // `break` is hoisted so the array is never indexed: no out-of-range + // read occurs regardless of `arr`'s contents. Iterations i = 0, 1, 2 + // accumulate 0 + 1 + 2 = 3 before the break, and the guarded + // `set total += arr[break]` is skipped. (A bare `let y = arr[break];` + // is instead an `AmbiguousTy` type error because the divergent index + // result is unconstrained, so the index is exercised in a typed operand + // slot here.) + let (result, output) = run(&mut interpreter, "IndexOperandBreak()"); + is_only_value(&result, &output, &Value::Int(3)); + } + + #[test] + fn operand_short_circuit_or_rhs_break_skips_consumer() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {r#" + operation SinkB(x : Int) : Bool { Message("BUG"); true } + operation ShortCircuitOrBreak() : Int { + mutable total = 0; + for i in 0..10 { + total += i; + if i == 2 { + if false or SinkB(break) { + total += 1000; + } + } + } + total + } + "#}, + ); + is_only_value(&result, &output, &Value::unit()); + // The `or` left operand is `false`, so the short-circuit reaches the + // right operand `SinkB(break)`. The buried `break` is hoisted, since the + // compound RHS is reshaped from `false or rhs` to `if false { true } else + // { rhs }`, so it fires before `SinkB` runs. Iterations i = 0, 1, 2 + // accumulate 0 + 1 + 2 = 3, and the guarded `set total += 1000` is + // skipped. A rewrite that evaluated the RHS eagerly would call `SinkB`, + // printing "BUG"; the empty output asserts it did not run. + let (result, output) = run(&mut interpreter, "ShortCircuitOrBreak()"); + is_only_value(&result, &output, &Value::Int(3)); + } + + #[test] + fn operand_short_circuit_and_rhs_continue_skips_consumer() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {r#" + operation SinkB(x : Int) : Bool { Message("BUG"); true } + operation ShortCircuitAndContinue() : Int { + mutable total = 0; + for i in 0..3 { + if i == 2 { + if true and SinkB(continue) { + total += 1000; + } + } + total += i; + } + total + } + "#}, + ); + is_only_value(&result, &output, &Value::unit()); + // The `and` left operand is `true`, so the short-circuit reaches the + // right operand `SinkB(continue)`. The buried `continue` is hoisted, + // since the compound RHS is reshaped from `true and rhs` to + // `if true { rhs } else { false }`, so it fires before `SinkB` runs. The + // inclusive range `0..3` iterates i = 0, 1, 2, 3. On i == 2 the continue + // skips the rest of that iteration, both the guarded `set total += 1000` + // and the trailing `set total += i`, but does not exit the loop, so + // `set total += i` runs for i = 0, 1, 3 -> 0 + 1 + 3 = 4. The empty + // output asserts `SinkB` did not run. + let (result, output) = run(&mut interpreter, "ShortCircuitAndContinue()"); + is_only_value(&result, &output, &Value::Int(4)); + } + + #[test] + fn operand_return_nested_break_exits_loop() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation ReturnNestedBreak() : Int { + mutable total = 0; + for i in 0..10 { + total += i; + if i == 2 { + return { if i == 2 { break } else { 999 } }; + } + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // The `break` is buried in a `return` operand. It is hoisted so it exits + // the loop and falls through to the trailing `total`, rather than the + // operation returning the block's else value 999. Iterations i = 0, 1, 2 + // accumulate 0 + 1 + 2 = 3 before the break at i == 2, so the operation + // returns the post-loop total 3, not 999. + let (result, output) = run(&mut interpreter, "ReturnNestedBreak()"); + is_only_value(&result, &output, &Value::Int(3)); + } + + #[test] + fn operand_for_iterable_break_binds_to_outer_loop() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {r#" + operation SinkArr() : Int { Message("BUG"); 0 } + function Combine(head : Int[], tag : Int) : Int[] { head } + operation ForIterableBreak() : Int { + mutable total = 0; + for i in 0..10 { + total += i; + if i == 2 { + for j in Combine(if i == 2 { break } else { [10, 20, 30] }, SinkArr()) { + total += 1000; + } + } + } + total + } + "#}, + ); + is_only_value(&result, &output, &Value::unit()); + // The `break` is buried in the inner `for`'s iterable, which is evaluated + // in the outer loop's scope, so the break binds to the outer loop. The + // iterable's operands evaluate left to right: the hoisted `break` fires + // before the sibling `SinkArr()` operand runs, so `SinkArr` (which would + // print "BUG") never executes and the inner loop body never runs. + // Iterations i = 0, 1, 2 accumulate 0 + 1 + 2 = 3 before the break exits + // the outer loop. The empty output asserts `SinkArr` did not run. + let (result, output) = run(&mut interpreter, "ForIterableBreak()"); + is_only_value(&result, &output, &Value::Int(3)); + } + + #[test] + fn operand_core_udt_break_array_backed() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + function Fields(c : Complex) : Int { + (c.Real == 1.0 and c.Imag == 2.0) ? 1 | 0 + } + operation CoreUdtOperandBreak() : Int { + mutable total = 0; + for i in 0..10 { + total += Fields(if i == 2 { break } else { Complex(1.0, 2.0) }); + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // `Complex` is a core-library UDT defined in a different package, read + // via the `c.Real` and `c.Imag` fields. The call-argument operand + // `if i == 2 { break } else { Complex(1.0, 2.0) }` has no classical + // default and is array-backed as `Complex[]`; array-backing needs only + // the universal `[]` default, never a default of the foreign type, so + // the cross-package operand is representable rather than rejected with + // `UnsupportedType`. On i = 0, 1 the operand is `Complex(1.0, 2.0)`, so + // `Fields` returns 1 and total reaches 2; on i == 2 the break fires + // before the operand value is read and the guarded read of the temp + // (`.operand_tmp_[0]`) is skipped, so the loop exits with total = 2. + // (A bare `let c = if i == 2 { break } else { Complex(1.0, 2.0) };` is + // instead rejected, because that binding is handled in place by the loop + // desugar, which needs a classical default of the foreign `Complex` + // type; the array-backing that avoids the default only applies to a + // call-argument operand, so the break is exercised in that slot here.) + let (result, output) = run(&mut interpreter, "CoreUdtOperandBreak()"); + is_only_value(&result, &output, &Value::Int(2)); + } + + #[test] + fn operand_compound_and_assign_break_skips_consumer() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {r#" + operation Sink(x : Bool) : Bool { Message("BUG: Sink ran"); x } + operation AndAssignBreak() : Int { + mutable total = 0; + mutable b = true; + for i in 0..10 { + total += i; + if i == 2 { + b and= Sink(break); + } + } + total + } + "#}, + ); + is_only_value(&result, &output, &Value::unit()); + // The `break` is buried in the bare operand `Sink(break)` of a compound + // short-circuit assignment `set b and= ...`. It is reshaped so the + // assignment, along with `Sink`, is guarded by the divergence: the break + // fires before `Sink` is called, so `Sink`, which would print "BUG", + // never runs. Iterations i = 0, 1, 2 accumulate 0 + 1 + 2 = 3 before the + // break exits the loop. The empty output asserts the eager consumer was + // skipped. + let (result, output) = run(&mut interpreter, "AndAssignBreak()"); + is_only_value(&result, &output, &Value::Int(3)); + } + + #[test] + fn operand_compound_or_assign_continue_skips_consumer() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {r#" + operation Sink(x : Bool) : Bool { Message("BUG: Sink ran"); x } + operation OrAssignContinue() : Int { + mutable total = 0; + mutable b = false; + for i in 0..3 { + if i == 1 { + b or= Sink(continue); + } + total += i; + } + total + } + "#}, + ); + is_only_value(&result, &output, &Value::unit()); + // `set b or= Sink(continue)` reaches its right operand because `b` is + // false; the `continue` buried in the bare operand `Sink(continue)` is + // reshaped so `Sink` is guarded and never runs, and the `continue` skips + // the rest of the i = 1 iteration, `set total += 1`. Iterations i = 0, 2, + // 3 accumulate 0 + 2 + 3 = 5. The empty output asserts `Sink` was skipped. + let (result, output) = run(&mut interpreter, "OrAssignContinue()"); + is_only_value(&result, &output, &Value::Int(5)); + } + + #[test] + fn let_rhs_break_core_udt_binding_supported() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + function Fields(c : Complex) : Int { + (c.Real == 1.0 and c.Imag == 2.0) ? 1 | 0 + } + operation LetRhsBreakUdt() : Int { + mutable total = 0; + for i in 0..10 { + let c = if i == 2 { break } else { Complex(1.0, 2.0) }; + total += Fields(c); + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // `let c : Complex = if i == 2 { break } else { Complex(1.0, 2.0) }` + // binds a non-defaultable core-library UDT directly from an `if` whose + // `then` branch is a `break`. The initializer is array-backed, stored as + // `Complex[]` whose default is `[]`, and the binding is relocated into + // the fall-through branch, so no `Complex` default is needed. On i = 0, 1 + // the binding is `Complex(1.0, 2.0)` and `Fields` returns 1, so total = 2; + // on i == 2 the break exits the loop before `c` is bound. + let (result, output) = run(&mut interpreter, "LetRhsBreakUdt()"); + is_only_value(&result, &output, &Value::Int(2)); + } + + #[test] + fn let_rhs_break_qubit_binding_supported() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation Op(q : Qubit) : Unit { X(q); } + operation LetRhsBreakQubit() : Int { + use q = Qubit(); + mutable count = 0; + for i in 0..10 { + let target = if i == 3 { break } else { q }; + Op(target); + Reset(target); + count += 1; + } + count + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // `let target : Qubit = if i == 3 { break } else { q }` binds a + // non-defaultable `Qubit` directly. The initializer is array-backed as + // `Qubit[]` and the binding is relocated into the fall-through branch, so + // on i = 0, 1, 2 `target` aliases `q`, `Op` runs, and count reaches 3; on + // i == 3 the break exits before `target` is bound. + let (result, output) = run(&mut interpreter, "LetRhsBreakQubit()"); + is_only_value(&result, &output, &Value::Int(3)); + } + + #[test] + fn break_before_non_defaultable_let_binding_supported() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + function Fields(c : Complex) : Int { (c.Real == 1.0) ? 1 | 0 } + operation BreakBeforeLet() : Int { + mutable total = 0; + for i in 0..10 { + if i == 2 { break; } + let c = Complex(1.0, 2.0); + total += Fields(c); + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // A non-defaultable `let c : Complex` that follows a break in the same + // block is relocated, along with the rest of the block, into the + // fall-through branch, so it needs no `Complex` default. On i = 0, 1 + // `Fields` returns 1, so total = 2; on i == 2 the break exits before + // `c` is bound. + let (result, output) = run(&mut interpreter, "BreakBeforeLet()"); + is_only_value(&result, &output, &Value::Int(2)); + } + + #[test] + fn discarded_value_block_break_supported() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {" + operation DiscardedValueBlockBreak() : Int { + mutable total = 0; + for i in 0..5 { + total += i; + { if i == 2 { break } else { Complex(1.0, 2.0) } }; + } + total + } + "}, + ); + is_only_value(&result, &output, &Value::unit()); + // A non-Unit block used as a statement with its result discarded, whose + // `then` branch is a `break`, has no `Complex` default. Because the value + // is discarded, its type is array-backed to `Complex[]` in place, so the + // break desugars with the universal `[]` default and the block compiles. + // On i = 0, 1 the discarded value is `Complex(1.0, 2.0)`; total reaches + // 0 + 1 + 2 = 3 before the break at i == 2 exits the loop. + let (result, output) = run(&mut interpreter, "DiscardedValueBlockBreak()"); + is_only_value(&result, &output, &Value::Int(3)); + } + } } diff --git a/source/compiler/qsc_ast/src/ast.rs b/source/compiler/qsc_ast/src/ast.rs index b43755ec490..a73cd3f1d6a 100644 --- a/source/compiler/qsc_ast/src/ast.rs +++ b/source/compiler/qsc_ast/src/ast.rs @@ -877,10 +877,14 @@ pub enum ExprKind { BinOp(BinOp, Box, Box), /// A block: `{ ... }`. Block(Box), + /// A break out of the innermost enclosing loop: `break`. + Break, /// A call: `a(b)`. Call(Box, Box), /// A conjugation: `within { ... } apply { ... }`. Conjugate(Box, Box), + /// A continuation to the next iteration of the innermost enclosing loop: `continue`. + Continue, /// An expression with invalid syntax that can't be parsed. #[default] Err, @@ -912,7 +916,7 @@ pub enum ExprKind { Path(PathKind), /// A range: `start..step..end`, `start..end`, `start...`, `...end`, or `...`. Range(Option>, Option>, Option>), - /// A repeat-until loop with an optional fixup: `repeat { ... } until a fixup { ... }`. + /// A repeat-until loop with an optional fixup: `repeat { ... } until condition fixup { ... }`. Repeat(Box, Box, Option>), /// A return: `return a`. Return(Box), @@ -941,8 +945,10 @@ impl Display for ExprKind { } ExprKind::BinOp(op, lhs, rhs) => display_bin_op(indent, *op, lhs, rhs)?, ExprKind::Block(block) => write!(indent, "Expr Block: {block}")?, + ExprKind::Break => write!(indent, "Break")?, ExprKind::Call(callable, arg) => display_call(indent, callable, arg)?, ExprKind::Conjugate(within, apply) => display_conjugate(indent, within, apply)?, + ExprKind::Continue => write!(indent, "Continue")?, ExprKind::Err => write!(indent, "Err")?, ExprKind::Fail(e) => write!(indent, "Fail: {e}")?, ExprKind::Field(expr, id) => display_field(indent, expr, id)?, diff --git a/source/compiler/qsc_ast/src/mut_visit.rs b/source/compiler/qsc_ast/src/mut_visit.rs index ce80b9ca87f..840a16ab652 100644 --- a/source/compiler/qsc_ast/src/mut_visit.rs +++ b/source/compiler/qsc_ast/src/mut_visit.rs @@ -401,7 +401,11 @@ pub fn walk_expr(vis: &mut impl MutVisitor, expr: &mut Expr) { vis.visit_expr(cond); vis.visit_block(block); } - ExprKind::Err | ExprKind::Hole | ExprKind::Lit(_) => {} + ExprKind::Break + | ExprKind::Continue + | ExprKind::Err + | ExprKind::Hole + | ExprKind::Lit(_) => {} } } diff --git a/source/compiler/qsc_ast/src/visit.rs b/source/compiler/qsc_ast/src/visit.rs index f123b320a0d..1d7b41c72df 100644 --- a/source/compiler/qsc_ast/src/visit.rs +++ b/source/compiler/qsc_ast/src/visit.rs @@ -373,7 +373,11 @@ pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: &'a Expr) { vis.visit_expr(cond); vis.visit_block(block); } - ExprKind::Err | ExprKind::Hole | ExprKind::Lit(_) => {} + ExprKind::Break + | ExprKind::Continue + | ExprKind::Err + | ExprKind::Hole + | ExprKind::Lit(_) => {} } } diff --git a/source/compiler/qsc_codegen/src/qsharp.rs b/source/compiler/qsc_codegen/src/qsharp.rs index 71eee205190..bc457ebc430 100644 --- a/source/compiler/qsc_codegen/src/qsharp.rs +++ b/source/compiler/qsc_codegen/src/qsharp.rs @@ -468,6 +468,9 @@ impl Visitor<'_> for QSharpGen { self.visit_expr(value); } ExprKind::Block(block) => self.visit_block(block), + ExprKind::Break => { + self.write("break"); + } ExprKind::Call(callee, arg) => { self.visit_expr(callee); self.visit_expr(arg); @@ -478,6 +481,9 @@ impl Visitor<'_> for QSharpGen { self.write("apply"); self.visit_block(apply); } + ExprKind::Continue => { + self.write("continue"); + } ExprKind::Fail(msg) => { self.write("fail "); self.visit_expr(msg); diff --git a/source/compiler/qsc_eval/src/lib.rs b/source/compiler/qsc_eval/src/lib.rs index c7eeaf2f6ca..f7d2554430a 100644 --- a/source/compiler/qsc_eval/src/lib.rs +++ b/source/compiler/qsc_eval/src/lib.rs @@ -948,6 +948,16 @@ impl State { ) -> Option<(StepResult, Span)> { if step == StepAction::Next && current_frame >= self.current_frame_id() { let block = globals.get_block((self.package, block).into()); + // A synthetic block introduced by a desugar (for example the guard + // blocks the loop `break`/`continue` desugar emits) carries + // `Span::default()`, marking generated code with no source location. + // Skip its exit as a step point, matching `check_for_break`'s handling + // of generated code. This also avoids underflowing the `u32` offset in + // `block.span.hi - 1` below, which would otherwise surface a bogus + // end-of-file location when stepping. + if block.span == Span::default() { + return None; + } let span = Span { lo: block.span.hi - 1, hi: block.span.hi, diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/normalize/tests/regression_and_depth.rs b/source/compiler/qsc_fir_transforms/src/return_unify/normalize/tests/regression_and_depth.rs index 8010addd7f8..ceb893efc3c 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/normalize/tests/regression_and_depth.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/normalize/tests/regression_and_depth.rs @@ -57,7 +57,7 @@ fn if_then_return_else_return_at_end_records_flag_lowered_shape() { [0] Local(Mutable, _.has_returned: Bool): Lit(Bool(false)) [1] Local(Mutable, _.ret_val: Int): Lit(Int(0)) [2] Local(Immutable, q: Qubit): Call[ty=Qubit] - [3] Local(Immutable, @generated_ident_59: Unit): If(cond=BinOp(Eq)[ty=Bool], then=Block[ty=Unit], else=Block[ty=Unit]) + [3] Local(Immutable, .generated_ident_59: Unit): If(cond=BinOp(Eq)[ty=Bool], then=Block[ty=Unit], else=Block[ty=Unit]) [4] Semi If(cond=UnOp(NotL)[ty=Bool], then=Block[ty=Unit]) [5] Expr Var[ty=Int]"#]], ); diff --git a/source/compiler/qsc_frontend/src/lower.rs b/source/compiler/qsc_frontend/src/lower.rs index 1113067d2ef..dcfd26636b8 100644 --- a/source/compiler/qsc_frontend/src/lower.rs +++ b/source/compiler/qsc_frontend/src/lower.rs @@ -743,6 +743,7 @@ impl With<'_> { Box::new(self.lower_expr(rhs)), ), ast::ExprKind::Block(block) => hir::ExprKind::Block(self.lower_block(block)), + ast::ExprKind::Break => hir::ExprKind::Break, ast::ExprKind::Call(callee, arg) => match &ty { Ty::Arrow(arrow) if is_partial_app(arg) => hir::ExprKind::Block( self.lower_partial_app(callee, arg, arrow.clone(), expr.span), @@ -755,6 +756,7 @@ impl With<'_> { ast::ExprKind::Conjugate(within, apply) => { hir::ExprKind::Conjugate(self.lower_block(within), self.lower_block(apply)) } + ast::ExprKind::Continue => hir::ExprKind::Continue, ast::ExprKind::Fail(message) => hir::ExprKind::Fail(Box::new(self.lower_expr(message))), ast::ExprKind::Field(container, FieldAccess::Ok(name)) => { let container = self.lower_expr(container); diff --git a/source/compiler/qsc_frontend/src/lower/tests.rs b/source/compiler/qsc_frontend/src/lower/tests.rs index 1237d6c105e..1d31df6983b 100644 --- a/source/compiler/qsc_frontend/src/lower/tests.rs +++ b/source/compiler/qsc_frontend/src/lower/tests.rs @@ -3058,3 +3058,114 @@ fn literal_complex_lowers_as_struct_decl() { ctl-adj: "#]], ); } + +#[test] +fn lower_break_continue_in_for() { + check_hir( + indoc! {" + operation Foo() : Unit { + for _ in 0..3 { + break; + continue; + } + } + "}, + &expect![[r#" + Package: + Item 0 [0-85] (Public): + Namespace (Ident 16 [0-85] "test"): Item 1 + Item 1 [0-85] (Internal): + Parent: 0 + Callable 0 [0-85] (operation): + name: Ident 1 [10-13] "Foo" + input: Pat 2 [13-15] [Type Unit]: Unit + output: Unit + functors: empty set + body: SpecDecl 3 [0-85]: Impl: + Block 4 [23-85] [Type Unit]: + Stmt 5 [29-83]: Expr: Expr 6 [29-83] [Type Unit]: For: + Pat 7 [33-34] [Type Int]: Discard + Expr 8 [38-42] [Type Range]: Range: + Expr 9 [38-39] [Type Int]: Lit: Int(0) + + Expr 10 [41-42] [Type Int]: Lit: Int(3) + Block 11 [43-83] [Type Unit]: + Stmt 12 [53-59]: Semi: Expr 13 [53-58] [Type Unit]: Break + Stmt 14 [68-77]: Semi: Expr 15 [68-76] [Type Unit]: Continue + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn lower_break_continue_in_while() { + check_hir( + indoc! {" + operation Foo() : Unit { + while true { + break; + continue; + } + } + "}, + &expect![[r#" + Package: + Item 0 [0-82] (Public): + Namespace (Ident 13 [0-82] "test"): Item 1 + Item 1 [0-82] (Internal): + Parent: 0 + Callable 0 [0-82] (operation): + name: Ident 1 [10-13] "Foo" + input: Pat 2 [13-15] [Type Unit]: Unit + output: Unit + functors: empty set + body: SpecDecl 3 [0-82]: Impl: + Block 4 [23-82] [Type Unit]: + Stmt 5 [29-80]: Expr: Expr 6 [29-80] [Type Unit]: While: + Expr 7 [35-39] [Type Bool]: Lit: Bool(true) + Block 8 [40-80] [Type Unit]: + Stmt 9 [50-56]: Semi: Expr 10 [50-55] [Type Unit]: Break + Stmt 11 [65-74]: Semi: Expr 12 [65-73] [Type Unit]: Continue + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn lower_break_continue_in_repeat() { + check_hir( + indoc! {" + operation Foo() : Unit { + repeat { + break; + continue; + } + until true; + } + "}, + &expect![[r#" + Package: + Item 0 [0-94] (Public): + Namespace (Ident 13 [0-94] "test"): Item 1 + Item 1 [0-94] (Internal): + Parent: 0 + Callable 0 [0-94] (operation): + name: Ident 1 [10-13] "Foo" + input: Pat 2 [13-15] [Type Unit]: Unit + output: Unit + functors: empty set + body: SpecDecl 3 [0-94]: Impl: + Block 4 [23-94] [Type Unit]: + Stmt 5 [29-92]: Semi: Expr 6 [29-91] [Type Unit]: Repeat: + Block 7 [36-76] [Type Unit]: + Stmt 8 [46-52]: Semi: Expr 9 [46-51] [Type Unit]: Break + Stmt 10 [61-70]: Semi: Expr 11 [61-69] [Type Unit]: Continue + Expr 12 [87-91] [Type Bool]: Lit: Bool(true) + + adj: + ctl: + ctl-adj: "#]], + ); +} diff --git a/source/compiler/qsc_frontend/src/typeck/rules.rs b/source/compiler/qsc_frontend/src/typeck/rules.rs index ea7a63ee6fa..a1007013842 100644 --- a/source/compiler/qsc_frontend/src/typeck/rules.rs +++ b/source/compiler/qsc_frontend/src/typeck/rules.rs @@ -600,6 +600,10 @@ impl<'a> Context<'a> { self.typed_holes.push((expr.id, expr.span)); converge(self.inferrer.fresh_ty(TySource::not_divergent(expr.span))) } + // A break or continue expression diverges, like return or fail. Using a + // fresh divergent type variable lets it compose in operand position, so + // `let x = if c { break } else { 3 };` infers `x : Int`. + ExprKind::Break | ExprKind::Continue => self.diverge(), ExprKind::Err | ast::ExprKind::Struct(ast::PathKind::Err(_), ..) => converge(Ty::Err), }; diff --git a/source/compiler/qsc_frontend/src/typeck/tests.rs b/source/compiler/qsc_frontend/src/typeck/tests.rs index 374c67f7621..8e9ef6b3f46 100644 --- a/source/compiler/qsc_frontend/src/typeck/tests.rs +++ b/source/compiler/qsc_frontend/src/typeck/tests.rs @@ -2392,6 +2392,144 @@ fn return_in_lambda_args_alone_diverges() { ); } +#[test] +fn break_diverges() { + check( + "", + indoc! {" + if true { + break + } else { + 4 + } + "}, + &expect![[r##" + #1 0-36 "if true {\n break\n} else {\n 4\n}" : Int + #2 3-7 "true" : Bool + #3 8-21 "{\n break\n}" : Int + #5 14-19 "break" : Unit + #6 22-36 "else {\n 4\n}" : Int + #7 27-36 "{\n 4\n}" : Int + #9 33-34 "4" : Int + "##]], + ); +} + +#[test] +fn continue_diverges() { + check( + "", + indoc! {" + if true { + continue + } else { + false + } + "}, + &expect![[r##" + #1 0-43 "if true {\n continue\n} else {\n false\n}" : Bool + #2 3-7 "true" : Bool + #3 8-24 "{\n continue\n}" : Bool + #5 14-22 "continue" : Unit + #6 25-43 "else {\n false\n}" : Bool + #7 30-43 "{\n false\n}" : Bool + #9 36-41 "false" : Bool + "##]], + ); +} + +#[test] +fn break_in_let_binding_infers_from_other_branch() { + check( + "", + indoc! {" + { + let x = if true { + break + } else { + 3 + }; + x + } + "}, + &expect![[r##" + #1 0-75 "{\n let x = if true {\n break\n } else {\n 3\n };\n x\n}" : Int + #2 0-75 "{\n let x = if true {\n break\n } else {\n 3\n };\n x\n}" : Int + #4 10-11 "x" : Int + #6 14-66 "if true {\n break\n } else {\n 3\n }" : Int + #7 17-21 "true" : Bool + #8 22-43 "{\n break\n }" : Int + #10 32-37 "break" : Unit + #11 44-66 "else {\n 3\n }" : Int + #12 49-66 "{\n 3\n }" : Int + #14 59-60 "3" : Int + #16 72-73 "x" : Int + "##]], + ); +} + +#[test] +fn continue_in_let_binding_infers_from_other_branch() { + check( + "", + indoc! {" + { + let y = if true { + continue + } else { + true + }; + y + } + "}, + &expect![[r##" + #1 0-81 "{\n let y = if true {\n continue\n } else {\n true\n };\n y\n}" : Bool + #2 0-81 "{\n let y = if true {\n continue\n } else {\n true\n };\n y\n}" : Bool + #4 10-11 "y" : Bool + #6 14-72 "if true {\n continue\n } else {\n true\n }" : Bool + #7 17-21 "true" : Bool + #8 22-46 "{\n continue\n }" : Bool + #10 32-40 "continue" : Unit + #11 47-72 "else {\n true\n }" : Bool + #12 52-72 "{\n true\n }" : Bool + #14 62-66 "true" : Bool + #16 78-79 "y" : Bool + "##]], + ); +} + +#[test] +fn break_in_array_operand_type_checks() { + check( + "", + indoc! {" + [1, break, 3] + "}, + &expect![[r##" + #1 0-13 "[1, break, 3]" : Int[] + #2 1-2 "1" : Int + #3 4-9 "break" : Int + #4 11-12 "3" : Int + "##]], + ); +} + +#[test] +fn continue_in_array_operand_type_checks() { + check( + "", + indoc! {" + [continue, 2, 3] + "}, + &expect![[r##" + #1 0-16 "[continue, 2, 3]" : Int[] + #2 1-9 "continue" : Int + #3 11-12 "2" : Int + #4 14-15 "3" : Int + "##]], + ); +} + #[test] fn return_mismatch() { check( diff --git a/source/compiler/qsc_hir/src/hir.rs b/source/compiler/qsc_hir/src/hir.rs index d2b2df4b6c0..f21bf254465 100644 --- a/source/compiler/qsc_hir/src/hir.rs +++ b/source/compiler/qsc_hir/src/hir.rs @@ -675,12 +675,16 @@ pub enum ExprKind { BinOp(BinOp, Box, Box), /// A block: `{ ... }`. Block(Block), + /// A break out of the innermost enclosing loop: `break`. + Break, /// A call: `a(b)`. Call(Box, Box), /// A closure that fixes the vector of local variables as arguments to the callable item. Closure(Vec, LocalItemId), /// A conjugation: `within { ... } apply { ... }`. Conjugate(Block, Block), + /// A continuation to the next iteration of the innermost enclosing loop: `continue`. + Continue, /// A failure: `fail "message"`. Fail(Box), /// A field accessor: `a::F` or `a.F`. @@ -701,7 +705,7 @@ pub enum ExprKind { Lit(Lit), /// A range: `start..step..end`, `start..end`, `start...`, `...end`, or `...`. Range(Option>, Option>, Option>), - /// A repeat-until loop with an optional fixup: `repeat { ... } until a fixup { ... }`. + /// A repeat-until loop with an optional fixup: `repeat { ... } until condition fixup { ... }`. Repeat(Block, Box, Option), /// A return: `return a`. Return(Box), @@ -742,9 +746,11 @@ impl Display for ExprKind { } ExprKind::BinOp(op, lhs, rhs) => display_bin_op(indent, *op, lhs, rhs)?, ExprKind::Block(block) => write!(indent, "Expr Block: {block}")?, + ExprKind::Break => write!(indent, "Break")?, ExprKind::Call(callable, arg) => display_call(indent, callable, arg)?, ExprKind::Closure(args, callable) => display_closure(indent, args, *callable)?, ExprKind::Conjugate(within, apply) => display_conjugate(indent, within, apply)?, + ExprKind::Continue => write!(indent, "Continue")?, ExprKind::Err => write!(indent, "Err")?, ExprKind::Fail(e) => write!(indent, "Fail: {e}")?, ExprKind::Field(expr, field) => display_field(indent, expr, field)?, diff --git a/source/compiler/qsc_hir/src/mut_visit.rs b/source/compiler/qsc_hir/src/mut_visit.rs index 50a8fc4b56f..b4955b0174e 100644 --- a/source/compiler/qsc_hir/src/mut_visit.rs +++ b/source/compiler/qsc_hir/src/mut_visit.rs @@ -130,6 +130,7 @@ pub fn walk_stmt(vis: &mut impl MutVisitor, stmt: &mut Stmt) { } } +#[allow(clippy::too_many_lines)] pub fn walk_expr(vis: &mut impl MutVisitor, expr: &mut Expr) { vis.visit_span(&mut expr.span); @@ -226,7 +227,9 @@ pub fn walk_expr(vis: &mut impl MutVisitor, expr: &mut Expr) { vis.visit_expr(cond); vis.visit_block(block); } - ExprKind::Closure(_, _) + ExprKind::Break + | ExprKind::Closure(_, _) + | ExprKind::Continue | ExprKind::Err | ExprKind::Hole | ExprKind::Lit(_) diff --git a/source/compiler/qsc_hir/src/visit.rs b/source/compiler/qsc_hir/src/visit.rs index 510c79decea..14ab3e3bf1f 100644 --- a/source/compiler/qsc_hir/src/visit.rs +++ b/source/compiler/qsc_hir/src/visit.rs @@ -113,6 +113,7 @@ pub fn walk_stmt<'a>(vis: &mut impl Visitor<'a>, stmt: &'a Stmt) { } } +#[allow(clippy::too_many_lines)] pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: &'a Expr) { match &expr.kind { ExprKind::Array(exprs) => exprs.iter().for_each(|e| vis.visit_expr(e)), @@ -207,7 +208,9 @@ pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: &'a Expr) { vis.visit_expr(cond); vis.visit_block(block); } - ExprKind::Closure(_, _) + ExprKind::Break + | ExprKind::Closure(_, _) + | ExprKind::Continue | ExprKind::Err | ExprKind::Hole | ExprKind::Lit(_) diff --git a/source/compiler/qsc_lowerer/src/lib.rs b/source/compiler/qsc_lowerer/src/lib.rs index 9b17f199dfe..c893415aee4 100644 --- a/source/compiler/qsc_lowerer/src/lib.rs +++ b/source/compiler/qsc_lowerer/src/lib.rs @@ -797,7 +797,9 @@ impl Lowerer { let args = args.iter().map(|arg| self.lower_generic_arg(arg)).collect(); fir::ExprKind::Var(res, args) } + hir::ExprKind::Break => panic!("break should be eliminated by passes"), hir::ExprKind::Conjugate(..) => panic!("conjugate should be eliminated by passes"), + hir::ExprKind::Continue => panic!("continue should be eliminated by passes"), hir::ExprKind::Err => panic!("error expr should not be present"), hir::ExprKind::For(..) => panic!("for-loop should be eliminated by passes"), hir::ExprKind::Hole => fir::ExprKind::Hole, // allowed for discards diff --git a/source/compiler/qsc_openqasm_compiler/src/ast_builder.rs b/source/compiler/qsc_openqasm_compiler/src/ast_builder.rs index 0daa91440ab..ccf13ec7db6 100644 --- a/source/compiler/qsc_openqasm_compiler/src/ast_builder.rs +++ b/source/compiler/qsc_openqasm_compiler/src/ast_builder.rs @@ -1351,6 +1351,26 @@ pub(crate) fn build_end_stmt(span: Span) -> Stmt { build_stmt_semi_from_expr_with_span(expr, span) } +pub(crate) fn build_break_stmt(span: Span) -> Stmt { + let expr = Expr { + kind: Box::new(ExprKind::Break), + span, + ..Default::default() + }; + + build_stmt_semi_from_expr_with_span(expr, span) +} + +pub(crate) fn build_continue_stmt(span: Span) -> Stmt { + let expr = Expr { + kind: Box::new(ExprKind::Continue), + span, + ..Default::default() + }; + + build_stmt_semi_from_expr_with_span(expr, span) +} + pub(crate) fn build_index_expr(expr: Expr, index_expr: Expr, span: Span) -> Expr { let kind = ExprKind::Index(Box::new(expr), Box::new(index_expr)); Expr { diff --git a/source/compiler/qsc_openqasm_compiler/src/compiler.rs b/source/compiler/qsc_openqasm_compiler/src/compiler.rs index 0cdaf57dc6f..660ce1267c0 100644 --- a/source/compiler/qsc_openqasm_compiler/src/compiler.rs +++ b/source/compiler/qsc_openqasm_compiler/src/compiler.rs @@ -17,11 +17,12 @@ use crate::{ ast_builder::{ build_angle_cast_call_by_name, build_angle_convert_call_with_two_params, build_arg_pat, build_argument_validation_stmts, build_array_reverse_expr, build_assignment_statement, - build_attr, build_barrier_call, build_binary_expr, build_call_no_params, + build_attr, build_barrier_call, build_binary_expr, build_break_stmt, build_call_no_params, build_call_stmt_no_params, build_call_with_param, build_call_with_params, - build_classical_decl, build_complex_from_expr, build_convert_call_expr, - build_convert_cast_call_by_name, build_end_stmt, build_expr_array_expr, build_for_stmt, - build_function_or_operation, build_functor_from_constraints, build_gate_call_param_expr, + build_classical_decl, build_complex_from_expr, build_continue_stmt, + build_convert_call_expr, build_convert_cast_call_by_name, build_end_stmt, + build_expr_array_expr, build_for_stmt, build_function_or_operation, + build_functor_from_constraints, build_gate_call_param_expr, build_gate_call_with_params_and_callee, build_if_expr_then_block, build_if_expr_then_block_else_block, build_if_expr_then_block_else_expr, build_if_expr_then_expr_else_expr, build_implicit_return_stmt, build_index_expr, @@ -706,13 +707,13 @@ impl QasmCompiler { semast::StmtKind::Barrier(stmt) => Self::compile_barrier_stmt(stmt), semast::StmtKind::Box(stmt) => self.compile_box_stmt(stmt), semast::StmtKind::Block(stmt) => self.compile_block_stmt(stmt), - semast::StmtKind::Break(stmt) => self.compile_break_stmt(stmt), + semast::StmtKind::Break(stmt) => Self::compile_break_stmt(stmt), semast::StmtKind::Calibration(cal) => self.compile_calibration_stmt(cal), semast::StmtKind::CalibrationGrammar(stmt) => { self.compile_calibration_grammar_stmt(stmt) } semast::StmtKind::ClassicalDecl(stmt) => self.compile_classical_decl(stmt), - semast::StmtKind::Continue(stmt) => self.compile_continue_stmt(stmt), + semast::StmtKind::Continue(stmt) => Self::compile_continue_stmt(stmt), semast::StmtKind::Def(def_stmt) => self.compile_def_stmt(def_stmt, &stmt.annotations), semast::StmtKind::DefCal(stmt) => self.compile_def_cal_stmt(stmt), semast::StmtKind::Delay(stmt) => self.compile_delay_stmt(stmt), @@ -991,9 +992,8 @@ impl QasmCompiler { Some(build_stmt_semi_from_expr(build_wrapped_block_expr(block))) } - fn compile_break_stmt(&mut self, stmt: &semast::BreakStmt) -> Option { - self.push_unsupported_error_message("break stmt", stmt.span); - None + fn compile_break_stmt(stmt: &semast::BreakStmt) -> Option { + Some(build_break_stmt(stmt.span)) } fn compile_calibration_stmt(&mut self, stmt: &semast::CalibrationStmt) -> Option { @@ -1032,9 +1032,8 @@ impl QasmCompiler { Some(stmt) } - fn compile_continue_stmt(&mut self, stmt: &semast::ContinueStmt) -> Option { - self.push_unsupported_error_message("continue stmt", stmt.span); - None + fn compile_continue_stmt(stmt: &semast::ContinueStmt) -> Option { + Some(build_continue_stmt(stmt.span)) } fn compile_def_stmt( diff --git a/source/compiler/qsc_openqasm_compiler/src/lib.rs b/source/compiler/qsc_openqasm_compiler/src/lib.rs index 4f6cb25659e..4202919fc02 100644 --- a/source/compiler/qsc_openqasm_compiler/src/lib.rs +++ b/source/compiler/qsc_openqasm_compiler/src/lib.rs @@ -7,7 +7,7 @@ // `cargo clippy` won't trigger the failure. If you want to reproduce the failure // with the minimal command possible, you can run `cargo clippy --test -- -D warnings`. // -// We tried to track down the error, but it is non-deterministic. Our assumpution +// We tried to track down the error, but it is non-deterministic. Our assumption // is that clippy is running out of stack memory because of how many and how large // the static strings in the test modules are. // @@ -155,16 +155,16 @@ impl Default for CompilerConfig { } } -/// Represents the type of compilation output to create +/// Represents the type of compilation output to create. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ProgramType { /// Creates an operation in a namespace as if the program is a standalone - /// file. Inputs are lifted to the operation params. Output are lifted to + /// file. Inputs are lifted to the operation parameters. Outputs are lifted to /// the operation return type. The operation is marked as `@EntryPoint` /// as long as there are no input parameters. File, - /// Programs are compiled to a standalone function. Inputs are lifted to - /// the operation params. Output are lifted to the operation return type. + /// Programs are compiled to a standalone operation. Inputs are lifted to + /// the operation parameters. Outputs are lifted to the operation return type. Operation, /// Creates a list of statements from the program. This is useful for /// interactive environments where the program is a list of statements @@ -176,7 +176,7 @@ pub enum ProgramType { /// Represents the signature of an operation. /// This is used to create a function signature for the /// operation that is created from the QASM source code. -/// This is the human readable form of the operation. +/// This is the human-readable form of the operation. #[derive(Debug, Clone, PartialEq, Eq)] pub struct OperationSignature { pub name: String, diff --git a/source/compiler/qsc_openqasm_compiler/src/tests/compiler_errors.rs b/source/compiler/qsc_openqasm_compiler/src/tests/compiler_errors.rs index d9688f2a18d..e40df9ace6d 100644 --- a/source/compiler/qsc_openqasm_compiler/src/tests/compiler_errors.rs +++ b/source/compiler/qsc_openqasm_compiler/src/tests/compiler_errors.rs @@ -87,64 +87,44 @@ fn check_compiler_error_spans_are_correct() { 37 | } `---- - Qdk.Qasm.Compiler.NotSupported - - x break stmt are not supported - ,-[Test.qasm:40:5] - 39 | for int i in [0:2] { - 40 | break; // NotSupported break - : ^^^^^^ - 41 | } - `---- - - Qdk.Qasm.Compiler.NotSupported - - x continue stmt are not supported - ,-[Test.qasm:44:5] - 43 | for int i in [0:2] { - 44 | continue; // NotSupported continue - : ^^^^^^^^^ - 45 | } - `---- - Qdk.Qasm.Compiler.NotSupported x mutable array references `mutable array[int[8], #dim = 1]` are not | supported - ,-[Test.qasm:49:24] - 48 | // NotSupported mutable array reference - 49 | def mut_subroutine_dyn(mutable array[int[8], #dim = 1] arr_arg) { + ,-[Test.qasm:41:24] + 40 | // NotSupported mutable array reference + 41 | def mut_subroutine_dyn(mutable array[int[8], #dim = 1] arr_arg) { : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 50 | + 42 | // body `---- Qdk.Qasm.Compiler.NotSupported x mutable array references `mutable array[int[8], 2, 3]` are not supported - ,-[Test.qasm:54:27] - 53 | // NotSupported mutable static sized array reference - 54 | def mut_subroutine_static(mutable array[int[8], 2, 3] arr_arg) { + ,-[Test.qasm:46:27] + 45 | // NotSupported mutable static sized array reference + 46 | def mut_subroutine_static(mutable array[int[8], 2, 3] arr_arg) { : ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 55 | + 47 | // body `---- Qdk.Qasm.Compiler.Unimplemented x this statement is not yet handled during OpenQASM 3 import: extern | statements - ,-[Test.qasm:78:1] - 77 | // Unimplemented - 78 | extern extern_func(int); + ,-[Test.qasm:70:1] + 69 | // Unimplemented + 70 | extern extern_func(int); : ^^^^^^^^^^^^^^^^^^^^^^^^ - 79 | + 71 | // End unimplemented statements. `---- Qdk.Qasm.Compiler.NotSupported x hardware qubit operands are not supported - ,-[Test.qasm:81:3] - 80 | // NotSupported hardware qubit - 81 | x $0; + ,-[Test.qasm:73:3] + 72 | // NotSupported hardware qubit + 73 | x $0; : ^^ `---- "#]], diff --git a/source/compiler/qsc_openqasm_compiler/src/tests/resources/openqasm_compiler_errors_test.qasm b/source/compiler/qsc_openqasm_compiler/src/tests/resources/openqasm_compiler_errors_test.qasm index 11ed4e10bd9..889a6caae7a 100644 --- a/source/compiler/qsc_openqasm_compiler/src/tests/resources/openqasm_compiler_errors_test.qasm +++ b/source/compiler/qsc_openqasm_compiler/src/tests/resources/openqasm_compiler_errors_test.qasm @@ -36,23 +36,15 @@ box [2ns] { // NotSupported box duration x [2ns] q; // NotSupported duration on gate call } -for int i in [0:2] { - break; // NotSupported break -} - -for int i in [0:2] { - continue; // NotSupported continue -} - // NotSupported mutable array reference def mut_subroutine_dyn(mutable array[int[8], #dim = 1] arr_arg) { - + // body } // NotSupported mutable static sized array reference def mut_subroutine_static(mutable array[int[8], 2, 3] arr_arg) { - + // body } // currently blocked by type checker @@ -76,6 +68,6 @@ def mut_subroutine_static(mutable array[int[8], 2, 3] arr_arg) { // Unimplemented extern extern_func(int); - +// End unimplemented statements. // NotSupported hardware qubit x $0; diff --git a/source/compiler/qsc_openqasm_compiler/src/tests/statement.rs b/source/compiler/qsc_openqasm_compiler/src/tests/statement.rs index ad8a8ca456a..7dcefe96c8e 100644 --- a/source/compiler/qsc_openqasm_compiler/src/tests/statement.rs +++ b/source/compiler/qsc_openqasm_compiler/src/tests/statement.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. mod annotation; +mod break_continue; mod const_eval; mod end; mod for_loop; diff --git a/source/compiler/qsc_openqasm_compiler/src/tests/statement/break_continue.rs b/source/compiler/qsc_openqasm_compiler/src/tests/statement/break_continue.rs new file mode 100644 index 00000000000..1577b6d7b66 --- /dev/null +++ b/source/compiler/qsc_openqasm_compiler/src/tests/statement/break_continue.rs @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::tests::compile_qasm_to_qsharp; +use expect_test::expect; +use miette::Report; + +fn compile_error_buffer(source: &str) -> String { + let Err(errors) = compile_qasm_to_qsharp(source) else { + panic!("Expected error"); + }; + errors + .iter() + .map(|e| format!("{e:?}")) + .collect::>() + .join("\n") +} + +#[test] +fn break_lowers_in_for_loop() -> miette::Result<(), Vec> { + let source = r#" + for int i in [0:2] { + break; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![[r#" + import Std.OpenQASM.Intrinsic.*; + for i : Int in 0..2 { + break; + } + "#]] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn continue_lowers_in_for_loop() -> miette::Result<(), Vec> { + let source = r#" + for int i in [0:2] { + continue; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![[r#" + import Std.OpenQASM.Intrinsic.*; + for i : Int in 0..2 { + continue; + } + "#]] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn break_lowers_in_while_loop() -> miette::Result<(), Vec> { + let source = r#" + int i = 0; + while (i < 5) { + i += 1; + break; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![[r#" + import Std.OpenQASM.Intrinsic.*; + mutable i = 0; + while i < 5 { + set i = i + 1; + break; + } + "#]] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn continue_lowers_in_while_loop() -> miette::Result<(), Vec> { + let source = r#" + int i = 0; + while (i < 5) { + i += 1; + continue; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![[r#" + import Std.OpenQASM.Intrinsic.*; + mutable i = 0; + while i < 5 { + set i = i + 1; + continue; + } + "#]] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn break_lowers_when_guarded_by_if_in_for_loop() -> miette::Result<(), Vec> { + let source = r#" + int sum = 0; + for int i in [0:10] { + if (i == 5) { + break; + } + sum += i; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![[r#" + import Std.OpenQASM.Intrinsic.*; + mutable sum = 0; + for i : Int in 0..10 { + if i == 5 { + break; + }; + set sum = sum + i; + } + "#]] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn continue_lowers_when_guarded_by_if_in_for_loop() -> miette::Result<(), Vec> { + let source = r#" + int sum = 0; + for int i in [0:10] { + if (i == 5) { + continue; + } + sum += i; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![[r#" + import Std.OpenQASM.Intrinsic.*; + mutable sum = 0; + for i : Int in 0..10 { + if i == 5 { + continue; + }; + set sum = sum + i; + } + "#]] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn nested_loop_control_targets_nearest_loop() -> miette::Result<(), Vec> { + let source = r#" + for int i in [0:2] { + while (true) { + break; + } + continue; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![[r#" + import Std.OpenQASM.Intrinsic.*; + for i : Int in 0..2 { + while true { + break; + } + continue; + } + "#]] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn break_in_switch_targets_enclosing_loop() -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.1; + for int i in [0:2] { + switch (i) { + case 1 { + break; + } + } + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![[r#" + import Std.OpenQASM.Intrinsic.*; + for i : Int in 0..2 { + if i == 1 { + break; + }; + } + "#]] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn break_in_switch_without_enclosing_loop_is_rejected() { + let source = r#" + OPENQASM 3.1; + int i = 1; + switch (i) { + case 1 { + break; + } + } + "#; + + let errors = compile_error_buffer(source); + assert!(errors.contains("Qasm.Lowerer.InvalidScope")); + assert!(errors.contains("break can only appear in loop scopes")); +} + +#[test] +fn break_outside_loop_is_rejected() { + let source = "break;"; + + expect![[r#" + Qdk.Qasm.Lowerer.InvalidScope + + x break can only appear in loop scopes + ,-[Test.qasm:1:1] + 1 | break; + : ^^^^^^ + `---- + "#]] + .assert_eq(&compile_error_buffer(source)); +} + +#[test] +fn continue_outside_loop_is_rejected() { + let source = "continue;"; + + expect![[r#" + Qdk.Qasm.Lowerer.InvalidScope + + x continue can only appear in loop scopes + ,-[Test.qasm:1:1] + 1 | continue; + : ^^^^^^^^^ + `---- + "#]] + .assert_eq(&compile_error_buffer(source)); +} + +#[test] +fn break_in_def_without_enclosing_loop_is_rejected() { + let source = r#" + def f() { + break; + } + "#; + + expect![[r#" + Qdk.Qasm.Lowerer.InvalidScope + + x break can only appear in loop scopes + ,-[Test.qasm:3:13] + 2 | def f() { + 3 | break; + : ^^^^^^ + 4 | } + `---- + "#]] + .assert_eq(&compile_error_buffer(source)); +} + +#[test] +fn continue_in_gate_without_enclosing_loop_is_rejected() { + let source = r#" + gate g q { + continue; + } + "#; + + expect![[r#" + Qdk.Qasm.Lowerer.InvalidScope + + x continue can only appear in loop scopes + ,-[Test.qasm:3:13] + 2 | gate g q { + 3 | continue; + : ^^^^^^^^^ + 4 | } + `---- + "#]] + .assert_eq(&compile_error_buffer(source)); +} diff --git a/source/compiler/qsc_parse/src/completion/word_kinds.rs b/source/compiler/qsc_parse/src/completion/word_kinds.rs index 1d04a59220a..a0e916be466 100644 --- a/source/compiler/qsc_parse/src/completion/word_kinds.rs +++ b/source/compiler/qsc_parse/src/completion/word_kinds.rs @@ -86,6 +86,8 @@ bitflags! { const Auto = keyword_bit(Keyword::Auto); const Body = keyword_bit(Keyword::Body); const Borrow = keyword_bit(Keyword::Borrow); + const Break = keyword_bit(Keyword::Break); + const Continue = keyword_bit(Keyword::Continue); const Controlled = keyword_bit(Keyword::Controlled); const ControlledUpper = keyword_bit(Keyword::ControlledUpper); const Ctl = keyword_bit(Keyword::Ctl); diff --git a/source/compiler/qsc_parse/src/expr.rs b/source/compiler/qsc_parse/src/expr.rs index 6dd3741b9c2..7eb9a8dbca0 100644 --- a/source/compiler/qsc_parse/src/expr.rs +++ b/source/compiler/qsc_parse/src/expr.rs @@ -229,6 +229,10 @@ fn expr_base(s: &mut ParserContext) -> Result> { Ok(Box::new(ExprKind::Repeat(body, cond, fixup))) } else if token(s, TokenKind::Keyword(Keyword::Return)).is_ok() { Ok(Box::new(ExprKind::Return(expr(s)?))) + } else if token(s, TokenKind::Keyword(Keyword::Break)).is_ok() { + Ok(Box::new(ExprKind::Break)) + } else if token(s, TokenKind::Keyword(Keyword::Continue)).is_ok() { + Ok(Box::new(ExprKind::Continue)) } else if !s.contains_language_feature(LanguageFeatures::V2PreviewSyntax) && token(s, TokenKind::Keyword(Keyword::Set)).is_ok() { diff --git a/source/compiler/qsc_parse/src/expr/tests.rs b/source/compiler/qsc_parse/src/expr/tests.rs index 7786259ad3e..27989f6e407 100644 --- a/source/compiler/qsc_parse/src/expr/tests.rs +++ b/source/compiler/qsc_parse/src/expr/tests.rs @@ -689,6 +689,141 @@ fn return_expr() { ); } +#[test] +fn break_expr() { + check(expr, "break", &expect!["Expr _id_ [0-5]: Break"]); +} + +#[test] +fn continue_expr() { + check(expr, "continue", &expect!["Expr _id_ [0-8]: Continue"]); +} + +#[test] +fn break_in_block() { + check( + expr, + "{ break; }", + &expect![[r#" + Expr _id_ [0-10]: Expr Block: Block _id_ [0-10]: + Stmt _id_ [2-8]: Semi: Expr _id_ [2-7]: Break"#]], + ); +} + +#[test] +fn continue_in_block() { + check( + expr, + "{ continue; }", + &expect![[r#" + Expr _id_ [0-13]: Expr Block: Block _id_ [0-13]: + Stmt _id_ [2-11]: Semi: Expr _id_ [2-10]: Continue"#]], + ); +} + +#[test] +fn break_in_for() { + check( + expr, + "for i in xs { break; }", + &expect![[r#" + Expr _id_ [0-22]: For: + Pat _id_ [4-5]: Bind: + Ident _id_ [4-5] "i" + Expr _id_ [9-11]: Path: Path _id_ [9-11] (Ident _id_ [9-11] "xs") + Block _id_ [12-22]: + Stmt _id_ [14-20]: Semi: Expr _id_ [14-19]: Break"#]], + ); +} + +#[test] +fn continue_in_while() { + check( + expr, + "while c { continue; }", + &expect![[r#" + Expr _id_ [0-21]: While: + Expr _id_ [6-7]: Path: Path _id_ [6-7] (Ident _id_ [6-7] "c") + Block _id_ [8-21]: + Stmt _id_ [10-19]: Semi: Expr _id_ [10-18]: Continue"#]], + ); +} + +#[test] +fn break_in_if() { + check( + expr, + "if c { break; }", + &expect![[r#" + Expr _id_ [0-15]: If: + Expr _id_ [3-4]: Path: Path _id_ [3-4] (Ident _id_ [3-4] "c") + Block _id_ [5-15]: + Stmt _id_ [7-13]: Semi: Expr _id_ [7-12]: Break"#]], + ); +} + +// `break`/`continue` parse in operand position; whether the placement is legal +// is validated in a later compilation stage, not by the parser. +#[test] +fn break_in_operand_position() { + check( + expr, + "{ let x = break; }", + &expect![[r#" + Expr _id_ [0-18]: Expr Block: Block _id_ [0-18]: + Stmt _id_ [2-16]: Local (Immutable): + Pat _id_ [6-7]: Bind: + Ident _id_ [6-7] "x" + Expr _id_ [10-15]: Break"#]], + ); +} + +#[test] +fn break_missing_semi() { + check( + expr, + "{ break x }", + &expect![[r#" + Expr _id_ [0-11]: Expr Block: Block _id_ [0-11]: + Stmt _id_ [2-7]: Expr: Expr _id_ [2-7]: Break + Stmt _id_ [8-9]: Expr: Expr _id_ [8-9]: Path: Path _id_ [8-9] (Ident _id_ [8-9] "x") + + [ + Error( + MissingSemi( + Span { + lo: 7, + hi: 7, + }, + ), + ), + ]"#]], + ); +} + +#[test] +fn continue_missing_semi() { + check( + expr, + "{ continue x }", + &expect![[r#" + Expr _id_ [0-14]: Expr Block: Block _id_ [0-14]: + Stmt _id_ [2-10]: Expr: Expr _id_ [2-10]: Continue + Stmt _id_ [11-12]: Expr: Expr _id_ [11-12]: Path: Path _id_ [11-12] (Ident _id_ [11-12] "x") + + [ + Error( + MissingSemi( + Span { + lo: 10, + hi: 10, + }, + ), + ), + ]"#]], + ); +} + #[test] fn set() { check( diff --git a/source/compiler/qsc_parse/src/keyword.rs b/source/compiler/qsc_parse/src/keyword.rs index e87c0ef6a90..615a7df695e 100644 --- a/source/compiler/qsc_parse/src/keyword.rs +++ b/source/compiler/qsc_parse/src/keyword.rs @@ -18,6 +18,8 @@ pub enum Keyword { Auto, Body, Borrow, + Break, + Continue, Controlled, ControlledUpper, Ctl, @@ -77,6 +79,8 @@ impl Keyword { Self::Auto => "auto", Self::Body => "body", Self::Borrow => "borrow", + Self::Break => "break", + Self::Continue => "continue", Self::Controlled => "controlled", Self::ControlledUpper => "Controlled", Self::Ctl => "Ctl", @@ -189,10 +193,12 @@ impl FromStr for Keyword { "until" => Ok(Self::Until), "repeat" => Ok(Self::Repeat), "fixup" => Ok(Self::Fixup), - // The next two are new keywords and their + // The next four are new keywords and their // usage has yet to be measured. "new" => Ok(Self::New), "struct" => Ok(Self::Struct), + "break" => Ok(Self::Break), + "continue" => Ok(Self::Continue), // The next three were not found or measured // in the standard library for priority order. "PauliY" => Ok(Self::PauliY), diff --git a/source/compiler/qsc_parse/src/stmt/tests.rs b/source/compiler/qsc_parse/src/stmt/tests.rs index 2113ed6adbe..ff24cf1184c 100644 --- a/source/compiler/qsc_parse/src/stmt/tests.rs +++ b/source/compiler/qsc_parse/src/stmt/tests.rs @@ -992,3 +992,50 @@ fn recover_call_with_arg_missing_close_paren_in_block() { ]"#]], ); } + +// `break` and `continue` are reserved keywords, so they cannot be used as +// identifiers. This is an intentional breaking change for existing code that +// used these words as bindings. +#[test] +fn break_reserved_as_identifier() { + check( + parse, + "let break = 1;", + &expect![[r#" + Error( + Rule( + "pattern", + Keyword( + Break, + ), + Span { + lo: 4, + hi: 9, + }, + ), + ) + "#]], + ); +} + +#[test] +fn continue_reserved_as_identifier() { + check( + parse, + "let continue = 1;", + &expect![[r#" + Error( + Rule( + "pattern", + Keyword( + Continue, + ), + Span { + lo: 4, + hi: 12, + }, + ), + ) + "#]], + ); +} diff --git a/source/compiler/qsc_passes/Cargo.toml b/source/compiler/qsc_passes/Cargo.toml index 7a39dbfe68c..90b0bdba24e 100644 --- a/source/compiler/qsc_passes/Cargo.toml +++ b/source/compiler/qsc_passes/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [dependencies] miette = { workspace = true } +num-bigint = { workspace = true } qsc_data_structures = { path = "../qsc_data_structures" } qsc_eval = { path = "../qsc_eval" } qsc_fir = { path = "../qsc_fir" } @@ -25,6 +26,7 @@ expect-test = { workspace = true } indoc = { workspace = true } qsc = { path = "../qsc" } qsc_eval = { path = "../qsc_eval" } +qsc_formatter = { path = "../qsc_formatter" } [lints] workspace = true diff --git a/source/compiler/qsc_passes/src/capabilitiesck.rs b/source/compiler/qsc_passes/src/capabilitiesck.rs index 35f8904d237..3490f031eca 100644 --- a/source/compiler/qsc_passes/src/capabilitiesck.rs +++ b/source/compiler/qsc_passes/src/capabilitiesck.rs @@ -433,7 +433,7 @@ impl<'a> Checker<'a> { if let ExprKind::Var(Res::Local(local_var_id), _) = expr.kind { let maybe_ident = self.find_local_var_ident(local_var_id); if let Some(ident) = maybe_ident { - return ident.name.starts_with('@'); + return ident.name.starts_with('.'); } } diff --git a/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive.rs b/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive.rs index 46db384f74e..4d26442eabd 100644 --- a/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive.rs +++ b/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive.rs @@ -5,18 +5,20 @@ use super::tests_common::{ CALL_DYNAMIC_FUNCTION, CALL_DYNAMIC_OPERATION, CALL_TO_CYCLIC_FUNCTION_WITH_CLASSICAL_ARGUMENT, CALL_TO_CYCLIC_FUNCTION_WITH_DYNAMIC_ARGUMENT, CALL_TO_CYCLIC_OPERATION_WITH_CLASSICAL_ARGUMENT, - CALL_TO_CYCLIC_OPERATION_WITH_DYNAMIC_ARGUMENT, CALL_UNRESOLVED_FUNCTION, CUSTOM_MEASUREMENT, + CALL_TO_CYCLIC_OPERATION_WITH_DYNAMIC_ARGUMENT, CALL_UNRESOLVED_FUNCTION, + CLASSICAL_BREAK_IN_LOOP, CUSTOM_MEASUREMENT, CUSTOM_MEASUREMENT_WITH_SIMULATABLE_INTRINSIC_ATTR, CUSTOM_RESET, - CUSTOM_RESET_WITH_SIMULATABLE_INTRINSIC_ATTR, DYNAMIC_ARRAY_BINARY_OP, - LOOP_WITH_DYNAMIC_CONDITION, MEASUREMENT_WITHIN_DYNAMIC_SCOPE, MINIMAL, - RETURN_WITHIN_DYNAMIC_SCOPE, USE_CLOSURE_FUNCTION, USE_DYNAMIC_BIG_INT, USE_DYNAMIC_BOOLEAN, - USE_DYNAMIC_DOUBLE, USE_DYNAMIC_FUNCTION, USE_DYNAMIC_INDEX, USE_DYNAMIC_INT, - USE_DYNAMIC_LHS_EXP_BINOP, USE_DYNAMIC_OPERATION, USE_DYNAMIC_PAULI, USE_DYNAMIC_QUBIT, - USE_DYNAMIC_RANGE, USE_DYNAMIC_RHS_EXP_BINOP, USE_DYNAMIC_STRING, USE_DYNAMIC_UDT, - USE_DYNAMICALLY_SIZED_ARRAY, USE_ENTRY_POINT_INT_ARRAY_IN_TUPLE, + CUSTOM_RESET_WITH_SIMULATABLE_INTRINSIC_ATTR, DYNAMIC_ARRAY_BINARY_OP, DYNAMIC_BREAK_IN_LOOP, + HAND_WRITTEN_DYNAMIC_STOP_LOOP, LOOP_WITH_DYNAMIC_CONDITION, MEASUREMENT_WITHIN_DYNAMIC_SCOPE, + MINIMAL, RETURN_WITHIN_DYNAMIC_SCOPE, USE_CLOSURE_FUNCTION, USE_DYNAMIC_BIG_INT, + USE_DYNAMIC_BOOLEAN, USE_DYNAMIC_DOUBLE, USE_DYNAMIC_FUNCTION, USE_DYNAMIC_INDEX, + USE_DYNAMIC_INT, USE_DYNAMIC_LHS_EXP_BINOP, USE_DYNAMIC_OPERATION, USE_DYNAMIC_PAULI, + USE_DYNAMIC_QUBIT, USE_DYNAMIC_RANGE, USE_DYNAMIC_RHS_EXP_BINOP, USE_DYNAMIC_STRING, + USE_DYNAMIC_UDT, USE_DYNAMICALLY_SIZED_ARRAY, USE_ENTRY_POINT_INT_ARRAY_IN_TUPLE, USE_ENTRY_POINT_STATIC_BIG_INT, USE_ENTRY_POINT_STATIC_BOOL, USE_ENTRY_POINT_STATIC_DOUBLE, USE_ENTRY_POINT_STATIC_INT, USE_ENTRY_POINT_STATIC_INT_IN_TUPLE, USE_ENTRY_POINT_STATIC_PAULI, - USE_ENTRY_POINT_STATIC_RANGE, USE_ENTRY_POINT_STATIC_STRING, check, check_for_exe, + USE_ENTRY_POINT_STATIC_RANGE, USE_ENTRY_POINT_STATIC_STRING, capability_error_kinds, check, + check_for_exe, }; use expect_test::{Expect, expect}; use qsc_data_structures::target::TargetCapabilityFlags; @@ -137,8 +139,8 @@ fn use_of_dynamic_qubit_yields_errors() { [ UseOfDynamicQubit( Span { - lo: 146, - hi: 162, + lo: 142, + hi: 158, }, ), ] @@ -594,6 +596,76 @@ fn loop_with_dynamic_condition_yields_errors() { ); } +#[test] +fn dynamic_break_in_loop_yields_loop_with_dynamic_condition() { + // A measurement-dependent `break` desugars to a `while` whose condition is + // dynamic, so the existing `LoopWithDynamicCondition` analysis flags it, + // which requires `BackwardsBranching`, with no break-specific capability code. + check_profile( + DYNAMIC_BREAK_IN_LOOP, + &expect![[r#" + [ + LoopWithDynamicCondition( + Span { + lo: 96, + hi: 200, + }, + ), + ] + "#]], + ); +} + +#[test] +fn hand_written_dynamic_stop_loop_yields_loop_with_dynamic_condition() { + // The hand-written flag loop the desugar mirrors is flagged the same way. + check_profile( + HAND_WRITTEN_DYNAMIC_STOP_LOOP, + &expect![[r#" + [ + LoopWithDynamicCondition( + Span { + lo: 130, + hi: 244, + }, + ), + ] + "#]], + ); +} + +#[test] +fn dynamic_break_matches_hand_written_flag_loop_capability_kinds() { + // A measurement-dependent `break` is flagged with exactly the same + // capability-error kinds as the equivalent hand-written + // `mutable stop = false; while not stop { ... }` loop, so no new + // RCA runtime feature is introduced for `break`/`continue`. + let break_kinds = + capability_error_kinds(DYNAMIC_BREAK_IN_LOOP, TargetCapabilityFlags::Adaptive); + let hand_written_kinds = capability_error_kinds( + HAND_WRITTEN_DYNAMIC_STOP_LOOP, + TargetCapabilityFlags::Adaptive, + ); + assert_eq!(break_kinds, hand_written_kinds); + assert!( + break_kinds.iter().any(|k| k == "LoopWithDynamicCondition"), + "expected LoopWithDynamicCondition, got {break_kinds:?}" + ); +} + +#[test] +fn classical_break_in_loop_does_not_over_trigger() { + // A classically-conditioned `break` keeps the desugared loop condition + // static, so it must not trigger the dynamic-condition backwards-branching + // analysis. + check_profile( + CLASSICAL_BREAK_IN_LOOP, + &expect![[r#" + [] + "#]], + ); +} + #[test] fn use_closure_allowed() { check_profile( diff --git a/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers.rs b/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers.rs index c4b147f958e..64b82bb7a63 100644 --- a/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers.rs +++ b/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers.rs @@ -126,8 +126,8 @@ fn use_of_dynamic_qubit_yields_errors() { [ UseOfDynamicQubit( Span { - lo: 146, - hi: 162, + lo: 142, + hi: 158, }, ), ] diff --git a/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers_and_floats.rs b/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers_and_floats.rs index 3055440cfd5..95e793bb24d 100644 --- a/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers_and_floats.rs +++ b/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers_and_floats.rs @@ -123,8 +123,8 @@ fn use_of_dynamic_qubit_yields_errors() { [ UseOfDynamicQubit( Span { - lo: 146, - hi: 162, + lo: 142, + hi: 158, }, ), ] diff --git a/source/compiler/qsc_passes/src/capabilitiesck/tests_base.rs b/source/compiler/qsc_passes/src/capabilitiesck/tests_base.rs index 3267000f359..e7a64491285 100644 --- a/source/compiler/qsc_passes/src/capabilitiesck/tests_base.rs +++ b/source/compiler/qsc_passes/src/capabilitiesck/tests_base.rs @@ -171,8 +171,8 @@ fn use_of_dynamic_qubit_yields_errors() { ), UseOfDynamicQubit( Span { - lo: 146, - hi: 162, + lo: 142, + hi: 158, }, ), ] diff --git a/source/compiler/qsc_passes/src/capabilitiesck/tests_common.rs b/source/compiler/qsc_passes/src/capabilitiesck/tests_common.rs index 922a3501588..8cd015bf0f9 100644 --- a/source/compiler/qsc_passes/src/capabilitiesck/tests_common.rs +++ b/source/compiler/qsc_passes/src/capabilitiesck/tests_common.rs @@ -37,6 +37,33 @@ pub fn check_for_exe(source: &str, expect: &Expect, capabilities: TargetCapabili expect.assert_debug_eq(&errors); } +/// Returns the sorted capability-check error variant names, with spans dropped, +/// for `source`. Comparing the kind lists of two programs shows they are flagged +/// the same way regardless of their differing source spans. +pub fn capability_error_kinds(source: &str, capabilities: TargetCapabilityFlags) -> Vec { + let compilation_context = CompilationContext::new(source, capabilities); + let (package, compute_properties) = compilation_context.get_package_compute_properties_tuple(); + let errors = check_supported_capabilities( + package, + compute_properties, + capabilities, + &compilation_context.fir_store, + ); + let mut kinds: Vec = errors + .iter() + .map(|error| { + let debug = format!("{error:?}"); + debug + .split('(') + .next() + .unwrap_or(debug.as_str()) + .to_string() + }) + .collect(); + kinds.sort(); + kinds +} + fn lower_hir_package_store( lowerer: &mut Lowerer, hir_package_store: &HirPackageStore, @@ -176,7 +203,7 @@ pub const USE_DYNAMIC_QUBIT: &str = r#" operation Foo() : Unit { use control = Qubit(); if M(control) == Zero { - use q = Qubit(); + use q = Qubit(); } } }"#; @@ -441,6 +468,52 @@ pub const LOOP_WITH_DYNAMIC_CONDITION: &str = r#" } }"#; +// A measurement-dependent `break` makes the desugared `while` condition +// dynamic, so the existing `LoopWithDynamicCondition` -> `BackwardsBranching` +// analysis flags it with no break-specific capability machinery. Uses a `while` +// so the desugar is directly comparable to the hand-written flag loop below. +pub const DYNAMIC_BREAK_IN_LOOP: &str = r#" + namespace Test { + operation Foo() : Unit { + use q = Qubit(); + while true { + if M(q) == One { + break; + } + } + } + }"#; + +// The hand-written flag loop the desugar mirrors; it must be flagged the same +// way as `DYNAMIC_BREAK_IN_LOOP`. +pub const HAND_WRITTEN_DYNAMIC_STOP_LOOP: &str = r#" + namespace Test { + operation Foo() : Unit { + use q = Qubit(); + mutable stop = false; + while not stop { + if M(q) == One { + stop = true; + } + } + } + }"#; + +// A classically-conditioned `break` keeps the desugared `while` condition +// static, so it must not over-trigger the dynamic-condition analysis. +pub const CLASSICAL_BREAK_IN_LOOP: &str = r#" + namespace Test { + operation Foo() : Unit { + use q = Qubit(); + for i in 0..10 { + if i == 5 { + break; + } + X(q); + } + } + }"#; + pub const USE_CLOSURE_FUNCTION: &str = r#" namespace Test { import Std.Math.*; diff --git a/source/compiler/qsc_passes/src/common.rs b/source/compiler/qsc_passes/src/common.rs index dabd20c9a25..b6199b2f698 100644 --- a/source/compiler/qsc_passes/src/common.rs +++ b/source/compiler/qsc_passes/src/common.rs @@ -10,11 +10,12 @@ use qsc_hir::{ StmtKind, }, ty::{GenericArg, Prim, Ty}, + visit::{self, Visitor}, }; use std::rc::Rc; pub(crate) fn generated_name(name: &str) -> Rc { - Rc::from(format!("@{name}")) + Rc::from(format!(".{name}")) } pub(crate) fn gen_ident(assigner: &mut Assigner, label: &str, ty: Ty, span: Span) -> IdentTemplate { @@ -112,3 +113,81 @@ pub(crate) fn create_gen_core_ref( kind: ExprKind::Var(Res::Item(callable.id), generics), } } + +/// A loop-nesting walk shared by scanners that classify a `break`/`continue` by +/// whether it binds to the loop that owns the scanned region. +/// +/// The walk tracks a loop depth for post-placement scans. Loop bodies are +/// visited at one greater depth, so a `break`/`continue` reached at depth 0 +/// binds to the enclosing loop while a deeper one binds to a loop nested inside +/// the scanned region. A `for` iterable and a `while` condition are evaluated in +/// the enclosing scope and so are visited at the current depth. For `repeat`, +/// the `until` condition and `fixup` block are traversed at the repeat loop's +/// depth because [`LoopControl`](crate::loop_control::LoopControl) has already +/// rejected user `break`/`continue` expressions in those positions. +/// +/// Implementors keep their own loop-depth counter and result state; the trait +/// only re-homes the identical nesting traversal. Each implementor supplies the +/// depth bookkeeping, an early-out predicate, and the action taken for a +/// `break`/`continue`. +pub(crate) trait LoopDepthScan<'a>: Visitor<'a> { + /// Current loop nesting depth relative to the scanned region. + fn loop_depth(&self) -> u32; + + /// Increments the loop nesting depth when entering a loop body. + fn enter_loop(&mut self); + + /// Decrements the loop nesting depth when leaving a loop body. + fn exit_loop(&mut self); + + /// Returns `true` once enough has been found that further walking is moot. + fn is_done(&self) -> bool; + + /// Records a `break`/`continue`; `at_enclosing_loop` is `true` when it binds + /// to the loop that owns the scanned region, at loop depth 0. + fn record_break_continue(&mut self, expr: &Expr, at_enclosing_loop: bool); + + /// Walks `expr`, dispatching loop constructs with depth tracking and invoking + /// [`record_break_continue`](Self::record_break_continue) for each + /// `break`/`continue`. Non-loop expressions fall through to the default walk. + fn walk_loop_depth(&mut self, expr: &'a Expr) { + if self.is_done() { + return; + } + match &expr.kind { + ExprKind::Break | ExprKind::Continue => { + let at_enclosing_loop = self.loop_depth() == 0; + self.record_break_continue(expr, at_enclosing_loop); + } + // A `for` iterable is evaluated in the enclosing loop scope; the body + // introduces a new innermost loop. + ExprKind::For(_, iter, body) => { + self.visit_expr(iter); + self.enter_loop(); + self.visit_block(body); + self.exit_loop(); + } + // A `while` condition is re-evaluated in the enclosing scope; the + // body introduces a new innermost loop. + ExprKind::While(cond, body) => { + self.visit_expr(cond); + self.enter_loop(); + self.visit_block(body); + self.exit_loop(); + } + // The `repeat` body introduces the loop. The `until` condition and + // `fixup` block are traversed at that depth after placement + // validation, which rejects user `break`/`continue` there. + ExprKind::Repeat(body, until, fixup) => { + self.enter_loop(); + self.visit_block(body); + self.visit_expr(until); + if let Some(f) = fixup { + self.visit_block(f); + } + self.exit_loop(); + } + _ => visit::walk_expr(self, expr), + } + } +} diff --git a/source/compiler/qsc_passes/src/conjugate_invert.rs b/source/compiler/qsc_passes/src/conjugate_invert.rs index ca7b7c54f72..0e5f03ec109 100644 --- a/source/compiler/qsc_passes/src/conjugate_invert.rs +++ b/source/compiler/qsc_passes/src/conjugate_invert.rs @@ -45,6 +45,13 @@ pub enum Error { #[error("return expressions are not allowed in apply-blocks")] #[diagnostic(code("Qdk.Qsc.ConjugateInvert.ReturnForbidden"))] ReturnForbidden(#[label] Span), + + #[error("break and continue expressions cannot escape an apply-block")] + #[diagnostic(help( + "a break or continue in an apply-block must be contained in a loop within that apply-block; one that binds to a loop outside the conjugate expression cannot be reversed" + ))] + #[diagnostic(code("Qsc.ConjugateInvert.BreakContinueForbidden"))] + BreakContinueForbidden(#[label] Span), } /// Generates adjoint inverted blocks for within-blocks across all conjugate expressions, @@ -84,7 +91,10 @@ impl MutVisitor for ConjugateElim<'_> { assign_check.visit_block(&apply); self.errors.extend(assign_check.errors); - let mut return_check = ReturnCheck { errors: Vec::new() }; + let mut return_check = ReturnCheck { + errors: Vec::new(), + loop_depth: 0, + }; return_check.visit_block(&apply); self.errors.extend(return_check.errors); @@ -233,14 +243,49 @@ impl AssignmentCheck { struct ReturnCheck { errors: Vec, + loop_depth: u32, } impl<'a> Visitor<'a> for ReturnCheck { fn visit_expr(&mut self, expr: &'a Expr) { - if matches!(&expr.kind, ExprKind::Return(..)) { - self.errors.push(Error::ReturnForbidden(expr.span)); - } else { - visit::walk_expr(self, expr); + match &expr.kind { + ExprKind::Return(..) => { + self.errors.push(Error::ReturnForbidden(expr.span)); + } + // A break or continue that is not enclosed by a loop body within the + // apply-block binds to a loop outside the conjugate expression, so it + // would escape the generated adjoint of the within-block. One that is + // contained in a loop inside the apply-block is fine and desugars later. + ExprKind::Break | ExprKind::Continue if self.loop_depth == 0 => { + self.errors.push(Error::BreakContinueForbidden(expr.span)); + } + // A loop iterable or condition is evaluated in the enclosing context, so + // only a loop body increases the depth that keeps a break/continue + // contained within this apply-block. + ExprKind::For(_, iter, body) => { + self.visit_expr(iter); + self.visit_loop_body(body); + } + ExprKind::While(cond, body) => { + self.visit_expr(cond); + self.visit_loop_body(body); + } + ExprKind::Repeat(body, until, fixup) => { + self.visit_loop_body(body); + self.visit_expr(until); + if let Some(fixup) = fixup { + self.visit_block(fixup); + } + } + _ => visit::walk_expr(self, expr), } } } + +impl ReturnCheck { + fn visit_loop_body(&mut self, body: &Block) { + self.loop_depth += 1; + self.visit_block(body); + self.loop_depth -= 1; + } +} diff --git a/source/compiler/qsc_passes/src/conjugate_invert/tests.rs b/source/compiler/qsc_passes/src/conjugate_invert/tests.rs index adc80e38a3d..4841eb1e51b 100644 --- a/source/compiler/qsc_passes/src/conjugate_invert/tests.rs +++ b/source/compiler/qsc_passes/src/conjugate_invert/tests.rs @@ -86,7 +86,7 @@ fn conjugate_invert() { Expr 20 [133-134] [Type (Int => Unit is Adj)]: Var: Item 1 (Package 1) Expr 21 [135-136] [Type Int]: Lit: Int(2) Stmt 44 [0-0]: Local (Immutable): - Pat 45 [0-0] [Type Unit]: Bind: Ident 43 [0-0] "@apply_res" + Pat 45 [0-0] [Type Unit]: Bind: Ident 43 [0-0] ".apply_res" Expr 46 [0-0] [Type Unit]: Expr Block: Block 22 [163-210] [Type Unit]: Stmt 23 [177-182]: Semi: Expr 24 [177-181] [Type Unit]: Call: Expr 25 [177-178] [Type (Int => Unit is Adj)]: Var: Item 1 (Package 1) @@ -166,7 +166,7 @@ fn conjugate_invert_with_output() { Expr 22 [142-143] [Type (Int => Unit is Adj)]: Var: Item 1 (Package 1) Expr 23 [144-145] [Type Int]: Lit: Int(2) Stmt 50 [0-0]: Local (Immutable): - Pat 51 [0-0] [Type Int]: Bind: Ident 49 [0-0] "@apply_res" + Pat 51 [0-0] [Type Int]: Bind: Ident 49 [0-0] ".apply_res" Expr 52 [0-0] [Type Int]: Expr Block: Block 24 [172-233] [Type Int]: Stmt 25 [186-191]: Semi: Expr 26 [186-190] [Type Unit]: Call: Expr 27 [186-187] [Type (Int => Unit is Adj)]: Var: Item 1 (Package 1) @@ -256,7 +256,7 @@ fn nested_conjugate_invert() { Expr 27 [180-181] [Type (Int => Unit is Adj)]: Var: Item 1 (Package 1) Expr 28 [182-183] [Type Int]: Lit: Int(2) Stmt 100 [0-0]: Local (Immutable): - Pat 101 [0-0] [Type Unit]: Bind: Ident 99 [0-0] "@apply_res" + Pat 101 [0-0] [Type Unit]: Bind: Ident 99 [0-0] ".apply_res" Expr 102 [0-0] [Type Unit]: Expr Block: Block 29 [218-277] [Type Unit]: Stmt 30 [236-241]: Semi: Expr 31 [236-240] [Type Unit]: Call: Expr 32 [236-237] [Type (Int => Unit is Adj)]: Var: Item 1 (Package 1) @@ -275,7 +275,7 @@ fn nested_conjugate_invert() { Expr 98 [160-161] [Type Int]: Lit: Int(1) Stmt 108 [0-0]: Expr: Expr 109 [0-0] [Type Unit]: Var: Local 99 Stmt 77 [0-0]: Local (Immutable): - Pat 78 [0-0] [Type Unit]: Bind: Ident 76 [0-0] "@apply_res" + Pat 78 [0-0] [Type Unit]: Bind: Ident 76 [0-0] ".apply_res" Expr 79 [0-0] [Type Unit]: Expr Block: Block 38 [302-349] [Type Unit]: Stmt 39 [316-321]: Semi: Expr 40 [316-320] [Type Unit]: Call: Expr 41 [316-317] [Type (Int => Unit is Adj)]: Var: Item 1 (Package 1) @@ -293,7 +293,7 @@ fn nested_conjugate_invert() { Expr 58 [180-181] [Type (Int => Unit is Adj)]: Var: Item 1 (Package 1) Expr 59 [182-183] [Type Int]: Lit: Int(2) Stmt 123 [0-0]: Local (Immutable): - Pat 124 [0-0] [Type Unit]: Bind: Ident 122 [0-0] "@apply_res" + Pat 124 [0-0] [Type Unit]: Bind: Ident 122 [0-0] ".apply_res" Expr 125 [0-0] [Type Unit]: Expr Block: Block 60 [218-277] [Type Unit]: Stmt 61 [258-263]: Semi: Expr 62 [258-262] [Type Unit]: Call: Expr 63 [258-259] [Type (Int => Unit is Adj)]: UnOp (Functor Adj): @@ -484,7 +484,7 @@ fn conjugate_allowed_mutable_with_invalid_lhs_ignored() { Expr 20 [138-139] [Type (Int => Unit is Adj)]: Var: Item 1 (Package 1) Expr 21 [140-141] [Type Int]: Lit: Int(1) Stmt 41 [0-0]: Local (Immutable): - Pat 42 [0-0] [Type Unit]: Bind: Ident 40 [0-0] "@apply_res" + Pat 42 [0-0] [Type Unit]: Bind: Ident 40 [0-0] ".apply_res" Expr 43 [0-0] [Type Unit]: Expr Block: Block 22 [168-226] [Type Unit]: Stmt 23 [182-198]: Semi: Expr 24 [182-197] [Type Unit]: Assign: Expr 25 [187-192] [Type Int]: BinOp (Add): @@ -570,6 +570,139 @@ fn conjugate_return_nested_in_apply_fail() { ); } +#[test] +fn conjugate_break_in_apply_crossing_boundary_fail() { + check( + indoc! {" + namespace Test { + operation B(i : Int) : Unit is Adj {} + operation A() : Unit { + for i in 0..1 { + within { + B(1); + } + apply { + break; + } + } + } + } + "}, + &expect![[r#" + [ + BreakContinueForbidden( + Span { + lo: 203, + hi: 208, + }, + ), + ] + "#]], + ); +} + +#[test] +fn conjugate_continue_in_apply_crossing_boundary_fail() { + check( + indoc! {" + namespace Test { + operation B(i : Int) : Unit is Adj {} + operation A() : Unit { + for i in 0..1 { + within { + B(1); + } + apply { + continue; + } + } + } + } + "}, + &expect![[r#" + [ + BreakContinueForbidden( + Span { + lo: 203, + hi: 211, + }, + ), + ] + "#]], + ); +} + +#[test] +fn conjugate_break_contained_in_apply_loop_succeeds() { + check( + indoc! {" + namespace Test { + operation B(i : Int) : Unit is Adj {} + operation A() : Unit { + within { + B(1); + } + apply { + for i in 0..1 { + break; + } + } + } + } + "}, + &expect![[r#" + Package: + Item 0 [0-229] (Public): + Namespace (Ident 29 [10-14] "Test"): Item 1, Item 2 + Item 1 [21-58] (Internal): + Parent: 0 + Callable 0 [21-58] (operation): + name: Ident 1 [31-32] "B" + input: Pat 2 [33-40] [Type Int]: Bind: Ident 3 [33-34] "i" + output: Unit + functors: Adj + body: SpecDecl 4 [21-58]: Impl: + Block 5 [56-58]: + adj: + ctl: + ctl-adj: + Item 2 [63-227] (Internal): + Parent: 0 + Callable 6 [63-227] (operation): + name: Ident 7 [73-74] "A" + input: Pat 8 [74-76] [Type Unit]: Unit + output: Unit + functors: empty set + body: SpecDecl 9 [63-227]: Impl: + Block 10 [84-227] [Type Unit]: + Stmt 11 [94-221]: Expr: Expr 47 [0-0] [Type Unit]: Expr Block: Block 40 [0-0] [Type Unit]: + Stmt 41 [0-0]: Expr: Expr 42 [0-0] [Type Unit]: Expr Block: Block 13 [101-130] [Type Unit]: + Stmt 14 [115-120]: Semi: Expr 15 [115-119] [Type Unit]: Call: + Expr 16 [115-116] [Type (Int => Unit is Adj)]: Var: Item 1 (Package 1) + Expr 17 [117-118] [Type Int]: Lit: Int(1) + Stmt 37 [0-0]: Local (Immutable): + Pat 38 [0-0] [Type Unit]: Bind: Ident 36 [0-0] ".apply_res" + Expr 39 [0-0] [Type Unit]: Expr Block: Block 18 [145-221] [Type Unit]: + Stmt 19 [159-211]: Expr: Expr 20 [159-211] [Type Unit]: For: + Pat 21 [163-164] [Type Int]: Bind: Ident 22 [163-164] "i" + Expr 23 [168-172] [Type Range]: Range: + Expr 24 [168-169] [Type Int]: Lit: Int(0) + + Expr 25 [171-172] [Type Int]: Lit: Int(1) + Block 26 [173-211] [Type Unit]: + Stmt 27 [191-197]: Semi: Expr 28 [191-196] [Type Unit]: Break + Stmt 43 [0-0]: Expr: Expr 44 [0-0] [Type Unit]: Expr Block: Block 30 [101-130] [Type Unit]: + Stmt 31 [115-120]: Semi: Expr 32 [115-119] [Type Unit]: Call: + Expr 33 [115-116] [Type (Int => Unit is Adj)]: UnOp (Functor Adj): + Expr 34 [115-116] [Type (Int => Unit is Adj)]: Var: Item 1 (Package 1) + Expr 35 [117-118] [Type Int]: Lit: Int(1) + Stmt 45 [0-0]: Expr: Expr 46 [0-0] [Type Unit]: Var: Local 36 + adj: + ctl: + ctl-adj: "#]], + ); +} + #[test] fn conjugate_mutable_correct_use_succeeds() { check( @@ -636,7 +769,7 @@ fn conjugate_mutable_correct_use_succeeds() { Pat 31 [201-202] [Type Int]: Bind: Ident 32 [201-202] "y" Expr 33 [205-206] [Type Int]: Var: Local 20 Stmt 72 [0-0]: Local (Immutable): - Pat 73 [0-0] [Type Unit]: Bind: Ident 71 [0-0] "@apply_res" + Pat 73 [0-0] [Type Unit]: Bind: Ident 71 [0-0] ".apply_res" Expr 74 [0-0] [Type Unit]: Expr Block: Block 34 [232-329] [Type Unit]: Stmt 35 [246-260]: Local (Mutable): Pat 36 [254-255] [Type Int]: Bind: Ident 37 [254-255] "b" diff --git a/source/compiler/qsc_passes/src/index_assignment/tests.rs b/source/compiler/qsc_passes/src/index_assignment/tests.rs index 5683a2fe032..1f523b1749e 100644 --- a/source/compiler/qsc_passes/src/index_assignment/tests.rs +++ b/source/compiler/qsc_passes/src/index_assignment/tests.rs @@ -61,7 +61,7 @@ fn convert_array_assign() { Expr 10 [48-49] [Type Int]: Lit: Int(1) Stmt 11 [56-67]: Semi: Expr 25 [56-66] [Type Unit]: Expr Block: Block 26 [56-66] [Type Unit]: Stmt 19 [0-0]: Local (Immutable): - Pat 20 [60-61] [Type Int]: Bind: Ident 18 [60-61] "@index_18" + Pat 20 [60-61] [Type Int]: Bind: Ident 18 [60-61] ".index_18" Expr 15 [60-61] [Type Int]: Lit: Int(0) Stmt 24 [0-0]: Expr: Expr 22 [0-0] [Type Unit]: AssignIndex: Expr 21 [56-59] [Type Int[]]: Var: Local 7 @@ -106,10 +106,10 @@ fn convert_2d_array_assign() { Expr 14 [57-58] [Type Int]: Lit: Int(3) Stmt 15 [66-80]: Semi: Expr 39 [66-79] [Type Unit]: Expr Block: Block 40 [66-79] [Type Unit]: Stmt 25 [0-0]: Local (Immutable): - Pat 26 [70-71] [Type Int]: Bind: Ident 24 [70-71] "@index_24" + Pat 26 [70-71] [Type Int]: Bind: Ident 24 [70-71] ".index_24" Expr 20 [70-71] [Type Int]: Lit: Int(0) Stmt 28 [0-0]: Local (Immutable): - Pat 29 [73-74] [Type Int]: Bind: Ident 27 [73-74] "@index_27" + Pat 29 [73-74] [Type Int]: Bind: Ident 27 [73-74] ".index_27" Expr 21 [73-74] [Type Int]: Lit: Int(1) Stmt 38 [0-0]: Expr: Expr 36 [0-0] [Type Unit]: AssignIndex: Expr 35 [66-69] [Type Int[][]]: Var: Local 7 @@ -167,13 +167,13 @@ fn convert_3d_array_assign() { Expr 22 [104-109] [Type Int]: Lit: Int(7) Stmt 23 [118-140]: Semi: Expr 59 [118-139] [Type Unit]: Expr Block: Block 60 [118-139] [Type Unit]: Stmt 35 [0-0]: Local (Immutable): - Pat 36 [122-123] [Type Int]: Bind: Ident 34 [122-123] "@index_34" + Pat 36 [122-123] [Type Int]: Bind: Ident 34 [122-123] ".index_34" Expr 29 [122-123] [Type Int]: Lit: Int(0) Stmt 38 [0-0]: Local (Immutable): - Pat 39 [125-126] [Type Int]: Bind: Ident 37 [125-126] "@index_37" + Pat 39 [125-126] [Type Int]: Bind: Ident 37 [125-126] ".index_37" Expr 30 [125-126] [Type Int]: Lit: Int(1) Stmt 41 [0-0]: Local (Immutable): - Pat 42 [128-129] [Type Int]: Bind: Ident 40 [128-129] "@index_40" + Pat 42 [128-129] [Type Int]: Bind: Ident 40 [128-129] ".index_40" Expr 31 [128-129] [Type Int]: Lit: Int(1) Stmt 58 [0-0]: Expr: Expr 56 [0-0] [Type Unit]: AssignIndex: Expr 55 [118-121] [Type Int[][][]]: Var: Local 7 @@ -228,7 +228,7 @@ fn convert_array_assign_range() { Expr 12 [54-55] [Type Int]: Lit: Int(3) Stmt 13 [62-84]: Semi: Expr 32 [62-83] [Type Unit]: Expr Block: Block 33 [62-83] [Type Unit]: Stmt 26 [0-0]: Local (Immutable): - Pat 27 [66-70] [Type Range]: Bind: Ident 25 [66-70] "@index_25" + Pat 27 [66-70] [Type Range]: Bind: Ident 25 [66-70] ".index_25" Expr 17 [66-70] [Type Range]: Range: Expr 18 [66-67] [Type Int]: Lit: Int(1) @@ -277,13 +277,13 @@ fn convert_array_assign_single_with_range() { Expr 12 [54-55] [Type Int]: Lit: Int(3) Stmt 13 [62-79]: Semi: Expr 39 [62-78] [Type Unit]: Expr Block: Block 40 [62-78] [Type Unit]: Stmt 25 [0-0]: Local (Immutable): - Pat 26 [66-70] [Type Range]: Bind: Ident 24 [66-70] "@index_24" + Pat 26 [66-70] [Type Range]: Bind: Ident 24 [66-70] ".index_24" Expr 18 [66-70] [Type Range]: Range: Expr 19 [66-67] [Type Int]: Lit: Int(1) Expr 20 [69-70] [Type Int]: Lit: Int(3) Stmt 28 [0-0]: Local (Immutable): - Pat 29 [72-73] [Type Int]: Bind: Ident 27 [72-73] "@index_27" + Pat 29 [72-73] [Type Int]: Bind: Ident 27 [72-73] ".index_27" Expr 21 [72-73] [Type Int]: Lit: Int(1) Stmt 38 [0-0]: Expr: Expr 36 [0-0] [Type Unit]: AssignIndex: Expr 35 [62-65] [Type Int[]]: Var: Local 7 @@ -329,7 +329,7 @@ fn convert_array_assign_op() { Expr 10 [48-49] [Type Int]: Lit: Int(1) Stmt 11 [56-68]: Semi: Expr 29 [56-67] [Type Unit]: Expr Block: Block 30 [56-67] [Type Unit]: Stmt 19 [0-0]: Local (Immutable): - Pat 20 [60-61] [Type Int]: Bind: Ident 18 [60-61] "@index_18" + Pat 20 [60-61] [Type Int]: Bind: Ident 18 [60-61] ".index_18" Expr 15 [60-61] [Type Int]: Lit: Int(0) Stmt 28 [0-0]: Expr: Expr 26 [0-0] [Type Unit]: AssignIndex: Expr 25 [56-59] [Type Int[]]: Var: Local 7 @@ -378,10 +378,10 @@ fn convert_2d_array_assign_op() { Expr 14 [57-58] [Type Int]: Lit: Int(3) Stmt 15 [66-81]: Semi: Expr 45 [66-80] [Type Unit]: Expr Block: Block 46 [66-80] [Type Unit]: Stmt 25 [0-0]: Local (Immutable): - Pat 26 [70-71] [Type Int]: Bind: Ident 24 [70-71] "@index_24" + Pat 26 [70-71] [Type Int]: Bind: Ident 24 [70-71] ".index_24" Expr 20 [70-71] [Type Int]: Lit: Int(0) Stmt 28 [0-0]: Local (Immutable): - Pat 29 [73-74] [Type Int]: Bind: Ident 27 [73-74] "@index_27" + Pat 29 [73-74] [Type Int]: Bind: Ident 27 [73-74] ".index_27" Expr 21 [73-74] [Type Int]: Lit: Int(1) Stmt 44 [0-0]: Expr: Expr 42 [0-0] [Type Unit]: AssignIndex: Expr 41 [66-69] [Type Int[][]]: Var: Local 7 diff --git a/source/compiler/qsc_passes/src/lib.rs b/source/compiler/qsc_passes/src/lib.rs index 04739e0f5c8..74c6eb13bcc 100644 --- a/source/compiler/qsc_passes/src/lib.rs +++ b/source/compiler/qsc_passes/src/lib.rs @@ -12,9 +12,13 @@ mod id_update; mod index_assignment; mod invert_block; mod logic_sep; +mod loop_control; +mod loop_normalize; mod loop_unification; mod measurement; mod noise_intrinsic; +#[cfg(test)] +pub(crate) mod qsharp_gen; mod replace_qubit_allocation; mod reset; mod spec_gen; @@ -27,6 +31,7 @@ use capabilitiesck::{ }; use entry_point::generate_entry_expr; use index_assignment::ConvertToWSlash; +use loop_control::LoopControl; use loop_unification::LoopUni; use miette::Diagnostic; use qsc_data_structures::target::TargetCapabilityFlags; @@ -61,6 +66,9 @@ pub enum Error { ConfigInline(config_inline::Error), ConjInvert(conjugate_invert::Error), EntryPoint(entry_point::Error), + LoopControl(loop_control::Error), + LoopNormalize(loop_normalize::Error), + LoopUnification(loop_unification::Error), Measurement(measurement::Error), NoiseIntrinsic(noise_intrinsic::Error), Reset(reset::Error), @@ -121,6 +129,13 @@ impl PassContext { call_limits.visit_package(package); let callable_errors = call_limits.errors; + // Validate break/continue placement before loop_unification desugars them, + // while they are still first-class HIR nodes and before spec_gen duplicates + // any bodies into generated specializations. + let mut loop_control = LoopControl::default(); + loop_control.visit_package(package); + let loop_control_errors = loop_control.errors; + ConvertToWSlash { assigner }.visit_package(package); Validator::default().visit_package(package); @@ -144,9 +159,39 @@ impl PassContext { let entry_point_errors = generate_entry_expr(package, assigner, package_type); Validator::default().visit_package(package); - LoopUni { core, assigner }.visit_package(package); + // Hoist operand-position break/continue to statement position so the + // loop_unification desugar can rewrite them in place. + let loop_normalize_errors = { + let mut normalize = loop_normalize::LoopNormalize::new(assigner); + normalize.visit_package(package); + normalize.errors + }; + Validator::default().visit_package(package); + + let loop_uni_errors = { + let mut loop_uni = LoopUni { + core, + assigner, + errors: Vec::new(), + }; + loop_uni.visit_package(package); + loop_uni.errors + }; Validator::default().visit_package(package); + // Defense-in-depth: after loop_unification, no raw `break`/`continue` + // node should remain. `ResidualBreakContinue` is an internal invariant + // check, so when `loop_control` already reported a misplaced + // `break`/`continue`, the surviving raw node is the expected consequence + // of that user error and `loop_control`'s diagnostic is the correct + // user-facing one — only surface the invariant when `loop_control` found + // no misplaced `break`/`continue`. + let residual_break_continue_errors = if loop_control_errors.is_empty() { + loop_unification::check_no_break_continue(package) + } else { + Vec::new() + }; + ReplaceQubitAllocation::new(core, assigner).visit_package(package); Validator::default().visit_package(package); @@ -156,6 +201,7 @@ impl PassContext { callable_errors .into_iter() .map(Error::CallableLimits) + .chain(loop_control_errors.into_iter().map(Error::LoopControl)) .chain(borrow_errors.drain(..).map(Error::BorrowCk)) .chain(spec_errors.into_iter().map(Error::SpecGen)) .chain(conjugate_errors.into_iter().map(Error::ConjInvert)) @@ -168,6 +214,13 @@ impl PassContext { .into_iter() .map(Error::NoiseIntrinsic), ) + .chain(loop_normalize_errors.into_iter().map(Error::LoopNormalize)) + .chain(loop_uni_errors.into_iter().map(Error::LoopUnification)) + .chain( + residual_break_continue_errors + .into_iter() + .map(Error::LoopUnification), + ) .chain(test_attribute_errors.into_iter().map(Error::TestAttribute)) .collect() } @@ -201,17 +254,45 @@ pub fn run_core_passes(core: &mut CompileUnit) -> Vec { let borrow_errors = borrow_check.errors; let table = global::iter_package(PackageId::CORE, &core.package).collect(); - LoopUni { - core: &table, - assigner: &mut core.assigner, - } - .visit_package(&mut core.package); + + // Hoist operand-position break/continue to statement position before the + // loop_unification desugar, matching the default pass pipeline. + let loop_normalize_errors = { + let mut normalize = loop_normalize::LoopNormalize::new(&mut core.assigner); + normalize.visit_package(&mut core.package); + normalize.errors + }; + Validator::default().visit_package(&core.package); + + let loop_uni_errors = { + let mut loop_uni = LoopUni { + core: &table, + assigner: &mut core.assigner, + errors: Vec::new(), + }; + loop_uni.visit_package(&mut core.package); + loop_uni.errors + }; Validator::default().visit_package(&core.package); + // The core pipeline has no `loop_control` pass, so this residual-node check + // is the sole guard that no raw `break`/`continue` survived desugaring. + let residual_break_continue_errors = loop_unification::check_no_break_continue(&core.package); + ReplaceQubitAllocation::new(&table, &mut core.assigner).visit_package(&mut core.package); Validator::default().visit_package(&core.package); - borrow_errors.into_iter().map(Error::BorrowCk).collect() + borrow_errors + .into_iter() + .map(Error::BorrowCk) + .chain(loop_normalize_errors.into_iter().map(Error::LoopNormalize)) + .chain(loop_uni_errors.into_iter().map(Error::LoopUnification)) + .chain( + residual_break_continue_errors + .into_iter() + .map(Error::LoopUnification), + ) + .collect() } pub fn run_rca( diff --git a/source/compiler/qsc_passes/src/logic_sep.rs b/source/compiler/qsc_passes/src/logic_sep.rs index fdc2333abc7..ab7c721a898 100644 --- a/source/compiler/qsc_passes/src/logic_sep.rs +++ b/source/compiler/qsc_passes/src/logic_sep.rs @@ -18,7 +18,7 @@ use thiserror::Error; pub enum Error { #[error("cannot generate adjoint with this expression")] #[diagnostic(help( - "assignments, repeat-loops, while-loops, and returns cannot be used in blocks that require generated adjoint" + "assignments, repeat-loops, while-loops, returns, and break/continue expressions cannot be used in blocks that require generated adjoint" ))] #[diagnostic(code("Qdk.Qsc.LogicSeparation.ExprFobidden"))] ExprForbidden(#[label] Span), @@ -104,10 +104,25 @@ impl<'a> Visitor<'a> for SepCheck { } fn visit_expr(&mut self, expr: &'a Expr) { - if let ExprKind::Call(callee, _) = &expr.kind - && matches!(&callee.ty, Ty::Arrow(arrow) if arrow.kind == CallableKind::Operation) - { - self.errors.push(Error::OpCallForbidden(expr.span)); + // `visit_expr` is reached for expressions in operand positions: call + // arguments, `if`/`for`/`while` conditions, array and tuple elements, + // binary-operator sides, and so on. Statement-position control flow is + // rejected in `handle_expr`, but forbidden control flow can also be + // buried inside an operand and would otherwise slip past this + // separability check. Adjoint generation reverses a separable block, and + // `break`/`continue`/`return` cannot be reversed, so accepting one in an + // operand could turn an `is Adj` body into a wrong adjoint. Reject it + // here as well. This closes a pre-existing gap that affected `return` + // just as much as `break`/`continue`. + match &expr.kind { + ExprKind::Call(callee, _) if matches!(&callee.ty, Ty::Arrow(arrow) if arrow.kind == CallableKind::Operation) => + { + self.errors.push(Error::OpCallForbidden(expr.span)); + } + ExprKind::Break | ExprKind::Continue | ExprKind::Return(_) => { + self.errors.push(Error::ExprForbidden(expr.span)); + } + _ => {} } walk_expr(self, expr); } @@ -207,6 +222,8 @@ impl SepCheck { | ExprKind::AssignOp(..) | ExprKind::AssignField(..) | ExprKind::AssignIndex(..) + | ExprKind::Break + | ExprKind::Continue | ExprKind::Repeat(..) | ExprKind::Return(..) | ExprKind::While(..) => { diff --git a/source/compiler/qsc_passes/src/logic_sep/tests.rs b/source/compiler/qsc_passes/src/logic_sep/tests.rs index 1d518f57a2e..30fe0763a13 100644 --- a/source/compiler/qsc_passes/src/logic_sep/tests.rs +++ b/source/compiler/qsc_passes/src/logic_sep/tests.rs @@ -477,3 +477,128 @@ fn return_forbidden() { "#]], ); } + +#[test] +fn break_in_loop_forbidden() { + check( + "{for i in 0..1 {break;}}", + &expect![[r#" + [ + ExprForbidden( + Span { + lo: 16, + hi: 21, + }, + ), + ] + "#]], + ); +} + +#[test] +fn continue_in_loop_forbidden() { + check( + "{for i in 0..1 {continue;}}", + &expect![[r#" + [ + ExprForbidden( + Span { + lo: 16, + hi: 24, + }, + ), + ] + "#]], + ); +} + +#[test] +fn break_nested_in_if_forbidden() { + check( + "{for i in 0..1 {if true {break;}}}", + &expect![[r#" + [ + ExprForbidden( + Span { + lo: 25, + hi: 30, + }, + ), + ] + "#]], + ); +} + +// Control flow buried in an operand position, such as a call argument or an +// `if` condition, must be rejected just like statement-position control flow. +// Otherwise it would slip past separability checking and let an `is Adj` body be +// reversed into a wrong adjoint. Statement-position occurrences already produce +// exactly one `ExprForbidden` via `handle_expr`; the following tests confirm +// operand-position occurrences are also rejected, and not double-reported. +#[test] +fn operand_break_in_call_arg_forbidden() { + check( + "{use q = Qubit(); for i in 0..1 {X(if true {break} else {q});}}", + &expect![[r#" + [ + ExprForbidden( + Span { + lo: 44, + hi: 49, + }, + ), + ] + "#]], + ); +} + +#[test] +fn operand_continue_in_call_arg_forbidden() { + check( + "{use q = Qubit(); for i in 0..1 {X(if true {continue} else {q});}}", + &expect![[r#" + [ + ExprForbidden( + Span { + lo: 44, + hi: 52, + }, + ), + ] + "#]], + ); +} + +#[test] +fn operand_break_in_if_cond_forbidden() { + check( + "{for i in 0..1 {if (if true {break} else {false}) {}}}", + &expect![[r#" + [ + ExprForbidden( + Span { + lo: 29, + hi: 34, + }, + ), + ] + "#]], + ); +} + +#[test] +fn operand_return_in_call_arg_forbidden() { + check( + "{use q = Qubit(); X(if true {return ()} else {q});}", + &expect![[r#" + [ + ExprForbidden( + Span { + lo: 29, + hi: 38, + }, + ), + ] + "#]], + ); +} diff --git a/source/compiler/qsc_passes/src/loop_control.rs b/source/compiler/qsc_passes/src/loop_control.rs new file mode 100644 index 00000000000..8e761813a0d --- /dev/null +++ b/source/compiler/qsc_passes/src/loop_control.rs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[cfg(test)] +mod tests; + +use miette::Diagnostic; +use qsc_data_structures::span::Span; +use qsc_hir::{ + hir::{Block, Expr, ExprKind}, + visit::{self, Visitor}, +}; +use thiserror::Error; + +#[derive(Clone, Debug, Diagnostic, Error)] +pub enum Error { + #[error("break and continue expressions can only be used inside a loop")] + #[diagnostic(code("Qsc.LoopControl.BreakContinueOutsideLoop"))] + OutsideLoop(#[label] Span), + + #[error("break and continue expressions cannot be used in a loop condition")] + #[diagnostic(code("Qsc.LoopControl.BreakContinueInLoopHeader"))] + InLoopHeader(#[label] Span), + + #[error("break and continue expressions cannot be used in a repeat-loop fixup block")] + #[diagnostic(code("Qsc.LoopControl.BreakContinueInFixup"))] + InFixup(#[label] Span), +} + +/// A position where `break`/`continue` are forbidden even inside a loop, tracked +/// only while it is not separated from the current expression by a nested loop body. +#[derive(Clone, Copy)] +enum ForbiddenPosition { + /// A `while` condition or a `repeat`-loop `until` condition. + Condition, + /// A `repeat`-loop `fixup` block. + Fixup, +} + +/// Validates the placement of `break` and `continue` expressions. +/// +/// `break`/`continue` bind to the innermost enclosing loop body and are errors +/// outside any loop, in a loop condition, or in a `repeat`-loop fixup block. This +/// runs on HIR after lambda lifting, so each callable is walked with a fresh loop +/// depth of zero and needs no lambda special-casing: a `break`/`continue` in a +/// lambda body binds only to a loop within that same lambda. +#[derive(Default)] +pub(super) struct LoopControl { + pub(super) errors: Vec, + loop_depth: u32, + forbidden: Option, +} + +impl LoopControl { + fn control_flow_error(&self, span: Span) -> Option { + match self.forbidden { + Some(ForbiddenPosition::Condition) => Some(Error::InLoopHeader(span)), + Some(ForbiddenPosition::Fixup) => Some(Error::InFixup(span)), + None if self.loop_depth == 0 => Some(Error::OutsideLoop(span)), + None => None, + } + } + + fn visit_loop_body(&mut self, body: &Block) { + // A break/continue inside a loop body binds to this loop, so increase the + // depth and clear any forbidden position inherited from an enclosing header. + let saved = self.forbidden.take(); + self.loop_depth += 1; + self.visit_block(body); + self.loop_depth -= 1; + self.forbidden = saved; + } + + fn visit_condition(&mut self, cond: &Expr) { + let saved = self.forbidden.replace(ForbiddenPosition::Condition); + self.visit_expr(cond); + self.forbidden = saved; + } + + fn visit_fixup(&mut self, fixup: &Block) { + let saved = self.forbidden.replace(ForbiddenPosition::Fixup); + self.visit_block(fixup); + self.forbidden = saved; + } +} + +impl<'a> Visitor<'a> for LoopControl { + fn visit_expr(&mut self, expr: &'a Expr) { + match &expr.kind { + ExprKind::Break | ExprKind::Continue => { + if let Some(error) = self.control_flow_error(expr.span) { + self.errors.push(error); + } + } + ExprKind::For(_, iter, body) => { + // The iterable is evaluated once before iteration, so a break/continue + // there binds to the enclosing loop, not this one. + self.visit_expr(iter); + self.visit_loop_body(body); + } + ExprKind::While(cond, body) => { + self.visit_condition(cond); + self.visit_loop_body(body); + } + ExprKind::Repeat(body, until, fixup) => { + self.visit_loop_body(body); + self.visit_condition(until); + if let Some(fixup) = fixup { + self.visit_fixup(fixup); + } + } + _ => visit::walk_expr(self, expr), + } + } +} diff --git a/source/compiler/qsc_passes/src/loop_control/tests.rs b/source/compiler/qsc_passes/src/loop_control/tests.rs new file mode 100644 index 00000000000..93de3312916 --- /dev/null +++ b/source/compiler/qsc_passes/src/loop_control/tests.rs @@ -0,0 +1,519 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use expect_test::{Expect, expect}; +use indoc::indoc; +use qsc_data_structures::{ + language_features::LanguageFeatures, source::SourceMap, target::TargetCapabilityFlags, +}; +use qsc_frontend::compile::{self, PackageStore, compile}; +use qsc_hir::visit::Visitor; + +use crate::loop_control::LoopControl; + +fn check(file: &str, expect: &Expect) { + let store = PackageStore::new(compile::core()); + let sources = SourceMap::new([("test".into(), file.into())], None); + let unit = compile( + &store, + &[], + sources, + TargetCapabilityFlags::all(), + LanguageFeatures::default(), + ); + assert!(unit.errors.is_empty(), "{:?}", unit.errors); + + let mut loop_control = LoopControl::default(); + loop_control.visit_package(&unit.package); + let errors = loop_control.errors; + expect.assert_debug_eq(&errors); +} + +#[test] +fn break_outside_loop_errors() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + break; + } + } + "}, + &expect![[r#" + [ + OutsideLoop( + Span { + lo: 54, + hi: 59, + }, + ), + ] + "#]], + ); +} + +#[test] +fn continue_outside_loop_errors() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + continue; + } + } + "}, + &expect![[r#" + [ + OutsideLoop( + Span { + lo: 54, + hi: 62, + }, + ), + ] + "#]], + ); +} + +#[test] +fn break_in_for_body_is_allowed() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + for _ in 0..3 { + break; + } + } + } + "}, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn continue_in_for_body_is_allowed() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + for _ in 0..3 { + continue; + } + } + } + "}, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn break_in_while_body_is_allowed() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + while true { + break; + } + } + } + "}, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn continue_in_while_body_is_allowed() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + while true { + continue; + } + } + } + "}, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn break_in_repeat_body_is_allowed() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + repeat { + break; + } until true; + } + } + "}, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn continue_in_repeat_body_is_allowed() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + repeat { + continue; + } until true; + } + } + "}, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn break_in_while_condition_errors() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + while break { + } + } + } + "}, + &expect![[r#" + [ + InLoopHeader( + Span { + lo: 60, + hi: 65, + }, + ), + ] + "#]], + ); +} + +#[test] +fn continue_in_while_condition_errors() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + while continue { + } + } + } + "}, + &expect![[r#" + [ + InLoopHeader( + Span { + lo: 60, + hi: 68, + }, + ), + ] + "#]], + ); +} + +#[test] +fn break_in_repeat_until_condition_errors() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + repeat { + } until break; + } + } + "}, + &expect![[r#" + [ + InLoopHeader( + Span { + lo: 79, + hi: 84, + }, + ), + ] + "#]], + ); +} + +#[test] +fn break_in_fixup_errors() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + repeat { + } until true + fixup { + break; + } + } + } + "}, + &expect![[r#" + [ + InFixup( + Span { + lo: 112, + hi: 117, + }, + ), + ] + "#]], + ); +} + +#[test] +fn continue_in_fixup_errors() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + repeat { + } until true + fixup { + continue; + } + } + } + "}, + &expect![[r#" + [ + InFixup( + Span { + lo: 112, + hi: 120, + }, + ), + ] + "#]], + ); +} + +#[test] +fn nested_loops_inner_break_binds_to_inner_loop() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + for _ in 0..3 { + for _ in 0..3 { + break; + } + } + } + } + "}, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn nested_loops_inner_continue_binds_to_inner_loop() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + while true { + repeat { + continue; + } until true; + } + } + } + "}, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn break_in_for_iterable_with_enclosing_loop_is_allowed() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + for _ in 0..3 { + for _ in { break; 0..3 } { + } + } + } + } + "}, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn continue_in_for_iterable_binds_to_enclosing_loop() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + while true { + for _ in { continue; 0..3 } { + } + } + } + } + "}, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn break_in_for_iterable_without_enclosing_loop_errors() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + for _ in { break; 0..3 } { + } + } + } + "}, + &expect![[r#" + [ + OutsideLoop( + Span { + lo: 65, + hi: 70, + }, + ), + ] + "#]], + ); +} + +#[test] +fn break_in_nested_loop_body_within_while_condition_is_allowed() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + while { for _ in 0..3 { break; } true } { + } + } + } + "}, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn break_in_inner_while_condition_within_outer_loop_errors() { + // A `break` directly in an inner loop's condition is rejected even though an + // outer loop encloses it: a condition-position `break`/`continue` can never + // bind to an enclosing loop, so the forbidden-header position takes + // precedence over the enclosing loop depth. + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + for _ in 0..3 { + while break { + } + } + } + } + "}, + &expect![[r#" + [ + InLoopHeader( + Span { + lo: 88, + hi: 93, + }, + ), + ] + "#]], + ); +} + +#[test] +fn continue_in_inner_repeat_until_condition_within_outer_loop_errors() { + // A `continue` directly in an inner `repeat`-loop's `until` condition is + // rejected even though an outer loop encloses it, mirroring the `while` + // condition case: the forbidden-header position takes precedence over the + // enclosing loop depth. + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + while true { + repeat { + } until continue; + } + } + } + "}, + &expect![[r#" + [ + InLoopHeader( + Span { + lo: 108, + hi: 116, + }, + ), + ] + "#]], + ); +} + +#[test] +fn break_in_lambda_defined_in_loop_errors() { + check( + indoc! {" + namespace Test { + operation Foo() : Unit { + for _ in 0..3 { + let f = () => { + if true { + break; + } + }; + f(); + } + } + } + "}, + &expect![[r#" + [ + OutsideLoop( + Span { + lo: 144, + hi: 149, + }, + ), + ] + "#]], + ); +} diff --git a/source/compiler/qsc_passes/src/loop_normalize.rs b/source/compiler/qsc_passes/src/loop_normalize.rs new file mode 100644 index 00000000000..cdd0975800c --- /dev/null +++ b/source/compiler/qsc_passes/src/loop_normalize.rs @@ -0,0 +1,836 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Administrative-normal-form (ANF) operand lifting for loop control flow. +//! +//! A Q# block is an expression, so `break`/`continue` can sit behind a +//! statement-carrying `Block`/`If`/`While`/`For`/`Repeat` that itself feeds an +//! enclosing operator, call, or binding — an operand position the later +//! `loop_unification` desugar cannot rewrite in place. This pass lifts each such +//! operand to a fresh spine `let` binding, innermost-first, so the buried +//! `break`/`continue` is exposed at a statement boundary before the desugar +//! runs. Earlier sibling operands are pinned to their own temps first so +//! left-to-right evaluation order and side effects are preserved. +//! +//! This is a port to HIR of the `return_unify` FIR transform's ANF operand +//! lift, `return_unify::normalize::anf`. Two things change from the original: +//! the IR is HIR rather than FIR, and the lift fires on `break`/`continue` +//! rather than `return`. `return` is already unified downstream by +//! `return_unify` on the FIR codegen path, so hoisting it here would needlessly +//! change return handling. The operand-slot walk and the innermost-first, +//! evaluation-order-preserving lift mirror the original. +//! +//! ## Control-flow scoping +//! +//! A `break`/`continue` escapes the scanned expression only when it binds to a +//! loop outside it. One inside a loop nested within the expression binds to +//! that loop and does not trigger a lift. +//! +//! ## Non-defaultable operands and array-backing +//! +//! A lifted candidate can diverge through the control flow it carries before +//! producing its value, so on the divergence path the hoisted temporary holds +//! no meaningful value. The later desugar linearizes that path and still reads +//! the temp's slot textually, so the slot needs a well-typed placeholder even +//! though the value is never observed, because the read is guarded behind the +//! `break`/`continue` flags. When the operand's type `T` has a classical +//! default the temp is bound directly and that default seeds the divergence +//! path. Otherwise the temp is array-backed: it is stored as `T[]`, whose +//! divergence-path default is the universal `[]` that is well-typed for every +//! element type. The candidate's produced value `v` is wrapped as `[v]`, and +//! the operand slot reads the element back through `.operand_tmp_[0]`. Because +//! the read is only reached on the fall-through path, the empty array is never +//! indexed. This mirrors the array-backed return slot of the `return_unify` +//! FIR transform and covers `Qubit`, arrow, user-defined types, and tuples +//! thereof uniformly, without synthesizing a default of the operand's own +//! type. Only a genuinely-unrepresentable operand type — an inference or +//! type-parameter placeholder, the error type, or an unresolved user-defined +//! type — is rejected with [`Error::UnsupportedType`]. None of these can occur +//! for a well-typed operand post-typecheck, so the rejection is a defensive +//! guard. +//! +//! This module runs immediately before `loop_unification` in the pass drivers, +//! so operand-position `break`/`continue` is exposed at a statement boundary +//! before the loop desugar rewrites it. + +#[cfg(test)] +mod tests; + +use miette::Diagnostic; +use qsc_data_structures::span::Span; +use qsc_hir::{ + assigner::Assigner, + hir::{ + BinOp, Block, Expr, ExprKind, Lit, Mutability, QubitInit, QubitInitKind, Res, Stmt, + StmtKind, StringComponent, UnOp, + }, + mut_visit::MutVisitor, + ty::{Prim, Ty}, + visit::Visitor, +}; +use thiserror::Error; + +use crate::common::{IdentTemplate, LoopDepthScan, gen_ident}; + +#[derive(Clone, Debug, Diagnostic, Error)] +pub enum Error { + #[error("cannot hoist `break`/`continue` out of an operand-position value of type `{0}`")] + #[diagnostic(code("Qsc.LoopNormalize.UnsupportedBreakContinueType"))] + #[diagnostic(help( + "the operand's type could not be resolved to a representable form, so the temporary \ + introduced to lift the break/continue cannot be synthesized; restructure the code so \ + the break/continue is not nested inside this operand" + ))] + UnsupportedType(String, #[label("operand with unsupported type")] Span), +} + +/// Lifts operand-position `break`/`continue` buried behind a statement-carrying +/// construct to statement-position `let` temps. +/// +/// Runs to a fixpoint per statement: each statement's surface expression is +/// rewritten until no operand-position candidate remains, then the resulting +/// statements are visited so nested blocks and deeper operands are normalized. +/// +/// # Before +/// ```text +/// Foo({ if c { break; } x }); +/// ``` +/// +/// # After +/// ```text +/// let .operand_tmp_ = { if c { break; } x }; +/// Foo(.operand_tmp_); +/// ``` +pub(super) struct LoopNormalize<'a> { + pub(super) assigner: &'a mut Assigner, + pub(super) errors: Vec, +} + +/// Which short-circuit operator form is being reshaped into an `If`, carrying +/// whether the operator is `and` (`true`) or `or` (`false`). +enum ShortCircuitForm { + /// A value-producing `a and/or b` `BinOp`. + BinOp(bool), + /// A compound `set p and=/or= rhs` assignment. + AssignOp(bool), +} + +impl<'a> LoopNormalize<'a> { + pub(super) fn new(assigner: &'a mut Assigner) -> Self { + Self { + assigner, + errors: Vec::new(), + } + } + + /// Extracts the surface expression of `stmt` and lifts one operand-position + /// candidate out of it, returning the spine `let` bindings to splice before + /// `stmt`, or `None` when no operand candidate remains. + fn lift_stmt_surface(&mut self, stmt: &mut Stmt) -> Option> { + match &mut stmt.kind { + StmtKind::Expr(e) | StmtKind::Semi(e) | StmtKind::Local(_, _, e) => self.lift_once(e), + StmtKind::Qubit(_, _, init, _) => { + let mut operands = Vec::new(); + collect_qubit_init_operands(init, &mut operands); + self.lift_operands(operands) + } + StmtKind::Item(_) => None, + } + } + + /// Lifts one innermost-first operand-position candidate out of `expr`, + /// rewriting the operand slot in place to read a fresh temp and returning + /// the spine `let` bindings to splice before the enclosing statement. + /// + /// Mirrors the operand-slot enumeration of the `return_unify` ANF: every + /// eager operand site is descended to find a deeper candidate first; a + /// statement-carrying construct in an operand slot is lifted whole by its + /// parent via [`is_candidate`]; an `If` condition is an unconditional + /// operand site, while `If` branches and loop bodies are separate blocks + /// normalized by the visitor recursion. A `While` condition is deliberately + /// not an operand site: it is re-evaluated each iteration, so lifting it + /// once to a spine temp would break per-iteration re-evaluation. + /// + /// # Before + /// ```text + /// (earlier(), { if c { break; } value }) + /// ``` + /// + /// # After + /// ```text + /// let .operand_tmp_ = earlier(); + /// let .operand_tmp_ = { if c { break; } value }; + /// (.operand_tmp_, .operand_tmp_) + /// ``` + /// + /// # Mutations + /// - Rewrites the lifted operand slot in place to read the fresh temp. + /// - Allocates temp `Pat`/`Expr`/`Stmt` nodes through `assigner`. + #[allow(clippy::too_many_lines)] // Exhaustive `ExprKind` operand-site dispatch. + fn lift_once(&mut self, expr: &mut Expr) -> Option> { + if !contains_control_flow(expr) { + return None; + } + // A short-circuit `and`/`or` whose conditional right operand buries + // escaping control flow is first reshaped in place into the equivalent + // `If`, so the buried break/continue can reach a statement boundary + // inside a branch block; the reshaped `If` is then re-dispatched here. + if self.rewrite_short_circuit_rhs_in_place(expr) { + return self.lift_once(expr); + } + match &mut expr.kind { + // Short-circuit `and`/`or` and `and=`/`or=`: only the left side + // evaluates unconditionally, so its buried control flow is lifted + // here. A `BinOp` right operand that itself buries escaping control + // flow was already reshaped into an `If` above, so any right operand + // still reaching this arm is conditional and stays put. A compound + // `and=`/`or=` is never reshaped, so its conditional RHS also stays. + ExprKind::BinOp(BinOp::AndL | BinOp::OrL, lhs, _) + | ExprKind::AssignOp(BinOp::AndL | BinOp::OrL, lhs, _) => { + self.lift_short_circuit_lhs(lhs.as_mut()) + } + // Assign family: the lvalue place never buries control flow, so only + // the value operand is a lift slot. + ExprKind::Assign(_, value) + | ExprKind::AssignOp(_, _, value) + | ExprKind::AssignField(_, _, value) => self.lift_operands(vec![value.as_mut()]), + // AssignIndex: the place is excluded; the index and value are operands. + ExprKind::AssignIndex(_, index, value) => { + self.lift_operands(vec![index.as_mut(), value.as_mut()]) + } + // Immutable update evaluates replacement/index operands before the + // container, matching HIR-to-FIR lowering and preserving effects. + ExprKind::UpdateField(record, _, replace) => { + self.lift_operands(vec![replace.as_mut(), record.as_mut()]) + } + ExprKind::UpdateIndex(container, index, replace) => { + self.lift_operands(vec![index.as_mut(), replace.as_mut(), container.as_mut()]) + } + // Two-operand eager compounds. + ExprKind::ArrayRepeat(a, b) + | ExprKind::BinOp(_, a, b) + | ExprKind::Call(a, b) + | ExprKind::Index(a, b) => self.lift_operands(vec![a.as_mut(), b.as_mut()]), + // N-ary eager compounds. + ExprKind::Array(exprs) | ExprKind::Tuple(exprs) => { + self.lift_operands(exprs.iter_mut().collect()) + } + // Single-operand eager compounds. + ExprKind::Fail(e) | ExprKind::Field(e, _) | ExprKind::UnOp(_, e) => { + self.lift_operands(vec![e.as_mut()]) + } + // Optional operands in left-to-right order. + ExprKind::Range(start, step, end) => { + let operands = [start, step, end] + .into_iter() + .flatten() + .map(Box::as_mut) + .collect(); + self.lift_operands(operands) + } + // The `copy` operand, when present, evaluates before the field values. + ExprKind::Struct(_, copy, fields) => { + let mut operands: Vec<&mut Expr> = Vec::with_capacity(fields.len() + 1); + if let Some(c) = copy { + operands.push(c.as_mut()); + } + for field in fields.iter_mut() { + operands.push(field.value.as_mut()); + } + self.lift_operands(operands) + } + // Interpolated string expression components, in source order. + ExprKind::String(components) => { + let operands = components + .iter_mut() + .filter_map(|c| match c { + StringComponent::Expr(e) => Some(e.as_mut()), + StringComponent::Lit(_) => None, + }) + .collect(); + self.lift_operands(operands) + } + // An `If` condition is an unconditional operand site; the branches + // are separate blocks visited independently by the recursion. + ExprKind::If(cond, _, _) => self.lift_operands(vec![cond.as_mut()]), + // A `return` operand may bury an escaping break/continue; lift the + // operand to a temp while leaving the `return` node in place for + // `loop_unification` to guard. The `return` is deliberately not + // hoisted, because break/continue is not return control flow, so + // only the buried divergence is exposed. + ExprKind::Return(e) => self.lift_operands(vec![e.as_mut()]), + // A `for` iterable is evaluated once in the enclosing scope; lift it + // so a buried break/continue carried by a compound iterable is + // exposed at statement position without hoisting the `for` itself. + // The body is a separate block normalized by the visitor recursion. + ExprKind::For(_, iterable, _) => self.lift_operands(vec![iterable.as_mut()]), + // Recursion leaves: statement-carrying constructs are lifted whole + // by their parent (never descended here), and the remaining kinds + // carry no operand slot. + ExprKind::Block(_) + | ExprKind::Break + | ExprKind::Closure(_, _) + | ExprKind::Conjugate(_, _) + | ExprKind::Continue + | ExprKind::Err + | ExprKind::Hole + | ExprKind::Lit(_) + | ExprKind::Repeat(_, _, _) + | ExprKind::Var(_, _) + | ExprKind::While(_, _) => None, + } + } + + fn lift_short_circuit_lhs(&mut self, lhs: &mut Expr) -> Option> { + self.lift_operands(vec![lhs]) + } + + /// Reshapes a short-circuit `and`/`or` whose conditional right operand + /// buries escaping control flow into the equivalent `If`, in place, + /// returning `true` when a reshape was applied. + /// + /// Only the left operand of `and`/`or` evaluates unconditionally, so a + /// `break`/`continue` buried in the right operand is reachable only on the + /// not-short-circuited path. Exposing that path as an explicit branch lets + /// the buried control flow later reach a statement boundary: + /// + /// ```text + /// a and -> if a { } else { false } + /// a or -> if a { true } else { } + /// set p and= -> if p { set p = } + /// set p or= -> if not p { set p = } + /// ``` + /// + /// The reshaped value `If` keeps the operator's `Bool` type and span; the + /// reshaped assignment `If` is `Unit`-typed with no `else`, since the + /// omitted branch is a no-op because `p` already holds the short-circuit + /// result. Unlike FIR — where `If` branches are bare expressions — HIR `If` + /// branches are `Block`-typed exprs, so each branch is a fresh + /// single-statement block. + /// + /// A short-circuit whose right operand carries no escaping control flow is + /// left untouched, and its left operand is lifted by the caller instead. For + /// the `and=`/`or=` form the reshape is applied only when the right operand + /// buries the control flow in a bare operand position, `!is_candidate`, + /// such as `set p and= Foo(break)`. A statement-carrying wrapper right + /// operand such as `set p and= if c { break } else v` already exposes the + /// break at a branch boundary the desugar guards in place, so it needs no + /// reshape. The assignment place is a simple mutable variable, so re-reading + /// it for the guard condition is side-effect-free. Mirrors + /// `return_unify::normalize::hoist_short_circuit`. + fn rewrite_short_circuit_rhs_in_place(&mut self, expr: &mut Expr) -> bool { + // Classify the short-circuit form first so the borrow of `expr.kind` + // ends before the in-place rewrite mutates it. + let form = match &expr.kind { + ExprKind::BinOp(op @ (BinOp::AndL | BinOp::OrL), _, rhs) + if contains_control_flow(rhs) => + { + ShortCircuitForm::BinOp(matches!(op, BinOp::AndL)) + } + ExprKind::AssignOp(op @ (BinOp::AndL | BinOp::OrL), place, rhs) + if contains_control_flow(rhs) + && !is_candidate(rhs) + && matches!(place.kind, ExprKind::Var(_, _)) => + { + ShortCircuitForm::AssignOp(matches!(op, BinOp::AndL)) + } + _ => return false, + }; + let span = expr.span; + match form { + ShortCircuitForm::BinOp(is_and) => { + let ExprKind::BinOp(_, cond, rhs) = std::mem::take(&mut expr.kind) else { + unreachable!("expr.kind was just matched as a short-circuit BinOp"); + }; + let rhs_branch = self.wrap_expr_in_bool_block(rhs); + let lit_branch = self.gen_bool_lit_block(!is_and, span); + let (then_branch, else_branch) = if is_and { + (rhs_branch, lit_branch) + } else { + (lit_branch, rhs_branch) + }; + expr.kind = ExprKind::If(cond, Box::new(then_branch), Some(Box::new(else_branch))); + } + ShortCircuitForm::AssignOp(is_and) => { + let ExprKind::AssignOp(_, place, rhs) = std::mem::take(&mut expr.kind) else { + unreachable!("expr.kind was just matched as a short-circuit AssignOp"); + }; + let cond = self.gen_place_guard_condition(&place, is_and); + let assign_block = self.wrap_assign_in_unit_block(place, rhs); + expr.kind = ExprKind::If(Box::new(cond), Box::new(assign_block), None); + } + } + true + } + + /// Builds the guard condition for a reshaped compound short-circuit + /// assignment: a fresh read of the assignment's place for `and=`, or its + /// logical negation for `or=`. The place is a simple mutable variable + /// reference, so re-reading it is side-effect-free. + fn gen_place_guard_condition(&mut self, place: &Expr, is_and: bool) -> Expr { + let read = Expr { + id: self.assigner.next_node(), + span: place.span, + ty: place.ty.clone(), + kind: place.kind.clone(), + }; + if is_and { + read + } else { + Expr { + id: self.assigner.next_node(), + span: place.span, + ty: Ty::Prim(Prim::Bool), + kind: ExprKind::UnOp(UnOp::NotL, Box::new(read)), + } + } + } + + /// Wraps `set = ` in a fresh `Unit`-typed single-statement + /// block expr, for use as the `then` branch of a reshaped compound + /// short-circuit assignment. + fn wrap_assign_in_unit_block(&mut self, place: Box, rhs: Box) -> Expr { + let span = rhs.span; + let assign = Expr { + id: self.assigner.next_node(), + span, + ty: Ty::UNIT, + kind: ExprKind::Assign(place, rhs), + }; + let stmt = Stmt { + id: self.assigner.next_node(), + span, + kind: StmtKind::Semi(assign), + }; + self.gen_block(vec![stmt], Ty::UNIT, span) + } + + /// Wraps `expr` (a `Bool`-valued operand) as a fresh single-statement block + /// expr typed `Bool`, for use as an `If` branch when reshaping a + /// short-circuit operator. + fn wrap_expr_in_bool_block(&mut self, expr: Box) -> Expr { + let span = expr.span; + let stmt = Stmt { + id: self.assigner.next_node(), + span, + kind: StmtKind::Expr(*expr), + }; + self.gen_bool_block(vec![stmt], span) + } + + /// Builds a fresh single-statement block expr whose trailing value is the + /// `Bool` literal `value`, typed `Bool`, for use as the constant `If` + /// branch when reshaping a short-circuit operator. + fn gen_bool_lit_block(&mut self, value: bool, span: Span) -> Expr { + let lit = Expr { + id: self.assigner.next_node(), + span, + ty: Ty::Prim(Prim::Bool), + kind: ExprKind::Lit(Lit::Bool(value)), + }; + let stmt = Stmt { + id: self.assigner.next_node(), + span, + kind: StmtKind::Expr(lit), + }; + self.gen_bool_block(vec![stmt], span) + } + + /// Wraps `stmts` in a fresh `Bool`-typed block expr. + fn gen_bool_block(&mut self, stmts: Vec, span: Span) -> Expr { + self.gen_block(stmts, Ty::Prim(Prim::Bool), span) + } + + /// Wraps `stmts` in a fresh block expr of type `ty`. + fn gen_block(&mut self, stmts: Vec, ty: Ty, span: Span) -> Expr { + Expr { + id: self.assigner.next_node(), + span, + ty: ty.clone(), + kind: ExprKind::Block(Block { + id: self.assigner.next_node(), + span, + ty, + stmts, + }), + } + } + + /// Lifts one operand from an ordered operand list, innermost-first. + /// + /// First recurses into each operand to lift a deeper candidate; only if + /// none is found does it lift the first directly-liftable operand at this + /// level, pinning every earlier operand to its own spine temp so + /// left-to-right evaluation order is preserved. + fn lift_operands(&mut self, mut operands: Vec<&mut Expr>) -> Option> { + // Innermost-first: try to lift a deeper operand inside any child first. + for op in &mut operands { + if let Some(stmts) = self.lift_once(op) { + return Some(stmts); + } + } + // No deeper candidate; lift the first liftable operand at this level. + let idx = operands.iter().position(|op| is_candidate(op))?; + let mut out = Vec::with_capacity(idx + 1); + for (i, op) in operands.iter_mut().enumerate() { + if i < idx { + // Pin each earlier operand to preserve evaluation order. + out.push(self.bind_temp(op, false)); + } else if i == idx { + out.push(self.bind_temp(op, true)); + break; + } + } + Some(out) + } + + /// Binds `op` to a fresh immutable `let .operand_tmp_ = ;` on the + /// statement spine and rewrites `op` in place to read the temp. + /// + /// When `check_defaultable` is set, the lifted candidate can diverge before + /// producing a value, so the temp must have a well-typed placeholder on the + /// divergence path. A type with a classical default is bound directly; any + /// other representable type is array-backed by + /// [`Self::bind_array_backed_temp`]; only a genuinely-unrepresentable type + /// records [`Error::UnsupportedType`], a defensive guard that is unreachable + /// for a well-typed operand post-typecheck, before falling through to a + /// direct bind so the tree stays well-formed. Pinned earlier siblings always + /// produce a value, so they are bound directly without any check. + fn bind_temp(&mut self, op: &mut Expr, check_defaultable: bool) -> Stmt { + let ty = op.ty.clone(); + let span = op.span; + if check_defaultable && !is_defaultable(&ty) { + if is_representable(&ty) { + return self.bind_array_backed_temp(op, &ty, span); + } + self.errors + .push(Error::UnsupportedType(format!("{ty}"), span)); + } + let ident = gen_ident(self.assigner, "operand_tmp", ty, span); + let init = std::mem::replace(op, ident.gen_local_ref(self.assigner)); + ident.gen_id_init(Mutability::Immutable, init, self.assigner) + } + + /// If `stmt` is a `let` binding whose initializer buries escaping control + /// flow and whose representable type has no classical default, array-backs + /// the initializer and returns the `let .operand_tmp_ : ty[] = ` statement to splice before the binding; the binding itself is + /// rewritten in place to read the value back through `.operand_tmp_[0]`. + /// Returns `None` for any other statement. + /// + /// A binding such as `let x = if c { break } else v`, where `x` has no + /// classical default, cannot be guarded in place by the later desugar, + /// because the divergence path would need a default of `x`'s type. + /// Array-backing seeds that path with the universal `[]` default of `ty[]` + /// instead, and the desugar relocates the guarded `.operand_tmp_[0]` read + /// into the fall-through branch, so the binding never needs a default of its + /// own type. A defaultable binding, a binding whose initializer carries no + /// escaping control flow, and an unrepresentable type are all left + /// untouched. + fn array_back_control_flow_local(&mut self, stmt: &mut Stmt) -> Option { + let StmtKind::Local(_, _, init) = &mut stmt.kind else { + return None; + }; + if is_defaultable(&init.ty) || !contains_control_flow(init) || !is_representable(&init.ty) { + return None; + } + let ty = init.ty.clone(); + let span = init.span; + Some(self.bind_array_backed_temp(init, &ty, span)) + } + + /// Array-backs a discarded value-block or `if` statement of the form + /// `;` that buries escaping control flow and whose type has no + /// classical default, for example `{ if c { break } else { Complex(..) } };`. + /// Because the value is discarded, it is retyped to `T[]` in place: each + /// produced leaf is wrapped as `[leaf]` while the buried break/continue keeps + /// its value position, so the value-block gains the universal `[]` default of + /// `T[]` on the divergence path and the later desugar needs no default of + /// `T`. + /// + /// Only a `Block`/`If` value is considered: a bare `break`/`continue` + /// statement and a loop are handled directly by the desugar, and an operand- + /// position value has already been lifted by the fixpoint above. A + /// defaultable value, a value without escaping control flow, and an + /// unrepresentable type are all left untouched. + fn array_back_discarded_control_flow_value(&mut self, stmt: &mut Stmt) { + let StmtKind::Semi(value) = &mut stmt.kind else { + return; + }; + if !matches!(value.kind, ExprKind::Block(_) | ExprKind::If(_, _, _)) + || is_defaultable(&value.ty) + || !is_representable(&value.ty) + || !contains_control_flow(value) + { + return; + } + let ty = value.ty.clone(); + self.arrayify_value_in_place(value, &ty); + } + + /// Binds `op` (a lifted candidate of the non-defaultable type `ty`) to a + /// fresh `let .operand_tmp_ : ty[] = ;` and rewrites `op` + /// in place to read the single stored element back through + /// `.operand_tmp_[0]`. + /// + /// Storing the value behind a length-1 array lets the later desugar seed the + /// divergence path with the universal `[]` default of `ty[]`, so no + /// classical default of `ty` itself is needed. The `[0]` read is only + /// reached on the fall-through path, which the desugar guards behind the + /// `break`/`continue` flags, so the empty array is never indexed. This + /// mirrors the array-backed return slot of the `return_unify` FIR transform. + fn bind_array_backed_temp(&mut self, op: &mut Expr, ty: &Ty, span: Span) -> Stmt { + let array_ty = Ty::Array(Box::new(ty.clone())); + let ident = gen_ident(self.assigner, "operand_tmp", array_ty, span); + let read = self.gen_singleton_index_read(&ident, ty); + let mut init = std::mem::replace(op, read); + self.arrayify_value_in_place(&mut init, ty); + ident.gen_id_init(Mutability::Immutable, init, self.assigner) + } + + /// Builds `@[0]`, reading element 0 (of type `elem_ty`) out of the + /// array-backed temp `` (of type `elem_ty[]`). + fn gen_singleton_index_read(&mut self, ident: &IdentTemplate, elem_ty: &Ty) -> Expr { + let array_ref = ident.gen_local_ref(self.assigner); + let zero = Expr { + id: self.assigner.next_node(), + span: ident.span, + ty: Ty::Prim(Prim::Int), + kind: ExprKind::Lit(Lit::Int(0)), + }; + Expr { + id: self.assigner.next_node(), + span: ident.span, + ty: elem_ty.clone(), + kind: ExprKind::Index(Box::new(array_ref), Box::new(zero)), + } + } + + /// Retypes the value produced by `expr` from `elem_ty` to `elem_ty[]` in + /// place, recursing through the value-producing positions a control-flow- + /// bearing candidate can take, namely a `Block`'s trailing value and both + /// `If` branches, so the buried `break`/`continue` is never moved out of + /// statement position. + /// + /// A divergent `break`/`continue`/`return` produces no value, so only its + /// surrounding type is adjusted; wrapping it as an array element would + /// re-bury it in an operand position and re-trigger the lift. Any other leaf + /// is a genuine `elem_ty`-valued result and is replaced with the + /// single-element array `[ ]`. A `While`/`For`/`Repeat` is always + /// `Unit`-typed, and `Unit` has a classical default, so a loop never takes + /// the array-backed path and never reaches this helper. Mirrors the + /// array-backing of the `return_unify` FIR transform. + fn arrayify_value_in_place(&mut self, expr: &mut Expr, elem_ty: &Ty) { + let array_ty = Ty::Array(Box::new(elem_ty.clone())); + match &mut expr.kind { + ExprKind::Block(block) => { + self.arrayify_block_tail_value(block, elem_ty); + expr.ty = array_ty; + } + ExprKind::If(_, then_branch, otherwise) => { + self.arrayify_value_in_place(then_branch, elem_ty); + if let Some(else_branch) = otherwise { + self.arrayify_value_in_place(else_branch, elem_ty); + } + expr.ty = array_ty; + } + ExprKind::Break | ExprKind::Continue | ExprKind::Return(_) => { + expr.ty = array_ty; + } + _ => self.wrap_leaf_value_in_place(expr, elem_ty), + } + } + + /// Retypes a block's trailing value, its last `Expr` statement if any, to + /// `elem_ty[]` and sets the block's own type to match. + fn arrayify_block_tail_value(&mut self, block: &mut Block, elem_ty: &Ty) { + if let Some(Stmt { + kind: StmtKind::Expr(tail), + .. + }) = block.stmts.last_mut() + { + self.arrayify_value_in_place(tail, elem_ty); + } + block.ty = Ty::Array(Box::new(elem_ty.clone())); + } + + /// Replaces an `elem_ty`-valued leaf `expr` in place with the single-element + /// array literal `[ ]` of type `elem_ty[]`, moving the original leaf + /// with all its children intact inside the new array. + fn wrap_leaf_value_in_place(&mut self, expr: &mut Expr, elem_ty: &Ty) { + let span = expr.span; + let inner = std::mem::take(expr); + *expr = Expr { + id: self.assigner.next_node(), + span, + ty: Ty::Array(Box::new(elem_ty.clone())), + kind: ExprKind::Array(vec![inner]), + }; + } +} + +impl MutVisitor for LoopNormalize<'_> { + fn visit_block(&mut self, block: &mut Block) { + let stmts = std::mem::take(&mut block.stmts); + let mut out = Vec::with_capacity(stmts.len()); + for mut stmt in stmts { + // Per-statement fixpoint: lift operand-position candidates from the + // surface expression into preceding `let` bindings until stable. + while let Some(prefix) = self.lift_stmt_surface(&mut stmt) { + for mut lifted in prefix { + // Normalize nested blocks and deeper operands inside each + // lifted temp's initializer. + self.visit_stmt(&mut lifted); + out.push(lifted); + } + } + // Array-back a non-defaultable `let` binding whose initializer buries + // escaping control flow, for example `let x = if c { break } else v` + // where `x` has no classical default. The value is stored behind a + // defaultable array temp, so the buried break/continue reaches a + // statement boundary without the binding needing a default of its own + // type; the `loop_unification` desugar then relocates the guarded + // read into the fall-through branch. + if let Some(mut backing) = self.array_back_control_flow_local(&mut stmt) { + self.visit_stmt(&mut backing); + out.push(backing); + } + // Array-back a discarded non-defaultable value-block statement, whose + // result is dropped, so a break/continue buried in it desugars + // without a default of the value's type. + self.array_back_discarded_control_flow_value(&mut stmt); + // Normalize nested blocks such as branches, loop bodies, and block + // exprs, along with any remaining sub-structure of the finalized + // statement. + self.visit_stmt(&mut stmt); + out.push(stmt); + } + block.stmts = out; + } +} + +/// Collects qubit-array length expressions in initializer evaluation order. +fn collect_qubit_init_operands<'a>(init: &'a mut QubitInit, operands: &mut Vec<&'a mut Expr>) { + match &mut init.kind { + QubitInitKind::Array(length) => operands.push(length), + QubitInitKind::Tuple(inits) => { + for init in inits { + collect_qubit_init_operands(init, operands); + } + } + QubitInitKind::Single | QubitInitKind::Err => {} + } +} + +/// Returns `true` when `expr` is an operand subexpression the lift should bind +/// to a spine temp: a bare `break`/`continue`, or a statement-carrying +/// `Block`/`If`/`While`/`For`/`Repeat`, that contains control flow escaping +/// `expr`. +/// +/// A bare `break`/`continue` sitting directly in an operand slot, for example +/// `Foo(break)` or `arr[break]`, is itself the escaping control flow, so it is +/// lifted to its own spine temp; the later desugar then guards the enclosing +/// operator behind the `break`/`continue` flags. Only the divergence is +/// exposed — the temp's placeholder value is never observed. +fn is_candidate(expr: &Expr) -> bool { + matches!( + expr.kind, + ExprKind::Break + | ExprKind::Continue + | ExprKind::Block(_) + | ExprKind::If(_, _, _) + | ExprKind::While(_, _) + | ExprKind::For(_, _, _) + | ExprKind::Repeat(_, _, _) + ) && contains_control_flow(expr) +} + +/// Read-only check whether `ty` has a classical default the `loop_unification` +/// desugar can materialize directly through its `build_default_kind`. This +/// selects the direct binding strategy; any other representable type is +/// array-backed instead. Kept in exact agreement with `build_default_kind` so +/// the two passes never disagree on which types are directly defaultable; +/// `Arrow` and every user-defined type are not. +fn is_defaultable(ty: &Ty) -> bool { + match ty { + Ty::Prim(Prim::Qubit) + | Ty::Arrow(_) + | Ty::Udt(..) + | Ty::Infer(_) + | Ty::Param { .. } + | Ty::Err => false, + Ty::Prim(_) | Ty::Array(_) => true, + Ty::Tuple(elems) => elems.iter().all(is_defaultable), + } +} + +/// Read-only check whether an array-backed temp of `ty` can be synthesized. +/// The universal `[]` default of `ty[]` is well-typed whenever `ty`'s structure +/// is resolvable, so a resolved user-defined type is representable regardless of +/// which package defines it: array-backing needs only the `[]` default of `T[]`, +/// never a default of the user-defined type itself. This excludes only +/// genuinely-unresolvable leaves: inference and type-parameter placeholders, the +/// error type, and an unresolved user-defined type. None of these can occur for +/// a well-typed operand post-typecheck, so the resulting rejection is a +/// defensive guard. +fn is_representable(ty: &Ty) -> bool { + match ty { + Ty::Prim(_) | Ty::Array(_) | Ty::Arrow(_) | Ty::Udt(_, Res::Item(_)) => true, + Ty::Tuple(elems) => elems.iter().all(is_representable), + Ty::Udt(_, _) | Ty::Infer(_) | Ty::Param { .. } | Ty::Err => false, + } +} + +/// Returns `true` when `expr` contains a `break`/`continue` that escapes to +/// the enclosing statement context, i.e. one binding to a loop outside `expr`. +/// +/// `return` is deliberately not treated as escaping: it is unified downstream +/// by `return_unify` on the FIR codegen path, so hoisting it here would change +/// return handling for no benefit to the loop desugar. +fn contains_control_flow(expr: &Expr) -> bool { + let mut scan = ControlFlowScan { + loop_depth: 0, + found: false, + }; + scan.visit_expr(expr); + scan.found +} + +/// Detects escaping `break`/`continue`, tracking loop nesting so one bound to a +/// loop inside the scanned expression is not counted. +struct ControlFlowScan { + loop_depth: u32, + found: bool, +} + +impl<'a> Visitor<'a> for ControlFlowScan { + fn visit_expr(&mut self, expr: &'a Expr) { + self.walk_loop_depth(expr); + } +} + +impl LoopDepthScan<'_> for ControlFlowScan { + fn loop_depth(&self) -> u32 { + self.loop_depth + } + + fn enter_loop(&mut self) { + self.loop_depth += 1; + } + + fn exit_loop(&mut self) { + self.loop_depth -= 1; + } + + fn is_done(&self) -> bool { + self.found + } + + fn record_break_continue(&mut self, _expr: &Expr, at_enclosing_loop: bool) { + // A break/continue escapes only when it binds to an enclosing loop + // outside the scanned expression. + if at_enclosing_loop { + self.found = true; + } + } +} diff --git a/source/compiler/qsc_passes/src/loop_normalize/tests.rs b/source/compiler/qsc_passes/src/loop_normalize/tests.rs new file mode 100644 index 00000000000..e565c9ab3d2 --- /dev/null +++ b/source/compiler/qsc_passes/src/loop_normalize/tests.rs @@ -0,0 +1,1451 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::too_many_lines)] + +use expect_test::{Expect, expect}; +use indoc::indoc; +use qsc_data_structures::{ + language_features::LanguageFeatures, source::SourceMap, target::TargetCapabilityFlags, +}; +use qsc_frontend::compile::{self, PackageStore, compile}; +use qsc_hir::{mut_visit::MutVisitor, validate::Validator, visit::Visitor}; + +use crate::loop_normalize::LoopNormalize; + +/// Compiles `file`, runs [`LoopNormalize`] once over the package, asserts the +/// package still validates and no rejection diagnostics were produced, then +/// snapshots the transformed package. +fn check(file: &str, expect: &Expect) { + let store = PackageStore::new(compile::core()); + let sources = SourceMap::new([("test".into(), file.into())], None); + let mut unit = compile( + &store, + &[], + sources, + TargetCapabilityFlags::all(), + LanguageFeatures::default(), + ); + assert!(unit.errors.is_empty(), "{:?}", unit.errors); + + let errors = { + let mut pass = LoopNormalize::new(&mut unit.assigner); + pass.visit_package(&mut unit.package); + pass.errors + }; + assert!(errors.is_empty(), "unexpected rejection errors: {errors:?}"); + Validator::default().visit_package(&unit.package); + expect.assert_eq(&crate::qsharp_gen::write_package_qsharp( + &store, + &unit.package, + )); +} + +/// Compiles `file`, runs [`LoopNormalize`] once, snapshots the rejection +/// diagnostics it produced, and returns the transformed package text. +fn check_errors(file: &str, expect: &Expect) -> String { + let store = PackageStore::new(compile::core()); + let sources = SourceMap::new([("test".into(), file.into())], None); + let mut unit = compile( + &store, + &[], + sources, + TargetCapabilityFlags::all(), + LanguageFeatures::default(), + ); + assert!(unit.errors.is_empty(), "{:?}", unit.errors); + + let errors = { + let mut pass = LoopNormalize::new(&mut unit.assigner); + pass.visit_package(&mut unit.package); + pass.errors + }; + // The package must remain structurally valid even on the rejection path. + Validator::default().visit_package(&unit.package); + expect.assert_debug_eq(&errors); + crate::qsharp_gen::write_package_qsharp(&store, &unit.package) +} + +/// Compiles `file`, runs [`LoopNormalize`] once, records the package, runs it a +/// second time, asserts the package is unchanged, confirming the pass is +/// idempotent, and returns the stable package text. +fn check_idempotent(file: &str) -> String { + let store = PackageStore::new(compile::core()); + let sources = SourceMap::new([("test".into(), file.into())], None); + let mut unit = compile( + &store, + &[], + sources, + TargetCapabilityFlags::all(), + LanguageFeatures::default(), + ); + assert!(unit.errors.is_empty(), "{:?}", unit.errors); + + LoopNormalize::new(&mut unit.assigner).visit_package(&mut unit.package); + let after_first = crate::qsharp_gen::write_package_qsharp(&store, &unit.package); + + LoopNormalize::new(&mut unit.assigner).visit_package(&mut unit.package); + let after_second = crate::qsharp_gen::write_package_qsharp(&store, &unit.package); + + assert_eq!( + after_first, after_second, + "second run of LoopNormalize changed the package" + ); + after_second +} + +/// Compiles `file`, runs [`LoopNormalize`] once, validates the result, and +/// returns the generated Q# text for targeted structural assertions. +fn normalize_to_string(file: &str) -> String { + let store = PackageStore::new(compile::core()); + let sources = SourceMap::new([("test".into(), file.into())], None); + let mut unit = compile( + &store, + &[], + sources, + TargetCapabilityFlags::all(), + LanguageFeatures::default(), + ); + assert!(unit.errors.is_empty(), "{:?}", unit.errors); + + let errors = { + let mut pass = LoopNormalize::new(&mut unit.assigner); + pass.visit_package(&mut unit.package); + pass.errors + }; + assert!(errors.is_empty(), "unexpected rejection errors: {errors:?}"); + Validator::default().visit_package(&unit.package); + crate::qsharp_gen::write_package_qsharp(&store, &unit.package) +} + +fn operand_temp_bind_count(package: &str) -> usize { + package.matches("let _operand_tmp").count() +} + +#[test] +fn logical_assign_rhs_is_not_hoisted() { + let package = normalize_to_string(indoc! {" + namespace Test { + operation Main() : Unit { + mutable cond = false; + mutable keepGoing = false; + while cond { + keepGoing and= if cond { break } else { true }; + } + } + } + "}); + + assert_eq!( + operand_temp_bind_count(&package), + 0, + "short-circuit assignment RHS must stay conditional\n{package}" + ); + + expect![[r#" + operation Main() : Unit { + mutable cond = false; + mutable keepGoing = false; + while cond { + keepGoing and= if cond { + break + } else { + true + }; + } + } + "#]] + .assert_eq(&package); +} + +#[test] +fn update_field_evaluates_replacement_before_record_when_hoisting() { + let package = normalize_to_string(indoc! {" + namespace Test { + newtype Pair = (A : Int, B : Int); + + operation Main() : Unit { + mutable cond = true; + mutable marker = 0; + while cond { + let updated = { marker += 1; Pair(1, 2) } w/ B <- if cond { break } else { 3 }; + } + } + } + "}); + + assert_eq!( + operand_temp_bind_count(&package), + 1, + "only the replacement operand should be hoisted before a record update\n{package}" + ); + + expect![[r#" + // newtype Pair + operation Main() : Unit { + mutable cond = true; + mutable marker = 0; + while cond { + let _operand_tmp_45 = if cond { + break + } else { + 3 + }; + let updated = { + marker += 1; + Pair(1, 2) + } w/::B <- _operand_tmp_45; + } + } + "#]] + .assert_eq(&package); +} + +#[test] +fn update_index_evaluates_index_and_replacement_before_container_when_hoisting() { + let package = normalize_to_string(indoc! {" + namespace Test { + operation Main() : Unit { + mutable cond = true; + mutable marker = 0; + while cond { + let updated = { marker += 1; [1, 2] } w/ if cond { break } else { 0 } <- 3; + } + } + } + "}); + + assert_eq!( + operand_temp_bind_count(&package), + 1, + "only the index operand should be hoisted before an array update\n{package}" + ); + + expect![[r#" + operation Main() : Unit { + mutable cond = true; + mutable marker = 0; + while cond { + let _operand_tmp_43 = if cond { + break + } else { + 0 + }; + let updated = { + marker += 1; + [1, 2] + } w/ _operand_tmp_43 <- 3; + } + } + "#]] + .assert_eq(&package); +} + +#[test] +fn hoist_break_in_call_argument() { + check( + indoc! {" + namespace Test { + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo(if cond { break } else { 3 }); + } + } + } + "}, + &expect![[r#" + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + let _operand_tmp_33 = Foo; + let _operand_tmp_37 = if cond { + break + } else { + 3 + }; + _operand_tmp_33(_operand_tmp_37); + } + } + "#]], + ); +} + +#[test] +fn hoist_continue_in_call_argument() { + check( + indoc! {" + namespace Test { + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo(if cond { continue } else { 3 }); + } + } + } + "}, + &expect![[r#" + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + let _operand_tmp_33 = Foo; + let _operand_tmp_37 = if cond { + continue + } else { + 3 + }; + _operand_tmp_33(_operand_tmp_37); + } + } + "#]], + ); +} + +#[test] +fn hoist_break_in_binop_operand() { + check( + indoc! {" + namespace Test { + operation Main() : Unit { + mutable cond = true; + mutable acc = 0; + while cond { + let y = acc + (if cond { break } else { 3 }); + } + } + } + "}, + &expect![[r#" + operation Main() : Unit { + mutable cond = true; + mutable acc = 0; + while cond { + let _operand_tmp_33 = acc; + let _operand_tmp_37 = if cond { + break + } else { + 3 + }; + let y = _operand_tmp_33 + _operand_tmp_37; + } + } + "#]], + ); +} + +#[test] +fn hoist_break_in_operand_block() { + check( + indoc! {" + namespace Test { + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo({ if cond { break }; 3 }); + } + } + } + "}, + &expect![[r#" + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + let _operand_tmp_34 = Foo; + let _operand_tmp_38 = { + if cond { + break + }; + 3 + }; + _operand_tmp_34(_operand_tmp_38); + } + } + "#]], + ); +} + +#[test] +fn hoist_nested_operand_blocks() { + check( + indoc! {" + namespace Test { + function Bar(x : Int) : Int { x } + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo(Bar(if cond { break } else { 3 })); + } + } + } + "}, + &expect![[r#" + function Bar(x : Int) : Int { + x + } + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + let _operand_tmp_43 = Bar; + let _operand_tmp_47 = if cond { + break + } else { + 3 + }; + Foo(_operand_tmp_43(_operand_tmp_47)); + } + } + "#]], + ); +} + +#[test] +fn hoist_break_in_tuple_operand() { + check( + indoc! {" + namespace Test { + operation Foo(x : (Int, Int)) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo((1, if cond { break } else { 3 })); + } + } + } + "}, + &expect![[r#" + operation Foo(x : (Int, Int)) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + let _operand_tmp_35 = 1; + let _operand_tmp_39 = if cond { + break + } else { + 3 + }; + Foo(_operand_tmp_35, _operand_tmp_39); + } + } + "#]], + ); +} + +#[test] +fn idempotent_after_hoisting_break() { + let package = check_idempotent(indoc! {" + namespace Test { + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo(if cond { break } else { 3 }); + } + } + } + "}); + + expect![[r#" + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + let _operand_tmp_33 = Foo; + let _operand_tmp_37 = if cond { + break + } else { + 3 + }; + _operand_tmp_33(_operand_tmp_37); + } + } + "#]] + .assert_eq(&package); +} + +#[test] +fn idempotent_after_hoisting_nested_operands() { + let package = check_idempotent(indoc! {" + namespace Test { + function Bar(x : Int) : Int { x } + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo(Bar(if cond { break } else { 3 })); + } + } + } + "}); + + expect![[r#" + function Bar(x : Int) : Int { + x + } + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + let _operand_tmp_43 = Bar; + let _operand_tmp_47 = if cond { + break + } else { + 3 + }; + Foo(_operand_tmp_43(_operand_tmp_47)); + } + } + "#]] + .assert_eq(&package); +} + +#[test] +fn preserves_type_of_surface_if() { + // The `if` is the direct initializer of the `let`, a statement position rather + // than an operand, so it is left in place and `x` keeps its `Int` type. + check( + indoc! {" + namespace Test { + operation Main() : Unit { + mutable cond = true; + while cond { + let x = if cond { break } else { 3 }; + let y = x + 1; + } + } + } + "}, + &expect![[r#" + operation Main() : Unit { + mutable cond = true; + while cond { + let x = if cond { + break + } else { + 3 + }; + let y = x + 1; + } + } + "#]], + ); +} + +#[test] +fn no_op_for_statement_position_break() { + // A break that is already a statement, inside a statement-position `if`, is + // not in operand position, so nothing is hoisted. + check( + indoc! {" + namespace Test { + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + if cond { break } + Foo(3); + } + } + } + "}, + &expect![[r#" + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + if cond { + break + } + Foo(3); + } + } + "#]], + ); +} + +#[test] +fn no_op_for_operand_block_without_control_flow() { + // An operand-position block that contains no escaping control flow is left + // untouched. + check( + indoc! {" + namespace Test { + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo({ let z = 1; z }); + } + } + } + "}, + &expect![[r#" + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo({ + let z = 1; + z + }); + } + } + "#]], + ); +} + +#[test] +fn no_op_for_break_bound_to_nested_loop() { + // The break binds to the inner `while`, so it does not escape the operand + // block and no hoist is performed. + check( + indoc! {" + namespace Test { + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo(if cond { while cond { break }; 3 } else { 4 }); + } + } + } + "}, + &expect![[r#" + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo(if cond { + while cond { + break + }; + 3 + } else { + 4 + }); + } + } + "#]], + ); +} + +#[test] +fn hoist_break_in_tuple_qubit_initializer_preserves_evaluation_order() { + check( + indoc! {" + namespace Test { + operation Length(value : Int) : Int { value } + operation Main() : Unit { + mutable cond = true; + while cond { + use (first, second) = ( + Qubit[Length(1)], + Qubit[if cond { break } else { Length(2) }] + ); + } + } + } + "}, + &expect![[r#" + operation Length(value : Int) : Int { + value + } + operation Main() : Unit { + mutable cond = true; + while cond { + let _operand_tmp_46 = Length(1); + let _operand_tmp_50 = if cond { + break + } else { + Length(2) + }; + use (first, second) = (Qubit[_operand_tmp_46], Qubit[_operand_tmp_50]); + } + } + "#]], + ); +} + +#[test] +fn hoist_break_in_qubit_operand_block_array_backed() { + // Lifting the operand block introduces a temporary of type `Qubit`, which + // has no classical default for the break path. Rather than reject it, the + // pass array-backs the temp as `Qubit[]`: the block's trailing value `q` is + // wrapped as `[q]`, and the operand slot reads it back through + // `.operand_tmp_[0]`. The later desugar seeds the break path with the + // universal `[]` default and guards the read, so `[]` is never indexed. + check( + indoc! {" + namespace Test { + operation Foo(q : Qubit) : Unit {} + operation Main() : Unit { + use q = Qubit(); + mutable cond = true; + while cond { + Foo({ if cond { break }; q }); + } + } + } + "}, + &expect![[r#" + operation Foo(q : Qubit) : Unit {} + operation Main() : Unit { + use q = Qubit(); + mutable cond = true; + while cond { + let _operand_tmp_38 = Foo; + let _operand_tmp_42 = { + if cond { + break + }; + [q] + }; + _operand_tmp_38(_operand_tmp_42[0]); + } + } + "#]], + ); +} + +#[test] +fn hoist_break_in_arrow_operand_block_array_backed() { + // An arrow-typed operand value-block has no classical default, so it is + // array-backed as `(Qubit => Unit)[]`. Previously such operands were hoisted + // directly and then rejected by the desugar; array-backing covers them. + check( + indoc! {" + namespace Test { + operation Bar(q : Qubit) : Unit {} + operation Foo(op : Qubit => Unit) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo(if cond { break } else { Bar }); + } + } + } + "}, + &expect![[r#" + operation Bar(q : Qubit) : Unit {} + operation Foo(op : (Qubit => Unit)) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + let _operand_tmp_39 = Foo; + let _operand_tmp_43 = if cond { + break + } else { + [Bar] + }; + _operand_tmp_39(_operand_tmp_43[0]); + } + } + "#]], + ); +} + +#[test] +fn hoist_break_in_udt_operand_block_array_backed() { + // A user-defined-type operand value-block is array-backed as `Pair[]`, + // uniformly with `Qubit` and arrow types and without constructing a `Pair` + // default. This is the case the two passes previously disagreed on: the + // normalize pass hoisted it directly, then the desugar rejected it. + check( + indoc! {" + namespace Test { + newtype Pair = (First : Int, Second : Int); + operation Foo(p : Pair) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo(if cond { break } else { Pair(1, 2) }); + } + } + } + "}, + &expect![[r#" + // newtype Pair + operation Foo(p : Pair) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + let _operand_tmp_38 = Foo; + let _operand_tmp_42 = if cond { + break + } else { + [Pair(1, 2)] + }; + _operand_tmp_38(_operand_tmp_42[0]); + } + } + "#]], + ); +} + +#[test] +fn hoist_break_in_tuple_with_qubit_operand_array_backed() { + // A tuple containing a `Qubit` is non-defaultable but representable, so the + // whole operand is array-backed as `(Int, Qubit)[]`; the trailing tuple + // value `(1, q)` is wrapped as `[(1, q)]` without decomposing the tuple. + check( + indoc! {" + namespace Test { + operation Foo(x : (Int, Qubit)) : Unit {} + operation Main() : Unit { + use q = Qubit(); + mutable cond = true; + while cond { + Foo(if cond { break } else { (1, q) }); + } + } + } + "}, + &expect![[r#" + operation Foo(x : (Int, Qubit)) : Unit {} + operation Main() : Unit { + use q = Qubit(); + mutable cond = true; + while cond { + let _operand_tmp_39 = Foo; + let _operand_tmp_43 = if cond { + break + } else { + [(1, q)] + }; + _operand_tmp_39(_operand_tmp_43[0]); + } + } + "#]], + ); +} + +#[test] +fn idempotent_after_array_backing_qubit_operand() { + let package = check_idempotent(indoc! {" + namespace Test { + operation Foo(q : Qubit) : Unit {} + operation Main() : Unit { + use q = Qubit(); + mutable cond = true; + while cond { + Foo({ if cond { break }; q }); + } + } + } + "}); + + expect![[r#" + operation Foo(q : Qubit) : Unit {} + operation Main() : Unit { + use q = Qubit(); + mutable cond = true; + while cond { + let _operand_tmp_38 = Foo; + let _operand_tmp_42 = { + if cond { + break + }; + [q] + }; + _operand_tmp_38(_operand_tmp_42[0]); + } + } + "#]] + .assert_eq(&package); +} + +#[test] +fn reject_break_in_unrepresentable_operand_block() { + // A type-parameter-typed operand value-block is conservatively excluded from + // array-backing. This matches the `return_unify` transform, which treats + // unresolved leaves such as type parameters as the sole rejecting case, so + // the pass records its defensive rejection. Such an operand cannot occur for + // a well-typed program post-typecheck once callables are monomorphized. + let package = check_errors( + indoc! {" + namespace Test { + operation Foo<'T>(x : 'T, g : 'T => Unit) : Unit { + mutable cond = true; + while cond { + g(if cond { break } else { x }); + } + } + } + "}, + &expect![[r#" + [ + UnsupportedType( + "Param<\"'T\": 0>", + Span { + lo: 136, + hi: 164, + }, + ), + ] + "#]], + ); + + expect![[r#" + operation Foo(x : 'T, g : ('T => Unit)) : Unit { + mutable cond = true; + while cond { + let _operand_tmp_31 = g; + let _operand_tmp_35 = if cond { + break + } else { + x + }; + _operand_tmp_31(_operand_tmp_35); + } + } + "#]] + .assert_eq(&package); +} + +#[test] +fn hoist_bare_break_in_call_argument() { + // A bare `break` sitting directly in a call-argument slot is itself the + // escaping control flow, so it is lifted to its own spine temp; the later + // desugar guards the call behind the break flag. Previously this bare + // operand was left in place and the call ran with a placeholder argument. + check( + indoc! {" + namespace Test { + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo(break); + } + } + } + "}, + &expect![[r#" + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + let _operand_tmp_24 = Foo; + let _operand_tmp_28 = break; + _operand_tmp_24(_operand_tmp_28); + } + } + "#]], + ); +} + +#[test] +fn hoist_bare_continue_in_call_argument() { + // A bare `continue` operand is lifted identically to a bare `break`; the + // two are handled uniformly by the operand lift. + check( + indoc! {" + namespace Test { + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo(continue); + } + } + } + "}, + &expect![[r#" + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + let _operand_tmp_24 = Foo; + let _operand_tmp_28 = continue; + _operand_tmp_24(_operand_tmp_28); + } + } + "#]], + ); +} + +#[test] +fn hoist_bare_break_in_assign_value() { + // The value operand of an assignment is lifted, so a bare `break` there is + // exposed at statement position before the assignment. + check( + indoc! {" + namespace Test { + operation Main() : Unit { + mutable x = 0; + mutable cond = true; + while cond { + x = break; + } + } + } + "}, + &expect![[r#" + operation Main() : Unit { + mutable x = 0; + mutable cond = true; + while cond { + let _operand_tmp_22 = break; + x = _operand_tmp_22; + } + } + "#]], + ); +} + +#[test] +fn hoist_bare_break_in_index_operand() { + // The index operand of an array access is lifted, so a bare `break` used as + // an index is exposed at statement position and the access is later guarded. + // The access is consumed by a call so its divergent result type is fixed. + check( + indoc! {" + namespace Test { + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + let arr = [1, 2, 3]; + mutable cond = true; + while cond { + Foo(arr[break]); + } + } + } + "}, + &expect![[r#" + operation Foo(x : Int) : Unit {} + operation Main() : Unit { + let arr = [1, 2, 3]; + mutable cond = true; + while cond { + let _operand_tmp_33 = arr; + let _operand_tmp_37 = break; + Foo(_operand_tmp_33[_operand_tmp_37]); + } + } + "#]], + ); +} + +#[test] +fn hoist_bare_break_in_if_condition() { + // An `if` condition is an unconditional operand site, so a bare `break` in + // the condition is lifted to a spine temp ahead of the `if`. + check( + indoc! {" + namespace Test { + operation Main() : Unit { + mutable cond = true; + while cond { + let y = if break { 1 } else { 2 }; + } + } + } + "}, + &expect![[r#" + operation Main() : Unit { + mutable cond = true; + while cond { + let _operand_tmp_27 = break; + let y = if _operand_tmp_27 { + 1 + } else { + 2 + }; + } + } + "#]], + ); +} + +#[test] +fn hoist_bare_break_in_short_circuit_lhs() { + // The left operand of a short-circuit `or` evaluates unconditionally, so a + // bare `break` there is lifted to a spine temp. + check( + indoc! {" + namespace Test { + operation Main() : Unit { + mutable y = true; + mutable cond = true; + while cond { + let z = break or y; + } + } + } + "}, + &expect![[r#" + operation Main() : Unit { + mutable y = true; + mutable cond = true; + while cond { + let _operand_tmp_24 = break; + let z = _operand_tmp_24 or y; + } + } + "#]], + ); +} + +#[test] +fn hoist_break_in_short_circuit_or_rhs() { + // The right operand of `or` is conditional, so when it buries escaping + // control flow the `BinOp` is reshaped into `if y { true } else { }` + // and the buried `break` reaches a statement boundary inside the else block. + check( + indoc! {" + namespace Test { + operation Foo(x : Int) : Bool { true } + operation Main() : Unit { + mutable y = true; + mutable cond = true; + while cond { + let z = y or Foo(break); + } + } + } + "}, + &expect![[r#" + operation Foo(x : Int) : Bool { + true + } + operation Main() : Unit { + mutable y = true; + mutable cond = true; + while cond { + let z = if y { + true + } else { + let _operand_tmp_41 = Foo; + let _operand_tmp_45 = break; + _operand_tmp_41(_operand_tmp_45) + }; + } + } + "#]], + ); +} + +#[test] +fn hoist_continue_in_short_circuit_and_rhs() { + // The right operand of `and` is conditional, so when it buries escaping + // control flow the `BinOp` is reshaped into `if y { } else { false }` + // and the buried `continue` reaches a statement boundary inside the then + // block. Mirrors the `or` reshape with the branches swapped. + check( + indoc! {" + namespace Test { + operation Foo(x : Int) : Bool { true } + operation Main() : Unit { + mutable y = true; + mutable cond = true; + while cond { + let z = y and Foo(continue); + } + } + } + "}, + &expect![[r#" + operation Foo(x : Int) : Bool { + true + } + operation Main() : Unit { + mutable y = true; + mutable cond = true; + while cond { + let z = if y { + let _operand_tmp_41 = Foo; + let _operand_tmp_45 = continue; + _operand_tmp_41(_operand_tmp_45) + } else { + false + }; + } + } + "#]], + ); +} + +#[test] +fn hoist_break_in_compound_short_circuit_and_assign_rhs() { + // A compound `and=` whose right operand buries a `break` in a bare operand + // position (`Foo(break)`, not a statement-carrying wrapper) is reshaped into + // `if b { b = Foo(break) }`, so the buried `break` reaches a statement + // boundary inside the guarded assignment block instead of running `Foo` with + // a default on the divergence path. The guard preserves the short-circuit: + // the assignment runs only when `b` is already true. + check( + indoc! {" + namespace Test { + operation Foo(x : Int) : Bool { true } + operation Main() : Unit { + mutable b = true; + mutable cond = true; + while cond { + b and= Foo(break); + } + } + } + "}, + &expect![[r#" + operation Foo(x : Int) : Bool { + true + } + operation Main() : Unit { + mutable b = true; + mutable cond = true; + while cond { + if b { + let _operand_tmp_37 = Foo; + let _operand_tmp_41 = break; + b = _operand_tmp_37(_operand_tmp_41); + }; + } + } + "#]], + ); +} + +#[test] +fn hoist_continue_in_compound_short_circuit_or_assign_rhs() { + // A compound `or=` whose right operand buries a `continue` in a bare operand + // position is reshaped into `if not b { b = Foo(continue) }`: the `or` + // short-circuits when `b` is already true, so the assignment, and the buried + // `continue`, runs only when `b` is false. + check( + indoc! {" + namespace Test { + operation Foo(x : Int) : Bool { true } + operation Main() : Unit { + mutable b = false; + mutable cond = true; + while cond { + b or= Foo(continue); + } + } + } + "}, + &expect![[r#" + operation Foo(x : Int) : Bool { + true + } + operation Main() : Unit { + mutable b = false; + mutable cond = true; + while cond { + if not b { + let _operand_tmp_38 = Foo; + let _operand_tmp_42 = continue; + b = _operand_tmp_38(_operand_tmp_42); + }; + } + } + "#]], + ); +} + +#[test] +fn array_back_non_defaultable_let_rhs_break() { + // A `let` binding whose value buries a `break` and whose type has no + // classical default, such as `Pair`, is array-backed: the initializer becomes a + // `Pair[]` temp whose `then` branch is a bare break and whose `else` branch is + // the singleton `[Pair(1, 2)]`, and the binding reads it back through + // `.operand_tmp_[0]`, so no `Pair` default is needed on the divergence path. + check( + indoc! {" + namespace Test { + newtype Pair = (First : Int, Second : Int); + operation Foo(p : Pair) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + let x = if cond { break } else { Pair(1, 2) }; + Foo(x); + } + } + } + "}, + &expect![[r#" + // newtype Pair + operation Foo(p : Pair) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + let _operand_tmp_42 = if cond { + break + } else { + [Pair(1, 2)] + }; + let x = _operand_tmp_42[0]; + Foo(x); + } + } + "#]], + ); +} + +#[test] +fn array_back_discarded_value_block_break() { + // A non-Unit block used as a statement with its result discarded, written + // `{ … };`, whose value buries a `break` and has no classical default such as + // `Pair`, is array-backed in place: the block's value type becomes `Pair[]`, + // with its `then` branch a bare break and its `else` branch the singleton + // `[Pair(1, 2)]`, so the buried break desugars with the universal `[]` default + // instead of a `Pair` default. The value stays discarded; no temp binding is + // introduced. + check( + indoc! {" + namespace Test { + newtype Pair = (First : Int, Second : Int); + operation Main() : Unit { + mutable cond = true; + while cond { + { if cond { break } else { Pair(1, 2) } }; + } + } + } + "}, + &expect![[r#" + // newtype Pair + operation Main() : Unit { + mutable cond = true; + while cond { + { + if cond { + break + } else { + [Pair(1, 2)] + } + }; + } + } + "#]], + ); +} + +#[test] +fn hoist_break_in_return_operand_block() { + // A `return` operand may bury an escaping `break`; the operand is lifted to + // a temp while the `return` node stays in place, so the buried `break` is + // exposed without hoisting the `return` itself. + check( + indoc! {" + namespace Test { + operation Main() : Int { + mutable cond = true; + while cond { + return { if cond { break }; 5 }; + } + 0 + } + } + "}, + &expect![[r#" + operation Main() : Int { + mutable cond = true; + while cond { + let _operand_tmp_29 = { + if cond { + break + }; + 5 + }; + return _operand_tmp_29; + } + 0 + } + "#]], + ); +} + +#[test] +fn hoist_break_in_for_iterable_nested_in_outer_loop() { + // A `for` iterable is evaluated once in the enclosing loop scope. A `break` + // buried in a compound iterable binds to the outer `while`, so the iterable + // is lifted to a spine temp ahead of the `for` and the buried `break` is + // exposed without hoisting the `for` itself. + check( + indoc! {" + namespace Test { + function F(a : Int[], b : Int) : Int[] { a } + operation G() : Int { 0 } + operation Main() : Unit { + let arr = [1, 2, 3]; + mutable cond = true; + while cond { + for j in F({ if cond { break }; arr }, G()) { + let k = j; + } + } + } + } + "}, + &expect![[r#" + function F(a : Int[], b : Int) : Int[] { + a + } + operation G() : Int { + 0 + } + operation Main() : Unit { + let arr = [1, 2, 3]; + mutable cond = true; + while cond { + let _operand_tmp_65 = { + if cond { + break + }; + arr + }; + for j in F(_operand_tmp_65, G()) { + let k = j; + } + } + } + "#]], + ); +} + +#[test] +fn hoist_break_in_core_udt_operand_block_array_backed() { + // A user-defined type defined in another package (`Complex`, from the core + // library) is array-backed just like a local newtype: array-backing needs + // only the universal `[]` default of `Complex[]`, never a default of the + // user-defined type itself, so the operand is representable regardless of + // which package defines it. This is the cross-package companion to + // `hoist_break_in_udt_operand_block_array_backed`. + check( + indoc! {" + namespace Test { + operation Foo(c : Complex) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + Foo(if cond { break } else { Complex(1.0, 2.0) }); + } + } + } + "}, + &expect![[r#" + operation Foo(c : Complex) : Unit {} + operation Main() : Unit { + mutable cond = true; + while cond { + let _operand_tmp_37 = Foo; + let _operand_tmp_41 = if cond { + break + } else { + [Complex(1., 2.)] + }; + _operand_tmp_37(_operand_tmp_41[0]); + } + } + "#]], + ); +} diff --git a/source/compiler/qsc_passes/src/loop_unification.rs b/source/compiler/qsc_passes/src/loop_unification.rs index 990c821c1f2..3ef8cfcd805 100644 --- a/source/compiler/qsc_passes/src/loop_unification.rs +++ b/source/compiler/qsc_passes/src/loop_unification.rs @@ -3,27 +3,89 @@ use std::mem::take; +use miette::Diagnostic; +use num_bigint::BigInt; use qsc_data_structures::span::Span; use qsc_hir::{ assigner::Assigner, global::Table, - hir::{BinOp, Block, Expr, ExprKind, Lit, Mutability, Pat, PrimField, Stmt, StmtKind, UnOp}, + hir::{ + BinOp, Block, Expr, ExprKind, Lit, Mutability, Package, Pat, Pauli, PrimField, Result, + Stmt, StmtKind, UnOp, + }, mut_visit::{MutVisitor, walk_expr}, ty::{GenericArg, Prim, Ty}, + visit::{self, Visitor}, }; +use thiserror::Error; use crate::CORE_NAMESPACE; -use crate::common::{IdentTemplate, create_gen_core_ref, gen_ident}; +use crate::common::{IdentTemplate, LoopDepthScan, create_gen_core_ref, gen_ident}; #[cfg(test)] mod tests; +#[cfg(test)] +mod test_utils; + +#[cfg(test)] +mod break_continue_tests; + +#[derive(Clone, Debug, Diagnostic, Error)] +pub enum Error { + #[error( + "cannot desugar `break`/`continue` nested in a value of type `{0}` that has no classical default" + )] + #[diagnostic(code("Qsc.LoopUnification.UnsupportedBreakContinueType"))] + #[diagnostic(help( + "on the branch that breaks or continues, this value is never produced, but its type has \ + no classical default to stand in for it while the surrounding effects are skipped. \ + Operand, `let`-binding, short-circuit-operand, and discarded value-block positions are \ + all supported; if you reach this, bind the value with a `let` or restructure so it is \ + not produced on the branch that breaks or continues" + ))] + UnsupportedType( + String, + #[label("break/continue in a value with no classical default")] Span, + ), + + #[error("internal error: `break`/`continue` was not eliminated by loop desugaring")] + #[diagnostic(code("Qsc.LoopUnification.ResidualBreakContinue"))] + #[diagnostic(help( + "this indicates a compiler invariant was violated; every `break`/`continue` should be \ + rewritten to loop-flag writes during desugaring" + ))] + ResidualBreakContinue(#[label("`break`/`continue` survived loop desugaring")] Span), +} + pub(crate) struct LoopUni<'a> { pub(crate) core: &'a Table, pub(crate) assigner: &'a mut Assigner, + pub(crate) errors: Vec, } impl LoopUni<'_> { + /// Rewrites `repeat`/`until` into a `while` driven by `.continue_cond_`. + /// + /// # Before + /// ```text + /// repeat { body } until cond fixup { fixup } + /// ``` + /// + /// # After + /// ```text + /// mutable .continue_cond_ = true; + /// while .continue_cond_ { + /// body + /// .continue_cond_ = not cond; + /// if .continue_cond_ { fixup } + /// } + /// ``` + /// + /// If `body` contains `break`, the condition and tail update are also + /// guarded by the `not .broke_` flag; if it contains `continue`, the tail still runs + /// before the next condition check. + #[allow(clippy::too_many_lines)] fn visit_repeat( &mut self, mut block: Block, @@ -49,6 +111,8 @@ impl LoopUni<'_> { self.assigner, ); + let flags = self.desugar_loop_body(&mut block); + let update = Stmt { id: self.assigner.next_node(), span: cond_span, @@ -67,10 +131,9 @@ impl LoopUni<'_> { ), }), }; - block.stmts.push(update); - if let Some(fix_body) = fixup { - let fix_if = Stmt { + let fix_if = if let Some(fix_body) = fixup { + Some(Stmt { id: self.assigner.next_node(), span: fix_body.span, kind: StmtKind::Expr(Expr { @@ -88,30 +151,93 @@ impl LoopUni<'_> { None, ), }), - }; - block.stmts.push(fix_if); - } + }) + } else { + None + }; - let new_block = Block { - id: self.assigner.next_node(), - span, - ty: Ty::UNIT, - stmts: vec![ - continue_cond_init, - Stmt { + let new_block = match flags { + None => { + block.stmts.push(update); + if let Some(fix_if) = fix_if { + block.stmts.push(fix_if); + } + Block { + id: self.assigner.next_node(), + span, + ty: Ty::UNIT, + stmts: vec![ + continue_cond_init, + Stmt { + id: self.assigner.next_node(), + span: Span::default(), + kind: StmtKind::Expr(Expr { + id: self.assigner.next_node(), + span, + ty: Ty::UNIT, + kind: ExprKind::While( + Box::new(continue_cond_id.gen_local_ref(self.assigner)), + block, + ), + }), + }, + ], + } + } + Some(flags) => { + // `.cont_` resets each iteration; declare it as the first body statement. + if let Some(cont_decl) = flags.cont_decl { + block.stmts.insert(0, cont_decl); + } + // The tail, the `until`/`fixup` step, runs on `continue`; when a + // `break` is present it is skipped on `break` under + // `if not .broke_ { ... }`, otherwise it runs unconditionally. + if let Some(broke) = &flags.broke { + let mut tail_stmts = vec![update]; + if let Some(fix_if) = fix_if { + tail_stmts.push(fix_if); + } + let tail_block = Block { + id: self.assigner.next_node(), + span: Span::default(), + ty: Ty::UNIT, + stmts: tail_stmts, + }; + let tail_if = self.gen_broke_guarded_block(broke, tail_block); + block.stmts.push(tail_if); + } else { + block.stmts.push(update); + if let Some(fix_if) = fix_if { + block.stmts.push(fix_if); + } + } + let cc_ref = continue_cond_id.gen_local_ref(self.assigner); + let while_cond = match &flags.broke { + Some(broke) => self.gen_broke_guard_cond(broke, cc_ref), + None => cc_ref, + }; + let while_stmt = Stmt { id: self.assigner.next_node(), span: Span::default(), kind: StmtKind::Expr(Expr { id: self.assigner.next_node(), span, ty: Ty::UNIT, - kind: ExprKind::While( - Box::new(continue_cond_id.gen_local_ref(self.assigner)), - block, - ), + kind: ExprKind::While(Box::new(while_cond), block), }), - }, - ], + }; + let mut stmts = vec![continue_cond_init]; + if let Some(broke_decl) = flags.broke_decl { + stmts.push(broke_decl); + } + stmts.push(while_stmt); + Block { + id: self.assigner.next_node(), + span, + ty: Ty::UNIT, + stmts, + } + } }; Expr { @@ -122,6 +248,28 @@ impl LoopUni<'_> { } } + /// Rewrites an array `for` loop to an index-driven `while` loop. + /// + /// # Before + /// ```text + /// for pat in array { body } + /// ``` + /// + /// # After + /// ```text + /// let .array_id_ = array; + /// let .len_id_ = Length(.array_id_); + /// mutable .index_id_ = 0; + /// while .index_id_ < .len_id_ { + /// let pat = .array_id_[.index_id_]; + /// body + /// .index_id_ += 1; + /// } + /// ``` + /// + /// A `break` adds a persistent `.broke_` flag that guards the condition and + /// step; a `continue` adds a per-iteration `.cont_` flag that skips the rest + /// of the body but still runs the step. #[allow(clippy::too_many_lines)] fn visit_for_array( &mut self, @@ -132,6 +280,8 @@ impl LoopUni<'_> { ) -> Expr { let iterable_span = iterable.span; + let flags = self.desugar_loop_body(&mut block); + let array_id = gen_ident( self.assigner, "array_id", @@ -217,9 +367,6 @@ impl LoopUni<'_> { }; let update_index = gen_id_add_update(self.assigner, &index_id, update_expr); - block.stmts.insert(0, pat_init); - block.stmts.push(update_index); - let cond = Expr { id: self.assigner.next_node(), span: iterable_span, @@ -231,15 +378,55 @@ impl LoopUni<'_> { ), }; - let while_stmt = Stmt { - id: self.assigner.next_node(), - span: Span::default(), - kind: StmtKind::Expr(Expr { - id: self.assigner.next_node(), - span, - ty: Ty::UNIT, - kind: ExprKind::While(Box::new(cond), block), - }), + let new_block_stmts = match flags { + None => { + block.stmts.insert(0, pat_init); + block.stmts.push(update_index); + let while_stmt = Stmt { + id: self.assigner.next_node(), + span: Span::default(), + kind: StmtKind::Expr(Expr { + id: self.assigner.next_node(), + span, + ty: Ty::UNIT, + kind: ExprKind::While(Box::new(cond), block), + }), + }; + vec![array_capture, len_capture, index_init, while_stmt] + } + Some(flags) => { + // `.cont_` resets each iteration; declare it before the loop variable. + block.stmts.insert(0, pat_init); + if let Some(cont_decl) = flags.cont_decl { + block.stmts.insert(0, cont_decl); + } + // The index step runs on `continue` but is skipped on `break`. + let step_stmt = match &flags.broke { + Some(broke) => self.gen_broke_guarded_step(broke, update_index), + None => update_index, + }; + block.stmts.push(step_stmt); + let while_cond = match &flags.broke { + Some(broke) => self.gen_broke_guard_cond(broke, cond), + None => cond, + }; + let while_stmt = Stmt { + id: self.assigner.next_node(), + span: Span::default(), + kind: StmtKind::Expr(Expr { + id: self.assigner.next_node(), + span, + ty: Ty::UNIT, + kind: ExprKind::While(Box::new(while_cond), block), + }), + }; + let mut stmts = vec![array_capture, len_capture, index_init]; + if let Some(broke_decl) = flags.broke_decl { + stmts.push(broke_decl); + } + stmts.push(while_stmt); + stmts + } }; Expr { @@ -250,11 +437,33 @@ impl LoopUni<'_> { id: self.assigner.next_node(), span, ty: Ty::UNIT, - stmts: vec![array_capture, len_capture, index_init, while_stmt], + stmts: new_block_stmts, }), } } + /// Rewrites a range `for` loop to an index-driven `while` loop. + /// + /// # Before + /// ```text + /// for pat in start..step..end { body } + /// ``` + /// + /// # After + /// ```text + /// let .range_id_ = start..step..end; + /// mutable .index_id_ = .range_id_::Start; + /// let .step_id_ = .range_id_::Step; + /// let .end_id_ = .range_id_::End; + /// while range_cond(.index_id_, .step_id_, .end_id_) { + /// let pat = .index_id_; + /// body + /// .index_id_ += .step_id_; + /// } + /// ``` + /// + /// Uses the same `.broke_`/`.cont_` guards as array loops. + #[allow(clippy::too_many_lines)] fn visit_for_range( &mut self, iter: Pat, @@ -264,6 +473,8 @@ impl LoopUni<'_> { ) -> Expr { let iterable_span = iterable.span; + let flags = self.desugar_loop_body(&mut block); + let range_id = gen_ident( self.assigner, "range_id", @@ -311,20 +522,57 @@ impl LoopUni<'_> { let update_expr = step_id.gen_local_ref(self.assigner); let update_index = gen_id_add_update(self.assigner, &index_id, update_expr); - block.stmts.insert(0, pat_init); - block.stmts.push(update_index); - let cond = gen_range_cond(self.assigner, &index_id, &step_id, &end_id, iterable_span); - let while_stmt = Stmt { - id: self.assigner.next_node(), - span: Span::default(), - kind: StmtKind::Expr(Expr { - id: self.assigner.next_node(), - span, - ty: Ty::UNIT, - kind: ExprKind::While(Box::new(cond), block), - }), + let new_block_stmts = match flags { + None => { + block.stmts.insert(0, pat_init); + block.stmts.push(update_index); + let while_stmt = Stmt { + id: self.assigner.next_node(), + span: Span::default(), + kind: StmtKind::Expr(Expr { + id: self.assigner.next_node(), + span, + ty: Ty::UNIT, + kind: ExprKind::While(Box::new(cond), block), + }), + }; + vec![range_capture, index_init, step_init, end_init, while_stmt] + } + Some(flags) => { + // `.cont_` resets each iteration; declare it before the loop variable. + block.stmts.insert(0, pat_init); + if let Some(cont_decl) = flags.cont_decl { + block.stmts.insert(0, cont_decl); + } + // The index step runs on `continue` but is skipped on `break`. + let step_stmt = match &flags.broke { + Some(broke) => self.gen_broke_guarded_step(broke, update_index), + None => update_index, + }; + block.stmts.push(step_stmt); + let while_cond = match &flags.broke { + Some(broke) => self.gen_broke_guard_cond(broke, cond), + None => cond, + }; + let while_stmt = Stmt { + id: self.assigner.next_node(), + span: Span::default(), + kind: StmtKind::Expr(Expr { + id: self.assigner.next_node(), + span, + ty: Ty::UNIT, + kind: ExprKind::While(Box::new(while_cond), block), + }), + }; + let mut stmts = vec![range_capture, index_init, step_init, end_init]; + if let Some(broke_decl) = flags.broke_decl { + stmts.push(broke_decl); + } + stmts.push(while_stmt); + stmts + } }; Expr { @@ -335,10 +583,201 @@ impl LoopUni<'_> { id: self.assigner.next_node(), span, ty: Ty::UNIT, - stmts: vec![range_capture, index_init, step_init, end_init, while_stmt], + stmts: new_block_stmts, }), } } + + /// Rebuilds a plain `while` whose body contains `break`/`continue`. + /// + /// # Before + /// ```text + /// while cond { body } + /// ``` + /// + /// # After + /// ```text + /// mutable .broke_ = false; + /// while not .broke_ and cond { body' } + /// ``` + /// + /// `body'` is the body after [`desugar_loop_body`]. Continue-only loops do + /// not need `.broke_`, so they stay as `while cond { body' }`; a plain + /// `while` has no loop step, so `continue` falls through to the next + /// condition check. + fn visit_while(&mut self, cond: Box, mut block: Block, span: Span) -> Expr { + let flags = self + .desugar_loop_body(&mut block) + .expect("while body should contain break/continue when rebuilt"); + if let Some(cont_decl) = flags.cont_decl { + block.stmts.insert(0, cont_decl); + } + let while_cond = match &flags.broke { + Some(broke) => self.gen_broke_guard_cond(broke, *cond), + None => *cond, + }; + match flags.broke_decl { + Some(broke_decl) => { + let while_stmt = Stmt { + id: self.assigner.next_node(), + span: Span::default(), + kind: StmtKind::Expr(Expr { + id: self.assigner.next_node(), + span, + ty: Ty::UNIT, + kind: ExprKind::While(Box::new(while_cond), block), + }), + }; + Expr { + id: self.assigner.next_node(), + span, + ty: Ty::UNIT, + kind: ExprKind::Block(Block { + id: self.assigner.next_node(), + span, + ty: Ty::UNIT, + stmts: vec![broke_decl, while_stmt], + }), + } + } + None => Expr { + id: self.assigner.next_node(), + span, + ty: Ty::UNIT, + kind: ExprKind::While(Box::new(while_cond), block), + }, + } + } + + /// Detects `break`/`continue` binding to this loop, creates only the needed + /// `.broke_`/`.cont_` flags, and rewrites the body in place. + /// + /// # Before + /// ```text + /// break; + /// stmt; + /// continue; + /// next; + /// ``` + /// + /// # After + /// ```text + /// .broke_ = true; + /// if not .broke_ { stmt; } + /// if not .broke_ { .cont_ = true; } + /// if not .broke_ and not .cont_ { next; } + /// ``` + /// + /// # Mutations + /// - Rewrites bare `break`/`continue` statements to flag assignments. + /// - Guards statements that follow a loop-control statement. + /// - Allocates only the flags required by the body. + fn desugar_loop_body(&mut self, body: &mut Block) -> Option { + let presence = body_break_continue(body); + if !presence.any() { + return None; + } + let (broke, broke_decl) = if presence.has_break { + let (ident, decl) = self.gen_flag_decl("broke"); + (Some(ident), Some(decl)) + } else { + (None, None) + }; + let (cont, cont_decl) = if presence.has_continue { + let (ident, decl) = self.gen_flag_decl("cont"); + (Some(ident), Some(decl)) + } else { + (None, None) + }; + let mut desugar_errors = { + let mut desugar = BreakContinueDesugar { + assigner: self.assigner, + broke: broke.as_ref(), + cont: cont.as_ref(), + errors: Vec::new(), + }; + desugar.visit_block(body); + desugar.errors + }; + self.errors.append(&mut desugar_errors); + Some(LoopFlags { + broke, + broke_decl, + cont_decl, + }) + } + + /// Creates a `mutable .