Skip to content

PyFrame drops execution_context and w_globals; the #1372 review follow-ups - #1388

Open
youknowone wants to merge 28 commits into
mainfrom
fib_recursive
Open

PyFrame drops execution_context and w_globals; the #1372 review follow-ups#1388
youknowone wants to merge 28 commits into
mainfrom
fib_recursive

Conversation

@youknowone

@youknowone youknowone commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Follow-up to #1372. Seven of the first eight commits answer review comments left on that PR after it was pushed; the eighth removes two PyFrame fields. The six that follow keep the fold that removal cost, correct two jit-stats baselines it re-recorded as improvements when they were regressions, convert this branch's line-number citations to symbols, and answer the review on this PR.

IncrementalMiniMarkGC — a surviving pin has to disable the minor root-walk skip

do_collect_nursery sampled any_pinned_object_kept into a discarded local and then always announced ExtraRootWalkKind::Minor. Four walkers read that kind and each skips a clean area on a minor: walk_rd_consts_refs, the front TargetToken walk, the method cache scan, and the mapdict scan.

collect_roots_in_nursery derives use_jit_frame_stoppers = not any_pinned_object_from_earlier from that same flag and hands it to walk_roots as is_minor. A pinned object created before the previous minor is still in the nursery and was never promoted, so the skip drops the only edge reaching it. The sampled flag now selects the announced kind, which makes all four walkers conservative together rather than patching each gate.

The test records the kind each registered walker sees across two minors with a pinned root alive. Without the fix the pair reads [Minor, Minor].

compute_gcmap — the cranelift site actually needed the skip

#1372 documented if arg is None: continue at the three gcmap sites and claimed cranelift could not reach a hole because spill_guard_fail_args routes through resolve_opref. The assert is real but all three of its call sites are call_may_force / call_release_gil, and infer_fail_arg_types types OpRef::NONE as Ref. Both type resolvers mint Ref for a virtual's hole, so a type-only test marks a rootless slot. The skip is now implemented, not just described.

Nursery::reset_range and the pinned next_free

reset_range is a safe pub fn that writes raw memory and upheld its bounds with debug_assert only; it now clamps to the nursery extent and returns on an empty range. next_free is clamped to next_top before the new barrier bounds are published.

PyFrame.execution_context and PyFrame.w_globals

interp_jit.py lists pycode, valuestackdepth and debugdata as the virtualizable scalars, and ec is a red in reds = ['frame', 'ec'], not a frame field. execution_context is removed and its readers take the current activation's context as space.getexecutioncontext() does; normalize_raise_varargs_fn no longer needs a frame pointer for it.

w_globals goes the same way. Upstream bcd8653e5ec dropped it when PyCode.frame_stores_global landed, and InstanceRepr._parse_field_list skips a _virtualizable_ name with no concrete field, so the stale list entry produced no scalar. get_w_globals reads the code object and FrameDebugData carries the snapshot the debug path wants. PYFRAME_W_GLOBALS_OFFSET becomes FRAME_DEBUG_DATA_W_GLOBALS_OFFSET, the vable scalar table loses its fourth entry, and failed_attr_cleanup narrows to u8.

majit_metainterp::backend_runtime collects set_jitframe_gc_type_id and install_gc_standalone behind the same feature selection that constructs BackendImpl, so a frontend does not repeat the test and disagree under workspace feature unification.

Smaller review items

  • finish_trace_for_parity_preserves_captured_snapshots carries a full assert_eq! body but had no #[test], so its #[allow(dead_code)] was hiding the fact that it never ran. It runs and passes.
  • may_force_test_lock in pyjitpl has no caller in any cfg — the cranelift backend has its own copy, which is the one every may_force test takes. The TID constant in a_named_field_resolves_by_name_through_an_ambiguous_offset is unused; that test calls field_specs_from_layout directly.
  • Ten more unreached helpers retired across compile.rs, jitdriver.rs, optimizeopt/{guard,unroll,vstring}.rs and resume.rs, continuing dead_code audit: a _json rooting gap, gcmap comments, per-item lint allows, and the retired SSARepr allocator #1372's audit. opencoder::refresh_from_gc is deliberately kept: rooted_ref_indices is populated by _encode_ptr and read only by release_roots, and walk_active_trace_refs never touches _refs, so _refs can retain pre-move addresses until a caller wires it.

get_w_globals promotes pycode

pyframe.py get_w_globals ends in jit.promote(self.pycode).w_globals. The port read
self.pycode unpromoted, which was harmless only while w_globals was still a virtualizable
scalar. With the field gone the code object is the sole source, so without the promote the
globals read is a load the optimizer cannot fold.

Quasi-immutable ownership and the recursive bridge's setup-abort test

quasiimmut.py QuasiImmut registers the owning JitCellToken; the registry held one
Arc<AtomicBool> per compiled artifact, so an invalidation reached only the fragments
registered by then. It now holds Arc<dyn QuasiImmutLoopToken>. can_inline_callable and
disable_noninlinable_function are reached through their typed green-key forms.

Trace gains recorded_ops_total, which cut does not rewind. The bridge driver's
setup-abort test reads it beside num_ops, so a body walk whose speculative operations were
cut back to the setup position no longer reads as a deterministic setup failure that
permanently declines its source guard.

80 jit-stats baseline files over 27 fixtures move with it.

Two of those re-recorded baselines were regressions, not improvements

  • raise_reg_unbound_jitstress went fbw_rolled_back_with_effects 0 -> 1. The kept-slot hole
    report in collect_outer_active_boxes consults the walk mirror and the edge-move recovery
    but not the virtualizable shadow, which the resolution directly below it already treats as
    authoritative — so it declined a case its own resolver handles, at a capture point a residual
    had already run past. Restored to fbw 0, loops_aborted 1, loops_compiled 9.
  • polymorphic_binary_receiver lost a bridge (3 -> 2, guard_failures 788 -> 845).
    replace_movable_load_global_namespace_with_frame_globals returned without substituting the
    null LOAD_GLOBAL namespace placeholder when the inlined callee's frame red was unseeded, so
    try_walker_load_global_cell_fold declined on the null namespace before reaching its
    builtins leg — which already handles frame_ptr == 0. The callee's own __globals__ is in
    inline_callee_consts; it is now named directly, immovable namespaces only. Restored to
    bridges 3, guard_failures 788, loops_aborted 0.

Review follow-up (#1388)

  • Codex: w_code_frame_stores_global read PyCode.w_globals and stored into it
    unsynchronized. pycode.py frame_stores_global does that pair under the GIL; pyre is
    free-threaded, so two threads first running one code object in different globals could both
    read the null and both answer false — and false is the answer that makes a frame take its
    globals from the code object. With PyFrame.w_globals gone the loser's frame then read the
    winner's namespace. The publication is now a compare-exchange, answered against whichever
    pointer won, matching what w_code_getconstant already does for co_consts_w.
  • Codex: pyre-wasm-test's launcher called PyFrame::new_with_context without installing the
    context in the OS-thread locals. With the frame-owned context gone that left
    space.getexecutioncontext() null for every program it runs, so
    eval_frame_plain_with_resume took its context-free arm — no enter/leave, no
    call_trace — and sys.settrace was inert. Fixed, and the enumeration in PyFrame::new's
    comment now names all three launchers.

Citations

Twelve line-number citations this branch added are converted to symbols
(maybe_compile_and_run, compute_gcmap, get_w_globals, PyFrame._virtualizable_,
PyPyJitDriver.reds), which scripts/check-new-line-citations.py now enforces on PRs.

Rebased onto 5bf59e1f008. #1400's ec_seed fallback that reads PyFrame.execution_context
off the root frame is dropped — that field no longer exists — while its two other consumers
(sym.execution_context and argboxes_r[ec_reg]) and its bridge-setup tally are kept.
bridge_global_fold_invalidate_hot, which both sides re-recorded, is resolved to the new base's
values and measured there.

Local gate on the rebased tree: dynasm 456/456, cranelift 456/456, wasm 448/448, zero jit-stats
diffs.

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 127 files, which is 27 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 360e73c1-381b-464f-a111-5fda20c50006

📥 Commits

Reviewing files that changed from the base of the PR and between d18224f and 6ce2bb3.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (127)
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-backend/src/lib.rs
  • majit/majit-gc/src/collector.rs
  • majit/majit-gc/src/nursery.rs
  • majit/majit-ir/src/descr.rs
  • majit/majit-ir/src/lib.rs
  • majit/majit-metainterp/src/compile.rs
  • majit/majit-metainterp/src/history.rs
  • majit/majit-metainterp/src/jitcode/assembler.rs
  • majit/majit-metainterp/src/jitdriver.rs
  • majit/majit-metainterp/src/optimizeopt/guard.rs
  • majit/majit-metainterp/src/optimizeopt/heap.rs
  • majit/majit-metainterp/src/optimizeopt/info.rs
  • majit/majit-metainterp/src/optimizeopt/unroll.rs
  • majit/majit-metainterp/src/optimizeopt/vstring.rs
  • majit/majit-metainterp/src/pyjitpl.rs
  • majit/majit-metainterp/src/recorder.rs
  • majit/majit-metainterp/src/resume.rs
  • majit/majit-metainterp/src/trace_ctx.rs
  • majit/majit-metainterp/src/warmstate.rs
  • pyre/bench/fib_recursive.cranelift.jitstats
  • pyre/bench/fib_recursive.dynasm.jitstats
  • pyre/bench/fib_recursive.wasm.jitstats
  • pyre/bench/synth/bridge_global_fold_invalidate_hot.cranelift.jitstats
  • pyre/bench/synth/bridge_global_fold_invalidate_hot.dynasm.jitstats
  • pyre/bench/synth/bridge_global_fold_invalidate_hot.wasm.jitstats
  • pyre/bench/synth/bridge_recursion_overflow.cranelift.jitstats
  • pyre/bench/synth/bridge_recursion_overflow.dynasm.jitstats
  • pyre/bench/synth/bridge_recursion_overflow.wasm.jitstats
  • pyre/bench/synth/ca_bridge_multiframe_resume_double_call.cranelift.jitstats
  • pyre/bench/synth/ca_bridge_multiframe_resume_double_call.dynasm.jitstats
  • pyre/bench/synth/ca_bridge_multiframe_resume_double_call.wasm.jitstats
  • pyre/bench/synth/calls_closures.cranelift.jitstats
  • pyre/bench/synth/calls_closures.dynasm.jitstats
  • pyre/bench/synth/calls_closures.wasm.jitstats
  • pyre/bench/synth/closure_freevar_branch_resume.cranelift.jitstats
  • pyre/bench/synth/closure_freevar_branch_resume.dynasm.jitstats
  • pyre/bench/synth/closure_freevar_branch_resume.wasm.jitstats
  • pyre/bench/synth/del_cellvar_walk_commit.cranelift.jitstats
  • pyre/bench/synth/del_cellvar_walk_commit.dynasm.jitstats
  • pyre/bench/synth/del_cellvar_walk_commit.wasm.jitstats
  • pyre/bench/synth/exception_escape_hot_callee_tb_node_once.cranelift.jitstats
  • pyre/bench/synth/exception_escape_hot_callee_tb_node_once.dynasm.jitstats
  • pyre/bench/synth/exception_escape_hot_callee_tb_node_once.wasm.jitstats
  • pyre/bench/synth/exception_reentry_guard_finally_residual.cranelift.jitstats
  • pyre/bench/synth/exception_reentry_guard_finally_residual.dynasm.jitstats
  • pyre/bench/synth/exception_reentry_guard_finally_residual.wasm.jitstats
  • pyre/bench/synth/exception_traceback_frame_lineno.cranelift.jitstats
  • pyre/bench/synth/exception_traceback_frame_lineno.dynasm.jitstats
  • pyre/bench/synth/exception_traceback_frame_lineno.wasm.jitstats
  • pyre/bench/synth/foriter_call_resume_drops_iteration.cranelift.jitstats
  • pyre/bench/synth/foriter_call_resume_drops_iteration.dynasm.jitstats
  • pyre/bench/synth/foriter_call_resume_drops_iteration.wasm.jitstats
  • pyre/bench/synth/foriter_exempt_nested_foriter.cranelift.jitstats
  • pyre/bench/synth/foriter_exempt_nested_foriter.dynasm.jitstats
  • pyre/bench/synth/foriter_exempt_nested_foriter.wasm.jitstats
  • pyre/bench/synth/foriter_exempt_shared_generator.cranelift.jitstats
  • pyre/bench/synth/foriter_exempt_shared_generator.dynasm.jitstats
  • pyre/bench/synth/foriter_exempt_shared_generator.wasm.jitstats
  • pyre/bench/synth/generator_tree_recursion.cranelift.jitstats
  • pyre/bench/synth/generator_tree_recursion.dynasm.jitstats
  • pyre/bench/synth/generator_tree_recursion.wasm.jitstats
  • pyre/bench/synth/inline_subwalk_user_iterator.cranelift.jitstats
  • pyre/bench/synth/inline_subwalk_user_iterator.dynasm.jitstats
  • pyre/bench/synth/inline_subwalk_user_iterator.wasm.jitstats
  • pyre/bench/synth/polymorphic_binary_receiver.cranelift.jitstats
  • pyre/bench/synth/polymorphic_binary_receiver.dynasm.jitstats
  • pyre/bench/synth/polymorphic_binary_receiver.wasm.jitstats
  • pyre/bench/synth/recursion_memo_branch.cranelift.jitstats
  • pyre/bench/synth/recursion_memo_branch.dynasm.jitstats
  • pyre/bench/synth/recursion_memo_branch.wasm.jitstats
  • pyre/bench/synth/recursive_forced_frame_kept_stack.cranelift.jitstats
  • pyre/bench/synth/recursive_forced_frame_kept_stack.dynasm.jitstats
  • pyre/bench/synth/selfrec_bridge_nontail_promote.cranelift.jitstats
  • pyre/bench/synth/selfrec_bridge_nontail_promote.dynasm.jitstats
  • pyre/bench/synth/selfrec_bridge_nontail_promote.wasm.jitstats
  • pyre/bench/synth/short_circuit_side_effects.cranelift.jitstats
  • pyre/bench/synth/short_circuit_side_effects.dynasm.jitstats
  • pyre/bench/synth/str_search_index_bounds.cranelift.jitstats
  • pyre/bench/synth/str_search_index_bounds.dynasm.jitstats
  • pyre/bench/synth/str_search_index_bounds.wasm.jitstats
  • pyre/bench/synth/trace_too_long_inline_multiframe.cranelift.jitstats
  • pyre/bench/synth/trace_too_long_inline_multiframe.dynasm.jitstats
  • pyre/bench/synth/trace_too_long_inline_multiframe.wasm.jitstats
  • pyre/bench/synth/wasm_ca_trampoline_decline.cranelift.jitstats
  • pyre/bench/synth/wasm_ca_trampoline_decline.dynasm.jitstats
  • pyre/bench/synth/wasm_ca_trampoline_decline.wasm.jitstats
  • pyre/extra_tests/parity_tests/shared_code_object_globals.py
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/call.rs
  • pyre/pyre-interpreter/src/eval.rs
  • pyre/pyre-interpreter/src/function.rs
  • pyre/pyre-interpreter/src/module/__pypy__/interp_buffer.rs
  • pyre/pyre-interpreter/src/module/_pickle/mod.rs
  • pyre/pyre-interpreter/src/module/_pickle/unpickler.rs
  • pyre/pyre-interpreter/src/module/sys/vm.rs
  • pyre/pyre-interpreter/src/pycode.rs
  • pyre/pyre-interpreter/src/pyframe.rs
  • pyre/pyre-jit-trace/src/descr.rs
  • pyre/pyre-jit-trace/src/frame_layout.rs
  • pyre/pyre-jit-trace/src/helpers.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/heapcache_ops.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit-trace/src/trace_opcode.rs
  • pyre/pyre-jit-trace/src/virtualizable_gen.rs
  • pyre/pyre-jit-trace/src/virtualizable_spec.rs
  • pyre/pyre-jit-trace/tests/multi_frame_restore_supported.rs
  • pyre/pyre-jit/src/call_jit.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/jit/codewriter.rs
  • pyre/pyre-jit/src/jit/cpu.rs
  • pyre/pyre-jit/src/jit/flatten.rs
  • pyre/pyre-object/Cargo.toml
  • pyre/pyre-object/src/celldict.rs
  • pyre/pyre-object/src/quasiimmut.rs
  • pyre/pyre-object/src/typeobject.rs
  • pyre/pyre-wasm-test/src/main.rs

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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

https://github.com/youknowone/pyre/blob/d55c4a45240b258f3ddc930f2082da6b2c13a511/pyre-interpreter/src/pyframe.rs#L2980
P2 Badge Seed TLS for every new_with_context entry point

PyFrame::new now installs the execution context in TLS, but pyre-wasm-test/src/main.rs:25-36 calls PyFrame::new_with_context directly without doing so. Since this commit removes the frame-owned context, that launcher executes every frame with a null context: eval_frame_plain_with_resume skips ExecutionContext::enter/leave, and APIs such as sys.settrace silently become no-ops. Install the supplied context in that launcher or make the entry adapter establish it before execution.

AGENTS.md reference: AGENTS.md:L66-L77

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

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 6ce2bb3).
Updated: 2026-08-24T18:05:29.471Z

Files in the reviewed diff
Cargo.lock
majit/majit-backend-cranelift/src/compiler.rs
majit/majit-backend/src/lib.rs
majit/majit-gc/src/collector.rs
majit/majit-gc/src/nursery.rs
majit/majit-ir/src/descr.rs
majit/majit-ir/src/lib.rs
majit/majit-metainterp/src/compile.rs
majit/majit-metainterp/src/history.rs
majit/majit-metainterp/src/jitcode/assembler.rs
majit/majit-metainterp/src/jitdriver.rs
majit/majit-metainterp/src/optimizeopt/guard.rs
majit/majit-metainterp/src/optimizeopt/heap.rs
majit/majit-metainterp/src/optimizeopt/info.rs
majit/majit-metainterp/src/optimizeopt/unroll.rs
majit/majit-metainterp/src/optimizeopt/vstring.rs
majit/majit-metainterp/src/pyjitpl.rs
majit/majit-metainterp/src/recorder.rs
majit/majit-metainterp/src/resume.rs
majit/majit-metainterp/src/trace_ctx.rs
majit/majit-metainterp/src/warmstate.rs
pyre/extra_tests/parity_tests/shared_code_object_globals.py
pyre/pyre-interpreter/src/baseobjspace.rs
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/call.rs
pyre/pyre-interpreter/src/eval.rs
pyre/pyre-interpreter/src/function.rs
pyre/pyre-interpreter/src/module/__pypy__/interp_buffer.rs
pyre/pyre-interpreter/src/module/_pickle/mod.rs
pyre/pyre-interpreter/src/module/_pickle/unpickler.rs
pyre/pyre-interpreter/src/module/sys/vm.rs
pyre/pyre-interpreter/src/pycode.rs
pyre/pyre-interpreter/src/pyframe.rs
pyre/pyre-jit-trace/src/descr.rs
pyre/pyre-jit-trace/src/frame_layout.rs
pyre/pyre-jit-trace/src/helpers.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/heapcache_ops.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs
pyre/pyre-jit-trace/src/state.rs
pyre/pyre-jit-trace/src/trace.rs
pyre/pyre-jit-trace/src/trace_opcode.rs
pyre/pyre-jit-trace/src/virtualizable_gen.rs
pyre/pyre-jit-trace/src/virtualizable_spec.rs
pyre/pyre-jit-trace/tests/multi_frame_restore_supported.rs
pyre/pyre-jit/src/call_jit.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-jit/src/jit/codewriter.rs
pyre/pyre-jit/src/jit/cpu.rs
pyre/pyre-jit/src/jit/flatten.rs
pyre/pyre-object/Cargo.toml
pyre/pyre-object/src/celldict.rs
pyre/pyre-object/src/quasiimmut.rs
pyre/pyre-object/src/typeobject.rs
pyre/pyre-wasm-test/src/main.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)

None.

4. Structural adaptations

  • pyre/pyre-interpreter/src/pycode.rs:2768 ↔ pypy/interpreter/pycode.py:159 — “atomic compare-exchange first-writer publication” versus PyPy’s GIL-serialized if self.w_globals is None: self.w_globals = .... This is a necessary free-threading adaptation and preserves PyPy’s winning-globals/other-frame-override behavior.

  • pyre/pyre-interpreter/src/eval.rs:2123 ↔ pypy/interpreter/pyframe.py:328 — “obtain the execution context from TLS” versus self.space.getexecutioncontext(). Removing per-frame EC storage restores PyPy’s thread-owned execution-context model; the separate portal red is a Rust/JIT representation detail.

youknowone added a commit that referenced this pull request Aug 22, 2026
`PyFrame` no longer carries the execution context, and this launcher never
installed the one it constructs in the OS-thread locals, so
`space.getexecutioncontext()` answers null for every program it runs.
`eval_frame_plain_with_resume` then takes its context-free arm: no
`ExecutionContext::enter`/`leave`, no `call_trace`/`return_trace`, so
`sys.settrace` and the rest of the state reached through the context are
inert.

`pyrex/src/lib.rs`, `pyre-wasm/src/lib.rs` and `PyFrame::new` each install it
themselves; the comment in `PyFrame::new` named the first two as the whole set
and now names this one too.

Reported by Codex on #1388.

Assisted-by: Claude
@youknowone

Copy link
Copy Markdown
Owner Author

Seed TLS for every new_with_context entry pointpyre-wasm-test/src/main.rs executes every frame with a null context.

Confirmed and fixed in dedd760.

eval_frame_plain_with_resume reads the context once and, when it is null, takes an arm that
skips ExecutionContext::enter/leave and call_trace/return_trace entirely — so with the
frame-owned context gone, every program this launcher runs executed untraced.

pyre-wasm-test/src/main.rs now calls set_last_exec_ctx before new_with_context, as
pyrex/src/lib.rs, pyre-wasm/src/lib.rs and PyFrame::new already do. The comment in
PyFrame::new that enumerated the launchers named only the first two, which is how this one was
missed; it now names all three.

One neighbouring gap left alone deliberately: this launcher also does not call
install_user_del_action, which pyre-wasm/src/lib.rs documents as required for any finalizer
to run. The harness runs no __del__ / generator-finally test, so it is inert here.

Also on this push: the branch is rebased onto current main, and the jit-stats failures the
last CI run reported are answered — two of the baselines re-recorded as improvements were
regressions (raise_reg_unbound_jitstress gained an fbw_rolled_back_with_effects,
polymorphic_binary_receiver lost a bridge), and both are fixed rather than re-recorded.
Local gate: dynasm 456/456, cranelift 456/456, wasm 448/448, zero jit-stats diffs.

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: dedd760b49

ℹ️ 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 on lines +4753 to +4755
let frame_stores_global = unsafe {
crate::w_code_frame_stores_global(_roots.get(root_base), _roots.get(root_base + 1))
};

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 Synchronize the first-globals cache before dropping frame storage

When two threads first call functions that share one code object but have different __globals__, both can reach this new branch concurrently. w_code_frame_stores_global performs an unsynchronized null check and write on PyCode.w_globals, so both calls may return false and neither frame creates the debugdata override; now that PyFrame.w_globals is removed, one frame's LOAD_GLOBAL then reads whichever shared pointer wins, in addition to the raw Rust data race. Make the first-globals publication atomic/locked or preserve the supplied globals on each frame when publication races.

AGENTS.md reference: AGENTS.md:L19-L24

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 21a9aea.

The race is real and this PR is what makes it a wrong answer rather than a wasted store: false
is the answer that sends a frame to the code object for its globals, so two threads that both
read the null and both store both answer false, and with PyFrame.w_globals gone the loser's
frame reads the winner's namespace.

w_code_frame_stores_global now publishes by compare-exchange and computes its answer against
whichever pointer won — the loser gets true and stores its own override. w_code_get_w_globals
and w_code_set_w_globals reach the slot through the same atomic view. This is the shape
w_code_getconstant already uses for co_consts_w ("PyPy's GIL serializes first access... Pyre
is free-threaded, so every reader and writer uses the AtomicPtr element").

The field keeps its raw-pointer type rather than becoming an AtomicPtr: the JIT reads this word
directly at CODE_W_GLOBALS_OFFSET and folds it, so the layout is left alone and only the
mutator-side accesses are made atomic. walk_w_globals_stamped_code_roots keeps its plain
&mut — it runs with every mutator at a safepoint, which its doc comment already states.

commented by Claude

youknowone added a commit that referenced this pull request Aug 22, 2026
`PyFrame` no longer carries the execution context, and this launcher never
installed the one it constructs in the OS-thread locals, so
`space.getexecutioncontext()` answers null for every program it runs.
`eval_frame_plain_with_resume` then takes its context-free arm: no
`ExecutionContext::enter`/`leave`, no `call_trace`/`return_trace`, so
`sys.settrace` and the rest of the state reached through the context are
inert.

`pyrex/src/lib.rs`, `pyre-wasm/src/lib.rs` and `PyFrame::new` each install it
themselves; the comment in `PyFrame::new` named the first two as the whole set
and now names this one too.

Reported by Codex on #1388.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 22, 2026
`pycode.py frame_stores_global` reads `w_globals` and stores into it under the
GIL, so the pair is indivisible. The port did the same read and store
unsynchronized. Two threads first running one code object in different globals
can both read the null and both store, and both then answer `false` — which is
the answer that makes a frame take its globals from the code object. With
`PyFrame.w_globals` removed the loser's frame reads the winner's namespace
instead of its own, and the unsynchronized store is a data race in its own
right.

The publication is now a compare-exchange, and the answer is computed against
whichever pointer won. `w_code_get_w_globals` and `w_code_set_w_globals` reach
the slot through the same atomic view. `walk_w_globals_stamped_code_roots`
keeps its plain `&mut`: it runs with every mutator at a safepoint.

Reported by Codex on #1388.

Assisted-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: 21a9aea328

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

self.concrete_namespace = frame.w_globals;
self.concrete_execution_context = frame.execution_context;
self.concrete_namespace = frame.get_w_globals();
self.frame_w_globals = ctx.const_ref(self.concrete_namespace as usize as i64);

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 Derive root globals from the live frame

When one PyCode is first compiled under module globals A and later invoked with an equal-shaped module globals B, the warm key remains (code, pc) and compatibility can admit the existing loop. Making frame_w_globals a constant here causes guard_current_frame_globals_identity to skip its runtime GuardValue, so the module-cell fold remains bound to A even though B's frame carries its override in debugdata; LOAD_GLOBAL can consequently return A's value. Keep this value derived from the live red frame/debugdata rather than the recording frame's namespace.

AGENTS.md reference: AGENTS.md:L26-L33

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 7941cd2.

The report is exactly right, and a committed repro now demonstrates it:
extra_tests/parity_tests/shared_code_object_globals.py compiles one code object,
execs it into two module namespaces with G = 1 / G = 2, and calls both interleaved
while hot. The pre-fix binary returns 60001 for the second module — the first namespace's
value for all but the deopted iteration.

guard_current_frame_globals_identity's constant arm no longer treats the recorded namespace
as the frame's. It reads the live frame's debugdata and decides from it:

  • the shadow is read through virtualizable_entry_at(DEBUGDATA_VABLE_FIELD_INDEX), so no op is
    recorded for the read itself, and a NO_CONCRETE recording declines;
  • if the recorded shadow carries a different w_globals, the fold declines before emitting
    anything, so no stray guard is left behind;
  • the presence of debugdata is pinned with GuardNonnull / GuardIsnull through the
    is_nullity_known / nullity_now_known heapcache pair, the same idiom fast2locals uses on
    this box. createframe_obj allocates debugdata only when frame_stores_global(w_globals), so
    a null word already means "the namespace IS pycode.w_globals" and needs no further check;
  • when present, a GETFIELD_GC_R on FrameDebugData.w_globals plus GuardValue against the
    recorded namespace pins the live value, and replace_box propagates it.

The w_globals descr is declared mutable on purpose — set_w_globals rebinds it — so the read
cannot be folded back to the recording namespace. That is asserted by
frame_debug_data_w_globals_descr_reads_the_mutable_override_field.

One design note, since it is not visible in the diff: the first version of this read the live
frame through a CALL_LOOPINVARIANT residual. It was dropped. CALL_LOOPINVARIANT is keyed by
the callee pointer alone, which is unsound across two frames, and a call does not leave the
peeled loop the way a getfield does — that shape cost 6.5x on attr_store_add_transition. The
debugdata getfield hoists into the preamble and leaves nothing after the second Label.

All three backends print OK on the repro; PYRE_JIT=0 agrees.

commented by Claude

Comment thread pyre/pyre-interpreter/src/pyframe.rs Outdated
Comment on lines +5098 to +5099
if frame_stores_global {
frame.set_w_globals(w_globals);

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 Root the globals override across frame allocation

When frame_stores_global is true, such as executing a shared code object with a second movable globals dictionary, FrameBox::new may collect before this branch runs. Its root set no longer includes w_globals, and the code object only retains the first globals dictionary, so the local pointer can be relocated or reclaimed before set_w_globals stores it in debugdata. Root and reread the override across the frame allocation; otherwise f_globals and subsequent namespace accesses can use a stale pointer.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 12db2c3.

frame_builtin_obj and FrameBox::new are both collection points, and with PyFrame.w_globals
gone the namespace argument was named only by this function's local between them. FrameBox::new
republishes the PyFrame's own GCREF fields, but the override is no longer one of them, and the
code object retains the FIRST globals dictionary — which for the shared-code-object case is a
different dictionary — so nothing else named this one.

createframe_obj now pushes code, w_globals and the builtins module onto the root stack and
reads each back after the last allocation before its use, the way PyFrame::new already does for
its own constructor. frame_stores_global and set_w_globals both take the re-read pointer.

commented by Claude

youknowone added a commit that referenced this pull request Aug 22, 2026
`PyFrame` no longer carries the execution context, and this launcher never
installed the one it constructs in the OS-thread locals, so
`space.getexecutioncontext()` answers null for every program it runs.
`eval_frame_plain_with_resume` then takes its context-free arm: no
`ExecutionContext::enter`/`leave`, no `call_trace`/`return_trace`, so
`sys.settrace` and the rest of the state reached through the context are
inert.

`pyrex/src/lib.rs`, `pyre-wasm/src/lib.rs` and `PyFrame::new` each install it
themselves; the comment in `PyFrame::new` named the first two as the whole set
and now names this one too.

Reported by Codex on #1388.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 22, 2026
`pycode.py frame_stores_global` reads `w_globals` and stores into it under the
GIL, so the pair is indivisible. The port did the same read and store
unsynchronized. Two threads first running one code object in different globals
can both read the null and both store, and both then answer `false` — which is
the answer that makes a frame take its globals from the code object. With
`PyFrame.w_globals` removed the loser's frame reads the winner's namespace
instead of its own, and the unsynchronized store is a data race in its own
right.

The publication is now a compare-exchange, and the answer is computed against
whichever pointer won. `w_code_get_w_globals` and `w_code_set_w_globals` reach
the slot through the same atomic view. `walk_w_globals_stamped_code_roots`
keeps its plain `&mut`: it runs with every mutator at a safepoint.

Reported by Codex on #1388.

Assisted-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: 900c8e9cc2

ℹ️ 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/pyre-jit-trace/src/state.rs Outdated
Comment on lines 14850 to 14852
} else {
crate::jitcode_dispatch::census_record("ReconstructedCallee::EcConstFallback");
ctx.const_ref(execution_context as i64)

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 Preserve the runtime EC red on reconstructed bridges

When bridge resume data lacks the portal EC color (bridge_ec_missing), this fallback embeds the recording activation's ExecutionContext pointer as a constant. If the same PyCode/compiled loop is later invoked after that OS thread installs a different execution context, the reconstructed inline callee uses the stale context for tracing, exception state, imports, and other thread-owned state; the pointer can also outlive its original Rc. Retain the EC in resume data or decline bridge compilation rather than substituting the recording-time value.

AGENTS.md reference: AGENTS.md:L26-L33

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 7276232, taking neither of the two suggested options.

Retaining the EC in resume data is not available: ec is a red in PyPyJitDriver.reds, not a
PyFrame field, so when the failing guard's frame-register section names no value at the portal
ec color there is no second live source to decode it from. Declining bridge compilation would
close a shape that is reached on ordinary code — unpack_drain_exact_kind.py hits it with color
10 absent from a guard whose live refs are [3, 9].

So the fallback now records a residual getexecutioncontext() instead of a constant:
emit_current_execution_context at both the reconstructed-callee seed and
MIFrame::ensure_execution_context, which is where the interpreter itself reads the context
from. The call is declared EF_CANNOT_RAISE and deliberately NOT elidable — it takes no
arguments, so an elidable descr would let the pure pass fold it back to the recording thread's
ExecutionContext and reintroduce exactly the defect reported here.

Each of the three recovery sites names itself in the decline census, so the count for "the live
red carries no value" stays separable from the counts that do. bridge_ec_missing no longer
asserts.

commented by Claude

youknowone added a commit that referenced this pull request Aug 23, 2026
`PyFrame` no longer carries the execution context, and this launcher never
installed the one it constructs in the OS-thread locals, so
`space.getexecutioncontext()` answers null for every program it runs.
`eval_frame_plain_with_resume` then takes its context-free arm: no
`ExecutionContext::enter`/`leave`, no `call_trace`/`return_trace`, so
`sys.settrace` and the rest of the state reached through the context are
inert.

`pyrex/src/lib.rs`, `pyre-wasm/src/lib.rs` and `PyFrame::new` each install it
themselves; the comment in `PyFrame::new` named the first two as the whole set
and now names this one too.

Reported by Codex on #1388.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 23, 2026
`pycode.py frame_stores_global` reads `w_globals` and stores into it under the
GIL, so the pair is indivisible. The port did the same read and store
unsynchronized. Two threads first running one code object in different globals
can both read the null and both store, and both then answer `false` — which is
the answer that makes a frame take its globals from the code object. With
`PyFrame.w_globals` removed the loser's frame reads the winner's namespace
instead of its own, and the unsynchronized store is a data race in its own
right.

The publication is now a compare-exchange, and the answer is computed against
whichever pointer won. `w_code_get_w_globals` and `w_code_set_w_globals` reach
the slot through the same atomic view. `walk_w_globals_stamped_code_roots`
keeps its plain `&mut`: it runs with every mutator at a safepoint.

Reported by Codex on #1388.

Assisted-by: Claude
@youknowone

Copy link
Copy Markdown
Owner Author

Regressions to PyPy parityhelpers.rs accepts but discards _w_globals when synthesizing
an inlined PyFrame, so a virtual inline frame for a shared code object materializes with the
code object's first namespace.

The discarded parameter is real and now fixed in e647aa8. The materialization it predicts is not
reachable today, and the reason it is not is worth stating, because nothing in helpers.rs said it.

Both emission sites decline a global-storing callee before they build the frame:
try_walker_call_assembler_self_recursive and the multi-frame seed path each call
w_code_frame_stores_global(w_code, callee_globals_obj) and take the residual when it answers true.
So a frame whose namespace differs from pycode.w_globals is never synthesized — the parameter was
discarded, but only in the case where it equals what get_w_globals already answers. The
reconstructed-bridge callee derives its namespace from w_code_get_w_globals itself, and a later
call through a different Function object at an inlined site deopts on the callable guard, since
each exec of a shared code object binds its own function.

What was missing is the frame shape stating what it can represent, which is exactly how the
PyFrame.w_globals removal turned a wired parameter into an unwired one without a compile error.
emit_new_pyframe_inline_with_params and its self-recursive twin now return Option<OpRef> and
answer None when the namespace is not the one the code object published; the Branch A site aborts
the walk (it is past its emission boundary), the seed site declines to the residual, and the
reconstructed callee propagates None. No jit-stats row moves and
extra_tests/parity_tests/shared_code_object_globals.py still prints OK, which is the expected
result: the check restates an invariant the callers already hold.

commented by Claude

youknowone added a commit that referenced this pull request Aug 23, 2026
`PyFrame` no longer carries the execution context, and this launcher never
installed the one it constructs in the OS-thread locals, so
`space.getexecutioncontext()` answers null for every program it runs.
`eval_frame_plain_with_resume` then takes its context-free arm: no
`ExecutionContext::enter`/`leave`, no `call_trace`/`return_trace`, so
`sys.settrace` and the rest of the state reached through the context are
inert.

`pyrex/src/lib.rs`, `pyre-wasm/src/lib.rs` and `PyFrame::new` each install it
themselves; the comment in `PyFrame::new` named the first two as the whole set
and now names this one too.

Reported by Codex on #1388.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 23, 2026
`pycode.py frame_stores_global` reads `w_globals` and stores into it under the
GIL, so the pair is indivisible. The port did the same read and store
unsynchronized. Two threads first running one code object in different globals
can both read the null and both store, and both then answer `false` — which is
the answer that makes a frame take its globals from the code object. With
`PyFrame.w_globals` removed the loser's frame reads the winner's namespace
instead of its own, and the unsynchronized store is a data race in its own
right.

The publication is now a compare-exchange, and the answer is computed against
whichever pointer won. `w_code_get_w_globals` and `w_code_set_w_globals` reach
the slot through the same atomic view. `walk_w_globals_stamped_code_roots`
keeps its plain `&mut`: it runs with every mutator at a safepoint.

Reported by Codex on #1388.

Assisted-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: 96b72da8bc

ℹ️ 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 on lines +1745 to +1749
w_globals: if pycode.is_null() {
pyre_object::PY_NULL
} else {
unsafe { crate::w_code_get_w_globals(pycode as PyObjectRef) }
},

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 Root the globals snapshot across debugdata allocation

When an optimized frame uses a movable exec/custom globals dictionary and first creates debugdata, this copies the current globals pointer into the stack-local FrameDebugData value, after which getorcreate_debug_data calls the collecting try_gc_alloc_stable_raw. The frame anchor lets the collector forward the authoritative pointer on PyCode, but it cannot rewrite the copy already stored in value, so the newly allocated debugdata can receive a stale pointer and later make f_globals or LOAD_GLOBAL read relocated/reclaimed memory. Root this new field across the allocation or construct/reload it afterward.

AGENTS.md reference: AGENTS.md:L19-L24

Useful? React with 👍 / 👎.

Comment on lines +2173 to +2175
if cell.flags & jc_flags::TEMPORARY != 0 {
crate::mc_diag_bump(25);
return self.counter.tick(bucket, self.increment_function_threshold);

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 the abort ceiling in typed entry checks

For ordinary function-entry keys with a concrete (code, pc), the new wrapper selects this typed path, but unlike should_trace_function_entry it never checks abort_count >= MAX_TRACE_ABORT_COUNT before ticking. Once a non-dead cell reaches the retry ceiling, this method therefore continues firing the threshold; try_function_entry_jit then decays every counter before force_start_tracing_for_key finally refuses the same cell, so one permanently failed function repeatedly suppresses hotness at unrelated locations. Mirror the untyped dead-token/abort-ceiling gate before this return.

AGENTS.md reference: AGENTS.md:L206-L211

Useful? React with 👍 / 👎.

youknowone added a commit that referenced this pull request Aug 24, 2026
`PyFrame` no longer carries the execution context, and this launcher never
installed the one it constructs in the OS-thread locals, so
`space.getexecutioncontext()` answers null for every program it runs.
`eval_frame_plain_with_resume` then takes its context-free arm: no
`ExecutionContext::enter`/`leave`, no `call_trace`/`return_trace`, so
`sys.settrace` and the rest of the state reached through the context are
inert.

`pyrex/src/lib.rs`, `pyre-wasm/src/lib.rs` and `PyFrame::new` each install it
themselves; the comment in `PyFrame::new` named the first two as the whole set
and now names this one too.

Reported by Codex on #1388.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 24, 2026
`pycode.py frame_stores_global` reads `w_globals` and stores into it under the
GIL, so the pair is indivisible. The port did the same read and store
unsynchronized. Two threads first running one code object in different globals
can both read the null and both store, and both then answer `false` — which is
the answer that makes a frame take its globals from the code object. With
`PyFrame.w_globals` removed the loser's frame reads the winner's namespace
instead of its own, and the unsynchronized store is a data race in its own
right.

The publication is now a compare-exchange, and the answer is computed against
whichever pointer won. `w_code_get_w_globals` and `w_code_set_w_globals` reach
the slot through the same atomic view. `walk_w_globals_stamped_code_roots`
keeps its plain `&mut`: it runs with every mutator at a safepoint.

Reported by Codex on #1388.

Assisted-by: Claude
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

youknowone added a commit that referenced this pull request Aug 24, 2026
`PyFrame` no longer carries the execution context, and this launcher never
installed the one it constructs in the OS-thread locals, so
`space.getexecutioncontext()` answers null for every program it runs.
`eval_frame_plain_with_resume` then takes its context-free arm: no
`ExecutionContext::enter`/`leave`, no `call_trace`/`return_trace`, so
`sys.settrace` and the rest of the state reached through the context are
inert.

`pyrex/src/lib.rs`, `pyre-wasm/src/lib.rs` and `PyFrame::new` each install it
themselves; the comment in `PyFrame::new` named the first two as the whole set
and now names this one too.

Reported by Codex on #1388.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 24, 2026
`pycode.py frame_stores_global` reads `w_globals` and stores into it under the
GIL, so the pair is indivisible. The port did the same read and store
unsynchronized. Two threads first running one code object in different globals
can both read the null and both store, and both then answer `false` — which is
the answer that makes a frame take its globals from the code object. With
`PyFrame.w_globals` removed the loser's frame reads the winner's namespace
instead of its own, and the unsynchronized store is a data race in its own
right.

The publication is now a compare-exchange, and the answer is computed against
whichever pointer won. `w_code_get_w_globals` and `w_code_set_w_globals` reach
the slot through the same atomic view. `walk_w_globals_stamped_code_roots`
keeps its plain `&mut`: it runs with every mutator at a safepoint.

Reported by Codex on #1388.

Assisted-by: Claude
`compute_gcmap` opens with `if arg is None: continue`, before it reads
`arg.type`. `collect_guards` read only the type, so a `None` fail arg
reached the slot list.

The skip is not vacuous here: `infer_fail_arg_types` and
`resolve_fail_arg_types` both type an `OpRef::NONE` as `Ref`, because a
virtual leaves a hole in fail_args and a virtual object is a GCREF. The
type test alone therefore marked the hole's slot, which owns no root.

The comment claimed no hole could reach the map, on the grounds that
`spill_guard_fail_args` resolves every fail arg through `resolve_opref`,
which asserts on `OpRef::NONE`. That assert exists, but the three
`spill_guard_fail_args` call sites are all on the `call_may_force` /
`call_release_gil` paths, so the argument does not cover a guard in
general. Replaced with the reason the skip is load-bearing.

Reported by CodeRabbit on #1372.

Assisted-by: Claude
`JitDriver::decode_descriptor_values`, `decode_exit_layout_values`,
`resume_layout_with_descriptor_slot_types`,
`compile::terminal_exit_layout_for_trace` and
`decode_values_with_layout` have no caller in either the lib build or
the test build.

The driver returns raw guard-failure data by design — "State restoration
and bridge/blackhole decision happen in the caller's handle_fail()" — so
the decode step these performed belongs to the caller. `pyre-jit`'s
`eval.rs` carries its own `decode_exit_layout_values` with three callers;
the copy here was a byte-identical twin of it.

None of the five carries a doc comment or an upstream citation, and no
`rpython/`/`pypy/` identifier matches their names under case- and
underscore-insensitive comparison.

The `exit_types` comment that named `decode_values_with_layout` now
names the property instead, and records that the gcmap is not a consumer
of that typing: `compute_gcmap` drops a `None` failarg before reading
its type.

Assisted-by: Claude
`Nursery::reset_range` is a safe `pub fn` that writes raw bytes at
caller-supplied addresses, and its three bounds checks were
`debug_assert!`. A release build performed none of them, so an
out-of-range `end` wrote outside the arena and a `start` greater than
`end` wrapped `end - start` into a fill of nearly `usize::MAX` bytes. It
now intersects the requested range with the arena and returns when the
result is empty; the assertions stay for debug builds.

`reserve_nursery_gap` derives `next_free` from `size_for_typeid`, which
decodes the pinned object's extent from its header. An extent that
overstates the object pushed `free` past the barrier being published in
the same block, and `Nursery::alloc` then compared against that bound
and returned memory beyond the gap. `next_free` is now clamped to
`next_top`.

`Nursery::set_free_ptr` and `set_top_ptr` keep their `debug_assert`s:
both are `unsafe fn` whose documented contract puts the bounds on the
caller.

Reported by CodeRabbit on #1372.

Assisted-by: Claude
None has a caller in either the lib build or the test build.

- `GuardOpt::set_guard` wraps a single `IndexMap::insert`.
- `visit_value`, a nested helper in the const-ptr walk, lost its caller
  while its siblings `visit_operands` / `visit_op` / `visit_op_info` keep
  theirs.
- `is_virtual_concat` and `is_virtual_slice` are `.is_some()` wrappers
  over `get_concat_info` / `get_slice_info`, which are called directly.
- `ResumeStorage::rd_consts_mut_for_gc` names
  `MetaInterp::walk_rd_consts_refs` as its only caller. That walker
  exists and is wired, but reaches the pool through
  `SharedConstPool::as_mut_vec_for_gc` directly, so the accessor sits
  beside the path rather than on it.

`Opencoder::refresh_from_gc` is left in place. It is also unreached, but
its doc calls it the required write-back point for the `_refs`
shadow-stack adaptation, and `rooted_ref_indices` is populated by
`_encode_ptr` and otherwise read only by `release_roots`. Deleting it
would remove the record of missing wiring rather than dead code.

Assisted-by: Claude
…t minor

`do_collect_nursery` sampled `any_pinned_object_kept` into a discarded
local and always announced `ExtraRootWalkKind::Minor`. The walkers that
read the kind — `walk_rd_consts_refs`, the front `TargetToken` walk, the
method cache and mapdict scans — each skip a clean area on a minor.

`collect_roots_in_nursery` derives `use_jit_frame_stoppers` from that
same flag and passes it as `walk_roots(is_minor=...)`: a pinned object
created before the previous minor is still in the nursery and was never
promoted, so the skip drops the only edge reaching it. The sampled flag
now selects the announced kind.

Assisted-by: Claude
…test items

`finish_trace_for_parity_preserves_captured_snapshots` carries a full
`assert_eq!` body but no `#[test]`, so `#[allow(dead_code)]` was silencing
the fact that it never ran. It runs and passes.

`may_force_test_lock` in `pyjitpl` has no caller in any cfg; the cranelift
backend has its own copy, which is the one every `may_force` test takes.
The `TID` constant in `a_named_field_resolves_by_name_through_an_ambiguous_offset`
is unused — that test calls `field_specs_from_layout` directly.

Assisted-by: Claude
`interp_jit.py PyFrame._virtualizable_` lists `pycode`, `valuestackdepth` and
`debugdata` as the virtualizable scalars; `ec` is a red in
`PyPyJitDriver.reds`, not a frame field. `PyFrame.execution_context` is
removed and the sites that read it now take the current activation's context,
as `space.getexecutioncontext()` does. `normalize_raise_varargs_fn` no longer
needs the frame pointer for it.

`PyFrame.w_globals` is likewise removed. Upstream `bcd8653e5ec` dropped it
when `PyCode.frame_stores_global` landed, and `InstanceRepr._parse_field_list`
skips a `_virtualizable_` name with no concrete field, so the entry produced
no scalar. `get_w_globals` reads the code object, and `FrameDebugData` carries
the snapshot the debug path needs. `PYFRAME_W_GLOBALS_OFFSET` is replaced by
`FRAME_DEBUG_DATA_W_GLOBALS_OFFSET`; the vable scalar table loses its fourth
entry and `failed_attr_cleanup` narrows to `u8`.

`majit_metainterp::backend_runtime` collects `set_jitframe_gc_type_id` and
`install_gc_standalone` behind the same feature selection that constructs
`BackendImpl`, so a frontend does not repeat the test and disagree under
workspace feature unification. `WarmRunnerDesc.make_cpu` constructs one CPU
and `MetaInterpStaticData.cpu` owns it.

Assisted-by: Claude
Records the kind each registered walker sees across two minors with a
pinned root alive. Without the preceding commit the pair reads
`[Minor, Minor]`; the second walk now announces `Major`.

Assisted-by: Claude
`pyframe.py get_w_globals` ends in `jit.promote(self.pycode).w_globals`.
The port read `self.pycode` unpromoted while the doc comment claimed the
promoted read, which was harmless only while `w_globals` was still a
virtualizable scalar on `PyFrame`. With that field gone the code object is
the sole source and nothing pins it to a trace constant, so the globals read
becomes a load the optimizer cannot fold.

The promote binds a local: `lower_promote_stmt` takes a single-ident LHS, so
a field assignment would drop it silently.

Assisted-by: Claude
`quasiimmut.py QuasiImmut` registers the owning `JitCellToken`, and
`invalidate` sets `looptoken.invalidated` and asks the CPU to activate every
still-unpatched `GUARD_NOT_INVALIDATED` that token owns. The registry held one
`Arc<AtomicBool>` per compiled artifact instead, so an invalidation reached
only the fragments registered by then. It now holds
`Arc<dyn QuasiImmutLoopToken>` and calls `invalidate_for_quasi_immut`, which
the backend implements on its own token.

`can_inline_callable` and `disable_noninlinable_function` are reached through
their typed green-key forms, so a recursive inline decision is looked up under
the key shape the warm state stores.

`Trace` gains `recorded_ops_total`, which `cut` does not rewind, and the bridge
driver's setup-abort test reads it beside `num_ops`. A body walk that recorded
operations and had them cut back to the setup position no longer answers that
test as though nothing had been recorded, so a trace-too-long abort stops
reading as a deterministic setup failure that permanently declines its source
guard.

The 80 jit-stats baseline files this moves, over 27 fixtures, are re-recorded.

Assisted-by: Claude
`collect_outer_active_boxes` reported a branch guard's kept operand-stack slot
as unsourced when the walk mirror held `OpRef::NONE` and no edge-move recovery
covered it, without consulting the virtualizable shadow. The resolution below
that report already reads the shadow for an operand-stack slot the guard pc's
color map does not claim, so the report declined a case its own resolver
handles. The decline raises `BranchGuardKeptSlotUnsourced` at a capture point a
residual has already run past, which the walk counts as
`fbw_rolled_back_with_effects`.

Report the hole only when the shadow also holds no live Ref for the semantic
slot. On `raise_reg_unbound_jitstress` the walk mirror stands at depth 1 while
the branch guard resumes at depth 3: both kept slots read NONE from the mirror
and a live `RefOp` from the shadow.

The `raise_reg_unbound_jitstress` baselines re-recorded in "jit: align
quasi-immutable ownership and recursive bridge state" are restored to their
previous values — fbw_rolled_back_with_effects 1 -> 0, loops_aborted 2 -> 1,
loops_compiled 8 -> 9.

Assisted-by: Claude
… unseeded

`replace_movable_load_global_namespace_with_frame_globals` substitutes the
codewriter's null LOAD_GLOBAL namespace placeholder with the frame's
`get_w_globals()`. For an inline sub-walk it reads that off the callee's
`portal_frame_reg`, and where that register is unseeded it returned, leaving the
placeholder standing. `try_walker_load_global_cell_fold` then declines on the
null namespace before reaching its builtins leg, the residual survives, and
`ensure_residual_call_args_bound` aborts the trace on the same unseeded frame
register as the call's third argument.

The unseeded frame does not by itself block the fold: its builtins leg already
handles `frame_ptr == 0` by deriving the builtin module from the namespace's
`__builtins__` cell. Only the null namespace does. The callee's own
`__globals__` is recorded in `inline_callee_consts`, so name it directly rather
than return. Immovable namespaces only, matching
`guard_current_frame_globals_identity` where both fold legs end: a movable one
declines there anyway, so substituting it would bake a pointer the GC may
forward into the surviving residual. The root frame is still never borrowed.

`polymorphic_binary_receiver` returns to the values it carried before "jit:
align quasi-immutable ownership and recursive bridge state": bridges_compiled
2 -> 3, guard_failures 845 -> 788, loops_aborted 1 -> 0. The
`retraces_compiled` entry those baselines gained there is kept.

Assisted-by: Claude
Twelve upstream citations this branch adds name a line number.
`scripts/check-new-line-citations.py` reports them against `origin/main`, which
now carries the check (#1408).

`warmstate.py:446/458/473/483/491/511` all fall inside `maybe_compile_and_run`;
`assembler.py:46` is `compute_gcmap`; `pyframe.py:132` is `get_w_globals`;
`interp_jit.py:25-30` is `PyFrame._virtualizable_` and `:67` is
`PyPyJitDriver.reds`.

Assisted-by: Claude
`PyFrame` no longer carries the execution context, and this launcher never
installed the one it constructs in the OS-thread locals, so
`space.getexecutioncontext()` answers null for every program it runs.
`eval_frame_plain_with_resume` then takes its context-free arm: no
`ExecutionContext::enter`/`leave`, no `call_trace`/`return_trace`, so
`sys.settrace` and the rest of the state reached through the context are
inert.

`pyrex/src/lib.rs`, `pyre-wasm/src/lib.rs` and `PyFrame::new` each install it
themselves; the comment in `PyFrame::new` named the first two as the whole set
and now names this one too.

Reported by Codex on #1388.

Assisted-by: Claude
`pycode.py frame_stores_global` reads `w_globals` and stores into it under the
GIL, so the pair is indivisible. The port did the same read and store
unsynchronized. Two threads first running one code object in different globals
can both read the null and both store, and both then answer `false` — which is
the answer that makes a frame take its globals from the code object. With
`PyFrame.w_globals` removed the loser's frame reads the winner's namespace
instead of its own, and the unsynchronized store is a data race in its own
right.

The publication is now a compare-exchange, and the answer is computed against
whichever pointer won. `w_code_get_w_globals` and `w_code_set_w_globals` reach
the slot through the same atomic view. `walk_w_globals_stamped_code_roots`
keeps its plain `&mut`: it runs with every mutator at a safepoint.

Reported by Codex on #1388.

Assisted-by: Claude
… is empty

`interp_jit.py PyPyJitDriver.reds = ['frame', 'ec']` makes the execution
context a red that the `-live-` markers force-keep at every guard, and
`setup_bridge_sym` decodes it out of the failing guard's frame-register
section. Two shapes leave it empty: a skeleton jitcode carries `u16::MAX` for
both portal red colors, and a resumed register can name no value.
`unpack_drain_exact_kind.py` reaches the second one — color 10 absent from a
guard whose live refs are `[3, 9]` — and aborted on the assertion this branch
put in its place, because dropping `PyFrame.execution_context` also dropped
the field the recovery read.

`emit_current_execution_context` records a residual `getexecutioncontext()`
instead, which is where the interpreter reads the context from. The call is
`EF_CANNOT_RAISE`, not elidable: it takes no arguments, so an elidable descr
would let the pure pass fold it to the recording thread's ExecutionContext and
every other thread entering the compiled trace would read that thread's
exception state. `jit_getexecutioncontext` wraps the accessor for the
`(i64xn) -> i64` residual ABI, as `jit_force_vref` does for `force_vref`.

The three recoveries — walk entry, `MIFrame::ensure_execution_context` and the
reconstructed bridge callee — each name themselves in the decline census, so
the count that the live red carries no call is separable from the counts that
do. The fixture records `ExecutionContext::WalkEntry: 1` and passes on dynasm,
cranelift and wasm.

Assisted-by: Claude
`short_circuit_side_effects` reads `bridges_compiled` 7 and `guard_failures`
1972 (wasm 1938) against 8 and 2104 (2092). Scaling the fixture's N by 4 and
16 reads 8/1998 and 8/2058 on both dynasm and cranelift: the eighth bridge is
acquired past this fixture's own N, so each counter moves with warm-up rather
than per iteration. 16x iterations of a per-iteration deopt would be near
31000.

Assisted-by: Claude
`frame_builtin_obj` and `FrameBox::new` can both collect, and this branch
removed `PyFrame.w_globals`, so between them the namespace argument is named
only by this function's own local: the code object retains the FIRST globals
dictionary (`pycode.py frame_stores_global`), not necessarily this one.

Push `code`, `w_globals` and the builtins module onto the root stack and read
each back after the last allocation before its use.

Assisted-by: Claude
…onstant namespace

`guard_current_frame_globals_identity` returned a plain constant comparison
whenever the sym's `frame_w_globals` was constant, emitting no guard.  On this
branch every seeding point publishes it as a constant read out of the recording
frame (`setup_call`, the vable-static republication in `trace_opcode.rs`,
`setup_reconstructed_callee_frame`), so the module-cell fold behind LOAD_GLOBAL
had nothing pinning the namespace at runtime.

`pycode.py frame_stores_global` publishes only the FIRST namespace a code object
runs under, so the constant does not distinguish a frame running that same code
object under a second `exec(code, ns)`: that frame carries its own namespace in
`debugdata` (`pyframe.py get_w_globals`) and enters the loop under the same
`(code, pc)` warm key.  `m2.hot()` returned `m1`'s total on dynasm and cranelift.

Emit what `get_w_globals` expresses.  `debugdata` is a virtualizable field, so
the walker reads it off `virtualizable_boxes` and records no op; the recorded
direction picks the arm and each arm pins it, `GuardIsnull` for the frame that
takes `promote(pycode).w_globals` and `GuardNonnull` plus a
`GETFIELD_GC_R`/`GuardValue` pair for the frame that carries an override.  The
payload address itself is per-frame, so only the value it holds is guarded.
`pyjitpl.py _establish_nullity` is the precedent for consulting the heapcache
first: every LOAD_GLOBAL in the frame reaches this fold, and re-recording the
same guard per site spends `trace_limit` on ops the optimizer then removes.

`FRAME_DEBUG_DATA_DESCR_GROUP` becomes headerless.  `StructPtrInfo::make_guards`
certifies a loaded pointer's runtime header against the descr's type id, and
this group declared none, so the GcCache minted one from the structural key --
a number naming nothing in the collector's type table, which failed every check
and cost a bridge per fold site.  `getorcreate_debug_data` also falls back to
`malloc_raw` when the owning frame is not collector-owned, so the pointer may
carry no header at all; `headerless` is the flag that keeps GUARD_GC_TYPE off
it.  The `locals()` fold reads the neighbouring `w_locals` through the same
group.

`shared_code_object_globals.py` execs one code object into two module
namespaces and reads both hot.

Assisted-by: Claude
`ITEMS_BLOCK_DESCR_GROUP` and `RBIGINT_PAIR_DESCR_GROUP` were gc-managed with
`type_id: 0`. That does not keep `StructPtrInfo::make_guards`
(`optimizeopt/info.rs`) from emitting `GUARD_GC_TYPE`: it emits one for every
gc-managed, non-headerless descr and takes the id from whatever the `GcCache`
slot for the key holds, which is how `FrameDebugData` came to be guarded
against 272 while its runtime header reads 103.

Both structs have an allocation path that returns a block with no `GcHeader`:
`alloc_items_block` (`std::alloc`, the `PYRE_GC_ITEMSBLOCK=0` and no-hook
fallback) and `alloc_rbigint_pair_nursery_collecting` /
`alloc_rbigint_pair_no_collect` (`malloc_raw`). `GUARD_GC_TYPE` reads at
`ref - GcHeader::SIZE`, so neither pointer may reach it. `ITEMS_BLOCK` has the
second reason it already carried: one descr fronting the three list strategies'
tids can name none of them.

`a_gc_managed_group_that_names_no_type_id_declares_itself_headerless` asserts
the rule over every `DECLARED_GROUPS` row.

check.py on this machine: dynasm 456 passed, cranelift 456 passed, wasm 448
passed, and the only jit-stats row that moves is `fib_recursive`, which reads
1601/7/1 here with or without this change.

Assisted-by: Claude
…answer

`emit_new_pyframe_inline_with_params` and its self-recursive twin took a
`w_globals` and discarded it. The store it used to feed was
`PyFrame.w_globals`, which this branch removed; a frame the JIT synthesizes
carries no `debugdata`, so `get_w_globals` on it answers with the code object's
published first namespace.

Both emission sites already decline a global-storing callee
(`pycode.py frame_stores_global`) before they build the frame, so no frame with
the wrong namespace is built today. What was missing is the frame shape saying
what it can represent: the parameter now feeds a check that the namespace is
the one the code object published, and the builders return `None` when it is
not. The Branch A site is past its emission boundary and aborts the walk; the
seed site declines to the residual; the reconstructed-bridge callee propagates
`None`, where the arm is unreachable because it derives the namespace from the
same published slot.

check.py on this machine: dynasm 456 passed, `shared_code_object_globals.py`
prints OK, and no jit-stats row moves.

Assisted-by: Claude
`resolve_gc_tid`'s doc said the structural `cache_key` "still resolves to the
dense tid" a `GUARD_GC_TYPE` needs. That holds for a struct the JIT allocates,
whose header the same GcCache id is written into. A struct the host allocates
under an id of its own resolves to the cache's instead, and the guard then
fails on every object — `FrameDebugData` guarded against 272 against a runtime
header of 103.

Name the precondition and point at `headerless`, which is the declaration such
a descr owes and which this function's caller checks first.

Assisted-by: Claude
The two `TopFrameRefGuard::new` calls this branch rebased onto read
`PyFrame.execution_context`, which the branch removes. They merged without a
conflict because they sit in regions the branch does not touch, and the tree
stopped compiling.

`build_jit_state` runs immediately before each of them and carries the same
context the removed field held.

Assisted-by: Claude
fib_recursive reads bridges_compiled=7, loops_aborted=1, guard_failures=1601
and fbw_blackhole_adopted_multi_frame=1 on all three backends here. The
committed 8/0/1600/0 reproduces on none of them, nor on the three CI hosts.

The added abort is `blackhole_if_trace_too_long` on the bridge for trace 3
guard 6. `find_biggest_function` names fib's own function-entry green key,
`disable_noninlinable_function_for_key` marks it, and the retry compiles that
bridge at 919 recorded ops. origin/main, built from the same base with the
same LLBC artefacts, compiles that bridge at 4091 ops and records a further
trace whose bridge adds 1195: 10125 recorded bridge ops against this branch's
6777.

Wall clock, 15 interleaved runs of each binary on this box:
origin/main min=0.310 median=0.340 mean=0.347;
this branch min=0.290 median=0.310 mean=0.324.

Assisted-by: Claude
`pycode_green_keys_preserve_wrapper_identity` calls `green_key_from_pycode`,
`green_key_typed_from_pycode` and `make_green_key`, all of which now take
`is_being_profiled` between the pc and the code operand.

Assisted-by: Claude
`green_key_from_pycode`'s `make_green_key` call and the
`note_inline_subwalk_start` typed-key argument each exceed the line width once
`is_being_profiled` is in the argument list.

Assisted-by: Claude
`assemble_peeled_trace_with_jump_args` appends a body-live, preamble-defined
box to the loop LABEL without a matching `used_boxes` entry, so
`inline_short_preamble` cannot produce it and a bridge closing onto that LABEL
arrives one arg short of `target_arglocs`. `compile_bridge` gives such a bridge
up.

The appended box is reachable again when it is a virtualizable static scalar:
the vable input layout is `[frame, static scalars.., array items..]`, so the
slot is `GetfieldGc*(frame, descr)` -- the load the preamble performed before
the optimizer folded it onto the seeded slot. `TargetToken` records that
`(opcode, descr)` pair per appended arg and `jump_to_existing_trace_impl`
emits the loads from JUMP arg 0. The list is all-or-nothing, since the recipes
rebuild a contiguous LABEL tail and a partial list makes the close overshoot,
and it is rewritten on every assembly so a recompiled token does not inherit a
stale one. An append with no recipe still reaches the giveup.

Reached here because `PyFrame.debugdata`'s `index_in_parent` is 4 on this
branch and 5 on main while `OptVirtualize` seeds that slot at 4 on both, so
the merge-point `GetfieldGcR(frame, debugdata)` recorded by
`record_portal_debugdata_guard` folds onto the seeded slot here and passes
through unfolded there. 68 dynasm and 60 cranelift fixtures failed on the
arity giveup; `store_global_hot` read `bridges_compiled=0 loops_aborted=494
guard_failures=98985` against a `bridges_compiled=2 loops_aborted=0
guard_failures=401` baseline, and reads the baseline again with this change.

check.py dynasm 470/470, cranelift 470/470, wasm 462/462, parity all pass,
`cargo test --workspace --features dynasm` green. No `.jitstats` baseline
moved.

Assisted-by: Claude
`FRAME_DEBUG_DATA_DESCR_GROUP`'s accessors name a field by its position in the
group's field list, so a field inserted ahead of one re-points it at its
neighbour and still compiles. `frame_debug_data_w_globals_descr` carried an
offset assert; `w_locals` and `w_extra_locals` did not.

Assisted-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

pyre/pyre/pyre-jit/src/eval.rs

Lines 13116 to 13118 in 6ce2bb3

let callee_ns = unsafe {
pyre_interpreter::w_code_get_w_globals(w_code as pyre_object::PyObjectRef)
as *const ()

P1 Badge Rebuild the inner frame before reading its globals

When a guard resumes inside an inlined activation whose code object has a per-frame globals override, this reads the code object's first-published w_globals rather than the activation's FrameDebugData.w_globals. Because build_resumed_frames also assigns the root virtualizable frame pointer to every decoded section, the inner ResumedFrame cannot recover its own override and can resume LOAD_GLOBAL against the wrong module namespace. Carry or reconstruct a distinct callee frame red and derive the namespace with its get_w_globals().

AGENTS.md reference: AGENTS.md:L19-L24

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

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