str: JIT quasi-immut retrace + DELETE_FAST walker lowering; utf8_mode flag#533
Conversation
|
Warning Review limit reached
Next review available in: 20 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (23)
WalkthroughThis PR adds full-body-walker method handling, explicit ChangesFull-body walker and JIT runtime paths
UnboundLocalError support
Optimizer dependency and warmstate updates
UTF-8 runtime flag resolution
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Walker
participant MethodFolder
participant InlineCall
participant ResidualCall
Walker->>MethodFolder: specialize LOAD_ATTR and fold LOAD_METHOD_SELF
MethodFolder->>InlineCall: provide method-form receiver and arguments
InlineCall->>ResidualCall: emit tagged LoadMethodSelf residual call
Possibly related issues
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d8ab01c5fe
ℹ️ 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".
| if !flags.ignore_environment { | ||
| if let Ok(value) = std::env::var("PYTHONUTF8") { | ||
| if !value.is_empty() { | ||
| return if value == "0" { 0 } else { 1 }; |
There was a problem hiding this comment.
Reject invalid PYTHONUTF8 values
When PYTHONUTF8 is set to any non-empty value other than 0, this branch silently enables UTF-8 mode; PyPy's launcher only accepts 0 or 1 and otherwise exits with the "invalid PYTHONUTF8 environment variable value" error (pypy/interpreter/app_main.py:759-771, also covered by lib-python/3/test/test_utf8_mode.py:95-98). In an environment like PYTHONUTF8=xxx, pyre now continues with sys.flags.utf8_mode == 1 instead of failing during startup.
Useful? React with 👍 / 👎.
| .or_else(|| std::env::var("LANG").ok()); | ||
| match locale.as_deref() { | ||
| None | Some("") | Some("C") | Some("POSIX") => true, | ||
| Some(value) => !value.contains('.'), |
There was a problem hiding this comment.
Limit locale auto-enable to C/POSIX
This treats every locale string without a dot as a request for UTF-8 mode, but the PyPy launcher only turns it on when the effective LC_CTYPE locale is exactly C or POSIX (pypy/interpreter/app_main.py:775-780). With a non-C locale name that lacks an encoding suffix, such as LANG=en_US, sys.flags.utf8_mode is reported as 1 even though the runtime should leave UTF-8 mode off, which changes locale-dependent stdlib behavior.
Useful? React with 👍 / 👎.
🤖 Codex parity reviewStatic analysis of this diff vs the local RPython/PyPy sources (commit 49b7525). Files in the reviewed diff1. Regressions to PyPy parity introduced by this patchNone. 2. Other mismatches introduced by this patchNone. 3. Pre-existing mismatches (already present before this patch)
4. Structural adaptations
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8bc1bd4cfc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| pub static EXC_TYPE_ERROR_TYPE: PyType = crate::pyobject::new_pytype("TypeError"); | ||
| pub static EXC_VALUE_ERROR_TYPE: PyType = crate::pyobject::new_pytype("ValueError"); | ||
| pub static EXC_NAME_ERROR_TYPE: PyType = crate::pyobject::new_pytype("NameError"); | ||
| pub static EXC_UNBOUND_LOCAL_ERROR_TYPE: PyType = crate::pyobject::new_pytype("UnboundLocalError"); |
There was a problem hiding this comment.
Register UnboundLocalError in the PyType hierarchy
Adding a new PyType here also needs a corresponding (EXC_UNBOUND_LOCAL_ERROR_TYPE, EXC_NAME_ERROR_TYPE) entry in pyre-object/src/pyobject.rs::all_foreign_pytypes; otherwise objects materialized for an unbound fast-local read have ob_type == EXC_UNBOUND_LOCAL_ERROR_TYPE but fail pyre_object::is_exception(). In that scenario the runtime skips exception-only paths such as traceback attachment in pytraceback::record and the special BaseException attribute handling, so the new exception class behaves unlike other W_BaseException subclasses.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
majit/majit-metainterp/src/pyjitpl.rs (1)
6546-7020: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winPropagate quasi-immutable deps from
compile_retrace
compile_retraceshould moveunroll_opt.quasi_immutable_depsintoself.last_quasi_immutable_depson success, like the other compile paths. Otherwiseregister_quasi_immutable_depsdrains an empty list after retrace, so those loops never register their invalidation watchers.🤖 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 `@majit/majit-metainterp/src/pyjitpl.rs` around lines 6546 - 7020, On the successful `compile_retrace` path, move `unroll_opt.quasi_immutable_deps` into `self.last_quasi_immutable_deps` before the optimizer is discarded, matching the propagation performed by the other compile paths. Ensure `register_quasi_immutable_deps` can subsequently drain and register these retrace dependencies.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pyre/pyre-jit-trace/src/jitcode_dispatch.rs`:
- Around line 14786-14792: Update the callable/receiver recovery loop to detect
and preserve the GcRef::NO_CONCRETE sentinel returned by ctx.trace_ctx.box_value
before constructing ConcreteValue::Ref. Do not convert usize::MAX into a
PyObjectRef; keep the concrete value null or otherwise follow the existing
unresolved-value path so invalid arguments never reach resolve_inlinable_callee
or method dispatch.
In `@pyre/pyre-object/src/interp_exceptions.rs`:
- Around line 1327-1329: Add regression assertions to
test_exc_kind_matches_hierarchy covering UnboundLocalError: verify it matches
NameError, Exception, and BaseException through the hierarchy-matching API. Keep
the existing hierarchy cases unchanged and place the assertions alongside the
current NameError coverage.
In `@pyre/pyrex/src/lib.rs`:
- Around line 123-128: Update the PYTHONUTF8 handling in the environment-flag
logic to accept only “0” and “1”; return the corresponding UTF-8 mode for those
values, and reject any other non-empty value by propagating an initialization
error instead of defaulting to enabled. Preserve the existing behavior for unset
or empty values and the ignore_environment path.
- Around line 108-117: Update locale_implies_utf8_mode to determine UTF-8 mode
from the active locale/codeset state using the platform locale APIs, rather than
treating locale strings without a dot as UTF-8. Preserve the existing true
behavior for unset, empty, C, and POSIX locales, and ensure values such as en_US
follow the active codeset result.
---
Outside diff comments:
In `@majit/majit-metainterp/src/pyjitpl.rs`:
- Around line 6546-7020: On the successful `compile_retrace` path, move
`unroll_opt.quasi_immutable_deps` into `self.last_quasi_immutable_deps` before
the optimizer is discarded, matching the propagation performed by the other
compile paths. Ensure `register_quasi_immutable_deps` can subsequently drain and
register these retrace dependencies.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 20f0a50c-8e88-41f2-856c-cd047ddd2a60
📒 Files selected for processing (19)
majit/majit-ir/src/effectinfo.rsmajit/majit-metainterp/src/optimizeopt/heap.rsmajit/majit-metainterp/src/optimizeopt/unroll.rsmajit/majit-metainterp/src/pyjitpl.rsmajit/majit-metainterp/src/warmstate.rspyre/pyre-interpreter/src/builtins.rspyre/pyre-interpreter/src/error.rspyre/pyre-interpreter/src/eval.rspyre/pyre-interpreter/src/importing.rspyre/pyre-interpreter/src/pyframe.rspyre/pyre-jit-trace/src/jitcode_dispatch.rspyre/pyre-jit-trace/src/opcode_handler_impls.rspyre/pyre-jit-trace/src/state.rspyre/pyre-jit-trace/src/trace_opcode.rspyre/pyre-jit/src/call_jit.rspyre/pyre-jit/src/jit/codewriter.rspyre/pyre-jit/src/jit/flatten.rspyre/pyre-object/src/interp_exceptions.rspyre/pyrex/src/lib.rs
- compile_retrace moves unroll_opt.quasi_immutable_deps into last_quasi_immutable_deps on the compiled-true path, so retraced loops register their quasi-immutable invalidation watchers like the other compile paths (register_quasi_immutable_deps otherwise drains an empty list after retrace). - the walker method-form call recovery rejects GcRef::NO_CONCRETE and null refs before writing ConcreteValue::Ref into the callable/receiver slots. - test_exc_kind_matches_hierarchy asserts UnboundLocalError matches NameError and the Exception/BaseException hierarchy. Assisted-by: Claude
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1d18088b58
ℹ️ 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".
| record_graph_op( | ||
| ¤t_block.block(), | ||
| "setarrayitem_vable_r", | ||
| vable_setarrayitem_ref_graph_args( | ||
| frame_var.into(), | ||
| v_idx.into(), | ||
| super::flow::Constant::none().into(), | ||
| ), |
There was a problem hiding this comment.
Preserve DELETE_FAST's unbound-local check
When DELETE_FAST runs on a slot that is already PY_NULL (for example def f(): del x or a second del x after the first clear), the interpreter now raises UnboundLocalError in eval.rs::delete_fast, but this JIT lowering only writes PY_NULL and continues. With PYRE_FBW_DELETE_FAST default-on, a traced/compiled path can suppress that exception and fall through or return normally, so the lowering needs the same null check and raise before clearing the local.
Useful? React with 👍 / 👎.
- compile_retrace moves unroll_opt.quasi_immutable_deps into last_quasi_immutable_deps on the compiled-true path, so retraced loops register their quasi-immutable invalidation watchers like the other compile paths (register_quasi_immutable_deps otherwise drains an empty list after retrace). - the walker method-form call recovery rejects GcRef::NO_CONCRETE and null refs before writing ConcreteValue::Ref into the callable/receiver slots. - test_exc_kind_matches_hierarchy asserts UnboundLocalError matches NameError and the Exception/BaseException hierarchy. Assisted-by: Claude
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1597b7a778
ℹ️ 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".
| if cleanup_dead_token_cell { | ||
| // warmstate.py:483-500 — an invalidated/dead procedure token is | ||
| // removed from the chain, resetting the hot counter so it re-arms. | ||
| self.cleanup_chain(green_key_hash); | ||
| return HotResult::NotHot; |
There was a problem hiding this comment.
Remove invalidated force-finish cells before rearming
If a compiled loop has previously been marked FORCE_FINISH (the trace-too-long segmenting path) and is later invalidated by a quasi-immutable dependency, this cleanup does not actually remove the dead cell: cleanup_chain() delegates to install_new_cell(), whose should_remove_jitcell() keeps any FORCE_FINISH cell even when get_procedure_token() is now None. Every subsequent entry sees the same has_seen_a_procedure_token && !has_procedure_token condition, resets the counter again, and returns NotHot, so that loop never re-arms or retraces after invalidation.
Useful? React with 👍 / 👎.
…8 / locale Default utf8_mode to 0; a C/POSIX or unset locale (LC_ALL/LC_CTYPE/LANG) coerces it to 1, and -X utf8 / PYTHONUTF8 override (PYTHONUTF8 suppressed under -E/-I), matching CPython 3.14. utf8_mode is reported only through sys.flags.utf8_mode; pyre's I/O stays UTF-8 regardless. Assisted-by: Claude
…ting them A green key whose procedure token was invalidated by a quasi-immutable mutation (module-dict `ModuleDictStrategy.version` bump on a new global key) was never re-traced: `loops_compiled` stayed 1 and later calls ran the loop interpreted. warmstate now re-arms such a cell — the loop-header (`counter_tick_checked`, `maybe_compile`, `maybe_compile_with_key`) and function-entry (`should_trace_function_entry`) counter paths detect a cell that has seen a procedure token but whose `get_procedure_token()` is now `None`, remove the dead-token cell via `cleanup_chain`, and re-count from cold (`warmstate.py:483-500`), so the loop re-traces when hot again. Supporting the re-trace cycle: - namespace quasi-immutable folds route through `record_namespace_quasiimmut_field`, which records `QuasiimmutField` and arms the paired `GUARD_NOT_INVALIDATED` once per heapcache epoch (`pyjitpl.py:1074-1089`); - the unroll optimizer merges `quasi_immutable_deps` from both phase optimizers and `pyjitpl` threads them into `last_quasi_immutable_deps` so a re-traced loop re-registers its version watcher (`compile.py:204-207`); - `has_compiled_targets` requires a live loop so an invalidated token is not treated as jumpable. global_reassign 0.58s -> 0.04s, global_reassign_obj 0.57s -> 0.04s; 163/163. Assisted-by: Claude
DELETE_FAST lowered to an abort_permanent marker, so any hot loop whose body contained a `del` — including the implicit `del e` an `except E as e:` handler emits — ran fully interpreted (loops_compiled=0). Give DELETE_FAST a walker-native lowering that writes PY_NULL into the local slot (mirroring eval.rs delete_fast and STORE_FAST's setarrayitem_vable_r), gated by PYRE_FBW_DELETE_FAST (default on; 0/false restores the marker). A LOAD_FAST_CHECK / LOAD_FAST pair reading a slot proven unbound raises UnboundLocalError. Add UnboundLocalError as a NameError subclass (ExcKind::UnboundLocalError): unbound fast-local reads (load_local_checked_value, deref_unbound_error for cellvars, bh_load_fast_check_fn) raise it, and `except NameError` catches it. Unbound freevars keep raising NameError. Assisted-by: Claude
…he method-form CALL try_walker_specialize_load_method_attr resolves a monomorphic instance method to a green ConstRef W_Function behind guard_class + exact-class + type version_tag + instance-map guards (via load_method_fast_path), and try_walker_fold_load_method_self const-folds compute_load_method_bound once attr is concrete. The CALL inliner now inlines the method form bh_call_fn_N(callable, self, args...) by passing the receiver as the first positional argument, so an inlined method body matches a plain inlined call. Gated by PYRE_FBW_LOADMETHOD_FOLD (default on). Adds PyreHelperKind::LoadMethodSelf (enum tail, repr(u8) discriminant-stable) and tags the load_method_self residual so the walker recognises it. Assisted-by: Claude
Assisted-by: Claude
…f8 coercion PYTHONUTF8 now accepts only "0"/"1"/empty; any other value emits the preconfig_init_utf8_mode fatal error and exits 1. -X utf8=<other> emits the same fatal error. locale_implies_utf8_mode coerces utf8_mode to 1 only for the C/POSIX or unset/empty locale; every named locale (en_US, C.UTF-8, en_US.UTF-8, ...) leaves it 0. Assisted-by: Claude
…ption GC tid DELETE_FAST on an already-unbound local now raises UnboundLocalError instead of silently succeeding. UnboundLocalError is listed in all_foreign_pytypes with NameError as its base so instances carry BaseException in the MRO (.args, isinstance against NameError/Exception). The per-ExcKind GC pre-registration loop in pyre-jit now covers UnboundLocalError, binding it to the shared W_BaseException tid; the foreign-pytype loop then skips it via the pytype_to_tid guard. Without the pre-registration that loop would assign an undersized sizeof(PyObject) standalone tid and shift every hardcoded post-loop GC tid, corrupting the descr-to-tid map (bigint external-size dispatch) in release builds where the debug_assert_eq guards are compiled out. Assisted-by: Claude
- compile_retrace moves unroll_opt.quasi_immutable_deps into last_quasi_immutable_deps on the compiled-true path, so retraced loops register their quasi-immutable invalidation watchers like the other compile paths (register_quasi_immutable_deps otherwise drains an empty list after retrace). - the walker method-form call recovery rejects GcRef::NO_CONCRETE and null refs before writing ConcreteValue::Ref into the callable/receiver slots. - test_exc_kind_matches_hierarchy asserts UnboundLocalError matches NameError and the Exception/BaseException hierarchy. Assisted-by: Claude
getitem_list / getitem_tuple / getitem_str iterated `while i < stop` with
`i += step`, which overflowed i64 when step approached sys.maxsize and produced
one extra element (list('abcde')[1::sys.maxsize] yielded ['b','e'] instead of
['b']). Route the three through slice_unpack + slice_adjust_indices and iterate
the adjusted slicelength, advancing i only between elements so the final step
never overflows. memoryview_slice_view moves to the same overflow-safe
slicelength form. Fixes test.test_str StrTest.test_extended_getslice.
Assisted-by: Claude
str_repeat judged the repeat overflow at the WTF-8 byte length, so a non-ASCII character (wider in WTF-8 than its Py_UCS storage) could push the byte product past isize::MAX and raise OverflowError where the code-point-count product does not overflow. Judge overflow on the code-point count (count * char_len) like unicode_repeat and let try_reserve_exact report the allocation failure as MemoryError. Fixes StrTest.test_raiseMemError. Assisted-by: Claude
The walker and codewriter DELETE_FAST lowering cleared the local slot without the bound check that pyopcode.py:998 DELETE_FAST and the interpreter delete_fast perform, so a JIT-compiled `del x` on an already-unbound fast local silently succeeded instead of raising UnboundLocalError. The walker raises when the local is unbound at trace time — checking both the concrete-local shadow and the traced IR box — and guards the slot non-null otherwise so a slot that is bound at trace time but null at runtime deopts and re-raises. The codewriter emits a load_fast_check residual that raises on PY_NULL and branches to an abort/deopt on a null runtime slot. Bridge and deopt assembly seed concrete_locals with Ref(PY_NULL) for unbound slots so the concrete check fires in bridge traces. Assisted-by: Claude
Assisted-by: Claude
When the codewriter models a LOAD_FAST_CHECK local as a `ConstantValue::None` slot (proven unbound at compile time, e.g. after a preceding DELETE_FAST), the load unconditionally raises UnboundLocalError. The previous lowering emitted the `load_fast_check` residual + `GuardNoException` + push, leaving a normal-return `Finish` continuation; a later guard-failure bridge resolved into that return and dropped the raise, so `x = 1; del x; return x` in a hot loop counted 2217 raises instead of 20000. Bail to the interpreter at the known-unbound load so it raises, matching the DeleteFast known-unbound handling. Assisted-by: Claude
A bridge compiled from an exception guard (GUARD_NO_EXCEPTION) whose raising
op is not caught in the current frame walked from the no-exception fallthrough
resume_pc and emitted `Finish(<null raised-call result>)`, so the compiled
bridge handed a NULL up to the caller ("call failed" crash) instead of
propagating the exception. The `emit_exception_bridge_prologue` GUARD_EXCEPTION
path that would trace the propagate-out continuation requires resume-data
replay that is not wired (its synthetic guard carries no rd_resume_position),
so extend the pending-exception decline to the escaping case, matching the
existing caught-in-frame handling; the blackhole resume propagates the
exception out of the frame to the caller's handler.
Assisted-by: Claude
…ast label
The wasm loop lowering assumes the closing back-edge JUMP targets the
LAST Label in the trace: `build_function` picks the loop-back label with
`ops.rposition(Label)`, `find_label_args` returns that label's args, and
the JUMP arm maps the jump args onto it positionally with
`n = min(jump_args, last_label_args)` then `br(0)`.
A loop re-traced after a quasi-immutable module-global invalidation can
instead close with a JUMP back to its 12-arg preamble entry label
(`LoopTargetDescr(start:0)`) while a narrower 4-arg peeled header sits
last. The lowering then maps the wide jump's args onto the narrow last
label's params, leaking preamble constants (the induction step) into
loop-carried slots and branching to the wrong label, so
`bench/synth/global_reassign` and `global_reassign_obj` returned wrong
values ("TypeError: not an iterator").
`compile_bridge` already declines the analogous non-last-label
loop-closing JUMP (BRIDGE_DIAG 9/10). Add the same guard to the
`compile_loop` path in `wasm_unsupported_trace_reason`: when the closing
JUMP's descr identity differs from the last Label's, decline so the
interpreter runs the loop. The invalidated source token is filtered by
`get_procedure_token`, so the declined re-trace falls back to the
interpreter rather than dispatching the stale first compile.
Assisted-by: Claude
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0a94099f2c
ℹ️ 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".
| if method_form { | ||
| callee_args.push(r_args[1]); | ||
| callee_arg_concretes.push(arg_concretes[1]); |
There was a problem hiding this comment.
Guard method-form self before inlining
When this method-form path is taken, the trace-time non-null null_or_self decision is used to prepend r_args[1] to the inlined callee, but no guard is emitted that the load_method_self result stays non-null. If the LOAD_METHOD fold did not install its class/map guards (for example with PYRE_FBW_LOADMETHOD_FOLD=0 or a method name the fold declines) and the instance later shadows the method with the same function object, the real bh_call_fn_impl would see null_or_self == PY_NULL and not prepend self, while the compiled inline path still passes the null sentinel as the first argument. Please either guard r_args[1] non-null before inlining the method form or decline this path unless the guarded LOAD_METHOD fold proved the binding decision.
Useful? React with 👍 / 👎.
Branch
strovermain— three independent changes, all verified164/164onpyre/check.py --backend dynasm.Commits
pyrex/importing: resolvesys.flags.utf8_modefrom-X utf8/PYTHONUTF8/ locale (default-1locale-selected), matching launcher parity.majit: re-trace quasi-immutable-invalidated loops instead of interpreting them. When a module-dict version bump invalidates a compiled loop's baked quasi-immutable cell (e.g. reassigning a global), the loop's procedure token becomes non-jumpable; the warmstate entry points now detecthas_seen_a_procedure_token() && get_procedure_token().is_none(), clean the cell chain, and re-arm the counter so the loop re-traces against the fresh value (warmstate.py:483-500) rather than falling back to the interpreter. Adds pairedQuasiimmutField+GUARD_NOT_INVALIDATEDrecording for namespace cells. Fixesglobal_reassign(14× → 1.0× CPython).majit: lowerDELETE_FASTin the walker and addUnboundLocalError.DELETE_FASTwas lowered to anabort_permanentmarker, so any hot loop whose body contained adel— including the implicitdel eanexcept E as e:handler emits — ran fully interpreted. It now has a walker-native lowering that writesPY_NULLinto the local slot (mirroringdelete_fast/ STORE_FAST'ssetarrayitem_vable_r), gated byPYRE_FBW_DELETE_FAST(default on).UnboundLocalErroris added as a properNameErrorsubclass so unbound fast-local reads raise it andexcept NameErrorstill catches it (unbound freevars keep raisingNameError).Verification
pyre/check.py --backend dynasm: 164/164.DELETE_FAST:swap_except_return_resume−97% (38×),finally_bare_raise−94% (16×),binary_slice_index−39%,build_set_hashability−37%,exception_oserror_fields−34%; the rest within ±4% noise. Zero regressions.UnboundLocalErrorsemantics match CPython 3.14 (MRO,issubclass(UnboundLocalError, NameError),except NameErrorcatch).Summary by CodeRabbit
Bug Fixes
UnboundLocalError, including in optimized and traced execution paths.Performance