Skip to content

jit: drop the in-place-append write-barrier residual, accept the full specialized binop tag set in the FOR_ITER inline gate - #782

Merged
youknowone merged 2 commits into
mainfrom
single-walker
Jul 25, 2026
Merged

jit: drop the in-place-append write-barrier residual, accept the full specialized binop tag set in the FOR_ITER inline gate#782
youknowone merged 2 commits into
mainfrom
single-walker

Conversation

@youknowone

@youknowone youknowone commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Two independent levers on the same FOR_ITER-body inline path. Commit 1 removes
a residual the walker was still recording inside an admitted callee; commit 2
widens which callees get admitted in the first place.

1. Stop recording list_write_barrier on the in-place Object-append arm

The #171 append fold descends w_list_append and folds the in-place Object
arm's store to SetarrayitemGc. That arm also calls list_write_barrier,
which is #[dont_look_inside], so the sub-walk left it in the trace as a
CallN residual — one per iteration of an Object-strategy comprehension. It
was the only residual call left in that loop.

The backend GC rewrite already marks the same store: a SetarrayitemGc with a
Ref value routes through handle_write_barrier_setarrayitem
(rewrite.py:936-944) into COND_CALL_GC_WB_ARRAY, and the rewriter runs in
production (runner.rs invokes it whenever gc_sync::is_initialized()). The
in-place arm does not replace the list's items pointer, so remembering the
W_ListObject adds nothing the array barrier does not already do.

Upstream emits no barrier call of its own — pyjitpl never executes one
(executor.py:446), and COND_CALL_GC_WB is neither can-raise nor a call
(resoperation.py:1124-1125). The barrier in a compiled loop is only ever the
one the backend rewrite inserts.

What this is not

It is not "delete the barrier from w_list_append". That function runs in
the interpreter too, where no rewriter exists — an empty body SIGABRTs.
Upstream carries two mechanisms for one source: the GC transform covers
translated code, the backend rewrite covers traces. pyre's hand-written barrier
is the transform stand-in, so it stays; only the recording of it stops.

FbwWalkMode::append_inplace_wb_covered_receiver carries the receiver address
(not a flag, so a barrier reached for any other list inside the sub-walk is
still recorded). orthodox_list_append_commit sets it when the fold enters the
in-place arm; the record site in residual_call.rs consumes it. The barrier
still runs concretely during the walk, because the walk mutates the live heap.

Suppression requires a GC-managed items block. Under PYRE_GC_ITEMSBLOCK=0 the
block is std::alloc memory with no GC header, the collector reaches its slots
only through the remembered W_ListObject, and the barrier is load-bearing —
verified 0 barrier calls in the loop by default, 10 retained off-GC.

Two comments claiming pyre has no backend GC-rewrite pass are corrected
(residual_call.rs, jit_fnaddr.rs); both are stale on this branch.

Effect

comprehension_object_append_hot steady loop: 30 → 29 ops, and no residual
call remains (call_may_force=0 both before and after).

-5.0% on that bench (user-CPU, startup-subtracted, interleaved median-9).
Sizing was established by a probe rather than inferred: appending a second
barrier per iteration — safe, since the barrier is idempotent — cost +9.0%,
which bounds what the removed call was worth. An earlier estimate of 20-55%
circulating from an RCA was not measured and is wrong.

2. Accept the full specialized binop tag set in the FOR_ITER inline gate

fbw_callee_body_replay_safety admits a FOR_ITER-in-flight callee whose only
unproven residual is a BINARY_OP a specialization table lowers to a native
op — nothing survives for a replay to double. The accepted tag set was Add
alone. A one-line helper of a - i or a += i therefore stayed a residual
call on every iteration.

The tag audit

Accept every tag whose lowering has no runtime decline path left. Both
try_walker_specialize_binary_op_int and ..._float key each in-place tag to
the same match arm as its plain form, so the two forms are admitted
together:

tag (+ its in-place form) int table float table verdict
Add / Subtract / Multiply IntAddOvf / IntSubOvf / IntMulOvf, needs_concrete_check=false FloatAdd / FloatSub / FloatMul accept
And / Or / Xor IntAnd / IntOr / IntXor, needs_concrete_check=false no arm accept, exact-int args only
FloorDivide / Remainder declines a zero or i64::MIN / -1 divisor no arm exclude
TrueDivide no arm declines a zero divisor so raising descr_truediv stays recorded exclude
Lshift declines outright (SHL masks the count mod 64; the guarded form breaks the cranelift bridge) no arm exclude
Rshift declines a negative or >= LONG_BIT count no arm exclude
Power no arm inlines _pow but keeps a cold-path residual exclude
Subscr / MatrixMultiply no arm no arm exclude

The same partition shows up independently in the bignum table added by #781:
try_walker_specialize_binary_op_long_int covers exactly Add/Sub/Mul/And/Or/Xor
plus their in-place forms, with Lshift/Rshift split into a separate function.

Because And / Or / Xor are int-table-only, they need a stricter argument
classification than the other six — hence the second flag rather than widening
the existing one.

The exactness fix

The argument classification now derives from is_plain_int1 /
is_plain_float_strict instead of is_int / is_float. The latter are
ob_type checks that a numeric subclass passes, while both
walker_int_specialization_operands and walker_float_specialization_operands
require is_exact_builtin_instance — correctly, since a subclass keeps the
builtin layout while its Python-visible class lives in w_class and may define
its own __add__.

So the predicate was claiming a specialization that would not happen: the
residual survived, and a replay re-applied it. This is not hypothetical —
widening the tag set with the old predicate made
synth/polymorphic_binary_receiver print 112692374 for 112973374 on both
backends. The fix also closes that hole on the pre-existing Add path. bool
arguments are no longer admitted.

A limitation is documented rather than papered over: both flags describe the
callee's incoming arguments, not the operands of the binop itself, which this
straight-line scan cannot name. A body can reach a non-numeric operand through
LoadConst / LoadGlobal, which the scan already treats as replay-safe reads.
The in-place tags do not widen that hole — an in-place result must be
stored back, and every store target outside the callee's own registers
(STORE_GLOBAL / STORE_ATTR / STORE_SUBSCR) is itself an unproven residual
that fails the scan first. Verified: a G += [i] body is still classified
Dirty.

Effect

One micro-bench per tag, gate metric (user CPU, startup-subtracted), dynasm.
The "before" column is the state after Subtract / Multiply were added but
before the audit widened the set further, so add/sub/mul/f* are already
Clean there and serve as controls:

tag before after gate verdict
add / sub / mul (control) 0.0035 / 0.0006 / 0.0049s 0.0014 / 0.0011 / 0.0043s CleanClean
fadd / fsub / fmul (control) 0.0019 / 0.0028 / 0.0015s ~0.0000s CleanClean
iadd 0.2282s 0.0005s DirtyClean
isub 0.2260s ~0.0000s DirtyClean
imul 0.2496s 0.0002s DirtyClean
fiadd 0.2172s ~0.0000s DirtyClean
and / or / xor 0.2375 / 0.2358 / 0.2334s 0.0006 / 0.0051 / 0.0049s DirtyClean
iand / ior / ixor 0.2485 / 0.2439 / 0.2462s ~0.0000 / ~0.0000 / 0.0021s DirtyClean
floordiv / mod / truediv / ftruediv 0.2346 / 0.2433 / 0.2322 / 0.2405s 0.2058 / 0.2270 / 0.2351 / 0.2043s DirtyDirty
lshift / rshift / pow 0.2391 / 0.2358 / 0.2656s 0.2160 / 0.2344 / 0.2576s DirtyDirty

Ten tags cross from Dirty to Clean and land on the control's timing. The
seven excluded tags stay Dirty and stay at ~0.20-0.26s, as intended.

Verification

  • check.py --backend dynasm,cranelift315/315 both
  • cargo test --all --no-default-features --features dynasm — no failures
  • cargo fmt --all --check — clean
  • New fixture list_append_write_barrier_gc.py: appends young elements to
    lists whose items block has aged out of the nursery, then reads every element
    back. Matches CPython 3.14 across dynasm/cranelift × JIT / PYRE_NO_JIT=1 /
    PYRE_GC_ITEMSBLOCK=0.
  • New fixture inlined_helper_arith_hot.py: one hot loop per accepted tag, so
    a tag dropping back out of the set shows up as the callee reappearing as a
    residual. Output matches CPython 3.14 on both backends; measured x5.7
    (dynasm) / x5.4 (cranelift) against a max-pypy-ratio=12 header.
  • All 23 per-tag probes (accepted and excluded alike) verified bit-exact
    against CPython 3.14 on dynasm, cranelift, and PYRE_NO_JIT=1.
  • comprehension_object_append_hot.py gets a max-pypy-ratio=40 header; it had
    no gate at all. Measured x29.6 (dynasm) / x30.3 (cranelift) on a loaded box.
    A 5% win cannot be gated by a pypy ratio — the noise band is wider than the
    signal, and .jitstats carries only loops_compiled/bridges_compiled
    ("watches what the JIT compiles, never how well"), so it cannot gate op
    counts either. 40 is set to catch a large structural regression without
    becoming a chronic red.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The JIT now detects GC-backed in-place list appends, suppresses duplicate residual write-barrier recording, and applies stricter numeric replay-safety checks. Diagnostics and arithmetic and GC stress benchmarks support the updated behavior.

Changes

JIT append barrier and replay safety

Layer / File(s) Summary
GC-backed append coverage state
pyre/pyre-object/src/listobject.rs, pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs, pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
Detects GC-managed in-place append storage and carries the covered receiver through the append sub-walk.
Residual barrier suppression
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs, pyre/pyre-interpreter/src/jit_fnaddr.rs
Skips recording covered residual barriers while executing them concretely, with documentation for runtime-patchable resolution and GC rewrite behavior.
Exact numeric replay-safety classification
pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs, pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs, pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
Separates exact plain integers from exact numerics and expands replay-safe arithmetic and bitwise operator handling.
Diagnostics and benchmarks
pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs, pyre/bench/synth/*
Adds inline diagnostics plus arithmetic and GC write-barrier stress scripts with assertions and printed results.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant orthodox_list_append_commit
  participant list_object_helper
  participant residual_dispatch
  participant list_write_barrier
  orthodox_list_append_commit->>list_object_helper: check GC-backed in-place storage
  list_object_helper-->>orthodox_list_append_commit: return coverage status
  orthodox_list_append_commit->>residual_dispatch: provide covered receiver
  residual_dispatch->>residual_dispatch: omit duplicate residual OpRef
  residual_dispatch->>list_write_barrier: execute barrier concretely
Loading

Possibly related PRs

Poem

A bunny watched the appenders grow,
While GC danced below.
Barriers ran, but traces knew
Which work was already due.
Exact sums hopped into view!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the two main changes: suppressing the in-place append write-barrier residual and widening the FOR_ITER binop specialization gate.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch single-walker

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit bb1e8a0).
Updated: 2026-07-25T13:07:38.059Z

Files in the reviewed diff
pyre/bench/synth/comprehension_object_append_hot.py
pyre/bench/synth/inlined_helper_arith_hot.py
pyre/bench/synth/list_append_write_barrier_gc.py
pyre/pyre-interpreter/src/jit_fnaddr.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
pyre/pyre-object/src/listobject.rs

1. Regressions to PyPy parity introduced by this patch

  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs:2279 ↔ rpython/jit/metainterp/pyjitpl.py:1944: the new admission accepts Subtract, Multiply, bitwise, and in-place tags based on all incoming callee arguments, not the two operands of the particular BINARY_OP. A LoadGlobal/LoadConst operand can be a numeric subclass with side-effecting __sub__, __imul__, etc.; specialization then declines, but the replay-safety scan labels the residual clean. A later guard resumes at the caller call boundary and replays the side effect. upstream/main admitted only Add, so the newly accepted tags regress parity.

2. Other mismatches introduced by this patch

None.

3. Pre-existing mismatches (already present before this patch)

  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs:2261 ↔ rpython/jit/metainterp/pyjitpl.py:1944: the same operand-provenance bug already existed for Add before this patch. The old args_all_numeric gate described callee inputs rather than the actual binary-operation operands, so def f(x): return global_numeric_subclass + x could be incorrectly considered replay-safe.
  • pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs:1388 ↔ rpython/jit/metainterp/pyjitpl.py:2445: inline-subwalk guards deliberately resume at the caller boundary because the callee frame is absent. PyPy creates and retains an MIFrame per call, then captures the full frame stack; collapsing the callee frame is a pre-existing frame-identity mismatch.

4. Structural adaptations

  • pyre/pyre-object/src/listobject.rs:1047 ↔ rpython/jit/backend/llsupport/rewrite.py:936: Rust’s Object-list backing block may be separately GC-managed, so the patch detects the in-place, GC-managed-array arm and relies on the local GC rewrite’s SetarrayitemGc barrier instead of recording the hand-written list barrier. This is a valid GC/storage adaptation; the off-GC block path remains conservatively barriered.
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs:2308 ↔ pypy/interpreter/pyopcode.py:706: the tag-based BINARY_OP handling is required by Pyre’s CPython-compatible compiler, whereas this PyPy source uses separate INPLACE_* opcode handlers. This opcode-shape difference is structural, not itself a parity defect.

The #171 append fold descends `w_list_append` and folds the in-place Object
arm's store to `SetarrayitemGc`. The arm also calls `list_write_barrier`, which
is `#[dont_look_inside]`, so the sub-walk left it in the trace as a `CallN`
residual — one per iteration of an Object-strategy comprehension.

The backend GC rewrite already marks that same store: `SetarrayitemGc` with a
Ref value routes through `handle_write_barrier_setarrayitem`
(rewrite.py:936-944) into `COND_CALL_GC_WB_ARRAY`, and the rewriter runs in
production (`runner.rs` invokes it whenever `gc_sync::is_initialized()`). The
in-place arm does not replace the list's `items` pointer, so remembering the
`W_ListObject` adds nothing the array barrier does not already do. Upstream
emits no barrier call of its own: pyjitpl never executes one
(executor.py:446) and `COND_CALL_GC_WB` is neither can-raise nor a call
(resoperation.py:1124-1125).

Carry the receiver on `FbwWalkMode` when the fold enters that arm, and skip
recording a `list_write_barrier` call for exactly that receiver. The barrier
still runs concretely during the walk, because the walk mutates the live heap.

The suppression requires a GC-managed items block. Under
`PYRE_GC_ITEMSBLOCK=0` the block is `std::alloc` memory with no GC header, the
collector reaches its slots only through the remembered `W_ListObject`, and the
barrier is kept.

Two comments claiming pyre has no backend GC-rewrite pass are corrected.

comprehension_object_append_hot: 30 -> 29 ops in the steady loop, and the loop
carries no residual call. Measured -5.0% on that bench (user-CPU,
startup-subtracted, interleaved median-9); a probe that appended a second
barrier per iteration cost +9.0%, which bounds what the removed call was worth.

Adds list_append_write_barrier_gc, which appends young elements to lists whose
items block has aged out of the nursery, and gives the bench a
max-pypy-ratio gate (it had none).

Assisted-by: Claude
… gate

`fbw_callee_body_replay_safety` admits a FOR_ITER-in-flight callee whose only
unproven residual is a BINARY_OP a specialization table lowers to a native op.
The accepted tag set was `Add` alone, so a body of `a - i`, `a * 2` or `a += i`
stayed a per-iteration residual call.

Accept every tag whose lowering has no runtime decline left. Both
`try_walker_specialize_binary_op_int` and `..._float` key each in-place tag to
the same arm as its plain form, so `Add` / `Subtract` / `Multiply` and
`InplaceAdd` / `InplaceSubtract` / `InplaceMultiply` are accepted together.
`And` / `Or` / `Xor` and their in-place tags are in the int table only, so they
are accepted under a new exact-plain-int argument classification. The divide,
remainder, shift, power, subscript and matrix-multiply tags stay excluded; the
per-tag reasons are in the predicate's doc comment.

Derive the argument classification from `is_plain_int1` /
`is_plain_float_strict` rather than `is_int` / `is_float`. The latter are
`ob_type` checks a numeric subclass passes, while `walker_int_specialization_operands`
and `walker_float_specialization_operands` both require
`is_exact_builtin_instance`; the predicate claimed a specialization that would
not happen, admitting a body whose surviving residual a replay re-applied
(`synth/polymorphic_binary_receiver` printed 112692374 for 112973374). `bool`
arguments are no longer admitted.

Add `PYRE_FBW_INLINE_DIAG` prints at the inline entry, the callee resolution
and the FOR_ITER gate, and `bench/synth/inlined_helper_arith_hot.py` with one
loop per accepted tag.

Assisted-by: Claude
@youknowone youknowone changed the title jit: stop recording list_write_barrier on the in-place Object-append arm jit: drop the in-place-append write-barrier residual, accept the full specialized binop tag set in the FOR_ITER inline gate Jul 25, 2026
@youknowone
youknowone merged commit e5ba6da into main Jul 25, 2026
18 of 19 checks passed
@youknowone
youknowone deleted the single-walker branch July 25, 2026 14:55
youknowone added a commit that referenced this pull request Jul 26, 2026
… rejection; object: root the block across a list regrow; interpreter: PyObject_Format fast path (#807)

* jit: reject numeric subclasses in the float binop/compare specialization

`walker_int_specialization_operands` rejects an operand that is not an exact
builtin instance, because the raw int path bypasses special-method dispatch.
`walker_float_specialization_operands` carried no such check: it classified its
operands with `is_int` / `is_float`, which read `ob_type` and are true for a
subclass. The `guard_class` the consumers emit reads `ob_type` too, so the
subclass was not caught at runtime either.

A float subclass overriding `__add__` / `__radd__` / `__sub__` / `__mul__` /
`__truediv__` therefore lost the override once the loop compiled, on both
backends. The helper is shared with the float COMPARE_OP specialization, so
`__lt__` / `__eq__` were bypassed as well.

Add the same exactness check to the float helper.

`bench/synth/float_subclass_binop_dispatch.py` drives each dunder hot; with the
check reverted it reports the raw IEEE result for 7 of its 9 lines, and its
`int` subclass control is unaffected either way.

Drop the `max-pypy-ratio` gate from `comprehension_object_append_hot`. pypy runs
it in ~0.019s, two ticks of the 10ms user-CPU resolution; a CI run in which pyre
went from 1.22s to 1.09s crossed the bound because pypy measured 0.03s instead
of 0.04s. The bench keeps its output check.

Assisted-by: Claude

* jit: prove operand provenance before exempting a BINARY_OP from the FOR_ITER inline gate

`residual_call_is_specialized_plain_numeric_binop` admitted a callee body on
`args_all_exact_numeric` / `args_all_exact_plain_int`, which describe the
callee's incoming arguments, and claimed from that the body's `BINARY_OP`
residual would be specialized away. The two are only the same fact when the
binop's operands ARE those arguments. In `def f(x): return G - x` the left
operand is a module global reached through a `LoadGlobal` the body scan already
treats as a replay-safe read, so a numeric subclass there declines the
specialization and leaves a residual the admission promised was not coming.

`fbw_callee_body_replay_safety` now carries the missing fact alongside the
freshness it already tracks: which registers hold a value whose class is
provably an immutable builtin. Three sources close over it — an incoming
argument, read out of a `localsplus` slot the exact-positional entry convention
bound (tracked through `getarrayitem_vable_r` / `setarrayitem_vable_r`, so
`LOAD_FAST` / `STORE_FAST` propagate); a `LoadConst` / `BoxInt` /
`NewtupleFromArray` result; and an already-accepted `BINARY_OP` result. The
callee's ref constant pool is seeded by testing the constant objects outright
with the exactness the specialization itself demands, since they are reachable
at gate time. `LoadGlobal` is excluded. The predicate now decodes the residual's
R-list and requires both operand registers to be in that set.

The accepted tag set is unchanged: all 16 admitted tags still admit and the 7
excluded ones still decline, measured per-tag. No observable wrong answer was
constructed either way — with the operand proof disabled the hazardous shapes
are admitted yet the effect still lands once, because the cases that realize the
replay are caught downstream — so this closes a latent gap rather than a live
bug.

`bench/synth/inline_gate_operand_provenance.py` covers the three shapes that
reach an unproven operand plus an arguments-only control.

Reported by the Codex parity review on #782 (sections 1 and 3; one bug, widened
in blast radius by that PR's larger tag set).

Assisted-by: Claude

* object: root the old items block, not each of its items, across a list regrow

`grow_list_items_block_gc` pinned every one of the old block's `live_len` items
before allocating the new block, then read each one back off the shadow stack.
Both halves call `gc_current_object_address`, so a resize cost `2 * live_len` GC
ownership queries — each a TLS lookup, a `RefCell` borrow, an arena range test
and a rawmalloced-set probe.

The block itself is a traced GC array of GC pointers
(`PY_OBJECT_ARRAY_GC_TYPE_ID` is registered `TypeInfo::varsize` over
`PyObjectRef` slots), so rooting it keeps every item reachable and lets a
collection inside the allocation relocate the slots in place. One root replaces
`live_len` of them and the copy becomes a `copy_nonoverlapping`. A block that
the nursery declined and `std::alloc` supplied cannot move, so it is not rooted
at all.

That is also the upstream shape: `_ll_list_resize_really` (rlist.py:262-267)
mallocs the new array and `ll_arraycopy`s into it, with no per-item root
bracket.

Resizes are ~1% of the appends in a list build but dominated its profile:
building 8M elements, work-thread samples under `w_list_grow_items_block` were
56% of the run, almost all inside `pin_root` / `shadow_stack_get` →
`dynasm_gc_owns_object` → `OldGen::contains`.

Interleaved A/B, same binary, 7 rounds, median user-CPU, startup subtracted,
`[<expr> for i in range(n)]` over 8M elements:

    form    per-item pin   block root   speedup
    none         0.2656s      0.1062s      2.50x
    str          0.2631s      0.1046s      2.51x
    tuple        0.3215s      0.2089s      1.54x
    int          0.1237s      0.1284s      0.96x   (Integer strategy; not this path)

`bench/synth/list_append_write_barrier_gc.py` gains `big_live_len_regrow`, which
drives resizes at the largest live lengths it can with allocation pressure
against the growth boundary and distinct per-slot identities, so a copy that
read pre-collection addresses would show up. The whole fixture matches CPython
3.14 and PyPy across dynasm/cranelift x JIT/no-JIT/`PYRE_GC_ITEMSBLOCK=0`.

Assisted-by: Claude

* interpreter: take PyObject_Format's exact-str/int fast path, and enforce the
int digit limit in the long conversion

`format_value_dispatch` gated its empty-spec fast path on the resolved
`__format__` being a `BUILTIN_FUNCTION_TYPE`, which no type-dict entry ever
is: type-slot builtins are built by `make_builtin_function_with_arity` →
`function_new_with_fixed_code`, so they are `FUNCTION_TYPE` with
`can_change_code = false` (they have to be descriptors to bind), and
`BUILTIN_FUNCTION_TYPE` is reserved for module-level builtins, which are
deliberately not descriptors. `type(int.__format__)` is therefore `function`,
the guard was always true, and every `f"{i}"` resolved and called
`int.__format__` through the generic call machinery to reach a body that
computes exactly what the skipped fast path computes.

`PyObject_Format` answers this with an exactness test ahead of the lookup:
an empty spec on an exact `str` or `int` cannot reach an override, so nothing
is resolved or called. `bool` stays out — `PyLong_CheckExact` rejects it and
`f"{True}"` is `True`, not `1`.

`sys.set_int_max_str_digits` was enforced only inside `int_to_decimal_string`,
whose one caller was the `int.__repr__` descriptor, so a conversion that did
not go through that descriptor skipped the limit. `long_to_decimal_string`
enforces it in the conversion, so `builtin_leaf_repr_string`'s `long` arm now
runs `int_to_decimal_string` and the helper returns a `Result`. The `int` arm
needs no check: a machine int is at most 19 digits, below the 640 floor
`sys.set_int_max_str_digits` accepts.

`[f"{i}" for i in range(1000)]` x 2000, median-5 user-CPU with startup
subtracted, against CPython 3.14: 24.35x -> 12.95x.

check.py dynasm 323/323, cranelift 323/323, wasm 320/320.

Assisted-by: Claude
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant