diff --git a/AGENTS.md b/AGENTS.md index e667135ab1a..c4ce1228151 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,13 @@ the user explicitly expands the scope. A skipped CPython test does not override this boundary; fix PyPy's real public-module owner or fallback instead. See “Module presence follows PyPy” below for the full rule and examples. +**Hard stop:** if neither the real `pypy3` import nor an upstream PyPy owner can +be verified, do not add the module or keep it in the implementation backlog. + +**Known exclusions:** `_testlimitedcapi` is a CPython-only test helper, and +PyPy has no `_datetime` extension module. Do not implement either one; preserve +and repair PyPy's pure-Python `datetime` path when the public module is broken. + ## The JIT is generated from the interpreter source pyre is structured like PyPy: `pyre-interpreter` is the RPython-interpreter diff --git a/pyre/extra_tests/snippets/builtin_complex.py b/pyre/extra_tests/snippets/builtin_complex.py index 136f26ef001..f57f114ec6c 100644 --- a/pyre/extra_tests/snippets/builtin_complex.py +++ b/pyre/extra_tests/snippets/builtin_complex.py @@ -226,6 +226,7 @@ def __eq__(self, other): # __complex__ z = 3 + 4j assert z.__complex__() == z +assert z.__complex__() is z assert type(z.__complex__()) == complex diff --git a/pyre/extra_tests/snippets/builtin_exceptions.py b/pyre/extra_tests/snippets/builtin_exceptions.py index 8879e130bc2..9b478c24dfa 100644 --- a/pyre/extra_tests/snippets/builtin_exceptions.py +++ b/pyre/extra_tests/snippets/builtin_exceptions.py @@ -78,6 +78,50 @@ def __init__(self, value): assert e.value == "test" +class KeywordException(Exception): + def __init__(self, *, value): + self.value = value + + +exc = KeywordException(value="test") +assert exc.value == "test" +assert exc.args == () + +# PyPy `check_and_find_best_base`: fieldless exception aliases reuse their +# parent's Layout, while concrete W_* siblings conflict. +class CompatibleException(ValueError, OSError): + pass + + +assert issubclass(CompatibleException, ValueError) +assert issubclass(CompatibleException, OSError) + +try: + class ConflictingUnicodeErrors(UnicodeTranslateError, UnicodeEncodeError): + pass +except TypeError as exc: + assert "layout conflict" in str(exc) or "lay-out conflict" in str(exc) +else: + assert False, "conflicting Unicode error layouts accepted" + +# `interp_group.W_BaseExceptionGroup` owns a concrete child Layout while +# `W_ExceptionGroup` reuses it. Fieldless exception aliases remain compatible +# and the group becomes the best base; concrete sibling layouts conflict. +class CompatibleGroup(ArithmeticError, ExceptionGroup): + pass + + +assert CompatibleGroup.__base__ is ExceptionGroup + +try: + class ConflictingGroup(OSError, BaseExceptionGroup): + pass +except TypeError as exc: + assert "layout conflict" in str(exc) or "lay-out conflict" in str(exc) +else: + assert False, "BaseExceptionGroup sibling layout conflict accepted" + + exc = SyntaxError("msg", 1, 2, 3, 4, 5) assert exc.msg == "msg" assert exc.filename is None diff --git a/pyre/extra_tests/snippets/builtin_property.py b/pyre/extra_tests/snippets/builtin_property.py index 441214ccc87..ad0714bedbe 100644 --- a/pyre/extra_tests/snippets/builtin_property.py +++ b/pyre/extra_tests/snippets/builtin_property.py @@ -1,3 +1,5 @@ +import pickle + from testutils import assert_raises @@ -119,3 +121,10 @@ class SlottedProperty(property): with assert_raises(TypeError) as caught: property().__set_name__(owner=object, name="value") assert str(caught.exception) == "property.__set_name__() takes no keyword arguments" + +# A property stores four native descriptor fields which an empty __newobj__ +# cannot reconstruct. CPython 3.14 rejects the inherited object reducer. +with assert_raises(TypeError): + property().__reduce_ex__(pickle.HIGHEST_PROTOCOL) +with assert_raises(TypeError): + pickle.dumps(property()) diff --git a/pyre/extra_tests/snippets/builtin_set.py b/pyre/extra_tests/snippets/builtin_set.py index fb948744ad6..55bcbaa28c0 100644 --- a/pyre/extra_tests/snippets/builtin_set.py +++ b/pyre/extra_tests/snippets/builtin_set.py @@ -90,6 +90,15 @@ class S(set): assert repr(S()) == "S()" assert repr(S([1, 2, 3])) == "S({1, 2, 3})" + +class SetCustomRepr(set): + def __repr__(self): + return "" + + +assert repr(SetCustomRepr()) == "" +assert repr(SetCustomRepr([1, 2, 3])) == "" + recursive = S() recursive.add(Hashable(recursive)) assert repr(recursive) == "S({S(...)})" @@ -447,6 +456,17 @@ class FS(frozenset): assert repr(FS([1, 2, 3])) == "FS({1, 2, 3})" +class FrozenSetCustomRepr(frozenset): + def __repr__(self): + return "" + + +assert repr(FrozenSetCustomRepr()) == "" +assert repr(FrozenSetCustomRepr([1, 2, 3])) == ( + "" +) + + class MutatingSetKey: enabled = False target = None diff --git a/pyre/extra_tests/snippets/builtin_typedef_census.py b/pyre/extra_tests/snippets/builtin_typedef_census.py index 4ffc6b4e9c7..896a9684484 100644 --- a/pyre/extra_tests/snippets/builtin_typedef_census.py +++ b/pyre/extra_tests/snippets/builtin_typedef_census.py @@ -6,6 +6,8 @@ import sys import types import weakref +from array import array +from collections import deque from testutils import assert_raises @@ -72,6 +74,35 @@ def _absent(tp, name): assert isinstance(memoryview.__doc__, str) assert "memoryview" in memoryview.__doc__ +for view_type in (type({}.keys()), type({}.values()), type({}.items())): + assert "__doc__" in view_type.__dict__ + assert view_type.__dict__["__doc__"] is None + +# `typeobject.py ensure_common_attributes` gives every TypeDef an own doc +# entry; `ensure_hash` suppresses an inherited object hash when equality is +# defined locally. +assert "__doc__" in array.__dict__ +assert array.__doc__.startswith("array(typecode [, initializer]) -> array\n") +assert "itemsize -- the length in bytes of one array item" in array.__doc__ +assert array.__dict__["__hash__"] is None +assert_raises(TypeError, hash, array("i")) + +assert deque.__dict__["__doc__"] == ( + "A list-like sequence optimized for data accesses near its endpoints." +) +for weak_type in ( + weakref.ReferenceType, + weakref.ProxyType, + weakref.CallableProxyType, +): + assert "__doc__" in weak_type.__dict__ + assert weak_type.__dict__["__doc__"] is None + +wrapper_descriptor = type(object.__str__) +assert wrapper_descriptor.__name__ == "wrapper_descriptor" +assert "__repr__" in wrapper_descriptor.__dict__ +assert wrapper_descriptor.__dict__["__repr__"](object.__str__) == repr(object.__str__) + # --- SPEC-omit: PyPy TypeDef key, CPython 3.14 type dict has no such key --- _absent(bool, "__str__") diff --git a/pyre/extra_tests/snippets/closure_over_parameter_not_double_wrapped.py b/pyre/extra_tests/snippets/closure_over_parameter_not_double_wrapped.py index b3f8a4b84a7..bce0b3fe7c0 100644 --- a/pyre/extra_tests/snippets/closure_over_parameter_not_double_wrapped.py +++ b/pyre/extra_tests/snippets/closure_over_parameter_not_double_wrapped.py @@ -9,3 +9,30 @@ def add(x): result = make_adder(10)(5) assert result == 15 + +# A cell passed as the captured argument is still an ordinary Python value. +# The new closure must therefore contain a distinct outer cell whose contents +# are the argument cell, rather than mistaking that argument for its own +# closure container. PyPy `PyFrame.init_cells` gets this from its separate +# argument/cell slots; pyre's unified locals-plus slot must preserve it too. +def external_cell(): + value = 42 + + def inner(): + return value + + return inner.__closure__[0] + + +cell_ext = external_cell() +def capture(arg): + def read(): + return arg + + return read + + +read = capture(cell_ext) +cell_closure = read.__closure__[0] +assert read() is cell_ext +assert cell_closure is not cell_ext diff --git a/pyre/extra_tests/snippets/pickle_native_getstate.py b/pyre/extra_tests/snippets/pickle_native_getstate.py new file mode 100644 index 00000000000..471c610d2d4 --- /dev/null +++ b/pyre/extra_tests/snippets/pickle_native_getstate.py @@ -0,0 +1,74 @@ +# pyre-check: gate=1 +# `object_getstate` calls an overriding `__getstate__` and only falls back to +# `object_getstate_default(required)` when the type still uses +# `object.__getstate__`. The `required` refusal therefore stops at the +# boundary that hook draws: a native layout publishing one is rebuilt from the +# state it returns, and one that publishes none cannot be rebuilt through an +# empty `__newobj__` call. +# +# `__reduce_ex__` is called directly rather than through `pickle`: this +# directory carries its own `_pickle.py`, which shadows the stdlib extension +# module and stops `import pickle` from working here. That keeps this file +# green under CPython too, unlike its `pickle_*` neighbours, which is what +# lets it be gated. +import io +import itertools +import types + + +def refuses(obj): + try: + obj.__reduce_ex__(2) + except TypeError as e: + assert "cannot pickle" in str(e), (obj, str(e)) + return True + return False + + +# --- publishes `__getstate__`: reduces through the hook --------------------- +b = io.BytesIO(b"abcdef") +b.seek(2) +b.tag = "kept" +assert io.BytesIO.__getstate__ is not object.__getstate__ +newobj, args, state, listitems, dictitems = b.__reduce_ex__(2) +assert args == (io.BytesIO,), args +assert state == (b"abcdef", 2, {"tag": "kept"}), state +assert listitems is None and dictitems is None, (listitems, dictitems) + +s = io.StringIO("hello") +s.read(2) +assert io.StringIO.__getstate__ is not object.__getstate__ +state = s.__reduce_ex__(2)[2] +# `(value, readnl, pos, dict)`; the trailing dict is empty here and pyre and +# CPython spell an empty one differently, so only value and pos are pinned. +assert state[0] == "hello", state +assert state[2] == 2, state + +# --- publishes a refusing `__getstate__`: the hook owns the refusal --------- +w = io.BufferedWriter(io.BytesIO()) +assert io.BufferedWriter.__getstate__ is not object.__getstate__ +assert refuses(w) + +# --- publishes none: `object_getstate_default(required)` refuses ------------ +for obj in ( + types.ModuleType("m"), + property(), + staticmethod(len), + classmethod(len), + itertools.count(), +): + assert type(obj).__getstate__ is object.__getstate__, obj + assert refuses(obj), obj + + +# The refusal reaches neither an ordinary instance nor a list or a dict. +class C: + def __init__(self): + self.x = 1 + + +assert C().__reduce_ex__(2)[2] == {"x": 1} +assert list(([1, 2]).__reduce_ex__(2)[3]) == [1, 2] +assert list(({"a": 1}).__reduce_ex__(2)[4]) == [("a", 1)] + +print("pickle_native_getstate OK") diff --git a/pyre/extra_tests/snippets/stdlib_collections.py b/pyre/extra_tests/snippets/stdlib_collections.py index cf971efa919..0c655fc0607 100644 --- a/pyre/extra_tests/snippets/stdlib_collections.py +++ b/pyre/extra_tests/snippets/stdlib_collections.py @@ -1,4 +1,5 @@ from collections import defaultdict, deque +from testutils import assert_raises # Python 3.14's defaultdict.__missing__ preserves a value installed by a @@ -73,6 +74,67 @@ def __setattr__(self, name, value): assert deque([1, 2, 3], 4) * 2 == deque([3, 1, 2, 3]) + +class DequeRepeatIndex: + def __index__(self): + # The receiver and count must stay rooted across arbitrary Python code. + import gc + + gc.collect() + return 2 + + +class DequeRepeatReflected: + def __rmul__(self, other): + return ("reflected", other) + + +class DequeRepeatOverride(deque): + def __mul__(self, other): + return ("override", other) + + def __rmul__(self, other): + return ("reflected override", other) + + +class SlottedDeque(deque): + __slots__ = ("slot_value", "__dict__") + + +repeat_source = deque([1, 2]) +assert repeat_source * DequeRepeatIndex() == deque([1, 2, 1, 2]) +assert DequeRepeatIndex() * repeat_source == deque([1, 2, 1, 2]) +assert repeat_source * DequeRepeatReflected() == ("reflected", repeat_source) +repeat_override = DequeRepeatOverride([1]) +assert repeat_override * 3 == ("override", 3) +assert 3 * repeat_override == ("reflected override", 3) +assert_raises(TypeError, deque.__mul__, repeat_source, object()) +assert_raises(OverflowError, lambda: repeat_source * (10**100)) + +slotted_deque = SlottedDeque([1]) +slotted_deque.slot_value = 2 +slotted_deque.dict_value = 3 +assert slotted_deque.__dict__ == {"dict_value": 3} +assert slotted_deque.__getstate__() == ( + {"dict_value": 3}, + {"slot_value": 2}, +) +del slotted_deque.slot_value +assert_raises(AttributeError, getattr, slotted_deque, "slot_value") + +repeat_big = deque([0]) +repeat_big *= 2**8 +assert_raises(MemoryError, lambda: repeat_big * (2**56)) +assert_raises(MemoryError, lambda: (2**56) * repeat_big) + + +def repeat_big_in_place(): + value = repeat_big.copy() + value *= 2**56 + + +assert_raises(MemoryError, repeat_big_in_place) + # Optional constructor args, including the `maxlen` keyword form. assert deque(maxlen=5).maxlen == 5 assert deque().maxlen is None diff --git a/pyre/extra_tests/snippets/stdlib_itertools.py b/pyre/extra_tests/snippets/stdlib_itertools.py index ce7a494713a..3bdce1e3d74 100644 --- a/pyre/extra_tests/snippets/stdlib_itertools.py +++ b/pyre/extra_tests/snippets/stdlib_itertools.py @@ -1,4 +1,5 @@ import itertools +import pickle from testutils import assert_raises @@ -62,6 +63,15 @@ with assert_raises(StopIteration): next(x) +# Python 3.14 deliberately omits PyPy's historical chain pickle state +# methods. The inherited object reducer rejects this native iterator, and no +# instance-only fallback may make __setstate__ appear outside the TypeDef. +x = chain([1], [2]) +with assert_raises(TypeError): + pickle.dumps(x) +with assert_raises(AttributeError): + x.__setstate__ + # itertools.count tests # default arguments @@ -143,6 +153,44 @@ with assert_raises(TypeError): itertools.cycle(10) +# Same 3.14 TypeDef decision as chain: cycle is not picklable and exposes no +# __setstate__, even after iteration has populated its saved-value buffer. +r = itertools.cycle([1, 2]) +assert next(r) == 1 +with assert_raises(TypeError): + pickle.dumps(r) +with assert_raises(AttributeError): + r.__setstate__ + +# None of the 3.14 itertools native layouts supplies enough state to the +# inherited object reducer. Constructors which have an explicit TypeDef +# reducer are handled before this path; every remaining common family must be +# rejected here instead of producing a superficially valid empty __newobj__. +unpickleable_native_iterators = [ + itertools.accumulate([1]), + itertools.combinations([1], 1), + itertools.combinations_with_replacement([1], 1), + itertools.compress([1], [1]), + itertools.count(), + itertools.dropwhile(bool, [1]), + itertools.filterfalse(bool, [1]), + itertools.groupby([1]), + itertools.islice([1], 1), + itertools.pairwise([1]), + itertools.permutations([1]), + itertools.product([1]), + itertools.repeat(1, 1), + itertools.starmap(lambda: 1, [()]), + itertools.takewhile(bool, [1]), + itertools.tee([1])[0], + itertools.zip_longest([1]), +] +for native_iterator in unpickleable_native_iterators: + with assert_raises(TypeError): + native_iterator.__reduce_ex__(pickle.HIGHEST_PROTOCOL) + with assert_raises(TypeError): + pickle.dumps(native_iterator) + # itertools.repeat tests # no times diff --git a/pyre/extra_tests/snippets/stdlib_weakref.py b/pyre/extra_tests/snippets/stdlib_weakref.py index d410f8cf6c6..198962e4612 100644 --- a/pyre/extra_tests/snippets/stdlib_weakref.py +++ b/pyre/extra_tests/snippets/stdlib_weakref.py @@ -26,6 +26,7 @@ class X: assert repr(b).startswith(" bool { for &w_other in keys_w { if crate::baseobjspace::is_w(w_other, w_key) { return true; } unsafe { - // Compared as raw buffers: `**{'\ud800': 1}` puts a lone surrogate - // in a keyword name, which has no `&str` spelling. if pyre_object::is_str(w_other) && pyre_object::is_str(w_key) - && pyre_object::w_str_get_wtf8(w_other) == pyre_object::w_str_get_wtf8(w_key) + && pyre_object::w_str_eq_w(w_other, w_key) { return true; } diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 8f69ea80ca5..d42d76f0e50 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -4648,8 +4648,9 @@ pub fn is_w(w_one: PyObjectRef, w_two: PyObjectRef) -> bool { } // `W_AbstractBytesObject.is_w` (bytesobject.py): for distinct // exact-`bytes` operands, `len(s2) > 1` returns `s1 is s2` (storage - // identity) — distinct `bytes` never share their backing buffer, so - // `false`; `len(s2) == 0` returns `len(s1) == 0`; `len(s2) == 1` + // identity); BytesListStrategy re-wraps the same erased rpython string, + // so distinct wrappers may deliberately share that backing block. + // `len(s2) == 0` returns `len(s1) == 0`; `len(s2) == 1` // (unique-ified) returns `len(s1) == 1 && s1[0] == s2[0]`. if pyre_object::pyobject::is_exact_type(w_one, &pyre_object::bytesobject::BYTES_TYPE) && pyre_object::pyobject::is_exact_type(w_two, &pyre_object::bytesobject::BYTES_TYPE) @@ -4657,7 +4658,8 @@ pub fn is_w(w_one: PyObjectRef, w_two: PyObjectRef) -> bool { let len1 = pyre_object::bytesobject::w_bytes_len(w_one); let len2 = pyre_object::bytesobject::w_bytes_len(w_two); if len2 > 1 { - return false; + return pyre_object::bytesobject::w_bytes_block(w_one) + == pyre_object::bytesobject::w_bytes_block(w_two); } if len2 == 0 { return len1 == 0; @@ -5155,6 +5157,9 @@ pub(crate) fn native_slot_get( if unsafe { pyre_object::is_list(obj) } { return Ok(unsafe { pyre_object::listobject::w_list_slot_get(obj, index as usize) }); } + if crate::module::_collections::is_deque(obj) { + return Ok(unsafe { crate::module::_collections::deque_slot_get(obj, index as usize) }); + } if unsafe { pyre_object::weakref::is_typed_weakref(obj) } { return Ok(unsafe { pyre_object::weakref::w_weakref_object_slot_get(obj, index as usize) }); } @@ -5199,6 +5204,10 @@ pub(crate) fn native_slot_set( unsafe { pyre_object::listobject::w_list_slot_set(obj, index as usize, value) }; return Ok(true); } + if crate::module::_collections::is_deque(obj) { + unsafe { crate::module::_collections::deque_slot_set(obj, index as usize, value) }; + return Ok(true); + } if unsafe { pyre_object::weakref::is_typed_weakref(obj) } { unsafe { pyre_object::weakref::w_weakref_object_slot_set(obj, index as usize, value) }; return Ok(true); @@ -5235,6 +5244,9 @@ pub(crate) fn native_slot_del(obj: PyObjectRef, name: &str, index: u32) -> Resul if unsafe { pyre_object::is_list(obj) } { return Ok(unsafe { pyre_object::listobject::w_list_slot_del(obj, index as usize) }); } + if crate::module::_collections::is_deque(obj) { + return Ok(unsafe { crate::module::_collections::deque_slot_del(obj, index as usize) }); + } if unsafe { pyre_object::weakref::is_typed_weakref(obj) } { return Ok(unsafe { pyre_object::weakref::w_weakref_object_slot_del(obj, index as usize) }); } @@ -5791,9 +5803,9 @@ fn getattr_str_impl(obj: PyObjectRef, name: &str, call_getattr: bool, suppress: // `__thisclass__`, ...) remain visible. } - // Native itertools fallback methods. The corresponding TypeDefs expose - // these slots directly; this path remains for the iterator families whose - // TypeDefs have not yet been installed. + // Native itertools fallback methods. Every concrete iterator TypeDef now + // exposes the iterator slots directly; keep this dispatch only as the + // interpreter-level next/iter adapter used by those implementations. unsafe { if pyre_object::interp_itertools::is_count(obj) || pyre_object::interp_itertools::is_repeat(obj) @@ -5817,21 +5829,6 @@ fn getattr_str_impl(obj: PyObjectRef, name: &str, call_getattr: bool, suppress: let entry: Option<(fn(&[PyObjectRef]) -> PyResult, &str, u16)> = match name { "__next__" => Some((iter_next_method, "__next__", 1)), "__iter__" => Some((iter_self_method, "__iter__", 1)), - // pairwise exposes no additional methods; cycle and chain - // still use the old PyPy pickle fallbacks until their own - // TypeDefs are ported. - "__reduce__" if pyre_object::interp_itertools::is_cycle(obj) => { - Some((cycle_reduce_method, "__reduce__", 1)) - } - "__setstate__" if pyre_object::interp_itertools::is_cycle(obj) => { - Some((cycle_setstate_method, "__setstate__", 2)) - } - "__reduce__" if pyre_object::interp_itertools::is_chain(obj) => { - Some((chain_reduce_method, "__reduce__", 1)) - } - "__setstate__" if pyre_object::interp_itertools::is_chain(obj) => { - Some((chain_setstate_method, "__setstate__", 2)) - } _ => None, }; if let Some((func, sname, arity)) = entry { @@ -18284,124 +18281,6 @@ pub(crate) fn iter_self_method(args: &[PyObjectRef]) -> PyResult { Ok(obj) } -/// `cycle.__reduce__` — `interp_itertools.py W_Cycle.descr_reduce`: -/// `(type(self), (iterable,), (list(saved), index))`. The saved buffer is -/// copied into a fresh list (`space.newlist(self.saved_w)`) so later -/// cycling cannot mutate the pickled state. -fn cycle_reduce_method(args: &[PyObjectRef]) -> PyResult { - // Capture every field before any allocation (`w_list_new` / - // `w_tuple_new` may collect): the saved elements go into a `Vec` - // that `w_list_new` pins, and `w_iterable` / `index` are read up - // front rather than across an allocation. - let w_type = crate::typedef::r#type(args[0]).map_or(PY_NULL, |p| p.as_ptr()); - let it = unsafe { &*(args[0] as *const pyre_object::interp_itertools::W_Cycle) }; - let w_iterable = it.w_iterable; - let index = it.index; - let n = unsafe { pyre_object::w_list_len(it.saved) }; - let mut saved = Vec::with_capacity(n); - for i in 0..n as i64 { - saved.push( - unsafe { pyre_object::w_list_getitem(it.saved, i) } - .expect("cycle saved index in range"), - ); - } - let state = w_tuple_new(vec![w_list_new(saved), w_int_new(index)]); - Ok(w_tuple_new(vec![ - w_type, - w_tuple_new(vec![w_iterable]), - state, - ])) -} - -/// `cycle.__setstate__` — `interp_itertools.py W_Cycle.descr_setstate`: -/// unpack `(saved, index)`, replace `saved_w` with a fresh list of the -/// unpacked elements, and restore `index`. Reassigning the `saved` -/// pointer field requires the GC write barrier so an old→young edge is -/// recorded. -fn cycle_setstate_method(args: &[PyObjectRef]) -> PyResult { - // `unpackiterable` iterates the pickled state and may collect; pin the - // receiver and the state tuple so they (and, transitively, the saved - // list reached through the tuple) survive each iteration. - let _roots = pyre_object::gc_roots::push_roots(); - let w_self = args[0]; - let w_state = if args.len() > 1 { args[1] } else { w_none() }; - let w_self = pyre_object::gc_roots::pin_root(w_self); - let w_state = pyre_object::gc_roots::pin_root(w_state); - let state_w = unpackiterable(w_state, 2)?; - let saved_w = unpackiterable(state_w[0], -1)?; - let w_saved = w_list_new(saved_w); - let index = int_w(state_w[1])?; - let it = unsafe { &mut *(w_self as *mut pyre_object::interp_itertools::W_Cycle) }; - it.saved = w_saved; - pyre_object::gc_hook::try_gc_write_barrier(w_self as *mut u8); - it.index = index; - Ok(w_none()) -} - -/// `chain.__reduce__` — `interp_itertools.py W_Chain.descr_reduce`. While -/// the chain still has a live `w_iterables` the state carries `(w_iterables,)` -/// or `(w_iterables, w_it)` so `__setstate__` can restore both; the args -/// tuple is empty because `chain()` takes no positional arguments (the -/// iterables come back through `__setstate__`). A spent chain -/// (`w_iterables is None`) reduces to just `(type, ())`. -fn chain_reduce_method(args: &[PyObjectRef]) -> PyResult { - // Read the pointer fields before any allocation (`w_tuple_new` may - // collect); `w_type` mirrors `space.type(self)`. - let w_type = crate::typedef::r#type(args[0]).map_or(PY_NULL, |p| p.as_ptr()); - let w_iterables = unsafe { pyre_object::interp_itertools::w_chain_get_iterables(args[0]) }; - let w_it = unsafe { pyre_object::interp_itertools::w_chain_get_it(args[0]) }; - if !w_iterables.is_null() { - let inner = if !w_it.is_null() { - vec![w_iterables, w_it] - } else { - vec![w_iterables] - }; - Ok(w_tuple_new(vec![ - w_type, - w_tuple_new(vec![]), - w_tuple_new(inner), - ])) - } else { - Ok(w_tuple_new(vec![w_type, w_tuple_new(vec![])])) - } -} - -/// `chain.__setstate__` — `interp_itertools.py W_Chain.descr_setstate`: -/// unpack the pickled state and restore `w_iterables` (and, with two -/// elements, `w_it`). Reassigning the pointer fields records an old→young -/// edge, so the setters run the GC write barrier. -fn chain_setstate_method(args: &[PyObjectRef]) -> PyResult { - // `unpackiterable` iterates the pickled state and may collect; pin the - // receiver and the state so they (and, transitively, the unpacked - // elements) survive each iteration. - let _roots = pyre_object::gc_roots::push_roots(); - let w_self = args[0]; - let w_state = if args.len() > 1 { args[1] } else { w_none() }; - let w_self = pyre_object::gc_roots::pin_root(w_self); - let w_state = pyre_object::gc_roots::pin_root(w_state); - let state = unpackiterable(w_state, -1)?; - let n = state.len(); - if n < 1 { - return Err(PyError::type_error(format!( - "function takes at least 1 argument ({n} given)" - ))); - } else if n == 1 { - unsafe { - pyre_object::interp_itertools::w_chain_set_iterables(w_self, state[0]); - } - } else if n == 2 { - unsafe { - pyre_object::interp_itertools::w_chain_set_iterables(w_self, state[0]); - pyre_object::interp_itertools::w_chain_set_it(w_self, state[1]); - } - } else { - return Err(PyError::type_error(format!( - "function takes at most 2 arguments ({n} given)" - ))); - } - Ok(w_none()) -} - /// PyPy: GeneratorIterator.descr_send(w_arg) pub(crate) fn generator_send_method(args: &[PyObjectRef]) -> PyResult { let gen_obj = if args.is_empty() { diff --git a/pyre/pyre-interpreter/src/builtins.rs b/pyre/pyre-interpreter/src/builtins.rs index d9939b5f6c8..934e3fe9bfe 100644 --- a/pyre/pyre-interpreter/src/builtins.rs +++ b/pyre/pyre-interpreter/src/builtins.rs @@ -8155,13 +8155,17 @@ fn exc_unicode_encode_error_init(args: &[PyObjectRef]) -> ResultObject. +/// PyPy: `_new.descr_new_base_exception` unpacks `__args__`, stores `args_w`, +/// and deliberately ignores `kwds_w`; each exception type's descr__new__ then +/// creates a W_Object. Pyre's flat builtin ABI carries those keywords +/// in a trailing marker dict, so remove it before constructing `args_w`. macro_rules! exc_new_wrapper { ($wrapper:ident, $ctor:ident) => { pub(crate) fn $wrapper(args: &[PyObjectRef]) -> Result { let cls = args.first().copied(); let rest: &[PyObjectRef] = if args.is_empty() { args } else { &args[1..] }; - let exc = $ctor(rest)?; + let (positional, _) = split_builtin_kwargs(rest); + let exc = $ctor(positional)?; // Set the exception's w_class to the actual exception type (e.g. AssertionError) // so that `type(e) is AssertionError` holds and `except ExcType` via isinstance works. if let Some(cls) = cls { @@ -8489,6 +8493,42 @@ fn make_exc_type_with_doc( make_exc_type_with_init(name, Some(doc), new_fn, None, base) } +/// The interpreter class that owns an exception TypeDef's instance layout. +/// +/// `interp_exceptions.py` uses `_new_exception` for fieldless classes; those +/// keep their base interpreter class and therefore reuse its Layout. The +/// concrete `class W_*` definitions below introduce fields and each owns a +/// child Layout: `W_BaseException`, `W_ImportError`, +/// `W_UnicodeTranslateError`, `W_StopIteration`, `W_OSError`, `W_NameError`, +/// `W_SyntaxError`, `W_SystemExit`, `W_UnicodeDecodeError`, +/// `W_AttributeError`, `W_UnicodeEncodeError`, and +/// `interp_group.W_BaseExceptionGroup`. +fn exception_layout_pytype(name: &str, base: PyObjectRef) -> *const pyre_object::PyType { + use pyre_object::interp_exceptions as exc; + match name { + "BaseException" => &exc::EXCEPTION_TYPE, + "ImportError" => &exc::EXC_IMPORT_ERROR_TYPE, + "UnicodeTranslateError" => &exc::EXC_UNICODE_TRANSLATE_ERROR_TYPE, + "StopIteration" => &exc::EXC_STOP_ITERATION_TYPE, + "OSError" => &exc::EXC_OS_ERROR_TYPE, + "NameError" => &exc::EXC_NAME_ERROR_TYPE, + "SyntaxError" => &exc::EXC_SYNTAX_ERROR_TYPE, + "SystemExit" => &exc::EXC_SYSTEM_EXIT_TYPE, + "UnicodeDecodeError" => &exc::EXC_UNICODE_DECODE_ERROR_TYPE, + "AttributeError" => &exc::EXC_ATTRIBUTE_ERROR_TYPE, + "UnicodeEncodeError" => &exc::EXC_UNICODE_ENCODE_ERROR_TYPE, + "BaseExceptionGroup" => &exc::EXC_BASE_EXCEPTION_GROUP_LAYOUT_TYPE, + _ => unsafe { + let parent = pyre_object::w_type_get_layout_ptr(base); + if parent.is_null() { + &exc::EXCEPTION_TYPE + } else { + (*parent).typedef + } + }, + } +} + /// Variant of `make_exc_type` that also installs a per-class `__init__` /// descriptor. Used for the three Unicode*Error subclasses whose PyPy /// `descr_init` does typed slot stamping after `__new__`'s raw @@ -8507,11 +8547,12 @@ pub(crate) fn make_exc_type_with_init( if let Some(cls) = lookup_exc_class(name) { return cls; } - // Every exception class shares one instance layout, distinct from - // `object`'s: `class E(Exception, ValueError)` is fine, `class E(Exception, - // list)` is an instance lay-out conflict. The subclasses reach the same - // Layout object through the reuse rule (their parent layout already names - // this typedef). + // `setup_builtin_type` reuses the parent's Layout for `_new_exception` + // aliases and creates a child for each concrete `W_*` interpreter class. + // That graph is what makes e.g. `(ValueError, OSError)` compatible while + // rejecting the sibling layouts `(UnicodeTranslateError, + // UnicodeEncodeError)`. + let layout_pytype = exception_layout_pytype(name, base); let cls = crate::typedef::make_builtin_type_with_layout( name, move |ns| { @@ -8995,7 +9036,7 @@ pub(crate) fn make_exc_type_with_init( } }, base, - &pyre_object::interp_exceptions::EXCEPTION_TYPE as *const pyre_object::PyType, + layout_pytype, ); if name == "SyntaxError" { for member_name in [ @@ -9738,7 +9779,8 @@ fn make_exception_group_type( if let Some(cls) = lookup_exc_class(name) { return cls; } - let cls = crate::typedef::make_builtin_type_with_bases( + let layout_pytype = exception_layout_pytype(name, bases[0]); + let cls = crate::typedef::make_builtin_type_with_bases_and_layout( name, move |ns| { let _roots = pyre_object::gc_roots::push_roots(); @@ -9835,6 +9877,7 @@ fn make_exception_group_type( } }, bases, + layout_pytype, ); if name == "BaseExceptionGroup" { for member_name in ["message", "exceptions"] { @@ -19832,6 +19875,65 @@ mod tests { } } + #[test] + fn exception_new_ignores_keyword_marker_when_storing_args() { + let _ = new_builtin_module_dict(); + let cls = lookup_exc_class("Exception").unwrap(); + let kwargs = pyre_object::w_dict_new(); + unsafe { + pyre_object::w_dict_store( + kwargs, + pyre_object::w_str_new("x"), + pyre_object::w_int_new(8), + ); + pyre_object::w_dict_store( + kwargs, + pyre_object::kw_marker::w_kw_marker_key(), + pyre_object::kw_marker::w_kw_marker_sentinel(), + ); + } + + let exc = exc_exception_new(&[cls, kwargs]).unwrap(); + let stored = unsafe { pyre_object::interp_exceptions::w_exception_get_args_storage(exc) }; + assert_eq!(unsafe { pyre_object::w_list_len(stored) }, 0); + } + + #[test] + fn exception_layouts_follow_concrete_pypy_interpreter_classes() { + let _ = new_builtin_module_dict(); + let value_error = lookup_exc_class("ValueError").unwrap(); + let os_error = lookup_exc_class("OSError").unwrap(); + let compatible = pyre_object::w_tuple_new(vec![value_error, os_error]); + let best = unsafe { crate::call::check_and_find_best_base(compatible) }.unwrap(); + assert!(std::ptr::eq(best, os_error)); + + let translate = lookup_exc_class("UnicodeTranslateError").unwrap(); + let encode = lookup_exc_class("UnicodeEncodeError").unwrap(); + let conflicting = pyre_object::w_tuple_new(vec![translate, encode]); + let error = unsafe { crate::call::check_and_find_best_base(conflicting) }.unwrap_err(); + assert_eq!(error.kind, crate::PyErrorKind::TypeError); + assert_eq!( + error.message_text(), + "instance layout conflicts in multiple inheritance" + ); + + let arithmetic = lookup_exc_class("ArithmeticError").unwrap(); + let exception_group = lookup_exc_class("ExceptionGroup").unwrap(); + let compatible_group = pyre_object::w_tuple_new(vec![arithmetic, exception_group]); + let best = unsafe { crate::call::check_and_find_best_base(compatible_group) }.unwrap(); + assert!(std::ptr::eq(best, exception_group)); + + let base_exception_group = lookup_exc_class("BaseExceptionGroup").unwrap(); + let conflicting_group = pyre_object::w_tuple_new(vec![os_error, base_exception_group]); + let error = + unsafe { crate::call::check_and_find_best_base(conflicting_group) }.unwrap_err(); + assert_eq!(error.kind, crate::PyErrorKind::TypeError); + assert_eq!( + error.message_text(), + "instance layout conflicts in multiple inheritance" + ); + } + #[test] fn in_memory_file_write_overwrites_at_the_seek_position() { let mut data = b"PK\0\0payload".to_vec(); diff --git a/pyre/pyre-interpreter/src/call.rs b/pyre/pyre-interpreter/src/call.rs index ab5f0538a8a..d43808455d7 100644 --- a/pyre/pyre-interpreter/src/call.rs +++ b/pyre/pyre-interpreter/src/call.rs @@ -6169,13 +6169,24 @@ pub(crate) unsafe fn check_and_find_best_base( continue; } let layout = pyre_object::w_type_get_layout_ptr(w_base); + // PyPy's concrete W_* exception classes have distinct + // TypeDefs in one inherited Layout chain. Pyre stores + // their fields in the flattened W_BaseException union, + // so distinct native PyType tags inside that family are + // physically compatible exactly when `issublayout` says + // they are. Keep the native-layout guard for unrelated + // Rust structs, where semantic inheritance alone does not + // make their allocations prefix-compatible. + let exception_layouts = + is_exception_layout(best_layout) && is_exception_layout(layout); let native_layout_conflict = !layout.is_null() && !std::ptr::eq((*best_layout).typedef, (*layout).typedef) && !std::ptr::eq( (*best_layout).typedef, &pyre_object::pyobject::INSTANCE_TYPE, ) - && !std::ptr::eq((*layout).typedef, &pyre_object::pyobject::INSTANCE_TYPE); + && !std::ptr::eq((*layout).typedef, &pyre_object::pyobject::INSTANCE_TYPE) + && !exception_layouts; if !layout.is_null() && (!(*best_layout).issublayout(layout) || native_layout_conflict) { @@ -6190,6 +6201,29 @@ pub(crate) unsafe fn check_and_find_best_base( } } +/// Whether `layout` belongs to the inherited `W_BaseException` interpreter +/// layout chain. +/// +/// Layout-only TypeDef identities such as PyPy's +/// `interp_group.W_BaseExceptionGroup` are not object vtables and therefore +/// have no `subclassrange` entry. Walk the Layout ownership chain itself, +/// exactly the axis `Layout.issublayout` uses, instead of asking +/// `ll_issubclass` about an allocation vtable. +unsafe fn is_exception_layout(mut layout: *const pyre_object::typeobject::Layout) -> bool { + unsafe { + while !layout.is_null() { + if std::ptr::eq( + (*layout).typedef, + &pyre_object::interp_exceptions::EXCEPTION_TYPE, + ) { + return true; + } + layout = (*layout).base_layout; + } + false + } +} + /// typedef.py `acceptable_as_base_class = '__new__' in rawdict`. /// typeobject.py:1116 checks this flag on the bestbase. unsafe fn is_acceptable_base_class(w_type: pyre_object::PyObjectRef) -> bool { diff --git a/pyre/pyre-interpreter/src/display.rs b/pyre/pyre-interpreter/src/display.rs index 96425a0ed80..58b55c1f538 100644 --- a/pyre/pyre-interpreter/src/display.rs +++ b/pyre/pyre-interpreter/src/display.rs @@ -488,6 +488,7 @@ pub(crate) unsafe fn builtin_subclass_dunder_obj( || std::ptr::eq(tp, &STR_TYPE as *const PyType) || std::ptr::eq(tp, &pyre_object::LIST_TYPE as *const PyType) || pyre_object::is_tuple(obj) + || pyre_object::is_set_or_frozenset(obj) || std::ptr::eq( tp, &pyre_object::bytearrayobject::BYTEARRAY_TYPE as *const PyType, @@ -554,6 +555,63 @@ pub(crate) unsafe fn builtin_subclass_dunder_obj( } } +/// `setobject.py W_BaseSetObject.descr_repr` / `setrepr`. +/// +/// This is the native descriptor body, separate from `space.repr`'s special +/// method dispatch. In particular, `set.__repr__(subclass_instance)` must +/// format the backing set instead of redispatching the subclass override that +/// called it. The copied item vector is rooted because each recursive repr +/// can collect and move every item still waiting in it. +pub(crate) unsafe fn set_repr_wtf8(obj: PyObjectRef) -> Result { + unsafe { + let _roots = pyre_object::gc_roots::push_roots(); + let obj_slot = pyre_object::gc_roots::shadow_stack_len(); + let _ = pyre_object::gc_roots::pin_root(obj); + let current_obj = || pyre_object::gc_roots::shadow_stack_get(obj_slot); + + let is_frozen = pyre_object::is_frozenset(current_obj()); + let is_exact_set = + pyre_object::is_exact_type(current_obj(), &pyre_object::setobject::SET_TYPE); + let class_name = crate::typedef::r#type(current_obj()) + .map(|w_type| pyre_object::w_type_get_name(w_type.as_ptr()).to_string()) + .unwrap_or_else(|| { + if is_frozen { + "frozenset".to_string() + } else { + "set".to_string() + } + }); + let Some(_guard) = ReprGuard::enter(current_obj()) else { + return Ok(Wtf8Buf::from_string(format!("{class_name}(...)"))); + }; + let items = pyre_object::w_set_items(current_obj()); + let item_base = pyre_object::gc_roots::pin_roots(&items); + let mut out = Wtf8Buf::new(); + if items.is_empty() { + out.push_str(&class_name); + out.push_str("()"); + return Ok(out); + } + if !is_exact_set { + out.push_str(&class_name); + out.push_str("("); + } + out.push_str("{"); + for index in 0..items.len() { + if index != 0 { + out.push_str(", "); + } + let item = pyre_object::gc_roots::shadow_stack_get(item_base + index); + out.push_wtf8(&py_repr_wtf8(item)?); + } + out.push_str("}"); + if !is_exact_set { + out.push_str(")"); + } + Ok(out) + } +} + /// Resolve a class object's special method on its metaclass. /// /// PyPy: `space.lookup(w_obj, name)` uses `space.type(w_obj)`, so a class @@ -850,41 +908,7 @@ pub unsafe fn py_repr_wtf8(obj: PyObjectRef) -> Result body } } else if pyre_object::is_set_or_frozenset(obj) { - // `pypy/objspace/std/setobject.py W_BaseSetObject.descr_repr` - // → `'%s({%s})' % (typename, items_repr_joined)` for - // frozenset and `'{%s}' % items_repr_joined` for set. Empty - // set keeps the `set()` constructor form. - let is_frozen = pyre_object::is_frozenset(obj); - let is_exact_set = pyre_object::is_exact_type(obj, &pyre_object::setobject::SET_TYPE); - let class_name = crate::typedef::r#type(obj) - .map(|w_type| pyre_object::w_type_get_name(w_type.as_ptr())) - .unwrap_or(if is_frozen { "frozenset" } else { "set" }); - let Some(_guard) = ReprGuard::enter(obj) else { - return Ok(Wtf8Buf::from_string(format!("{class_name}(...)"))); - }; - let items = pyre_object::w_set_items(obj); - let mut out = Wtf8Buf::new(); - if items.is_empty() { - out.push_str(class_name); - out.push_str("()"); - return Ok(out); - } - if !is_exact_set { - out.push_str(class_name); - out.push_str("("); - } - out.push_str("{"); - for (i, &item) in items.iter().enumerate() { - if i != 0 { - out.push_str(", "); - } - out.push_wtf8(&py_repr_wtf8(item)?); - } - out.push_str("}"); - if !is_exact_set { - out.push_str(")"); - } - return Ok(out); + return set_repr_wtf8(obj); } else if std::ptr::eq(tp, &STR_TYPE as *const PyType) { format_wtf8_repr(pyre_object::w_str_get_wtf8(obj)) } else if std::ptr::eq(tp, &NONE_TYPE as *const PyType) { diff --git a/pyre/pyre-interpreter/src/eval.rs b/pyre/pyre-interpreter/src/eval.rs index 29add666ff5..d27a64b5a04 100644 --- a/pyre/pyre-interpreter/src/eval.rs +++ b/pyre/pyre-interpreter/src/eval.rs @@ -3935,15 +3935,17 @@ impl OpcodeStepExecutor for PyFrame { /// closure creation via BUILD_TUPLE + SET_FUNCTION_ATTRIBUTE). /// /// `initialize_frame_scopes` already installs an empty cell for every - /// pure cellvar (a cellvar not shadowing a parameter). Only an - /// argument slot promoted to a cellvar still holds a raw value here, - /// so wrap solely when the slot is not already a cell — otherwise a - /// never-reassigned cellvar like `__class__` would become a - /// cell-wrapping-a-cell, and `fast2locals` / closure reads would - /// surface the inner cell instead of the value. + /// pure cellvar (a cellvar not sharing a locals slot). A cellvar which + /// does share a locals slot must always wrap that slot's raw value here, + /// even when the Python argument itself happens to be a `cell` object. + /// PyPy's `PyFrame.init_cells` has distinct argument and cell slots, so + /// `cell.set(locals[argnum])` naturally preserves this extra level. + /// Only the pre-installed pure-cellvar slot is already the cell which + /// MAKE_CELL denotes (notably the implicit `__class__` cell). fn make_cell(&mut self, idx: usize) -> Result<(), PyError> { let current = locals_w!(self)[idx]; - if current.is_null() || !unsafe { pyre_object::is_cell(current) } { + let shares_local_slot = idx < self.code().varnames.len(); + if shares_local_slot || current.is_null() || !unsafe { pyre_object::is_cell(current) } { // pyframe.py `PyFrame.initialize_frame_scopes` `Cell(..., self.pycode.cell_families[i])` — // the cellvar this parameter slot was promoted for. let family = unsafe { diff --git a/pyre/pyre-interpreter/src/function.rs b/pyre/pyre-interpreter/src/function.rs index 469c8f588bc..367cb04e4ad 100644 --- a/pyre/pyre-interpreter/src/function.rs +++ b/pyre/pyre-interpreter/src/function.rs @@ -2958,15 +2958,20 @@ pub fn immutable_unique_id(obj: PyObjectRef) -> Option { } if is_exact_type(obj, &pyre_object::bytesobject::BYTES_TYPE) { // `W_AbstractBytesObject.immutable_unique_id` - // (bytesobject.py): `len(s) > 1` is address-based - // (`compute_unique_id(s)`) so returning `None` falls back to the - // object address (invariant-preserving — distinct `bytes` never - // share storage). `len(s) <= 1` is unique-ified: - // `base = ord(s[0])` (0..255) for one byte, `base = 256` for the - // empty bytes, `uid = (base << IDTAG_SHIFT) | IDTAG_SPECIAL`. + // (bytesobject.py): `len(s) > 1` is `compute_unique_id(s)` — the + // id of the STORAGE, which is what `is_w` compares at that length. + // BytesListStrategy re-wraps one erased rpython string, so the + // wrapper address would answer a fresh value per read and make + // `id(a) == id(b)` disagree with `a is b`. `len(s) <= 1` is + // unique-ified: `base = ord(s[0])` (0..255) for one byte, + // `base = 256` for the empty bytes, + // `uid = (base << IDTAG_SHIFT) | IDTAG_SPECIAL`. let len = pyre_object::bytesobject::w_bytes_len(obj); if len > 1 { - return None; + let block = pyre_object::bytesobject::w_bytes_block(obj); + return Some(pyre_object::intobject::w_int_new( + pyre_object::gc_hook::gc_identity_hash(block as usize) as i64, + )); } let base: i64 = if len == 1 { pyre_object::bytesobject::w_bytes_getitem(obj, 0) as i64 diff --git a/pyre/pyre-interpreter/src/module/__pypy__/mod.rs b/pyre/pyre-interpreter/src/module/__pypy__/mod.rs index adf4131d128..5f3cd28de7c 100644 --- a/pyre/pyre-interpreter/src/module/__pypy__/mod.rs +++ b/pyre/pyre-interpreter/src/module/__pypy__/mod.rs @@ -103,6 +103,40 @@ fn hidden_applevel(args: &[pyre_object::PyObjectRef]) -> crate::PyResult { Ok(w_func) } +/// `interp_magic.py strategy` — expose the live implementation strategy of a +/// dict, list, set, or mapdict-backed instance. +/// +/// This is intentionally a diagnostic of the representation pyre actually +/// uses. In particular, ascii lists and integer sets currently report their +/// Object strategies; that makes those remaining PyPy strategy ports +/// visible instead of hiding them behind a missing `__pypy__` function. +fn strategy(args: &[pyre_object::PyObjectRef]) -> crate::PyResult { + let obj = args[0]; + let dict = crate::type_methods::resolve_dict_backing(obj); + if !dict.is_null() && unsafe { pyre_object::is_dict(dict) } { + return Ok(pyre_object::w_str_new(unsafe { + pyre_object::dictmultiobject::w_dict_strategy_name(dict) + })); + } + if unsafe { pyre_object::is_list(obj) } { + return Ok(pyre_object::w_str_new(unsafe { + pyre_object::listobject::w_list_strategy_name(obj) + })); + } + if unsafe { pyre_object::setobject::is_set_or_frozenset(obj) } { + // W_SetObject currently has one ObjectKey-backed representation. The + // helper reports that real shape; EmptySetStrategy and + // IntegerSetStrategy remain explicit builtin-type porting work. + return Ok(pyre_object::w_str_new("ObjectSetStrategy")); + } + if let Some(name) = unsafe { crate::objspace::std::mapdict::mapdict_strategy_repr(obj) } { + return Ok(pyre_object::w_str_from_wtf8(name)); + } + Err(crate::PyError::type_error( + "expecting dict or list or set object, or instance of some kind", + )) +} + /// `interp_dict.py:43-45 / 79-81` `isinstance(w_obj, W_DictMultiObject)`: /// resolve the backing of an exact dict, module dict, or dict subclass, /// rejecting a read-only `mappingproxy` (a `W_Root`, not a `W_DictMultiObject`) @@ -247,6 +281,7 @@ crate::py_module! { "objects_in_repr" / 0 = objects_in_repr, "write_unraisable" / 3 = write_unraisable, "hidden_applevel" / 1 = hidden_applevel, + "strategy" / 1 = strategy, "newmemoryview" / * = interp_buffer::newmemoryview, }, extra_init: |ns| { diff --git a/pyre/pyre-interpreter/src/module/_collections/mod.rs b/pyre/pyre-interpreter/src/module/_collections/mod.rs index 93f5c471bac..37c95be3ee5 100644 --- a/pyre/pyre-interpreter/src/module/_collections/mod.rs +++ b/pyre/pyre-interpreter/src/module/_collections/mod.rs @@ -65,6 +65,11 @@ pub struct W_Deque { maxlen: i64, /// Lightweight iteration-lock counter (`interp_deque.py` `state`); bumped on every mutation. state: i64, + /// PyPy `BaseUserClassMapdict` indexed storage for a deque subclass's + /// app-level `__slots__`. The translated user layout owns these values; + /// pyre's fixed native payload keeps the equivalent object-resident list. + /// `PY_NULL` means no slot has been assigned yet. + w_slots: PyObjectRef, } // PyPy's deque block/endpoint transitions execute atomically under its GIL. @@ -135,6 +140,31 @@ fn deque_len(self_obj: PyObjectRef) -> i64 { W_Deque::from_obj(self_obj).map(|d| d.len).unwrap_or(0) } +/// Whether `obj` has PyPy's `W_Deque` layout, including a Python subclass. +pub(crate) fn is_deque(obj: PyObjectRef) -> bool { + W_Deque::from_obj(obj).is_some() +} + +/// Read one app-level `__slots__` entry from a deque subclass. +pub(crate) unsafe fn deque_slot_get(obj: PyObjectRef, index: usize) -> Option { + unsafe { pyre_object::slots::slot_get(obj, index, deque_slots_field) } +} + +/// Write one app-level `__slots__` entry on a deque subclass. +pub(crate) unsafe fn deque_slot_set(obj: PyObjectRef, index: usize, value: PyObjectRef) { + unsafe { pyre_object::slots::slot_set(obj, index, value, deque_slots_field) } +} + +/// Clear one app-level `__slots__` entry on a deque subclass. +pub(crate) unsafe fn deque_slot_del(obj: PyObjectRef, index: usize) -> bool { + unsafe { pyre_object::slots::slot_del(obj, index, deque_slots_field) } +} + +/// Address of `W_Deque::w_slots` for the shared native-subclass slot helpers. +unsafe fn deque_slots_field(obj: PyObjectRef) -> *mut PyObjectRef { + unsafe { &mut (*(obj as *mut W_Deque)).w_slots } +} + /// Snapshot the backing list into a `Vec`. /// /// The walk reads `leftblock`/`leftindex`/`len` and then follows `rightlink`, @@ -795,13 +825,33 @@ fn deque_compare( /// `W_Deque.mul` — repeat the elements `num` times, then re-bound by /// `maxlen` by routing through the constructor (which trims). -fn deque_repeat(self_obj: PyObjectRef, n: PyObjectRef) -> Result { - if !unsafe { is_int(n) } { - return Ok(pyre_object::w_not_implemented()); - } - let num = unsafe { w_int_get_value(n) }.max(0); +pub(crate) fn deque_repeat( + self_obj: PyObjectRef, + n: PyObjectRef, +) -> Result { + // PyPy's W_Deque.mul receives `w_int` and starts with `space.int_w`. + // CPython v3.14.6 routes the public sq_repeat slot through + // abstract.c sequence_repeat first, so only __index__ is admitted and an + // oversized result names an index-sized integer. Keep PyPy's body below + // after applying that observable gateway contract. + let _roots = pyre_object::gc_roots::push_roots(); + let roots = pyre_object::gc_roots::pin_roots(&[self_obj, n]); + let w_count = + crate::baseobjspace::getindex_repeat(pyre_object::gc_roots::shadow_stack_get(roots + 1))?; + let num = crate::baseobjspace::int_w(w_count)?.max(0) as usize; + let self_obj = pyre_object::gc_roots::shadow_stack_get(roots); let base = snapshot(self_obj); - let mut items = Vec::with_capacity(base.len().saturating_mul(num as usize)); + // interp_deque.py W_Deque.mul: ovfcheck(self.len * num) raises + // MemoryError. `try_reserve_exact` is the implicit allocation edge that + // follows the explicit overflow check in the RPython body. + let total = base + .len() + .checked_mul(num) + .ok_or_else(|| crate::PyError::memory_error(""))?; + let mut items = Vec::new(); + items + .try_reserve_exact(total) + .map_err(|_| crate::PyError::memory_error(""))?; for _ in 0..num { items.extend_from_slice(&base); } @@ -815,7 +865,14 @@ fn deque_repeat(self_obj: PyObjectRef, n: PyObjectRef) -> Result Result<(), PyError> { } /// Build a Python `list` from `items` and pin it in the shadow stack, -/// returning its slot. `w_list_new` pins each element across its own +/// returning its slot. `w_list_new_object` pins each element across its own /// allocation, so the snapshot is captured safely; thereafter the GC walks /// the list and rewrites its entries, so `pinned_get` reads the relocated /// element even after the recursive `save` calls below trigger collections. +/// +/// The Object strategy is what makes `pinned_get` answer the SAME object it +/// was handed: an unboxing strategy stores the payload and wraps a fresh +/// object per read, and `memo_get` resolves its hash bucket by pointer +/// identity, so a container of unboxable elements would miss the memo and +/// write each element again instead of a GET. fn pin_items(items: Vec) -> usize { - let w_list = pyre_object::listobject::w_list_new(items); + let w_list = pyre_object::listobject::w_list_new_object(items); let _ = pyre_object::gc_roots::pin_root(w_list); pyre_object::gc_roots::shadow_stack_len() - 1 } diff --git a/pyre/pyre-interpreter/src/module/_pickle/unpickler.rs b/pyre/pyre-interpreter/src/module/_pickle/unpickler.rs index 5928c522d18..d6fb889a539 100644 --- a/pyre/pyre-interpreter/src/module/_pickle/unpickler.rs +++ b/pyre/pyre-interpreter/src/module/_pickle/unpickler.rs @@ -63,6 +63,9 @@ pub struct W_Unpickler { w_file_read: PyObjectRef, w_file_readline: PyObjectRef, /// Result stack — a Python `list` (GC-managed across `read` allocs). + /// Object strategy, like `w_memo`: a GET pushes the memoized object here + /// and whatever pops it must be that same object, which an unboxing + /// strategy breaks by wrapping a fresh one per read. w_stack: PyObjectRef, /// Saved stacks for the MARK machinery — a Python `list` of lists. w_metastack: PyObjectRef, @@ -220,7 +223,7 @@ impl W_Unpickler { } // The memo persists across `load` calls (a multi-object stream may // back-reference an object memoized by an earlier load). - let memo = pyre_object::listobject::w_list_new(Vec::new()); + let memo = pyre_object::listobject::w_list_new_empty(); let _ = pyre_object::gc_roots::pin_root(memo); let memo_slot = pyre_object::gc_roots::shadow_stack_len() - 1; // A non-None `buffers` is consumed as an iterator by NEXT_BUFFER. @@ -296,16 +299,16 @@ impl W_Unpickler { // Fresh stack each load; the memo persists across `load` calls so a // later object can back-reference one memoized by an earlier load // (lazily created when the unpickler was built only via `__new__`). - let w_stack = pyre_object::listobject::w_list_new(Vec::new()); + let w_stack = pyre_object::listobject::w_list_new_empty(); let me = cur(slot); me.w_stack = w_stack; unpickler_write_barrier(me as *mut W_Unpickler as PyObjectRef); - let w_metastack = pyre_object::listobject::w_list_new(Vec::new()); + let w_metastack = pyre_object::listobject::w_list_new_empty(); let me = cur(slot); me.w_metastack = w_metastack; unpickler_write_barrier(me as *mut W_Unpickler as PyObjectRef); if unsafe { pyre_object::is_none(cur(slot).w_memo) } { - let w_memo = pyre_object::listobject::w_list_new(Vec::new()); + let w_memo = pyre_object::listobject::w_list_new_empty(); let me = cur(slot); me.w_memo = w_memo; me.memo_index = 0; @@ -523,7 +526,7 @@ impl W_Unpickler { return Err(PyError::value_error("memo key must be positive integers.")); } } - let empty = pyre_object::listobject::w_list_new(Vec::new()); + let empty = pyre_object::listobject::w_list_new_empty(); let me = unsafe { &mut *(pyre_object::gc_roots::shadow_stack_get(self_slot) as *mut W_Unpickler) }; @@ -651,7 +654,7 @@ mod memo_proxy { let _roots = pyre_object::gc_roots::push_roots(); let _ = pyre_object::gc_roots::pin_root(w_unpickler); let slot = pyre_object::gc_roots::shadow_stack_len() - 1; - let empty = pyre_object::listobject::w_list_new(Vec::new()); + let empty = pyre_object::listobject::w_list_new_empty(); let u = unsafe { &mut *(pyre_object::gc_roots::shadow_stack_get(slot) as *mut W_Unpickler) }; @@ -708,7 +711,7 @@ fn top(slot: usize, opcode_name: &str) -> Result { fn mark(slot: usize) { let me = cur(slot); unsafe { pyre_object::listobject::w_list_append(me.w_metastack, me.w_stack) }; - let new_stack = pyre_object::listobject::w_list_new(Vec::new()); + let new_stack = pyre_object::listobject::w_list_new_empty(); let me = cur(slot); me.w_stack = new_stack; unpickler_write_barrier(me as *mut W_Unpickler as PyObjectRef); diff --git a/pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs b/pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs index bad010e0c74..e76bee03343 100644 --- a/pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs +++ b/pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs @@ -249,6 +249,11 @@ impl Drop for InstanceRoot { /// ) /// ``` fn init_weakref_type(ns: PyObjectRef) { + // [3.14-spec] PyPy `W_Weakref.typedef` supplies a descriptive string, + // while CPython 3.14 `_PyWeakref_RefType.tp_doc` is null. Leave the key + // to `ensure_common_attributes`, which publishes the observable + // `ReferenceType.__dict__["__doc__"] is None` without changing PyPy's + // weakref payload or ownership. unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, @@ -822,6 +827,14 @@ pub fn dereference(w_ref: PyObjectRef) -> PyObjectRef { /// ``` pub fn descr__repr__(args: &[PyObjectRef]) -> Result { let w_self = args[0]; + // PyPy's interp2app gateway types W_WeakrefBase.descr__repr__ as a + // `weakref-or-proxy` method before entering this shared body. + if !is_w_weakref(w_self) && !is_w_abstract_proxy(w_self) { + return Err(PyError::type_error(format!( + "'weakref-or-proxy' object expected, got '{}' instead", + crate::baseobjspace::object_functionstr_type_name(w_self) + ))); + } let w_obj = dereference(w_self); let type_name = unsafe { match crate::typedef::r#type(w_self) { diff --git a/pyre/pyre-interpreter/src/module/array/mod.rs b/pyre/pyre-interpreter/src/module/array/mod.rs index 2479047df2f..29f050882e9 100644 --- a/pyre/pyre-interpreter/src/module/array/mod.rs +++ b/pyre/pyre-interpreter/src/module/array/mod.rs @@ -1400,8 +1400,78 @@ fn array_reconstructor(args: &[PyObjectRef]) -> PyResult { // Type / module registration. // ────────────────────────────────────────────────────────────────────── +// CPython 3.14 `arraymodule.c` `arraytype_doc`. PyPy's +// `W_ArrayBase.typedef` leaves its TypeDef doc empty; the public 3.14 value is +// observable through both `array.array.__doc__` and the type dictionary, so +// only this metadata string departs from the PyPy owner/implementation shape. +const ARRAY_TYPE_DOC: &str = concat!( + "array(typecode [, initializer]) -> array\n", + "\n", + "Return a new array whose items are restricted by typecode, and\n", + "initialized from the optional initializer value, which must be a list,\n", + "string or iterable over elements of the appropriate type.\n", + "\n", + "Arrays represent basic values and behave very much like lists, except\n", + "the type of objects stored in them is constrained. The type is specified\n", + "at object creation time by using a type code, which is a single character.\n", + "The following type codes are defined:\n", + "\n", + " Type code C Type Minimum size in bytes\n", + " 'b' signed integer 1\n", + " 'B' unsigned integer 1\n", + " 'u' Unicode character 2 (see note)\n", + " 'h' signed integer 2\n", + " 'H' unsigned integer 2\n", + " 'i' signed integer 2\n", + " 'I' unsigned integer 2\n", + " 'l' signed integer 4\n", + " 'L' unsigned integer 4\n", + " 'q' signed integer 8 (see note)\n", + " 'Q' unsigned integer 8 (see note)\n", + " 'f' floating-point 4\n", + " 'd' floating-point 8\n", + "\n", + "NOTE: The 'u' typecode corresponds to Python's unicode character. On\n", + "narrow builds this is 2-bytes on wide builds this is 4-bytes.\n", + "\n", + "NOTE: The 'q' and 'Q' type codes are only available if the platform\n", + "C compiler used to build Python supports 'long long', or, on Windows,\n", + "'__int64'.\n", + "\n", + "Methods:\n", + "\n", + "append() -- append a new item to the end of the array\n", + "buffer_info() -- return information giving the current memory info\n", + "byteswap() -- byteswap all the items of the array\n", + "count() -- return number of occurrences of an object\n", + "extend() -- extend array by appending multiple elements from an iterable\n", + "fromfile() -- read items from a file object\n", + "fromlist() -- append items from the list\n", + "frombytes() -- append items from the string\n", + "index() -- return index of first occurrence of an object\n", + "insert() -- insert a new item into the array at a provided position\n", + "pop() -- remove and return item (default last)\n", + "remove() -- remove first occurrence of an object\n", + "reverse() -- reverse the order of the items in the array\n", + "tofile() -- write all items to a file object\n", + "tolist() -- return the array converted to an ordinary list\n", + "tobytes() -- return the array converted to a string\n", + "\n", + "Attributes:\n", + "\n", + "typecode -- the typecode character used to create the array\n", + "itemsize -- the length in bytes of one array item\n", +); + /// Register all `array.array` methods/getsets into the type namespace. pub fn init_array_type(ns: PyObjectRef) { + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + "__doc__", + pyre_object::w_str_new(ARRAY_TYPE_DOC), + ) + }; unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, diff --git a/pyre/pyre-interpreter/src/objspace/descroperation.rs b/pyre/pyre-interpreter/src/objspace/descroperation.rs index cf22b9678a0..a94bee403cd 100644 --- a/pyre/pyre-interpreter/src/objspace/descroperation.rs +++ b/pyre/pyre-interpreter/src/objspace/descroperation.rs @@ -1994,7 +1994,11 @@ pub(crate) unsafe fn tuple_repeat(t: PyObjectRef, n: PyObjectRef) -> PyResult { /// The builtin sequences repeat through `sq_repeat`, never `nb_multiply`. pub(crate) unsafe fn is_repeat_sequence(obj: PyObjectRef) -> bool { - is_str(obj) || is_list(obj) || is_tuple(obj) || pyre_object::bytesobject::is_bytes_like(obj) + is_str(obj) + || is_list(obj) + || is_tuple(obj) + || pyre_object::bytesobject::is_bytes_like(obj) + || crate::module::_collections::is_deque(obj) } /// `sequence_repeat` for a receiver [`is_repeat_sequence`] accepted, with the @@ -2006,8 +2010,10 @@ unsafe fn sequence_repeat(seq: PyObjectRef, count: PyObjectRef) -> PyResult { list_repeat(seq, count) } else if is_tuple(seq) { tuple_repeat(seq, count) - } else { + } else if pyre_object::bytesobject::is_bytes_like(seq) { bytes_repeat(seq, count) + } else { + crate::module::_collections::deque_repeat(seq, count) } } @@ -3059,25 +3065,36 @@ pub(crate) unsafe fn seq_repeat_override(obj: PyObjectRef, dunders: &[&str]) -> if pyre_object::is_exact_builtin_instance(obj) { return false; } - let tp: *const pyre_object::PyType = if is_str(obj) { - &pyre_object::STR_TYPE + let t = if is_str(obj) { + let Some(t) = crate::typedef::gettypefor(&pyre_object::STR_TYPE) else { + return false; + }; + t.as_ptr() } else if is_list(obj) { - &pyre_object::LIST_TYPE + let Some(t) = crate::typedef::gettypefor(&pyre_object::LIST_TYPE) else { + return false; + }; + t.as_ptr() } else if is_tuple(obj) { - &pyre_object::TUPLE_TYPE + let Some(t) = crate::typedef::gettypefor(&pyre_object::TUPLE_TYPE) else { + return false; + }; + t.as_ptr() } else if pyre_object::bytesobject::is_bytes_like(obj) { - if pyre_object::bytesobject::is_bytes(obj) { + let tp = if pyre_object::bytesobject::is_bytes(obj) { &pyre_object::bytesobject::BYTES_TYPE } else { &pyre_object::bytearrayobject::BYTEARRAY_TYPE - } + }; + let Some(t) = crate::typedef::gettypefor(tp) else { + return false; + }; + t.as_ptr() + } else if crate::module::_collections::is_deque(obj) { + crate::module::_collections::type_object() } else { return false; }; - let Some(t) = crate::typedef::gettypefor(tp) else { - return false; - }; - let t = t.as_ptr(); dunders .iter() .any(|dunder| dunder_overridden(obj, dunder, t)) diff --git a/pyre/pyre-interpreter/src/objspace/std/mapdict.rs b/pyre/pyre-interpreter/src/objspace/std/mapdict.rs index d07a6bdda1c..78e568b1122 100644 --- a/pyre/pyre-interpreter/src/objspace/std/mapdict.rs +++ b/pyre/pyre-interpreter/src/objspace/std/mapdict.rs @@ -1146,6 +1146,67 @@ impl MapNode { } } + /// `mapdict.py AbstractAttribute.repr` and concrete overrides. + /// + /// The result stays WTF-8 because attribute names may contain lone + /// surrogates. PyPy builds the same recursive spelling from the live map + /// chain for `__pypy__.strategy(instance)`. + pub unsafe fn repr_wtf8(&self) -> rustpython_wtf8::Wtf8Buf { + match self { + MapNode::Terminator(t) => { + let class_name = match t.kind { + TerminatorKind::Dict => "DictTerminator", + TerminatorKind::NoDict => "NoDictTerminator", + TerminatorKind::Devolved => "DevolvedDictTerminator", + }; + let mut out = rustpython_wtf8::Wtf8Buf::from_string(format!( + "<{class_name} w_cls=>"); + out + } + MapNode::Plain(p) => { + let mut out = rustpython_wtf8::Wtf8Buf::new(); + if let Some(unboxed) = &p.unboxed { + out.push_str(""); + out + } + } + } + /// mapdict.py `AbstractAttribute.cache_attrs`. pub fn cache_attrs(&self) -> &Mutex> { match self { @@ -1155,6 +1216,23 @@ impl MapNode { } } +/// `interp_magic.py strategy` fallback — return the live map's recursive +/// representation for an object carrying `MapdictStorageMixin`. +/// +/// # Safety +/// `obj` must be null or a live Python object. +pub unsafe fn mapdict_strategy_repr(obj: PyObjectRef) -> Option { + if !unsafe { has_mapdict_layout(obj) } { + return None; + } + let map = unsafe { mapdict_carrier(obj) }._get_mapdict_map(); + if map.is_null() { + None + } else { + Some(unsafe { (*map).repr_wtf8() }) + } +} + /// mapdict.py `AbstractAttribute.search`. /// /// # Safety @@ -6060,6 +6138,18 @@ mod tests { } } + #[test] + fn map_repr_follows_recursive_pypy_strategy_spelling() { + unsafe { + let (_, _, b) = build_chain(); + let repr = (*b).repr_wtf8(); + assert_eq!( + repr.as_bytes(), + b">>>" + ); + } + } + #[test] fn cache_entry_validity_keys_on_map_identity_and_version_tag() { unsafe { diff --git a/pyre/pyre-interpreter/src/reduce_protocol.rs b/pyre/pyre-interpreter/src/reduce_protocol.rs index 0775f10872c..11736cbbf69 100644 --- a/pyre/pyre-interpreter/src/reduce_protocol.rs +++ b/pyre/pyre-interpreter/src/reduce_protocol.rs @@ -399,26 +399,51 @@ pub fn descr_reduce_ex(w_obj: PyObjectRef, proto: i64) -> PyResult { let args_slot = pyre_object::gc_roots::shadow_stack_len(); let _ = pyre_object::gc_roots::pin_root(w_args); let _ = pyre_object::gc_roots::pin_root(w_kwargs); - // objectobject.py:276 / `_PyObject_GetState(required)`: a type whose - // instances carry C-level state that `__dict__`/`__slots__` cannot - // reconstruct, and that supplies no `__getnewargs__`, cannot be - // rebuilt via `__newobj__`. `reduce_newobj` gates this on - // `tp_basicsize` exceeding the object+dict+weakref+slots baseline; - // pyre has no basicsize notion, so recognise the native layouts that - // reach object-reduce with unreconstructable C-level state: `module` - // (native name + dict payload) and `memoryview` (private buffer view - // geometry/export state). This is the `_PyObject_GetState(required)` - // `staticmethod` / `classmethod` likewise keep their wrapped function - // in native descriptor storage that an empty `__newobj__` cannot - // restore. This is the `_PyObject_GetState(required)` refusal used by - // CPython 3.14 for every pickle protocol. - if !hasargs - && unsafe { - pyre_object::is_module(current_obj()) - || pyre_object::memoryview::is_w_memoryview(current_obj()) - || pyre_object::function::is_staticmethod(current_obj()) - || pyre_object::function::is_classmethod(current_obj()) + // CPython 3.14 `object_getstate_default(required)`: when no + // `__getnewargs__` supplied constructor state, a native layout whose + // basicsize exceeds the plain object + managed dict/weakref/slots + // baseline cannot be rebuilt through an empty `__newobj__` call. + // PyPy reaches the same safe shape by giving picklable native TypeDefs + // their own reducer instead of teaching object.__reduce_ex__ their + // fields. Pyre's `ob_type` is the RPython-vtable/layout tag: only the + // plain INSTANCE_TYPE layout has no hidden native payload. Lists and + // dicts are the two upstream exceptions because reduce_2 serializes + // their contents through listitems/dictitems below. This one layout + // test replaces the incomplete per-type census (module, memoryview, + // staticmethod, classmethod) and also covers property and every + // itertools TypeDef whose 3.14 type supplies no reducer. + let native_layout = unsafe { + !std::ptr::eq( + pyre_object::ll_type(current_obj()), + &pyre_object::INSTANCE_TYPE as *const pyre_object::PyType, + ) + }; + // `object_getstate` hands the call to an overriding `__getstate__` + // and only falls through to `object_getstate_default(required)` when + // the type still uses `object.__getstate__`; `descr__reduce_ex__` + // spells the same gate as a `space.lookup` of the hook. A native + // layout that publishes its own hook is rebuilt from the state that + // hook returns, so the refusal below must not see it: `_io.BytesIO` + // and `_io.StringIO` hold a native buffer and pickle for exactly this + // reason. `getnewargs` above can collect, so the type is read back + // rather than reused. + let w_type = crate::typedef::r#type(current_obj()) + .ok_or_else(|| PyError::type_error("cannot determine type for __reduce_ex__"))?; + let supplies_getstate = unsafe { + let w_cls_getstate = + crate::baseobjspace::lookup_in_type(w_type.as_ptr(), "__getstate__"); + let w_obj_getstate = + crate::baseobjspace::lookup_in_type(crate::typedef::w_object(), "__getstate__"); + match (w_cls_getstate, w_obj_getstate) { + (Some(w_cls), Some(w_obj)) => !crate::baseobjspace::is_w(w_cls, w_obj), + (w_cls, _) => w_cls.is_some(), } + }; + if !hasargs + && native_layout + && !supplies_getstate + && !unsafe { pyre_object::is_list(current_obj()) } + && !unsafe { pyre_object::is_dict(current_obj()) } { return Err(PyError::type_error(format!( "cannot pickle '{}' object", diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index 20233bd2585..60657e5aac3 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -2511,6 +2511,22 @@ fn new_builtin_typeobject( layout_pytype: *const PyType, w_metatype: PyObjectRef, ) -> PyObjectRef { + // `typeobject.py ensure_common_attributes` runs for every PyPy TypeDef, + // not only for types which spell these entries in their rawdict. Keep + // the same common preparation at the single builtin construction point: + // every type owns a `__doc__` entry, and `ensure_hash` prevents a type + // defining equality from silently inheriting an unrelated hash. + let ns = dict_ptr as PyObjectRef; + unsafe { + if pyre_object::w_dict_getitem_str(ns, "__doc__").is_none() { + pyre_object::w_dict_setitem_str_no_proxy(ns, "__doc__", pyre_object::w_none()); + } + if pyre_object::w_dict_getitem_str(ns, "__eq__").is_some() + && pyre_object::w_dict_getitem_str(ns, "__hash__").is_none() + { + pyre_object::w_dict_setitem_str_no_proxy(ns, "__hash__", pyre_object::w_none()); + } + } let type_obj = w_type_new_builtin(name, bases, dict_ptr, layout_pytype); let w_metatype = match w_metatype.is_null() { true => w_type(), @@ -2746,6 +2762,23 @@ pub fn make_builtin_type_with_bases( (*parent_layout).typedef } }; + make_builtin_type_with_bases_and_layout(name, init, bases, layout_pytype) +} + +/// [`make_builtin_type_with_bases`] with an explicit interpreter TypeDef +/// identity for a concrete class that introduces its own instance Layout. +/// +/// PyPy `setup_builtin_type` receives the concrete `instancetypedef` even for +/// a TypeDef with multiple declared bases. Most multi-base builtins introduce +/// no interpreter class and use the wrapper above; concrete owners such as +/// `interp_group.W_BaseExceptionGroup` take this path. +pub fn make_builtin_type_with_bases_and_layout( + name: &str, + init: impl FnOnce(PyObjectRef), + bases: &[PyObjectRef], + layout_pytype: *const PyType, +) -> PyObjectRef { + let base = bases[0]; let _roots = pyre_object::gc_roots::push_roots(); let ns_slot = pyre_object::gc_roots::shadow_stack_len(); let ns = pyre_object::w_dict_new(); @@ -15081,6 +15114,26 @@ fn init_slot_wrapper_type(ns: PyObjectRef) { function_descr_call_impl(positional, kwargs, descr) }), ); + // [3.14-spec] PyPy keeps slot functions as + // `FunctionWithFixedCode`, while CPython's `PyWrapperDescr_Type` + // publishes its own `__repr__` slot. The public descriptor type is + // already the CPython projection; route the slot through the same + // exact-carrier formatter used by `space.repr` rather than duplicating + // its text here. + pyre_object::w_dict_setitem_str_no_proxy( + ns, + "__repr__", + make_builtin_function_with_arity( + "__repr__", + |args| { + let descr = slot_wrapper_receiver(args[0], "__repr__")?; + Ok(pyre_object::w_str_from_wtf8_managed(unsafe { + crate::display::py_repr_wtf8(descr)? + })) + }, + 1, + ), + ); pyre_object::w_dict_setitem_str_no_proxy( ns, "__reduce__", @@ -19136,7 +19189,16 @@ fn init_complex_type(ns: PyObjectRef) { make_builtin_function_with_arity( "__complex__", |args| { - // Return a plain `complex` with the same components. + // PyPy `W_ComplexObject.descr_complex` creates a base + // complex, while `W_ComplexObject.is_w` makes that result + // identical-by-value to an exact receiver. Pyre follows + // CPython 3.14 `ComplexTest.test___complex__` for complex + // identity, so preserve the same observable result by + // returning an exact receiver itself. A strict subclass + // still has to shed its class through a fresh base value. + if unsafe { pyre_object::is_exact_type(args[0], &pyre_object::COMPLEX_TYPE) } { + return Ok(args[0]); + } let (re, im) = unsafe { ( pyre_object::w_complex_get_real(args[0]), @@ -25877,7 +25939,7 @@ fn setlike_descr_iter(args: &[PyObjectRef]) -> Result Result { unsafe { Ok(pyre_object::w_str_from_wtf8_managed( - crate::display::py_repr_wtf8(args[0])?, + crate::display::set_repr_wtf8(args[0])?, )) } } diff --git a/pyre/pyre-jit-trace/src/descr.rs b/pyre/pyre-jit-trace/src/descr.rs index 5977252cfb5..9503675ebd0 100644 --- a/pyre/pyre-jit-trace/src/descr.rs +++ b/pyre/pyre-jit-trace/src/descr.rs @@ -2058,7 +2058,7 @@ static W_LIST_DESCR_GROUP: LazyLock = LazyLock::new(|| { // Upstream `rpython/rtyper/lltypesystem/rlist.py:116` // GcStruct("list", ("length", Signed), ("items", Ptr(ITEMARRAY))) // The parity-field pair is `(length, items)`. `strategy` + - // `int_items` / `float_items` are pyre-only PRE-EXISTING- + // `int_items` / `float_items` / `bytes_items` are pyre-only PRE-EXISTING- // ADAPTATIONs for the PyPy interp-level strategy split. build_object_descr_group_with_def_path( std::mem::size_of::(), @@ -2203,6 +2203,29 @@ static W_LIST_DESCR_GROUP: LazyLock = LazyLock::new(|| { false, false, ), + // listobject.py `BytesListStrategy` stores erased `rpython str` + // pointers in its own GcArray(GCREF). Keep these entries at the + // end so the established descriptor indices above remain stable. + ( + "bytes_items.len", + std::mem::offset_of!(W_ListObject, bytes_items) + + pyre_object::bytes_array::BYTES_ARRAY_LEN_OFFSET, + std::mem::size_of::(), + Type::Int, + false, + false, + false, + ), + ( + "bytes_items.block", + std::mem::offset_of!(W_ListObject, bytes_items) + + pyre_object::bytes_array::BYTES_ARRAY_BLOCK_OFFSET, + std::mem::size_of::(), + Type::Ref, + false, + false, + false, + ), ], "W_ListObject", "listobject::W_ListObject", @@ -3529,7 +3552,8 @@ pub fn int_mutable_cell_value_descr() -> DescrRef { /// Size descriptor for `W_ListObject` allocation via NewWithVtable. /// vtable = &LIST_TYPE; the Object-strategy fields `length` / `items` / -/// `strategy` are SetField'd after. `int_items.block` / `float_items.block` +/// `strategy` are SetField'd after. `int_items.block` / `float_items.block` / +/// `bytes_items.block` /// are GC-pointer fields of this descr, so `rewrite.py:498-504 /// clear_gc_fields` zeroes them behind the allocation (== empty, never read /// under the Object strategy); their `len` halves are plain ints and stay at @@ -4089,7 +4113,7 @@ pub fn w_object_object_size_descr() -> DescrRef { } /// rlist.py:116 `l.length` — live length of a list under the Object -/// strategy. Under Integer/Float strategies this field is 0 and +/// strategy. Under Integer/Float/Bytes strategies this field is 0 and /// consumers must dispatch on `list.strategy` first. pub fn list_length_descr() -> DescrRef { field_descr_from_group(&W_LIST_DESCR_GROUP, 0) @@ -4130,6 +4154,14 @@ pub fn list_float_items_block_descr() -> DescrRef { field_descr_from_group(&W_LIST_DESCR_GROUP, 6) } +pub fn list_bytes_items_len_descr() -> DescrRef { + field_descr_from_group(&W_LIST_DESCR_GROUP, 10) +} + +pub fn list_bytes_items_block_descr() -> DescrRef { + field_descr_from_group(&W_LIST_DESCR_GROUP, 11) +} + pub fn list_w_class_descr() -> DescrRef { field_descr_from_group(&W_LIST_DESCR_GROUP, 7) } @@ -6099,7 +6131,7 @@ mod tests { } #[test] - fn make_descr_from_bh_bridges_codewriter_int_items_leaves_to_group() { + fn make_descr_from_bh_bridges_codewriter_strategy_items_leaves_to_group() { use majit_ir::descr::ArrayFlag; use majit_translate::jitcode::BhDescr; @@ -6111,6 +6143,18 @@ mod tests { for (name, expected, ty) in [ ("int_items.len", list_int_items_len_descr(), Type::Int), ("int_items.block", list_int_items_block_descr(), Type::Ref), + ("float_items.len", list_float_items_len_descr(), Type::Int), + ( + "float_items.block", + list_float_items_block_descr(), + Type::Ref, + ), + ("bytes_items.len", list_bytes_items_len_descr(), Type::Int), + ( + "bytes_items.block", + list_bytes_items_block_descr(), + Type::Ref, + ), ] { let descr = make_descr_from_bh(&BhDescr::Field { offset: 0, @@ -6140,7 +6184,8 @@ mod tests { use majit_ir::descr::ArrayFlag; use majit_translate::jitcode::BhDescr; - // A bare `int_items` / `float_items` read (the `w_list_append` body + // A bare `int_items` / `float_items` / `bytes_items` read (the + // `w_list_append` body // reads the typed-storage struct base before reaching `.ptr`/`.len`) // must bridge to the same canonical `.block` group entry as the dotted // `.block` leaf — a populated parent_descr and the `.block` offset, not @@ -6148,6 +6193,7 @@ mod tests { for (name, expected) in [ ("int_items", list_int_items_block_descr()), ("float_items", list_float_items_block_descr()), + ("bytes_items", list_bytes_items_block_descr()), ] { let descr = make_descr_from_bh(&BhDescr::Field { offset: 0, @@ -6227,6 +6273,10 @@ mod tests { .map(|field| field.offset()) .collect(); assert!(list_gc_offsets.contains(&std::mem::offset_of!(W_ListObject, w_slots))); + assert!(list_gc_offsets.contains( + &(std::mem::offset_of!(W_ListObject, bytes_items) + + pyre_object::bytes_array::BYTES_ARRAY_BLOCK_OFFSET) + )); } #[test] @@ -7377,6 +7427,8 @@ pub fn make_descr_from_bh(bh: &majit_translate::jitcode::BhDescr) -> DescrRef { "int_items.block" => return list_int_items_block_descr(), "float_items.len" => return list_float_items_len_descr(), "float_items.block" => return list_float_items_block_descr(), + "bytes_items.len" => return list_bytes_items_len_descr(), + "bytes_items.block" => return list_bytes_items_block_descr(), // A bare `int_items` / `float_items` read addresses the // typed-storage struct base, which is its first field // (`block`, `INT_ARRAY_BLOCK_OFFSET == 0`) — the same @@ -7386,6 +7438,7 @@ pub fn make_descr_from_bh(bh: &majit_translate::jitcode::BhDescr) -> DescrRef { // `.block` group entry so the read resolves a parent_descr. "int_items" => return list_int_items_block_descr(), "float_items" => return list_float_items_block_descr(), + "bytes_items" => return list_bytes_items_block_descr(), // The `w_list_append` body's `match list.strategy` reads the // header `strategy` field directly. The codewriter resolves // its offset but produces a `SimpleFieldDescr` with no diff --git a/pyre/pyre-jit-trace/src/helpers.rs b/pyre/pyre-jit-trace/src/helpers.rs index 920e04c2943..7d3f570b86d 100644 --- a/pyre/pyre-jit-trace/src/helpers.rs +++ b/pyre/pyre-jit-trace/src/helpers.rs @@ -907,14 +907,14 @@ pub fn emit_mapdict_add_unboxed_attr_inline( /// A `BUILD_LIST` caller must restrict to Object-strategy-eligible args /// (non-empty AND not all-int AND not all-float), since an app-level list /// picks its representation from the element types and the typed Integer / -/// Float strategies use `int_items` / `float_items` with `items` null. +/// Float and Bytes strategies use their typed storage with `items` null. /// An exception's `args_w` has no such restriction: `w_exception_args_new` /// pins this one representation at every arity, so the `raise Type(...)` /// emit reproduces it for any element types and for zero arguments. pub fn emit_object_list_inline(ctx: &mut TraceCtx, items: &[OpRef]) -> OpRef { use crate::descr::{ - list_float_items_len_descr, list_int_items_len_descr, list_items_descr, list_length_descr, - list_strategy_descr, w_list_size_descr, + list_bytes_items_len_descr, list_float_items_len_descr, list_int_items_len_descr, + list_items_descr, list_length_descr, list_strategy_descr, w_list_size_descr, }; use crate::state::pyobject_gcarray_descr; @@ -953,7 +953,11 @@ pub fn emit_object_list_inline(ctx: &mut TraceCtx, items: &[OpRef]) -> OpRef { ctx.heapcache_setfield_cached(list, length_idx, len_ref); let zero = ctx.const_int(0); - for inactive_len_descr in [list_int_items_len_descr(), list_float_items_len_descr()] { + for inactive_len_descr in [ + list_int_items_len_descr(), + list_float_items_len_descr(), + list_bytes_items_len_descr(), + ] { let inactive_len_idx = inactive_len_descr.index(); ctx.record_op_with_descr(OpCode::SetfieldGc, &[list, zero], inactive_len_descr); ctx.heapcache_setfield_cached(list, inactive_len_idx, zero); @@ -978,7 +982,8 @@ pub fn emit_object_list_inline(ctx: &mut TraceCtx, items: &[OpRef]) -> OpRef { /// wrapper plus the `strategy` store, mirroring `w_list_new(vec![])` / /// `w_list_new_with_strategy(vec![], Empty)`. /// -/// `items` and the typed `int_items` / `float_items` blocks stay null because +/// `items` and the typed `int_items` / `float_items` / `bytes_items` blocks +/// stay null because /// they are GC-pointer fields of the size descr, so `rewrite.py:498-504 /// clear_gc_fields` zeroes them behind the `NewWithVtable`. `length` gets no /// such pending zero — the recycled nursery bytes a `CALL_MALLOC_NURSERY` @@ -987,8 +992,8 @@ pub fn emit_object_list_inline(ctx: &mut TraceCtx, items: &[OpRef]) -> OpRef { /// OptVirtualize folds the whole wrapper when the list never escapes. pub fn emit_empty_list_inline(ctx: &mut TraceCtx) -> OpRef { use crate::descr::{ - list_float_items_len_descr, list_int_items_len_descr, list_length_descr, - list_strategy_descr, w_list_size_descr, + list_bytes_items_len_descr, list_float_items_len_descr, list_int_items_len_descr, + list_length_descr, list_strategy_descr, w_list_size_descr, }; let list = ctx.record_op_with_descr(OpCode::NewWithVtable, &[], w_list_size_descr()); @@ -1000,7 +1005,11 @@ pub fn emit_empty_list_inline(ctx: &mut TraceCtx) -> OpRef { ctx.record_op_with_descr(OpCode::SetfieldGc, &[list, zero], length_descr); ctx.heapcache_setfield_cached(list, length_idx, zero); - for inactive_len_descr in [list_int_items_len_descr(), list_float_items_len_descr()] { + for inactive_len_descr in [ + list_int_items_len_descr(), + list_float_items_len_descr(), + list_bytes_items_len_descr(), + ] { let inactive_len_idx = inactive_len_descr.index(); ctx.record_op_with_descr(OpCode::SetfieldGc, &[list, zero], inactive_len_descr); ctx.heapcache_setfield_cached(list, inactive_len_idx, zero); @@ -1126,8 +1135,8 @@ pub fn emit_typed_list_inline( strategy: pyre_object::listobject::ListStrategy, ) -> OpRef { use crate::descr::{ - list_float_items_len_descr, list_int_items_len_descr, list_length_descr, - list_strategy_descr, w_list_size_descr, + list_bytes_items_len_descr, list_float_items_len_descr, list_int_items_len_descr, + list_length_descr, list_strategy_descr, w_list_size_descr, }; let len = raws.len(); @@ -1164,6 +1173,7 @@ pub fn emit_typed_list_inline( list_length_descr(), list_int_items_len_descr(), list_float_items_len_descr(), + list_bytes_items_len_descr(), ] { let scalar_idx = scalar_descr.index(); ctx.record_op_with_descr(OpCode::SetfieldGc, &[list, zero], scalar_descr); @@ -1302,13 +1312,16 @@ pub fn emit_promote_empty_list_inline( // already resolves to the concrete block. } pyre_object::listobject::ListStrategy::Empty - | pyre_object::listobject::ListStrategy::IntOrFloat => { - // First append can only select Integer, Float, or Object; + | pyre_object::listobject::ListStrategy::IntOrFloat + | pyre_object::listobject::ListStrategy::Bytes => { + // The specialized first-append path only admits Integer, Float, + // or Object. Exact bytes are declined before this emitter; // IntOrFloat is reached later by a numeric strategy transition. debug_assert!(matches!( strategy, pyre_object::listobject::ListStrategy::Empty | pyre_object::listobject::ListStrategy::IntOrFloat + | pyre_object::listobject::ListStrategy::Bytes )); } } diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs index b76bd7c6fe0..6636d85b0ef 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs @@ -1394,7 +1394,8 @@ pub(crate) fn fbw_store_journal_rollback() { } } pyre_object::listobject::ListStrategy::Float - | pyre_object::listobject::ListStrategy::Empty => { + | pyre_object::listobject::ListStrategy::Empty + | pyre_object::listobject::ListStrategy::Bytes => { crate::trace::fbw_diag::bump( crate::trace::fbw_diag::STORE_JOURNAL_ROLLBACK_FAILED, ); @@ -1441,6 +1442,9 @@ pub(crate) fn fbw_store_journal_rollback() { // Empty never enters the append journal (no spare-capacity // fold path records it); nothing to rewind. pyre_object::listobject::ListStrategy::Empty => {} + // Bytes append does not enter this journal until the + // walker has a BytesBlock store emitter. + pyre_object::listobject::ListStrategy::Bytes => {} } pyre_object::listobject::w_list_set_allocated(list, allocated_before); } diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs index 4f0e1e90314..7a81d0b25f2 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs @@ -4884,6 +4884,9 @@ pub(crate) fn try_walker_specialize_newlist( // leave construction to the ordinary residual instead of emitting an // Integer array whose values would have the wrong representation. ListStrategy::IntOrFloat => return Ok(None), + // The generic residual constructs the erased rpython-string array. + // The walker has no BytesBlock payload emitter yet. + ListStrategy::Bytes => return Ok(None), // Empty is impossible here (len >= 1); decline defensively. ListStrategy::Empty => return Ok(None), }; @@ -12959,7 +12962,8 @@ unsafe fn orthodox_list_append_recognize( // traced strategy from the concrete one the commit installs. let obj_ok = !value.is_null() && !pyre_object::is_plain_int1(value) - && !pyre_object::is_float_strategy_item(value); + && !pyre_object::is_float_strategy_item(value) + && !pyre_object::pyobject::is_exact_type(value, &pyre_object::bytesobject::BYTES_TYPE); if !int_ok && !float_ok && !obj_ok { return None; } diff --git a/pyre/pyre-jit/src/eval.rs b/pyre/pyre-jit/src/eval.rs index a9dd20688d0..281dcf8d5b0 100644 --- a/pyre/pyre-jit/src/eval.rs +++ b/pyre/pyre-jit/src/eval.rs @@ -1156,7 +1156,7 @@ unsafe fn unicode_user_object_custom_trace( /// the list). Forward each live element slot in place, exactly as /// `tuple_object_custom_trace`, so a moving collector relocates young /// elements and a major collection marks them. Only the Object strategy -/// stores `PyObjectRef`s; Integer/Float keep unboxed arrays (`items` null) +/// stores `PyObjectRef`s; Integer/Float/Bytes keep typed arrays (`items` null) /// and Empty has no block. Trace `length` live slots, not capacity — the /// spare tail past the live length may hold stale pointers a shrink left /// behind. @@ -1206,6 +1206,21 @@ unsafe fn list_object_custom_trace(obj_addr: usize, f: &mut dyn FnMut(*mut majit { f(float_block_slot as *mut majit_ir::GcRef); } + // BytesListStrategy stores erased `rpython str` pointers in a + // `GcArray(GCREF)`. A managed block's varsize trace walks the entries; + // the std::alloc fallback must be walked here explicitly. + let bytes_block_slot = unsafe { std::ptr::addr_of_mut!((*list_ptr).bytes_items.block) }; + let bytes_block = unsafe { *bytes_block_slot }; + if !bytes_block.is_null() { + if pyre_object::gc_hook::try_gc_owns_object(bytes_block as *mut u8) { + f(bytes_block_slot as *mut majit_ir::GcRef); + } else { + let base = unsafe { pyre_object::object_array::items_block_items_base(bytes_block) }; + for i in 0..list.bytes_items.len() { + f(unsafe { base.add(i) } as *mut majit_ir::GcRef); + } + } + } } /// Custom trace for `W_MemoryView`. Its geometry and backing live in an diff --git a/pyre/pyre-object/src/bytes_array.rs b/pyre/pyre-object/src/bytes_array.rs new file mode 100644 index 00000000000..2b4082e9a98 --- /dev/null +++ b/pyre/pyre-object/src/bytes_array.rs @@ -0,0 +1,284 @@ +use std::ops::{Index, IndexMut}; + +use crate::bytesobject::BytesBlock; +use crate::object_array::{ + ItemsBlock, alloc_list_items_block_gc, dealloc_list_items_block, items_block_capacity, + items_block_items_base, +}; +use crate::pyobject::PyObjectRef; + +/// PyPy `BytesListStrategy`'s erased `[rpython str]` storage. +/// +/// Each entry is the GC pointer to a `BytesBlock`, not a boxed +/// `W_BytesObject`. `ItemsBlock` is the runtime's `GcArray(GCREF)` shape, so +/// its existing varsize trace forwards both the backing block and every raw +/// string pointer it contains. +#[repr(C)] +pub struct BytesArray { + pub block: *mut ItemsBlock, + len: usize, +} + +pub const BYTES_ARRAY_BLOCK_OFFSET: usize = std::mem::offset_of!(BytesArray, block); +pub const BYTES_ARRAY_LEN_OFFSET: usize = std::mem::offset_of!(BytesArray, len); + +impl BytesArray { + #[inline] + fn base(&self) -> *mut PyObjectRef { + unsafe { items_block_items_base(self.block) } + } + + pub fn empty() -> Self { + Self { + block: std::ptr::null_mut(), + len: 0, + } + } + + pub fn from_vec(values: Vec<*const BytesBlock>) -> Self { + let mut refs = Vec::with_capacity(values.len()); + for value in values { + refs.push(value as PyObjectRef); + } + let len = refs.len(); + Self { + block: unsafe { alloc_list_items_block_gc(&refs) }, + len, + } + } + + #[must_use] + pub fn pin_block(&self) -> usize { + let slot = crate::gc_roots::shadow_stack_len(); + let _ = crate::gc_roots::pin_root(self.block as PyObjectRef); + slot + } + + pub fn reload_block(&mut self, slot: usize) { + self.block = crate::gc_roots::shadow_stack_get(slot) as *mut ItemsBlock; + } + + pub fn install(&mut self, fresh: BytesArray) { + let _roots = crate::gc_roots::push_roots(); + let slot = fresh.pin_block(); + *self = fresh; + self.reload_block(slot); + } + + #[inline] + fn capacity(&self) -> usize { + unsafe { items_block_capacity(self.block) } + } + + #[inline] + pub fn spare_capacity(&self) -> usize { + self.capacity().saturating_sub(self.len) + } + + #[inline] + pub fn heap_capacity(&self) -> usize { + self.capacity() + } + + #[inline] + pub fn set_len(&mut self, new_len: usize) { + assert!(new_len <= self.capacity()); + self.len = new_len; + } + + #[inline] + pub fn is_inline(&self) -> bool { + false + } + + /// The room `capacity` must already hold for `additional` more entries. + /// + /// A fresh block is young, and this array is embedded in the owning + /// `W_ListObject` — the only object through which a collection reaches it. + /// An old-gen owner that gains that edge without being on the remembered + /// set is skipped by the minor collection that would forward the block, and + /// the block, along with every `BytesBlock` reachable only through it, is + /// reclaimed while the list still names it. `BytesArray` cannot reach its + /// owner to barrier it, so it never allocates a block: the list reserves + /// room through `W_ListObject::bytes_grow`, which barriers on both sides of + /// the allocation. Refuse loudly rather than grow behind the owner's back. + #[inline] + fn assert_room(&self, additional: usize) { + assert!( + self.len + additional <= self.capacity(), + "BytesArray needs {additional} more slot(s) than its capacity {}; \ + reserve through W_ListObject::bytes_grow first", + self.capacity(), + ); + } + + #[inline] + fn barrier(&self) { + if !self.block.is_null() { + crate::gc_hook::try_gc_write_barrier(self.block as *mut u8); + } + } + + pub fn push(&mut self, value: *const BytesBlock) { + let _roots = crate::gc_roots::push_roots(); + let value_slot = crate::gc_roots::shadow_stack_len(); + let _ = crate::gc_roots::pin_root(value as PyObjectRef); + self.assert_room(1); + self.barrier(); + unsafe { *self.base().add(self.len) = crate::gc_roots::shadow_stack_get(value_slot) }; + self.len += 1; + } + + #[inline] + pub fn len(&self) -> usize { + self.len + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + pub fn as_slice(&self) -> &[*const BytesBlock] { + unsafe { std::slice::from_raw_parts(self.base() as *const *const BytesBlock, self.len) } + } + + pub fn as_mut_slice(&mut self) -> &mut [*const BytesBlock] { + unsafe { std::slice::from_raw_parts_mut(self.base() as *mut *const BytesBlock, self.len) } + } + + pub fn to_vec(&self) -> Vec<*const BytesBlock> { + self.as_slice().to_vec() + } + + pub fn insert(&mut self, index: usize, value: *const BytesBlock) { + assert!(index <= self.len); + let _roots = crate::gc_roots::push_roots(); + let value_slot = crate::gc_roots::shadow_stack_len(); + let _ = crate::gc_roots::pin_root(value as PyObjectRef); + self.assert_room(1); + self.barrier(); + unsafe { + let p = self.base().add(index); + std::ptr::copy(p, p.add(1), self.len - index); + *p = crate::gc_roots::shadow_stack_get(value_slot); + } + self.len += 1; + } + + pub fn set(&mut self, index: usize, value: *const BytesBlock) { + assert!(index < self.len); + let _roots = crate::gc_roots::push_roots(); + let slot = crate::gc_roots::shadow_stack_len(); + let _ = crate::gc_roots::pin_root(value as PyObjectRef); + self.barrier(); + unsafe { *self.base().add(index) = crate::gc_roots::shadow_stack_get(slot) }; + } + + pub fn remove(&mut self, index: usize) -> *const BytesBlock { + assert!(index < self.len); + let value = self.as_slice()[index]; + unsafe { + let p = self.base().add(index); + std::ptr::copy(p.add(1), p, self.len - index - 1); + *p.add(self.len - index - 1) = std::ptr::null_mut(); + } + self.len -= 1; + value + } + + pub fn pop(&mut self) -> *const BytesBlock { + assert!(self.len > 0); + let value = self.as_slice()[self.len - 1]; + self.len -= 1; + unsafe { *self.base().add(self.len) = std::ptr::null_mut() }; + value + } + + pub fn reverse(&mut self) { + self.as_mut_slice().reverse(); + } + + pub fn splice(&mut self, start: usize, remove_count: usize, values: &[*const BytesBlock]) { + let old_len = self.len; + let start = start.min(old_len); + let removed = remove_count.min(old_len - start); + let new_len = old_len - removed + values.len(); + let _roots = crate::gc_roots::push_roots(); + let root_base = crate::gc_roots::shadow_stack_len(); + for &value in values { + let _ = crate::gc_roots::pin_root(value as PyObjectRef); + } + assert!( + new_len <= self.capacity(), + "BytesArray splice needs {new_len} slots but capacity is {}; \ + reserve through W_ListObject::bytes_grow first", + self.capacity(), + ); + self.barrier(); + unsafe { + let base = self.base(); + std::ptr::copy( + base.add(start + removed), + base.add(start + values.len()), + old_len - start - removed, + ); + self.len = new_len; + for i in 0..values.len() { + *base.add(start + i) = crate::gc_roots::shadow_stack_get(root_base + i); + } + for i in new_len..old_len { + *base.add(i) = std::ptr::null_mut(); + } + } + } + + pub fn drain(&mut self, range: std::ops::Range) { + assert!(range.start <= range.end && range.end <= self.len); + let count = range.end - range.start; + if count == 0 { + return; + } + unsafe { + let base = self.base(); + std::ptr::copy( + base.add(range.end), + base.add(range.start), + self.len - range.end, + ); + for i in self.len - count..self.len { + *base.add(i) = std::ptr::null_mut(); + } + } + self.len -= count; + } + + pub fn clear(&mut self) { + unsafe { + for i in 0..self.len { + *self.base().add(i) = std::ptr::null_mut(); + } + } + self.len = 0; + } +} + +impl Drop for BytesArray { + fn drop(&mut self) { + unsafe { dealloc_list_items_block(self.block) }; + } +} + +impl Index for BytesArray { + type Output = *const BytesBlock; + + fn index(&self, index: usize) -> &Self::Output { + &self.as_slice()[index] + } +} + +impl IndexMut for BytesArray { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.as_mut_slice()[index] + } +} diff --git a/pyre/pyre-object/src/bytesobject.rs b/pyre/pyre-object/src/bytesobject.rs index e45b5e73417..539e5cc295e 100644 --- a/pyre/pyre-object/src/bytesobject.rs +++ b/pyre/pyre-object/src/bytesobject.rs @@ -276,6 +276,50 @@ pub fn w_bytes_from_bytes(bytes: &[u8]) -> PyObjectRef { w_bytes } +/// Wrap an existing PyPy `rpython str` payload for +/// `BytesListStrategy.wrap`. The immutable block is shared; only the +/// `W_BytesObject` wrapper is newly allocated. +#[majit_macros::dont_look_inside] +pub fn w_bytes_from_block(data: *const BytesBlock) -> PyObjectRef { + let _roots = crate::gc_roots::push_roots(); + let data_slot = crate::gc_roots::shadow_stack_len(); + let _ = crate::gc_roots::pin_root(data as PyObjectRef); + let class_slot = crate::gc_roots::shadow_stack_len(); + let _ = crate::gc_roots::pin_root(get_instantiate(&BYTES_TYPE)); + let raw = crate::gc_hook::try_gc_alloc_stable_raw(W_BYTES_GC_TYPE_ID, W_BYTES_OBJECT_SIZE); + let data = crate::gc_roots::shadow_stack_get(data_slot) as *const BytesBlock; + let body = W_BytesObject { + ob_header: PyObject { + ob_type: &BYTES_TYPE as *const PyType, + w_class: crate::gc_roots::shadow_stack_get(class_slot), + }, + data, + len: unsafe { (*data).length }, + ctypes_keepalive_refs: 0, + w_dict: PY_NULL, + w_weakreflifeline: PY_NULL, + }; + if raw.is_null() { + crate::lltype::malloc_typed(body) as PyObjectRef + } else { + unsafe { std::ptr::write(raw as *mut W_BytesObject, body) }; + // The creation barrier the GC transform emits after a `SETFIELD_GC` + // into a non-nursery struct. A stable allocation is always old-gen, + // and `oldgen_birth_flags` (incminimark.py) stamps one born during a + // major marking cycle black, so the marker never traces it. Unlike + // every other bytes constructor, `data` here is a block that already + // existed and may still be white, and `BytesListStrategy` drops the + // array holding it in the same strategy switch that wraps it — leaving + // this wrapper its only owner. Remembering the wrapper is what puts + // it back on `more_objects_to_trace` at the next minor + // (`_add_to_more_objects_to_trace`, incminimark.py), which is the + // invariant `_debug_check_object_marking` states: a black object must + // never point to a white one. + crate::gc_hook::try_gc_write_barrier_managed(raw); + raw as PyObjectRef + } +} + /// Allocate a bytes-subclass instance in the managed heap. PyPy's /// `W_BytesObject` user subclasses carry mapdict state and therefore /// participate in cycle collection; only exact immutable bytes may use the @@ -416,6 +460,11 @@ pub unsafe fn w_bytes_data(obj: PyObjectRef) -> &'static [u8] { } } +/// Return the erased `rpython str` stored by PyPy's BytesListStrategy. +pub unsafe fn w_bytes_block(obj: PyObjectRef) -> *const BytesBlock { + unsafe { (*(obj as *const W_BytesObject)).data } +} + /// bytes.find(sub, start) — find first occurrence of byte value. /// # Safety /// The caller must uphold every validity, runtime-type, aliasing, and lifetime diff --git a/pyre/pyre-object/src/dictmultiobject.rs b/pyre/pyre-object/src/dictmultiobject.rs index f00dc140447..259c81f6685 100644 --- a/pyre/pyre-object/src/dictmultiobject.rs +++ b/pyre/pyre-object/src/dictmultiobject.rs @@ -2037,6 +2037,38 @@ pub unsafe fn w_dict_strategy_id(obj: PyObjectRef) -> usize { d.dstrategy as *const DictStrategyRef as usize } +/// `interp_magic.py strategy` — concrete dict strategy class name. +/// +/// Module dictionaries currently retain their per-dict +/// `ModuleDictStrategy` allocation after promotion, so consult the live +/// storage mode before the trait object. This reports the same strategy +/// transition PyPy exposes while the remaining single-`dstorage` structural +/// refactor is carried out. +/// +/// # Safety +/// `obj` must point to a live `W_DictObject` or `W_ModuleDictObject`. +pub unsafe fn w_dict_strategy_name(obj: PyObjectRef) -> &'static str { + if is_module_dict(obj) { + return if w_module_dict_is_object_strategy(obj) { + "ObjectDictStrategy" + } else { + "ModuleDictStrategy" + }; + } + match w_dict_get_strategy(obj).strategy_kind() { + StrategyKind::Empty => "EmptyDictStrategy", + StrategyKind::EmptyKwargs => "EmptyKwargsDictStrategy", + StrategyKind::Object => "ObjectDictStrategy", + StrategyKind::Bytes => "BytesDictStrategy", + StrategyKind::Unicode => "UnicodeDictStrategy", + StrategyKind::Int => "IntDictStrategy", + StrategyKind::Identity => "IdentityDictStrategy", + StrategyKind::Kwargs => "KwargsDictStrategy", + StrategyKind::Module => "ModuleDictStrategy", + StrategyKind::Map => "MapDictStrategy", + } +} + /// Key-set mutation state captured by dict iterators. /// /// PyPy's `BaseIteratorImplementation` owns a live iterator over the @@ -7668,6 +7700,22 @@ mod tests { crate::dict_eq_hook::register_hash_str_hook(builtin_structural_str_hash); } + #[test] + fn strategy_class_name_tracks_regular_and_module_transitions() { + install_test_hash_hook(); + let dict = w_dict_new(); + let module = w_module_dict_new(); + unsafe { + assert_eq!(w_dict_strategy_name(dict), "EmptyDictStrategy"); + w_dict_setitem(dict, 1, crate::w_none()); + assert_eq!(w_dict_strategy_name(dict), "IntDictStrategy"); + + assert_eq!(w_dict_strategy_name(module), "ModuleDictStrategy"); + w_dict_setitem(module, 1, crate::w_none()); + assert_eq!(w_dict_strategy_name(module), "ObjectDictStrategy"); + } + } + #[test] fn test_dict_int_key() { let dict = w_dict_new(); diff --git a/pyre/pyre-object/src/gc_roots.rs b/pyre/pyre-object/src/gc_roots.rs index 9cd4d7822b3..4f26bff1b09 100644 --- a/pyre/pyre-object/src/gc_roots.rs +++ b/pyre/pyre-object/src/gc_roots.rs @@ -277,6 +277,40 @@ fn with_shadow_stack(f: impl FnOnce(&RootStack) -> R) -> R { ROOT_STACK.with(f) } +/// Normalize one already-published root in place. +/// +/// RPython's caller and callee livevars are one GC-transformed graph, so a +/// collection between argument marshalling and the callee's root push rewrites +/// the value the callee sees. A native JIT call copies the argument into the +/// host ABI first: its jitframe home is rewritten, but that register/stack copy +/// can still name the forwarding stub when the callee publishes it. Follow +/// same-thread nursery forwarding unconditionally before applying the +/// free-threaded synchronization path. +#[inline] +fn normalize_published_slot(stack: &RootStack, index: usize) -> PyObjectRef { + // SAFETY: callers claimed `index` before entering this helper and keep the + // surrounding RootScope alive throughout it. + let mut root = unsafe { *stack.slot(index) }; + let current = majit_gc::gc_current_object_address(root as usize) as PyObjectRef; + if current != root { + // SAFETY: same live slot. + unsafe { *stack.slot(index) = current }; + root = current; + } + if !majit_gc::gc_sync::foreign_mutator_seen() { + return root; + } + // This query is a safepoint. The slot already contains the locally + // normalized value and is therefore safe for a foreign root walk. + root = unsafe { *stack.slot(index) }; + let normalized = crate::gc_hook::try_gc_current_object_address(root as *mut u8) as PyObjectRef; + if normalized != root { + // SAFETY: same live slot. + unsafe { *stack.slot(index) = normalized }; + } + normalized +} + /// `increase_root_stack_depth(new_depth)` (`rlib/rgc.py` → /// `shadowstack.py:351-364`). `sys.setrecursionlimit` scales the root stack /// with the limit at `pypy/module/sys/vm.py:97`; the depth can only grow. @@ -357,21 +391,8 @@ impl RootScope { *stack.incr_stack() = root; index }; - // RPython has one active mutator under the GIL, so the value just - // published cannot have been forwarded between the caller's copy and - // this root write. Pyre's free-threaded seam needs the normalization - // only after a second mutator has existed; the sticky predicate also - // covers one which collected and unregistered before this point. - if !majit_gc::gc_sync::foreign_mutator_seen() { - return root; - } - let normalized = - crate::gc_hook::try_gc_current_object_address(root as *mut u8) as PyObjectRef; - if normalized != root { - // SAFETY: same cell, and `index` is still live. - unsafe { *(*self.stack_slot).slot(index) = normalized }; - } - normalized + // SAFETY: `stack_slot` is this thread's live root-stack cell. + normalize_published_slot(unsafe { &*self.stack_slot }, index) } /// Scope-local [`shadow_stack_get`] using the cached cell. @@ -403,24 +424,9 @@ impl RootScope { pub fn normalize(&self, base: usize, len: usize) { #[cfg(debug_assertions)] assert_shadow_stack_not_walking(); - // Same guard as `normalize_roots`: RPython has one active mutator under - // the GIL, so no root becomes a forwarding stub between its publication - // and the allocation bracket. - if !majit_gc::gc_sync::foreign_mutator_seen() { - return; - } for index in base..base + len { - // Re-read the slot each time, as [`normalize_roots`] does: a - // collection triggered by an earlier query may already have - // rewritten every published root in place. // SAFETY: `publish` claimed every index in this range. - let root = unsafe { *(*self.stack_slot).slot(index) }; - let current = - crate::gc_hook::try_gc_current_object_address(root as *mut u8) as PyObjectRef; - if current != root { - // SAFETY: same slot, still live. - unsafe { *(*self.stack_slot).slot(index) = current }; - } + normalize_published_slot(unsafe { &*self.stack_slot }, index); } } @@ -435,18 +441,8 @@ impl RootScope { // before anything consults the collector about it. // SAFETY: same cell; `slot` bounds-checks `index`. unsafe { *(*self.stack_slot).slot(index) = root }; - // Same guard as `shadow_stack_set`, after the raw publish: RPython has - // one active mutator under the GIL, so the value just published cannot - // have been forwarded between the caller's copy and this write. - if !majit_gc::gc_sync::foreign_mutator_seen() { - return; - } - let normalized = - crate::gc_hook::try_gc_current_object_address(root as *mut u8) as PyObjectRef; - if normalized != root { - // SAFETY: same slot, still live. - unsafe { *(*self.stack_slot).slot(index) = normalized }; - } + // SAFETY: `stack_slot` is this thread's live root-stack cell. + normalize_published_slot(unsafe { &*self.stack_slot }, index); } } @@ -578,9 +574,9 @@ impl Default for RootedItems { /// so the matching [`Drop`] truncates the entry. Pinning without a /// guard is a leak from the GC's perspective once the backend GC consumes the stack. /// -/// Returns the word now held by the shadow-stack slot. A foreign collection -/// may have forwarded `root` while this call waited at its normalization -/// safepoint, so callers that use the word after pinning must use this value. +/// Returns the word now held by the shadow-stack slot. A collection may have +/// forwarded `root` after the caller copied it, so callers that use the word +/// after pinning must use this value. /// /// Pushes onto the thread-local `ROOT_STACK`, a runtime-mutable root the /// tracer cannot type; the JIT residualises the call instead of tracing into @@ -602,28 +598,7 @@ pub fn pin_root(root: PyObjectRef) -> PyObjectRef { unsafe { *stack.incr_stack() = root }; index }); - // A foreign mutator may have completed a nursery collection after the - // caller copied this GCREF but before it entered this explicit root - // bracket. RPython's `_trace_drag_out` always rewrites a root that names - // an already-forwarded nursery object; normalize the host-side copy at - // the same boundary so the shadow stack never gains a forwarding stub. - if !majit_gc::gc_sync::foreign_mutator_seen() { - return root; - } - let normalized = crate::gc_hook::try_gc_current_object_address(root as *mut u8) as PyObjectRef; - // The slot already holds `root`, so a query that found no forwarding stub - // has nothing to write back. That is the steady state — nothing collected - // between the push above and the query — and skipping it keeps a second - // thread-local resolve off a path that sits on the interpreter's - // allocation and call paths. - if normalized != root { - with_shadow_stack(|stack| { - // SAFETY: `index` was live when claimed and only this thread can - // have shortened the stack, which it has not. - unsafe { *stack.slot(index) = normalized } - }); - } - normalized + with_shadow_stack(|stack| normalize_published_slot(stack, index)) } /// Publish a complete translated livevar set before performing any @@ -675,22 +650,8 @@ pub fn publish_roots(roots: &[PyObjectRef]) -> usize { pub fn normalize_roots(base: usize, len: usize) { #[cfg(debug_assertions)] assert_shadow_stack_not_walking(); - if !majit_gc::gc_sync::foreign_mutator_seen() { - return; - } for index in base..base + len { - // Read the slot each time: a collection triggered by an earlier query - // may already have rewritten every published root in place. - // SAFETY: every index in this range was claimed just above. - let root = with_shadow_stack(|stack| unsafe { *stack.slot(index) }); - let current = crate::gc_hook::try_gc_current_object_address(root as *mut u8) as PyObjectRef; - // Same write-back economy as `pin_root`: the slot already holds `root`, - // so a query that found no forwarding stub has nothing to store, and - // skipping it saves a second thread-local resolve per livevar. - if current != root { - // SAFETY: same slot, still live. - with_shadow_stack(|stack| unsafe { *stack.slot(index) = current }); - } + with_shadow_stack(|stack| normalize_published_slot(stack, index)); } } @@ -755,10 +716,10 @@ pub fn shadow_stack_get(index: usize) -> PyObjectRef { // relocation between the pin and this read updates the slot in place and // no forwarding stub can be observed here. // - // Only [`pin_root`] normalizes, and it must: the value it receives is - // copied from outside the bracket, so a foreign mutator can have collected - // after that copy and before the pin. That is also the pin's safepoint; - // a read allocates nothing and has no reason to park. + // Root publication APIs normalize, and they must: the value they receive + // can be copied from outside the bracket before a collection and reach the + // new slot as a forwarding stub. A read allocates nothing and has no + // reason to park. // SAFETY: `slot` bounds-checks `index` against the live length. with_shadow_stack(|stack| unsafe { *stack.slot(index) }) } @@ -811,14 +772,9 @@ pub fn shadow_stack_set(index: usize, root: PyObjectRef) { // visible to that collector before we enter the query safepoint. // SAFETY: `slot` bounds-checks `index` against the live length. with_shadow_stack(|stack| unsafe { *stack.slot(index) = root }); - // Then normalize a GCREF the caller may have copied before a foreign - // mutator's nursery collection, so the slot never keeps a forwarding stub. - if !majit_gc::gc_sync::foreign_mutator_seen() { - return; - } - let root = crate::gc_hook::try_gc_current_object_address(root as *mut u8) as PyObjectRef; - // SAFETY: same slot, still live. - with_shadow_stack(|stack| unsafe { *stack.slot(index) = root }); + with_shadow_stack(|stack| { + normalize_published_slot(stack, index); + }); } /// Visit every pinned root in the shadow stack with mutable access. diff --git a/pyre/pyre-object/src/interp_exceptions.rs b/pyre/pyre-object/src/interp_exceptions.rs index 94fdae93f8a..61d26c1848a 100644 --- a/pyre/pyre-object/src/interp_exceptions.rs +++ b/pyre/pyre-object/src/interp_exceptions.rs @@ -15,6 +15,16 @@ use crate::pyobject::*; use rustpython_wtf8::Wtf8; pub static EXCEPTION_TYPE: PyType = crate::pyobject::new_pytype("BaseException"); +/// PyPy `interp_group.W_BaseExceptionGroup` is a concrete interpreter class +/// over `W_BaseException`, so its TypeDef owns a child instance Layout. +/// +/// Pyre flattens the group's fields into [`W_BaseException`], and group +/// instances therefore keep the BaseException/Exception `ob_type` selected by +/// their leaf policy. This static is consequently a TypeDef/Layout identity, +/// not a separately allocated object vtable, and is deliberately absent from +/// `pyobject::all_foreign_pytypes`. +pub static EXC_BASE_EXCEPTION_GROUP_LAYOUT_TYPE: PyType = + crate::pyobject::new_pytype("BaseExceptionGroup"); pub static EXC_EXCEPTION_TYPE: PyType = crate::pyobject::new_pytype("Exception"); pub static EXC_ARITHMETIC_ERROR_TYPE: PyType = crate::pyobject::new_pytype("ArithmeticError"); pub static EXC_OVERFLOW_ERROR_TYPE: PyType = crate::pyobject::new_pytype("OverflowError"); diff --git a/pyre/pyre-object/src/lib.rs b/pyre/pyre-object/src/lib.rs index 65eac4fc95b..23a47bdf737 100644 --- a/pyre/pyre-object/src/lib.rs +++ b/pyre/pyre-object/src/lib.rs @@ -13,6 +13,7 @@ pub mod boolobject; pub mod buffer; pub mod bufferview; pub mod bytearrayobject; +pub mod bytes_array; pub mod bytesobject; pub mod celldict; pub mod complexobject; diff --git a/pyre/pyre-object/src/listobject.rs b/pyre/pyre-object/src/listobject.rs index 38e168dda5f..d3f2dcbc40c 100644 --- a/pyre/pyre-object/src/listobject.rs +++ b/pyre/pyre-object/src/listobject.rs @@ -14,9 +14,17 @@ use crate::object_array::{ }; use crate::pyobject::*; use crate::{ - FloatArray, IntArray, floatobject::w_float_get_value, floatobject::w_float_new, - intobject::w_int_get_value, intobject::w_int_new, longobject::jit_bigint_to_i64_value, - longobject::w_long_fits_int, longobject::w_long_get_value, tupleobject::is_plain_float_strict, + FloatArray, IntArray, + bytes_array::BytesArray, + bytesobject::{BYTES_TYPE, w_bytes_block, w_bytes_from_block}, + floatobject::w_float_get_value, + floatobject::w_float_new, + intobject::w_int_get_value, + intobject::w_int_new, + longobject::jit_bigint_to_i64_value, + longobject::w_long_fits_int, + longobject::w_long_get_value, + tupleobject::is_plain_float_strict, }; use std::cell::UnsafeCell; use std::sync::LazyLock; @@ -93,6 +101,25 @@ pub enum ListStrategy { /// `int_items` signed-longlong array: int32 values use RPython's /// 0xfffffffe NaN payload and floats keep their raw IEEE-754 bits. IntOrFloat = 4, + /// listobject.py BytesListStrategy — erased `[rpython str]` payloads. + Bytes = 5, +} + +impl ListStrategy { + /// `interp_magic.py strategy` reads the concrete strategy class name. + /// Keep the spelling beside the representation discriminant so adding a + /// PyPy list strategy cannot silently leave the diagnostic surface stale. + #[inline] + pub const fn class_name(self) -> &'static str { + match self { + Self::Object => "ObjectListStrategy", + Self::Integer => "IntegerListStrategy", + Self::Float => "FloatListStrategy", + Self::Empty => "EmptyListStrategy", + Self::IntOrFloat => "IntOrFloatListStrategy", + Self::Bytes => "BytesListStrategy", + } + } } /// Python list object. @@ -104,10 +131,10 @@ pub enum ListStrategy { /// offset-0 header holds the allocated capacity /// (upstream `len(l.items)` per rlist.py:251). /// -/// `strategy`, `int_items`, `float_items` implement PyPy's list strategy split -/// (`pypy/objspace/std/listobject.py`). Only the Object strategy reads/writes -/// `length` + `items`; Integer/IntOrFloat/Float strategies operate on their -/// own typed arrays and keep `length = 0`, `items = null`. +/// `strategy`, `int_items`, `float_items`, `bytes_items` implement PyPy's list +/// strategy split (`pypy/objspace/std/listobject.py`). Only the Object strategy +/// reads/writes `length` + `items`; Integer/IntOrFloat/Float/Bytes strategies +/// operate on their own typed arrays and keep `length = 0`, `items = null`. #[repr(C)] pub struct W_ListObject { pub ob_header: PyObject, @@ -126,11 +153,12 @@ pub struct W_ListObject { /// the `ItemsBlock` whose offset-0 header is the allocated /// capacity (= upstream `len(l.items)` per rlist.py:251). Null /// when the list is in a non-Object strategy (Empty/Integer/ - /// IntOrFloat/Float); lazily allocated on strategy switch. + /// IntOrFloat/Float/Bytes); lazily allocated on strategy switch. pub items: *mut ItemsBlock, pub strategy: ListStrategy, pub int_items: IntArray, pub float_items: FloatArray, + pub bytes_items: BytesArray, /// PyPy `BaseUserClassMapdict` indexed instance storage for a native /// `list` subclass declaring `__slots__`. Kept on the object itself, /// just like `W_UnicodeObject.w_slots`; `PY_NULL` means that no slot has @@ -160,6 +188,7 @@ impl W_ListObject { ListStrategy::Integer => self.int_items.len(), ListStrategy::IntOrFloat => self.int_items.len(), ListStrategy::Float => self.float_items.len(), + ListStrategy::Bytes => self.bytes_items.len(), } } @@ -250,6 +279,65 @@ impl W_ListObject { crate::gc_roots::shadow_stack_get(obj_slot) } + /// Grow `bytes_items` to accommodate at least `min_cap` slots — the + /// Bytes-strategy counterpart of [`W_ListObject::object_grow`], and the + /// only place a fresh `bytes_items` block may be published. + /// + /// The grow has to be driven from the list. `BytesArray` cannot reach its + /// owner, so growing from inside it allocates the block and stores it into + /// the list as one step, with no way to barrier the list in between. A + /// barrier the caller ran before the call is spent by the collection that + /// allocation itself starts: the list leaves the remembered set again, and + /// the young block then reaches an old list that the next minor collection + /// never visits, which drops the block and every `BytesBlock` reachable + /// only through it. Barrier on both sides of the allocation, with the fresh + /// block rooted across the second one, exactly as `object_grow` does. + unsafe fn bytes_grow(obj: PyObjectRef, min_cap: usize) -> PyObjectRef { + let _roots = crate::gc_roots::push_roots(); + let obj_slot = crate::gc_roots::shadow_stack_len(); + let obj = crate::gc_roots::pin_root(obj); + let list = &*(obj as *const W_ListObject); + let current_cap = list.bytes_items.heap_capacity(); + let target_cap = min_cap.max(current_cap.saturating_mul(2).max(4)); + list_write_barrier(obj); + let new_block_slot = crate::gc_roots::shadow_stack_len(); + let obj = crate::gc_roots::shadow_stack_get(obj_slot); + let list = &*(obj as *const W_ListObject); + let new_block = + grow_list_items_block_gc(list.bytes_items.block, target_cap, list.bytes_items.len()); + let _ = crate::gc_roots::pin_root(new_block as PyObjectRef); + let obj = crate::gc_roots::shadow_stack_get(obj_slot); + list_write_barrier(obj); + let obj = crate::gc_roots::shadow_stack_get(obj_slot); + let list = &mut *(obj as *mut W_ListObject); + let old_block = list.bytes_items.block; + list.bytes_items.block = + crate::gc_roots::shadow_stack_get(new_block_slot) as *mut ItemsBlock; + dealloc_list_items_block(old_block); + crate::gc_roots::shadow_stack_get(obj_slot) + } + + /// Publish an already-built `BytesArray` as this list's `bytes_items`, + /// under [`W_ListObject::bytes_grow`]'s barrier discipline. + /// + /// `fresh` holds a block that nothing roots yet, so it travels on the + /// shadow stack across the owner barrier — the barrier waits on the GC + /// operation gate and can therefore let a collection move the block before + /// `install` pins it. + unsafe fn install_bytes_items(obj: PyObjectRef, fresh: BytesArray) -> PyObjectRef { + let _roots = crate::gc_roots::push_roots(); + let obj_slot = crate::gc_roots::shadow_stack_len(); + let obj = crate::gc_roots::pin_root(obj); + let block_slot = fresh.pin_block(); + list_write_barrier(obj); + let obj = crate::gc_roots::shadow_stack_get(obj_slot); + let list = &mut *(obj as *mut W_ListObject); + let mut fresh = fresh; + fresh.reload_block(block_slot); + list.bytes_items.install(fresh); + crate::gc_roots::shadow_stack_get(obj_slot) + } + /// Upstream list.append equivalent for the object strategy. /// (listobject.py `AbstractUnwrappedStrategy.append` for the /// Object case: no unwrap, just append.) @@ -475,6 +563,15 @@ impl W_ListObject { } } +/// `interp_magic.py strategy` — concrete list strategy class name. +/// +/// # Safety +/// `obj` must point to a live `W_ListObject`. +#[inline] +pub unsafe fn w_list_strategy_name(obj: PyObjectRef) -> &'static str { + unsafe { (*(obj as *const W_ListObject)).strategy.class_name() } +} + /// Grow the Object-strategy backing of `obj` to hold at least one more /// element and return `value` relocated to its post-collection address. /// @@ -509,6 +606,29 @@ pub unsafe fn w_list_grow_items_block(obj: PyObjectRef, value: PyObjectRef) -> P crate::gc_roots::shadow_stack_get(save + 1) } +/// [`w_list_grow_items_block`] for the Bytes strategy: makes room for one more +/// erased `rpython str` and returns `value` at its post-grow address. +/// +/// `value` is pinned across the grow for the same reason the object arm pins +/// it — [`W_ListObject::bytes_grow`] allocates in the moving nursery and may +/// collect, so the caller must store the returned pointer, not the argument it +/// passed. +/// +/// # Safety +/// `obj` must point to a valid Bytes-strategy `W_ListObject`; `value` must be +/// a live `PyObjectRef`. +#[majit_macros::dont_look_inside] +pub unsafe fn w_list_grow_bytes_block(obj: PyObjectRef, value: PyObjectRef) -> PyObjectRef { + let _roots = crate::gc_roots::push_roots(); + let save = crate::gc_roots::shadow_stack_len(); + let _ = crate::gc_roots::pin_root(obj); + let _ = crate::gc_roots::pin_root(value); + let obj = crate::gc_roots::shadow_stack_get(save); + let list = &*(obj as *const W_ListObject); + W_ListObject::bytes_grow(obj, list.bytes_items.len() + 1); + crate::gc_roots::shadow_stack_get(save + 1) +} + /// listobject.py is_plain_int1(w_obj) /// /// Accepts exact W_IntObject (not bool, not int subclass) or W_LongObject @@ -735,6 +855,40 @@ fn boxed_from_floats(values: &[f64]) -> Vec { .collect() } +#[inline] +fn is_bytes_strategy_item(item: PyObjectRef) -> bool { + unsafe { is_exact_type(item, &BYTES_TYPE) } +} + +fn all_bytes(items: &[PyObjectRef]) -> bool { + items.iter().all(|&item| is_bytes_strategy_item(item)) +} + +/// Box each erased `rpython str` of the list pinned at `obj_slot`. +/// +/// Unlike the int/float pair this cannot walk a slice taken once: every +/// `w_bytes_from_block` allocates, and a collection inside the loop forwards +/// `bytes_items.block`, so a base pointer captured up front goes on naming the +/// outgoing block. Re-read the array from the pinned list at each step. +/// +/// # Safety +/// `obj_slot` must hold a live `W_ListObject` in the Bytes strategy. +unsafe fn boxed_from_bytes(obj_slot: usize) -> Vec { + let _roots = crate::gc_roots::push_roots(); + let root_base = crate::gc_roots::shadow_stack_len(); + let bytes_items = |slot: usize| -> &BytesArray { + &(*(crate::gc_roots::shadow_stack_get(slot) as *const W_ListObject)).bytes_items + }; + let len = bytes_items(obj_slot).len(); + for i in 0..len { + let value = bytes_items(obj_slot).as_slice()[i]; + let _ = crate::gc_roots::pin_root(w_bytes_from_block(value)); + } + (0..len) + .map(|i| crate::gc_roots::shadow_stack_get(root_base + i)) + .collect() +} + /// Cold list strategy dehomogenization: a typed int/float list gained a /// non-numeric element, so its unboxed backing storage is bulk re-boxed into /// an Object-strategy items block one time. @@ -763,6 +917,7 @@ pub unsafe fn switch_to_object_strategy(list: &mut W_ListObject) -> PyObjectRef ListStrategy::Integer => boxed_from_ints(list.int_items.as_slice()), ListStrategy::IntOrFloat => boxed_from_int_or_float(list.int_items.as_slice()), ListStrategy::Float => boxed_from_floats(list.float_items.as_slice()), + ListStrategy::Bytes => boxed_from_bytes(obj_slot), ListStrategy::Object | ListStrategy::Empty => Vec::new(), }; let obj = crate::gc_roots::shadow_stack_get(obj_slot); @@ -781,12 +936,20 @@ pub unsafe fn switch_to_object_strategy(list: &mut W_ListObject) -> PyObjectRef list.strategy = ListStrategy::Object; let obj = crate::gc_roots::shadow_stack_get(obj_slot); let list = &mut *(obj as *mut W_ListObject); - // Object strategy reads neither typed array again, so drop both to the - // empty form instead of installing two fresh single-slot blocks. + // Object strategy reads none of the typed arrays again, so drop all three + // to the empty form instead of installing fresh single-slot blocks. Each + // `install` pins and reloads its incoming block, so it is a safepoint and + // the list is re-read from its slot before the next one: writing a later + // field through the reference the previous install left behind stores it + // into the moved-from copy, and the live list keeps the outgoing block — + // which the custom trace then forwards as a stale child. list.int_items.install(IntArray::empty()); let obj = crate::gc_roots::shadow_stack_get(obj_slot); let list = &mut *(obj as *mut W_ListObject); list.float_items.install(FloatArray::empty()); + let obj = crate::gc_roots::shadow_stack_get(obj_slot); + let list = &mut *(obj as *mut W_ListObject); + list.bytes_items.install(BytesArray::empty()); crate::gc_roots::shadow_stack_get(obj_slot) } @@ -819,6 +982,17 @@ unsafe fn switch_to_correct_strategy(list: &mut W_ListObject, w_item: PyObjectRe let obj = crate::gc_roots::shadow_stack_get(root_base); let list = &mut *(obj as *mut W_ListObject); list.strategy = ListStrategy::Float; + } else if is_bytes_strategy_item(w_item) { + // The immediately following append grows this null/zero rlist form + // before storing. Avoiding a bulk Vec conversion here keeps the + // generated append graph on PyPy's look-inside path. + let fresh = BytesArray::empty(); + let obj = crate::gc_roots::shadow_stack_get(root_base); + let list = &mut *(obj as *mut W_ListObject); + list.bytes_items.install(fresh); + let obj = crate::gc_roots::shadow_stack_get(root_base); + let list = &mut *(obj as *mut W_ListObject); + list.strategy = ListStrategy::Bytes; } else { list.set_object_items_from_vec(Vec::new()); let obj = crate::gc_roots::shadow_stack_get(root_base); @@ -841,6 +1015,8 @@ pub fn list_strategy_for(items: &[PyObjectRef]) -> ListStrategy { ListStrategy::Float } else if all_int_or_float(items) { ListStrategy::IntOrFloat + } else if all_bytes(items) { + ListStrategy::Bytes } else { ListStrategy::Object } @@ -1017,6 +1193,12 @@ unsafe fn build_list_storage(items: &[PyObjectRef], strategy: ListStrategy) -> L FloatArray::empty() }; let float_block_root = float_items.pin_block(); + let bytes_items = if let ListStrategy::Bytes = strategy { + BytesArray::from_vec(items.iter().map(|&item| w_bytes_block(item)).collect()) + } else { + BytesArray::empty() + }; + let bytes_block_root = bytes_items.pin_block(); let (length, block) = if let ListStrategy::Object = strategy { (items.len(), alloc_list_items_block_gc(items)) } else { @@ -1027,8 +1209,10 @@ unsafe fn build_list_storage(items: &[PyObjectRef], strategy: ListStrategy) -> L block, int_items, float_items, + bytes_items, int_block_root, float_block_root, + bytes_block_root, } } @@ -1039,8 +1223,10 @@ struct ListStorage { block: *mut ItemsBlock, int_items: IntArray, float_items: FloatArray, + bytes_items: BytesArray, int_block_root: usize, float_block_root: usize, + bytes_block_root: usize, } impl ListStorage { @@ -1050,6 +1236,7 @@ impl ListStorage { fn reload_typed_blocks(&mut self) { self.int_items.reload_block(self.int_block_root); self.float_items.reload_block(self.float_block_root); + self.bytes_items.reload_block(self.bytes_block_root); } } @@ -1071,6 +1258,7 @@ pub fn w_list_new_with_strategy(items: Vec, strategy: ListStrategy) ListStrategy::Integer => all_ints(&items), ListStrategy::Float => all_floats(&items), ListStrategy::IntOrFloat => all_int_or_float(&items), + ListStrategy::Bytes => all_bytes(&items), ListStrategy::Object => true, }, "list items do not support the requested storage strategy", @@ -1079,7 +1267,7 @@ pub fn w_list_new_with_strategy(items: Vec, strategy: ListStrategy) // pin every PyObjectRef in `items` before the GC malloc paths // below (`alloc_list_items_block_gc`, the collecting header allocation) so the // shadow stack walker sees them if a collection fires inside the - // allocator. The Empty / Integer / Float strategies still hold + // allocator. The Empty / Integer / Float / Bytes strategies still hold // PyObjectRef pointers in `items` until each element is unboxed // (`plain_int_w`, `w_float_get_value`); pinning all of them at // function entry covers every strategy uniformly. @@ -1126,6 +1314,7 @@ pub fn w_list_new_with_strategy(items: Vec, strategy: ListStrategy) ListStrategy::Object => items_block as *mut u8, ListStrategy::Integer | ListStrategy::IntOrFloat => storage.int_items.block as *mut u8, ListStrategy::Float => storage.float_items.block as *mut u8, + ListStrategy::Bytes => storage.bytes_items.block as *mut u8, ListStrategy::Empty => std::ptr::null_mut(), }; let mut needs_write_barrier = true; @@ -1149,6 +1338,7 @@ pub fn w_list_new_with_strategy(items: Vec, strategy: ListStrategy) let ListStorage { int_items, float_items, + bytes_items, .. } = storage; // Re-read the (possibly relocated) nursery items block before either the @@ -1165,6 +1355,7 @@ pub fn w_list_new_with_strategy(items: Vec, strategy: ListStrategy) strategy, int_items, float_items, + bytes_items, w_slots: PY_NULL, }); return Box::into_raw(boxed) as PyObjectRef; @@ -1180,6 +1371,7 @@ pub fn w_list_new_with_strategy(items: Vec, strategy: ListStrategy) strategy, int_items, float_items, + bytes_items, w_slots: PY_NULL, }, ); @@ -1188,7 +1380,7 @@ pub fn w_list_new_with_strategy(items: Vec, strategy: ListStrategy) // spill to old-gen (for example around pinned nursery gaps); only that // placement needs remembering for its young Object-strategy items edge. // Integer/Float blocks are old-gen leaf arrays and need no barrier. - if strategy == ListStrategy::Object && needs_write_barrier { + if matches!(strategy, ListStrategy::Object | ListStrategy::Bytes) && needs_write_barrier { list_write_barrier_impl(raw as PyObjectRef, true); } raw as PyObjectRef @@ -1438,6 +1630,14 @@ pub unsafe fn w_list_getitem(obj: PyObjectRef, index: i64) -> Option { + let len = list.bytes_items.len() as i64; + let idx = if index < 0 { index + len } else { index }; + if idx < 0 || idx >= len { + return None; + } + Some(w_bytes_from_block(list.bytes_items[idx as usize])) + } } } @@ -1543,6 +1743,24 @@ pub unsafe fn w_list_setitem(obj: PyObjectRef, index: i64, value: PyObjectRef) - ) } } + ListStrategy::Bytes => { + let len = list.bytes_items.len() as i64; + let idx = if index < 0 { index + len } else { index }; + if idx < 0 || idx >= len { + return false; + } + if is_bytes_strategy_item(value) { + list.bytes_items.set(idx as usize, w_bytes_block(value)); + true + } else { + switch_to_object_strategy(list); + w_list_setitem( + crate::gc_roots::shadow_stack_get(root_base), + index, + crate::gc_roots::shadow_stack_get(root_base + 1), + ) + } + } } } @@ -1724,6 +1942,29 @@ pub unsafe fn w_list_append_inner(obj: PyObjectRef, value: PyObjectRef) { list.object_push(value); } } + ListStrategy::Bytes => { + if is_bytes_strategy_item(value) { + let value = prepare_list_ref_store(obj, value); + let obj = current_gc_ref(obj); + let list = &*(obj as *const W_ListObject); + // At capacity, route the grow through the list the way + // `object_push` does: the fresh block reaches `bytes_items` + // with the owner barrier directly in front of the store. + let value = if list.bytes_items.spare_capacity() == 0 { + w_list_grow_bytes_block(obj, value) + } else { + value + }; + let obj = current_gc_ref(obj); + let list = &mut *(obj as *mut W_ListObject); + list.bytes_items.push(w_bytes_block(value)); + } else { + let obj = switch_to_object_strategy(list); + let value = current_gc_ref(value); + let list = &mut *(obj as *mut W_ListObject); + list.object_push(value); + } + } } } @@ -1835,6 +2076,7 @@ pub unsafe fn w_list_len(obj: PyObjectRef) -> usize { ListStrategy::Integer => ll_list_int_length(list), ListStrategy::IntOrFloat => list.int_items.len(), ListStrategy::Float => list.float_items.len(), + ListStrategy::Bytes => list.bytes_items.len(), } } @@ -1945,6 +2187,7 @@ pub unsafe fn w_list_can_append_without_realloc(obj: PyObjectRef) -> bool { ListStrategy::Integer => list.int_items.spare_capacity() > 0, ListStrategy::IntOrFloat => list.int_items.spare_capacity() > 0, ListStrategy::Float => list.float_items.spare_capacity() > 0, + ListStrategy::Bytes => list.bytes_items.spare_capacity() > 0, } } @@ -1964,6 +2207,7 @@ pub unsafe fn w_list_is_inline_storage(obj: PyObjectRef) -> bool { ListStrategy::Integer => list.int_items.is_inline(), ListStrategy::IntOrFloat => list.int_items.is_inline(), ListStrategy::Float => list.float_items.is_inline(), + ListStrategy::Bytes => list.bytes_items.is_inline(), } } @@ -2035,7 +2279,8 @@ unsafe fn rebuild_object_items(list: &mut W_ListObject, items: Vec) /// Snapshot all items of a list as a `Vec`, regardless of /// strategy. Integer/Float items are wrapped into `W_IntObject` / -/// `W_FloatObject`, matching listobject.py:363-371 +/// `W_FloatObject`, and Bytes items are re-wrapped from their erased strings, +/// matching listobject.py `_temporarily_as_objects()`. /// `_temporarily_as_objects()`. Used by callers outside `pyre-object` /// (e.g. the interpreter's unpack / set-update / list-to-tuple paths) /// that need a uniform object view. @@ -2048,8 +2293,9 @@ pub unsafe fn w_list_items_copy_as_vec(obj: PyObjectRef) -> Vec { } /// Raw `(ptr, len)` view of an Object-strategy list's `PyObjectRef` items for -/// GC root walking. Returns `None` for Empty / Integer / Float strategies: -/// those store unboxed scalars with no GC children, and materialising them +/// GC root walking. Returns `None` for Empty / Integer / Float / Bytes +/// strategies: scalar strategies have no GC children, while Bytes storage is +/// walked by `list_object_custom_trace`; materialising either representation /// would allocate — forbidden while the collector is marking. /// /// # Safety @@ -2100,6 +2346,16 @@ unsafe fn temporarily_as_objects(list: &W_ListObject) -> Vec { .map(|i| crate::gc_roots::shadow_stack_get(root_base + i)) .collect() } + ListStrategy::Bytes => { + // The wraps allocate, so the list has to be reachable by slot for + // the re-read `boxed_from_bytes` does per element. + let _roots = crate::gc_roots::push_roots(); + let _ = crate::gc_roots::pin_root( + (list as *const W_ListObject as *mut W_ListObject) as PyObjectRef, + ); + let obj_slot = crate::gc_roots::shadow_stack_len() - 1; + boxed_from_bytes(obj_slot) + } } } @@ -2208,6 +2464,32 @@ pub unsafe fn w_list_insert(obj: PyObjectRef, index: i64, value: PyObjectRef) { let list = &mut *(obj as *mut W_ListObject); list.sync_allocated(old_size); } + ListStrategy::Bytes => { + if is_bytes_strategy_item(value) { + let idx = normalize_insert_index(index, list.bytes_items.len()); + let value = prepare_list_ref_store(obj, value); + let obj = current_gc_ref(obj); + let list = &*(obj as *const W_ListObject); + // Same reservation the append arm makes: `insert` may not + // publish a fresh block itself. + let value = if list.bytes_items.spare_capacity() == 0 { + w_list_grow_bytes_block(obj, value) + } else { + value + }; + let obj = current_gc_ref(obj); + let list = &mut *(obj as *mut W_ListObject); + list.bytes_items.insert(idx, w_bytes_block(value)); + list.sync_allocated(old_size); + } else { + switch_to_object_strategy(list); + w_list_insert( + crate::gc_roots::shadow_stack_get(root_base), + index, + crate::gc_roots::shadow_stack_get(root_base + 1), + ); + } + } } } @@ -2278,6 +2560,14 @@ pub unsafe fn w_list_pop(obj: PyObjectRef, index: i64) -> Option { } Some(list.object_remove(idx as usize)) } + ListStrategy::Bytes => { + let len = list.bytes_items.len() as i64; + let idx = if index < 0 { index + len } else { index }; + if idx < 0 || idx >= len { + return None; + } + Some(w_bytes_from_block(list.bytes_items.remove(idx as usize))) + } }; if result.is_some() { list.sync_allocated(old_size); @@ -2314,6 +2604,7 @@ pub unsafe fn w_list_pop_end(obj: PyObjectRef) -> Option { ListStrategy::IntOrFloat => list.int_items.len(), ListStrategy::Float => list.float_items.len(), ListStrategy::Object => list.length, + ListStrategy::Bytes => list.bytes_items.len(), }; if length == 0 { None @@ -2355,6 +2646,7 @@ pub unsafe fn w_list_pop_end_inner(obj: PyObjectRef) -> PyObjectRef { } ListStrategy::Float => w_float_new(list.float_items.pop()), ListStrategy::Object => list.object_pop(), + ListStrategy::Bytes => w_bytes_from_block(list.bytes_items.pop()), } } @@ -2486,7 +2778,11 @@ pub unsafe fn w_list_init_items(obj: PyObjectRef, items: Vec) { list.strategy = strategy; list.int_items = storage.int_items; list.float_items = storage.float_items; - if strategy == ListStrategy::Object { + list.bytes_items = storage.bytes_items; + // Object and Bytes storage both publish a freshly allocated GC block from + // an existing list header. Integer/Float blocks are old-generation leaf + // arrays and need no remembered-set edge. + if matches!(strategy, ListStrategy::Object | ListStrategy::Bytes) { list_write_barrier(obj); } } @@ -2517,6 +2813,7 @@ pub unsafe fn w_list_clear(obj: PyObjectRef) { // matching one through `switch_to_correct_strategy`. list.int_items.install(IntArray::empty()); list.float_items.install(FloatArray::empty()); + list.bytes_items.install(BytesArray::empty()); list.strategy = ListStrategy::Empty; list.allocated = 0; } @@ -2548,6 +2845,7 @@ pub unsafe fn w_list_reverse(obj: PyObjectRef) { ListStrategy::Integer => list.int_items.as_mut_slice().reverse(), ListStrategy::IntOrFloat => list.int_items.as_mut_slice().reverse(), ListStrategy::Float => list.float_items.as_mut_slice().reverse(), + ListStrategy::Bytes => list.bytes_items.reverse(), ListStrategy::Object => list.object_reverse(), } } @@ -2591,6 +2889,15 @@ pub unsafe fn w_list_delslice(obj: PyObjectRef, start: usize, end: usize) { changed = true; } } + ListStrategy::Bytes => { + let len = list.bytes_items.len(); + let s = start.min(len); + let e = end.min(len); + if s < e { + list.bytes_items.drain(s..e); + changed = true; + } + } ListStrategy::Object => { let len = list.length; let s = start.min(len); @@ -2833,6 +3140,18 @@ unsafe fn w_list_setslice_inner( list.strategy = ListStrategy::Float; return Ok(()); } + ListStrategy::Bytes => { + let other = + &*(crate::gc_roots::shadow_stack_get(root_base + 1) as *const W_ListObject); + let fresh = BytesArray::from_vec(other.bytes_items.to_vec()); + let obj = W_ListObject::install_bytes_items( + crate::gc_roots::shadow_stack_get(root_base), + fresh, + ); + let list = &mut *(obj as *mut W_ListObject); + list.strategy = ListStrategy::Bytes; + return Ok(()); + } ListStrategy::Object => { list.set_object_items_from_vec(other.object_to_vec()); let obj = crate::gc_roots::shadow_stack_get(root_base); @@ -2956,6 +3275,41 @@ unsafe fn w_list_setslice_inner( } return Ok(()); } + ListStrategy::Bytes => { + let obj = crate::gc_roots::shadow_stack_get(root_base); + let w_other = crate::gc_roots::shadow_stack_get(root_base + 1); + let list = &*(obj as *const W_ListObject); + let other = &*(w_other as *const W_ListObject); + let donates = list.strategy == other.strategy; + let s = start.min(list.bytes_items.len()); + let e = end.min(list.bytes_items.len()); + if obj == w_other { + let mut values = list.bytes_items.to_vec(); + let donated = values.clone(); + values.splice(s..e, donated.into_iter()); + W_ListObject::install_bytes_items(obj, BytesArray::from_vec(values)); + return Ok(()); + } + // `splice` may not publish a fresh block itself, so the + // room it needs is reserved through the list first. The + // grow collects, so the donor slice is taken after it. + let donated = if donates { other.bytes_items.len() } else { 0 }; + let grown = list.bytes_items.len() - (e - s) + donated; + if grown > list.bytes_items.heap_capacity() { + W_ListObject::bytes_grow(obj, grown); + } + let obj = crate::gc_roots::shadow_stack_get(root_base); + let w_other = crate::gc_roots::shadow_stack_get(root_base + 1); + let list = &mut *(obj as *mut W_ListObject); + let other = &*(w_other as *const W_ListObject); + let new_items = if donates { + other.bytes_items.as_slice() + } else { + &[] + }; + list.bytes_items.splice(s, e - s, new_items); + return Ok(()); + } ListStrategy::Object => {} } } @@ -3028,6 +3382,19 @@ mod tests { use super::*; use crate::intobject::w_int_new; + #[test] + fn strategy_class_names_follow_interp_magic_spellings() { + assert_eq!(ListStrategy::Empty.class_name(), "EmptyListStrategy"); + assert_eq!(ListStrategy::Object.class_name(), "ObjectListStrategy"); + assert_eq!(ListStrategy::Integer.class_name(), "IntegerListStrategy"); + assert_eq!(ListStrategy::Float.class_name(), "FloatListStrategy"); + assert_eq!( + ListStrategy::IntOrFloat.class_name(), + "IntOrFloatListStrategy" + ); + assert_eq!(ListStrategy::Bytes.class_name(), "BytesListStrategy"); + } + #[test] fn test_list_create_and_access() { let items = vec![w_int_new(10), w_int_new(20), w_int_new(30)]; @@ -3078,6 +3445,7 @@ mod tests { assert_eq!(l.strategy, ListStrategy::Object); assert!(l.int_items.block.is_null()); assert!(l.float_items.block.is_null()); + assert!(l.bytes_items.block.is_null()); assert!(l.int_items.as_slice().is_empty()); assert!(l.float_items.as_slice().is_empty()); @@ -3085,11 +3453,44 @@ mod tests { assert_eq!(l.strategy, ListStrategy::Integer); assert!(!l.int_items.block.is_null()); assert!(l.float_items.block.is_null()); + assert!(l.bytes_items.block.is_null()); let l = &*(float_list as *const W_ListObject); assert_eq!(l.strategy, ListStrategy::Float); assert!(l.int_items.block.is_null()); assert!(!l.float_items.block.is_null()); + assert!(l.bytes_items.block.is_null()); + } + } + + #[test] + fn bytes_strategy_stores_erased_blocks_and_dehomogenizes() { + let a = crate::bytesobject::w_bytes_from_bytes(b"a"); + let b = crate::bytesobject::w_bytes_from_bytes(b"b"); + let list = w_list_new(vec![a, b]); + unsafe { + let l = &*(list as *const W_ListObject); + assert_eq!(l.strategy, ListStrategy::Bytes); + assert!(l.items.is_null()); + assert!(!l.bytes_items.block.is_null()); + assert_eq!( + crate::bytesobject::w_bytes_data(w_list_getitem(list, 0).unwrap()), + b"a" + ); + + let c = crate::bytesobject::w_bytes_from_bytes(b"c"); + w_list_append(list, c); + assert_eq!( + (*(list as *const W_ListObject)).strategy, + ListStrategy::Bytes + ); + assert_eq!(w_list_len(list), 3); + + w_list_append(list, crate::w_str_new("not bytes")); + let l = &*(list as *const W_ListObject); + assert_eq!(l.strategy, ListStrategy::Object); + assert!(l.bytes_items.block.is_null()); + assert_eq!(w_list_len(list), 4); } } @@ -3120,6 +3521,7 @@ mod tests { assert_eq!(l.strategy, ListStrategy::Empty); assert!(l.int_items.block.is_null()); assert!(l.float_items.block.is_null()); + assert!(l.bytes_items.block.is_null()); // The next append reinstalls the matching typed storage. w_list_append(list, w_int_new(9)); let l = &*(list as *const W_ListObject); diff --git a/pyre/pyre-object/src/specialisedtupleobject.rs b/pyre/pyre-object/src/specialisedtupleobject.rs index 5d420898c85..29bd6c7a00c 100644 --- a/pyre/pyre-object/src/specialisedtupleobject.rs +++ b/pyre/pyre-object/src/specialisedtupleobject.rs @@ -18,9 +18,8 @@ //! Each variant also carries Python 3.14's mutable-once tuple hash cache; //! this is the requested 3.14 delta from PyPy's specialized layouts. //! -//! Data structures are landed. Construction -//! dispatch (`makespecialisedtuple2`) and JIT specialisation are -//! not yet implemented. +//! Construction dispatch lives in `tupleobject::makespecialisedtuple2`; tuple +//! readers and the generated JIT dispatch on these three concrete layouts. #![allow(non_camel_case_types)] diff --git a/pyre/pyre-object/src/unicodeobject.rs b/pyre/pyre-object/src/unicodeobject.rs index 87e42737936..1b2ae0787dd 100644 --- a/pyre/pyre-object/src/unicodeobject.rs +++ b/pyre/pyre-object/src/unicodeobject.rs @@ -60,6 +60,20 @@ pub struct W_UnicodeObject { pub hash: i64, } +impl W_UnicodeObject { + /// `unicodeobject.py W_UnicodeObject.eq_w` — the typed equality shortcut + /// used by `UnicodeDictStrategy` and `argument.contains_w_names`. + /// + /// Both operands are already proven `W_UnicodeObject`s by those callers, + /// so this compares the underlying WTF-8 buffers directly and never + /// dispatches an app-level `__eq__`. WTF-8 preserves PyPy's `_utf8` + /// byte equality for lone surrogates as well as ordinary Unicode. + #[inline] + pub fn eq_w(&self, w_other: &W_UnicodeObject) -> bool { + unsafe { &*self.value == &*w_other.value } + } +} + /// The translated user-subclass layout selected by `typedef.py:174-227`. /// The builtin string payload stays unchanged; the generated user class adds /// `MapdictStorageMixin` after it. @@ -835,6 +849,19 @@ pub unsafe fn w_str_get_wtf8(obj: PyObjectRef) -> &'static Wtf8 { } } +/// Object-space entry to [`W_UnicodeObject::eq_w`]. +/// +/// # Safety +/// Both arguments must point to valid `W_UnicodeObject`s. +#[inline] +pub unsafe fn w_str_eq_w(obj: PyObjectRef, w_other: PyObjectRef) -> bool { + unsafe { + let this = &*(obj as *const W_UnicodeObject); + let other = &*(w_other as *const W_UnicodeObject); + this.eq_w(other) + } +} + /// `rstr.py ll_strhash` — the memoized digest, or zero while it has /// not been computed yet ("our malloc initializes the memory to zero, so we /// use zero as the value of a string whose hash is not computed yet"). @@ -1274,6 +1301,31 @@ mod tests { } } + #[test] + fn test_str_eq_w_compares_wtf8_without_python_dispatch() { + let a = w_str_new("café"); + let b = w_str_new("café"); + let c = w_str_new("cafe"); + unsafe { + assert!(w_str_eq_w(a, b)); + assert!(!w_str_eq_w(a, c)); + } + + let mut left = Wtf8Buf::new(); + left.push(CodePoint::from_u32(0xD800).unwrap()); + let mut right = Wtf8Buf::new(); + right.push(CodePoint::from_u32(0xD800).unwrap()); + let mut different = Wtf8Buf::new(); + different.push(CodePoint::from_u32(0xD801).unwrap()); + let left = w_str_from_wtf8(left); + let right = w_str_from_wtf8(right); + let different = w_str_from_wtf8(different); + unsafe { + assert!(w_str_eq_w(left, right)); + assert!(!w_str_eq_w(left, different)); + } + } + #[test] fn test_str_codepoint_at_indexes_code_points_not_bytes() { let ascii = w_str_new("hello");