Fix HUGR stall when lazy measurement values cross CFG branch transitions (#467) - #468
Conversation
…urvive branch crossings
…ugr-branch-carry-stall
…ugr-branch-carry-stall
|
Independent review pass complete on this PR's delta (the replay-selection change, reviewed No findings. The pass targeted the one failure mode the fix's tests could not rule out by three-level nested conditionals (both directions exercised) / measurement-dependent loop count / All computed results matched their hand-derived expectations exactly; all runs deterministic; With this, the stack is review-complete: this PR merges into |
* Defer Call return publication until measurement outcomes resolve, fixing the HUGR stall on measured values crossing Call boundaries * Replay all retained CFG transition edges so lazy measurement values survive branch crossings (#468)
Summary
Closes #467. A lazy measurement outcome that crossed a CFG branch transition before arriving
stalled execution -- the branch-transition sibling of #466's Call-boundary fix, found by that
PR's independent review pass and confirmed pre-existing on dev (all four dev/branch x
call/no-call combinations stalled identically).
Stacked on #466 (
hugr-call-deferred-stall): both touch the replay machinery. Merge #466first; this PR then retargets dev.
Root cause (trace-confirmed)
Cross-block measurement replay kept only each CFG's newest transition cascade. But cascade IDs
count invocations of the transition routine, not value generations -- consecutive live blocks
get increasing IDs just like loop iterations do. An outcome that had to walk an earlier edge
before the newest one found its intermediate port never filled, and the consumer starved.
The generation distinction the filter was trying to enforce already exists in the data:
re-entering a block purges prior edges into that target at recording time. Superseded loop
edges are gone by construction; live acyclic edges remain.
The fix
Delete the newest-cascade filter; replay every retained edge of active CFGs in insertion
order, fill-only. The purge-at-recording invariant is now documented where the filter used to
be. No special cases, no retry counters; the stall detector is untouched.
Also answered en route: plain
measurenever hit this because it ends the dispatch batch atemission, while
measure_and_resetrecords a late output wire and lets dispatch run aheadthrough subsequent blocks -- the difference is batch scheduling, not measurement semantics.
Tests
Five value-asserting regressions (results checked, not merely absence of stall): no-call
branch carry, called variant, measurement-dependent dynamic branch, two consecutive branch
crossings, and a TailLoop that re-measures per iteration asserting the consumer sees the LAST
generation's value -- the case that would fail if replay-all resurrected stale generations.
Mutation (filter restored): exactly the five new tests fail; the same-block and Call-boundary
controls pass.
Verification
clippy
-D warningsclean; fmt clean; pecos-hugr 85 + 6 doctests; guppy 496; pecos 2130;qec 1147; doc blocks 9/10 (the #427 acceptance) still pass; pre-commit two passes exit 0.
#466's four regressions and its minimal repro all green on this branch.