From b8aa2f5713d893a163393ebef60f588255d9e511 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 31 Jul 2026 11:56:37 +0900 Subject: [PATCH 01/16] call: reject non-type entries in find_best_base The loop skipped them, so `type('C', (object, object()), {})` built a class whose MRO contained a plain instance; `compute_mro` appends every supplied base and attribute lookup then reads that entry as a type layout. `best_base` raises `bases must be types` in the same loop. `__bases__` assignment screens its tuple earlier and keeps its own message. Assisted-by: Claude --- pyre/pyre-interpreter/src/call.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pyre/pyre-interpreter/src/call.rs b/pyre/pyre-interpreter/src/call.rs index aeba29e0e92..c536a109d05 100644 --- a/pyre/pyre-interpreter/src/call.rs +++ b/pyre/pyre-interpreter/src/call.rs @@ -4693,7 +4693,8 @@ unsafe fn create_weakref_slot(w_type: pyre_object::PyObjectRef) { } } -/// typeobject.py:1335-1353 find_best_base. +/// typeobject.py:1335-1353 find_best_base, with the non-type rejection +/// `Objects/typeobject.c` `best_base` performs in the same loop. unsafe fn find_best_base( w_bases: pyre_object::PyObjectRef, ) -> Result { @@ -4705,8 +4706,16 @@ unsafe fn find_best_base( let mut w_bestbase: pyre_object::PyObjectRef = std::ptr::null_mut(); for i in 0..len { if let Some(w_candidate) = pyre_object::w_tuple_getitem(w_bases, i as i64) { + // `best_base` rejects a non-type entry rather than skipping + // it. Skipping is not merely a lost diagnostic: `compute_mro` + // appends every supplied base, so a plain instance reaches + // the MRO and attribute lookup then reads it as a type + // layout. `__bases__` assignment screens its tuple before + // this and keeps its own message. if !pyre_object::is_type(w_candidate) { - continue; + return Err(crate::PyError::type_error( + "bases must be types".to_string(), + )); } // typeobject.py:1343-1345 — a custom metaclass mro() may // expose the nascent type before its MRO is installed, but From 6591326309d7e0d6291ce57c1565b6a318e3ec0e Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 31 Jul 2026 11:56:37 +0900 Subject: [PATCH 02/16] itertools: validate the product subtype before reading any argument `product_descr_new` reached `check_user_subclass` only through `itertools_alloc_for_class`, after the keyword census, the `repeat` conversion and a full pass over the input iterables. An unbound call such as `itertools.product.__new__(int, gen())` therefore consumed the iterable and could report an unrelated error first; `tp_new_wrapper` screens the subtype before `product_new` runs. Assisted-by: Claude --- pyre/pyre-interpreter/src/typedef.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index a1b4135c53f..4e1df5f2e80 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -24764,6 +24764,12 @@ fn product_descr_new(args: &[PyObjectRef]) -> Result Date: Fri, 31 Jul 2026 11:56:37 +0900 Subject: [PATCH 03/16] generic alias: root subs_parameters owners and results across substitution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `newargs`, `old_arg` and the owner arguments were raw `PyObjectRef`s in Rust locals across `__typing_prepare_subst__`, `__typing_subst__`, the recursive descent into nested lists and tuples, and each `w_tuple_new` / `w_list_new` — all of which allocate or run Python. The collector moves objects and does not scan Rust locals. Keeps them on the shadow stack and rereads them after anything that can collect; produced arguments are held as slots so an entry survives the later iterations' allocations. Assisted-by: Claude --- .../src/_pypy_generic_alias.rs | 155 ++++++++++++++---- 1 file changed, 119 insertions(+), 36 deletions(-) diff --git a/pyre/pyre-interpreter/src/_pypy_generic_alias.rs b/pyre/pyre-interpreter/src/_pypy_generic_alias.rs index 2e6628f2a46..df99a173c89 100644 --- a/pyre/pyre-interpreter/src/_pypy_generic_alias.rs +++ b/pyre/pyre-interpreter/src/_pypy_generic_alias.rs @@ -366,13 +366,34 @@ pub(crate) fn subs_parameters( "{repr} is not a generic class" ))); } + // Substitution runs arbitrary Python — `__typing_prepare_subst__`, + // `__typing_subst__`, and the recursive descent into nested lists and + // tuples — and allocates at nearly every step. The collector moves + // objects and does not scan Rust locals, so the owners and every produced + // argument live on the shadow stack and are reread after anything that + // can collect. + let _roots = pyre_object::gc_roots::push_roots(); + pyre_object::gc_roots::pin_root(self_); + let self_slot = pyre_object::gc_roots::shadow_stack_len() - 1; + pyre_object::gc_roots::pin_root(args); + let args_slot = pyre_object::gc_roots::shadow_stack_len() - 1; + pyre_object::gc_roots::pin_root(params); + let params_slot = pyre_object::gc_roots::shadow_stack_len() - 1; // `items = _unpack_args(items)` flattens unpacked `tuple[...]` aliases. // `__typing_prepare_subst__` then reshapes `items` for // `ParamSpec`/`TypeVarTuple` parameters — honoured per param, missing // attribute (the `None` default) skips it. - let mut items = unpack_args(items)?; + pyre_object::gc_roots::pin_root(items); + let items_slot = pyre_object::gc_roots::shadow_stack_len() - 1; + let unpacked = unpack_args(pyre_object::gc_roots::shadow_stack_get(items_slot))?; + pyre_object::gc_roots::shadow_stack_set(items_slot, unpacked); for i in 0..nparams { - let Some(param) = (unsafe { w_tuple_getitem(params, i as i64) }) else { + let Some(param) = (unsafe { + w_tuple_getitem( + pyre_object::gc_roots::shadow_stack_get(params_slot), + i as i64, + ) + }) else { continue; }; // `prepare = getattr(param, '__typing_prepare_subst__', None)` then @@ -384,20 +405,28 @@ pub(crate) fn subs_parameters( Err(e) => return Err(e), }; if !unsafe { pyre_object::is_none(prepare) } { - items = crate::call::call_function_impl_result(prepare, &[self_, items])?; + let reshaped = crate::call::call_function_impl_result( + prepare, + &[ + pyre_object::gc_roots::shadow_stack_get(self_slot), + pyre_object::gc_roots::shadow_stack_get(items_slot), + ], + )?; + pyre_object::gc_roots::shadow_stack_set(items_slot, reshaped); } } // Non-tuple `items` (a broken `__typing_prepare_subst__`) counts as one arg // per CPython `_Py_subs_parameters`; a bare `w_tuple_len` would crash on it. - let is_tuple_items = unsafe { is_tuple(items) }; + let is_tuple_items = unsafe { is_tuple(pyre_object::gc_roots::shadow_stack_get(items_slot)) }; let nitems = if is_tuple_items { - unsafe { w_tuple_len(items) } + unsafe { w_tuple_len(pyre_object::gc_roots::shadow_stack_get(items_slot)) } } else { 1 }; if nparams != nitems { let direction = if nitems > nparams { "many" } else { "few" }; - let s = unsafe { crate::display::py_repr(self_)? }; + let s = + unsafe { crate::display::py_repr(pyre_object::gc_roots::shadow_stack_get(self_slot))? }; return Err(crate::PyError::type_error(format!( "Too {direction} arguments for {s}; actual {nitems}, expected {nparams}" ))); @@ -405,81 +434,135 @@ pub(crate) fn subs_parameters( // `argitems` is the tuple view CPython indexes: `item` itself when it is a // tuple, otherwise a 1-tuple wrapping the single non-tuple `item`. let argitems = if is_tuple_items { - items + pyre_object::gc_roots::shadow_stack_get(items_slot) } else { - w_tuple_new(vec![items]) + w_tuple_new(vec![pyre_object::gc_roots::shadow_stack_get(items_slot)]) }; - let mut newargs: Vec = Vec::new(); - let args_are_tuple = unsafe { is_tuple(args) }; + pyre_object::gc_roots::pin_root(argitems); + let argitems_slot = pyre_object::gc_roots::shadow_stack_len() - 1; + // Slots rather than values: each entry has to survive every later + // iteration's allocations before the caller builds the result from them. + let mut newarg_slots: Vec = Vec::new(); + let mut push_newarg = |value: PyObjectRef, slots: &mut Vec| { + pyre_object::gc_roots::pin_root(value); + slots.push(pyre_object::gc_roots::shadow_stack_len() - 1); + }; + let args_are_tuple = unsafe { is_tuple(pyre_object::gc_roots::shadow_stack_get(args_slot)) }; let nargs = if args_are_tuple { - unsafe { w_tuple_len(args) } + unsafe { w_tuple_len(pyre_object::gc_roots::shadow_stack_get(args_slot)) } } else { - unsafe { w_list_len(args) } + unsafe { w_list_len(pyre_object::gc_roots::shadow_stack_get(args_slot)) } }; for i in 0..nargs { + let args_now = pyre_object::gc_roots::shadow_stack_get(args_slot); let old_arg = if args_are_tuple { - unsafe { w_tuple_getitem(args, i as i64) } + unsafe { w_tuple_getitem(args_now, i as i64) } } else { - unsafe { w_list_getitem(args, i as i64) } + unsafe { w_list_getitem(args_now, i as i64) } }; let Some(old_arg) = old_arg else { continue; }; if unsafe { is_type(old_arg) } { - newargs.push(old_arg); + push_newarg(old_arg, &mut newarg_slots); continue; } + pyre_object::gc_roots::pin_root(old_arg); + let old_arg_slot = pyre_object::gc_roots::shadow_stack_len() - 1; // CPython 3.14 `_Py_subs_parameters`: lists and tuples containing // parameters are recursively substituted, preserving their shape. if unsafe { is_tuple(old_arg) || is_list(old_arg) } { - let subargs = subs_parameters(self_, old_arg, params, items)?; - newargs.push(if unsafe { is_tuple(old_arg) } { - w_tuple_new(subargs) - } else { - w_list_new(subargs) - }); + let subargs = subs_parameters( + pyre_object::gc_roots::shadow_stack_get(self_slot), + pyre_object::gc_roots::shadow_stack_get(old_arg_slot), + pyre_object::gc_roots::shadow_stack_get(params_slot), + pyre_object::gc_roots::shadow_stack_get(items_slot), + )?; + // The recursion returned through its own `_roots` scope, so the + // entries are unrooted again; build the container before anything + // else can allocate. + let nested = + if unsafe { is_tuple(pyre_object::gc_roots::shadow_stack_get(old_arg_slot)) } { + w_tuple_new(subargs) + } else { + w_list_new(subargs) + }; + push_newarg(nested, &mut newarg_slots); continue; } // `unpack = _is_unpacked_typevartuple(old_arg)` decides whether the // produced `arg` is spliced (`newargs.extend`) or appended. - let unpack = is_unpacked_typevartuple(old_arg)?; + let unpack = + is_unpacked_typevartuple(pyre_object::gc_roots::shadow_stack_get(old_arg_slot))?; // `meth = getattr(old_arg, '__typing_subst__', None)` then // `if meth is not None`: a missing attribute and an attribute // explicitly set to `None` both fall through to `subs_tvars`. - let meth = match crate::baseobjspace::getattr_str(old_arg, "__typing_subst__") { + let meth = match crate::baseobjspace::getattr_str( + pyre_object::gc_roots::shadow_stack_get(old_arg_slot), + "__typing_subst__", + ) { Ok(m) => m, Err(e) if e.kind == crate::PyErrorKind::AttributeError => w_none(), Err(e) => return Err(e), }; let arg = if !unsafe { pyre_object::is_none(meth) } { - let iparam = tuple_index(params, old_arg)? - .ok_or_else(|| crate::PyError::value_error("tuple.index(x): x not in tuple"))?; - let item = unsafe { w_tuple_getitem(argitems, iparam as i64) }.unwrap_or_else(w_none); - crate::call::call_function_impl_result(meth, &[item])? + pyre_object::gc_roots::pin_root(meth); + let meth_slot = pyre_object::gc_roots::shadow_stack_len() - 1; + let iparam = tuple_index( + pyre_object::gc_roots::shadow_stack_get(params_slot), + pyre_object::gc_roots::shadow_stack_get(old_arg_slot), + )? + .ok_or_else(|| crate::PyError::value_error("tuple.index(x): x not in tuple"))?; + let item = unsafe { + w_tuple_getitem( + pyre_object::gc_roots::shadow_stack_get(argitems_slot), + iparam as i64, + ) + } + .unwrap_or_else(w_none); + crate::call::call_function_impl_result( + pyre_object::gc_roots::shadow_stack_get(meth_slot), + &[item], + )? } else { - subs_tvars(old_arg, params, argitems)? + subs_tvars( + pyre_object::gc_roots::shadow_stack_get(old_arg_slot), + pyre_object::gc_roots::shadow_stack_get(params_slot), + pyre_object::gc_roots::shadow_stack_get(argitems_slot), + )? }; if unpack { + pyre_object::gc_roots::pin_root(arg); + let arg_slot = pyre_object::gc_roots::shadow_stack_len() - 1; // GH-138497: an unpacked `__typing_subst__` must return a tuple. // (authority = CPython 3.14) - if !unsafe { is_tuple(arg) } { + if !unsafe { is_tuple(pyre_object::gc_roots::shadow_stack_get(arg_slot)) } { return Err(crate::PyError::type_error(format!( "expected __typing_subst__ of {} objects to return a tuple, not {}", - crate::type_methods::arg_type_name(old_arg), - crate::type_methods::arg_type_name(arg), + crate::type_methods::arg_type_name(pyre_object::gc_roots::shadow_stack_get( + old_arg_slot + )), + crate::type_methods::arg_type_name(pyre_object::gc_roots::shadow_stack_get( + arg_slot + )), ))); } - let n = unsafe { w_tuple_len(arg) }; + let n = unsafe { w_tuple_len(pyre_object::gc_roots::shadow_stack_get(arg_slot)) }; for j in 0..n { - if let Some(x) = unsafe { w_tuple_getitem(arg, j as i64) } { - newargs.push(x); + if let Some(x) = unsafe { + w_tuple_getitem(pyre_object::gc_roots::shadow_stack_get(arg_slot), j as i64) + } { + push_newarg(x, &mut newarg_slots); } } } else { - newargs.push(arg); + push_newarg(arg, &mut newarg_slots); } } - Ok(newargs) + Ok(newarg_slots + .into_iter() + .map(pyre_object::gc_roots::shadow_stack_get) + .collect()) } /// `subs_tvars(obj, params, argitems)` (`_pypy_generic_alias.py:183`) — From 2d1f324c81c56ccae202e59e56d54397fe025161 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 31 Jul 2026 16:08:10 +0900 Subject: [PATCH 04/16] struct: root iter_unpack buffer lease --- pyre/pyre-interpreter/src/module/struct/mod.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pyre/pyre-interpreter/src/module/struct/mod.rs b/pyre/pyre-interpreter/src/module/struct/mod.rs index fed00ea8c06..5c292fc9d1e 100644 --- a/pyre/pyre-interpreter/src/module/struct/mod.rs +++ b/pyre/pyre-interpreter/src/module/struct/mod.rs @@ -1352,7 +1352,17 @@ pub mod unpack_iter { pyre_object::w_type_set_acceptable_as_base_class(iter_type, false); pyre_object::w_type_set_disallow_instantiation(iter_type); } - let buf = unsafe { readbuf(buffer)? }; + // `readbuf_w` may dispatch a Python-level `__buffer__` hook. Keep + // both operands rooted across that call and the stable allocation: + // PyPy's W_UnpackIter stores the live `self.view` lease on the + // iterator, never an unrooted caller-local pointer. + let _roots = pyre_object::gc_roots::push_roots(); + let sp = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(format); + pyre_object::gc_roots::pin_root(buffer); + let r_format = pyre_object::gc_roots::shadow_stack_get(sp); + let r_buffer = pyre_object::gc_roots::shadow_stack_get(sp + 1); + let buf = unsafe { readbuf(r_buffer)? }; if size <= 0 { return Err(struct_error(format!( "cannot iteratively unpack with a struct of length {size}" @@ -1363,14 +1373,14 @@ pub mod unpack_iter { "iterative unpacking requires a buffer of a multiple of {size} bytes" ))); } - let export_active = unsafe { crate::builtins::buffer_export_incref(buffer) }; + let export_active = unsafe { crate::builtins::buffer_export_incref(r_buffer) }; let w_iter = W_UnpackIter::allocate_stable(W_UnpackIter { ob: pyre_object::PyObject { ob_type: std::ptr::null(), w_class: std::ptr::null_mut(), }, - format, - buffer, + format: r_format, + buffer: pyre_object::gc_roots::shadow_stack_get(sp + 1), size, index: 0, export_active, From 7ff52c9ce1ce46acd9d066e759203a28bcb3381d Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 31 Jul 2026 16:37:22 +0900 Subject: [PATCH 05/16] types: complete SimpleNamespace 3.14 parity --- pyre/pyre-interpreter/src/module/sys/vm.rs | 418 ++++++++++++++++----- 1 file changed, 321 insertions(+), 97 deletions(-) diff --git a/pyre/pyre-interpreter/src/module/sys/vm.rs b/pyre/pyre-interpreter/src/module/sys/vm.rs index 542c56ce878..aa894d954d8 100644 --- a/pyre/pyre-interpreter/src/module/sys/vm.rs +++ b/pyre/pyre-interpreter/src/module/sys/vm.rs @@ -64,64 +64,142 @@ fn sys_namespace_init(args: &[PyObjectRef]) -> crate::PyResult { /// instance dict directly, so `setdictvalue` is used rather than `setattr` — a /// subclass `__setattr__` is not consulted during construction. fn namespace_apply_kwargs(self_obj: PyObjectRef, kwargs: Option) -> crate::PyResult { - // `self.__dict__.update(kwargs)` evaluates `self.__dict__` first, so a - // receiver without an instance dict raises AttributeError even for an - // empty keyword set. - crate::baseobjspace::getattr_str(self_obj, "__dict__")?; if let Some(dict) = kwargs { - unsafe { - for (key, value) in pyre_object::w_dict_items(dict) { - if pyre_object::is_str(key) { - if let Ok(name) = pyre_object::w_str_get_wtf8(key).as_str() { - if name == "__pyre_kw__" { - continue; - } - crate::baseobjspace::setdictvalue_native(self_obj, name, value); - continue; - } - } - crate::baseobjspace::setattr(self_obj, key, value)?; - } - } + namespace_update_dict(self_obj, dict, true)?; + } else { + // `self.__dict__.update(kwargs)` evaluates `self.__dict__` first, so + // a receiver without an instance dict raises even for no keywords. + crate::baseobjspace::getattr_str(self_obj, "__dict__")?; } Ok(w_none()) } +/// CPython 3.14 `PyDict_Update(ns->ns_dict, source)`, with the flat builtin +/// ABI's private `__pyre_kw__` marker optionally omitted. Validate every key +/// before the first store, matching `PyArg_ValidateKeywordArguments`: a bad +/// mapping cannot partially update the namespace. The destination is the +/// real instance dict, so subclass `__setattr__` is deliberately bypassed. +fn namespace_update_dict( + self_obj: PyObjectRef, + source: PyObjectRef, + skip_kw_marker: bool, +) -> Result<(), crate::PyError> { + let _roots = pyre_object::gc_roots::push_roots(); + let sp = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(self_obj); + pyre_object::gc_roots::pin_root(source); + let destination = crate::baseobjspace::getattr_str( + pyre_object::gc_roots::shadow_stack_get(sp), + "__dict__", + )?; + pyre_object::gc_roots::pin_root(destination); + let items = unsafe { + pyre_object::w_dict_items(pyre_object::gc_roots::shadow_stack_get(sp + 1)) + }; + let items_sp = pyre_object::gc_roots::shadow_stack_len(); + for &(key, value) in &items { + pyre_object::gc_roots::pin_root(key); + pyre_object::gc_roots::pin_root(value); + } + for i in 0..items.len() { + let key = pyre_object::gc_roots::shadow_stack_get(items_sp + i * 2); + if !unsafe { pyre_object::is_str(key) } { + return Err(crate::PyError::type_error("keywords must be strings")); + } + } + for i in 0..items.len() { + let key = pyre_object::gc_roots::shadow_stack_get(items_sp + i * 2); + let value = pyre_object::gc_roots::shadow_stack_get(items_sp + i * 2 + 1); + if skip_kw_marker + && unsafe { pyre_object::w_str_get_wtf8(key).as_str() == Ok("__pyre_kw__") } + { + continue; + } + crate::type_methods::dict_store_checked( + pyre_object::gc_roots::shadow_stack_get(sp + 2), + key, + value, + )?; + } + Ok(()) +} + /// Allocate a fresh stub instance whose type supports `setattr`. Used for /// all the CPython-style attribute bags surfaced by the sys module. fn make_sys_namespace_instance() -> PyObjectRef { w_instance_new(sys_namespace_type()) } -/// `_structseq.py:171 SimpleNamespace.__init__(self, **kwargs)` — keyword-only, -/// so a positional argument raises the arg-count TypeError instead of being -/// accepted as a mapping. +/// CPython 3.14 `namespace_init`: accept at most one positional mapping or +/// iterable of pairs, validate that its resulting dict has only string keys, +/// merge it into the instance, then overlay keyword arguments. This is the +/// target-version delta from PyPy 3.11's keyword-only `_structseq.py` class. fn simple_namespace_init(args: &[PyObjectRef]) -> crate::PyResult { - let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args); + let _roots = pyre_object::gc_roots::push_roots(); + let sp = pyre_object::gc_roots::shadow_stack_len(); + for &arg in args { + pyre_object::gc_roots::pin_root(arg); + } + let rooted = (0..args.len()) + .map(|i| pyre_object::gc_roots::shadow_stack_get(sp + i)) + .collect::>(); + let (positional, kwargs) = crate::builtins::split_builtin_kwargs(&rooted); let Some(&self_obj) = positional.first() else { return Err(crate::PyError::type_error( "__init__() missing 1 required positional argument: 'self'", )); }; - if positional.len() > 1 { + if positional.len() > 2 { return Err(crate::PyError::type_error(format!( - "SimpleNamespace.__init__() takes 1 positional argument but {} were given", - positional.len() + "SimpleNamespace expected at most 1 argument, got {}", + positional.len() - 1 ))); } - namespace_apply_kwargs(self_obj, kwargs) + // The kwargs carrier is not guaranteed to occupy the last raw ABI slot + // relative to positional arguments. Pin the parsed operands into a + // canonical order before any allocation instead of deriving their slots + // from the flat input layout. + let operands_sp = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(self_obj); + if positional.len() == 2 { + pyre_object::gc_roots::pin_root(positional[1]); + } + if let Some(kwargs) = kwargs { + pyre_object::gc_roots::pin_root(kwargs); + } + if positional.len() == 2 { + let temporary = w_dict_new(); + pyre_object::gc_roots::pin_root(temporary); + let temporary_slot = pyre_object::gc_roots::shadow_stack_len() - 1; + let temporary = pyre_object::gc_roots::shadow_stack_get(temporary_slot); + crate::type_methods::dict_update1( + temporary, + pyre_object::gc_roots::shadow_stack_get(operands_sp + 1), + )?; + namespace_update_dict( + pyre_object::gc_roots::shadow_stack_get(operands_sp), + pyre_object::gc_roots::shadow_stack_get(temporary_slot), + false, + )?; + } + namespace_apply_kwargs( + pyre_object::gc_roots::shadow_stack_get(operands_sp), + kwargs.map(|_| { + pyre_object::gc_roots::shadow_stack_get( + operands_sp + 1 + usize::from(positional.len() == 2), + ) + }), + ) } /// `types.SimpleNamespace` — the attribute-bag type exposed as /// `type(sys.implementation)` and re-published by `types.py:20` /// (`SimpleNamespace = type(sys.implementation)`). /// -/// `_structseq.py:166 SimpleNamespace`: keyword-only construction that copies -/// into the instance dict, a `namespace(...)` repr over the sorted items with -/// a recursion guard, structural `__eq__`/`__ne__` (NotImplemented against a -/// non-namespace), and no `__hash__` (so instances are unhashable). The -/// keyword copy into the instance dict is shared with `sys.namespace` via -/// `namespace_apply_kwargs`; the positional rejection is `SimpleNamespace`-specific. +/// `_structseq.py:166 SimpleNamespace`, with CPython 3.14's newer constructor, +/// full rich-comparison surface, pickle reducer and `__replace__`. Storage +/// remains PyPy-shaped: the values live in the instance dict, not a side +/// table or a second native mapping. fn simple_namespace_type() -> PyObjectRef { static TYPE: OnceLock = OnceLock::new(); let raw = *TYPE.get_or_init(|| { @@ -147,6 +225,28 @@ fn simple_namespace_type() -> PyObjectRef { "__ne__", make_builtin_function_with_arity("__ne__", simple_namespace_ne, 2), ); + for (name, function) in [ + ("__lt__", simple_namespace_lt as fn(&[PyObjectRef]) -> crate::PyResult), + ("__le__", simple_namespace_le), + ("__gt__", simple_namespace_gt), + ("__ge__", simple_namespace_ge), + ] { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + name, + make_builtin_function_with_arity(name, function, 2), + ); + } + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + "__reduce__", + make_builtin_function_with_arity("__reduce__", simple_namespace_reduce, 1), + ); + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + "__replace__", + make_builtin_function("__replace__", simple_namespace_replace), + ); // SimpleNamespace defines no `__hash__`, so it inherits None // and is unhashable. pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy(ns, "__hash__", w_none()); @@ -163,91 +263,107 @@ fn simple_namespace_type() -> PyObjectRef { raw as PyObjectRef } -/// `_structseq.py:174 SimpleNamespace.__repr__` — `namespace(k=v, ...)` over -/// the sorted `__dict__` items (`%s=%r`), returning `namespace(...)` when the -/// instance is already being repr'd on this thread. +/// CPython 3.14 `namespace_repr`, layered over PyPy's recursion guard. Exact +/// instances use `namespace`, subclasses use their concrete type name. Walk +/// a snapshot of the insertion-ordered keys, then re-read each live value so +/// a re-entrant repr that mutates the dict has CPython's skip/update behavior. fn simple_namespace_repr(args: &[PyObjectRef]) -> crate::PyResult { let Some(&self_obj) = args.first() else { return Err(crate::PyError::type_error( "__repr__() missing 1 required positional argument: 'self'", )); }; + let _roots = pyre_object::gc_roots::push_roots(); + let sp = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(self_obj); + let actual_type = crate::typedef::r#type(self_obj) + .map(|tp| tp.as_ptr()) + .unwrap_or(simple_namespace_type()); + let name = if std::ptr::eq(actual_type, simple_namespace_type()) { + "namespace".to_string() + } else { + unsafe { w_type_get_name(actual_type) }.to_string() + }; let Some(_guard) = crate::display::ReprGuard::enter(self_obj) else { - return Ok(w_str_new("namespace(...)")); + return Ok(w_str_new(&format!("{name}(...)"))); }; - let dict = crate::baseobjspace::getattr_str(self_obj, "__dict__")?; - // A user `__lt__`, `__str__` or `__repr__` below can collect, and the - // moving GC relocates the items this snapshot holds. Pin every key and - // value on the shadow stack and address them by slot from here on: a raw - // `(key, value)` vector would go stale at the first such call. - let _roots = pyre_object::gc_roots::push_roots(); - let base = pyre_object::gc_roots::shadow_stack_len(); - let items = unsafe { pyre_object::w_dict_items(dict) }; - for (k, v) in &items { - pyre_object::gc_roots::pin_root(*k); - pyre_object::gc_roots::pin_root(*v); - } - let key = |i: usize| pyre_object::gc_roots::shadow_stack_get(base + i * 2); - let value = |i: usize| pyre_object::gc_roots::shadow_stack_get(base + i * 2 + 1); - - // `sorted(self.__dict__.items())` — order by the key objects with Python - // `<`, not by their `str()`. Incomparable keys (e.g. `int` mixed with - // `str`) raise, halting the repr as the sort itself does. Rust's `sort_by` - // closure cannot return `Result`, so a raising comparison is captured in a - // `Cell` and surfaced once the sort completes. - let sort_error: std::cell::Cell> = std::cell::Cell::new(None); - let lt = |x: usize, y: usize| -> bool { - if let Some(e) = sort_error.take() { - sort_error.set(Some(e)); - return false; - } - match crate::baseobjspace::compare(key(x), key(y), crate::baseobjspace::CompareOp::Lt) { - Ok(r) => crate::baseobjspace::is_true(r).unwrap_or_else(|e| { - sort_error.set(Some(e)); - false - }), - Err(e) => { - sort_error.set(Some(e)); - false - } - } + let dict = crate::baseobjspace::getattr_str( + pyre_object::gc_roots::shadow_stack_get(sp), + "__dict__", + )?; + pyre_object::gc_roots::pin_root(dict); + let keys = unsafe { + pyre_object::w_dict_items(pyre_object::gc_roots::shadow_stack_get(sp + 1)) + .into_iter() + .map(|(key, _)| key) + .collect::>() }; - let mut order: Vec = (0..items.len()).collect(); - order.sort_by(|&a, &b| { - if lt(a, b) { - std::cmp::Ordering::Less - } else if lt(b, a) { - std::cmp::Ordering::Greater - } else { - std::cmp::Ordering::Equal - } - }); - if let Some(e) = sort_error.take() { - return Err(e); + let keys_sp = pyre_object::gc_roots::shadow_stack_len(); + for &key in &keys { + pyre_object::gc_roots::pin_root(key); } - let mut parts = Vec::with_capacity(order.len()); - for i in order { + let mut parts = Vec::with_capacity(keys.len()); + for i in 0..keys.len() { + let key = pyre_object::gc_roots::shadow_stack_get(keys_sp + i); + if !unsafe { pyre_object::is_str(key) } + || unsafe { pyre_object::w_str_len(key) == 0 } + { + continue; + } + let value = match crate::baseobjspace::getitem( + pyre_object::gc_roots::shadow_stack_get(sp + 1), + key, + ) { + Ok(value) => value, + Err(err) if err.kind == crate::PyErrorKind::KeyError => continue, + Err(err) => return Err(err), + }; + pyre_object::gc_roots::pin_root(value); parts.push(format!( "{}={}", - unsafe { crate::display::py_str(key(i))? }, - unsafe { crate::display::py_repr(value(i))? } + unsafe { crate::display::py_str(key)? }, + unsafe { + crate::display::py_repr(pyre_object::gc_roots::shadow_stack_get( + pyre_object::gc_roots::shadow_stack_len() - 1, + ))? + } )); } - Ok(w_str_new(&format!("namespace({})", parts.join(", ")))) + Ok(w_str_new(&format!("{name}({})", parts.join(", ")))) } /// `_structseq.py:185 SimpleNamespace.__eq__` — structural over `__dict__` /// when `other` is a namespace, NotImplemented otherwise. fn simple_namespace_eq(args: &[PyObjectRef]) -> crate::PyResult { - simple_namespace_richcompare(args, "__eq__", false) + simple_namespace_richcompare(args, "__eq__", crate::baseobjspace::CompareOp::Eq) } /// `_structseq.py:190 SimpleNamespace.__ne__`. fn simple_namespace_ne(args: &[PyObjectRef]) -> crate::PyResult { - simple_namespace_richcompare(args, "__ne__", true) + simple_namespace_richcompare(args, "__ne__", crate::baseobjspace::CompareOp::Ne) +} + +fn simple_namespace_lt(args: &[PyObjectRef]) -> crate::PyResult { + simple_namespace_richcompare(args, "__lt__", crate::baseobjspace::CompareOp::Lt) +} + +fn simple_namespace_le(args: &[PyObjectRef]) -> crate::PyResult { + simple_namespace_richcompare(args, "__le__", crate::baseobjspace::CompareOp::Le) +} + +fn simple_namespace_gt(args: &[PyObjectRef]) -> crate::PyResult { + simple_namespace_richcompare(args, "__gt__", crate::baseobjspace::CompareOp::Gt) +} + +fn simple_namespace_ge(args: &[PyObjectRef]) -> crate::PyResult { + simple_namespace_richcompare(args, "__ge__", crate::baseobjspace::CompareOp::Ge) } -fn simple_namespace_richcompare(args: &[PyObjectRef], name: &str, negate: bool) -> crate::PyResult { +fn simple_namespace_richcompare( + args: &[PyObjectRef], + name: &str, + op: crate::baseobjspace::CompareOp, +) -> crate::PyResult { // `def __eq__(self, other)` — a missing argument is an arity error, not a // NotImplemented result. let (Some(&self_obj), Some(&other)) = (args.first(), args.get(1)) else { @@ -256,15 +372,123 @@ fn simple_namespace_richcompare(args: &[PyObjectRef], name: &str, negate: bool) if args.is_empty() { "self" } else { "other" } ))); }; - if !unsafe { crate::baseobjspace::isinstance_w(other, simple_namespace_type()) } { + let other_type = crate::typedef::r#type(other) + .map(|tp| tp.as_ptr()) + .unwrap_or(PY_NULL); + if !unsafe { crate::baseobjspace::issubtype_w(other_type, simple_namespace_type()) } { return Ok(w_not_implemented()); } - // `self.__dict__ == other.__dict__` — read through the descriptor so a - // subclass `__dict__` override is honoured, as PyPy's attribute access is. + // CPython 3.14 forwards all six operations to the two namespace dicts. + // In particular, ordering reaches dict's TypeError instead of returning + // NotImplemented from the namespace type itself. let self_dict = crate::baseobjspace::getattr_str(self_obj, "__dict__")?; let other_dict = crate::baseobjspace::getattr_str(other, "__dict__")?; - let equal = crate::baseobjspace::eq_w(self_dict, other_dict)?; - Ok(w_bool_from(equal ^ negate)) + crate::baseobjspace::compare(self_dict, other_dict, op) +} + +/// CPython 3.14 `namespace_reduce`: `(type(self), (), self.__dict__)`. +fn simple_namespace_reduce(args: &[PyObjectRef]) -> crate::PyResult { + let Some(&self_obj) = args.first() else { + return Err(crate::PyError::type_error( + "__reduce__() missing 1 required positional argument: 'self'", + )); + }; + let _roots = pyre_object::gc_roots::push_roots(); + let sp = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(self_obj); + let w_type = crate::typedef::r#type(self_obj) + .map(|tp| tp.as_ptr()) + .unwrap_or(PY_NULL); + pyre_object::gc_roots::pin_root(w_type); + let w_args = w_tuple_new(Vec::new()); + pyre_object::gc_roots::pin_root(w_args); + let w_dict = crate::baseobjspace::getattr_str( + pyre_object::gc_roots::shadow_stack_get(sp), + "__dict__", + )?; + pyre_object::gc_roots::pin_root(w_dict); + Ok(w_tuple_new(vec![ + pyre_object::gc_roots::shadow_stack_get(sp + 1), + pyre_object::gc_roots::shadow_stack_get(sp + 2), + pyre_object::gc_roots::shadow_stack_get(sp + 3), + ])) +} + +/// CPython 3.14 `namespace_replace`: construct `type(self)()` first, require +/// that its actual type remains a SimpleNamespace subtype, copy the source +/// dict, then overlay keyword changes. +fn simple_namespace_replace(args: &[PyObjectRef]) -> crate::PyResult { + let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args); + let Some(&self_obj) = positional.first() else { + return Err(crate::PyError::type_error( + "__replace__() missing 1 required positional argument: 'self'", + )); + }; + if positional.len() != 1 { + return Err(crate::PyError::type_error( + "__replace__() takes no positional arguments", + )); + } + let _roots = pyre_object::gc_roots::push_roots(); + let sp = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(self_obj); + if let Some(kwargs) = kwargs { + pyre_object::gc_roots::pin_root(kwargs); + } + let self_type = crate::typedef::r#type(self_obj) + .map(|tp| tp.as_ptr()) + .unwrap_or(PY_NULL); + pyre_object::gc_roots::pin_root(self_type); + let result = crate::call::call_function_impl_result( + pyre_object::gc_roots::shadow_stack_get( + sp + 1 + usize::from(kwargs.is_some()), + ), + &[], + )?; + pyre_object::gc_roots::pin_root(result); + let result = pyre_object::gc_roots::shadow_stack_get( + sp + 2 + usize::from(kwargs.is_some()), + ); + let result_type = crate::typedef::r#type(result) + .map(|tp| tp.as_ptr()) + .unwrap_or(PY_NULL); + if !unsafe { crate::baseobjspace::issubtype_w(result_type, simple_namespace_type()) } { + let constructed = unsafe { + crate::baseobjspace::type_fully_qualified_name( + pyre_object::gc_roots::shadow_stack_get( + sp + 1 + usize::from(kwargs.is_some()), + ), + ) + }; + let returned = if result_type.is_null() { + "object" + } else { + unsafe { w_type_get_name(result_type) } + }; + return Err(crate::PyError::type_error(format!( + "expect types.SimpleNamespace type, but {constructed}() returned '{returned}' object" + ))); + } + let source_dict = crate::baseobjspace::getattr_str( + pyre_object::gc_roots::shadow_stack_get(sp), + "__dict__", + )?; + pyre_object::gc_roots::pin_root(source_dict); + namespace_update_dict( + result, + pyre_object::gc_roots::shadow_stack_get( + sp + 3 + usize::from(kwargs.is_some()), + ), + false, + )?; + if kwargs.is_some() { + namespace_update_dict( + result, + pyre_object::gc_roots::shadow_stack_get(sp + 1), + true, + )?; + } + Ok(result) } /// `pypy/module/sys/vm.py:217 space.getexecutioncontext()` access for From 74a1fd5c9721a151157770cbd0d8cc69af09c852 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 31 Jul 2026 17:03:17 +0900 Subject: [PATCH 06/16] types: expose UnionType special methods --- pyre/pyre-interpreter/src/typedef.rs | 69 ++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index 4e1df5f2e80..9006bc3794c 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -8790,6 +8790,52 @@ fn union_class_getitem(args: &[PyObjectRef]) -> crate::PyResult { crate::_pypy_generic_alias::union_from_items(&items) } +/// `UnionType.__repr__` (`_pypy_generic_alias.py:286-287`). +/// +/// The display slot already renders unions correctly, but the method must +/// also be present when accessed explicitly as `union.__repr__()`. Keeping +/// this as the same central formatter avoids a second representation path. +fn union_repr_method(args: &[PyObjectRef]) -> crate::PyResult { + let self_ = args.first().copied().unwrap_or(pyre_object::PY_NULL); + if !unsafe { pyre_object::is_union(self_) } { + return Err(crate::PyError::type_error( + "descriptor '__repr__' requires a 'types.UnionType' object", + )); + } + let rendered = unsafe { crate::display::py_repr(self_)? }; + Ok(pyre_object::w_str_new(&rendered)) +} + +/// `UnionType.__hash__` (`_pypy_generic_alias.py:275`). +fn union_hash_method(args: &[PyObjectRef]) -> crate::PyResult { + let self_ = args.first().copied().unwrap_or(pyre_object::PY_NULL); + if !unsafe { pyre_object::is_union(self_) } { + return Err(crate::PyError::type_error( + "descriptor '__hash__' requires a 'types.UnionType' object", + )); + } + Ok(pyre_object::w_int_new(crate::builtins::try_hash_value( + self_, + )?)) +} + +/// `UnionType.__mro_entries__` (`_pypy_generic_alias.py` parity). +/// +/// A union can be used in an annotation but cannot be used as a base class. +/// CPython exposes the method and raises this error when it is called. +fn union_mro_entries_method(args: &[PyObjectRef]) -> crate::PyResult { + let self_ = args.first().copied().unwrap_or(pyre_object::PY_NULL); + if !unsafe { pyre_object::is_union(self_) } { + return Err(crate::PyError::type_error( + "descriptor '__mro_entries__' requires a 'types.UnionType' object", + )); + } + let rendered = unsafe { crate::display::py_repr(self_)? }; + Err(crate::PyError::type_error(format!( + "Cannot subclass {rendered}" + ))) +} + fn init_union_type(ns: PyObjectRef) { // Python 3.14's shared `types.UnionType` / `typing.Union` runtime type // exposes `__module__` on union *instances* as well as on the type. Keep @@ -8923,6 +8969,29 @@ fn init_union_type(ns: PyObjectRef) { ), ) }; + // These are slots for ordinary operations, but CPython 3.14 and PyPy + // expose them as callable attributes on union instances as well. + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + "__repr__", + make_builtin_function("__repr__", union_repr_method), + ) + }; + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + "__hash__", + make_builtin_function("__hash__", union_hash_method), + ) + }; + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + "__mro_entries__", + make_builtin_function("__mro_entries__", union_mro_entries_method), + ) + }; } static GETSET_DESCRIPTOR_TYPE: std::sync::OnceLock = std::sync::OnceLock::new(); From 8ed057b09354c77394f80136daa04436870f747e Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 31 Jul 2026 20:50:19 +0900 Subject: [PATCH 07/16] itertools: run the product subtype check after the keyword census `W_Product__new__` extracts `repeat` and raises for a leftover keyword before `allocate_instance` screens the subtype, so `product.__new__(int, gen(), bogus=1)` reports the keyword, not the class. Placing the check first inverted that pair. The check still precedes `W_Product.__init__`, so the `repeat` conversion and the pass over the input iterables stay behind it and `product.__new__(int, gen())` leaves `gen()` unconsumed. Assisted-by: Claude --- pyre/pyre-interpreter/src/typedef.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index 9006bc3794c..a81cad4cef7 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -24833,14 +24833,17 @@ fn product_descr_new(args: &[PyObjectRef]) -> Result Date: Fri, 31 Jul 2026 22:21:50 +0900 Subject: [PATCH 08/16] call, baseobjspace: walk a classic base through abstract_mro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `find_best_base` skipped a non-type base and nothing downstream looked at it again: `compute_mro` appends every supplied base, so `type('C', (object, object()), {})` built a class whose MRO held a plain instance and attribute lookup then read that entry as a type layout. `find_best_base` (typeobject.py:1341-1342) is not where such a base is rejected — it belongs to `get_mro` (typeobject.py:1680-1684), whose non-`W_TypeObject` branch walks the base with `abstract_mro` (typeobject.py:1665-1678) and reads `__bases__` off it. Port that branch into `validate_c3_mro`, the fallible front door pyre already routes the C3 merge through, and restore the `continue`. `setup_user_defined_type` reaches `compute_mro` only after `check_and_find_best_base` accepted the tuple, so the classic walk is gated on some base being a type; an all-classic tuple keeps `check_and_find_best_base`'s own message. `abstract_mro` reads `__bases__`, which can run `__getattr__` and allocate, so the C3 list build now accumulates shadow-stack slots and materializes them only for the merge, which allocates nothing. Assisted-by: Claude --- pyre/pyre-interpreter/src/baseobjspace.rs | 112 ++++++++++++++++++++-- pyre/pyre-interpreter/src/call.rs | 16 +--- 2 files changed, 110 insertions(+), 18 deletions(-) diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 34d94cbb83f..5c11d42371b 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -9340,6 +9340,54 @@ pub(crate) unsafe fn compute_and_set_mro(w_self: PyObjectRef) -> PyResult { Ok(pyre_object::w_none()) } +/// Pin `w` into the ambient root scope and hand back its slot. +fn pin_slot(w: PyObjectRef) -> usize { + pyre_object::gc_roots::pin_root(w); + pyre_object::gc_roots::shadow_stack_len() - 1 +} + +/// `typeobject.py:1665-1678 abstract_mro` — the app-level classic-class walk +/// that `get_mro` (typeobject.py:1680-1684) applies to a base which is not a +/// `W_TypeObject`. Reading `__bases__` is what rejects a plain instance +/// handed in as a base. +/// +/// Returns shadow-stack slots rather than values: a `__bases__` read can run +/// `__getattr__` and allocate, so the caller's scope owns the walk's roots and +/// the entries stay addressable across the reads that follow. +/// +/// `klass not in mro` is answered by pointer identity, matching the C3 merge +/// this feeds instead of running an app-level `__eq__` between the reads. +unsafe fn abstract_mro(w_klass: PyObjectRef) -> Result, crate::PyError> { + let mut mro_slots: Vec = Vec::new(); + let mut stack_slots: Vec = vec![pin_slot(w_klass)]; + while let Some(slot) = stack_slots.pop() { + let w_cls = pyre_object::gc_roots::shadow_stack_get(slot); + if mro_slots + .iter() + .any(|&seen| std::ptr::eq(pyre_object::gc_roots::shadow_stack_get(seen), w_cls)) + { + continue; + } + mro_slots.push(slot); + let bases_slot = pin_slot(getattr_str(w_cls, "__bases__")?); + if !is_tuple(pyre_object::gc_roots::shadow_stack_get(bases_slot)) { + return Err(crate::PyError::type_error("__bases__ must be a tuple")); + } + // `stack += klass.__bases__[::-1]` — the reversed extend leaves + // `__bases__[0]` on top, so the walk descends in declaration order. + let nbases = w_tuple_len(pyre_object::gc_roots::shadow_stack_get(bases_slot)); + for j in (0..nbases).rev() { + if let Some(w_base) = w_tuple_getitem( + pyre_object::gc_roots::shadow_stack_get(bases_slot), + j as i64, + ) { + stack_slots.push(pin_slot(w_base)); + } + } + } + Ok(mro_slots) +} + /// Reject a base tuple whose C3 merge has no valid next head. /// /// Type construction calls this before allocating the new type or running @@ -9350,9 +9398,15 @@ pub unsafe fn validate_c3_mro(bases: PyObjectRef) -> Result<(), crate::PyError> return Ok(()); } let n = w_tuple_len(bases); + // `is_type_like_w` dispatches through the object space and the classic-base + // walk below runs Python outright, so the tuple is pinned for the whole + // validation and reread after anything that can collect. + let _roots = pyre_object::gc_roots::push_roots(); + let bases_slot = pin_slot(bases); // CPython's typeobject.c reports duplicate direct bases before the C3 // merge. PyPy's mro_error discovers the same case in its final list. for i in 0..n { + let bases = pyre_object::gc_roots::shadow_stack_get(bases_slot); let Some(base) = w_tuple_getitem(bases, i as i64) else { continue; }; @@ -9370,23 +9424,67 @@ pub unsafe fn validate_c3_mro(bases: PyObjectRef) -> Result<(), crate::PyError> ))); } } - let mut lists: Vec> = Vec::with_capacity(n + 1); - let mut bases_list = Vec::with_capacity(n); + // typeobject.py:1519,1560 — `setup_user_defined_type` runs + // `check_and_find_best_base` before it reaches `compute_mro`, so the C3 + // merge only ever sees a tuple that already holds at least one type. pyre + // runs this validation ahead of the best-base check, so reproduce that + // precondition here: with no type among the bases, leave the tuple to + // `check_and_find_best_base` and its own message. + let has_type_base = (0..n).any(|i| { + w_tuple_getitem( + pyre_object::gc_roots::shadow_stack_get(bases_slot), + i as i64, + ) + .is_some_and(|base| is_type_like_w(base)) + }); + + // typeobject.py:1689-1690 `orderlists = [get_mro(space, base) for base in + // cls.bases_w]` then `orderlists.append([cls] + cls.bases_w)`. The + // classic branch of `get_mro` runs Python, so the lists are accumulated as + // shadow-stack slots and only materialized once the build is over — the + // merge below allocates nothing, so reading the values there is safe. + let mut list_slots: Vec> = Vec::with_capacity(n + 1); + let mut bases_slots = Vec::with_capacity(n); for i in 0..n { - let Some(base) = w_tuple_getitem(bases, i as i64) else { + let Some(base) = w_tuple_getitem( + pyre_object::gc_roots::shadow_stack_get(bases_slot), + i as i64, + ) else { continue; }; + // typeobject.py:1680-1684 `get_mro`: a `W_TypeObject` contributes its + // own linearization, anything else is walked as a classic class. if is_type_like_w(base) { let mro = w_type_get_mro(base); - lists.push(if mro.is_null() { + let entry = if mro.is_null() { compute_mro(base) } else { (*mro).to_vec() - }); + }; + list_slots.push(entry.into_iter().map(pin_slot).collect()); + } else if has_type_base { + list_slots.push(abstract_mro(base)?); } - bases_list.push(base); + bases_slots.push(pin_slot( + w_tuple_getitem( + pyre_object::gc_roots::shadow_stack_get(bases_slot), + i as i64, + ) + .unwrap_or(base), + )); } - lists.push(bases_list); + list_slots.push(bases_slots); + + let bases = pyre_object::gc_roots::shadow_stack_get(bases_slot); + let mut lists: Vec> = list_slots + .into_iter() + .map(|slots| { + slots + .into_iter() + .map(pyre_object::gc_roots::shadow_stack_get) + .collect() + }) + .collect(); loop { lists.retain(|list| !list.is_empty()); diff --git a/pyre/pyre-interpreter/src/call.rs b/pyre/pyre-interpreter/src/call.rs index c536a109d05..cf14971376a 100644 --- a/pyre/pyre-interpreter/src/call.rs +++ b/pyre/pyre-interpreter/src/call.rs @@ -4693,8 +4693,7 @@ unsafe fn create_weakref_slot(w_type: pyre_object::PyObjectRef) { } } -/// typeobject.py:1335-1353 find_best_base, with the non-type rejection -/// `Objects/typeobject.c` `best_base` performs in the same loop. +/// typeobject.py:1335-1353 find_best_base. unsafe fn find_best_base( w_bases: pyre_object::PyObjectRef, ) -> Result { @@ -4706,16 +4705,11 @@ unsafe fn find_best_base( let mut w_bestbase: pyre_object::PyObjectRef = std::ptr::null_mut(); for i in 0..len { if let Some(w_candidate) = pyre_object::w_tuple_getitem(w_bases, i as i64) { - // `best_base` rejects a non-type entry rather than skipping - // it. Skipping is not merely a lost diagnostic: `compute_mro` - // appends every supplied base, so a plain instance reaches - // the MRO and attribute lookup then reads it as a type - // layout. `__bases__` assignment screens its tuple before - // this and keeps its own message. + // typeobject.py:1341-1342 — a non-type base is skipped here, + // not rejected: it is a classic base, and `get_mro` walks it + // through `abstract_mro` when the C3 merge reaches it. if !pyre_object::is_type(w_candidate) { - return Err(crate::PyError::type_error( - "bases must be types".to_string(), - )); + continue; } // typeobject.py:1343-1345 — a custom metaclass mro() may // expose the nascent type before its MRO is installed, but From ec89b9d7b2c8607d13dabba5c9253a82c5757fa7 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 31 Jul 2026 22:21:59 +0900 Subject: [PATCH 09/16] types: root the union substitution fold across each __or__ `union_getitem` reduced `subs_parameters`' result with `curr |= newargs[i]` while both the accumulator and the remaining members lived in a plain Rust `Vec`: `subs_parameters` releases its root scope on the way out, and every `|` dispatches `__or__`/`__ror__` and allocates a fresh union, so from the first step on the loop held pre-relocation addresses. _pypy_generic_alias.py:315-323 folds an app-level `newargs` list the collector traces. Pin the members and reread the accumulator and the next member from their slots after each step. Assisted-by: Claude --- pyre/pyre-interpreter/src/typedef.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index a81cad4cef7..777b7e67c26 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -8760,12 +8760,27 @@ fn union_getitem(args: &[PyObjectRef]) -> crate::PyResult { pyre_object::w_tuple_new(vec![]), )); } - // `curr = newargs[0]; for i in range(1, ...): curr |= newargs[i]`. - let mut curr = newargs[0]; - for &next in &newargs[1..] { - curr = crate::objspace::descroperation::or_(curr, next)?; - } - Ok(curr) + // `curr = newargs[0]; for i in range(1, len(newargs)): curr |= newargs[i]`. + // + // `newargs` is a plain Rust `Vec` — `subs_parameters` released its root + // scope on the way out — while every `|` dispatches `__or__`/`__ror__` and + // allocates a fresh union. PyPy's `newargs` is an app-level list the + // collector traces, so pin the members and reread both the accumulator and + // the next member from their slots after each step. + let _roots = pyre_object::gc_roots::push_roots(); + let base = pyre_object::gc_roots::shadow_stack_len(); + for &arg in &newargs { + pyre_object::gc_roots::pin_root(arg); + } + pyre_object::gc_roots::pin_root(pyre_object::gc_roots::shadow_stack_get(base)); + let curr_slot = pyre_object::gc_roots::shadow_stack_len() - 1; + for i in 1..newargs.len() { + let next = pyre_object::gc_roots::shadow_stack_get(base + i); + let curr = pyre_object::gc_roots::shadow_stack_get(curr_slot); + let joined = crate::objspace::descroperation::or_(curr, next)?; + pyre_object::gc_roots::shadow_stack_set(curr_slot, joined); + } + Ok(pyre_object::gc_roots::shadow_stack_get(curr_slot)) } /// `UnionType.__class_getitem__(items)` — `typing.Union` is bound to this From 57b8f38bcf89112673ec432973dd8f3551a4e120 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 31 Jul 2026 22:21:59 +0900 Subject: [PATCH 10/16] struct: reread the iter_unpack roots after the buffer hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `unpack_iter` pinned `format` and `buffer` and then kept reading the values it had captured before `readbuf` — which may dispatch a Python-level `__buffer__` hook — so `buffer_export_incref` and the iterator's `format` field both named pre-relocation addresses. Read both slots back after the hooks instead, as the `buffer` field already did. Assisted-by: Claude --- pyre/pyre-interpreter/src/module/struct/mod.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pyre/pyre-interpreter/src/module/struct/mod.rs b/pyre/pyre-interpreter/src/module/struct/mod.rs index 5c292fc9d1e..01c693ca42b 100644 --- a/pyre/pyre-interpreter/src/module/struct/mod.rs +++ b/pyre/pyre-interpreter/src/module/struct/mod.rs @@ -1360,9 +1360,7 @@ pub mod unpack_iter { let sp = pyre_object::gc_roots::shadow_stack_len(); pyre_object::gc_roots::pin_root(format); pyre_object::gc_roots::pin_root(buffer); - let r_format = pyre_object::gc_roots::shadow_stack_get(sp); - let r_buffer = pyre_object::gc_roots::shadow_stack_get(sp + 1); - let buf = unsafe { readbuf(r_buffer)? }; + let buf = unsafe { readbuf(pyre_object::gc_roots::shadow_stack_get(sp + 1))? }; if size <= 0 { return Err(struct_error(format!( "cannot iteratively unpack with a struct of length {size}" @@ -1373,13 +1371,19 @@ pub mod unpack_iter { "iterative unpacking requires a buffer of a multiple of {size} bytes" ))); } - let export_active = unsafe { crate::builtins::buffer_export_incref(r_buffer) }; + let export_active = unsafe { + crate::builtins::buffer_export_incref(pyre_object::gc_roots::shadow_stack_get(sp + 1)) + }; + // Both slots are reread here rather than reused from a local: `readbuf` + // and `buffer_export_incref` above can each run Python and collect, and + // `allocate_stable` itself allocates, so a value captured before them + // would name the pre-relocation address. let w_iter = W_UnpackIter::allocate_stable(W_UnpackIter { ob: pyre_object::PyObject { ob_type: std::ptr::null(), w_class: std::ptr::null_mut(), }, - format: r_format, + format: pyre_object::gc_roots::shadow_stack_get(sp), buffer: pyre_object::gc_roots::shadow_stack_get(sp + 1), size, index: 0, From 465bcf49751843894d18b598373daa046ba31e82 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 31 Jul 2026 22:45:45 +0900 Subject: [PATCH 11/16] baseobjspace: walk classic bases at compute_mro, not at the pre-flight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `setup_user_defined_type` runs `check_and_find_best_base` (typeobject.py:1519) before `compute_mro` (typeobject.py:1560), so an unacceptable base type or a layout conflict is reported without any classic base's `__bases__` ever executing. pyre's `validate_c3_mro` pre-flight runs ahead of the best-base check, and putting `get_mro`'s classic branch there inverted that pair: `type('C', (bool, object()), {})` reported the classic base's `AttributeError` instead of "type 'bool' is not an acceptable base type". Gate the classic walk on a `walk_classic_bases` argument. The pre-flight passes `false` and stays a pure C3 check; the two calls standing in for `compute_mro` — `compute_and_set_mro` and the default `type()` path's `compute_default_mro`, which cannot raise on its own — pass `true`. Assisted-by: Claude --- pyre/pyre-interpreter/src/baseobjspace.rs | 38 ++++++++++++++--------- pyre/pyre-interpreter/src/builtins.rs | 2 +- pyre/pyre-interpreter/src/call.rs | 7 ++++- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 5c11d42371b..e2501ffe964 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -9281,7 +9281,7 @@ pub(crate) unsafe fn compute_and_set_mro(w_self: PyObjectRef) -> PyResult { // low-level Vec-returning helper cannot carry that exception, so preserve // the same ordering through its fallible validation front door. let w_bases = pyre_object::typeobject::w_type_get_bases(w_self); - validate_c3_mro(w_bases)?; + validate_c3_mro(w_bases, true)?; let default_mro = compute_default_mro(w_self); if pyre_object::w_type_is_heaptype(w_self) { let w_metaclass = (*w_self).w_class; @@ -9393,7 +9393,17 @@ unsafe fn abstract_mro(w_klass: PyObjectRef) -> Result, crate::PyErro /// Type construction calls this before allocating the new type or running /// descriptor/class-subclass hooks, so an invalid hierarchy cannot escape as /// a later and unrelated lookup failure. -pub unsafe fn validate_c3_mro(bases: PyObjectRef) -> Result<(), crate::PyError> { +/// +/// `walk_classic_bases` selects `get_mro`'s classic branch, which reads a +/// non-type base's `__bases__` and so can run Python. Only the call standing +/// in for `compute_mro` (typeobject.py:1560) passes `true`: that one runs after +/// `check_and_find_best_base` (typeobject.py:1519), so a bad type base is still +/// reported before any classic base's `__bases__` executes. The early +/// pre-flight passes `false` and stays a pure C3 check. +pub unsafe fn validate_c3_mro( + bases: PyObjectRef, + walk_classic_bases: bool, +) -> Result<(), crate::PyError> { if bases.is_null() || !is_tuple(bases) { return Ok(()); } @@ -9426,17 +9436,17 @@ pub unsafe fn validate_c3_mro(bases: PyObjectRef) -> Result<(), crate::PyError> } // typeobject.py:1519,1560 — `setup_user_defined_type` runs // `check_and_find_best_base` before it reaches `compute_mro`, so the C3 - // merge only ever sees a tuple that already holds at least one type. pyre - // runs this validation ahead of the best-base check, so reproduce that - // precondition here: with no type among the bases, leave the tuple to - // `check_and_find_best_base` and its own message. - let has_type_base = (0..n).any(|i| { - w_tuple_getitem( - pyre_object::gc_roots::shadow_stack_get(bases_slot), - i as i64, - ) - .is_some_and(|base| is_type_like_w(base)) - }); + // merge only ever sees a tuple that already holds at least one type. With + // no type among the bases the tuple belongs to `check_and_find_best_base` + // and its own message, not to the classic walk. + let walk_classic_bases = walk_classic_bases + && (0..n).any(|i| { + w_tuple_getitem( + pyre_object::gc_roots::shadow_stack_get(bases_slot), + i as i64, + ) + .is_some_and(|base| is_type_like_w(base)) + }); // typeobject.py:1689-1690 `orderlists = [get_mro(space, base) for base in // cls.bases_w]` then `orderlists.append([cls] + cls.bases_w)`. The @@ -9462,7 +9472,7 @@ pub unsafe fn validate_c3_mro(bases: PyObjectRef) -> Result<(), crate::PyError> (*mro).to_vec() }; list_slots.push(entry.into_iter().map(pin_slot).collect()); - } else if has_type_base { + } else if walk_classic_bases { list_slots.push(abstract_mro(base)?); } bases_slots.push(pin_slot( diff --git a/pyre/pyre-interpreter/src/builtins.rs b/pyre/pyre-interpreter/src/builtins.rs index 1ededf8ec1a..73b4b2e3b44 100644 --- a/pyre/pyre-interpreter/src/builtins.rs +++ b/pyre/pyre-interpreter/src/builtins.rs @@ -4820,7 +4820,7 @@ fn type_descr_new_with_metaclass( // This is type.__new__'s own construction path. A different winning // metaclass above received the original bases without a C3 pre-check. - unsafe { crate::baseobjspace::validate_c3_mro(w_effective_bases)? }; + unsafe { crate::baseobjspace::validate_c3_mro(w_effective_bases, false)? }; let _dict_root = pyre_object::gc_roots::push_roots(); let dict_root = pyre_object::gc_roots::shadow_stack_len(); diff --git a/pyre/pyre-interpreter/src/call.rs b/pyre/pyre-interpreter/src/call.rs index cf14971376a..c1bcc85b9a4 100644 --- a/pyre/pyre-interpreter/src/call.rs +++ b/pyre/pyre-interpreter/src/call.rs @@ -3861,7 +3861,7 @@ fn build_class_inner( // A custom metaclass owns its bases until (and unless) it invokes // type.__new__; do not perform type's C3 validation before dispatch. if w_metaclass.is_none() { - unsafe { crate::baseobjspace::validate_c3_mro(w_effective_bases)? }; + unsafe { crate::baseobjspace::validate_c3_mro(w_effective_bases, false)? }; } // Create class via metaclass or default type() // PyPy: typeobject.py — metaclass(name, bases, dict_w) or type.__new__ @@ -4032,6 +4032,11 @@ fn build_class_inner( unsafe { (*w).w_class = crate::typedef::w_type(); } + // typeobject.py:1560 `compute_mro(w_self)`, reached only once + // `check_and_find_best_base` inside `create_all_slots` above accepted + // the tuple. `compute_default_mro` cannot raise, so `get_mro`'s + // classic branch runs through the fallible validation here. + unsafe { crate::baseobjspace::validate_c3_mro(w_effective_bases, true)? }; let mro = unsafe { crate::baseobjspace::compute_default_mro(w) }; unsafe { pyre_object::w_type_set_mro(w, mro) }; // typeobject.py:373-377 ready() — register self on each base's From f8289fb2174042904ba2f5ac44577406c3fe0cbf Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 31 Jul 2026 23:14:14 +0900 Subject: [PATCH 12/16] types: reread the SimpleNamespace __replace__ result across each update `simple_namespace_replace` pinned the constructed namespace and then kept the value it had captured, but the `__dict__` lookup and each `namespace_update_dict` in between can run Python and collect: the second update and the return both named the pre-relocation address. The function's other operands already reread their slots; do the same for the result. Assisted-by: Claude --- pyre/pyre-interpreter/src/module/sys/vm.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pyre/pyre-interpreter/src/module/sys/vm.rs b/pyre/pyre-interpreter/src/module/sys/vm.rs index aa894d954d8..9af5d4c0a90 100644 --- a/pyre/pyre-interpreter/src/module/sys/vm.rs +++ b/pyre/pyre-interpreter/src/module/sys/vm.rs @@ -446,9 +446,11 @@ fn simple_namespace_replace(args: &[PyObjectRef]) -> crate::PyResult { &[], )?; pyre_object::gc_roots::pin_root(result); - let result = pyre_object::gc_roots::shadow_stack_get( - sp + 2 + usize::from(kwargs.is_some()), - ); + // Reread from this slot at every use below: the `__dict__` lookup and each + // `namespace_update_dict` can run Python and collect, so a local captured + // here would name the pre-relocation address. + let result_slot = sp + 2 + usize::from(kwargs.is_some()); + let result = pyre_object::gc_roots::shadow_stack_get(result_slot); let result_type = crate::typedef::r#type(result) .map(|tp| tp.as_ptr()) .unwrap_or(PY_NULL); @@ -475,7 +477,7 @@ fn simple_namespace_replace(args: &[PyObjectRef]) -> crate::PyResult { )?; pyre_object::gc_roots::pin_root(source_dict); namespace_update_dict( - result, + pyre_object::gc_roots::shadow_stack_get(result_slot), pyre_object::gc_roots::shadow_stack_get( sp + 3 + usize::from(kwargs.is_some()), ), @@ -483,12 +485,12 @@ fn simple_namespace_replace(args: &[PyObjectRef]) -> crate::PyResult { )?; if kwargs.is_some() { namespace_update_dict( - result, + pyre_object::gc_roots::shadow_stack_get(result_slot), pyre_object::gc_roots::shadow_stack_get(sp + 1), true, )?; } - Ok(result) + Ok(pyre_object::gc_roots::shadow_stack_get(result_slot)) } /// `pypy/module/sys/vm.py:217 space.getexecutioncontext()` access for From 3c234870bb982adc80a05393e64ad20507164327 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sat, 1 Aug 2026 00:46:03 +0900 Subject: [PATCH 13/16] types: root the nascent type and MRO snapshots across type creation build_class_inner left the freshly created W_TypeObject in an untraced Rust local across create_all_slots, the classic-base C3 validation and every __set_name__ call, and held the class-dict snapshot those calls iterate in a plain Vec. compute_and_set_mro did the same for w_self and kept the default MRO plus a metaclass mro() result in Vecs across the Python calls that follow. Pin each of them and reread from the slot after every step that can collect. simple_namespace_repr reused a key read from before the getitem that fetches its value; simple_namespace_richcompare had no root scope around its two __dict__ lookups. Assisted-by: Claude --- pyre/pyre-interpreter/src/baseobjspace.rs | 41 +++++++++++++++------- pyre/pyre-interpreter/src/call.rs | 28 +++++++++++++-- pyre/pyre-interpreter/src/module/sys/vm.rs | 32 +++++++++++++---- 3 files changed, 80 insertions(+), 21 deletions(-) diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index e2501ffe964..da7c5e9e027 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -9280,15 +9280,30 @@ pub(crate) unsafe fn compute_and_set_mro(w_self: PyObjectRef) -> PyResult { // PyPy's C3 merge raises here when the bases are inconsistent; pyre's // low-level Vec-returning helper cannot carry that exception, so preserve // the same ordering through its fallible validation front door. + // The validation and the metaclass `mro()` below both execute Python. A + // type reaching `type.__new__` has no referrer yet beyond this argument, + // and the MRO snapshots are untraced Rust Vecs, so root all of them and + // reread every use that crosses one of those calls. + let _roots = pyre_object::gc_roots::push_roots(); + let self_slot = pin_slot(w_self); let w_bases = pyre_object::typeobject::w_type_get_bases(w_self); validate_c3_mro(w_bases, true)?; + let w_self = pyre_object::gc_roots::shadow_stack_get(self_slot); let default_mro = compute_default_mro(w_self); + let default_mro_start = pyre_object::gc_roots::shadow_stack_len(); + let default_mro_len = default_mro.len(); + for w_class in default_mro { + pyre_object::gc_roots::pin_root(w_class); + } + let default_mro = + |index: usize| pyre_object::gc_roots::shadow_stack_get(default_mro_start + index); if pyre_object::w_type_is_heaptype(w_self) { let w_metaclass = (*w_self).w_class; if !w_metaclass.is_null() { if let Some((w_where, w_mro_func)) = lookup_where_with_method_cache(w_metaclass, "mro") { if !std::ptr::eq(w_where, crate::typedef::w_type()) { + let w_self = pyre_object::gc_roots::shadow_stack_get(self_slot); let w_mro = get_and_call_function(w_mro_func, w_self, w_metaclass, &[])?; let mro_w = crate::builtins::collect_iterable(w_mro)?; // `fixedview` keeps PyPy's items GC-visible through the @@ -9307,27 +9322,28 @@ pub(crate) unsafe fn compute_and_set_mro(w_self: PyObjectRef) -> PyResult { return Err(PyError::type_error("mro() returned a non-class")); } } - let mro_w: Vec<_> = (0..mro_w.len()) - .map(|index| { - pyre_object::gc_roots::shadow_stack_get(mro_root_start + index) - }) - .collect(); - if !mro_w.iter().any(|&entry| std::ptr::eq(entry, w_self)) { + let mro_len = mro_w.len(); + let mro_at = |index: usize| { + pyre_object::gc_roots::shadow_stack_get(mro_root_start + index) + }; + let w_self = pyre_object::gc_roots::shadow_stack_get(self_slot); + if !(0..mro_len).any(|index| std::ptr::eq(mro_at(index), w_self)) { return Err(PyError::type_error( "mro() returned a result without the new class", )); } - pyre_object::w_type_set_mro(w_self, mro_w.clone()); + pyre_object::w_type_set_mro(w_self, (0..mro_len).map(mro_at).collect()); // typeobject.py `_add_mro_classes_as_subclasses`: custom // MRO entries outside the default hierarchy participate // in invalidation just like real bases. - for w_ancestor in mro_w { - if !default_mro - .iter() - .any(|&default| std::ptr::eq(default, w_ancestor)) + for index in 0..mro_len { + let w_ancestor = mro_at(index); + if !(0..default_mro_len) + .any(|default| std::ptr::eq(default_mro(default), w_ancestor)) && pyre_object::is_type(w_ancestor) { + let w_self = pyre_object::gc_roots::shadow_stack_get(self_slot); pyre_object::typeobject::w_type_add_subclass(w_ancestor, w_self); } } @@ -9336,7 +9352,8 @@ pub(crate) unsafe fn compute_and_set_mro(w_self: PyObjectRef) -> PyResult { } } } - pyre_object::w_type_set_mro(w_self, default_mro); + let w_self = pyre_object::gc_roots::shadow_stack_get(self_slot); + pyre_object::w_type_set_mro(w_self, (0..default_mro_len).map(default_mro).collect()); Ok(pyre_object::w_none()) } diff --git a/pyre/pyre-interpreter/src/call.rs b/pyre/pyre-interpreter/src/call.rs index c1bcc85b9a4..533e1d106f4 100644 --- a/pyre/pyre-interpreter/src/call.rs +++ b/pyre/pyre-interpreter/src/call.rs @@ -4024,11 +4024,18 @@ fn build_class_inner( } } let dict_obj = pyre_object::gc_roots::shadow_stack_get(dict_root); + // Slot creation, C3 validation and `__set_name__` below all allocate + // and can execute Python; the nascent type has no other referrer + // until its mro is installed, so keep it rooted and reread it after + // every such step. + let w_root = pyre_object::gc_roots::shadow_stack_len(); let w = pyre_object::w_type_new(name, w_effective_bases, dict_obj as *mut u8); + pyre_object::gc_roots::pin_root(w); crate::builtins::type_new_take_qualname(w, dict_obj)?; // typeobject.py:1143-1204 create_all_slots parity. unsafe { create_all_slots(w, w_effective_bases)? }; // baseobjspace.py:76 — set w_class to 'type' (default metaclass) + let w = pyre_object::gc_roots::shadow_stack_get(w_root); unsafe { (*w).w_class = crate::typedef::w_type(); } @@ -4037,6 +4044,7 @@ fn build_class_inner( // the tuple. `compute_default_mro` cannot raise, so `get_mro`'s // classic branch runs through the fallible validation here. unsafe { crate::baseobjspace::validate_c3_mro(w_effective_bases, true)? }; + let w = pyre_object::gc_roots::shadow_stack_get(w_root); let mro = unsafe { crate::baseobjspace::compute_default_mro(w) }; unsafe { pyre_object::w_type_set_mro(w, mro) }; // typeobject.py:373-377 ready() — register self on each base's @@ -4050,6 +4058,7 @@ fn build_class_inner( // provisional class-body namespace. if let Some(classdictcell_root) = classdictcell_root { let classdictcell = pyre_object::gc_roots::shadow_stack_get(classdictcell_root); + let w = pyre_object::gc_roots::shadow_stack_get(w_root); let type_dict = unsafe { pyre_object::w_type_get_dict_ptr(w) as PyObjectRef }; if !type_dict.is_null() { unsafe { pyre_object::w_cell_set(classdictcell, type_dict) }; @@ -4061,16 +4070,29 @@ fn build_class_inner( // __set_name__. The metaclass path above goes through type.__new__() // which handles __set_name__ in builtins.rs, so we must NOT call it // again there to avoid double invocation. - if unsafe { pyre_object::is_type(w) } { + if unsafe { pyre_object::is_type(pyre_object::gc_roots::shadow_stack_get(w_root)) } { let dict_obj = pyre_object::gc_roots::shadow_stack_get(dict_root); let entries = unsafe { pyre_object::w_dict_items(dict_obj) }; + // Every `__set_name__` runs Python, so the snapshot cannot stay in + // an untraced Vec across the loop. + let _entry_roots = pyre_object::gc_roots::push_roots(); + let entries_root = pyre_object::gc_roots::shadow_stack_len(); + let mut pinned = 0; for (w_name, value) in entries { if !value.is_null() && unsafe { pyre_object::is_str(w_name) } { - unsafe { crate::baseobjspace::set_name(w, w_name, value) }?; + pyre_object::gc_roots::pin_root(w_name); + pyre_object::gc_roots::pin_root(value); + pinned += 1; } } + for i in 0..pinned { + let w_name = pyre_object::gc_roots::shadow_stack_get(entries_root + i * 2); + let value = pyre_object::gc_roots::shadow_stack_get(entries_root + i * 2 + 1); + let w = pyre_object::gc_roots::shadow_stack_get(w_root); + unsafe { crate::baseobjspace::set_name(w, w_name, value) }?; + } } - w + pyre_object::gc_roots::shadow_stack_get(w_root) }; // `_store_type_in_classcell` runs inside type.__new__, which the diff --git a/pyre/pyre-interpreter/src/module/sys/vm.rs b/pyre/pyre-interpreter/src/module/sys/vm.rs index 9af5d4c0a90..e952b21e123 100644 --- a/pyre/pyre-interpreter/src/module/sys/vm.rs +++ b/pyre/pyre-interpreter/src/module/sys/vm.rs @@ -318,14 +318,16 @@ fn simple_namespace_repr(args: &[PyObjectRef]) -> crate::PyResult { Err(err) if err.kind == crate::PyErrorKind::KeyError => continue, Err(err) => return Err(err), }; + let value_sp = pyre_object::gc_roots::shadow_stack_len(); pyre_object::gc_roots::pin_root(value); + // `getitem` above ran a lookup that can collect, so the key has to be + // reread from its slot rather than reused from before the call. + let key = pyre_object::gc_roots::shadow_stack_get(keys_sp + i); parts.push(format!( "{}={}", unsafe { crate::display::py_str(key)? }, unsafe { - crate::display::py_repr(pyre_object::gc_roots::shadow_stack_get( - pyre_object::gc_roots::shadow_stack_len() - 1, - ))? + crate::display::py_repr(pyre_object::gc_roots::shadow_stack_get(value_sp))? } )); } @@ -381,9 +383,27 @@ fn simple_namespace_richcompare( // CPython 3.14 forwards all six operations to the two namespace dicts. // In particular, ordering reaches dict's TypeError instead of returning // NotImplemented from the namespace type itself. - let self_dict = crate::baseobjspace::getattr_str(self_obj, "__dict__")?; - let other_dict = crate::baseobjspace::getattr_str(other, "__dict__")?; - crate::baseobjspace::compare(self_dict, other_dict, op) + // + // A subclass `__getattribute__` runs on each lookup, so both operands and + // the first dict have to survive it. + let _roots = pyre_object::gc_roots::push_roots(); + let sp = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(self_obj); + pyre_object::gc_roots::pin_root(other); + let self_dict = crate::baseobjspace::getattr_str( + pyre_object::gc_roots::shadow_stack_get(sp), + "__dict__", + )?; + pyre_object::gc_roots::pin_root(self_dict); + let other_dict = crate::baseobjspace::getattr_str( + pyre_object::gc_roots::shadow_stack_get(sp + 1), + "__dict__", + )?; + crate::baseobjspace::compare( + pyre_object::gc_roots::shadow_stack_get(sp + 2), + other_dict, + op, + ) } /// CPython 3.14 `namespace_reduce`: `(type(self), (), self.__dict__)`. From 6f323839e48f0db969b8a7a20d2f4431b36d646b Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sat, 1 Aug 2026 00:46:03 +0900 Subject: [PATCH 14/16] types: give UnionType __repr__, __hash__ and __mro_entries__ a fixed arity They were registered through make_builtin_function, which accepts any argument count, so `(int | str).__repr__('x')` returned the repr instead of raising TypeError. Assisted-by: Claude --- pyre/pyre-interpreter/src/typedef.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index 777b7e67c26..d755939a5ee 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -8990,21 +8990,21 @@ fn init_union_type(ns: PyObjectRef) { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, "__repr__", - make_builtin_function("__repr__", union_repr_method), + make_builtin_function_with_arity("__repr__", union_repr_method, 1), ) }; unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, "__hash__", - make_builtin_function("__hash__", union_hash_method), + make_builtin_function_with_arity("__hash__", union_hash_method, 1), ) }; unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, "__mro_entries__", - make_builtin_function("__mro_entries__", union_mro_entries_method), + make_builtin_function_with_arity("__mro_entries__", union_mro_entries_method, 2), ) }; } From 7fd56b59e39ffd55d2805f9ae2ea4d4fb060e725 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sat, 1 Aug 2026 00:51:05 +0900 Subject: [PATCH 15/16] generic alias: expose the 3.14 slot method surface --- .../src/_pypy_generic_alias.rs | 128 +++++++++++++++++- pyre/pyre-interpreter/src/call.rs | 5 +- pyre/pyre-interpreter/src/eval.rs | 39 ++++++ 3 files changed, 165 insertions(+), 7 deletions(-) diff --git a/pyre/pyre-interpreter/src/_pypy_generic_alias.rs b/pyre/pyre-interpreter/src/_pypy_generic_alias.rs index df99a173c89..ee879a1eca5 100644 --- a/pyre/pyre-interpreter/src/_pypy_generic_alias.rs +++ b/pyre/pyre-interpreter/src/_pypy_generic_alias.rs @@ -212,6 +212,72 @@ fn self_alias(args: &[PyObjectRef]) -> Result { Ok(self_) } +/// `GenericAlias.__repr__` (`_pypy_generic_alias.py:57`). +fn ga_repr(args: &[PyObjectRef]) -> crate::PyResult { + let self_ = self_alias(args)?; + Ok(w_str_new(&unsafe { repr(self_)? })) +} + +/// `GenericAlias.__hash__` (`_pypy_generic_alias.py:82`). +fn ga_hash(args: &[PyObjectRef]) -> crate::PyResult { + let self_ = self_alias(args)?; + Ok(w_int_new(crate::builtins::try_hash_value(self_)?)) +} + +/// `GenericAlias.__call__` (`_pypy_generic_alias.py:41-46`). +fn ga_call(args: &[PyObjectRef]) -> crate::PyResult { + let self_ = self_alias(args)?; + let origin = unsafe { w_generic_alias_get_origin(self_) }; + let _roots = pyre_object::gc_roots::push_roots(); + let root_base = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(self_); + pyre_object::gc_roots::pin_root(origin); + let result = crate::builtins::call_forwarding_args( + unsafe { pyre_object::gc_roots::shadow_stack_get(root_base + 1) }, + &args[1..], + )?; + pyre_object::gc_roots::pin_root(result); + crate::call::set_orig_class( + unsafe { pyre_object::gc_roots::shadow_stack_get(root_base + 2) }, + unsafe { pyre_object::gc_roots::shadow_stack_get(root_base) }, + )?; + Ok(unsafe { pyre_object::gc_roots::shadow_stack_get(root_base + 2) }) +} + +/// `GenericAlias.__getattribute__` (`_pypy_generic_alias.py:52-55`). +fn ga_getattribute(args: &[PyObjectRef]) -> crate::PyResult { + let self_ = self_alias(args)?; + let name_obj = args.get(1).copied().unwrap_or_else(w_none); + let name = crate::baseobjspace::text_w(name_obj)?; + if !is_attr_exception(name) && !is_attr_blocked(name) { + let origin = unsafe { w_generic_alias_get_origin(self_) }; + crate::baseobjspace::getattr_str(origin, name) + } else { + crate::baseobjspace::object_getattribute(self_, name) + } +} + +/// `GenericAlias.__iter__` (`_pypy_generic_alias.py:108-109`). +fn ga_iter(args: &[PyObjectRef]) -> crate::PyResult { + let self_ = self_alias(args)?; + let _roots = pyre_object::gc_roots::push_roots(); + pyre_object::gc_roots::pin_root(self_); + let starred = make_starred(self_)?; + let starred_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(starred); + let singleton = w_tuple_new(vec![unsafe { + pyre_object::gc_roots::shadow_stack_get(starred_slot) + }]); + let singleton_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(singleton); + crate::baseobjspace::iter(unsafe { pyre_object::gc_roots::shadow_stack_get(singleton_slot) }) +} + +/// `GenericAlias.__dir__` (`_pypy_generic_alias.py:85-88`). +fn ga_dir(args: &[PyObjectRef]) -> crate::PyResult { + dir_list(self_alias(args)?) +} + /// `GenericAlias.__eq__` (`_pypy_generic_alias.py:64`). fn ga_eq(args: &[PyObjectRef]) -> crate::PyResult { let self_ = args.first().copied().unwrap_or_else(w_none); @@ -231,6 +297,22 @@ fn ga_eq(args: &[PyObjectRef]) -> crate::PyResult { Ok(w_bool_from(eq)) } +/// CPython 3.14's `ga_richcompare`: `!=` is the inverse of the structural +/// equality result; the four ordering operations return `NotImplemented`. +fn ga_ne(args: &[PyObjectRef]) -> crate::PyResult { + let result = ga_eq(args)?; + if unsafe { is_not_implemented(result) } { + Ok(result) + } else { + Ok(w_bool_from(!unsafe { w_bool_get_value(result) })) + } +} + +fn ga_ordering(args: &[PyObjectRef]) -> crate::PyResult { + self_alias(args)?; + Ok(w_not_implemented()) +} + /// `GenericAlias.__mro_entries__` (`_pypy_generic_alias.py:49`) — /// `(self.__origin__,)`, so `class C(list[int])` resolves to `list`. fn ga_mro_entries(args: &[PyObjectRef]) -> crate::PyResult { @@ -907,9 +989,42 @@ pub(crate) fn init_generic_alias_type(ns: PyObjectRef) { make_builtin_function("__eq__", ga_eq), ) }; - // __hash__ and __call__ are resolved at their dispatch points - // (`builtins::hash_value`, `call::call_function_impl_result`) because - // pyre does not consult a typedef slot for them on builtin W_Roots. + for (name, method) in [ + ("__ne__", ga_ne as fn(&[PyObjectRef]) -> crate::PyResult), + ("__lt__", ga_ordering), + ("__le__", ga_ordering), + ("__gt__", ga_ordering), + ("__ge__", ga_ordering), + ] { + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + name, + make_builtin_function_with_arity(name, method, 2), + ) + }; + } + // Pyre's ordinary operation dispatch has native fast paths for these + // slots, but CPython 3.14 and PyPy also expose the methods through the + // GenericAlias type dictionary for unbound calls. + for (name, method, arity) in [ + ( + "__repr__", + ga_repr as fn(&[PyObjectRef]) -> crate::PyResult, + Some(1), + ), + ("__hash__", ga_hash, Some(1)), + ("__call__", ga_call, None), + ("__getattribute__", ga_getattribute, Some(2)), + ("__iter__", ga_iter, Some(1)), + ("__dir__", ga_dir, Some(1)), + ] { + let function = match arity { + Some(arity) => make_builtin_function_with_arity(name, method, arity), + None => make_builtin_function(name, method), + }; + unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy(ns, name, function) }; + } unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, @@ -983,9 +1098,10 @@ pub(crate) fn init_generic_alias_type(ns: PyObjectRef) { ), ) }; - // `__iter__` and `__dir__` are intercepted directly by `baseobjspace::iter` - // and `builtins::builtin_dir`; explicit `ga.__iter__`/`ga.__dir__` access - // delegates to `__origin__` (they are not in `_ATTR_EXCEPTIONS`). + // Instance attribute access for `ga.__iter__`/`ga.__dir__` still delegates + // to `__origin__` because they are not in `_ATTR_EXCEPTIONS`; the typedef + // entries above provide CPython's `types.GenericAlias.__iter__(ga)` and + // `types.GenericAlias.__dir__(ga)` unbound-call surface. } /// Render a GenericAlias for `repr()` (`GenericAlias.__repr__`, diff --git a/pyre/pyre-interpreter/src/call.rs b/pyre/pyre-interpreter/src/call.rs index 533e1d106f4..c651cab6831 100644 --- a/pyre/pyre-interpreter/src/call.rs +++ b/pyre/pyre-interpreter/src/call.rs @@ -753,7 +753,10 @@ pub fn call_user_function_resolved( /// `__origin__`, set `result.__orig_class__ = self`. This is wrapped in /// `try: ... except (AttributeError, TypeError): pass`, so only those two /// errors are swallowed; anything else propagates. -fn set_orig_class(result: PyObjectRef, alias: PyObjectRef) -> Result<(), crate::PyError> { +pub(crate) fn set_orig_class( + result: PyObjectRef, + alias: PyObjectRef, +) -> Result<(), crate::PyError> { match crate::baseobjspace::setattr_str(result, "__orig_class__", alias) { Ok(_) => Ok(()), Err(e) diff --git a/pyre/pyre-interpreter/src/eval.rs b/pyre/pyre-interpreter/src/eval.rs index b84df72ca3e..a556d96b090 100644 --- a/pyre/pyre-interpreter/src/eval.rs +++ b/pyre/pyre-interpreter/src/eval.rs @@ -6855,6 +6855,45 @@ result = c.f([1, 2, 3])"; } } + #[test] + fn test_generic_alias_type_exposes_cpython_314_slot_methods() { + let source = r#" +GA = type(list[int]) +required = { + '__repr__', '__hash__', '__call__', + '__getattribute__', '__iter__', '__dir__', + '__ne__', '__lt__', '__le__', '__gt__', '__ge__', +} +assert required <= GA.__dict__.keys() +assert GA.__repr__(list[int]) == 'list[int]' +assert GA.__hash__(list[int]) == hash(list[int]) +assert GA.__getattribute__(list[int], '__origin__') is list +assert list(GA.__iter__(tuple[int, str])) == [(*tuple[int, str],)[0]] +assert '__origin__' in GA.__dir__(list[int]) +assert '__bases__' not in GA.__dir__(list[int]) + +class C: + def __init__(self, *, value): + self.value = value + +alias = GA(C, int) +instance = GA.__call__(alias, value=42) +assert instance.value == 42 +assert instance.__orig_class__ is alias + +# Instance lookup still follows PyPy GenericAlias.__getattribute__: names +# outside _ATTR_EXCEPTIONS delegate to the origin despite the type-dict rows. +assert list[int].__repr__ is list.__repr__ +assert list[int].__hash__ is list.__hash__ +assert GA.__ne__(list[int], list[str]) is True +assert GA.__ne__(list[int], list[int]) is False +assert GA.__ne__(list[int], list) is NotImplemented +assert GA.__lt__(list[int], list[str]) is NotImplemented +"#; + let (result, _frame) = run_exec_frame(source); + result.expect("GenericAlias explicit slot-method surface failed"); + } + /// `pypy/interpreter/typedef.py BuiltinFunction.typedef.acceptable_as_base_class /// = False` plus CPython 3.14's null `tp_new` enforce that `type(len)()` /// raises `TypeError("cannot create 'builtin_function_or_method' From a231d3a7f7af50157a698269b1244cf4eba61bd3 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sat, 1 Aug 2026 01:13:13 +0900 Subject: [PATCH 16/16] types: root the bases tuple and reject a foreign __replace__ receiver build_class_inner captured the effective bases tuple once and reused that address after create_all_slots, which unpacks __slots__ and can therefore run Python. Pin the tuple and reread it at w_type_new, create_all_slots, the classic-base validation and __init_subclass__ dispatch. simple_namespace_replace called type(self)() before checking anything, so an unbound call such as types.SimpleNamespace.__replace__(Foreign()) ran a foreign constructor. Reject a non-namespace receiver up front with namespace___replace__'s descriptor message. Assisted-by: Claude --- pyre/pyre-interpreter/src/call.rs | 27 ++++++++++++++++++---- pyre/pyre-interpreter/src/module/sys/vm.rs | 13 +++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/pyre/pyre-interpreter/src/call.rs b/pyre/pyre-interpreter/src/call.rs index c651cab6831..a8ab0b7bb9a 100644 --- a/pyre/pyre-interpreter/src/call.rs +++ b/pyre/pyre-interpreter/src/call.rs @@ -3861,9 +3861,15 @@ fn build_class_inner( } else { bases }; + // The C3 validations read `__bases__` off classic bases and + // `create_all_slots` unpacks `__slots__`; both execute Python, so the + // tuple cannot stay in an untraced local across them. + let bases_root = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(w_effective_bases); // A custom metaclass owns its bases until (and unless) it invokes // type.__new__; do not perform type's C3 validation before dispatch. if w_metaclass.is_none() { + let w_effective_bases = pyre_object::gc_roots::shadow_stack_get(bases_root); unsafe { crate::baseobjspace::validate_c3_mro(w_effective_bases, false)? }; } // Create class via metaclass or default type() @@ -4032,11 +4038,15 @@ fn build_class_inner( // until its mro is installed, so keep it rooted and reread it after // every such step. let w_root = pyre_object::gc_roots::shadow_stack_len(); - let w = pyre_object::w_type_new(name, w_effective_bases, dict_obj as *mut u8); + let w = pyre_object::w_type_new( + name, + pyre_object::gc_roots::shadow_stack_get(bases_root), + dict_obj as *mut u8, + ); pyre_object::gc_roots::pin_root(w); crate::builtins::type_new_take_qualname(w, dict_obj)?; // typeobject.py:1143-1204 create_all_slots parity. - unsafe { create_all_slots(w, w_effective_bases)? }; + unsafe { create_all_slots(w, pyre_object::gc_roots::shadow_stack_get(bases_root))? }; // baseobjspace.py:76 — set w_class to 'type' (default metaclass) let w = pyre_object::gc_roots::shadow_stack_get(w_root); unsafe { @@ -4046,7 +4056,12 @@ fn build_class_inner( // `check_and_find_best_base` inside `create_all_slots` above accepted // the tuple. `compute_default_mro` cannot raise, so `get_mro`'s // classic branch runs through the fallible validation here. - unsafe { crate::baseobjspace::validate_c3_mro(w_effective_bases, true)? }; + unsafe { + crate::baseobjspace::validate_c3_mro( + pyre_object::gc_roots::shadow_stack_get(bases_root), + true, + )? + }; let w = pyre_object::gc_roots::shadow_stack_get(w_root); let mro = unsafe { crate::baseobjspace::compute_default_mro(w) }; unsafe { pyre_object::w_type_set_mro(w, mro) }; @@ -4169,7 +4184,11 @@ fn build_class_inner( }, _ => Vec::new(), }; - call_init_subclass_on_bases(w_type, w_effective_bases, &init_subclass_kwargs)?; + call_init_subclass_on_bases( + w_type, + pyre_object::gc_roots::shadow_stack_get(bases_root), + &init_subclass_kwargs, + )?; } Ok(w_type) diff --git a/pyre/pyre-interpreter/src/module/sys/vm.rs b/pyre/pyre-interpreter/src/module/sys/vm.rs index e952b21e123..31771524848 100644 --- a/pyre/pyre-interpreter/src/module/sys/vm.rs +++ b/pyre/pyre-interpreter/src/module/sys/vm.rs @@ -458,6 +458,19 @@ fn simple_namespace_replace(args: &[PyObjectRef]) -> crate::PyResult { let self_type = crate::typedef::r#type(self_obj) .map(|tp| tp.as_ptr()) .unwrap_or(PY_NULL); + // `namespace___replace__` is a method on the namespace type, so a foreign + // receiver is rejected by the descriptor before anything else runs — in + // particular before `type(self)()` could execute a foreign constructor. + if !unsafe { crate::baseobjspace::issubtype_w(self_type, simple_namespace_type()) } { + let received = if self_type.is_null() { + "object".to_string() + } else { + unsafe { crate::baseobjspace::type_fully_qualified_name(self_type) } + }; + return Err(crate::PyError::type_error(format!( + "descriptor '__replace__' for 'types.SimpleNamespace' objects doesn't apply to a '{received}' object" + ))); + } pyre_object::gc_roots::pin_root(self_type); let result = crate::call::call_function_impl_result( pyre_object::gc_roots::shadow_stack_get(