Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions majit/majit-metainterp/src/virtualref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,12 @@ pub use crate::jit::InvalidVirtualRef;
/// so the address is stable across a minor collection. The `forced` frame may
/// be young, hence the creation write barrier.
///
/// The `Box` fallback covers the window before `set_vref_gc_type_id` has run
/// (the id still reads its unset sentinel) and an old-gen allocation failure;
/// it is leaked, and reclamation is what it gives up.
/// The `Box` fallback covers only the window before `set_vref_gc_type_id` has
/// run (the id still reads its unset sentinel), where there is no registered
/// type to allocate and nothing has handed a vref to the collector yet; it is
/// leaked, and reclamation is what it gives up. Once the id is set the
/// allocation stays collector-owned or fails loudly — a host box past that
/// point would silently drop the `forced` edge this object exists to hold.
fn alloc_virtual_ref(real_object: *mut u8) -> *mut u8 {
let vref = JitVirtualRef {
super_: ObjectHeader {
Expand All @@ -186,12 +189,14 @@ fn alloc_virtual_ref(real_object: *mut u8) -> *mut u8 {
let type_id = vref_gc_type_id();
if type_id != VREF_GC_TYPE_ID_UNSET {
let gcref = majit_gc::alloc_oldgen_typed(type_id, std::mem::size_of::<JitVirtualRef>());
if gcref.0 != 0 {
unsafe { std::ptr::write(gcref.0 as *mut JitVirtualRef, vref) };
// Creation write barrier: an old-gen vref may point at a young frame.
majit_gc::gc_write_barrier(gcref);
return gcref.0 as *mut u8;
}
assert!(
gcref.0 != 0,
"JitVirtualRef old-gen allocation failed after the type was registered",
);
unsafe { std::ptr::write(gcref.0 as *mut JitVirtualRef, vref) };
// Creation write barrier: an old-gen vref may point at a young frame.
majit_gc::gc_write_barrier(gcref);
return gcref.0 as *mut u8;
}
Box::into_raw(Box::new(vref)) as *mut u8
}
Expand Down
50 changes: 50 additions & 0 deletions pyre/bench/synth/type_metatype_method_call.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# A metaclass resolves `Cls.name()` before the class's own MRO does:
# `type.__getattribute__` lets a metatype DATA descriptor win outright, and a
# metatype `__getattribute__` override produces the value itself. Either way the
# call must use what the metaclass returned, not rebind the class onto it.


class MetaProp(type):
@property
def where(cls):
return lambda: 'meta-prop'


class ByProp(metaclass=MetaProp):
@classmethod
def where(cls):
return 'own-classmethod'


class MetaGetattr(type):
def __getattribute__(cls, name):
if name == 'ping':
return lambda: 'meta-getattr'
return type.__getattribute__(cls, name)


class ByGetattr(metaclass=MetaGetattr):
@classmethod
def ping(cls):
return 'own-classmethod'


class Plain:
@classmethod
def tag(cls):
return cls.__name__


def main():
prop = getattr_ = plain = None
for _ in range(20000):
prop = ByProp.where()
getattr_ = ByGetattr.ping()
# an ordinary class still binds its classmethod's cls
plain = Plain.tag()
print('prop', prop)
print('getattr', getattr_)
print('plain', plain)


main()
27 changes: 18 additions & 9 deletions pyre/pyre-interpreter/src/baseobjspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8905,11 +8905,12 @@ pub unsafe fn load_method_fast_path(
/// same shape a plain instance method takes, with the class in the receiver
/// slot instead of an instance.
///
/// `is_type` is exact-metatype, so a custom metaclass declines (its
/// `__getattribute__` may not follow `type.__getattribute__`). A name the
/// metatype itself defines is declined too: a metatype attribute would shadow
/// the class attribute (data descriptor) or be returned in its place. An
/// uncacheable type and any non-`classmethod` descriptor also decline.
/// The metatype is read off the class (`getclass()`) and must be `type`
/// itself, so a custom metaclass declines (its `__getattribute__` may not
/// follow `type.__getattribute__`). A name the metatype itself defines is
/// declined too: a metatype attribute would shadow the class attribute (data
/// descriptor) or be returned in its place. An uncacheable type and any
/// non-`classmethod` descriptor also decline.
///
/// # Safety
/// `w_obj` must be a valid object pointer (null tolerated).
Expand All @@ -8921,10 +8922,18 @@ pub unsafe fn classmethod_on_type_fast_path(
return None;
}
let w_type = w_obj;
// `is_type` pins the metaclass to exactly `type`, so `type.__getattribute__`
// is the resolution path. A metatype attribute of the same name would win
// over the class attribute, so decline any name the metatype defines.
let metatype = &pyre_object::pyobject::TYPE_TYPE as *const _ as PyObjectRef;
// `is_type` answers for the object's physical layout — every type object
// carries the same `ob_type` — so it says nothing about the metaclass.
// `getclass()` (baseobjspace.py) reads the metaclass off `w_class`; only
// `type` itself resolves the name through `type.__getattribute__`, so any
// other metaclass declines rather than have its `__getattribute__`
// override bypassed.
let metatype = crate::typedef::r#type(w_obj)?.as_ptr();
if !std::ptr::eq(metatype, crate::typedef::w_type()) {
return None;
}
// A metatype attribute of the same name would win over the class attribute,
// so decline any name the metatype defines.
if lookup_in_type(metatype, name).is_some() {
return None;
}
Expand Down
14 changes: 14 additions & 0 deletions pyre/pyre-interpreter/src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2979,6 +2979,20 @@ pub fn compute_load_method_bound(obj: PyObjectRef, attr: PyObjectRef, name: &str
// METAclass MRO (`space.type(w_obj)`), so a name found in the
// type's own MRO reaches the call as a plain getattr value
// with no binding.
//
// `is_type` reports the physical layout every type object shares,
// not the metaclass, so read the metaclass and require it to be
// `type`. The shape inferred below is what
// `type.__getattribute__` produces; a custom metaclass can
// override `__getattribute__` or define a data descriptor of the
// same name, and either one produced `attr` in place of the
// class's own MRO entry — binding `cls` onto that value would
// pass the class to something that never asked for it.
let metatype_is_type = crate::typedef::r#type(obj)
.is_some_and(|meta| std::ptr::eq(meta.as_ptr(), crate::typedef::w_type()));
if !metatype_is_type {
return PY_NULL;
}
Comment on lines +2982 to +2995

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicate exact-metaclass check across two files. Both sites independently implement the same "receiver's actual metaclass is exactly type" predicate (crate::typedef::r#type(obj) followed by std::ptr::eq(..., crate::typedef::w_type())). This predicate is the core correctness fix for this PR; keeping it in two places risks future divergence.

  • pyre/pyre-interpreter/src/eval.rs#L2982-L2995: extract this check into a shared helper (for example crate::typedef::has_exact_type_metaclass) and call it here.
  • pyre/pyre-interpreter/src/baseobjspace.rs#L8925-L8936: call the same shared helper here instead of reimplementing the pointer-identity check.
📍 Affects 2 files
  • pyre/pyre-interpreter/src/eval.rs#L2982-L2995 (this comment)
  • pyre/pyre-interpreter/src/baseobjspace.rs#L8925-L8936
🤖 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/eval.rs` around lines 2982 - 2995, Extract the
exact-metaclass predicate into a shared helper such as has_exact_type_metaclass
in pyre/pyre-interpreter/src/eval.rs#L2982-L2995, preserving the existing
pointer-identity behavior, and call that helper from the local check. Update
pyre/pyre-interpreter/src/baseobjspace.rs#L8925-L8936 to use the same helper
instead of duplicating crate::typedef::r#type and std::ptr::eq logic.

let raw = crate::baseobjspace::lookup_in_type(obj, name);
match raw {
Some(d) if pyre_object::is_classmethod(d) => obj,
Expand Down
12 changes: 12 additions & 0 deletions pyre/pyre-jit-trace/src/descr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2886,6 +2886,18 @@ pub fn pytraceback_w_next_descr() -> DescrRef {
field_descr_from_group(&PYTRACEBACK_DESCR_GROUP, index)
}

/// Field descriptor for `PyTraceback.lineno`, the line `descr_get_tb_lineno`
/// reports. Located by offset for the same reason as
/// [`pytraceback_w_next_descr`].
pub fn pytraceback_lineno_descr() -> DescrRef {
let index = PYTRACEBACK_DESCR_GROUP
.field_descrs
.iter()
.position(|d| d.offset() == pyre_interpreter::pytraceback::PYTRACEBACK_LINENO_OFFSET)
.expect("PyTraceback descr group has no lineno field");
field_descr_from_group(&PYTRACEBACK_DESCR_GROUP, index)
}

/// Cached field descriptor for a raw reference slot selected by the
/// exception attribute fold. Indices are those of `build_w_exception_group`;
/// no parallel descriptor is constructed.
Expand Down
17 changes: 17 additions & 0 deletions pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2148,6 +2148,21 @@ pub(crate) fn fbw_callee_body_replay_safety(
// [`fbw_store_journal_rollback`] replays the journal in reverse
// on a non-committed exit — so a folded store is undone for the
// replay, and an unfolded one never runs.
// `binary_op` / `compare_op` over operands this scan could not
// prove exact-numeric is the same shape one more time: which
// `__add__` / `__lt__` runs is a property of the operand's
// runtime class, not of this body. The proven-operand case was
// already accepted above; what is left here is exactly the
// operand whose provenance the scan lost — most commonly a
// `LOAD_ATTR` result, since that arm is itself deferred and
// clears numeric provenance. Deferring instead of declining is
// what lets `self.v + i` inline: at trace time the attribute
// read folds to a mapdict slot with a concrete int shadow, so
// the walker's numeric specialization erases the residual before
// the backstop is reached. An operand pair that stays opaque
// leaves the residual standing, and it reaches
// `fbw_abort_nested_unjournaled_residual` like any other — the
// helper never runs.
if matches!(
ei.pyre_helper,
majit_ir::PyreHelperKind::CallFn
Expand All @@ -2156,6 +2171,8 @@ pub(crate) fn fbw_callee_body_replay_safety(
| majit_ir::PyreHelperKind::RaiseVarargs
| majit_ir::PyreHelperKind::SetCurrentException
| majit_ir::PyreHelperKind::LoadAttr
| majit_ir::PyreHelperKind::BinaryOp
| majit_ir::PyreHelperKind::CompareOp
) {
deferred_call = true;
// The callee this resolves to is a runtime value, so what it
Expand Down
39 changes: 25 additions & 14 deletions pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2681,7 +2681,17 @@ pub(crate) fn try_walker_inline_resolved_user_call<Sym: WalkSym>(
let legacy_admit = match safety {
CalleeReplaySafety::Clean => true,
CalleeReplaySafety::DeferredCall => {
foriter_deferred_admit = !fbw_foriter_deferred_call_denied(callee_code_key);
// The deferred promise rests on the abort REWINDING to the
// enclosing CALL and re-executing it from scratch. A binop
// dunder dispatch (the only entry carrying an
// `arg_class_guard`) reaches this lever from a `BINARY_OP`
// instead, and that opcode is not a call boundary the rewind
// can name: the flush resumes one operand short and the whole
// iteration's contribution is dropped, silently. A `Clean`
// body is still admitted from there — it has nothing that can
// abort.
foriter_deferred_admit =
arg_class_guard.is_none() && !fbw_foriter_deferred_call_denied(callee_code_key);
Comment on lines +2684 to +2694

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs --items all

rg -n -C 8 --type rust \
  'FBW_FORITER_DEFERRED_DENY|fbw_foriter_deferred_call_denied|foriter_deferred.*den' \
  pyre

Repository: youknowone/pyre

Length of output: 13081


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re
from pathlib import Path

fbw = Path('pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs').read_text()
inline = Path('pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs').read_text()

needles = {
    'FBW_FORITER_DEFERRED_DENY': None,
    'FBW_FORITER_DEFERRED_INLINE': None,
    'FBW_HAZARDOUS_INLINE_DENY': None,
}
for m in re.finditer(r'(?s)\s*const\s+(\w+)_STORAGE', fbw):
    for var_name in needles:
        if var_name in fbw[m.end():]:
            needles[var_name] = (
                m.start(),
                m.group(1),
                re.search(r'(?s)\b' + var_name + r'\s*=', fbw[m.end():]).group(0)
            )
            break

print('TLS/storage declarations:')
for name, result in needles.items():
    if result:
        start, storage_prefix, ref = result
        # Print the containing const storage declaration if the variable belongs to it
        prev = fbw[:start].rfind('pub(crate) const ')
        decl = fbw[prev:start+ref.find(name)+1+len(name)]
        print(f'--- {name}')
        print(decl)
    else:
        print(f'--- {name}: not found')

print('\ndeferrals functions context:')
for fn in [
    'fbw_foriter_deferred_call_denied',
    'fbw_foriter_deny_deferred_call',
    'fbw_foriter_deferred_inline_outermost',
]:
    i = fbw.find(f'fn {fn}')
    if i != -1:
        print(f'--- {fn}')
        print(fbw[i:i+fbw.find('\npub(crate) fn', i+14)-i] if fbw.find('\npub(crate) fn', i+14) != -1 else fbw[i:i+500])

print('\ninline usage context:')
i = max(inline.find('fbw_foriter_deferred_call_denied'), inline.find('ForiterDeferredInlineGuard'), inline.find('CalleeReplaySafety'))
print(inline[max(0,i-800):i+900])

checks = [
    ('FBW_FORITER_DEFERRED_DENY declared as TLS_STORAGE', any('TLS_STORAGE<' in (ne or fbw) and 'FBW_FORITER_DEFERRED_DENY' in ne for ne,(_,_,fbw) in [] )),  # placeholder
]
PY

Repository: youknowone/pyre

Length of output: 2778


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1060,1210p' pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs

printf '\nTop-level FBW_* declarations (near file start):\n'
rg -n --type rust 'pub *static|static .*FBW_|TLS_STORAGE|thread_local' pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs | sed -n '1,220p'

printf '\nTLS_STORAGE type definitions:\n'
rg -n --type rust 'macro_rules! TLS_STORAGE|struct TLS_STORAGE|pub *struct TLS_STORAGE|thread_local!|const .*TLS_STORAGE' pyre/pyre-jit-trace/src -C 6

Repository: youknowone/pyre

Length of output: 40878


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --type rust 'fbw_foriter_deferred_deny_deferred_call|fbw_foriter_deferred_call_denied|CalleeReplaySafety::DeferredCall|fn fbw_callee_body_replay_safety|CalleeReplaySafety' pyre/pyre-jit-trace/src/jitcode_dispatch -C 5

printf '\nRelevant replay safety implementation:\n'
sed -n '1800,1930p' pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs

printf '\nRelevant inline admission and sub-walk gating:\n'
sed -n '2620,2710p' pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
sed -n '2710,2785p' pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs

Repository: youknowone/pyre

Length of output: 32222


**Move the deferred-callee deny registry out of thread_local!. **

FBW_FORITER_DEFERRED_DENY stores CodeObject keys and changes whether CalleeReplaySafety::DeferredCall can inline. A denied callee observed on one tracing thread stays visible only on that thread, so the same callee can replay as Clean later on another thread and produce different JIT behavior under the interpreter semantics invariant. Store this registry with the interpreter/JIT-session owner instead of per-thread state, with an upstream citation if per-thread scope is intended.

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

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs` around lines 2684 -
2694, Move FBW_FORITER_DEFERRED_DENY and its accessors, including
fbw_foriter_deferred_call_denied, out of thread_local! into
interpreter/JIT-session-owned shared state so denials for a CodeObject are
visible across tracing threads. Update all reads and writes, including the
foriter_deferred_admit calculation, to use the owner-scoped registry and
preserve consistent DeferredCall replay behavior.

Source: Coding guidelines

foriter_deferred_admit
}
CalleeReplaySafety::Dirty => {
Expand Down Expand Up @@ -3196,18 +3206,19 @@ pub(crate) fn try_walker_inline_resolved_user_call<Sym: WalkSym>(
// Stored bound methods carry their explicit receiver and callee frame,
// so their Ref operands remain available to the resume path.
//
// This is the one precondition here that still aborts instead of
// returning `Ok(None)`, and deliberately so. Residualizing it does
// work — `bench/synth/_pending/gc_bug_bridge_flavor_traceback_names`
// goes from 98 aborts to 2 and
// `_pending/exception_nested_exc_info_restore` from 5 aborts to 0,
// both compiling loops they never compiled before — but the loops it
// newly compiles then print traceback tuples missing their outermost
// frame, diverging from the interpreter (that fixture pins its
// expected output in its header). The abort was masking a lost
// `PyTraceback` node on the compiled exception path, not preventing
// one. Restore `Ok(None)` here once that node is recorded; it is the
// largest single win left in this function.
// This precondition used to abort the enclosing trace rather than
// decline the inline, because residualizing it let loops compile that
// then printed traceback tuples missing their OUTERMOST frame. That
// node is now recorded — the two bridge handler-entry arms attach the
// catching frame's own node — so the decline joins every other
// precondition here and returns `Ok(None)`.
//
// The abort was expensive out of all proportion to the inline it was
// protecting: a callee that walks a traceback (`while tb is not None`)
// lowers to exactly this instruction, so any handler calling such a
// helper aborted every retrace of the enclosing loop. The guard whose
// bridge the retrace was building therefore never got one and deopted
// on every delivery.
if bound_method.is_none()
&& (0..callee_code.instructions.len()).any(|pc| {
matches!(
Expand All @@ -3221,7 +3232,7 @@ pub(crate) fn try_walker_inline_resolved_user_call<Sym: WalkSym>(
})
{
if try_multiframe {
return Err(DispatchError::callee_inline_unsupported(op.pc));
return Ok(None);
}
break 'seed;
}
Expand Down
Loading
Loading