Repository navigation
typedef: method-wrapper and classmethod_descriptor types, plus jit-stats baselines - #970
Conversation
…abort `bh_load_global_fn(namespace_ptr, w_code, frame, namei)` discards `namespace_ptr` and resolves the namespace from the executing frame or the callee's own promoted `w_code`; the operand survives only as the cell-fold recogniser's hint. A nested function's `LOAD_GLOBAL` folds it to the NULL constant, and both NULL-Ref-arg refusals treated that as the broken baked-NULL self-slot shape. `walker_abort_if_mayforce_null_ref_arg` then aborted the walk after an effectful residual had already run concretely, so the caller replayed the region: a callee that stores a residual result into a local and then raises (`value = self.stack.pop(); raise _Stop(value)`) ran its `pop` twice. Add the arg-0 exemption to both refusals, which the comments already require to match. synth/pickle_terminal_raise_resume prints `checksum = 216` on dynasm, cranelift, PYRE_JIT=0, pypy3 and cpython; it crashed with `IndexError: pop from empty list` on all three pyre arms before. Assisted-by: Claude
Registers two descriptor types the namespace sweeps did not model: - `method-wrapper`, the bound form of a `wrapper_descriptor`, produced by `bind_slot_wrapper` and `baseobjspace::get`. Both retag sweeps now route through `gateway::is_slot_wrapper`, so a slot dunder becomes a `wrapper_descriptor` and its `__get__` a `method-wrapper`, while a `tp_methods` entry stays a `method_descriptor` binding to `builtin_function_or_method`. - `classmethod_descriptor`, a builtin type's `METH_CLASS` entry. The payload stays the `ClassMethod` a `@classmethod` uses and only the published class differs; `classmethod_descr_get` binds it through `builtin_bound_method_new`. Types built before that type object exists are retagged by a closing pass of `init_typeobjects`. `is_bound_builtin_method` now tests the published class, which both bound forms share. `wrapper_descriptor` and `method_descriptor` gain the `__reduce__` that `copyreg._reduce_ex` needs. `is_native_exception_dunder` accepts every fixed-code carrier, since `__str__` and `__repr__` are now `wrapper_descriptor`. `lib-python/3/inspect.py` drops the adaptations these types replace: `ismethod`, `isbuiltin`, `ismethodwrapper`, `ismethoddescriptor`, `_NonUserDefinedCallables`, `_signature_get_user_defined_method`, and the `_slot_wrapper_type` / `_builtin_code_type` discriminators. `dict.update.__code__` no longer resolves, so the latter was already None. Assisted-by: Claude
`check.py`'s floor hard-fails a synthetic benchmark that emits a `[jit-stats]` line without a committed baseline. Seven had none on any backend: hot_loop_exit_then_class_stmt, inline_freevar_after_mayforce, math_log_trig_hot, pickle_terminal_raise_resume, pypy_dict_primitives_nonbinding, raise_reg_unbound_jitstress and tuple_unpack_array_backed_hot. Recorded with `check.py --snapshot --synthetic-only --synthetic-pattern <name>.py --backend dynasm,cranelift,wasm`; all three backends passed for all seven. Two baselines pin a nonzero badness field: - `raise_reg_unbound_jitstress` loops_aborted=2, identical on all three backends. - `pickle_terminal_raise_resume` loops_aborted=54 on wasm against 0 on both native backends. The value is what the wasm backend does today; the divergence is unexplained and the baseline only pins it against a rise. Assisted-by: Claude
float_div_zero_caught_loop, mutate_then_raise_caught, inline_subwalk_property_mutates and inline_subwalk_mutating_residual all fail the `guard_failures` bounded-rise gate by a constant +209/210 with exactly one extra bridge each. Attributed to the `try_walker_inline_resolved_user_call` arm in `pyre-jit-trace/src/jitcode_dispatch/inline_call.rs` that declines to inline a branchy callee owning an exception handler when `fbw_callee_body_replay_safety` is not `Clean`: re-entering the opcode that owns the abort jitcode pc would repeat its residual effect, because the callee-rebuild payload cannot carry a post-op stack anchor. Reverting that hunk alone restores the old counters exactly, so the rise is the guard's price and the guard stays. Recorded on all three backends. No `JITSTATS_BADNESS_FIELDS` value rises: `loops_aborted` falls on every affected fixture (1 -> 0, 2 -> 1, 2 -> 0) and `loops_compiled` rises where it moves. The five remaining jit-stats failures are left alone — closure_freevar_branch_resume, exception_args_virtual, exception_multi_handler_warmup, exception_reraise_tb_depth_jitstress and list_length_hint_validate raise `loops_aborted` and lower `loops_compiled` with no attributed cause, and re-recording them would disarm the floor. Assisted-by: Claude
WalkthroughThe PR adds CPython-compatible method-wrapper and classmethod-descriptor behavior, simplifies callable inspection, permits NULL LoadGlobal namespace hints during residual calls, and updates synthetic JIT statistics. ChangesDescriptor compatibility
Callable classification
Synthetic JIT statistics
LoadGlobal NULL handling
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant BuiltinTypeSetup
participant DescriptorCarriers
participant BoundMethod
BuiltinTypeSetup->>DescriptorCarriers: register and retag method-wrapper carriers
DescriptorCarriers->>BoundMethod: construct bound slot-wrapper method
BoundMethod->>BoundMethod: validate receiver and invoke wrapped slot
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
🤖 Codex parity reviewStatic analysis of this diff vs the local RPython/PyPy sources (commit eeecf68). 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)None. 4. Structural adaptations
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eeecf68f34
ℹ️ 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 is_load_global && i == 0 { | ||
| continue; |
There was a problem hiding this comment.
Preserve the callee frame for LOAD_GLOBAL
When an inlined callee's namespace operand is NULL—the exact case this branch exempts—the residual executes even though its frame operand aliases an outer frame. bh_load_global_fn ignores the namespace operand, falls back to the code object's first-stamped globals, and still obtains builtins from the aliased outer frame; consequently, a shared code object used with distinct function globals or a cross-module callee with custom __builtins__ can resolve LOAD_GLOBAL from the wrong namespace after JIT compilation. Thread the callee's red frame/namespace instead of whitelisting the collapsed operand.
AGENTS.md reference: AGENTS.md:L32-L42
Useful? React with 👍 / 👎.
| match positional.get(1) { | ||
| Some(&cls) if pyre_object::is_type(cls) => {} |
There was a problem hiding this comment.
Require the descriptor owner to be a subtype
Checking only that cls is a type accepts unrelated classes, although a classmethod_descriptor requires a subtype of its defining owner. For example, dict.__dict__['fromkeys'](list, ['x']) is forwarded into dict_fromkeys_impl with list as cls rather than raising the required TypeError, potentially producing an unrelated container; explicit __get__ has the same missing validation. Check issubtype_w(cls, __objclass__) before binding or calling the wrapped function.
AGENTS.md reference: AGENTS.md:L195-L195
Useful? React with 👍 / 👎.
| make_builtin_function_with_arity( | ||
| "__get__", | ||
| |args| { | ||
| classmethod_descriptor_function(args[0], "__get__")?; | ||
| classmethod_descr_get(args) | ||
| }, | ||
| 3, |
There was a problem hiding this comment.
Accept an omitted owner in descriptor get
classmethod_descriptor.__get__ has the CPython signature __get__(instance, owner=None), so calls such as dict.__dict__['fromkeys'].__get__({}) must infer dict from the instance and bind successfully. Registering this implementation with fixed arity 3 rejects the two-argument raw call before classmethod_descr_get can exercise its existing missing-owner fallback; use an optional/variadic signature instead.
AGENTS.md reference: AGENTS.md:L195-L195
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pyre/pyre-interpreter/src/typedef.rs`:
- Around line 13288-13300: Update classmethod_descriptor_function to report
receiver mismatches using CPython’s standard descriptor text, including the
descriptor name, expected classmethod_descriptor type, and received object type.
Match the formatting used by method_wrapper_receiver and
method_descriptor_receiver, while preserving the existing successful return
path.
- Around line 13361-13390: Update the __call__ implementation registered for
classmethod_descriptor to validate that argument 2 is not only a type but also a
subtype of the descriptor’s d_type, using crate::baseobjspace::issubtype_w.
Preserve the existing missing-argument and non-type errors, and reject unrelated
types before invoking function_descr_call_impl.
- Around line 12759-12770: Update descr_reduce to root the owner obtained from
fget_func_objclass before allocating the name string, then root the allocated
name before constructing the nested argument tuple. Reload both rooted values
after each allocation-triggering call before passing them to w_tuple_new,
preserving the existing getattr result structure.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 7e2b3d90-ec8a-4943-a044-409c0c91d712
📒 Files selected for processing (39)
lib-python/3/inspect.pypyre/bench/synth/float_div_zero_caught_loop.cranelift.jitstatspyre/bench/synth/float_div_zero_caught_loop.dynasm.jitstatspyre/bench/synth/float_div_zero_caught_loop.wasm.jitstatspyre/bench/synth/hot_loop_exit_then_class_stmt.cranelift.jitstatspyre/bench/synth/hot_loop_exit_then_class_stmt.dynasm.jitstatspyre/bench/synth/hot_loop_exit_then_class_stmt.wasm.jitstatspyre/bench/synth/inline_freevar_after_mayforce.cranelift.jitstatspyre/bench/synth/inline_freevar_after_mayforce.dynasm.jitstatspyre/bench/synth/inline_freevar_after_mayforce.wasm.jitstatspyre/bench/synth/inline_subwalk_mutating_residual.cranelift.jitstatspyre/bench/synth/inline_subwalk_mutating_residual.dynasm.jitstatspyre/bench/synth/inline_subwalk_mutating_residual.wasm.jitstatspyre/bench/synth/inline_subwalk_property_mutates.cranelift.jitstatspyre/bench/synth/inline_subwalk_property_mutates.dynasm.jitstatspyre/bench/synth/inline_subwalk_property_mutates.wasm.jitstatspyre/bench/synth/math_log_trig_hot.cranelift.jitstatspyre/bench/synth/math_log_trig_hot.dynasm.jitstatspyre/bench/synth/math_log_trig_hot.wasm.jitstatspyre/bench/synth/mutate_then_raise_caught.cranelift.jitstatspyre/bench/synth/mutate_then_raise_caught.dynasm.jitstatspyre/bench/synth/mutate_then_raise_caught.wasm.jitstatspyre/bench/synth/pickle_terminal_raise_resume.cranelift.jitstatspyre/bench/synth/pickle_terminal_raise_resume.dynasm.jitstatspyre/bench/synth/pickle_terminal_raise_resume.wasm.jitstatspyre/bench/synth/pypy_dict_primitives_nonbinding.cranelift.jitstatspyre/bench/synth/pypy_dict_primitives_nonbinding.dynasm.jitstatspyre/bench/synth/pypy_dict_primitives_nonbinding.wasm.jitstatspyre/bench/synth/raise_reg_unbound_jitstress.cranelift.jitstatspyre/bench/synth/raise_reg_unbound_jitstress.dynasm.jitstatspyre/bench/synth/raise_reg_unbound_jitstress.wasm.jitstatspyre/bench/synth/tuple_unpack_array_backed_hot.cranelift.jitstatspyre/bench/synth/tuple_unpack_array_backed_hot.dynasm.jitstatspyre/bench/synth/tuple_unpack_array_backed_hot.wasm.jitstatspyre/pyre-interpreter/src/baseobjspace.rspyre/pyre-interpreter/src/builtins.rspyre/pyre-interpreter/src/function.rspyre/pyre-interpreter/src/typedef.rspyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
| /// `descrobject.c descr_reduce` — a descriptor reconstructs through | ||
| /// `getattr(objclass, name)`. Shared by `wrapper_descriptor` and | ||
| /// `method_descriptor`; without it `copyreg._reduce_ex` refuses the object at | ||
| /// protocols 0 and 1. | ||
| fn descr_reduce(descr: PyObjectRef) -> crate::PyResult { | ||
| let owner = unsafe { crate::function::fget_func_objclass(descr)? }; | ||
| let name = pyre_object::w_str_new(unsafe { crate::function::function_get_name(descr) }); | ||
| Ok(pyre_object::w_tuple_new(vec![ | ||
| crate::baseobjspace::builtin_callable("getattr"), | ||
| pyre_object::w_tuple_new(vec![owner, name]), | ||
| ])) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm the `builtin_callable` symbol used by `descr_reduce`.
rg -nP -C4 '\bfn\s+builtin_callable\s*\(' pyre/pyre-interpreter/src
# Confirm the shadow-stack rooting helpers and the allocation behaviour of the callees.
rg -nP -C3 '\bpub fn (w_str_new|w_tuple_new)\s*\(' pyre/pyre-object/src
rg -nP -C3 '\bpub fn (push_roots|pin_root|shadow_stack_get|shadow_stack_len)\s*\(' pyre/pyre-object/src/gc_roots.rsRepository: youknowone/pyre
Length of output: 4656
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== baseobjspace builtin_callable =="
sed -n '2175,2220p' pyre/pyre-interpreter/src/baseobjspace.rs
echo "== typedef descr_reduce and nearby stamp_method_rooting docs/usages =="
sed -n '1945,1975p' pyre/pyre-interpreter/src/typedef.rs
sed -n '12745,12775p' pyre/pyre-interpreter/src/typedef.rs
echo "== gc_roots relevant API =="
sed -n '220,320p' pyre/pyre-object/src/gc_roots.rs
sed -n '360,410p' pyre/pyre-object/src/gc_roots.rs
echo "== tuple allocator =="
sed -n '165,205p' pyre/pyre-object/src/tupleobject.rs
echo "== unicode allocator quick scan =="
sed -n '1,100p' pyre/pyre-object/src/unicodeobject.rsRepository: youknowone/pyre
Length of output: 18794
Root descriptor fields before allocations in descr_reduce.
fget_func_objclass(descr) produces an object field, then w_str_new and w_tuple_new may run GC. Store these values in roots before the allocation calls and read them back before constructing the result tuple. Also root the allocated string before passing it into the nested tuple so the collector can relocate both fields across the GC malloc on the collection path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pyre/pyre-interpreter/src/typedef.rs` around lines 12759 - 12770, Update
descr_reduce to root the owner obtained from fget_func_objclass before
allocating the name string, then root the allocated name before constructing the
nested argument tuple. Reload both rooted values after each
allocation-triggering call before passing them to w_tuple_new, preserving the
existing getattr result structure.
| /// The wrapped builtin callable of a `classmethod_descriptor`, which carries | ||
| /// the owner, name, doc and signature the descriptor publishes. | ||
| fn classmethod_descriptor_function( | ||
| obj: PyObjectRef, | ||
| name: &str, | ||
| ) -> Result<PyObjectRef, crate::PyError> { | ||
| if !crate::function::is_classmethod_descriptor(obj) { | ||
| return Err(crate::PyError::type_error(format!( | ||
| "descriptor '{name}' requires a 'classmethod_descriptor' object" | ||
| ))); | ||
| } | ||
| Ok(unsafe { pyre_object::function::w_classmethod_get_func(obj) }) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Align the receiver-mismatch message with CPython.
CPython raises the standard descriptor mismatch text for these getsets, for example descriptor '__objclass__' for 'classmethod_descriptor' objects doesn't apply to a 'int' object. The current message drops the received type and uses a different wording. The sibling helpers method_wrapper_receiver (Line 12971) and method_descriptor_receiver already produce the CPython form, so this helper is the only divergence.
🐛 Proposed fix
fn classmethod_descriptor_function(
obj: PyObjectRef,
name: &str,
) -> Result<PyObjectRef, crate::PyError> {
if !crate::function::is_classmethod_descriptor(obj) {
+ let received = type_name_of(obj);
return Err(crate::PyError::type_error(format!(
- "descriptor '{name}' requires a 'classmethod_descriptor' object"
+ "descriptor '{name}' for 'classmethod_descriptor' objects \
+ doesn't apply to a '{received}' object"
)));
}
Ok(unsafe { pyre_object::function::w_classmethod_get_func(obj) })
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /// The wrapped builtin callable of a `classmethod_descriptor`, which carries | |
| /// the owner, name, doc and signature the descriptor publishes. | |
| fn classmethod_descriptor_function( | |
| obj: PyObjectRef, | |
| name: &str, | |
| ) -> Result<PyObjectRef, crate::PyError> { | |
| if !crate::function::is_classmethod_descriptor(obj) { | |
| return Err(crate::PyError::type_error(format!( | |
| "descriptor '{name}' requires a 'classmethod_descriptor' object" | |
| ))); | |
| } | |
| Ok(unsafe { pyre_object::function::w_classmethod_get_func(obj) }) | |
| } | |
| /// The wrapped builtin callable of a `classmethod_descriptor`, which carries | |
| /// the owner, name, doc and signature the descriptor publishes. | |
| fn classmethod_descriptor_function( | |
| obj: PyObjectRef, | |
| name: &str, | |
| ) -> Result<PyObjectRef, crate::PyError> { | |
| if !crate::function::is_classmethod_descriptor(obj) { | |
| let received = type_name_of(obj); | |
| return Err(crate::PyError::type_error(format!( | |
| "descriptor '{name}' for 'classmethod_descriptor' objects \ | |
| doesn't apply to a '{received}' object" | |
| ))); | |
| } | |
| Ok(unsafe { pyre_object::function::w_classmethod_get_func(obj) }) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pyre/pyre-interpreter/src/typedef.rs` around lines 13288 - 13300, Update
classmethod_descriptor_function to report receiver mismatches using CPython’s
standard descriptor text, including the descriptor name, expected
classmethod_descriptor type, and received object type. Match the formatting used
by method_wrapper_receiver and method_descriptor_receiver, while preserving the
existing successful return path.
| pyre_object::w_dict_setitem_str_no_proxy( | ||
| ns, | ||
| "__call__", | ||
| make_builtin_function("__call__", |args| { | ||
| let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args); | ||
| let descr = positional.first().copied().unwrap_or(pyre_object::PY_NULL); | ||
| let function = classmethod_descriptor_function(descr, "__call__")?; | ||
| // `descrobject.c classmethoddescr_call` — the receiver is the | ||
| // class itself, so it must be a type rather than an instance. | ||
| let name = crate::function::function_get_name(function); | ||
| let owner_name = | ||
| pyre_object::w_type_get_name(crate::function::fget_func_objclass(function)?); | ||
| match positional.get(1) { | ||
| Some(&cls) if pyre_object::is_type(cls) => {} | ||
| Some(&cls) => { | ||
| let got = type_name_of(cls); | ||
| return Err(crate::PyError::type_error(format!( | ||
| "descriptor '{name}' for type '{owner_name}' needs a type, \ | ||
| not a '{got}' as arg 2" | ||
| ))); | ||
| } | ||
| None => { | ||
| return Err(crate::PyError::type_error(format!( | ||
| "descriptor '{name}' of '{owner_name}' object needs an argument" | ||
| ))); | ||
| } | ||
| } | ||
| function_descr_call_impl(positional, kwargs, function) | ||
| }), | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Locate the subtype predicate used elsewhere for descriptor receiver checks.
rg -nP -C3 '\bfn\s+(issubtype_w|abstract_issubclass_w|issubclass_w)\s*\(' pyre/pyre-interpreter/src
# Compare with the instance-side descriptor checks already implemented.
rg -nP -C8 '\bfn\s+(method_descriptor_check_instance|slot_wrapper_check_instance)\s*\(' pyre/pyre-interpreter/src/typedef.rsRepository: youknowone/pyre
Length of output: 2192
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the classmethod descriptor call implementation and nearby helpers.
sed -n '13320,13415p' pyre/pyre-interpreter/src/typedef.rs
printf '\n--- related checks ---\n'
sed -n '12700,12910p' pyre/pyre-interpreter/src/typedef.rs
printf '\n--- issubtype definition ---\n'
sed -n '355,390p' pyre/pyre-interpreter/src/baseobjspace.rsRepository: youknowone/pyre
Length of output: 14143
Add the missing subtype check in classmethod_descriptor.__call__.
classmethod_descriptor.__call__ currently checks only that argument 2 exists and is a type. classmethoddescr_call also requires that the argument be a subtype of the descriptor’s d_type; without it, dict.__dict__['fromkeys'](int, [1]) can enter the dict implementation with an unrelated class. Use crate::baseobjspace::issubtype_w for this check.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pyre/pyre-interpreter/src/typedef.rs` around lines 13361 - 13390, Update the
__call__ implementation registered for classmethod_descriptor to validate that
argument 2 is not only a type but also a subtype of the descriptor’s d_type,
using crate::baseobjspace::issubtype_w. Preserve the existing missing-argument
and non-type errors, and reject unrelated types before invoking
function_descr_call_impl.
Source: Coding guidelines
…line Its committed baseline is `loops_aborted=3 loops_compiled=6 guard_failures=23380 bridges_compiled=0`. It measures `0 / 3 / 606 / 3`, identically on dynasm and cranelift, and `loops_aborted=0` on wasm. check.py failed it on `loops_compiled 6 -> 3`, a `JITSTATS_FALL_FIELDS` gate. That fall is not a capability loss: `loops_compiled + bridges_compiled` is 6 on both sides, so the three units it stopped compiling as separate loops are the three it now compiles as bridges, while the aborts go to 0 and the guard failures fall 38x. Every gated field therefore moves the tightening way — `loops_aborted` and `guard_failures` down, and the fall gate now pinned at 3 rather than 6. Only `bridges_compiled` rises, and it is in no gated group. Six further fixtures had no baseline at all and were recorded here before the rebase; that half is dropped because #970 has since recorded all six on main. Where the two recordings disagreed — `raise_reg_unbound_jitstress`, `loops_aborted` 2 vs 10 and `loops_compiled` 5 vs 3 — main's is both the authoritative side and the tighter gate, and a build at this base reproduces main's numbers exactly. Four fixtures are still left red on purpose: `exception_args_virtual` (`loops_aborted` 0 -> 3), `exception_multi_handler_warmup` (0 -> 23), `exception_reraise_tb_depth_jitstress` (0 -> 1198, `loops_compiled` 805 -> 305) and `list_length_hint_validate` (14 -> 34). These raise a `JITSTATS_BADNESS_FIELDS` counter whose healthy value is 0, so recording them would switch the floor off for those fixtures permanently. They reproduce byte-identically on dynasm and cranelift, so the drift is walker-level, and they are unchanged across #967's IS_OP fold and #971's front-lowering folds. Assisted-by: Claude
…ed base/top interpreter root stack (#966) * macros: restore #[inline(never)] on the elidable and may-force surfaces Reverts eb59bf3. That commit removed #[inline(never)] from #[elidable], #[elidable_promote], the #[look_inside_iff] trampoline, #[jit_may_force] and #[loop_invariant] on the grounds that the upstream decorators set only their own flag (rlib/jit.py:72 sets _elidable_function_ and nothing else) and leave the C backend's inliner free, while _dont_inline_ (objectmodel.py:214, "not the JIT!") is a separate flag none of them carry. The flag reading is correct, but the inliner it leaves free is not LLVM's. auto_inlining stops a graph at weight >= threshold (backendopt/inline.py:654) with weight = 0.9999 * measure_median_execution_cost + static_instruction_count (:539 inlining_heuristic, hard reject at count >= 200) and roughly one weight per lltype op (:479 OP_WEIGHTS), so a straight-line graph is charged about twice its op count. The default DEFL_INLINE_THRESHOLD is 32.4, described at config/translationoption.py:11-12 as "just enough to inline add__Int_Int() and just small enough to prevent inlining of some rlist functions" -- on the order of sixteen operations. 54 of the 124 elidable sites are in descroperation.rs and 25 in longobject.rs; those graphs are far past that budget, so removing the attribute hands them to a cost model upstream's free inliner would not have applied. Nothing measured argues the other way. Throughput was flat (1.0070 / 1.0000 on the shadow-stack probes, 1.0000 on a pure integer control) and the bodies inlining into the eval loop cost frame size: reachable Python recursion depth fell 167700 to 159700 through the SP-based stack_check.rs current_sp(). eb59bf3's CI run also lost five macOS perf-ratio gates, which this commit originally claimed as evidence. That attribution was wrong: f947413 raised check.py's STARTUP_SAMPLES from 3 to 5 for exactly that failure mode -- a high startup sample collapsing the exec-time denominator of a short bench -- and macOS check.py is green on this branch with eb59bf3 still in it. The argument here rests on the inlining budget, not on those gates. #[dont_look_inside] keeps the removal from c29a31b: those bodies are inside the budget -- gc_roots::shadow_stack_len is one call, pyopcode::label_arg_to_usize one field read -- which is also why they fold under /OPT:ICF. Of the three folded pairs 10b45ac names, label_arg_to_usize / load_fast_var_num_to_index dissolves here because the latter is #[elidable_cannot_raise]; convert_value_arg / special_method_arg and hash_str_hooked_bytes / hash_str_hooked remain, so the last-write-wins insert stays load-bearing. Assisted-by: Claude * gc_roots: give the interpreter root stack a fixed base/top sized off the recursion limit `push_roots`/`pop_roots`'s thread-local shadow stack was a `Vec<PyObjectRef>`. RPython's is a raw buffer of `root_stack_depth` slots addressed by `root_stack_base` / `root_stack_top` (shadowstack.py:75-88), raw_malloc'd once by `_prepare_unused_stack` (:344-349) and replaced only by `increase_root_stack_depth` (:351-364). majit-gc's jitframe root stack already carries that shape, from the same 163840 default. Port the interpreter's stack to it: `RootStack{base, top, limit}` in `Cell`s, `incr_stack` returning the slot it claimed, `truncate` rewinding `top`, and `grow` copying the used portion into a larger raw allocation. Callers hold slot indices rather than slot addresses across a push, so a grow is transparent to them; `capture_shadow_stack_area` hands out the address of the `RootStack` cell and not of its buffer, and `walk_shadow_stack_cell` re-reads `base` every iteration, so a re-entrant push that grows cannot strand a walk in the freed buffer. The container is the point. A `Vec` puts drop glue on the thread-local, and a thread-local carrying a destructor pays a registration-state check on every access -- `pin_root` and `shadow_stack_get` sit on the interpreter's allocation and call paths. User CPU against this branch minus this commit, min of 15 interleaved runs: gc22_any.py .8888 gc22_all.py .8811 gc22_control.py .9722 list sort; roots too, so not a control gc36_pure_arith 1.0000 pure integer control The buffer is released at thread exit by a `RootStackOwner` in its own thread-local key, touched only from `grow`, so the key on the push path keeps no destructor of its own. sys.setrecursionlimit now sizes this stack the way pypy/module/sys/vm.py:97 sizes upstream's: `increase_root_stack_depth(int(new_limit * 0.001 * 163840))`. stack_check.rs already did that for the jitframe stack; both root stacks now follow the limit. `RootScope` keeps caching the resolved cell for the bracket's lifetime, as `*mut Vec<PyObjectRef>`. It names the cell and not its buffer, so a `grow` under an open bracket leaves it valid. Unlike upstream's unchecked `incr_stack`, the push tests `limit` and grows on reaching it. pyre pins one slot per call argument as well as per livevar, so a single variadic call can outrun the headroom `root_stack_depth` reserves per recursion level, and the native stack check upstream relies on would not fire first. Also corrects two comments this branch left stale: the `RootStack` rename kept naming `SHADOW_STACK` in five doc comments (one of them an intra-doc link that no longer resolves), and the elidable site total quoted in `majit-macros` was 124 where the expansion covers 126 non-test sites. Assisted-by: Claude * gc_roots: let shadow_stack_copy_range take an empty range at the top of the stack The `RootStack` conversion routed the copy's source through `slot(base)`, whose bounds check is `index < len`. The range check above it admits `base == len` when `dst` is empty, so an empty copy at the top of the stack passed the range check and then panicked inside `slot`. The `Vec` version this replaced took `&stack[base..base + dst.len()]`, and `&stack[len..len]` is an empty slice, not a panic. Return before the copy when there is nothing to copy. That also keeps `copy_nonoverlapping` off a null source on a thread whose root stack has never been allocated. A range starting past the live length still panics whether or not it is empty, as indexing did. No caller reaches this today: both `object_array.rs` sites guard on `len > 0` / `cap > 0`, and `call_builtin_code_many_from_roots` sizes its buffer `1 + nargs`. `pop_roots` is emitted even for zero livevars (`shadowstack.py:37-40`), so the empty shape is one a caller may take. Assisted-by: Claude * bench/synth: re-record closure_freevar_branch_resume's jit-stats baseline Its committed baseline is `loops_aborted=3 loops_compiled=6 guard_failures=23380 bridges_compiled=0`. It measures `0 / 3 / 606 / 3`, identically on dynasm and cranelift, and `loops_aborted=0` on wasm. check.py failed it on `loops_compiled 6 -> 3`, a `JITSTATS_FALL_FIELDS` gate. That fall is not a capability loss: `loops_compiled + bridges_compiled` is 6 on both sides, so the three units it stopped compiling as separate loops are the three it now compiles as bridges, while the aborts go to 0 and the guard failures fall 38x. Every gated field therefore moves the tightening way — `loops_aborted` and `guard_failures` down, and the fall gate now pinned at 3 rather than 6. Only `bridges_compiled` rises, and it is in no gated group. Six further fixtures had no baseline at all and were recorded here before the rebase; that half is dropped because #970 has since recorded all six on main. Where the two recordings disagreed — `raise_reg_unbound_jitstress`, `loops_aborted` 2 vs 10 and `loops_compiled` 5 vs 3 — main's is both the authoritative side and the tighter gate, and a build at this base reproduces main's numbers exactly. Four fixtures are still left red on purpose: `exception_args_virtual` (`loops_aborted` 0 -> 3), `exception_multi_handler_warmup` (0 -> 23), `exception_reraise_tb_depth_jitstress` (0 -> 1198, `loops_compiled` 805 -> 305) and `list_length_hint_validate` (14 -> 34). These raise a `JITSTATS_BADNESS_FIELDS` counter whose healthy value is 0, so recording them would switch the floor off for those fixtures permanently. They reproduce byte-identically on dynasm and cranelift, so the drift is walker-level, and they are unchanged across #967's IS_OP fold and #971's front-lowering folds. Assisted-by: Claude
|
Follow-up in #981 for the review findings here. Fixed, each verified against a CPython 3.14.2 oracle:
Not applicable, with the measurements:
New parity script — commented by Claude |
`builtin_function_qualname` derived a bound builtin method's qualname from `type(__self__)` unconditionally. A `classmethod_descriptor` binds the class itself, so once #970 made `dict.fromkeys` such a descriptor the receiver's type became `type` and the qualname read `type.fromkeys`; `int.from_bytes` and `float.__getformat__` were affected the same way. Take the descriptor's stamped qualname when the receiver is a type. That is the defining class `stamp_method_owners` recorded, which is also what `bool.from_bytes` reporting `int.from_bytes` needs. typedef::tests::builtin_static_and_class_methods_have_qualified_names: FAILED -> ok. Assisted-by: Claude
`builtin_function_qualname` derived a bound builtin method's qualname from `type(__self__)` unconditionally. A `classmethod_descriptor` binds the class itself, so once #970 made `dict.fromkeys` such a descriptor the receiver's type became `type` and the qualname read `type.fromkeys`; `int.from_bytes` and `float.__getformat__` were affected the same way. Take the descriptor's stamped qualname when the receiver is a type. That is the defining class `stamp_method_owners` recorded, which is also what `bool.from_bytes` reporting `int.from_bytes` needs. typedef::tests::builtin_static_and_class_methods_have_qualified_names: FAILED -> ok. Assisted-by: Claude
`builtin_function_qualname` derived a bound builtin method's qualname from `type(__self__)` unconditionally. A `classmethod_descriptor` binds the class itself, so once #970 made `dict.fromkeys` such a descriptor the receiver's type became `type` and the qualname read `type.fromkeys`; `int.from_bytes` and `float.__getformat__` were affected the same way. Take the descriptor's stamped qualname when the receiver is a type. That is the defining class `stamp_method_owners` recorded, which is also what `bool.from_bytes` reporting `int.from_bytes` needs. typedef::tests::builtin_static_and_class_methods_have_qualified_names: FAILED -> ok. Assisted-by: Claude
`builtin_function_qualname` derived a bound builtin method's qualname from `type(__self__)` unconditionally. A `classmethod_descriptor` binds the class itself, so once #970 made `dict.fromkeys` such a descriptor the receiver's type became `type` and the qualname read `type.fromkeys`; `int.from_bytes` and `float.__getformat__` were affected the same way. Take the descriptor's stamped qualname when the receiver is a type. That is the defining class `stamp_method_owners` recorded, which is also what `bool.from_bytes` reporting `int.from_bytes` needs. typedef::tests::builtin_static_and_class_methods_have_qualified_names: FAILED -> ok. Assisted-by: Claude
`builtin_function_qualname` derived a bound builtin method's qualname from `type(__self__)` unconditionally. A `classmethod_descriptor` binds the class itself, so once #970 made `dict.fromkeys` such a descriptor the receiver's type became `type` and the qualname read `type.fromkeys`; `int.from_bytes` and `float.__getformat__` were affected the same way. Take the descriptor's stamped qualname when the receiver is a type. That is the defining class `stamp_method_owners` recorded, which is also what `bool.from_bytes` reporting `int.from_bytes` needs. typedef::tests::builtin_static_and_class_methods_have_qualified_names: FAILED -> ok. Assisted-by: Claude
`builtin_function_qualname` derived a bound builtin method's qualname from `type(__self__)` unconditionally. A `classmethod_descriptor` binds the class itself, so once #970 made `dict.fromkeys` such a descriptor the receiver's type became `type` and the qualname read `type.fromkeys`; `int.from_bytes` and `float.__getformat__` were affected the same way. Take the descriptor's stamped qualname when the receiver is a type. That is the defining class `stamp_method_owners` recorded, which is also what `bool.from_bytes` reporting `int.from_bytes` needs. typedef::tests::builtin_static_and_class_methods_have_qualified_names: FAILED -> ok. Assisted-by: Claude
`builtin_function_qualname` derived a bound builtin method's qualname from `type(__self__)` unconditionally. A `classmethod_descriptor` binds the class itself, so once #970 made `dict.fromkeys` such a descriptor the receiver's type became `type` and the qualname read `type.fromkeys`; `int.from_bytes` and `float.__getformat__` were affected the same way. Take the descriptor's stamped qualname when the receiver is a type. That is the defining class `stamp_method_owners` recorded, which is also what `bool.from_bytes` reporting `int.from_bytes` needs. typedef::tests::builtin_static_and_class_methods_have_qualified_names: FAILED -> ok. Assisted-by: Claude
`builtin_function_qualname` derived a bound builtin method's qualname from `type(__self__)` unconditionally. A `classmethod_descriptor` binds the class itself, so once #970 made `dict.fromkeys` such a descriptor the receiver's type became `type` and the qualname read `type.fromkeys`; `int.from_bytes` and `float.__getformat__` were affected the same way. Take the descriptor's stamped qualname when the receiver is a type. That is the defining class `stamp_method_owners` recorded, which is also what `bool.from_bytes` reporting `int.from_bytes` needs. typedef::tests::builtin_static_and_class_methods_have_qualified_names: FAILED -> ok. Assisted-by: Claude
`builtin_function_qualname` derived a bound builtin method's qualname from `type(__self__)` unconditionally. A `classmethod_descriptor` binds the class itself, so once #970 made `dict.fromkeys` such a descriptor the receiver's type became `type` and the qualname read `type.fromkeys`; `int.from_bytes` and `float.__getformat__` were affected the same way. Take the descriptor's stamped qualname when the receiver is a type. That is the defining class `stamp_method_owners` recorded, which is also what `bool.from_bytes` reporting `int.from_bytes` needs. typedef::tests::builtin_static_and_class_methods_have_qualified_names: FAILED -> ok. Assisted-by: Claude
…950) * functools: port reduce and cmp_to_key accelerators * builtins: preserve ExceptionGroup sequence repr * builtins: release exhausted list reverse iterators * builtins: make UnicodeError formatting mutation-safe * builtins: validate SyntaxError details * builtins: implement sys excepthook aliases * builtins: preserve compiler SyntaxError locations * builtins: clamp SyntaxError display offsets * builtins: render SyntaxError file source ranges * builtins: format file encoding errors * builtins: expose missing frame line as None * builtins: support multidimensional memoryview subviews * builtins: qualify wrapped builtin methods * builtins: port __pypy__ newmemoryview * builtins: port __pypy__ bufferable * builtins: port unicodedata UCD type * builtins: complete identity_dict mapping API * builtins: align eval and AST compile contracts * builtins: preserve float NaN list ordering semantics * builtins: align type and exception descriptors * builtins: preserve exhausted list reverse iterators * sys: bind coroutine origin depth keyword * imp: align frozen and builtin module APIs * posix: bind stat follow_symlinks keyword * pickle: align PickleBuffer public type surface * builtins: align bytes and unpack iterator surfaces * memoryview: reject multidimensional count * builtins: add function attribute deleters and method dir forwarding function.__name__, __qualname__, __code__ and __dict__ gain deleters that raise the setter's TypeError instead of falling through to a generic delete. method.__class__ is rejected in object_delattr, and method.__dir__ forwards the underlying function's instance dict since the bound-method wrapper has no writable dict field of its own. func_set_code warns DeprecationWarning when the new code object changes the callable's kind (plain / generator / coroutine / async generator). Assisted-by: Claude * builtins, struct, __pypy__: apply cargo fmt Assisted-by: Claude * Revert "builtins: preserve float NaN list ordering semantics" This reverts commit c8ef282. The float arm bypassed eq_w's identity shortcut and asked compare(Eq) directly, so `[x] == [x]` for a single NaN object became False. pyre stores list floats unboxed, so w_list_getitem hands back a fresh box and the ptr::eq guard ahead of that arm never fires. That made `x is y` true while `[x] == [y]` was false, and disagreed with both oracles on the same-object case, which CPython 3.14 and PyPy both answer True. synth/list_nan_identity failed as "wrong output" on every backend and platform. The commit aimed to keep two distinct NaN objects unequal; with unboxed floats pyre has no such distinction to preserve. Assisted-by: Claude * builtins: raise NotImplementedError for memoryview sub-views again An integer index into a multidimensional view and a tuple key shorter than `ndim` returned a live sub-view; `memoryobject.py:326-346` raises `NotImplementedError` for both, as does CPython. Removes the now unreachable `BufferView::ViewNDOffset` variant and its GC tracing arm. Assisted-by: Claude * builtins: root __setstate__ entries and name the receiver's type `w_dict_items` returns raw pointers in an untraced Vec and `setattr` can run a Python-level `__setattr__` that reaches a collection safepoint, so the remaining pairs were read back after a move. Publishes the receiver and every entry through `pin_roots` first. `SyntaxError.__init__` and `SystemExit.__init__` are inherited, so their "takes no keyword arguments" message now reports the receiver's own type instead of the class the initializer is installed on. Assisted-by: Claude * _functools: publish operands with pin_roots and fix reduce's arity messages `pin_root` runs a forwarding query that can park behind another thread's collection, so a second operand still held in a Rust local (or read out of a payload through `&W_KeyWrapper`) could name a pre-move address. `reduce` also read its initial value after `iter()` had allocated. All three sites now publish every operand in one `pin_roots` call and read them back from their slots. More than three positional arguments reported "expected at least 2 arguments"; both arity errors now carry their own wording. Assisted-by: Claude * sys: bind one object to displayhook/excepthook and their originals Each name held a separately constructed function, so `sys.excepthook is sys.__excepthook__` was False. Stores one object under both names, as `breakpointhook` and `unraisablehook` already do. Assisted-by: Claude * _struct: install the unpack_iterator type entries once `make_unpack_iter` rewrote `__module__` and `__getattribute__` on the shared type dict on every `iter_unpack()` call, allocating a fresh string and function object each time and mutating a dict other threads read through attribute lookup. Moves the writes and the TypeDef flags behind a `Once`. Also compares the `identity_dict` key with `is`, since the snippet's two equal-but-distinct lists made the equality assertion pass either way. Assisted-by: Claude * descroperation: drop the float arm from tuple element comparison `compare_slot`'s tuple loop kept the float special case that the list side already reverted, so `(a,) == (b,)` was False for two `float("nan")` values while `a is b` and `[a] == [b]` were both True. Restores `tupleobject.py:137`'s plain `eq_w` element check. Assisted-by: Claude * baseobjspace, _functools: decline the slot fold under a custom __getattribute__ and batch reduce's sequence operand `exception_attr_slot_fold` only checked which class defines the name, so a receiver whose type overrides `__getattribute__` would still fold to a slot read. Applies the same `getattribute_if_not_from_object` decline `load_attr_fast_path` uses (mapdict.py:1497-1499). `reduce` pinned its function and initial value together but then read `positional[1]` out of the argument slice afterwards, past a `pin_roots` that can park behind another thread's collection. All three operands are now published in one batch and the sequence is read back from its slot. Assisted-by: Claude * builtins: root the exception reduce and group constructor operands Three sites held managed pointers in untraced Rust locals across calls that allocate or run Python: - `attribute_error_reduce` kept the fresh `w_exception_get_args` tuple in a local across `w_dict_new`. - `import_error_reduce` kept the same tuple across the instance dict's Python-level `copy()`. - `exception_group_new` kept the `w_str_new` of a custom sequence's `__repr__` across `fixedview`, which runs that sequence's `__len__` and `__getitem__`, and kept the `fixedview` result across the per-item `isinstance` walk. Each now publishes its operands with `pin_roots` and reads them back from their slots. Assisted-by: Claude * display: refetch the receiver between unicode error slot reads The three `UnicodeError` `__str__` formatters read `encoding`, `start`, `end` and `reason` off one `obj` local. `unicode_err_int_slot` walks `__index__` through `int_w` and `unicode_err_str_slot` calls `__str__`, so any of those reads can run Python and move the exception; only the final `object` read was refetched from the shadow stack. Each read now takes the receiver from its slot. Assisted-by: Claude * unicodedata: route ucd_3_2_0.name through the 3.2.0 database view `W_UCD::name` called the module-level `name`, which reads the latest bundled database, so `ucd_3_2_0.name` returned names for code points Unicode 3.2.0 leaves unassigned. Add `name_impl(db, args)`, gate the name lookup on the view's own general category being other than `Cn`, and select `LEGACY`/`MODERN` in `W_UCD::name` as the sibling methods do. The latest view is unaffected: no code point in it has category `Cn` and a name. Assisted-by: Claude * builtins: root SyntaxError.__init__ arguments across the details fixedview `fixedview` runs the details object's iteration protocol and can reach a collection safepoint. `call_builtin_code_positional` hands builtins a native slice copied off the shadow stack, and a collection does not rewrite it, so the receiver and the positional arguments read after the call named pre-move addresses. Publish them with `pin_roots` first and read each back from its slot, building the args list before reloading the receiver so the list allocation cannot invalidate it. Report the shorter-than-4 details tuple as `function takes at least 4 arguments (n given)`, matching the `at most 6` message in the same match. Assisted-by: Claude * __pypy__: report newmemoryview geometry errors with the reference wording The six `ValueError` diagnostics named neither the offending geometry nor the source view. Derive `lgt`/`old_size` from the source and spell each message as `interp_buffer.newmemoryview` does, so the shape, strides, itemsize and source length appear in the text. `lgt` comes from the byte extent rather than `len(w_obj)`: for an N-D source `shape[0]` covers only the outermost dimension, and the reference aborts on such a source before reaching these checks. The length-mismatch message names the parsed shape; the reference raises before filling its shape accumulator and always reports an empty list there. Assisted-by: Claude * extra_tests: exercise os.stat follow_symlinks against a real symlink The existing assertion compared `os.stat(".", follow_symlinks=False)` with `os.stat(".")`; `.` is not a symlink, so both describe the same file whether or not the flag is honored. Create a symlink in a temporary directory and check that the two calls disagree on `st_ino` and `st_size`, and that the no-follow result matches `os.lstat`. Assisted-by: Claude * call: identify the key wrapper by its type, not its type name `user_call_slot` admitted the `_functools` key wrapper past the generic-instance guard by comparing the type's name against the literal "functools.KeyWrapper". Use `W_KeyWrapper::from_obj`, which checks the registered type directly, so the branch no longer depends on the name string and costs a type check rather than a string compare. Assisted-by: Claude * importing: decode source through the BOM and the PEP 263 cookie `SourceProvider` handed back a `String`, so every provider decoded as plain UTF-8 and a file declaring another encoding was lost. A traceback into a `# -*- coding: latin-1 -*-` module printed no source line at all, and `load_source_module` failed the import outright. Make `read_to_bytes` the provider primitive and decode at the call sites: - `error.rs` renders the traceback line through `decode_source_bytes`, the rule `linecache`'s `tokenize.open` applies. - `load_source_module` decodes the same way; a bad declaration surfaces as the tokenizer's SyntaxError rather than an ImportError. - `pyrex` reads the script through the provider in every build, so the sandbox path decodes too and the two arms collapse into one. `read_source_to_string` stays for callers holding UTF-8 text rather than Python source. Assisted-by: Claude * extra_tests: skip the os.stat symlink checks on Windows `os.symlink` needs a privilege the runner cannot be assumed to grant, and pyre does not create the link there, so the added assertions failed on Windows while CPython passed. Guard the block on `os.name`; the `follow_symlinks` keyword checks above it still run everywhere. Assisted-by: Claude * jit, builtins: register unicodedata.UCD and __pypy__.Bufferable in build_gc Both are `#[pyre_class]` types allocated through `allocate_stable` with no inline object payload, so the header `w_class` is the only edge their marker forwards. Neither was wired into `build_gc`, which the GC-root registration completeness assertion rejects at init. Registered after `functools.KeyWrapper` at the append-only tail and given subclass-range ids 157 and 158, so no established AUTO-ID moves. Assisted-by: Claude * extra_tests: cover the producer-specific iterator identities `iter()` on bytes, bytearray, str (ASCII and non-ASCII) and memoryview reports `bytes_iterator`, `bytearray_iterator`, `str_ascii_iterator`, `str_iterator` and `memory_iterator`, each in `builtins`, and yields the same elements as the source. Assisted-by: Claude * exceptions: port Python 3.14 pickle state semantics * builtins: complete Python 3.14 compensated sum * numeric: honor subclass NotImplemented dispatch * unpack: report exact dict size on excess values * format: port Python 3.14 spec validation * weakref: complete proxy operation forwarding * weakref: preserve constructor keyword binding * weakref: preserve lifeline finalization ordering * generator: register finalizers only for protected yields * jit: preserve recursive closure operand stacks * builtins: preserve Python 3.14 NaN identity * builtins: bind object classmethod qualnames * builtins: use short names in setattr errors * builtins: attach set-name exception notes * builtins: validate structseq constructor fields * builtins: add structseq replace support * builtins: construct structseq classes as heap types * builtins: complete generic class dispatch * _structseq, call: apply rustfmt Assisted-by: Claude * builtins: validate duplicate integer grouping * stdlib: implement atexit callback shutdown * collections: preserve deque copy identities * tuple: preserve boxed float identities * generator: bind methods through the typedef * generator: chain caller exceptions on resume * call: apply rustfmt to the structseq keyword predicate Assisted-by: Claude * _functools: build the comparison operand before reading the result slot `w_int_new` can allocate for a custom numeric comparator result, so evaluating it after the shadow-stack read left the first argument naming a pre-move address. Assisted-by: Claude * baseobjspace: restore the bitwise float and complex is_w Pointer identity left `a is b` false while `function::immutable_unique_id` still derived the same `id()` from the bits, and made `[a][0] is a` false after FloatListStrategy unboxed and reboxed the element. Assisted-by: Claude * _structseq: batch-publish the constructor roots The forwarding query inside the first `pin_root` can park behind another thread's collection, leaving the remaining items and extras held only in the Rust vectors. Assisted-by: Claude * _functools: export the app-level cmp_to_key again The generated `W_KeyWrapper.__call__` is positional-only, so `key(obj=3)` raised and `inspect` reported `(obj, /)` instead of `(obj)`; both are required by test.test_functools, whose baseline main raised to PASS. Assisted-by: Claude * importing: root every bound module's dict, not just the named ones `walk_module_dicts_gc` and `walk_process_import_roots` both enumerated `SYS_MODULES.values()`. A builtin module is allocated immortal (`w_module_new_aliasing_dict` -> `malloc_typed`), so the collector never traces it and its `w_dict` was reachable only through those two walks. Binding a second module to a name therefore dropped the first module's dict out of the root set while Python still held the module: after `del sys.modules['_functools']`, a fresh `import _functools` and a `gc.collect()`, reading `_functools.__dict__` spun inside `w_module_dict_getitem_str` on freed storage. Add `MODULE_DICT_ROOTS`, an add-only set of every module `set_sys_module` has bound, and drive both walks from it through a shared `walk_bound_module_dicts`. test.test_functools: FAIL -> PASS. Assisted-by: Claude * __pypy__: align newmemoryview argument handling with interp_buffer.py `if w_shape` / `if w_strides` in `interp_buffer.py:newmemoryview` are RPython reference tests, so an empty sequence counts as supplied and only the default `None` selects the derive-it path. Key both on the argument object rather than on the parsed vector being empty, which makes `newmemoryview(mv, 1, 'B', ())` report `shape/itemsize []/1 does not match obj len/itemsize 12/1` instead of deriving a `(12,)` shape. Three further mismatches in the same function: - Strides without a shape now raise "strides must have a single value if shape not provided" rather than deriving a shape and reporting a shape/strides mismatch. - The supplied-shape/supplied-strides length mismatch raises before the reference fills its shape accumulator, so the diagnostic names `shape []`. - `lgt` is the source's own length and `nbytes` follows from it, so an N-D source reports its outermost dimension rather than its element count. Assisted-by: Claude * address review: excepthook None stream, unraisable tag read, dimensions rooting `sys_excepthook` collapsed an explicitly disabled `sys.stderr = None` into the same `wrote == false` branch as a missing `sys` or a failing lookup, so the host-seam fallback wrote the render to fd 2 for a stream the application had turned off. Return without printing for the `None` case, matching `_PyErr_Display`, and keep the seam for the lookup failures. `__pypy__.write_unraisable` handed its second argument to the unchecked `PyError::from_exc_object`, which reads the `kind` tag past the header of a `W_BaseException`; `write_unraisable(where, obj, x)` on a plain instance therefore read out of bounds and transmuted the byte into an `ExcKind`. Classify with `w_exception_kind_checked` first and fall back to the kind a bare `BaseException` maps to, keeping the object as `exc_object`. `interp_buffer::dimensions` converted the unpacked elements in place while holding them in a plain `Vec`; an element's `__index__` allocates and can relocate the ones not yet converted. Publish the batch and read each back through its root slot. Assisted-by: Claude * typedef: a class receiver names the defining class, not the metatype `builtin_function_qualname` derived a bound builtin method's qualname from `type(__self__)` unconditionally. A `classmethod_descriptor` binds the class itself, so once #970 made `dict.fromkeys` such a descriptor the receiver's type became `type` and the qualname read `type.fromkeys`; `int.from_bytes` and `float.__getformat__` were affected the same way. Take the descriptor's stamped qualname when the receiver is a type. That is the defining class `stamp_method_owners` recorded, which is also what `bool.from_bytes` reporting `int.from_bytes` needs. typedef::tests::builtin_static_and_class_methods_have_qualified_names: FAILED -> ok. Assisted-by: Claude * gc: make runtime code wrappers collectable * gc: root code wrappers retained by jitcodes * builtins: align class construction and comparison dispatch * type: expose Python 3.14 annotation descriptors * gc: re-mark ephemeron edges after finalizer resurrection `deal_with_objects_with_finalizers` marks a dying finalizer object's whole graph, which can mark owners the preceding `mark_ephemeron_values_to_fixed_point` had classified as dead. An address-keyed side table is not reached by that tracing, so `prune_ephemeron_tables` kept those entries while nothing marked their values, and the next major seeded a swept address as a root. Run the ephemeron fixed point again once the resurrection loop finishes. `seed_major_root` now validates the header type_id and takes the name of the walker that produced the root, so an invalid root fails at the walker rather than later in `mark_object`. `cpython_tests/run.py` appends the panic message line to the crash reason, as `check.py` already does. Assisted-by: Claude * exceptions: expose exception group members * exceptions: expose BaseException instance dict * exceptions: expose SyntaxError private metadata * exceptions: make ExceptionGroup weakrefable * exceptions: store StopIteration return value * exceptions: enforce suppress context member semantics * descr: add the w_value and w_weakreflifeline exception fields `W_BASE_EXCEPTION_GC_PTR_OFFSETS` gained the StopIteration `w_value` and the `ExceptionGroup` weakref lifeline slots, but `build_w_exception_group` still listed 20 fields, so `clear_gc_fields` left both uninitialized on a JIT-inlined exception allocation. `exception_size_descr_clears_every_runtime_traced_gc_field` covers this. Assisted-by: Claude * address review: ephemeron fixed point, callee shadow owner, __code__ rooting `mark_ephemeron_values_to_fixed_point` compared `incr_state.objects_marked` across a pass, but only `incremental_mark_step` ever increments it, so the loop always ran exactly once. Count what the pass itself drained: a second owner made live by the first owner's lifeline now gets its own value marked before `prune_ephemeron_tables` keeps its entry. `reconcile_vstack_at_boundary` chose the callee-local shadow on `fbw_callee_vstack_enabled()`, while a recursive closure is admitted with that env gate off. Such a mirror filled its holes from the outer portal virtualizable, which can record a caller operand as the callee's kept-stack value. Key the choice on `inline_subwalk`, the admission predicate. `fset_func_code` called `warn_deprecation` and `function_get_doc` — both allocate — and then stored through the raw `obj` / `w_code` locals. Publish both and read them back from their root slots. Assisted-by: Claude * exceptions: expose AttributeError members * exceptions: expose NameError name member * exceptions: expose ImportError members * exceptions: expose OSError members * exceptions: expose SystemExit code member * exceptions: port SyntaxError member slots * exceptions: expose Unicode error members * exceptions: document exception group members * descriptors: expose class wrapper members * descriptors: expose property members * check.py: record the shutdown atexit trace abort in the jit-stats baselines `run_atexit_callbacks` imports the `atexit` module at interpreter shutdown, so `app_atexit.py`'s module body executes there. The seven synthetic fixtures that call `pypyjit.set_param("function_threshold=1")` therefore trace that body: it reads a module global, which installs the module dict's `version?` quasi-immutable watcher, and then writes one, which `opimpl_jit_force_quasi_immutable` answers by forcing the field and abandoning the trace. That is one additional `loops_aborted` per process, measured identically on all six fixtures whose regression floor it crosses. Assisted-by: Claude * descriptors: expose range members * descriptors: expose slice members * descriptors: expose super members * descriptors: expose type flags and base members * type: expose CPython 3.14 ABI members * type: complete CPython 3.14 flags * gc: walk the function a staticmethod/classmethod wraps A builtin type dict binds the `staticmethod` / `classmethod` wrapper, so `walk_raw_function_roots` applied to the dict value stopped at the wrapper and never reached `w_function`. That function is Box-immortal, so nothing else traces it, while the `w_qualname` / `w_objclass` stamped onto it by the TypeCache sweep and its lazily allocated `w_func_dict` are young. Add `walk_raw_wrapped_function_roots` and call it from the three root walks that already descend through immortal wrappers. Assisted-by: Claude * extra_tests: drop two assertions CPython 3.14 does not hold `float("0.125") is float("0.125")` is False on CPython and True on pyre and PyPy, where `is` between plain floats compares `floatval`; give the pair that must stay distinguishable different values instead. `generator.__next__` is a `method-wrapper` on CPython, which has no `__func__`; assert `__self__` instead. Assisted-by: Claude * type: expose descriptor protocol surface * type: add type.__sizeof__ `w_type_get_abi_sizeof` reports `PyTypeObject` for a static type and `PyHeapTypeObject` plus the cached-keys block for a heap type with a dict; `init_type_type` binds it as `type.__sizeof__` with a text signature. Assisted-by: Claude * exceptions: fold slot reads past the typedef member descriptors `exception_attr_slot_fold`'s shadow gate accepted only the canonical getset, so moving `errno` / `strerror` / `filename` / `filename2` (and the SystemExit, AttributeError, NameError, ImportError and UnicodeError fields) to member descriptors made every such read decline the fold and residualize. `is_exception_typedef_member` matches the descriptor's direct-member kind against the selected slot; `direct_member_get` reads the same slot accessor and diverges only when the slot is NULL, which the fold already declines. Assisted-by: Claude * exceptions: add Python 3.14 builtin docs * builtins, __pypy__: read __dict__ through the attribute protocol in vars; keep newmemoryview geometry omission distinct from None vars() dropped its is_instance/is_type/is_function/is_module whitelist and now reads obj.__dict__ through getattr, mapping only AttributeError to the TypeError; exceptions answer instead of being rejected. newmemoryview() tracks whether shape/strides were supplied from the parsed argument rather than testing the resulting value for None, so an explicit None reaches dimensions() and raises "'NoneType' object is not iterable". Assisted-by: Claude * exceptions: add BaseExceptionGroup initializer * exceptions: move the exception group message and exceptions onto W_BaseException slots BaseExceptionGroup stored its message, its flattened exceptions tuple and the constructor-time repr of the supplied sequence under __pyre_* keys in the instance dictionary, so eg.__dict__ listed all three and a write to those keys changed what `message`, `exceptions` and `repr` answered. They become three traced W_BaseException fields with the write barrier the other exception slots use, registered in the per-kind descr group and in W_BASE_EXCEPTION_GC_PTR_OFFSETS. Assisted-by: Claude * exceptions: correct the exception group repr fallback comment The comment attributed the constructor-time spelling to CPython 3.14, which re-renders args[1] live; the behaviour matches PyPy. Restate what the branch does and against which reference. Assisted-by: Claude * jit: finish materialized frames on exit * builtins: root the memoryview lookup operands and store StopIteration.value before delegating memoryview.count kept the iterator, the view and the sought value in raw locals across the element comparison, and memoryview.index kept the view and the value across it; the comparison runs a user __eq__, so a collection triggered from there left the next iteration dereferencing vacated memory. Both now pin the operands and reload them from their shadow-stack slots, reusing one slot for the element under test. W_StopIteration.descr_init assigns w_value before running W_Exception.descr_init; the initializer allocates the replacement args list, so the previous ordering also read w_self and w_value after a collection point. Assisted-by: Claude * check.py: re-record the dynasm jit-stats floor for trace_too_long_inline_multiframe Measured on this branch with freshly extracted LLBC: loops_compiled 0 -> 2 and loops_aborted 23 -> 24. The bench sits at the trace-length boundary, and the branch's added exception surface moves it: the tracer now compiles two loops it previously never compiled and aborts one more attempt. cranelift and wasm are unchanged and keep their existing floors. Assisted-by: Claude * gc: publish multi-operand root sets in one write phase `pin_root` resolves forwarding, and that resolve is a safepoint, so a sequence of `pin_root` calls reads every operand behind the first one after a foreign collection could already have moved it. Replace those sequences with a single `pin_roots`, or with paired `publish_roots` + `normalize_roots` where the operands arrive in two slices: - `DictOperationGuard::new` pinned the dictionary, then the caller's `refs` slice, so every `w_dict_setitem*` / lookup operand crossed the dictionary's own pin. - `newmemoryview` pinned its five arguments in a loop. - `W_PickleBuffer::__new__` pinned the acquired buffer, then the exporter. - `memoryview` slice-view, `__eq__`, `count` and `index` pinned the view, then the second operand. `AttributeError.__getstate__` now pins and reloads the `name` slot before handing it to `w_dict_setitem_str`, matching the `args` insertion below. Assisted-by: Claude * type: declare ABI layout members beyond the builtins namespace `cpython314_builtin_abi` only covered the `builtins` namespace, so every other runtime type fell back to a bare object header: `function`, `module`, `code`, `frame`, `generator`, `coroutine`, `async_generator`, `traceback`, `cell`, `mappingproxy`, `method`, `builtin_function_or_method`, `types.SimpleNamespace`, `types.GenericAlias`, `typing.Union`, the six descriptor kinds, the iterator kinds and the dict views all reported `__basicsize__` 16 with zero item/dict/weakref offsets. `ImportWarning` and `SyntaxWarning` were missing from the exception group and hit the same fallback. Structseq classes stay on the fallback: their header follows the field count, so it belongs to the structseq constructor rather than to a per-name table. Measured against CPython 3.14 on 64-bit: 31 of the 33 diverging types now match, the two remaining being the structseq `flags` and `version_info`. `type_members_python314.py` asserts the added layouts. Assisted-by: Claude * check.py: carry the trace_too_long_inline_multiframe abort floor to cranelift The dynasm floor for this bench was re-recorded at 24 aborts; cranelift kept 23 and now reports the same rise, so the gate stayed red on that backend alone. wasm still measures 23 and keeps its floor. `loops_aborted` is the only field CI reported as diverging, so it is the only one moved here; `loops_compiled` stays at its recorded 0 rather than taking a value this change did not measure. Assisted-by: Claude * jit: clear caught residual exceptions on normal finish * compile: raise SyntaxError for source a declared codec cannot decode `decode_source_bytes` converted only LookupError, so a file declaring a real codec whose bytes that codec rejects — `# coding: ascii` followed by `0xff` — propagated the decoder's UnicodeDecodeError out of the import. `pyparse.py:130-139 _handle_encoding` turns both failures into the parser's SyntaxError and lets anything else through; do the same, with the decoder's own text as the message. Both now carry the file they came from, so `str()` renders the `(name, line 0)` suffix instead of a bare message, and the unknown-codec message no longer embeds the path. `syntax_error_located` gained `error.py:4-7 wrap_pos`: a zero offset, end_lineno or end_offset reads back as `None` rather than as `0`. `lineno` is exempt, matching the reference. Assisted-by: Claude * exceptions: exclude stateful kinds from the traced args-only constructor The traced inline constructor rebuilds an exception from `kind` / `w_class` / `args_w`, and `has_trivial_args_constructor` named only the OSError and Unicode-error kinds as unsafe for that. Every kind whose `descr_init` writes flattened fields is equally unsafe, and this branch gave several of them dedicated slots: a hot `SyntaxError("m", ("f.py", 1, 2, "src"))` came back with `msg` / `filename` / `lineno` / `offset` / `text` all `None`, and a hot `StopIteration(n)` lost its independent `value` once `args` was reassigned. Add `SyntaxError`, `StopIteration`, `AttributeError`, `NameError`, `SystemExit`, `ImportError` and `ModuleNotFoundError`. The subclasses that inherit one of these initializers share the parent's kind, so `UnboundLocalError`, `IndentationError` and `TabError` are covered. `exception_stateful_init_jit.py` builds each one under a threshold-1 JIT and reads a field `args_w` cannot carry. `check.py --backend dynasm --synthetic-only` is 355 passed with no jit-stats movement. Assisted-by: Claude * type, unicodedata: root the abstractmethods operands, close the UCD constructor `__abstractmethods__`'s setter truth-tested the value, then stored the receiver and the value from the gateway's native slice. `is_true` runs the value's own `__bool__`, which is a collection point, and no collector rewrites that slice, so the class dictionary could receive a pre-move address. Publish both operands before the test and reload them after. `abstract_instantiation_error` skipped a `__abstractmethods__` that was not a set, reporting an empty method list. `objectobject.py:17-21 _abstract_method_error` reaches the attribute through `sorted(...)`, so a non-container reports its own TypeError; iterate it the same way and let that error through. `unicodedata.UCD` was constructible: `interp_ucd.py:311 UCD.typedef` declares no `__new__`, so generic allocation handed back a `UCD` with no database. Mark it non-instantiable, as every sibling interpreter-owned type already is. Assisted-by: Claude * gc: report both generations in the invalid-child panic `grey_child`'s panic named neither end's generation, so an old-gen holder naming a nursery child (a store that never reached the remembered set) and an old-gen child (a sweep that reclaimed a still-referenced object) read identically. Add `child_gen` / `holder_gen`. Assisted-by: Claude * gc: log old-gen allocations under the lifetime knob `MAJIT_GC_LIFETIME_LOG` recorded remembered-set adds and old-gen frees but not births, so a dangling reference could not be split into "the referrer outlived its target" and "the referrer was born holding a dead address". Log both the born-old and the promotion allocation. Assisted-by: Claude * _structseq: root the structseq heap types for the process lifetime `make_heap_structseq_type` builds the type with `w_type_new`, which is the mortal GC-managed constructor, while every caller caches its raw address in a `OnceLock<usize>` the collector neither traces nor rewrites. Once nothing else referenced the type the sweep reclaimed it and the cache kept handing the address out, so each later instance was built with a `w_class` naming freed memory; a major collection then greyed that address out of `tuple_object_custom_trace` and panicked. Register the type in a stable slot with `try_gc_add_root`, as `intern_str` does for a managed dynamic string. Assisted-by: Claude * typedef: store subclass tags through the write barrier The seven `w_class` retag sites wrote the slot directly. The instances come from a stable-address allocator, so they are born old — and born black while a major cycle is marking, which leaves nothing to rescan them; only the barrier's remembered-set entry brings a modified black object back to gray (`incminimark.py:1792-1799`). `pyre_class`'s `__new__` already carries the barrier for the same store. Route all seven through one helper. This is not the producer of the `tuple_object_custom_trace` panic — that one was the unrooted structseq type cache. Assisted-by: Claude * exceptions: source the implicit context per resume kind, record it once `handle_exception` derived every implicit `__context__` from `sys_exc_info()`, and re-derived it in each frame the exception unwound through. An exception thrown into a resumed generator now reads the flat EC slot instead of walking the generator chain: `push_gen_or_coroutine` has already swapped the generator's own handled exception into that slot, and the caller's belongs to the caller, which delivered the exception rather than raising it under a live handler. An exception a frame raised itself records its context only where it first surfaces, witnessed by `w_exception_get_traceback` still being null there -- `record_application_traceback` stamps this frame's node further down the same function. A thrown-in exception is exempt from the witness: it starts a new error over an object that already carries a traceback. `test.test_asyncio.test_timeouts` goes 33/34 -> 34/34 with the JIT on and off. Assisted-by: Claude
Follow-up work on top of #845.
jit: exempt the load-global namespace operand from the NULL-Ref walk abortpickle_terminal_raise_resumeproduced wrong code. The residual-call walkaborted on a NULL
Refoperand that aLOAD_GLOBALnamespace slot legitimatelycarries.
typedef: addmethod-wrapperandclassmethod_descriptortypesmethod_descriptor_kind.pyreported(list, '__len__', method_descriptor)where the descriptor is a
wrapper_descriptor. Gating the second retag sweep ongateway::is_slot_wrapperneeds a bound form for the retaggedwrapper_descriptor, so both missing types are added:method-wrapperMethoda bound method descriptor usesw_method_set_public_classinfunction::method_wrapper_newclassmethod_descriptorClassMethoda@classmethodusesw_classwrite infunction::classmethod_retag_descriptorNeither writes
ob_type, sopy_type_checkpredicates (is_method,is_classmethod) and every binding path are unaffected — onlytypedef::r#type,which prefers
w_class, sees the new type.classmethod_descr_getpicksbuiltin_bound_method_newfor aclassmethod_descriptorandw_method_newotherwise, sodict.fromkeysis abuiltin_function_or_methodwhile a user@classmethodbinds tomethod.is_bound_builtin_methodwidens from "wraps amethod_descriptor" to"published as
builtin_function_or_method" — the classmethod case wraps aplain builtin, so the old test rejected it and
repr(dict.fromkeys)raised.wrapper_descriptorandmethod_descriptorget__reduce__(
getattr(objclass, name));copyreg._reduce_exrefuses them otherwise.classmethod_descriptordeliberately gets none — pickling one raises"cannot pickle 'classmethod_descriptor' object".
builtins::is_native_exception_dunderguarded onis_function, which excludesSLOT_WRAPPER_TYPE; once__str__/__repr__retag, the anti-recursion guardstopped firing and
str(SyntaxError("bad"))overflowed the native stack. Itnow guards on
is_function_carrier.The
classmethod_descriptorretag is two-phase:init_typeobjectsruns thesweeps inside
TYPEOBJECT_CACHE.get_or_init, wheregettypeobjectreturnsnothing, so the type is registered with
set_instantiateat creation, the retagreads
get_instantiateand no-ops on null, and an idempotent closing pass overreg.values()covers the namespaces built earlier (object,type,int,dict,mappingproxy).lib-python/3/inspect.pyreturns to pristine CPython 3.14 forismethod,isbuiltin,ismethodwrapper,ismethoddescriptor,_NonUserDefinedCallablesand
_signature_get_user_defined_method._slot_wrapper_typeand the_builtin_code_typeblock are deleted —dict.update.__code__stops resolvingafter the split, so the
isfunctionoverride it fed was already dead.bench: record the seven missing jit-stats baselines21 files for hot_loop_exit_then_class_stmt, inline_freevar_after_mayforce,
math_log_trig_hot, pickle_terminal_raise_resume, pypy_dict_primitives_nonbinding,
raise_reg_unbound_jitstress and tuple_unpack_array_backed_hot on all three
backends. All seven were inherited without a baseline.
Two nonzero badness values are pinned as-is:
raise_reg_unbound_jitstressloops_aborted=2on every backend, andpickle_terminal_raise_resumeloops_aborted=54on wasm against 0 on both native backends — unexplained, andthe baseline only pins it against a rise.
bench: re-record the four caught-in-callee jit-stats baselinesfloat_div_zero_caught_loop, mutate_then_raise_caught,
inline_subwalk_property_mutates and inline_subwalk_mutating_residual. The cause
is the
try_walker_inline_resolved_user_calldecline for a branchy callee withits own exception handler when
fbw_callee_body_replay_safetyis notClean—reverting that hunk alone puts all four back on the old numbers exactly.
The re-record raises no gated badness field:
guard_failuresbridges_compiledloops_abortedloops_compiledGates
pyre/check.py --backend dynasm: 5 failed, 361 passed (16 failed, 350 passedbefore this branch).
The five are left red on purpose — closure_freevar_branch_resume,
exception_args_virtual, exception_multi_handler_warmup,
exception_reraise_tb_depth_jitstress and list_length_hint_validate raise
loops_abortedand lowerloops_compiledwith no attributed cause, andre-recording them would disarm the floor.
pyre/check.py --backend cranelift: 7 failed, 359 passed — the same five plustwo perf-ratio rows that are the pypy-startup clamp, not a regression:
0.66/131.2 = 0.00503and0.12/24.8 = 0.00484are bothEXEC_TIME_FLOOR_S, sothe pypy denominator was clamped and the ratio is
pyre_exec / 5ms. The isolatedpyre times are higher while the ratios fall.
extra_tests/parity_tests/run.py: green on cpython, dynasm and cranelift.cpython_tests/run.py --backend dynasm: 46 pass, no regressions.test_inspect: 362/362.Descriptor kinds and bound/unbound type pairs were diffed against a CPython
3.14 oracle for
object.__init_subclass__,object.__subclasshook__,type.__prepare__,int.from_bytes,dict.fromkeys,dict.__class_getitem__,float.fromhex,bytes.fromhexandset.__class_getitem__.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests