Severity: Low (latent — no live repro found; surfaced by a /code-review pass on PR #2922)
Summary
mark_nonretryable_escape's own contract, stated on is_retryable_stop, is that a driver
which ends a drive because something escaped — a decode failure, a Halt from the stage it
piped into — must stash that escape and mark the stop non-retryable, because everything
between it and a ?// alternative sees only Flow::Stopped { pending: None } and would
otherwise re-run the alternative.
Two escape-stashing sinks in the path resolvers answer Demand::Stop without that call:
Both park an EvalEscape beside the drive and return Demand::Stop, which is exactly the
escape/downstream/ended idiom the contract is written for.
Repro
None found. A review pass that fuzzed ~9,000 shapes (including ?// retries, halt_error in
key/bound streams, and label/break interaction) produced no observable divergence, so this is
reported as a contract violation rather than a confirmed bug. Whether it is reachable at all
depends on whether a ?// alternative can sit above these resolvers with a Halt or decode
failure in the key/bound generator — that is the first thing to establish.
Root cause
src/jq/eval.rs — the two escape! macros expand to target_escape = Some($control); return Demand::Stop; with no mark_nonretryable_escape. stop_with_escape/stop_with_downstream/
stop_with_escape_cell are the helpers that do it correctly; neither resolver routes through
them, because their slot is an Option<EvalEscape> rather than the Option<Control> those
helpers take.
Why this is separate from #2267
#2267 is about ordering — which generator is pulled when. This is about the retry
classification of a stop, is pre-existing on one of the two sites, and needs its own oracle
work (a ?//-over-a-failing-resolver capture from jq 1.7.1) before anything is changed.
Fixing it speculatively inside an ordering PR would change ?// retry semantics with no
repro to pin it.
Suggested fix direction
Either give the two resolvers an EvalEscape-shaped stop_with_escape sibling (one
definition, so the two cannot drift — the #106 lesson these resolvers have already been
bitten by twice), or convert their slots to Option<Control> and use the existing helper.
Then pin with a ?// alternative over a halt_error in a computed key and in a slice
bound, captured live from jq 1.7.1 first.
Refs #2267, #2920, #2922, #1519.
Severity: Low (latent — no live repro found; surfaced by a
/code-reviewpass on PR #2922)Summary
mark_nonretryable_escape's own contract, stated onis_retryable_stop, is that a driverwhich ends a drive because something escaped — a decode failure, a
Haltfrom the stage itpiped into — must stash that escape and mark the stop non-retryable, because everything
between it and a
?//alternative sees onlyFlow::Stopped { pending: None }and wouldotherwise re-run the alternative.
Two escape-stashing sinks in the path resolvers answer
Demand::Stopwithout that call:resolve_slice_expr'sresolve_pairclosure (src/jq/eval.rs), via itsescape!macro —pre-existing, introduced with fix(jq): interleave a computed slice's bounds with its target, as jq does #2920's interleaving and present on
maintoday.resolve_index_expr's key sink, via its ownescape!macro — the sibling added by PR fix(jq): interleave a computed index's key with its target, as jq does #2922.Both park an
EvalEscapebeside the drive and returnDemand::Stop, which is exactly theescape/downstream/endedidiom the contract is written for.Repro
None found. A review pass that fuzzed ~9,000 shapes (including
?//retries,halt_errorinkey/bound streams, and label/
breakinteraction) produced no observable divergence, so this isreported as a contract violation rather than a confirmed bug. Whether it is reachable at all
depends on whether a
?//alternative can sit above these resolvers with aHaltor decodefailure in the key/bound generator — that is the first thing to establish.
Root cause
src/jq/eval.rs— the twoescape!macros expand totarget_escape = Some($control); return Demand::Stop;with nomark_nonretryable_escape.stop_with_escape/stop_with_downstream/stop_with_escape_cellare the helpers that do it correctly; neither resolver routes throughthem, because their slot is an
Option<EvalEscape>rather than theOption<Control>thosehelpers take.
Why this is separate from #2267
#2267 is about ordering — which generator is pulled when. This is about the retry
classification of a stop, is pre-existing on one of the two sites, and needs its own oracle
work (a
?//-over-a-failing-resolver capture from jq 1.7.1) before anything is changed.Fixing it speculatively inside an ordering PR would change
?//retry semantics with norepro to pin it.
Suggested fix direction
Either give the two resolvers an
EvalEscape-shapedstop_with_escapesibling (onedefinition, so the two cannot drift — the #106 lesson these resolvers have already been
bitten by twice), or convert their slots to
Option<Control>and use the existing helper.Then pin with a
?//alternative over ahalt_errorin a computed key and in a slicebound, captured live from jq 1.7.1 first.
Refs #2267, #2920, #2922, #1519.