Skip to content

gc: separate a failed managed allocation from an absent GC - #1020

Merged
youknowone merged 1 commit into
mainfrom
rbigint
Aug 4, 2026
Merged

gc: separate a failed managed allocation from an absent GC#1020
youknowone merged 1 commit into
mainfrom
rbigint

Conversation

@youknowone

@youknowone youknowone commented Aug 4, 2026

Copy link
Copy Markdown
Owner

try_gc_alloc* returns None when no hook is installed and Some(null) when an
installed hook could not satisfy the request. Four sites collapsed the two with
.filter(|p| !p.is_null()) and fell back to lltype::malloc_raw for both, so a
failed managed allocation silently produced a headerless object.

site traced edge walker
alloc_rbigint_nursery_impl _digits W_LongObject's LONG_VALUE_OFFSET — plain offset walker, no try_gc_owns_object guard
alloc_rbigint_nursery_collecting_impl same same
alloc_rbigint_stable same same
alloc_frame_block previous chain head is guarded, but the interior previous is the unguarded offset walker

Reachable, not theoretical: try_alloc_with_type_no_collect_body returns
GcRef(0) once the nursery bump and spill_to_oldgen_or_null both fail. The
result is a Box::into_raw payload forwarded as though it carried a header —
the failure class behind the SIGBUS fixed in #994.

Change

GcAllocOutcome::{Allocated, Failed, NoRoute} with from_hook and
allocated_or_abort, plus try_gc_alloc_stable_or_abort for the
dont_look_inside raw-return callers. Failed reaches gc_alloc_failed
(#[cold] #[inline(never)] -> !), which calls handle_alloc_error exactly as
alloc_typed_items_block_nursery already does for the digit array — so an
rbigint payload now fails the same way its own digits do. NoRoute alone
returns the caller to its malloc_raw path.

Upstream has neither state: the GC is a prebuilt constant
(framework.py:254), and a nursery that cannot satisfy the request reaches
collect_and_reserve (incminimark.py:981-985), which raises MemoryError
rather than returning null. Abort rather than panic because these run under JIT
frames that cannot unwind.

What makes the abort safe: init_gc_subsystem runs install_gc_into_backend()
before install_pyre_object_hooks, so once a pyre-object hook is visible the
set_active_* cells are too — Some(null) can never mean "no backend".

Deliberately unchanged

Sites whose mixed GC/raw population is supported and guarded, judged by the
walker rather than by the fallback spelling:

  • gc_alloc_storage_boxset_object_custom_trace guards set.items with
    try_gc_owns_object ("A no-GC-hook fallback allocation is not
    collector-owned"). I changed this one first and reverted it.
  • clone_debugdata_ptr / getorcreate_debug_data — guarded, with a deliberate
    in-place walk for Box payloads.
  • alloc_dict_object, w_long_new — fall back to malloc_typed, which
    prepends a GcHeader.

Also replaces the module doc's claim that the Box::into_raw fallback is
dropped "as the hook's reliability is verified under the full bench suite". That
criterion cannot be met: the state that matters is Some(null), which a green
benchmark never exercises.

Verification

Green on base 7b0aca4bc8e: check.py dynasm 371/371, cranelift 371/371, wasm
367/367, with HEAD unchanged across the run.

On the current base, extract-llbc.py, the release build, and pyre-object
312 / pyre-interpreter 490 / majit-gc 231 all pass. The full local
check.py was not completed on this base: the shared working tree was
rebased twice mid-run (13:07 and 15:17), the second time pulling in #1019, which
re-records jit-stats baselines — so the comparison's baseline changed under a
binary built from the previous base. Those runs were discarded rather than
reported as results. CI is the gate for this base.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved memory allocation safety by preventing managed allocations from silently falling back to untracked memory.
    • Allocation failures under active garbage collection now terminate safely instead of risking incorrect memory tracking.
    • Preserved fallback behavior when no garbage-collection route is available.
  • Tests

    • Added coverage for allocation outcomes, failure handling, and fallback eligibility.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@youknowone, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2313e6fb-92b5-4389-a668-e139c6981c59

📥 Commits

Reviewing files that changed from the base of the PR and between a4ce037 and 9e84bc9.

📒 Files selected for processing (3)
  • pyre/pyre-interpreter/src/pyframe.rs
  • pyre/pyre-object/src/gc_hook.rs
  • pyre/pyre-object/src/rbigint.rs

Walkthrough

GC allocation hooks now distinguish no route, successful allocation, and managed allocation failure. Frame blocks and RBigInt allocations abort on managed failure instead of falling back to untraced raw allocation.

Changes

GC allocation failure handling

Layer / File(s) Summary
Allocation outcome contract
pyre/pyre-object/src/gc_hook.rs
The GC hook API classifies allocation outcomes, adds aborting allocation helpers, and tests null-hook behavior.
RBigInt allocation paths
pyre/pyre-object/src/rbigint.rs
Nursery and stable RBigInt allocation paths distinguish missing GC routes from managed allocation failures.
Managed frame block allocation
pyre/pyre-interpreter/src/pyframe.rs
Managed frame blocks use GcAllocOutcome::allocated_or_abort instead of raw fallback allocation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • youknowone/pyre#149: It introduces GC-header-backed frame allocation in the same frame allocation path.
  • youknowone/pyre#398: It modifies GC allocation behavior and allocation APIs in the same areas.
  • youknowone/pyre#961: It modifies shared GC allocation hooks and rbigint allocation paths.

Suggested reviewers: lifthrasiir

Poem

A rabbit checks each heap-bound call,
No managed failure hides at all.
Missing routes may still fall through,
Failed routes stop as they should do.
Safe frames and big ints leap—
While GC guards the heap.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: distinguishing failed managed allocations from an absent GC route.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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 rbigint

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 Aug 4, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 9e84bc9).
Updated: 2026-08-04T17:49:21.760Z

Files in the reviewed diff
pyre/pyre-interpreter/src/pyframe.rs
pyre/pyre-object/src/gc_hook.rs
pyre/pyre-object/src/rbigint.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

None.

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

  • pyre/pyre-object/src/gc_hook.rs:314 ↔ rpython/memory/gc/minimark.py:554: unchanged try_gc_alloc_stable_raw() collapses an installed allocator’s null result with “no route,” allowing unmodified callers to take a raw-allocation fallback. MiniMark instead collects/reserves and returns a managed object; its bounded-heap failure is a MemoryError, not a headerless raw object (minimark.py:1720).

4. Structural adaptations

  • pyre/pyre-object/src/gc_hook.rs:167 ↔ rpython/memory/gctransform/framework.py:803: GcAllocOutcome::{NoRoute, Failed} is a necessary Rust layering adaptation. RPython’s translated gct_fv_gc_malloc always receives the GC instance and emits a direct allocator call; pyre’s lower-level crate must support bootstrap/no-backend execution through an optional process-global hook.

  • pyre/pyre-object/src/gc_hook.rs:197 ↔ rpython/memory/gc/minimark.py:1720: an installed hook returning null now terminates through Rust’s allocation-failure path, rather than raising PyPy’s catchable MemoryError. This is an explicit bare-pointer/JIT-ABI adaptation. The policy is consistently applied at pyre/pyre-object/src/rbigint.rs:600, pyre/pyre-object/src/rbigint.rs:657, pyre/pyre-object/src/rbigint.rs:698, and pyre/pyre-interpreter/src/pyframe.rs:1091; unlike main, none can splice a raw payload into a managed traced graph after managed allocation failure.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

https://github.com/youknowone/pyre/blob/0383073e97d8176156a27564fef5a6f13ff06667/pyre-object/src/gc_hook.rs#L182
P1 Badge Propagate MemoryError instead of aborting allocation failures

When an installed GC hook returns null—for example when an old-generation spill cannot allocate—this arm reaches handle_alloc_error and terminates the process. The upstream behavior documented immediately below is to raise MemoryError, so the new RBigInt and frame-block paths turn a catchable Python allocation failure into an interpreter crash; thread the failure through the interpreter/JIT exception protocol rather than aborting.

AGENTS.md reference: AGENTS.md:L194-L196

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyre/pyre-object/src/gc_hook.rs`:
- Around line 163-172: Update GcAllocOutcome::from_hook and the
stable-allocation flow so an absent GC_ALLOC_STABLE_HOOK is distinguishable from
an inactive or unsupported GC when an independent GC is active; do not classify
that case as NoRoute or fall through to malloc_raw. Apply the corresponding
handling at pyre/pyre-object/src/gc_hook.rs:318-328 and preserve the required
RBigInt allocation behavior at pyre/pyre-object/src/rbigint.rs:698. Extend the
independent-hook test to cover the missing-stable-route fallback, including the
untraced headerless RBigInt/_digits case.
- Around line 175-201: Preserve managed allocation failures as exception-capable
results rather than aborting: update gc_alloc_failed and allocated_or_abort in
pyre/pyre-object/src/gc_hook.rs:175-201, propagate the result through
try_gc_alloc_stable_or_abort in pyre/pyre-object/src/gc_hook.rs:318-328, and
through the affected rbigint allocation paths at
pyre/pyre-object/src/rbigint.rs:593-600, 649-657, and 698 and frame-block
allocation in pyre/pyre-interpreter/src/pyframe.rs:1088-1091. Ensure generated
JIT execution observes MemoryError when malloc_raw is disabled, and add a test
covering that path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: da07407a-5262-47bf-ba8f-96d0b912d9f5

📥 Commits

Reviewing files that changed from the base of the PR and between 40c82e5 and 0383073.

📒 Files selected for processing (3)
  • pyre/pyre-interpreter/src/pyframe.rs
  • pyre/pyre-object/src/gc_hook.rs
  • pyre/pyre-object/src/rbigint.rs

Comment thread pyre/pyre-object/src/gc_hook.rs
Comment thread pyre/pyre-object/src/gc_hook.rs
@youknowone

Copy link
Copy Markdown
Owner Author

pyre/check.py (macos-latest) is red here, and it is inherited from the base, not introduced by this PR.

The same job is already completed/failure on three consecutive main commits, including this PR's base ff42f0e3317:

commit pyre/check.py (macos-latest)
ff42f0e3317 (this PR's base) failure
7fd8d544b74 (#1019) failure
4d7588324eb (#1014) failure

Comparing the failing sets keyed on backend + fixture + the counter delta itself, not just the fixture name:

main @ ff42f0e3317 : 24 fails    this PR : 24 fails
only in PR   : (empty)
only in main : (empty)

Byte-identical, e.g. cranelift synth/exception_bridge_traceback_head jit-stats change: loops_aborted 1 -> 0, bridges_compiled 6 -> 4, guard_failures 1203 -> 802 appears on both sides with the same numbers. This PR adds no jit-stats change of its own, which is what you would expect: the only new code is a #[cold] #[inline(never)] -> ! arm taken when a managed allocation fails.

I have not re-recorded any baseline to make this green. The counters moved for reasons that belong to the base, and re-recording would bury a real signal that is not mine to bury.

Locally this branch was green on base 7b0aca4bc8echeck.py dynasm 371/371, cranelift 371/371, wasm 367/367, with HEAD verified unchanged across the run.

commented by Claude

@youknowone

Copy link
Copy Markdown
Owner Author

All checks have settled. Extending the earlier control to all three platforms: every pyre/check.py failure here is inherited from the base ff42f0e3317, and this PR adds none of its own.

platform base ff42f0e3317 this PR only in PR only in base
macos-latest 24 fails 24 fails
ubuntu-24.04 38 fails 38 fails
windows-latest 25 fails 25 fails

Sets compared on backend + fixture + the counter delta text, not the fixture name alone. All 87 entries are a single category, jit-stats change — no crash, no test failure, no new category. The pyre/check.py job is completed/failure on ff42f0e3317, 7fd8d544b74 (#1019) and 4d7588324eb (#1014), so main cannot currently take a PR through this gate green.

Everything else passes (15/15): cargo test on all three platforms, CPython suite gate, sandbox e2e, both wasm builds, Charon/LLBC prepare on all three, fmt, pre-commit, and both review gates.

No baseline was re-recorded. The counters moved for reasons owned by the base, and re-recording them here would bury a signal that is not mine to bury.

commented by Claude

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a06cbb4fe0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

pub fn gc_alloc_failed(payload_size: usize) -> ! {
let layout = std::alloc::Layout::from_size_align(payload_size, std::mem::align_of::<usize>())
.unwrap_or_else(|_| std::alloc::Layout::new::<usize>());
std::alloc::handle_alloc_error(layout)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate MemoryError instead of aborting on managed OOM

When an active-GC allocation returns null—for example, after a nursery-full allocation cannot spill to old-gen—this non-returning call terminates the process rather than producing the MemoryError that the preceding comment identifies as the upstream behavior. Python code cannot catch the failure, and treating JIT unwinding as the reason for changing semantics is a generation defect to address rather than a valid behavioral deviation; propagate an interpreter exception through the residual/JIT path instead.

AGENTS.md reference: AGENTS.md:L14-L20

Useful? React with 👍 / 👎.

@youknowone

Copy link
Copy Markdown
Owner Author

Force-pushed: rebased onto a8f3cdb36ed and dropped a06cbb4fe04 ("check: record managed-allocation JIT stats"). The branch now carries the GC commit only.

Why the re-record went away rather than getting narrowed to its improvements: main's 7a7d9174088 (#945) re-recorded 108 .jitstats baselines after this branch was cut, and it already covers almost all of that commit. Comparing its 38 files against current origin/main:

file current origin/main dropped commit
exception_inline_callee_tb_frames.wasm guard_failures=860 975 — rise
exception_traceback_frame_lineno.cranelift guard_failures=811, loops_compiled=17 808, 14 — loops fall
raise_reg_unbound_jitstress.cranelift loops_aborted=1, loops_compiled=6 2, 8 — abort rise
raise_reg_unbound_jitstress.dynasm loops_aborted=1, loops_compiled=6 2, 8 — abort rise

Each of those was measured against a tree without #945, so writing it on top of #945 would pin a number that does not describe the tree — and two of the four move in the bad direction anyway.

This also supersedes the inherited-red report I posted earlier: the 24/38/25 jit-stats change rows at base ff42f0e3317 were main's own stale baselines, and #945 is what recorded them. Any counter that still moves at the new base will be recorded from a run at that base, improvements only.

commented by Claude

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyre/pyre-object/src/gc_hook.rs`:
- Around line 994-1001: Update
installed_hook_returning_null_classifies_as_failure_not_no_route so
clear_gc_alloc_hook() runs immediately after try_gc_alloc_fast returns and
before converting or asserting the outcome. Preserve the existing Failed
classification assertion while ensuring the assertion does not depend on a
registered GC_ALLOC_FAST_HOOK.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 21107846-1b14-4b90-acaf-5311b346cfeb

📥 Commits

Reviewing files that changed from the base of the PR and between 0383073 and a4ce037.

📒 Files selected for processing (3)
  • pyre/pyre-interpreter/src/pyframe.rs
  • pyre/pyre-object/src/gc_hook.rs
  • pyre/pyre-object/src/rbigint.rs

Comment on lines +994 to +1001
#[test]
fn installed_hook_returning_null_classifies_as_failure_not_no_route() {
let _hook_lock = hook_test_guard();
register_gc_alloc_hook(null_hook);
let outcome = GcAllocOutcome::from_hook(unsafe { try_gc_alloc_fast(1, 8) });
clear_gc_alloc_hook();
assert_eq!(outcome, GcAllocOutcome::Failed);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
git rev-parse --show-toplevel >/dev/null

rg -n -C 3 --glob '*.rs' 'GC_ALLOC_FAST_HOOK|clear_gc_alloc_fast_hook|register_gc_alloc_fast_hook|fn try_gc_alloc_fast\b' pyre majit

Repository: youknowone/pyre

Length of output: 2753


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== gc_hook outline relevant =="
ast-grep outline pyre/pyre-object/src/gc_hook.rs --view expanded | sed -n '1,220p'

echo "== gc_hook lines 80-250 =="
sed -n '80,250p' pyre/pyre-object/src/gc_hook.rs | nl -ba -v80

echo "== tests around hook =="
sed -n '960,1025p' pyre/pyre-object/src/gc_hook.rs | nl -ba -v960

echo "== sibling hooks and tests =="
rg -n -C 4 'clear_gc_alloc_hook|clear_gc_alloc_with_placement_hook|no_collect_placement_hook_has_conservative_fallback|installed_hook_returning_null' pyre/pyre-object/src/gc_hook.rs

echo "== fast hook usages outside pyre-object =="
rg -n -C 3 --glob '*.rs' 'register_gc_alloc_fast_hook|clear_gc_alloc_fast_hook|try_gc_alloc_fast' pyre majit

Repository: youknowone/pyre

Length of output: 5933


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== gc_hook lines 80-250 =="
sed -n '80,250p' pyre/pyre-object/src/gc_hook.rs

echo "== tests around hook and sibling fallback =="
sed -n '960,1100p' pyre/pyre-object/src/gc_hook.rs

echo "== fast hook usages outside pyre-object =="
rg -n -C 3 --glob '*.rs' 'register_gc_alloc_fast_hook|clear_gc_alloc_fast_hook|try_gc_alloc_fast' pyre majit

echo "== all fast-allocation hook registrations/clears in tracked Rust =="
rg -n --glob '*.rs' 'register_gc_alloc_fast_hook|clear_gc_alloc_fast_hook' .

Repository: youknowone/pyre

Length of output: 18051


Clear the fast-allocation hook before asserting the classification.

try_gc_alloc_fast reads GC_ALLOC_FAST_HOOK before falling back to try_gc_alloc, so leaving that hook registered makes this test depend on an untested hook state. Clear it explicitly before asserting the expected behavior.

♻️ Proposed test hardening
     fn installed_hook_returning_null_classifies_as_failure_not_no_route() {
         let _hook_lock = hook_test_guard();
+        clear_gc_alloc_fast_hook();
         register_gc_alloc_hook(null_hook);
         let outcome = Gc_allocOutcome::from_hook(unsafe { try_gc_alloc_fast(1, 8) });
         clear_gc_alloc_hook();
         assert_eq!(outcome, GcAllocOutcome::Failed);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-object/src/gc_hook.rs` around lines 994 - 1001, Update
installed_hook_returning_null_classifies_as_failure_not_no_route so
clear_gc_alloc_hook() runs immediately after try_gc_alloc_fast returns and
before converting or asserting the outcome. Preserve the existing Failed
classification assertion while ensuring the assertion does not depend on a
registered GC_ALLOC_FAST_HOOK.

@youknowone

Copy link
Copy Markdown
Owner Author

Correction to my previous comment. I wrote that all four DIFF files "were measured against a tree without #945, so writing them on top of #945 would pin a number that does not describe the tree." That is wrong for three of the four.

git log ff42f0e3317..a8f3cdb36ed -- <file> per file:

file touched by #945? last changed by
raise_reg_unbound_jitstress.dynasm no c1e2c017550 (#1003)
raise_reg_unbound_jitstress.cranelift no c1e2c017550 (#1003)
exception_inline_callee_tb_frames.wasm no a0314863dd5 (#1009)
exception_traceback_frame_lineno.cranelift yesguard_failures=811, loops_compiled=17 7a7d9174088 (#945)

A file #945 never touched is not stale by construction, so only the last row was the case I described.

And a bracketed local run at the new base (HEAD_BEFORE == HEAD_AFTER == a4ce0376fbe, check.py --synthetic-only) observes exactly the dropped numbers: raise_reg_unbound_jitstress loops_aborted 1 -> 2, loops_compiled 6 -> 8 on both dynasm and cranelift, and exception_inline_callee_tb_frames.wasm guard_failures 860 -> 975. Those three do describe the current tree.

What that does not change: they are still not recorded on this branch. Each moves a counter the wrong way — loops_aborted rise (×2), and a guard_failures rise with loops_compiled/bridges_compiled unchanged at 6/4 — and this branch records improvements only.

They are inherited regardless: main's own run at a8f3cdb36ed (ubuntu, job 91949366146) reports all of them, 46 rows in total. 42 of those 46 are loops_compiled-fall rows on dynasm/cranelift (arith_int_bool 8 -> 7, ast_compile_roundtrip 1 -> 0, sre_*, type_dotted_name, …) that do not reproduce on macOS, so main also has a host-divergence class open on that gate. Windows reports 43 rows.

commented by Claude

@youknowone

Copy link
Copy Markdown
Owner Author

Set diff against main's own run at this branch's base (a8f3cdb36ed), all three platforms, keyed on backend + fixture + counter-delta text:

platform main a8f3cdb36ed PR a4ce0376fbe only in PR only in main
macos-latest 2 2 4 (perf, not jit-stats)
ubuntu-24.04 46 46
windows-latest 43 43

Per-backend counts match exactly too (ubuntu dynasm 21 / cranelift 21 / wasm 4; windows dynasm 21 / cranelift 22). cargo test passes on all three platforms. The four macOS rows main has and this branch does not are all [pypy exec clamped to floor; ratio not a measurement], i.e. the collapsed-denominator flake, not a signal.

Nothing was re-recorded, because at this base no row is an improvement. Every moving row carries at least one counter in the bad direction:

  • raise_reg_unbound_jitstress (dynasm, cranelift, all 3 OS) — loops_aborted 1 -> 2 alongside loops_compiled 6 -> 8.
  • exception_inline_callee_tb_frames.wasmguard_failures 860 -> 975 with loops_compiled/bridges_compiled unchanged at 6/4, so the rise is not paid for by anything.
  • pickle_terminal_raise_resume.wasmloops_compiled 74 -> 73, a plain fall.
  • list_append_write_barrier_gc.wasm, recursion_memo_branch.wasmloops_aborted rises (0 -> 1, 1 -> 2) alongside large loop/bridge gains.
  • the 42 dynasm/cranelift rows on ubuntu and windows — all loops_compiled falls (arith_int_bool 8 -> 7, ast_compile_roundtrip 1 -> 0, sre_*, type_dotted_name, …), and they do not reproduce on macOS at all, so main has a host-divergence class open on this gate independently of this PR.

I verified list_append_write_barrier_gc specifically rather than assuming it: this branch renames the dont_look_inside symbol try_gc_alloc_stable_raw -> try_gc_alloc_stable_or_abort, which can move a trace boundary, and that fixture is a GC/write-barrier one. main's own run reports the identical delta, so it is not this branch's.

A local bracketed run (HEAD_BEFORE == HEAD_AFTER == a4ce0376fbe, check.py --synthetic-only, macOS) reproduces the same six host-independent rows byte for byte.

commented by Claude

@youknowone

Copy link
Copy Markdown
Owner Author

Revised — two rows were recordable, and are now recorded (32f0b3239af). My earlier "no row at this base is an improvement" was a direction-only reading of the counters, which is too coarse. Adjudicating each row by what caused it and by the upstream oracle instead:

Attribution, by set-diffing pyre/check.py (ubuntu-24.04) logs across consecutive main commits (keys normalised — #945 added the (observed …) suffix to the message, so raw string diffs mis-attribute every row):

row first appears at cause
raise_reg_unbound_jitstress dynasm+cranelift 1→2 / 6→8 ff42f0e3317 (#950) builtins parity reaching a qmut-bumping namespace write
exception_inline_callee_tb_frames.wasm guard 860→975 ff42f0e3317 (#950)
list_append_write_barrier_gc.wasm, recursion_memo_branch.wasm 7a7d9174088 (#945) #945 itself
pickle_terminal_raise_resume.wasm 74→73 7a7d9174088 (#945) #945 itself
the 42 loops_compiled −1 rows on ubuntu+windows 7a7d9174088 (#945) see below

The decisive fact for the two recorded rows: #945 wrote the identical move into the dynasm and cranelift files of both fixtures and left the wasm file at #1009's values.

fixture #945 wrote (dynasm/cranelift) wasm file wasm run
list_append_write_barrier_gc 1→12 loops, 1→5 bridges, 0→1 aborted, 200→1335 guards untouched, 1 / 1 / 0 / 200 12 / 5 / 1 / 1335
recursion_memo_branch 2→3 loops, 13→16 bridges, 0→1 aborted, 1757→2613 guards untouched, 2 / 11 / 1 / 2227 3 / 14 / 2 / 3083

So the wasm rows are the same event on the third backend, not a new one. PYPYLOG=jit-summary:- pypy3 (7.3.22) on the same scripts agrees on direction: list_append_write_barrier_gc 13 loops / 5 bridges / 0 aborts, recursion_memo_branch 3 loops / 21 bridges / 0 aborts — 12/5 and 3/14 are closer to upstream than the recorded 1/1 and 2/11.

Left unrecorded, deliberately:

Since my earlier comments, main's #1016 (ca1d3d4a8de) recorded raise_reg_unbound_jitstress.{dynasm,cranelift} to 2/8 and exception_inline_callee_tb_frames.wasm to 975, so those rows are gone from this branch independently.

Verification, bracketed (HEAD_BEFORE == HEAD_AFTER == f6bd8497f6f, rebased onto ca1d3d4a8de): check.py --synthetic-only reads dynasm 358/358, cranelift 358/358 and wasm 3 failures before the record, 1 after — the remaining one is pickle_terminal_raise_resume above.

commented by Claude

`try_gc_alloc*` returns `None` when no hook is installed and `Some(null)`
when an installed hook could not satisfy the request.
`alloc_rbigint_nursery_impl`, `alloc_rbigint_nursery_collecting_impl`,
`alloc_rbigint_stable` and `alloc_frame_block` collapsed the two and fell
back to `lltype::malloc_raw` for both. `W_LongObject` registers
`LONG_VALUE_OFFSET` and `FrameBlock` registers `previous` as gc-pointer
offsets whose walkers carry no `try_gc_owns_object` guard, so a raw payload
reached through either is forwarded as though it had a header.
`try_alloc_with_type_no_collect_body` returns `GcRef(0)` once the nursery
bump and `spill_to_oldgen_or_null` both fail.

Add `GcAllocOutcome::{Allocated, Failed, NoRoute}` with `from_hook` and
`allocated_or_abort`, and `try_gc_alloc_stable_or_abort` for the raw-return
callers. `Failed` reaches `gc_alloc_failed`, which calls `handle_alloc_error`
as `alloc_typed_items_block_nursery` already does for the digit array;
`NoRoute` alone returns the caller to its `malloc_raw` path.

`gc_alloc_storage_box`, `clone_debugdata_ptr`, `getorcreate_debug_data` and
`alloc_dict_object` keep the collapsed form: the first three are guarded by
`try_gc_owns_object` at their walkers, and the last falls back to
header-bearing `malloc_typed`.

Replace the module doc's statement that the `Box::into_raw` fallback is
dropped as the hook's reliability is verified under the bench suite.

Assisted-by: Claude
@youknowone

Copy link
Copy Markdown
Owner Author

Reply to the Codex P1 and the two CodeRabbit findings, all three of which land on the same arm.

1. "Propagate MemoryError instead of aborting" — the premise does not hold for the arm this diff changes

Before this PR that arm did not raise MemoryError. It was .filter(|p| !p.is_null()) followed by lltype::malloc_raw, so a failed managed allocation produced a headerless payload and handed it back as an ordinary object. W_LongObject.value is registered as a plain gc-pointer offset — object_subclass_with_gc_ptrs(size_of::<W_LongObject>(), object_tid, vec![LONG_VALUE_OFFSET]) at pyre/pyre-jit/src/eval.rs:2125-2129, no custom trace and no try_gc_owns_object guard — so the collector forwards that slot unconditionally and reads a header that was never written. The change is silent corruption → abort, not MemoryError → abort.

The repo already has a two-tier convention for exactly this question, and the change follows it rather than inventing one:

  • infallible surface, abortsalloc_typed_items_block_nursery (pyre/pyre-object/src/object_array.rs:748) is try_… .unwrap_or_else(|| handle_alloc_error(…)). Its own doc says: "Once the GC hook is installed, allocation failure must remain a failure: a raw fallback would leave RBigInt._digits pointing outside the managed heap even though its descriptor traces that field as GcArray(Signed)."
  • fallible companion, carries the edgetry_alloc_typed_items_block_nursery (object_array.rs:768) already returns None on Some(null) and only falls back to alloc on None, feeding RBigInt::try_new / RBigInt::try_with_sizeRBigIntError::Memory. object_array.rs:837-841 states the rule: "The ordinary translated allocation path above remains infallible at the Rust type level, matching RPython's implicit exception edge; this entry point makes that edge explicit for Rust methods that already return Result."

So after this PR the RBigInt payload fails the same way its own digit array already failed. Threading MemoryError out of the infallible form would need an error channel these functions do not have — they return a bare pointer, are #[dont_look_inside], and run under JIT frames that cannot unwind. Where a channel exists it is already wired, and it already reports the failure rather than substituting a raw block.

Upstream never has this state: malloc_fixedsize cannot return null, because collect_and_reserve (incminimark.py:981-985) raises MemoryError itself. pyre's try_alloc_with_type_no_collect_body returns GcRef(0) once the nursery bump and spill_to_oldgen_or_null both fail, which is why the state needs an answer here at all.

2. "An absent GC_ALLOC_STABLE_HOOK while an independent GC is active is classified as NoRoute"

Not reachable in production. install_pyre_object_hooks (pyre/pyre-jit/src/eval.rs:3948-3996) registers the fast, regular, placement, stable, collecting and rooted-collecting hooks in one function body, and init_gc_subsystem runs install_gc_into_backend() at eval.rs:4062 before PYRE_OBJECT_HOOKS_INSTALLED.call_once(install_pyre_object_hooks) at eval.rs:4070. There is no ordering in which the fast hook answers while the stable hook is absent — only a unit test registering hooks individually can build that state.

3. Deliberately not changed: try_alloc_typed_items_block (the stable one)

object_array.rs:842 does still conflate the two states — try_gc_alloc_stable_raw collapses them and it falls through to alloc_zeroed. That site is not the corruption class, so it is out of scope rather than overlooked: its consumers are W_ListObject.int_items.block / float_items.block, and list_object_custom_trace guards both with try_gc_owns_object (eval.rs:933-940), so a std::alloc block there is a supported mixed population the walker leaves in place. Turning that fallback into MemoryError would convert a survivable managed-heap exhaustion into an exception while the system heap still has room.

The test that decides any site here is whether the walker that reaches the pointer carries an ownership guard — not how the fallback is spelled. That is why three sites in rbigint.rs and one in pyframe.rs changed and the guarded ones (gc_alloc_storage_box, FrameDebugData, alloc_dict_object) did not.

commented by Claude

@youknowone
youknowone merged commit 2adc3f5 into main Aug 4, 2026
14 of 17 checks passed
@youknowone
youknowone deleted the rbigint branch August 4, 2026 21:42
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