From ee6745a8da077cdb2c476ec6fc0a8939edff22a5 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 16 Aug 2026 19:54:07 +0900 Subject: [PATCH 1/2] display, type_methods, opcode_ops: root six walks across the Python they run `dict_repr` and the dict-view repr arm iterated a native snapshot `Vec`, whose entries the collector does not walk, while each key's and value's `__repr__` ran Python. The exception-args arm walked the tuple `w_exception_get_args` mints on every call. That header is old-gen and does not move, but nothing rooted it, so the collector never traced it and its element slots kept the pre-move addresses of any argument an item's `__repr__` relocated. `dict_view_snapshot`'s `Items` arm called `w_tuple_new` once per pair with the remaining pairs, and the tuples already built, unrooted. `dict_view_all_contained_in` walked its snapshot across `contains`, which runs a user `__contains__`/`__eq__`. `match_class_value` accumulated extracted attributes in a native `Vec` across the `getattr` calls that produce them, and re-read `subject` from a copy taken before `isinstance`. `repr_items_list` re-fetched by index from a `list` local held across each element's repr, and a `W_ListObject` header moves. Each site now pins its values and reads them back from the shadow stack at the point of use. Assisted-by: Claude --- .../src/_pypy_generic_alias.rs | 10 ++++- pyre/pyre-interpreter/src/display.rs | 39 ++++++++++++++---- pyre/pyre-interpreter/src/opcode_ops.rs | 40 ++++++++++++++----- pyre/pyre-interpreter/src/type_methods.rs | 23 +++++++++-- pyre/pyre-interpreter/src/typedef.rs | 11 ++++- 5 files changed, 100 insertions(+), 23 deletions(-) diff --git a/pyre/pyre-interpreter/src/_pypy_generic_alias.rs b/pyre/pyre-interpreter/src/_pypy_generic_alias.rs index 8407acd26e8..d48d18660dc 100644 --- a/pyre/pyre-interpreter/src/_pypy_generic_alias.rs +++ b/pyre/pyre-interpreter/src/_pypy_generic_alias.rs @@ -1489,10 +1489,16 @@ fn join_wtf8(parts: &[rustpython_wtf8::Wtf8Buf], sep: &str) -> rustpython_wtf8:: /// after its predecessor's repr so mutation during a callback raises /// `IndexError` rather than reading stale storage. unsafe fn repr_items_list(list: PyObjectRef) -> Result { - let n = w_list_len(list); + // An element's repr runs Python, and a `W_ListObject` header moves, so the + // list is re-read from the shadow stack before every fetch. + let _roots = pyre_object::gc_roots::push_roots(); + let list_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(list); + let list = || pyre_object::gc_roots::shadow_stack_get(list_slot); + let n = w_list_len(list()); let mut parts = Vec::with_capacity(n); for i in 0..n { - let item = w_list_getitem(list, i as i64) + let item = w_list_getitem(list(), i as i64) .ok_or_else(|| crate::PyError::index_error("list index out of range"))?; parts.push(repr_item(item)?); } diff --git a/pyre/pyre-interpreter/src/display.rs b/pyre/pyre-interpreter/src/display.rs index 125ee9fda7f..acd50b9f1ca 100644 --- a/pyre/pyre-interpreter/src/display.rs +++ b/pyre/pyre-interpreter/src/display.rs @@ -251,17 +251,26 @@ pub unsafe fn dict_repr(obj: PyObjectRef) -> Result { return Ok(Wtf8Buf::from_string("{...}".to_string())); }; let entries = pyre_object::w_dict_items(obj); + // The pairs live in a native Vec the collector does not walk, and every + // key's and value's `__repr__` runs Python. Pin them and read each one + // back at its use, the way `list_repr` re-reads its container: a copy + // taken before a collection addresses the pre-move object. + let _roots = pyre_object::gc_roots::push_roots(); + let flat: Vec = entries.iter().flat_map(|&(k, v)| [k, v]).collect(); + let pair_base = pyre_object::gc_roots::pin_roots(&flat); let mut out = Wtf8Buf::new(); out.push_str("{"); - for (i, (k, v)) in entries.into_iter().enumerate() { + for i in 0..entries.len() { // `dictmultiobject.py:388` joins the pairs by position, so a key or // value whose `__repr__` answers `""` still gets its separator. if i != 0 { out.push_str(", "); } - out.push_wtf8(&py_repr_wtf8(k)?); + let key = pyre_object::gc_roots::shadow_stack_get(pair_base + i * 2); + out.push_wtf8(&py_repr_wtf8(key)?); out.push_str(": "); - out.push_wtf8(&py_repr_wtf8(v)?); + let value = pyre_object::gc_roots::shadow_stack_get(pair_base + i * 2 + 1); + out.push_wtf8(&py_repr_wtf8(value)?); } out.push_str("}"); Ok(out) @@ -897,13 +906,23 @@ pub unsafe fn py_repr_wtf8(obj: PyObjectRef) -> Result let args_obj = unsafe { pyre_object::interp_exceptions::w_exception_get_args(obj) }; let mut inner = Wtf8Buf::new(); if !args_obj.is_null() && pyre_object::is_tuple(args_obj) { - let n = pyre_object::w_tuple_len(args_obj); + // `w_exception_get_args` mints this tuple on every call. Its + // header is old-gen and never moves, but nothing roots it, so + // the collector does not walk it and its element slots keep the + // pre-move addresses of any argument an item's `__repr__` + // relocates. Pinning makes it traced, and the elements are + // read back through the pinned tuple. + let _args_roots = pyre_object::gc_roots::push_roots(); + let args_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(args_obj); + let args_obj = || pyre_object::gc_roots::shadow_stack_get(args_slot); + let n = pyre_object::w_tuple_len(args_obj()); if n == 1 { - let item = pyre_object::w_tuple_getitem(args_obj, 0).unwrap_or(args_obj); + let item = pyre_object::w_tuple_getitem(args_obj(), 0).unwrap_or(args_obj()); inner.push_wtf8(&py_repr_wtf8(item)?); } else { for i in 0..n { - if let Some(item) = pyre_object::w_tuple_getitem(args_obj, i as i64) { + if let Some(item) = pyre_object::w_tuple_getitem(args_obj(), i as i64) { // `interp_exceptions.py:135-147` spells the args // with `repr(tuple(args))`, which separates by // position — an argument whose `__repr__` answers @@ -1012,13 +1031,19 @@ pub unsafe fn py_repr_wtf8(obj: PyObjectRef) -> Result pyre_object::dictmultiobject::DictViewKind::Items => "dict_items", }; let snapshot = crate::type_methods::dict_view_snapshot(obj); + // The snapshot is a native Vec the collector does not walk, and an + // item's `__repr__` runs Python. Pin it and read each element back + // from the shadow stack. + let _snapshot_roots = pyre_object::gc_roots::push_roots(); + let item_base = pyre_object::gc_roots::pin_roots(&snapshot); let mut out = Wtf8Buf::new(); out.push_str(label); out.push_str("(["); - for (i, &item) in snapshot.iter().enumerate() { + for i in 0..snapshot.len() { if i != 0 { out.push_str(", "); } + let item = pyre_object::gc_roots::shadow_stack_get(item_base + i); out.push_wtf8(&py_repr_wtf8(item)?); } out.push_str("])"); diff --git a/pyre/pyre-interpreter/src/opcode_ops.rs b/pyre/pyre-interpreter/src/opcode_ops.rs index f552ebdd7fa..1da17aca42c 100644 --- a/pyre/pyre-interpreter/src/opcode_ops.rs +++ b/pyre/pyre-interpreter/src/opcode_ops.rs @@ -322,11 +322,26 @@ pub fn match_class_value( } let type_name = unsafe { pyre_object::w_type_get_name(cls) }; - if !crate::baseobjspace::isinstance(subject, cls)? { + // `isinstance`, the `__match_args__` probe and each attribute fetch below + // run Python, so the subject moves under the pattern and so does every + // attribute extracted before it. Pin the subject and read it back at each + // use; hold the extracted values as shadow-stack slots rather than as raw + // copies and read them back when the result tuple is built. `cls` is a + // type, which is old-gen and never moves. Pinning `kwd_attrs` is what + // makes the collector walk it, so its name slots are forwarded too. + let roots = pyre_object::gc_roots::push_roots(); + let subject_slot = pyre_object::gc_roots::shadow_stack_len(); + roots.pin_root(subject); + let kwd_attrs_slot = pyre_object::gc_roots::shadow_stack_len(); + roots.pin_root(kwd_attrs); + let subject = || roots.get(subject_slot); + let kwd_attrs = || roots.get(kwd_attrs_slot); + + if !crate::baseobjspace::isinstance(subject(), cls)? { return Ok(pyre_object::w_none()); } - let mut extracted: Vec = Vec::new(); + let mut extracted: Vec = Vec::new(); let mut seen: Vec = Vec::new(); if count > 0 { @@ -385,8 +400,11 @@ pub fn match_class_value( ))); } seen.push(attr_name.to_string()); - match crate::baseobjspace::getattr_str(subject, attr_name) { - Ok(v) => extracted.push(v), + match crate::baseobjspace::getattr_str(subject(), attr_name) { + Ok(v) => { + extracted.push(pyre_object::gc_roots::shadow_stack_len()); + roots.pin_root(v); + } Err(e) if e.kind == crate::PyErrorKind::AttributeError => { return Ok(pyre_object::w_none()); } @@ -423,7 +441,7 @@ pub fn match_class_value( }; if is_self { if count == 1 { - extracted.push(subject); + extracted.push(subject_slot); } else { return Err(PyError::type_error(format!( "{type_name}() accepts 1 positional sub-pattern ({count} given)" @@ -437,7 +455,7 @@ pub fn match_class_value( } } - let kwd_items = unsafe { pyre_object::tupleobject::w_tuple_items_copy_as_vec(kwd_attrs) }; + let kwd_items = unsafe { pyre_object::tupleobject::w_tuple_items_copy_as_vec(kwd_attrs()) }; for name_obj in kwd_items { let name = match unsafe { pyre_object::w_str_get_value_opt(name_obj) } { Some(s) => s, @@ -449,8 +467,11 @@ pub fn match_class_value( ))); } seen.push(name.to_string()); - match crate::baseobjspace::getattr_str(subject, name) { - Ok(v) => extracted.push(v), + match crate::baseobjspace::getattr_str(subject(), name) { + Ok(v) => { + extracted.push(pyre_object::gc_roots::shadow_stack_len()); + roots.pin_root(v); + } Err(e) if e.kind == crate::PyErrorKind::AttributeError => { return Ok(pyre_object::w_none()); } @@ -458,7 +479,8 @@ pub fn match_class_value( } } - Ok(pyre_object::w_tuple_new(extracted)) + let values: Vec = extracted.iter().map(|&slot| roots.get(slot)).collect(); + Ok(pyre_object::w_tuple_new(values)) } pub fn truth_value(value: PyObjectRef) -> Result { diff --git a/pyre/pyre-interpreter/src/type_methods.rs b/pyre/pyre-interpreter/src/type_methods.rs index 52a6d7ec60d..0d9aa79f34e 100644 --- a/pyre/pyre-interpreter/src/type_methods.rs +++ b/pyre/pyre-interpreter/src/type_methods.rs @@ -6413,10 +6413,25 @@ pub fn dict_view_snapshot(view: PyObjectRef) -> Vec { pyre_object::dictmultiobject::DictViewKind::Values => { items.into_iter().map(|(_, v)| v).collect() } - pyre_object::dictmultiobject::DictViewKind::Items => items - .into_iter() - .map(|(k, v)| w_tuple_new(vec![k, v])) - .collect(), + pyre_object::dictmultiobject::DictViewKind::Items => { + // `w_tuple_new` allocates, so a pair still waiting in this native + // Vec moves under the loop. A pair already built into a tuple is + // just as exposed: that tuple's header is old-gen, but until + // something roots it the collector does not walk it and its two + // slots keep pre-move addresses. Pin both sides of the loop. + let _roots = pyre_object::gc_roots::push_roots(); + let flat: Vec = items.iter().flat_map(|&(k, v)| [k, v]).collect(); + let pair_base = pyre_object::gc_roots::pin_roots(&flat); + let mut built = Vec::with_capacity(items.len()); + for i in 0..items.len() { + let k = pyre_object::gc_roots::shadow_stack_get(pair_base + i * 2); + let v = pyre_object::gc_roots::shadow_stack_get(pair_base + i * 2 + 1); + let tuple_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(w_tuple_new(vec![k, v])); + built.push(pyre_object::gc_roots::shadow_stack_get(tuple_slot)); + } + built + } } } diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index ce6578b2af9..a830c439424 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -8093,7 +8093,16 @@ fn dict_view_all_contained_in( other: pyre_object::PyObjectRef, ) -> Result { let snapshot = crate::type_methods::dict_view_snapshot(view); - for item in snapshot { + // `contains` runs a user `__contains__`/`__eq__`, so `other` and every + // item still waiting in this native Vec move under the loop. Pin them and + // read each one back at its use. + let _roots = pyre_object::gc_roots::push_roots(); + let other_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(other); + let item_base = pyre_object::gc_roots::pin_roots(&snapshot); + for i in 0..snapshot.len() { + let other = pyre_object::gc_roots::shadow_stack_get(other_slot); + let item = pyre_object::gc_roots::shadow_stack_get(item_base + i); if !crate::baseobjspace::contains(other, item)? { return Ok(false); } From 3610c99ce83c7aeb9d728d7c7f7dfcb910e793a6 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Mon, 17 Aug 2026 00:31:11 +0900 Subject: [PATCH 2/2] call, baseobjspace, ast, codecs, display: root seventeen more walks across the Python they run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sites where a `PyObjectRef` was held in a Rust local, a native `Vec`, or an off-heap struct across code that allocates or runs Python, and then used without being read back from the shadow stack. `call.rs`: the metaclass / staticmethod / classmethod `__call__` override arms in all three dispatchers forwarded the argument view built before the override probe, which binds through `baseobjspace::get` and so runs Python for a property or a user `__get__`. `call_function_impl_result`'s `user_call_slot` arm and `call_with_kwargs_in_ctx_impl`'s builtin ABI arms already rebuilt the view from the roots; the override arms now do the same, and `call_non_function_callable_with_mode` gained the roots it had none of. `baseobjspace.rs call_args_and_c_profile_args` had no `push_roots` at all while running the profile hook before the call, the callee, and the return hook. `arguments` is now `&mut` so its vectors can be refreshed between the hooks, and the callee's result is published across the return hook. `builtins.rs import_error_setstate` held the state dict across three `dict.pop` calls — the converted twin `base_exception_setstate` sits 190 lines above. `opcode_ops.rs dict_update_value` walked a snapshot across `w_dict_store` on the `is_dict` fast path. `typedef.rs dict_view_isdisjoint` and `setlike_descr_isdisjoint`, `_pypy_generic_alias.rs unpack_args` and `subs_tvars`, and the three `_collections` deque extend arms each walked a `collect_iterable` Vec across user `__eq__`/`__hash__` or block allocation. `_ast/convert.rs Converter::pin` pinned its argument and returned the pre-pin local, so the pin was a no-op; `node` and `module_to_object` now read their values back. The remaining window at the 77 `node` call sites is documented at `pin`. `_codecs`' registry list and two dicts were forwarded only behind the prebuilt-roots gate, which a minor collection skips once the dirty bit is clear, and `CodecState::new` sets no dirty bit; the walk moves up beside `_pickle` and the audit hooks, which were hoisted for that same reason. `display.rs`' mid-repr cycle set keyed on raw addresses, so a container that moved mid-walk stopped matching itself and its cycle recursed instead of emitting the placeholder. It now holds the objects and is registered as a per-mutator root area. Assisted-by: Claude --- .../src/_pypy_generic_alias.rs | 66 ++++++++++++---- pyre/pyre-interpreter/src/baseobjspace.rs | 54 ++++++++++--- pyre/pyre-interpreter/src/builtins.rs | 20 ++++- pyre/pyre-interpreter/src/call.rs | 79 ++++++++++++++++--- pyre/pyre-interpreter/src/display.rs | 60 +++++++++++--- pyre/pyre-interpreter/src/eval.rs | 7 +- .../src/module/_ast/convert.rs | 66 +++++++++++++--- .../src/module/_collections/mod.rs | 39 ++++++--- pyre/pyre-interpreter/src/opcode_ops.rs | 19 ++++- pyre/pyre-interpreter/src/typedef.rs | 25 +++++- pyre/pyre-jit/src/eval.rs | 16 ++++ 11 files changed, 372 insertions(+), 79 deletions(-) diff --git a/pyre/pyre-interpreter/src/_pypy_generic_alias.rs b/pyre/pyre-interpreter/src/_pypy_generic_alias.rs index d48d18660dc..5252d2f11e6 100644 --- a/pyre/pyre-interpreter/src/_pypy_generic_alias.rs +++ b/pyre/pyre-interpreter/src/_pypy_generic_alias.rs @@ -365,10 +365,21 @@ fn tuple_index(t: PyObjectRef, item: PyObjectRef) -> Result, crate /// unpacked `tuple[...]` alias (one exposing `__typing_unpacked_tuple_args__`) /// into its members, unless those end in `...`. Returns a fresh items tuple. fn unpack_args(items: PyObjectRef) -> Result { - let n = unsafe { w_tuple_len(items) }; - let mut newargs: Vec = Vec::new(); + // The loop body runs Python at every turn, so the accumulator holds slot + // indices rather than values — the same shape `push_newarg` uses above — + // and `items` is read back before each element fetch. + let _roots = pyre_object::gc_roots::push_roots(); + let items_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(items); + let items = || pyre_object::gc_roots::shadow_stack_get(items_slot); + let n = unsafe { w_tuple_len(items()) }; + let mut newarg_slots: Vec = Vec::new(); + let mut push_newarg = |value: PyObjectRef, slots: &mut Vec| { + slots.push(pyre_object::gc_roots::shadow_stack_len()); + pyre_object::gc_roots::pin_root(value); + }; for i in 0..n { - let Some(arg) = (unsafe { w_tuple_getitem(items, i as i64) }) else { + let Some(arg) = (unsafe { w_tuple_getitem(items(), i as i64) }) else { continue; }; let subargs = match crate::baseobjspace::getattr_str(arg, "__typing_unpacked_tuple_args__") @@ -392,13 +403,21 @@ fn unpack_args(items: PyObjectRef) -> Result { }; if do_unpack { // `newargs.extend(subargs)` — any iterable, not just a tuple. - for x in crate::builtins::collect_iterable(subargs)? { - newargs.push(x); + // Publish the collected members in one go: `collect_iterable`'s own + // scope has popped, so its Vec is untraced from here on. + let members = crate::builtins::collect_iterable(subargs)?; + let member_base = pyre_object::gc_roots::pin_roots(&members); + for index in 0..members.len() { + newarg_slots.push(member_base + index); } } else { - newargs.push(arg); + push_newarg(arg, &mut newarg_slots); } } + let newargs: Vec = newarg_slots + .iter() + .map(|&slot| pyre_object::gc_roots::shadow_stack_get(slot)) + .collect(); Ok(w_tuple_new(newargs)) } @@ -719,27 +738,46 @@ fn subs_tvars( return Ok(obj); } let nsub = unsafe { w_tuple_len(subparams) }; - let mut subargs: Vec = Vec::with_capacity(nsub); + // `tuple_index` runs a user `__eq__` on every turn, so the operands and the + // arguments picked so far are held as shadow-stack slots and read back when + // the substitution tuple is built. + let _roots = pyre_object::gc_roots::push_roots(); + let base = pyre_object::gc_roots::pin_roots(&[obj, params, argitems, subparams]); + let obj = || pyre_object::gc_roots::shadow_stack_get(base); + let params = || pyre_object::gc_roots::shadow_stack_get(base + 1); + let argitems = || pyre_object::gc_roots::shadow_stack_get(base + 2); + let subparams = || pyre_object::gc_roots::shadow_stack_get(base + 3); + let mut subarg_slots: Vec = Vec::with_capacity(nsub); for i in 0..nsub { - let Some(param) = (unsafe { w_tuple_getitem(subparams, i as i64) }) else { + let Some(param) = (unsafe { w_tuple_getitem(subparams(), i as i64) }) else { continue; }; // `try: argitems[params.index(param)] except ValueError: param`. - let arg = match tuple_index(params, param)? { - Some(idx) => unsafe { w_tuple_getitem(argitems, idx as i64) }.unwrap_or(param), + let arg = match tuple_index(params(), param)? { + Some(idx) => unsafe { w_tuple_getitem(argitems(), idx as i64) }.unwrap_or(param), None => param, }; // `if isinstance(param, TypeVarTuple): subargs.extend(arg)` — a // `TypeVarTuple` captures a sequence, so its bound `arg` is spliced. if is_typevartuple(param) { - for x in crate::builtins::collect_iterable(arg)? { - subargs.push(x); + let members = crate::builtins::collect_iterable(arg)?; + let member_base = pyre_object::gc_roots::pin_roots(&members); + for index in 0..members.len() { + subarg_slots.push(member_base + index); } } else { - subargs.push(arg); + subarg_slots.push(pyre_object::gc_roots::shadow_stack_len()); + pyre_object::gc_roots::pin_root(arg); } } - crate::baseobjspace::getitem(obj, w_tuple_new(subargs)) + let subargs: Vec = subarg_slots + .iter() + .map(|&slot| pyre_object::gc_roots::shadow_stack_get(slot)) + .collect(); + // Build the substitution tuple before reading `obj` back: `w_tuple_new` + // allocates, so a receiver read ahead of it would be the pre-move one. + let subs = w_tuple_new(subargs); + crate::baseobjspace::getitem(obj(), subs) } /// `_make_starred(ga)` (`_pypy_generic_alias.py:118`) — a copy of the alias diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 12cebe7b4e8..d129b013b1f 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -12724,8 +12724,8 @@ pub fn call_args_and_c_profile( callable: PyObjectRef, args: &[PyObjectRef], ) -> PyObjectRef { - let arguments = crate::argument::Arguments::positional_only(args); - call_args_and_c_profile_args(frame, callable, &arguments, args) + let mut arguments = crate::argument::Arguments::positional_only(args); + call_args_and_c_profile_args(frame, callable, &mut arguments, args) } /// `baseobjspace.py:1269-1278 call_args_and_c_profile` with a @@ -12746,15 +12746,41 @@ pub fn call_args_and_c_profile( pub fn call_args_and_c_profile_args( frame: &mut crate::pyframe::PyFrame, callable: PyObjectRef, - arguments: &crate::argument::Arguments, + arguments: &mut crate::argument::Arguments, flat_args: &[PyObjectRef], ) -> PyObjectRef { + // Reaching here means a profile function is installed, so the tracer hooks + // below run Python — and the callee runs in between. `callable`, the flat + // slice and the `Arguments` vectors are native storage no root walker + // updates. Root them all, dispatch from the roots, and refresh the + // `Arguments` vectors before the return hook reads them again. + let _roots = pyre_object::gc_roots::push_roots(); + let callable_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(callable); + let flat_base = pyre_object::gc_roots::pin_roots(flat_args); + let positional_base = pyre_object::gc_roots::pin_roots(&arguments.arguments_w); + let keyword_base = arguments + .keywords_w + .as_ref() + .map(|values| pyre_object::gc_roots::pin_roots(values)); + let callable = || pyre_object::gc_roots::shadow_stack_get(callable_slot); + let refresh = |arguments: &mut crate::argument::Arguments| { + for (index, slot) in arguments.arguments_w.iter_mut().enumerate() { + *slot = pyre_object::gc_roots::shadow_stack_get(positional_base + index); + } + if let (Some(base), Some(values)) = (keyword_base, arguments.keywords_w.as_mut()) { + for (index, slot) in values.iter_mut().enumerate() { + *slot = pyre_object::gc_roots::shadow_stack_get(base + index); + } + } + }; + let ec = crate::call::getexecutioncontext() as *mut crate::PyExecutionContext; if !ec.is_null() && let Err(err) = unsafe { (*ec).c_call_trace( frame as *mut crate::pyframe::PyFrame, - callable, + callable(), Some(arguments), ) } @@ -12762,7 +12788,10 @@ pub fn call_args_and_c_profile_args( crate::call::set_call_error(err); return pyre_object::PY_NULL; } - let w_res = call_function(callable, flat_args); + let flat_args: Vec = (0..flat_args.len()) + .map(|index| pyre_object::gc_roots::shadow_stack_get(flat_base + index)) + .collect(); + let w_res = call_function(callable(), &flat_args); if w_res == pyre_object::PY_NULL { if !ec.is_null() { // baseobjspace.py:1274-1276 — `except OperationError: @@ -12773,19 +12802,24 @@ pub fn call_args_and_c_profile_args( // stash already holds the original OperationError; if // c_exception_trace raises, overwrite the stash so the // tracer error is what propagates. - if let Err(trace_err) = - unsafe { (*ec).c_exception_trace(frame as *mut crate::pyframe::PyFrame, callable) } - { + if let Err(trace_err) = unsafe { + (*ec).c_exception_trace(frame as *mut crate::pyframe::PyFrame, callable()) + } { crate::call::set_call_error(trace_err); } } return pyre_object::PY_NULL; } + refresh(arguments); + // The return hook runs Python too, so the callee's result is published + // before it and read back afterwards. + let result_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(w_res); if !ec.is_null() && let Err(err) = unsafe { (*ec).c_return_trace( frame as *mut crate::pyframe::PyFrame, - callable, + callable(), Some(arguments), ) } @@ -12793,7 +12827,7 @@ pub fn call_args_and_c_profile_args( crate::call::set_call_error(err); return pyre_object::PY_NULL; } - w_res + pyre_object::gc_roots::shadow_stack_get(result_slot) } /// PyPy: baseobjspace.py `call_method`. diff --git a/pyre/pyre-interpreter/src/builtins.rs b/pyre/pyre-interpreter/src/builtins.rs index e7985d1bb9f..a3fc1f93e10 100644 --- a/pyre/pyre-interpreter/src/builtins.rs +++ b/pyre/pyre-interpreter/src/builtins.rs @@ -7093,6 +7093,11 @@ fn import_error_setstate(args: &[PyObjectRef]) -> Result Result Result PyResult { + // Binding an override below runs `baseobjspace::get`, whose property and + // general `__get__` arms execute Python. Root the arguments first and + // dispatch each bound call from the forwarded roots — this native slice is + // not one the collector updates. + let _override_roots = pyre_object::gc_roots::push_roots(); + let override_base = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_roots(args); + let reloaded_args = || { + let mut reloaded = Vec::with_capacity(args.len()); + for index in 0..args.len() { + reloaded.push(pyre_object::gc_roots::shadow_stack_get( + override_base + index, + )); + } + reloaded + }; + if unsafe { pyre_object::is_type(callable) } { if let Some(bound) = metaclass_call_override(callable) { - return call_callable_with_mode(execution_context, bound, args, mode); + return call_callable_with_mode(execution_context, bound, &reloaded_args(), mode); } return type_descr_call_with_mode(execution_context, callable, args, mode); } @@ -1696,10 +1713,10 @@ fn call_non_function_callable_with_mode( return call_callable_with_mode(execution_context, func, args, mode); } if let Some(bound) = staticmethod_call_override(callable)? { - return call_callable_with_mode(execution_context, bound, args, mode); + return call_callable_with_mode(execution_context, bound, &reloaded_args(), mode); } if let Some(bound) = classmethod_call_override(callable)? { - return call_callable_with_mode(execution_context, bound, args, mode); + return call_callable_with_mode(execution_context, bound, &reloaded_args(), mode); } // The base ClassMethod defines no descr_call (function.py), so a raw // classmethod object falls through to the not-callable error. @@ -2506,13 +2523,31 @@ fn call_with_kwargs_in_ctx_impl( let func = unsafe { pyre_object::w_staticmethod_get_func(callable) }; return call_with_kwargs_in_ctx(execution_context, func, pos_args, kwargs); } + // Binding any of the three overrides below runs Python, so each bound call + // is dispatched from the roots pinned above rather than from the incoming + // slices — the same rebuild the builtin ABI arms already do. + let overridden_args = || { + let mut current = Vec::with_capacity(pos_args.len()); + extend_current_args(&mut current); + current + }; if let Some(bound) = staticmethod_call_override(callable)? { - return call_with_kwargs_in_ctx(execution_context, bound, pos_args, kwargs); + return call_with_kwargs_in_ctx( + execution_context, + bound, + &overridden_args(), + ¤t_kwargs(), + ); } if unsafe { pyre_object::is_classmethod(callable) } { if let Some(bound) = classmethod_call_override(callable)? { - return call_with_kwargs_in_ctx(execution_context, bound, pos_args, kwargs); + return call_with_kwargs_in_ctx( + execution_context, + bound, + &overridden_args(), + ¤t_kwargs(), + ); } let type_name = crate::typedef::r#type(callable) .map(|tp| unsafe { pyre_object::w_type_get_name(tp.as_ptr()) }) @@ -2540,7 +2575,12 @@ fn call_with_kwargs_in_ctx_impl( && unsafe { pyre_object::is_type(callable) } && let Some(bound) = metaclass_call_override(callable) { - return call_with_kwargs_in_ctx(execution_context, bound, pos_args, kwargs); + return call_with_kwargs_in_ctx( + execution_context, + bound, + &overridden_args(), + ¤t_kwargs(), + ); } if unsafe { crate::is_function_carrier(callable) } { @@ -2591,7 +2631,7 @@ fn call_with_kwargs_in_ctx_impl( .collect(); let keywords_w: Vec = kwargs.iter().map(|(_, v)| *v).collect(); - let arguments = crate::argument::Arguments::with_kw( + let mut arguments = crate::argument::Arguments::with_kw( pos_args, &keyword_names_w, &keywords_w, @@ -2599,7 +2639,7 @@ fn call_with_kwargs_in_ctx_impl( let w_res = crate::baseobjspace::call_args_and_c_profile_args( unsafe { &mut *frame_ptr }, callable, - &arguments, + &mut arguments, &bound, ); if w_res == pyre_object::PY_NULL { @@ -2699,7 +2739,7 @@ fn call_with_kwargs_in_ctx_impl( (0..kwargs.len()).map(current_kwarg).collect(); let refreshed_pos: Vec = (0..pos_args.len()).map(current_pos_arg).collect(); - let arguments = crate::argument::Arguments::with_kw( + let mut arguments = crate::argument::Arguments::with_kw( &refreshed_pos, &keyword_names_w, &keywords_w, @@ -2712,7 +2752,7 @@ fn call_with_kwargs_in_ctx_impl( let w_res = crate::baseobjspace::call_args_and_c_profile_args( unsafe { &mut *frame_ptr }, callable, - &arguments, + &mut arguments, &full_args, ); if w_res == pyre_object::PY_NULL { @@ -3293,6 +3333,19 @@ pub fn call_function_impl_result( wide_args.as_slice() }; + // Binding an override descriptor below runs `baseobjspace::get`, whose + // property and general `__get__` arms execute Python. That updates the + // entry roots but not this native view, so the override arms rebuild it + // from the roots before dispatching, as the `user_call_slot` arm does. + let arg_count = args.len(); + let reloaded_args = || { + let mut reloaded = Vec::with_capacity(arg_count); + for i in 0..arg_count { + reloaded.push(pyre_object::gc_roots::shadow_stack_get(root_base + 1 + i)); + } + reloaded + }; + unsafe { if pyre_object::is_method(callable) { let func = pyre_object::w_method_get_func(callable); @@ -3342,7 +3395,7 @@ pub fn call_function_impl_result( // PyPy: typeobject.py descr_call → lookup __new__, call, then __init__ if pyre_object::is_type(callable) { if let Some(bound) = metaclass_call_override(callable) { - return call_function_impl_result(bound, args); + return call_function_impl_result(bound, &reloaded_args()); } clear_call_error(); let result = type_descr_call_impl(callable, args); @@ -3360,10 +3413,10 @@ pub fn call_function_impl_result( return call_function_impl_result(func, args); } if let Some(bound) = staticmethod_call_override(callable)? { - return call_function_impl_result(bound, args); + return call_function_impl_result(bound, &reloaded_args()); } if let Some(bound) = classmethod_call_override(callable)? { - return call_function_impl_result(bound, args); + return call_function_impl_result(bound, &reloaded_args()); } // ClassMethod has no descr_call (function.py:718-768; CPython 3.14 // `PyClassMethod_Type.tp_call = 0`), so a raw wrapper falls through diff --git a/pyre/pyre-interpreter/src/display.rs b/pyre/pyre-interpreter/src/display.rs index acd50b9f1ca..8d63836f07b 100644 --- a/pyre/pyre-interpreter/src/display.rs +++ b/pyre/pyre-interpreter/src/display.rs @@ -185,10 +185,40 @@ thread_local! { /// recursive container branches against unbounded recursion on a /// reference cycle (a list holding itself, a dict valued by itself). /// Mirrors the per-thread reprlist behind `Py_ReprEnter`/`Py_ReprLeave`. - static REPR_ACTIVE: std::cell::RefCell> = + /// Entries are the objects themselves, not their addresses: the guarded + /// container is a list or a dict on every interesting path, and both move. + /// `walk_repr_active_area` is registered per mutator so the collector + /// forwards these slots — a raw address recorded here would stop matching + /// its own object after the first collection, and the cycle would recurse + /// unbounded instead of emitting the `...` placeholder. + static REPR_ACTIVE: std::cell::RefCell> = const { std::cell::RefCell::new(Vec::new()) }; } +/// This thread's mid-repr set, for `register_mutator_extra_area`. +pub fn capture_repr_active_area() -> *const () { + REPR_ACTIVE.with(|active| active as *const _ as *const ()) +} + +/// Forward the mid-repr set of the mutator that owns `data`. +/// +/// # Safety +/// `data` must be a pointer returned by [`capture_repr_active_area`] on a +/// thread that is still registered. +pub unsafe fn walk_repr_active_area( + data: *const (), + visitor: &mut dyn FnMut(&mut pyre_object::PyObjectRef), +) { + let active = unsafe { &*(data as *const std::cell::RefCell>) }; + // A collection can be entered from inside `repr_enter`/`repr_leave`, which + // hold the borrow. Skipping the walk then would drop the forwarding, so + // take the pointer to the buffer instead of a second borrow. + let entries = unsafe { &mut *active.as_ptr() }; + for entry in entries.iter_mut() { + visitor(entry); + } +} + /// Record `obj` as mid-repr on this thread, or report `false` when it already /// is (`Py_ReprEnter`). Writes the runtime-mutable `REPR_ACTIVE` thread-local, /// not a build-time constant, so the JIT residualises the call instead of @@ -196,13 +226,12 @@ thread_local! { /// shape), the [`repr_leave`] twin. #[majit_macros::dont_look_inside] pub(crate) fn repr_enter(obj: PyObjectRef) -> bool { - let key = obj as usize; REPR_ACTIVE.with(|active| { let mut active = active.borrow_mut(); - if active.contains(&key) { + if active.contains(&obj) { false } else { - active.push(key); + active.push(obj); true } }) @@ -211,29 +240,42 @@ pub(crate) fn repr_enter(obj: PyObjectRef) -> bool { /// Drop `obj` from the mid-repr set (`Py_ReprLeave`) — see [`repr_enter`]. #[majit_macros::dont_look_inside] pub(crate) fn repr_leave(obj: PyObjectRef) { - let key = obj as usize; REPR_ACTIVE.with(|active| { let mut active = active.borrow_mut(); - if let Some(pos) = active.iter().rposition(|&k| k == key) { + if let Some(pos) = active.iter().rposition(|&entry| entry == obj) { active.remove(pos); } }); } +/// Drop the entry `repr_enter` pushed at `index` — see [`repr_leave`]. +/// +/// The guard leaves by position rather than by value because its own copy of +/// the object is a Rust local the collector does not update, so matching on it +/// would miss a forwarded entry and leave the set holding a finished repr. +fn repr_leave_at(index: usize) { + REPR_ACTIVE.with(|active| { + let mut active = active.borrow_mut(); + if index < active.len() { + active.remove(index); + } + }); +} + /// RAII cycle guard. `enter` returns `None` when `obj` is already being /// repr'd on this thread — the caller emits the `...` placeholder — and /// otherwise records `obj`, removing it again when the guard drops. -pub(crate) struct ReprGuard(PyObjectRef); +pub(crate) struct ReprGuard(usize); impl ReprGuard { pub(crate) fn enter(obj: PyObjectRef) -> Option { - repr_enter(obj).then_some(ReprGuard(obj)) + repr_enter(obj).then(|| ReprGuard(REPR_ACTIVE.with(|active| active.borrow().len() - 1))) } } impl Drop for ReprGuard { fn drop(&mut self) { - repr_leave(self.0); + repr_leave_at(self.0); } } diff --git a/pyre/pyre-interpreter/src/eval.rs b/pyre/pyre-interpreter/src/eval.rs index 5e15cc70311..d3bb0d4c74b 100644 --- a/pyre/pyre-interpreter/src/eval.rs +++ b/pyre/pyre-interpreter/src/eval.rs @@ -1306,6 +1306,10 @@ fn walk_global_prebuilt_roots(visitor: &mut dyn FnMut(&mut majit_ir::GcRef)) { // code, so a hook callable is young when it lands and cannot wait for // the prebuilt-roots scan below. crate::module::sys::vm::walk_audit_hooks_gc(&mut fwd); + // `space.fromcache(CodecState)` publishes a young list and two young + // dicts on first use and marks no prebuilt-roots dirty bit, so it + // belongs with these two rather than behind the gate below. + crate::module::_codecs::walk_codec_state_gc(&mut fwd); } // `space.fromcache(MethodCache)` is a live interpreter-global GC root, // not a write-once prebuilt object, and one cache serves every mutator. @@ -1342,9 +1346,6 @@ fn walk_global_prebuilt_roots(visitor: &mut dyn FnMut(&mut majit_ir::GcRef)) { walk_raw_wrapped_function_roots(*slot, visitor); }; walk_builtin_type_dicts_gc(&mut forward); - // interp_codecs.CodecState is `space.fromcache(CodecState)` in PyPy: - // one process/interpreter-owned registry, not one copy per mutator. - crate::module::_codecs::walk_codec_state_gc(&mut forward); // interp_posix.ApplevelForkCallbacks is another object-space cache. #[cfg(not(target_arch = "wasm32"))] crate::module::posix::interp_posix::walk_fork_callback_roots(&mut forward); diff --git a/pyre/pyre-interpreter/src/module/_ast/convert.rs b/pyre/pyre-interpreter/src/module/_ast/convert.rs index 8972de7cec2..cc3d94f25a8 100644 --- a/pyre/pyre-interpreter/src/module/_ast/convert.rs +++ b/pyre/pyre-interpreter/src/module/_ast/convert.rs @@ -1749,15 +1749,25 @@ fn module_to_object( } else { "Module" }; - let body = converter.stmt_list(&module.body)?; + // `converter.list` allocates the `type_ignores` list, so read the + // body back from its own slot after that sibling is built. + let body_slot = converter.pin_slot(converter.stmt_list(&module.body)?); if root_name == "Module" { + let type_ignores = converter.list(Vec::new()); converter.node( root_name, None, - &[("body", body), ("type_ignores", converter.list(Vec::new()))], + &[ + ("body", pyre_object::gc_roots::shadow_stack_get(body_slot)), + ("type_ignores", type_ignores), + ], ) } else { - converter.node(root_name, None, &[("body", body)]) + converter.node( + root_name, + None, + &[("body", pyre_object::gc_roots::shadow_stack_get(body_slot))], + ) } } } @@ -1769,9 +1779,31 @@ struct Converter<'a> { } impl Converter<'_> { + /// Publish `value` as a root of `module_to_object`'s scope and hand back + /// the published slot's contents, not the caller's copy — the pin is what + /// makes the collector forward it, and reading the pre-pin local back would + /// discard that forwarding. + /// + /// STILL OPEN: the value is fresh when it is returned, but a caller that + /// builds a `&[(&str, PyObjectRef)]` field array allocates for the sibling + /// elements before [`Converter::node`] receives it, and only the shadow + /// slot is forwarded across that window. Lists are the only movable field + /// values here (nodes are instances, and the rest are str/int/None), so + /// closing it means having the list-producing helpers return slots and + /// `node` take them — a change across all 77 `node` call sites, not a + /// rooting patch. Reproduces only under `PYPY_GC_NURSERY=1` + /// (`ast.unparse` emits invalid source); the default nursery is unaffected. fn pin(&self, value: PyObjectRef) -> PyObjectRef { + let slot = self.pin_slot(value); + pyre_object::gc_roots::shadow_stack_get(slot) + } + + /// `pin`, returning the slot index so a caller that runs Python between the + /// pin and the use can re-read the value at each use. + fn pin_slot(&self, value: PyObjectRef) -> usize { + let slot = pyre_object::gc_roots::shadow_stack_len(); pyre_object::gc_roots::pin_root(value); - value + slot } fn list(&self, values: Vec) -> PyObjectRef { @@ -1793,9 +1825,20 @@ impl Converter<'_> { fields: &[(&str, PyObjectRef)], ) -> crate::PyResult { let node_type = crate::baseobjspace::getattr_str(self.ast_module, name)?; - let node = self.pin(pyre_object::w_instance_new(node_type)); - for &(field, value) in fields { - crate::baseobjspace::setattr_str(node, field, value)?; + // Every `setattr_str` below runs Python, so the node and the field + // values move under the loop. Publish them and read each back at the + // store that consumes it. + let node_slot = self.pin_slot(pyre_object::w_instance_new(node_type)); + let value_base = pyre_object::gc_roots::shadow_stack_len(); + for &(_, value) in fields { + pyre_object::gc_roots::pin_root(value); + } + for (index, &(field, _)) in fields.iter().enumerate() { + crate::baseobjspace::setattr_str( + pyre_object::gc_roots::shadow_stack_get(node_slot), + field, + pyre_object::gc_roots::shadow_stack_get(value_base + index), + )?; } if let Some((start, end)) = range { let (lineno, col_offset) = self.location(start as usize); @@ -1806,14 +1849,17 @@ impl Converter<'_> { ("end_lineno", end_lineno), ("end_col_offset", end_col_offset), ] { + // Box the position before reading the node back: `w_int_new` + // allocates, so a receiver read ahead of it is the pre-move one. + let w_value = pyre_object::w_int_new(value as i64); crate::baseobjspace::setattr_str( - node, + pyre_object::gc_roots::shadow_stack_get(node_slot), field, - pyre_object::w_int_new(value as i64), + w_value, )?; } } - Ok(node) + Ok(pyre_object::gc_roots::shadow_stack_get(node_slot)) } fn location(&self, offset: usize) -> (usize, usize) { diff --git a/pyre/pyre-interpreter/src/module/_collections/mod.rs b/pyre/pyre-interpreter/src/module/_collections/mod.rs index f050e85af2b..0cd03630371 100644 --- a/pyre/pyre-interpreter/src/module/_collections/mod.rs +++ b/pyre/pyre-interpreter/src/module/_collections/mod.rs @@ -522,6 +522,31 @@ pub mod deque_rev_iter { } } +/// Append every element of `iterable` to `self_obj` through `append`. +/// +/// `collect_iterable`'s own root scope has popped by the time it returns, so +/// the elements sit in a native Vec the collector does not walk while each +/// append allocates the next block. Publish the whole run up front and read +/// the deque and each element back at the turn that consumes them. +fn extend_from_iterable( + self_obj: PyObjectRef, + iterable: PyObjectRef, + append: fn(PyObjectRef, PyObjectRef), +) -> Result<(), crate::PyError> { + let items = crate::builtins::collect_iterable(iterable)?; + let _roots = pyre_object::gc_roots::push_roots(); + let deque_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(self_obj); + let item_base = pyre_object::gc_roots::pin_roots(&items); + for index in 0..items.len() { + append( + pyre_object::gc_roots::shadow_stack_get(deque_slot), + pyre_object::gc_roots::shadow_stack_get(item_base + index), + ); + } + Ok(()) +} + /// `W_Deque.append` + `trimleft`, ported from `interp_deque.py`. fn append_right(self_obj: PyObjectRef, item: PyObjectRef) { let _roots = pyre_object::gc_roots::push_roots(); @@ -865,9 +890,7 @@ impl W_Deque { clear_blocks(self_obj); } if let Some(it) = iterable { - for item in crate::builtins::collect_iterable(it)? { - append_right(self_obj, item); - } + extend_from_iterable(self_obj, it, append_right)?; } Ok(()) } @@ -893,19 +916,13 @@ impl W_Deque { } fn extend(&mut self, iterable: PyObjectRef) -> Result<(), crate::PyError> { let self_obj = self as *mut W_Deque as PyObjectRef; - for item in crate::builtins::collect_iterable(iterable)? { - append_right(self_obj, item); - } - Ok(()) + extend_from_iterable(self_obj, iterable, append_right) } fn extendleft(&mut self, iterable: PyObjectRef) -> Result<(), crate::PyError> { let self_obj = self as *mut W_Deque as PyObjectRef; // Each element is appended on the left, so the result is // the reverse of `iterable`. - for item in crate::builtins::collect_iterable(iterable)? { - append_left(self_obj, item); - } - Ok(()) + extend_from_iterable(self_obj, iterable, append_left) } fn count(&self, x: PyObjectRef) -> Result { let self_obj = self as *const W_Deque as PyObjectRef; diff --git a/pyre/pyre-interpreter/src/opcode_ops.rs b/pyre/pyre-interpreter/src/opcode_ops.rs index 1da17aca42c..fb80f6eb95d 100644 --- a/pyre/pyre-interpreter/src/opcode_ops.rs +++ b/pyre/pyre-interpreter/src/opcode_ops.rs @@ -684,8 +684,23 @@ pub fn map_add_value( pub fn dict_update_value(dict: PyObjectRef, source: PyObjectRef) -> Result<(), PyError> { unsafe { if pyre_object::is_dict(source) { - for (k, v) in pyre_object::w_dict_items(source) { - pyre_object::w_dict_store(dict, k, v); + // `w_dict_store` allocates when the storage grows or the strategy + // is promoted, and `dict` is itself a moving `W_DictObject`, so + // neither it nor the pairs waiting in this native snapshot survive + // the loop unrooted. The general mapping path below already roots + // its own loop for the same reason. + let _roots = pyre_object::gc_roots::push_roots(); + let dict_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(dict); + let entries = pyre_object::w_dict_items(source); + let flat: Vec = entries.iter().flat_map(|&(k, v)| [k, v]).collect(); + let pair_base = pyre_object::gc_roots::pin_roots(&flat); + for index in 0..entries.len() { + pyre_object::w_dict_store( + pyre_object::gc_roots::shadow_stack_get(dict_slot), + pyre_object::gc_roots::shadow_stack_get(pair_base + index * 2), + pyre_object::gc_roots::shadow_stack_get(pair_base + index * 2 + 1), + ); } return Ok(()); } diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index a830c439424..9d770994c0b 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -8164,7 +8164,16 @@ fn dict_view_isdisjoint( )); } let other_items = crate::builtins::collect_iterable(other)?; - for item in other_items { + // `collect_iterable`'s own scope has popped by the time it returns, so the + // items sit in an untraced native Vec while each `contains` runs a user + // `__eq__`. Same shape as `dict_view_all_contained_in` above. + let _roots = pyre_object::gc_roots::push_roots(); + let view_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(self_view); + let item_base = pyre_object::gc_roots::pin_roots(&other_items); + for index in 0..other_items.len() { + let self_view = pyre_object::gc_roots::shadow_stack_get(view_slot); + let item = pyre_object::gc_roots::shadow_stack_get(item_base + index); if crate::baseobjspace::contains(self_view, item)? { return Ok(pyre_object::w_bool_from(false)); } @@ -25177,8 +25186,18 @@ fn setlike_descr_isdisjoint(args: &[PyObjectRef]) -> Result