diff --git a/pyre/pyre-interpreter/src/_pypy_generic_alias.rs b/pyre/pyre-interpreter/src/_pypy_generic_alias.rs index 2e6628f2a46..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 { @@ -366,13 +448,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 +487,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 +516,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`) — @@ -824,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, @@ -900,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/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 34d94cbb83f..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)?; + 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,23 +9352,88 @@ 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()) } +/// 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 /// 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(()); } 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 +9451,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. 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 + // 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 walk_classic_bases { + 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/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 aeba29e0e92..a8ab0b7bb9a 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) @@ -3858,10 +3861,16 @@ 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() { - unsafe { crate::baseobjspace::validate_c3_mro(w_effective_bases)? }; + 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() // PyPy: typeobject.py — metaclass(name, bases, dict_w) or type.__new__ @@ -4024,14 +4033,36 @@ fn build_class_inner( } } let dict_obj = pyre_object::gc_roots::shadow_stack_get(dict_root); - let w = pyre_object::w_type_new(name, w_effective_bases, dict_obj as *mut u8); + // 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, + 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 { (*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( + 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) }; // typeobject.py:373-377 ready() — register self on each base's @@ -4045,6 +4076,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) }; @@ -4056,16 +4088,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 @@ -4139,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) @@ -4705,6 +4754,9 @@ 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) { + // 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) { continue; } 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' diff --git a/pyre/pyre-interpreter/src/module/struct/mod.rs b/pyre/pyre-interpreter/src/module/struct/mod.rs index fed00ea8c06..01c693ca42b 100644 --- a/pyre/pyre-interpreter/src/module/struct/mod.rs +++ b/pyre/pyre-interpreter/src/module/struct/mod.rs @@ -1352,7 +1352,15 @@ 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 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}" @@ -1363,14 +1371,20 @@ 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(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, - buffer, + format: pyre_object::gc_roots::shadow_stack_get(sp), + buffer: pyre_object::gc_roots::shadow_stack_get(sp + 1), size, index: 0, export_active, diff --git a/pyre/pyre-interpreter/src/module/sys/vm.rs b/pyre/pyre-interpreter/src/module/sys/vm.rs index 542c56ce878..31771524848 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,109 @@ 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), + }; + 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(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(value_sp))? + } )); } - 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_richcompare(args: &[PyObjectRef], name: &str, negate: bool) -> crate::PyResult { +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, + 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 +374,156 @@ 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. - 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)) + // 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. + // + // 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__)`. +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); + // `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( + sp + 1 + usize::from(kwargs.is_some()), + ), + &[], + )?; + pyre_object::gc_roots::pin_root(result); + // 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); + 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( + pyre_object::gc_roots::shadow_stack_get(result_slot), + pyre_object::gc_roots::shadow_stack_get( + sp + 3 + usize::from(kwargs.is_some()), + ), + false, + )?; + if kwargs.is_some() { + namespace_update_dict( + pyre_object::gc_roots::shadow_stack_get(result_slot), + pyre_object::gc_roots::shadow_stack_get(sp + 1), + true, + )?; + } + Ok(pyre_object::gc_roots::shadow_stack_get(result_slot)) } /// `pypy/module/sys/vm.py:217 space.getexecutioncontext()` access for diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index a1b4135c53f..d755939a5ee 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)?; + // `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(curr) + Ok(pyre_object::gc_roots::shadow_stack_get(curr_slot)) } /// `UnionType.__class_getitem__(items)` — `typing.Union` is bound to this @@ -8790,6 +8805,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 +8984,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_with_arity("__repr__", union_repr_method, 1), + ) + }; + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + "__hash__", + 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_with_arity("__mro_entries__", union_mro_entries_method, 2), + ) + }; } static GETSET_DESCRIPTOR_TYPE: std::sync::OnceLock = std::sync::OnceLock::new(); @@ -24765,7 +24849,16 @@ fn product_descr_new(args: &[PyObjectRef]) -> Result