Summary
try_walker_inline_user_compareop discards an inline sub-walk result with a bare
Err(DispatchError::callee_inline_unsupported(..)). The error is not latched, so the walk
driver rolls back and replays the loop from entry — re-running a rich-compare dunder whose
body already applied its effects.
This is on main; it is not introduced by any open branch. It was found while adjudicating
the same defect in a sibling route (try_walker_inline_user_contains, PR #1405), which was
withdrawn for this reason.
Reproduction
class R:
def __gt__(self, other): return True
class L:
def __init__(self): self.n = 0
def __lt__(self, other):
self.n = self.n + 1 # observable effect
return NotImplemented # forces the route's Err path
l = L(); r = R()
N = 3000
hits = 0
i = 0
while i < N:
if l < r:
hits += 1
i += 1
print("iterations", N, "body_runs", l.n, "hits", hits)
|
body_runs |
| CPython |
3000 |
| pyre, JIT off |
3000 |
| pyre dynasm |
3002 |
The answers stay correct — only the effect count is wrong.
The caller's loop shape matters. Under a for loop the FOR_ITER admission gate
(inline_call.rs, the CalleeReplaySafety::Dirty decline) rejects such bodies before the
route is reached, so a for-driven repro reads 3000 and looks like a refutation. Only
while reproduces.
Root cause
latch_abort_call_resume is the mechanism that turns a discarded sub-walk into a forward
resume point, and its own doc comment states the rule:
Every caller that discards a sub-walk result must go through this — returning the error
without latching leaves the driver replaying the loop, which repeats the callee's effects.
Two things make it unavailable here:
- It delegates to
reconstructed_all_ref_call_stack, which returns None unless
pyre_helper is CallFn | CallFunctionEx. A rich-compare route arrives with a
CompareOp descriptor, so even an explicit latch call is a no-op.
- It refuses to latch when
fbw_executed_effect_count() != executed_effects_before — i.e.
precisely when the body already applied something, which is the case that needs it.
So the route has no sound way to abort after the body has run.
Why NotImplemented is the trigger
NotImplemented from __lt__ means "try the reflected operand", which is ordinary working
code, not a failure. A dunder that returns it on a hot path is normal, so the abort is
reachable in a loop rather than once.
Notes for whoever picks this up
- Gating admission on
body_facts.exc_override_sample_safe was measured on the sibling
route and admits only LoadConst/BoxInt bodies — self.a, x + 1 and
len(self.items) all decline. It removes the route's value rather than fixing it.
- The disposition that is actually sound is to emit the object-level truth/reflected
dispatch as a residual instead of aborting, so the already-run body's result is
converted rather than discarded. No walker-side helper mints that descr today.
MAJIT_STATS=1 is the cheap discriminator: the repro reads loops_aborted=2 with the
route active.
— reported by Claude
Summary
try_walker_inline_user_compareopdiscards an inline sub-walk result with a bareErr(DispatchError::callee_inline_unsupported(..)). The error is not latched, so the walkdriver rolls back and replays the loop from entry — re-running a rich-compare dunder whose
body already applied its effects.
This is on
main; it is not introduced by any open branch. It was found while adjudicatingthe same defect in a sibling route (
try_walker_inline_user_contains, PR #1405), which waswithdrawn for this reason.
Reproduction
The answers stay correct — only the effect count is wrong.
The caller's loop shape matters. Under a
forloop the FOR_ITER admission gate(
inline_call.rs, theCalleeReplaySafety::Dirtydecline) rejects such bodies before theroute is reached, so a
for-driven repro reads 3000 and looks like a refutation. Onlywhilereproduces.Root cause
latch_abort_call_resumeis the mechanism that turns a discarded sub-walk into a forwardresume point, and its own doc comment states the rule:
Two things make it unavailable here:
reconstructed_all_ref_call_stack, which returnsNoneunlesspyre_helperisCallFn | CallFunctionEx. A rich-compare route arrives with aCompareOpdescriptor, so even an explicit latch call is a no-op.fbw_executed_effect_count() != executed_effects_before— i.e.precisely when the body already applied something, which is the case that needs it.
So the route has no sound way to abort after the body has run.
Why
NotImplementedis the triggerNotImplementedfrom__lt__means "try the reflected operand", which is ordinary workingcode, not a failure. A dunder that returns it on a hot path is normal, so the abort is
reachable in a loop rather than once.
Notes for whoever picks this up
body_facts.exc_override_sample_safewas measured on the siblingroute and admits only
LoadConst/BoxIntbodies —self.a,x + 1andlen(self.items)all decline. It removes the route's value rather than fixing it.dispatch as a residual instead of aborting, so the already-run body's result is
converted rather than discarded. No walker-side helper mints that descr today.
MAJIT_STATS=1is the cheap discriminator: the repro readsloops_aborted=2with theroute active.
— reported by Claude