Nested control-flow macros can capture exits meant for user loops
perfloop-oss/cpp2rust · UNCATALOGUED CORRECTNESS
https://perfloop.ai/t/oss/case_kcaa4pww7b
Verdict
VERIFIED · settled 2026-11-02 · merged as Cpp2Rust/cpp2rust#581
What happened: The assertion is violated on the comparison and satisfied with this change.
Hypothesis
Nested control-flow macros can send a user exit to an implementation loop instead of its intended target. A break can leave only a generated state machine and allow the user loop to continue. The documentation promises transparent loop exits; affected application prevalence remains unknown.
PropagateRewriter::visit_expr_mut rewrites visible unlabelled break/continue expressions, but its ordinary syn traversal does not inspect nested macro token bodies. The outer emitter therefore cannot propagate an exit that a nested macro emits later. That nested macro's unlabelled bailout can target the outer generated loop. Actual user loops must still stop propagation, while switch must retain its own break target.
The checkProperty run for prop_680d5f9srr at e2b679de9fa36c3b015b4604759ab0ff53003fa4 completed all 300 bounded public-API/reference-model cases and reported 45 trace mismatches. The grammar covers every goto/switch wrapper word of depth 0..3, five exit actions and four trigger positions in a three-iteration user loop. Depth-zero/one controls passed. An eight-visit guard makes erroneous repeated continues finite; this is not an observed unbounded runtime or production measurement.
The stored six-byte reduced replay, `3 0 1`, selects two nested goto blocks and a break on user iteration zero. The subject continues through user iterations one and two; the model exits immediately. A subsequent checkProperty run using `cargo test --manifest-path libcc2rs-macros/Cargo.toml --test perfloop_goto_targets -- --nocapture --test-threads=1` reproduced that same trace mismatch from the saved replay before enumeration. An earlier fixture-parsing failure had no semantic coverage and was repaired.
This is a supported public-macro contract in docs/src/runtime/control-flow.md: goto blocks are transparent to enclosing-loop exits, switch owns break but not continue, and the macros mix freely. Converter::VisitBreakStmt emits unlabelled loop breaks, so nested translated labelled compounds are a source-supported route. Converter::VisitContinueStmt emits a labelled continue; no translated-continue mismatch is established here. The smallest useful Case check is the reduced native trace followed by the bounded grammar, with an end-to-end nested-compound C comparison to confirm translator exposure. Correct target-equivalent traces would confirm repair; disagreement with a valid native reference would refute it.
The refreshed open-PR page and actual implementations inspected at https://github.com/Cpp2Rust/cpp2rust/pull/561 and https://github.com/Cpp2Rust/cpp2rust/pull/579 concern rule matching and descriptor/stream ownership. Neither implements nested control-target propagation. Specific prior searches for PropagateRewriter and nested continue repair found no overlap.
Change to test: Preserve the original break and continue targets across supported nesting of goto_block! and switch!, propagating exits past generated state-machine loops without bypassing real user loops or switch break targets. Keep explicitly labelled exits unchanged.
Where it lives
perfloop-oss/cpp2rust · libcc2rs-macros/src/lib.rs
Evidence
The assertion is violated on the comparison and satisfied with this change: `For all 300 combinations of goto_block!/switch! wrapper words of depth zero through three, no exit or unlabelled/explicitly labelled break/continue, and four trigger positions in a three-iteration user loop, the public macros produce the reference target-equivalent traces: goto blocks are transparent to loop exits and switch owns unlabelled break, not continue.`
Checks: 5 of 5 passed. Verification: no defect found.