Skip to content

jit: module-scope LOAD_NAME builtins fold; interpreter: shutdown module teardown - #1187

Merged
youknowone merged 6 commits into
mainfrom
agent/stdlib-foundations
Aug 13, 2026
Merged

jit: module-scope LOAD_NAME builtins fold; interpreter: shutdown module teardown#1187
youknowone merged 6 commits into
mainfrom
agent/stdlib-foundations

Conversation

@youknowone

@youknowone youknowone commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Two independent pieces of work, plus the shutdown teardown they uncovered.

jit: fold the builtins fallback for module-scope LOAD_NAME

try_walker_load_global_cell_fold already folded a name that misses the module
dict and resolves through get_builtin().getdictvalue.
try_walker_load_name_cell_fold stopped at emit_module_dict_cell_fold, so a
module-scope LOAD_NAME of a builtins-only name residualized bh_load_name_fn
on every iteration. The shared leg now lives in emit_builtins_cell_fold and
both folds call it; the guard sequence is unchanged.

The discriminator was not the call but the lookup: binding the same builtin to a
module global (_len = len) erased the residual. Inside a function the read
compiles to LOAD_GLOBAL and already folded, so the gap was module-scope only.

2.4M iterations, module scope time final ops
total + len(s) 0.2635 s 30, call_may_force=1
after the fix 0.0119 s 21, call_may_force=0
_len = len control 0.0205 s 21, 0 (unchanged)

Faithfulness: at module scope load_name_value skips the locals probe and calls
load_global_value, i.e. globals then get_builtin().getdictvalue — the
pyopcode LOAD_NAME_load_global chain. A non-module globals is refused by
walker_pin_namespace_version, which is what closes the "a plain dict slot
lookup returns None, meaning absent" trap.

bench/synth: gate it

There was no test anywhere for either fold. load_name_builtin_cell_fold.py
runs the hot loop at module scope and then rebinds len, so the module dict
version? bump has to invalidate the folded loop. Ceiling 8 against measured
1.6x dynasm / 2.1x cranelift / 1.9x wasm; the residual form this gates measured
about 140x, and the pypy denominator sits near the execution floor so the ratios
moved by about a quarter between runs.

The shape is load-bearing and is documented in the file: inside a function the
read compiles to LOAD_GLOBAL, and a module-scope del of a global drops
mc_entered to 0 and runs the loop interpreted.

interpreter: clear module globals at shutdown

finalize_runtime released only __main__'s globals, so an object stored in
another module's namespace was never finalized — struct.x = C() with a
__del__ printed nothing at exit. This is finalize_modules /
_PyModule_ClearDict: snapshot sys.modules in insertion order, detach every
entry except sys and builtins (the unraisable path reads their live dicts
while released modules are finalized), then walk newest-first and clear each
module dict in two name passes — single leading underscore, then every name but
__builtins__ — assigning None rather than deleting.

Keeping __builtins__ bound, the underscore-first pass, the reverse import
order and skipping sys/builtins are the upstream mitigation for "clearing a
module invalidates globals a pending callback needs"; no extra guard was added.

An earlier attempt cleared only values whose type carried hasuserdel and was
removed in 55e9816, which recorded test.test_struct as FAIL. That baseline
entry returns to PASS. Note that FAIL is module-granular, so it had also
ungated every other test in that module.

Verification

  • cargo fmt --check clean
  • cargo test --all --no-default-features --features dynasm7790 passed, 0 failed
  • pyre/cpython_tests/run.py --backend dynasm209 PASS, 0 FAIL, no regressions
  • pyre/check.py --synthetic-only --backend dynasm,cranelift,wasm on the new bench — 3/3 backends pass
  • Shutdown behaviour pinned against CPython and pypy3 on 7 probes: global shadow
    mid-loop, builtins rebind, unshadow via del, del builtins.abs
    NameError, globals()[...] insert, class body inside the loop, and
    __builtins__ rebound to a dict.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved module-level name resolution, including correct fallback to builtins and invalidation when names are shadowed.
    • Added more compatible HMAC, PBKDF2-HMAC, scrypt, and BLAKE2 behavior.
    • Added a default 32 MiB scrypt memory limit when no limit is specified.
  • Bug Fixes

    • Improved interpreter shutdown cleanup and module finalization ordering.
    • Corrected handling of unsupported digest modules and HMAC type relationships.
  • Tests

    • Added benchmark coverage and JIT statistics for builtin lookup, cache invalidation, and loop behavior.

@coderabbitai

coderabbitai Bot commented Aug 12, 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: 6 seconds

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: 40e0b87e-06cb-4624-9fa8-52e33977cfc4

📥 Commits

Reviewing files that changed from the base of the PR and between 2482351 and 0e837d4.

📒 Files selected for processing (13)
  • .github/workflows/pyre-ci.yml
  • pyre/bench/synth/load_name_builtin_cell_fold.cranelift.jitstats
  • pyre/bench/synth/load_name_builtin_cell_fold.dynasm.jitstats
  • pyre/bench/synth/load_name_builtin_cell_fold.py
  • pyre/bench/synth/load_name_builtin_cell_fold.wasm.jitstats
  • pyre/bench/synth/str_fstring.cranelift.jitstats
  • pyre/check.py
  • pyre/cpython_tests/baseline.json
  • pyre/pyre-interpreter/src/importing.rs
  • pyre/pyre-interpreter/src/module/_blake2/_blake2_app.py
  • pyre/pyre-interpreter/src/module/_hashlib/mod.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
  • pyre/pyrex/src/lib.rs

Walkthrough

The PR adds JIT builtin fallback folding for module-scope LOAD_NAME, revises runtime module shutdown ordering, updates hashlib and BLAKE2 behavior, and adds a benchmark with backend JIT statistics.

Changes

JIT builtin lookup

Layer / File(s) Summary
Shared builtin-cell specialization
pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
LOAD_GLOBAL and module-scope LOAD_NAME use shared builtin-cell folding with namespace identity, version, and validity guards.
Builtin lookup benchmark validation
pyre/bench/synth/load_name_builtin_cell_fold.py, pyre/bench/synth/load_name_builtin_cell_fold.*.jitstats
The benchmark measures builtin len resolution before and after global shadowing. JIT statistics record two guard failures and two compiled loops.

Runtime shutdown teardown

Layer / File(s) Summary
Named module release
pyre/pyre-interpreter/src/importing.rs
Shutdown release returns WTF-8 module names with module objects and retains sys and builtins.
Ordered module cleanup
pyre/pyrex/src/lib.rs
Finalization clears module dictionaries in two passes, runs finalizers between module clears, and releases detached modules last.

Hashlib behavior

Layer / File(s) Summary
HMAC and PBKDF2 digest handling
pyre/pyre-interpreter/src/module/_hashlib/mod.rs
HMAC declares HASH as its base type. HMAC and PBKDF2 report unsupported digest modules and algorithms with UnsupportedDigestmodError.
Scrypt and BLAKE2 limits
pyre/pyre-interpreter/src/module/_hashlib/mod.rs, pyre/pyre-interpreter/src/module/_blake2/_blake2_app.py
Scrypt uses a 32 MiB default for maxmem=0 and reports memory-limit failures with an OpenSSL-style error. BLAKE2 applies updated key, salt, and personalization length handling.

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

Mergeability Score: 🟠 High · up to cc4f2

The current changes still contain a lookup-folding path that can return the wrong value and a BLAKE2 input path that may exhaust memory instead of rejecting oversized input; the added benchmark also fails reported lint rules. Merge should wait for these issues to be addressed.

Possibly related PRs

Poem

I’m a rabbit tracing builtins through the fold,
While shutdown clears modules in order, controlled.
Hashes gain limits, errors speak clear,
Two loops compile as benchmarks cheer.
Hop, hop—the runtime is tidy and bright!

🚥 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 identifies both main changes: the module-scope LOAD_NAME builtins fold and interpreter shutdown module teardown.
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 agent/stdlib-foundations

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.

@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: cc4f22d2a0

ℹ️ 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".

Comment thread pyre/pyrex/src/lib.rs Outdated
Comment on lines +1255 to +1256
clear_shutdown_module_dict(dict);
collect_and_run_finalizers(ec_ptr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Run finalizers between the module-clear passes

When an imported module stores a finalizable object under a private name such as _obj, and its __del__ reads a public module global, clear_shutdown_module_dict first replaces _obj but then clears every public global before this collection runs. Unlike CPython's refcounting _PyModule_ClearDict, assigning None does not immediately finalize the object here, so the underscore-first pass provides no protection and __del__ observes its dependencies as None. Collect after releasing private bindings (or otherwise preserve per-binding finalization ordering) before clearing the second pass.

AGENTS.md reference: AGENTS.md:L231-L233

Useful? React with 👍 / 👎.

@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: 3

🤖 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/bench/synth/load_name_builtin_cell_fold.py`:
- Around line 13-20: Update the benchmark loops to avoid unused loop-variable
warnings, and replace the module-scope lambda assignment to len with a named
function that preserves the constant-returning behavior and unused argument.
Retain the module-scope len binding for invalidation coverage, adding an
explicit A001 suppression for the intentional builtin shadowing.

In `@pyre/pyre-interpreter/src/module/_hashlib/mod.rs`:
- Around line 856-875: Update the BLAKE2 parameter handling around the
max_key_size, salt_size, and person_size checks to obtain length-only buffer
views for key, salt, and person before calling read_hash_buffer. Validate each
view’s length against its corresponding limit first, then copy only validated
buffers so oversized memoryviews return ValueError without allocating their full
contents.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs`:
- Line 13171: Update the builtin-fold path around emit_builtins_cell_fold to
return Ok(false) when live_globals is null or does not equal w_globals,
preventing specialization against a non-authoritative dictionary. Preserve the
existing fold only when both references identify the same globals dictionary.
🪄 Autofix

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: 9cbcb7bf-e23a-4665-b594-949e756af029

📥 Commits

Reviewing files that changed from the base of the PR and between 3c15ade and cc4f22d.

📒 Files selected for processing (9)
  • pyre/bench/synth/load_name_builtin_cell_fold.cranelift.jitstats
  • pyre/bench/synth/load_name_builtin_cell_fold.dynasm.jitstats
  • pyre/bench/synth/load_name_builtin_cell_fold.py
  • pyre/bench/synth/load_name_builtin_cell_fold.wasm.jitstats
  • pyre/pyre-interpreter/src/importing.rs
  • pyre/pyre-interpreter/src/module/_blake2/_blake2_app.py
  • pyre/pyre-interpreter/src/module/_hashlib/mod.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
  • pyre/pyrex/src/lib.rs
💤 Files with no reviewable changes (1)
  • pyre/pyre-interpreter/src/module/_blake2/_blake2_app.py

Comment on lines +13 to +20
for i in range(N):
total = total + len(s)
print(total)

len = lambda x: 100
shadowed = 0
for i in range(M):
shadowed = shadowed + len(s)

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 | 🟡 Minor | ⚡ Quick win

Make the benchmark pass Ruff.

Ruff reports unused loop variables, builtin shadowing, lambda assignment, and an unused lambda argument. Keep the module-scope len binding for invalidation coverage, but use a named function and an explicit A001 suppression.

Proposed fix
-for i in range(N):
+for _i in range(N):
     total = total + len(s)
 print(total)

-len = lambda x: 100
+def len(_x):  # noqa: A001
+    return 100
+
 shadowed = 0
-for i in range(M):
+for _i in range(M):
     shadowed = shadowed + len(s)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for i in range(N):
total = total + len(s)
print(total)
len = lambda x: 100
shadowed = 0
for i in range(M):
shadowed = shadowed + len(s)
for _i in range(N):
total = total + len(s)
print(total)
def len(_x): # noqa: A001
return 100
shadowed = 0
for _i in range(M):
shadowed = shadowed + len(s)
🧰 Tools
🪛 Ruff (0.16.1)

[warning] 13-13: Loop control variable i not used within loop body

Rename unused i to _i

(B007)


[error] 17-17: Variable len is shadowing a Python builtin

(A001)


[error] 17-17: Do not assign a lambda expression, use a def

Rewrite len as a def

(E731)


[warning] 17-17: Unused lambda argument: x

(ARG005)


[warning] 19-19: Loop control variable i not used within loop body

Rename unused i to _i

(B007)

🤖 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/bench/synth/load_name_builtin_cell_fold.py` around lines 13 - 20, Update
the benchmark loops to avoid unused loop-variable warnings, and replace the
module-scope lambda assignment to len with a named function that preserves the
constant-returning behavior and unused argument. Retain the module-scope len
binding for invalidation coverage, adding an explicit A001 suppression for the
intentional builtin shadowing.

Source: Linters/SAST tools

Comment on lines +856 to +875
let (max_key_size, salt_size, person_size) = match name {
"blake2b" => (64, 16, 16),
"blake2s" => (32, 8, 8),
_ => unreachable!(),
};
if key.len() > max_key_size {
return Err(crate::PyError::value_error(format!(
"maximum key length is {max_key_size} bytes"
)));
}
if salt.len() > salt_size {
return Err(crate::PyError::value_error(format!(
"maximum salt length is {salt_size} bytes"
)));
}
if person.len() > person_size {
return Err(crate::PyError::value_error(format!(
"maximum person length is {person_size} bytes"
)));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- target context ---'
sed -n '800,890p' pyre/pyre-interpreter/src/module/_hashlib/mod.rs
printf '%s\n' '--- read_hash_buffer definitions and calls ---'
rg -n -C 5 'read_hash_buffer' pyre
printf '%s\n' '--- BLAKE2 constructor call sites ---'
rg -n -C 4 '_blake2_new|blake2b|blake2s' pyre/pyre-interpreter/src/module/_hashlib

Repository: youknowone/pyre

Length of output: 22416


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- read_hash_buffer implementation ---'
sed -n '955,1010p' pyre/pyre-interpreter/src/module/_hashlib/mod.rs
printf '%s\n' '--- buffer protocol implementation references ---'
rg -n -C 5 'PyBUF_SIMPLE|buffer.*view|BufferView|view.*len|as_bytes|to_vec|Vec::from|copy_from_slice' pyre/pyre-interpreter/src pyre/pyre-object/src | head -n 240
printf '%s\n' '--- BLAKE2 app-level validation ---'
sed -n '1,180p' pyre/pyre-interpreter/src/module/_hashlib/_hashlib_app.py

Repository: youknowone/pyre

Length of output: 21510


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- simple_buffer_bytes definition ---'
rg -n -C 10 'fn simple_buffer_bytes|simple_buffer_bytes' pyre
printf '%s\n' '--- buffer view types and byte conversion ---'
rg -n -C 8 'struct BufferView|impl BufferView|fn as_bytes|pub fn as_bytes|Buffer::|getbuffer|buffer.*bytes' pyre/pyre-object/src pyre/pyre-interpreter/src | head -n 320
printf '%s\n' '--- large-buffer constructors and allocation paths ---'
rg -n -C 5 'bytearray|w_bytearray|memoryview|Buffer::Owned|Vec<u8>|with_capacity' pyre/pyre-object/src | head -n 320

Repository: youknowone/pyre

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- buffer acquisition implementation ---'
sed -n '8460,8615p' pyre/pyre-interpreter/src/baseobjspace.rs
printf '%s\n' '--- SimpleBufferBytes definition ---'
rg -n -C 12 'struct SimpleBufferBytes|impl SimpleBufferBytes|enum BufferRequest' pyre/pyre-interpreter/src/baseobjspace.rs

Repository: youknowone/pyre

Length of output: 8542


🏁 Script executed:

#!/bin/bash
set -e
sed -n '8613,8755p' pyre/pyre-interpreter/src/baseobjspace.rs

Repository: youknowone/pyre

Length of output: 5373


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- _blake2 bindings and app-level callers ---'
rg -n -C 8 '_blake2_new|_blake2|blake2_new' pyre --glob '*.rs' --glob '*.py'
printf '%s\n' '--- buffer gather implementation ---'
rg -n -C 8 'fn memoryview_gather_bytes|memoryview_gather_bytes' pyre
printf '%s\n' '--- BLAKE2 tests and size limits ---'
rg -n -C 6 'maximum key length|maximum salt length|maximum person length|blake2b.*key|blake2s.*key|memoryview' pyre --glob '*test*' --glob '*.py' --glob '*.rs' | head -n 260

Repository: youknowone/pyre

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -e
sed -n '35,175p' pyre/pyre-interpreter/src/module/_blake2/_blake2_app.py

Repository: youknowone/pyre

Length of output: 5849


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re

hashlib = Path("pyre/pyre-interpreter/src/module/_hashlib/mod.rs").read_text()
base = Path("pyre/pyre-interpreter/src/baseobjspace.rs").read_text()
builtins = Path("pyre/pyre-interpreter/src/builtins.rs").read_text()

def body(source, signature, end_marker):
    start = source.index(signature)
    end = source.index(end_marker, start)
    return source[start:end]

blake = body(hashlib, "fn blake2_new(", "/// Keyword names")
reader = body(hashlib, "fn read_hash_buffer(", "/// Keyword names")
buffer = body(base, "fn buffer_bytes(", "/// Look up a descriptor")
gather = body(builtins, "pub(crate) unsafe fn memoryview_gather_bytes", "/// `PyBuffer_ToContiguous")

calls = [blake.index(f"read_hash_buffer(arg({i}))?") for i in (3, 4, 5)]
checks = [blake.index(token) for token in ("if key.len()", "if salt.len()", "if person.len()")]

assert calls == sorted(calls), "BLAKE2 parameter buffers are not read in source order"
assert max(calls) < min(checks), "A parameter buffer is checked before materialization"
assert "buf.as_bytes().to_vec()" in reader, "read_hash_buffer no longer copies the acquired view"
assert "memoryview_gather_bytes" in buffer, "buffer_bytes does not gather memoryview contents"
assert "-> Vec<u8>" in gather, "memoryview_gather_bytes is not Vec-returning"

print("read_hash_buffer copies acquired buffer contents")
print("buffer_bytes gathers memoryview contents into Vec<u8>")
print("all BLAKE2 key/salt/person buffers are materialized before size checks")
PY

Repository: youknowone/pyre

Length of output: 294


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path

hashlib = Path("pyre/pyre-interpreter/src/module/_hashlib/mod.rs").read_text()
base = Path("pyre/pyre-interpreter/src/baseobjspace.rs").read_text()
builtins = Path("pyre/pyre-interpreter/src/builtins.rs").read_text()

def after(source, start_text, end_text):
    start = source.index(start_text)
    end = source.index(end_text, start)
    return source[start:end]

blake = after(hashlib, "fn blake2_new(", "const NEW_KEYWORDS")
reader = after(hashlib, "fn read_hash_buffer(", "const NEW_KEYWORDS")
buffer = after(base, "fn buffer_bytes(", "/// Look up a descriptor")
gather = after(
    builtins,
    "pub(crate) unsafe fn memoryview_gather_bytes",
    "/// `PyBuffer_ToContiguous",
)

calls = [blake.index(f"read_hash_buffer(arg({i}))?") for i in (3, 4, 5)]
checks = [blake.index(token) for token in ("if key.len()", "if salt.len()", "if person.len()")]

assert calls == sorted(calls)
assert max(calls) < min(checks)
assert "buf.as_bytes().to_vec()" in reader
assert "memoryview_gather_bytes" in buffer
assert "-> Vec<u8>" in gather

print("read_hash_buffer copies acquired buffer contents")
print("buffer_bytes gathers memoryview contents into Vec<u8>")
print("all BLAKE2 key/salt/person buffers are materialized before size checks")
PY

Repository: youknowone/pyre

Length of output: 294


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path

hashlib = Path("pyre/pyre-interpreter/src/module/_hashlib/mod.rs").read_text()
base = Path("pyre/pyre-interpreter/src/baseobjspace.rs").read_text()
builtins = Path("pyre/pyre-interpreter/src/builtins.rs").read_text()

start = hashlib.index("fn blake2_new(")
param_reads = [
    hashlib.index(f"read_hash_buffer(arg({i}))?", start)
    for i in (3, 4, 5)
]
checks = [
    hashlib.index(token, start)
    for token in ("if key.len()", "if salt.len()", "if person.len()")
]

assert param_reads == sorted(param_reads)
assert max(param_reads) < min(checks)
assert "buf.as_bytes().to_vec()" in hashlib
assert "memoryview_gather_bytes" in base
assert "pub(crate) unsafe fn memoryview_gather_bytes" in builtins
assert "-> Vec<u8>" in builtins[builtins.index("pub(crate) unsafe fn memoryview_gather_bytes"):]

print("read_hash_buffer copies acquired buffer contents")
print("buffer_bytes gathers memoryview contents into Vec<u8>")
print("all BLAKE2 key/salt/person buffers are materialized before size checks")
PY

Repository: youknowone/pyre

Length of output: 327


Reject oversized BLAKE2 parameter buffers before copying them.

Use a length-only buffer view for key, salt, and person before read_hash_buffer copies them. A large memoryview can otherwise allocate its full contents and abort on OOM instead of returning ValueError.

🤖 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-interpreter/src/module/_hashlib/mod.rs` around lines 856 - 875,
Update the BLAKE2 parameter handling around the max_key_size, salt_size, and
person_size checks to obtain length-only buffer views for key, salt, and person
before calling read_hash_buffer. Validate each view’s length against its
corresponding limit first, then copy only validated buffers so oversized
memoryviews return ValueError without allocating their full contents.

Source: MCP tools

unsafe { pyre_object::w_dict_getitem_str(w_globals, "__builtins__") }
.unwrap_or(pyre_object::PY_NULL)
};
emit_builtins_cell_fold(ctx, op_pc, dst, dst_bank, w_globals, w_builtin, &name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Decline the builtin fold when the authoritative globals differ.

If live_globals differs from w_globals, module_dict_cell_slot_direct cannot prove that an ordinary dictionary lacks name. The call then pins and checks w_globals, but the residual lookup reads live_globals. If that dictionary contains name, the trace returns the builtin value instead of the global value.

Return Ok(false) whenever live_globals is null or differs from w_globals. Alternatively, perform and guard an exact lookup against live_globals.

Proposed fix
-        if !live_globals.is_null()
-            && live_globals as usize != w_globals as usize
-            && crate::state::module_dict_cell_slot_direct(live_globals, &name).is_some()
-        {
+        if live_globals.is_null() || live_globals as usize != w_globals as usize {
             return Ok(false);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
emit_builtins_cell_fold(ctx, op_pc, dst, dst_bank, w_globals, w_builtin, &name)
if live_globals.is_null() || live_globals as usize != w_globals as usize {
return Ok(false);
}
🤖 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-jit-trace/src/jitcode_dispatch/specialize.rs` at line 13171, Update
the builtin-fold path around emit_builtins_cell_fold to return Ok(false) when
live_globals is null or does not equal w_globals, preventing specialization
against a non-authoritative dictionary. Preserve the existing fold only when
both references identify the same globals dictionary.

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 0e837d4).
Updated: 2026-08-13T07:39:11.297Z

Files in the reviewed diff
.github/workflows/pyre-ci.yml
pyre/bench/synth/load_name_builtin_cell_fold.py
pyre/check.py
pyre/cpython_tests/baseline.json
pyre/pyre-interpreter/src/importing.rs
pyre/pyre-interpreter/src/module/_blake2/_blake2_app.py
pyre/pyre-interpreter/src/module/_hashlib/mod.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
pyre/pyrex/src/lib.rs

1. Regressions to PyPy parity introduced by this patch

  • pyre/pyrex/src/lib.rs:1147 ↔ pypy/interpreter/baseobjspace.py:481: “try_gc_collect()” introduces forced collection/finalizer execution; PyPy ObjSpace.finish() only waits for threads, runs atexit, marks finalizing, flushes streams, and shuts down started builtin modules.

  • pyre/pyrex/src/lib.rs:1328 ↔ pypy/interpreter/baseobjspace.py:481: “release __main__ globals … rebound to None” has no PyPy counterpart. PyPy does not clear __main__ during finish(), so this changes visibility and __del__ timing.

  • pyre/pyre-interpreter/src/importing.rs:1707 ↔ pypy/interpreter/baseobjspace.py:498: “detach import-cache entries” plus pyre/pyrex/src/lib.rs:1277 ↔ pypy/interpreter/baseobjspace.py:499 “clear detached module dictionaries” tears down all imported modules. PyPy only invokes shutdown() on started builtin modules; it neither empties sys.modules nor clears module dictionaries.

2. Other mismatches introduced by this patch

  • pyre/pyre-interpreter/src/module/_hashlib/mod.rs:861 ↔ lib_pypy/_blake2/__init__.py:42: moving BLAKE2 key/salt/person length validation behind all app-level tree-parameter validation changes first-exception ordering. PyPy validates salt/person before fanout (lib_pypy/_blake2/__init__.py:55) and key later (lib_pypy/_blake2/__init__.py:98); the patch now reports an invalid fanout before an oversized salt/person/key.

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

  • pyre/pyre-interpreter/src/module/_hashlib/mod.rs:481 ↔ lib_pypy/_hashlib/__init__.py:232: “cannot create _hashlib.HMAC instances” conflicts with PyPy’s public, directly constructible HMAC(HASH) class.

  • pyre/pyre-interpreter/src/module/_hashlib/mod.rs:496 ↔ lib_pypy/_hashlib/__init__.py:237: Pyre exposes digest_size as a getter only, while PyPy assigns the public attribute during HMAC.__init__; the same read-only treatment applies to Pyre’s name getter at mod.rs:491 versus PyPy’s assignment at lib_pypy/_hashlib/__init__.py:239.

4. Structural adaptations

  • pyre/pyre-interpreter/src/module/_hashlib/mod.rs:479 ↔ lib_pypy/_hashlib/__init__.py:232: Rust’s separate native W_Hmac payload needs macro-level base-type wiring (“base = hash_state_class::type_object()”) to represent PyPy’s class HMAC(HASH) inheritance.

  • pyre/pyre-interpreter/src/module/_hashlib/mod.rs:815 ↔ lib_pypy/_hashlib/__init__.py:430: the Rust backend lacks PyPy’s direct OpenSSL EVP_PBE_scrypt call, so it spells out OpenSSL’s nominal 32-MiB default for maxmem=0. Provider-private OpenSSL validation/accounting cannot be ported byte-for-byte.

  • pyre/pyre-interpreter/src/module/_hashlib/mod.rs:580 ↔ lib_pypy/_hashlib/__init__.py:552: Pyre normalizes a missing digestmod.__name__ to UnsupportedDigestmodError; PyPy lets the attribute access fail. This is the stated CPython-3.14 compatibility adaptation.

@youknowone
youknowone force-pushed the agent/stdlib-foundations branch from cc4f22d to 8baaf87 Compare August 13, 2026 01:52

@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: 8baaf874c4

ℹ️ 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".

Comment thread pyre/pyrex/src/lib.rs
Comment on lines +1202 to +1205
let keys: Vec<rustpython_wtf8::Wtf8Buf> = unsafe { pyre_object::w_dict_str_entries_wtf8(dict) }
.into_iter()
.map(|(name, _)| name)
.collect();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear globals stored under non-string keys

When a program stores a finalizable object as globals()[1] = C() or module.__dict__[1] = C(), w_dict_str_entries_wtf8 omits that binding, so this teardown never replaces it with None. The modules remain pinned through the last collection in clear_shutdown_modules—and native modules such as __main__ retain permanently rooted dictionaries—so the object stays reachable and its __del__ is never run before process::exit. The shutdown walk must also release non-string-keyed values, or perform a collection after the module roots are actually released.

AGENTS.md reference: AGENTS.md:L231-L233

Useful? React with 👍 / 👎.

Comment on lines +866 to +874
if salt.len() > salt_size {
return Err(crate::PyError::value_error(format!(
"maximum salt length is {salt_size} bytes"
)));
}
if person.len() > person_size {
return Err(crate::PyError::value_error(format!(
"maximum person length is {person_size} bytes"
)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve BLAKE2 buffer-error ordering

Moving these size checks into _blake2_new means they run only after the app-level wrapper has validated fanout, depth, and the remaining tree parameters. For example, hashlib.blake2b(salt=b'x' * 17, fanout=256) now reports the fanout error, whereas the prior PyPy-shaped implementation and CPython report the oversized-salt error; the same regression occurs for person combined with invalid fanout or depth. Keep byte-accurate buffer validation, but perform it at the original point before the later range checks so exception precedence remains compatible.

AGENTS.md reference: AGENTS.md:L231-L233

Useful? React with 👍 / 👎.

`try_walker_load_global_cell_fold` folds a name that misses the module dict
and resolves through `get_builtin().getdictvalue`.
`try_walker_load_name_cell_fold` ended at `emit_module_dict_cell_fold`, so
module-scope `LOAD_NAME` of such a name residualized `bh_load_name_fn` on
every iteration.

Move that leg into `emit_builtins_cell_fold` and call it from both folds.
The guard sequence is unchanged: the name must be absent from the module
dict, whose `version?` is pinned so a later shadowing insert fails
GUARD_NOT_INVALIDATED, and `emit_namespace_cell_fold` pins the builtins
dict's own `version?`.

A 2.4M-iteration module-scope `total + len(s)` loop compiles to 21 ops with
no `call_may_force`, matching the same loop with `len` bound to a module
global; it recorded 30 ops and one `call_may_force` before.

Assisted-by: Claude
The hot loop reads `len` at module scope, where the name misses the module
dict and resolves through the frame's builtin module.  The trailing
`len = lambda x: 100` plus a second loop pins the invalidation: the module
dict `version?` bump has to be seen, so the second loop prints 40000000.
Output matches CPython and PyPy.

Ceiling 8 against measured 1.6x dynasm, 2.1x cranelift, 1.9x wasm.  The pypy
denominator sits near the execution floor, so the ratios moved by about a
quarter between runs; the residual form this gates measured about 140x.

The shape is load-bearing: inside a function the read compiles to LOAD_GLOBAL,
which folded already, and a module-scope `del` of a global drops `mc_entered`
to 0 and runs the loop interpreted.

Assisted-by: Claude
`finalize_runtime` ended after the `ObjSpace.finish()` phases, so nothing held
only by a namespace was finalized: `struct.x = C()` whose class defines
`__del__` printed nothing at exit, and a `__main__` global's `__del__` did not
run either.

Restore `collect_and_run_finalizers`, `release_frees_nothing` and the `__main__`
newest-to-oldest release loop that #1158 removed.  A `__del__` reading a module
global needs a collection to run while the remaining names are still bound;
`test_start_new_thread_at_finalization` reads `_thread` and otherwise sees
`None`.

Port `finalize_modules` / `_PyModule_ClearDict` for the namespaces `__main__`
does not reach.  `release_sys_modules_for_shutdown` snapshots `sys.modules` in
insertion order and detaches every entry except `sys` and `builtins`, which the
unraisable path still reads while the released modules are finalized.
`clear_shutdown_modules` then walks the snapshot newest-first, skipping those
two, and clears each module dict in two name passes -- a single leading
underscore first, then every name but `__builtins__` -- assigning `None` rather
than deleting.  One collection follows the whole walk: a sweep per module costs
a full mark-and-sweep for each of the ~100 modules a bare `import unittest`
loads, which measured 905ms of teardown against 39ms for the single sweep, and
`test_regrtest` spends it once per subprocess.

`test.test_struct` and `test.test_threading` return to PASS in the baseline;
`test_struct_cleans_up_at_runtime_shutdown` and
`test_start_new_thread_at_finalization` are the tests they cover.

Assisted-by: Claude
guard_failures 659 -> 658.

Assisted-by: Claude
The suite ran twice per CI round: once inside `pyre/check.py` on the
macos-latest leg, and once in the dedicated `cpython-tests` job.  It dominates
this script's wall time, so make it opt-in -- `--cpython-suite` replaces
`--no-cpython-suite` and no CI job passes it, leaving the dedicated job as the
only place CI pays for the run.

The stage itself is unchanged and still skips off darwin-arm64.  Its docstring
no longer claims the CI job pins `runs-on: macos-latest`, which
`ci: make the CPython gate host-aware` changed to ubuntu-24.04.

Assisted-by: Claude
@youknowone
youknowone force-pushed the agent/stdlib-foundations branch from 8baaf87 to 0e837d4 Compare August 13, 2026 06:55
@youknowone
youknowone merged commit 6e50b9f into main Aug 13, 2026
11 of 12 checks passed
@youknowone
youknowone deleted the agent/stdlib-foundations branch August 13, 2026 06:56

@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: 0e837d457e

ℹ️ 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".

Comment thread pyre/pyrex/src/lib.rs
// reads a global reaches its own already-cleared namespace either way, and
// it costs a full mark-and-sweep for each of the ~100 modules a bare
// `import unittest` loads.
collect_and_run_finalizers(ec_ptr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drop module pins before the final sweep

When sys.modules contains a managed ModuleType subclass whose only remaining reference is the import cache, the snapshot roots every module at line 1261 and those roots are still active during this final collection. Consequently the module itself cannot become unreachable, and after _roots is dropped the function returns without another collection, so its user-defined __del__ never runs before process exit. Release the temporary module roots before performing the final sweep while retaining whatever core roots shutdown still needs.

AGENTS.md reference: AGENTS.md:L231-L233

Useful? React with 👍 / 👎.

youknowone added a commit that referenced this pull request Aug 13, 2026
…erves

#1187 re-recorded the shared `str_fstring.cranelift` baseline 659 -> 658. No
`linux` overlay exists, so the ubuntu leg reads that shared file, and it
observes 659 — its check.py leg on #1187 was already red on this line and has
stayed red on main since, including at d953ddc.

The shared file goes back to 659 and darwin's 658 moves into
`str_fstring.cranelift.darwin.jitstats`. The `darwin.github-actions` and
`win32.github-actions` overlays already carry 658 and are unchanged, so the
macos and windows legs read the same value as before.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 13, 2026
… observes

`str_fstring` reads a different `guard_failures` on each host, and the
overlays no longer matched. Both backends are now keyed the same way: the
shared file carries what linux and windows observe, and darwin gets its own
overlay.

  cranelift  linux 659   windows 658   darwin 658
  dynasm     linux 658   windows 658   darwin 659

For cranelift the shared file goes back to 659, undoing #1187's re-record —
no `linux` overlay exists, so the ubuntu leg reads that file and observes 659;
#1187's own ubuntu leg was already red on this line and main has stayed red
since, including at df365f9. darwin's 658 moves into a `.darwin` overlay
beside the existing `darwin.github-actions` and `win32.github-actions` ones,
which already read 658 and are unchanged.

For dynasm the shared file becomes 658 and the `win32.github-actions` overlay
is dropped, since windows observes the same 658; darwin's 659 moves into a
`.darwin` overlay. main fails this row on both its ubuntu and windows legs at
df365f9 with the identical `guard_failures 659 -> 658`.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 13, 2026
… observes

`str_fstring` reads a different `guard_failures` on each host, and the
overlays no longer matched. Both backends are now keyed the same way: the
shared file carries what linux and windows observe, and darwin gets its own
overlay.

  cranelift  linux 659   windows 658   darwin 658
  dynasm     linux 658   windows 658   darwin 659

For cranelift the shared file goes back to 659, undoing #1187's re-record —
no `linux` overlay exists, so the ubuntu leg reads that file and observes 659;
since, including at df365f9. darwin's 658 moves into a `.darwin` overlay
beside the existing `darwin.github-actions` and `win32.github-actions` ones,
which already read 658 and are unchanged.

For dynasm the shared file becomes 658 and the `win32.github-actions` overlay
is dropped, since windows observes the same 658; darwin's 659 moves into a
`.darwin` overlay. main fails this row on both its ubuntu and windows legs at
df365f9 with the identical `guard_failures 659 -> 658`.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 13, 2026
…1188)

* jit: carry NotVirtualStateInfoPtr.lenbound on the virtual-state pointer leaf

NotVirtualStateInfoPtr.__init__ (virtualstate.py:508-518) records
info.getlenbound(None).widen() on every non-virtual pointer leaf, and
_generate_guards (virtualstate.py:529-537) compares it before dispatching on
LEVEL_NONNULL / LEVEL_KNOWNCLASS / the base level, raising
VirtualStatesCantMatch("length bound does not match") when the incoming bound
is not within the expected range. An incoming leaf with no bound is treated as
IntBound.nonnegative().

pyre exported PtrInfo::Array and PtrInfo::Str as a bare NonNull leaf, so the
bound was dropped at export and no arm of generate_guards_for_entry_recursive
compared one.

Store it on VirtualStateInfoNode, which already carries the other per-instance
NotVirtualStateInfo attributes (position, position_in_notvirtuals), rather than
widening the VirtualStateInfo variants. Populate it at export for the four
pointer levels and preserve it across clone and deep_clone_node. The single
comparison site sits where upstream puts it, after the force-to-virtual branch
and the incoming-is-virtual rejection and before the level dispatch; both
VirtualState::generalization_of and VirtualState::generate_guards route through
that function, matching virtualstate.py:636-644 reaching the check through the
same generate_guards.

Moves nothing on the bench corpus: an A/B over check.py --backend dynasm with
and without the change reported 419/419 passed on both arms. Full gate green at
dynasm 420/420, cranelift 419/419, wasm 414/414.

Assisted-by: Claude

* jit: publish quasi-immutable deps and the invalidation flag on every compiled artifact

record_loop_or_bridge (compile.py:204-207) registers a trace's
quasi_immutable_deps against its loop token unconditionally, and it runs for
every compiled loop and bridge. pyre cannot register at that depth (the
dependency target is a pyre-interpreter watcher and majit-metainterp sits below
the pyre crates), so it stages onto last_quasi_immutable_deps plus
last_compiled_artifact_invalidation_flag and drains both from
register_quasi_immutable_deps in eval.rs. That drain takes the deps first and
then returns early when the flag is None, so any path that stages one without
the other silently discards the dependencies.

Three paths never reached it:

- compile_entry_bridge published neither the deps nor the flag.
- compile_simple_loop assigned the deps but never the flag, so the drain bailed.
- handle_fail called the drain from three of the four BridgeResolution arms;
  ResumeBlackhole was an empty block, and a bridge that compiled and attached
  still resolves that way — call_jit.rs returns it on the CALL_ASSEMBLER
  ca-finish-noreplay path. Hoist the single call out of the match so it runs for
  every resolution, matching record_loop_or_bridge running for every compiled
  bridge regardless of what the interpreter does next.

A compiled artifact whose deps never reached a watcher has a
GUARD_NOT_INVALIDATED watching an AtomicBool on nobody's list: a later
_version_tag or dict-version bump does not arm it and the artifact keeps
returning the pre-mutation constant.

Add MC_DIAG slots 75-77 (qmut_deps_simple_loop, qmut_deps_entry_bridge,
qmut_deps_blackhole_arm), each bumped only when that path publishes a non-empty
dependency list. Over 427 bench files under MAJIT_STATS: entry_bridge 10 across
10 benches, simple_loop 0, blackhole_arm 0 — so the entry-bridge path was
dropping dependencies in practice and the other two are latent.

Gate green: dynasm 422/422, cranelift 421/421, wasm 416/416.

Assisted-by: Claude

* jit: decay the jitcounter periodically, not only when a bound is reached

JitCounter.__init__ (counter.py:104-121) installs invoke_after_minor_collection
into translator._jit2gc, and the GC transformer reads it back out as
finished_minor_collection (framework.py:135-138), so it runs after every minor
collection and calls decay_all_counters() on every 32nd. decay_all_counters
(counter.py:266-278) states the purpose: "to gradually decay counters that
didn't reach their maximum. Thus if a counter is incremented very slowly, it
will never reach the maximum. This avoids altogether the JIT compilation of rare
paths."

pyre ported decay_all_counters and calls set_decay(40), so decay_by_mult is 0.96
in production, but only the warmstate.py:429 bound_reached call site was wired.
Once every hot key is compiled, bound_reached stops firing and the counters are
effectively monotonic, so a guard that fails sparsely still accumulates to the
bound and gets a bridge.

majit-gc gains a fn() hook invoked after minor_collections is incremented;
majit-trace registers it from JitCounter::new and keeps the 32-step count in
module statics. The hook body only touches two atomics: it runs inside a minor
collection, and reaching the counter table from the collector would re-enter a
borrow the GC does not hold. The decay is applied instead at the top of the next
JitCounter::tick. That is a deferral, not upstream's synchronous application;
the counters are only read at tick time, so the deferral is observationally
equivalent rather than identical.

Re-record guard_failures on six synth benches across the backends:
arith_int_bool 2214->2219 (dynasm, cranelift), exception_traceback_loop_forms
811->812 (dynasm, cranelift), generator_tree_recursion 1240->1243 (dynasm) and
->1241 (cranelift), inline_chain_depth_typeflip 3818->3820 (all three),
binary_int_overflow_local_resume 647->651 (wasm), recursion_memo_branch
4703->4728 (wasm). loops_compiled and bridges_compiled are unchanged on every
one of them: the decayed counters reach the bound later, so the same bridges are
compiled after more guard failures have accumulated. Nothing on this corpus is
rare enough for the decay to suppress a compilation outright.

Gate green: dynasm 422/422, cranelift 421/421, wasm 416/416.

Assisted-by: Claude

* jit: drain quasi-immutable deps on the CALL_ASSEMBLER bridge path, and start every bridge attempt without an artifact flag

`register_quasi_immutable_deps` is the only drain of
`last_quasi_immutable_deps`. It ran from the loop path and from the general
guard-failure path in `handle_fail`, but not from the two CALL_ASSEMBLER
callers of `trace_and_compile_from_bridge` in call_jit.rs, so a bridge
compiled through either of those staged its dependencies and no watcher
received the invalidation flag.

`trace_and_compile_from_bridge` now clears
`last_compiled_artifact_invalidation_flag` on entry; only `compile_bridge`
cleared it, so an attempt that gave up before reaching `compile_bridge` left
the previous compilation's flag in place for the drain to attach to.

Assisted-by: Claude

* bench: re-record the jit-stats baselines this branch moves

`arith_int_bool` guard_failures reads 2214 on every platform now, so the
shared files carry it and the darwin override is gone.
`class_body_exec_hot_loop` no longer compiles its bridge on dynasm and
cranelift; wasm still does.

Assisted-by: Claude

* bench: split str_fstring's jit-stats baselines by the value each host observes

`str_fstring` reads a different `guard_failures` on each host, and the
overlays no longer matched. Both backends are now keyed the same way: the
shared file carries what linux and windows observe, and darwin gets its own
overlay.

  cranelift  linux 659   windows 658   darwin 658
  dynasm     linux 658   windows 658   darwin 659

For cranelift the shared file goes back to 659, undoing #1187's re-record —
no `linux` overlay exists, so the ubuntu leg reads that file and observes 659;
since, including at df365f9. darwin's 658 moves into a `.darwin` overlay
beside the existing `darwin.github-actions` and `win32.github-actions` ones,
which already read 658 and are unchanged.

For dynasm the shared file becomes 658 and the `win32.github-actions` overlay
is dropped, since windows observes the same 658; darwin's 659 moves into a
`.darwin` overlay. main fails this row on both its ubuntu and windows legs at
df365f9 with the identical `guard_failures 659 -> 658`.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 26, 2026
…and a whole-heap collection per released `__main__` global (#1505)

* jit: decline bridge-tracing frames before install_current_frame

`install_current_frame` and `ExecutionContext::enter` both link a frame
into the `topframeref`/`f_backref` chain, and `execute_frame_plain`
reaches `enter` through `eval_frame_plain_with_resume`.  The
bridge-tracing decline sat between the two calls, so the frame was
linked twice and the second link made `f_backref` name the frame
itself; `walk_pyframe_roots` follows `f_backref` with no cycle guard.

Move the decline above `install_current_frame`, where every other
decline in the function already sits, and add a `debug_assert_ne!` in
`enter` for the same pairing.

Assisted-by: Claude

* function: barrier the old-gen Function after its initializing write

`function_new_impl` publishes every field with one `ptr::write`, which
no write barrier sees, so an old-gen Function that never joins the
remembered set is not scanned by a minor collection.  Call
`function_write_barrier` after the write.

Assisted-by: Claude

* gate-triage: add the PYRE_GC_SIZE_AUDIT row

`every_live_gate_has_a_triage_entry` reports it: the gate is read from
the environment in `majit-gc/src/collector.rs` and had no entry in
`pyre/gate-triage.md`.  List it in §6c and say what it checks.

Assisted-by: Claude

* shutdown: stop collecting once per released `__main__` global

`finalize_runtime` releases `__main__`'s globals newest-to-oldest and called
`collect_and_run_finalizers` after every name whose value was not an exact
scalar or a `sys.modules`-registered module.  `release_frees_nothing` answers
`false` for every function, class and container, so that was a whole-heap
mark-and-sweep per binding: `PYRE_GC_DIAG` reports `major` = names + 3, and a
script binding 400 globals spent 3015ms against pypy3's 54ms.  `runpy.
_run_module_as_main` execs a module's code into the real `__main__` dict, so
`pyre -m inspect sys` walked `inspect`'s 182 names -- 1854ms, of which the
Python work is 104ms.  `test.test_inspect` pays it in four subprocesses.

The collection is there to *find* a finalizer, so replace it with two O(1)
questions where the collection is O(heap).  `gc_has_pending_finalizers` asks
whether the collector owes any delivery at all: `deal_with_objects_with_
finalizers` is the one pass that hands control back to the program, and
`rawrefcount` answers for the whole of itself.  `gc_object_finalizer_pending`
asks whether the released object is one it owes, reading the
`FINALIZER_REGISTERED` bit `register_finalizer` sets -- the whole of
`hasuserdel`, plus the `_io`, coroutine and weakref-lifeline registrations
whose types carry no such flag.

The destructor lists are deliberately not consulted.  A destructor is RPython's
light finalizer and may not run interpreter code, so where in the walk it runs
is not observable; in pyre they are drop glue (`function_destructor`,
`type_object_destructor`, `pycode_destructor`, `storage_box_destructor`), which
puts every function, type and code object on them and makes a predicate that
counted them answer non-zero in every program.

`major` for 400 globals 403 -> 3, 3015ms -> 115ms; `pyre -m inspect sys`
1854ms -> 179ms; `test.test_inspect` 7.56s -> 3.17s with its 362 tests still
passing.  `test_start_new_thread_at_finalization` and
`test_struct_cleans_up_at_runtime_shutdown`, the two tests #1187 restored this
loop for, still pass, as do the `__del__`-ordering and unclosed-file-flush
shapes.

Releasing a *container* of a finalizable is where this stops being exact: no
O(1) test on the container sees the object it frees, so that `__del__` runs at
the walk's trailing collection and reads `None` for the globals released after
it.  It still runs.  `PYRE_GC_DIAG` gains `teardown_released`/`teardown_swept`
so a run that falls back to a sweep per name is visible without a rebuild.

Assisted-by: Claude

* jit: keep an operand store out of the virtualizable identity slot

`virtualizable_boxes` is `[static fields][array slots][identity]`, and the
trailing identity entry is the box `_nonstandard_virtualizable` compares every
vable op's frame against.  STORE_DEREF's codewriter lowering read the cell with
`emit_load_fast_ref!` -- the LOAD_FAST opcode, a `getarrayitem_vable_r` plus a
`pushvalue` -- while the value it is about to pop still held the top of the
stack, so the scratch push addressed one slot past `co_stacksize`, which is
that identity entry.

A class body reaches it with no headroom.  `class C: def m(self): pass`
compiles to `co_stacksize=1` with a single `__classdict__` cellvar, so
`locals_cells_stack_w` is 2 slots and `STORE_DEREF __classdict__` runs with the
`LOAD_LOCALS` result on the stack: the push lands on flat index 7 of an 8-slot
shadow.  The trace then records `PtrEq(frame, cell)` + `GuardFalse`, declares
the real frame nonstandard, and writes `PyFrame.last_instr`, the force token
and the exit flush into the cell.  The same class statement in a loop past the
function-entry threshold dies reading the cell's `locals_cells_stack_w` at
`obj - 4` inside the write-barrier fast path -- `EXC_BAD_ACCESS` at
0xfffffffffffffffc, one unsymbolicated frame.

`emit_read_local_ref!` emits the read alone, which is what `pyopcode.py`
STORE_DEREF's `cells[varindex]` is.  The six other `emit_load_fast_ref!` +
pop scratch reads are net-push or run at depth 0, so their slot is in range.

Two bounds close the class rather than the one instance.
`vable_array_flat_index` restores `_get_arrayitem_vable_index`'s
`assert 0 <= index < get_array_length` as a `None` return -- the read paths
already fall back to a heap `GETARRAYITEM_GC_*` and the write path reports
`VableArrayStore::OutOfVable` -- and `set_virtualizable_entry_at` refuses the
identity slot outright.  `write_stack_slot` / `swap_stack_slots` bound at
`len - 1`: the existing `>= len` test was one slot too loose and passed exactly
this store through.

The regression test scores the exit status.  The crash prints nothing on either
stream, so a harness reading output calls it a pass.

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