Skip to content

Fix HUGR stall when measured values cross Call boundaries (#427) - #466

Merged
ciaranra merged 6 commits into
devfrom
hugr-call-deferred-stall
Aug 10, 2026
Merged

Fix HUGR stall when measured values cross Call boundaries (#427)#466
ciaranra merged 6 commits into
devfrom
hugr-call-deferred-stall

Conversation

@ciaranra

@ciaranra ciaranra commented Aug 9, 2026

Copy link
Copy Markdown
Member

Summary

Closes #427. Guppy programs stalled with "HUGR execution stalled before completion ... starved
deferred nodes" whenever a measurement-derived value was returned from a called function and
then reported via result(...). This hit the transversal-CNOT documentation examples (the
original report, test_qec_guppy blocks 9 and 10) and reduces to a 14-line program.

Root cause (trace-proven)

Measurement outcomes are intentionally lazy: the gate is emitted, the node is marked processed,
and the outcome arrives later when the quantum batch flushes. The callee's CFG therefore
completes before the outcome exists. Call completion then performed a one-time snapshot of
the callee's return values -- copying None -- and removed the Call from active_calls. When
the outcome later arrived it populated the measurement node's wire, but nothing forwarded it to
the Call's already-published empty output, so the caller's bool.read deferred forever against
an empty wire. Same-block programs never stalled because the consumer reads the measurement's
own wire, which measurement handling populates directly.

Diagnosis was performed first, with instrumentation, and contradicted the initial hypothesis
(the callee CFG was believed still active; the trace showed it had completed) -- the fix was
designed against the traced mechanism, not the guess.

The fix

Invariant: a Call may not be marked processed or leave active_calls while any of its return
ports is unresolved.

  • When the callee CFG completes with an unresolved return, the Call parks in a
    pending_call_returns set, retaining the propagation context to replay once outcomes arrive.
    Only the return-copy-and-mark-processed phase defers -- the CFG's own completion bookkeeping
    still runs, so the quantum batch that produces the outcome still flushes.
  • Measurement handling retries pending Call returns, and all returns publish atomically before
    the Call is released.
  • The stall detector is untouched: it remains the guard that turned silent truncation into an
    error, and now never fires for these programs.
  • No forwarding links, no bool.read special cases, no retry counters or timeouts.

Tests

New test_call_measurement_returns.py: the minimal Call-boundary case, the same-block control,
a two-level nested Call chain, and the struct/helper/array integration shape. Mutation check:
removing the post-measurement retry fails exactly the three Call-crossing tests while the
same-block control passes -- the wake-up path is what the tests pin.

Acceptance: test_qec_guppy blocks 9 and 10 -- failing on dev since the issue was filed --
pass. Verified independently of the implementation run.

Verification

clippy -D warnings clean; fmt clean; cargo test -p pecos-hugr 85 + 6 doctests; guppy suite
491 passed; pecos suite 2130 passed; qec suite 1147 passed; doc generation clean; pre-commit two
passes exit 0.

Closes #467 (delivered by #468, which merges into this branch; GitHub only auto-closes from PRs merging to the default branch).

…ing the HUGR stall on measured values crossing Call boundaries
@ciaranra

ciaranra commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

Review-status note for the record: an independent correctness-review pass was dispatched over
this change (pending-set lifecycle under repeated and nested calls, stall-detector preservation,
the arity change vs a dev-built extension, publication ordering for mixed measured/classical
returns, and a runtime smoke check). The run was interrupted and did not produce a report, so
this PR has NOT had its independent review pass -- unlike #462, which went through three rounds.

What the PR does carry: a trace-proven diagnosis, four regression shapes (minimal, same-block
control, nested two-level, struct/array integration), a mutation check isolating the
post-measurement wake-up path, the blocks-9/10 acceptance run, and full green verification
(pecos-hugr, guppy, pecos, qec suites; clippy; two-pass lint). Reviewer attention is best spent
on the pending_call_returns lifecycle (reset between shots, multiple simultaneous pending calls)
and the analysis.rs arity change, which the interrupted pass would have stress-tested.

@ciaranra

ciaranra commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

The independent review pass has now completed (re-run after the earlier interruption). Verdict:

One High finding -- and it is pre-existing on dev, not a regression from this PR. A lazy
measurement value crossing a CFG branch transition stalls, the sibling of the Call-boundary
defect this PR fixes. Verified by execution in all four combinations (dev / this branch, with /
without an intervening call): all stall identically. Filed as #467 with the reproduction and the
review's suspected locus (cfg.rs latest-cascade replay selection).

Everything else clean, by executed stress cases: two simultaneous pending Calls resolve to
the correct sites; repeated Calls across TailLoop iterations return the current iteration's
value; taken/untaken conditional branches behave; nested pending Calls resolve; 50-shot runs are
deterministic under a fixed seed across repeated runs. The stall detector still fires promptly
for genuinely unresolvable programs with useful diagnostics. The arity change was exercised over
zero/multi/qubit/tuple return shapes against a dev-built extension with no divergence.
Publication ordering: classical-first and measured-first consumers agree. Runtime scaling is
linear in Call count (6/20/42 calls per shot: 0.13/0.45/1.04s vs dev 0.12/0.36/0.90s -- a small
constant overhead, no super-linear growth).

This supersedes the earlier "review pass incomplete" note. The PR is review-complete from the
automation side; #467 is the follow-on work.

@ciaranra
ciaranra merged commit 49c1c0e into dev Aug 10, 2026
61 checks passed
@ciaranra
ciaranra deleted the hugr-call-deferred-stall branch August 10, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant