diff --git a/dist-workspace.toml b/dist-workspace.toml index 1cc1f94c195..c6d9d70ff7b 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -29,7 +29,7 @@ install-updater = false # and it is included beside the executable in every archive and installer. # `include` keeps each item's own name and has no per-target form, so the one # entry names the stdlib directory on every platform: `lib` holding -# `pyre3.14`, except on Windows where the stdlib is `Lib` itself and the two +# `pyre3.14t`, except on Windows where the stdlib is `Lib` itself and the two # spellings are the same path. include = ["dist-assets/lib"] github-build-setup = "../pyre-dist-build-setup.yml" diff --git a/pyre/extra_tests/parity_tests/dict_set_sizeof_python314.py b/pyre/extra_tests/parity_tests/dict_set_sizeof_python314.py index c9b4adc9308..c4c7145e141 100644 --- a/pyre/extra_tests/parity_tests/dict_set_sizeof_python314.py +++ b/pyre/extra_tests/parity_tests/dict_set_sizeof_python314.py @@ -3,19 +3,30 @@ """Python 3.14 ``__sizeof__`` surface for dict and set-like types.""" +import struct +import sysconfig + for typ in (dict, set, frozenset): assert "__sizeof__" in typ.__dict__ assert typ.__sizeof__.__text_signature__ == "($self, /)" -assert dict().__sizeof__() == 48 -assert {0: None}.__sizeof__() == 208 -assert {str(i): None for i in range(6)}.__sizeof__() == 256 -assert dict.fromkeys(range(11)).__sizeof__() == 616 +# The numbers below are the layout of a build that has a global interpreter +# lock. Without one the object header carries the thread id, flags, mutex, gc +# bits and the two refcount halves rather than one refcount, which is two words +# wider, and every container answers that much more. `test.support.calcobjsize` +# derives its struct sizes from the same config var, so the oracle and pyre can +# disagree about the build and still agree about the layout. +HEADER = 2 * struct.calcsize("P") if sysconfig.get_config_var("Py_GIL_DISABLED") else 0 + +assert dict().__sizeof__() == 48 + HEADER +assert {0: None}.__sizeof__() == 208 + HEADER +assert {str(i): None for i in range(6)}.__sizeof__() == 256 + HEADER +assert dict.fromkeys(range(11)).__sizeof__() == 616 + HEADER for typ in (set, frozenset): - assert typ().__sizeof__() == 200 - assert typ(range(4)).__sizeof__() == 200 - assert typ(range(5)).__sizeof__() == 712 - assert typ(range(19)).__sizeof__() == 2248 + assert typ().__sizeof__() == 200 + HEADER + assert typ(range(4)).__sizeof__() == 200 + HEADER + assert typ(range(5)).__sizeof__() == 712 + HEADER + assert typ(range(19)).__sizeof__() == 2248 + HEADER print("OK") diff --git a/pyre/pyre-interpreter/include/pyre3.14/Python.h b/pyre/pyre-interpreter/include/pyre3.14t/Python.h similarity index 100% rename from pyre/pyre-interpreter/include/pyre3.14/Python.h rename to pyre/pyre-interpreter/include/pyre3.14t/Python.h diff --git a/pyre/pyre-interpreter/src/eval.rs b/pyre/pyre-interpreter/src/eval.rs index 569f3625018..5e15cc70311 100644 --- a/pyre/pyre-interpreter/src/eval.rs +++ b/pyre/pyre-interpreter/src/eval.rs @@ -8420,7 +8420,7 @@ result = ( 24, 24, 24, 24] and [allocation(item) for item in sources] == [6, 6, 6, 8, 8, 8, 8, 8, 8] and allocation(list(HintTwenty())) == 8 - and sort_seen == [(0, 32), (0, 32), (0, 32)] + and sort_seen == [(0, 56), (0, 56), (0, 56)] and sorted_value == [1, 2, 3] and allocation(sorted_value) == 4 and noop_sorted == [1, 2, 3] diff --git a/pyre/pyre-interpreter/src/importing.rs b/pyre/pyre-interpreter/src/importing.rs index 195620eeee1..9777da34f18 100644 --- a/pyre/pyre-interpreter/src/importing.rs +++ b/pyre/pyre-interpreter/src/importing.rs @@ -849,9 +849,8 @@ fn init_string_module(ns: PyObjectRef) { /// `sysconfig._init_non_posix` SUBSCRIPTS `Py_GIL_DISABLED` and `Py_DEBUG` to /// spell `ABIFLAGS`, so on Windows a missing key is a `KeyError` out of the /// first `get_config_var` call rather than the `None` the `.get()` readers -/// take. Pyre runs its mutators under a global interpreter lock -/// (`majit-gc/src/rgil.rs`, the `thread_gil.c` port), so `Py_GIL_DISABLED` is -/// 0 and the derived `ABIFLAGS` is empty; `Py_DEBUG` is 0. +/// take. Pyre runs its mutators without a global interpreter lock, so +/// `Py_GIL_DISABLED` is 1 and the derived `ABIFLAGS` is `t`; `Py_DEBUG` is 0. /// /// `EXT_SUFFIX` and `SOABI` name pyre's own cpyext ABI, never CPython's ABI /// tag. They are build metadata rather than a claim that an extension loader @@ -869,7 +868,7 @@ fn init_sysconfig_stub(ns: PyObjectRef) { let vars = pyre_object::w_dict_new(); let so_ext = extension_abi_suffix(); unsafe { - for (name, value) in [("Py_DEBUG", 0), ("Py_GIL_DISABLED", 0)] { + for (name, value) in [("Py_DEBUG", 0), ("Py_GIL_DISABLED", 1)] { pyre_object::w_dict_store( vars, pyre_object::w_str_new(name), @@ -1087,14 +1086,12 @@ fn init_sysconfigdata(ns: PyObjectRef) { )); let vars = pyre_object::w_dict_new(); - // Empty, matching the `Py_GIL_DISABLED` of 0 that `_init_non_posix` - // derives the non-posix spelling from, and matching the lower-case - // `abiflags` that forms the include and site-packages directory names — - // a separate variable read from `sys.abiflags` (`sysconfig:545`). The - // release tree has no `t` suffix on its stdlib directory for - // `site.py:409` to find, and `sysconfig`'s `abi_thread` must name the - // same directory `site.py` does. - store_str(vars, "ABIFLAGS", ""); + // `_init_non_posix` derives the same `t` from `Py_GIL_DISABLED` below. + // The lower-case `abiflags` that forms the include and site-packages + // directory names is a separate variable, read from `sys.abiflags` + // (`sysconfig:545`), and stays empty: the release tree has no `t` suffix + // on its stdlib directory for `site.py:409` to find. + store_str(vars, "ABIFLAGS", "t"); store_str(vars, "SOABI", &soabi); // Deprecated in Python 3, kept for backward compatibility. store_str(vars, "SO", &so_ext); @@ -1114,13 +1111,11 @@ fn init_sysconfigdata(ns: PyObjectRef) { store_str(vars, "EXE", ""); store_str(vars, "VERSION", "3.14"); store_str(vars, "LDVERSION", "3.14"); - // cpyext never uses Py_DEBUG. Pyre runs its mutators under a global - // interpreter lock (`majit-gc/src/rgil.rs` ports `thread_gil.c`), so it is - // not a free-threaded build: `test.support.Py_GIL_DISABLED` reads this key - // and skips whole suites on it. Py_ENABLE_SHARED at 1 would add a python - // shared object to link lines as `-lpython3.x`. + // cpyext never uses Py_DEBUG. Pyre runs its mutators without a global + // interpreter lock. Py_ENABLE_SHARED at 1 would add a python shared + // object to link lines as `-lpython3.x`. store_int(vars, "Py_DEBUG", 0); - store_int(vars, "Py_GIL_DISABLED", 0); + store_int(vars, "Py_GIL_DISABLED", 1); store_int(vars, "Py_ENABLE_SHARED", 0); // Pyre currently has neither a CPython-compatible C API nor a separately // linkable runtime library. Keep the build ABI metadata above for wheel @@ -1781,7 +1776,7 @@ pub(crate) struct StartupPathConfig { pub base_prefix: PathBuf, /// Bootstrap search entries in PyPy's order. A source checkout keeps /// `lib_pypy` before `lib-python/3`; an installed tree has one merged - /// `lib/pyre3.14` entry. + /// `lib/pyre3.14t` entry. pub stdlib_paths: Vec, /// The language stdlib root (the entry containing `os.py` / `site.py`). /// This is exposed as `sys._stdlib_dir` and is deliberately distinct from @@ -1913,7 +1908,7 @@ fn find_invoked_executable() -> PathBuf { } /// Recognize both PyPy-style source trees (`lib-python/3`) and the packaged -/// Pyre layout (`lib/pyre3.14`). The latter is the installation shape pip and +/// Pyre layout (`lib/pyre3.14t`). The latter is the installation shape pip and /// venv will consume; accepting the former keeps the repository executable as /// the untranslated/development oracle, matching PyPy's two /// `compute_stdlib_path_*` arms. @@ -1950,7 +1945,7 @@ fn stdlib_at_prefix(prefix: &Path) -> Option<(Vec, Option)> { // `compute_stdlib_path`: release packaging merges the two source trees // into one implementation-version directory. for packaged in [ - prefix.join("lib").join("pyre3.14"), + prefix.join("lib").join("pyre3.14t"), // cargo-dist's Homebrew formula installs non-binary archive contents // into `pkgshare` (`/share/pyrex`). This is still a single // interpreter-owned prefix, analogous to PyPy's macOS bundle search @@ -1959,7 +1954,7 @@ fn stdlib_at_prefix(prefix: &Path) -> Option<(Vec, Option)> { .join("share") .join("pyrex") .join("lib") - .join("pyre3.14"), + .join("pyre3.14t"), ] { if packaged.join("site.py").is_file() { let mut paths = vec![packaged.clone()]; @@ -2081,7 +2076,7 @@ fn prefix_from_stdlib(stdlib: &Path) -> PathBuf { .is_some_and(|name| name == "lib") { // cargo-dist Homebrew data lives at - // `/share/pyrex/lib/pyre3.14`, while the executable still lives + // `/share/pyrex/lib/pyre3.14t`, while the executable still lives // at `/bin/pyre`. An explicit PYRE_STDLIB pointing at that data // must recover the keg, not claim `/share/pyrex` as sys.prefix. if let Some(pyre_share) = parent.and_then(Path::parent) @@ -5299,12 +5294,12 @@ mod tests { PathBuf::from("/src/pyre") ); assert_eq!( - prefix_from_stdlib(Path::new("/opt/pyre/lib/pyre3.14")), + prefix_from_stdlib(Path::new("/opt/pyre/lib/pyre3.14t")), PathBuf::from("/opt/pyre") ); assert_eq!( prefix_from_stdlib(Path::new( - "/opt/homebrew/Cellar/pyrex/0.0.2/share/pyrex/lib/pyre3.14" + "/opt/homebrew/Cellar/pyrex/0.0.2/share/pyrex/lib/pyre3.14t" )), PathBuf::from("/opt/homebrew/Cellar/pyrex/0.0.2") ); @@ -5322,7 +5317,7 @@ mod tests { #[test] fn stdlib_layout_prefers_the_versioned_zip_without_claiming_a_directory() { let tree = tempfile::tempdir().unwrap(); - let packaged = tree.path().join("lib/pyre3.14"); + let packaged = tree.path().join("lib/pyre3.14t"); std::fs::create_dir_all(&packaged).unwrap(); std::fs::write(packaged.join("site.py"), "").unwrap(); let zip = tree.path().join("python314.zip"); @@ -5343,7 +5338,7 @@ mod tests { let tree = tempfile::tempdir().unwrap(); let base = tree.path().join("base"); let base_executable = base.join("bin/pyre"); - let base_stdlib = base.join("lib/pyre3.14"); + let base_stdlib = base.join("lib/pyre3.14t"); std::fs::create_dir_all(base_executable.parent().unwrap()).unwrap(); std::fs::create_dir_all(&base_stdlib).unwrap(); std::fs::write(&base_executable, "").unwrap(); diff --git a/pyre/pyre-interpreter/src/module/sys/vm.rs b/pyre/pyre-interpreter/src/module/sys/vm.rs index 644bd85afe3..c485582ba23 100644 --- a/pyre/pyre-interpreter/src/module/sys/vm.rs +++ b/pyre/pyre-interpreter/src/module/sys/vm.rs @@ -71,24 +71,21 @@ fn get_sizeof(w_obj: PyObjectRef) -> crate::PyResult { )); } - // `_PyType_PreHeaderSize(Py_TYPE(o))` adds its two components - // independently: a two-word GC header for tracked objects, plus a two-word - // managed dict/weakref prefix where the instance type requests it. A - // tracked builtin such as list has only the first; a normal heap instance - // has both; an untracked heap-derived value may have only the second. + // `_PyType_PreHeaderSize(Py_TYPE(o))` — the two-word managed dict/weakref + // prefix an instance type requests. Its other term, a `PyGC_Head` ahead of + // every tracked object, is compiled out of a build without a global + // interpreter lock: the collector keeps its bits in the object header + // there. That is why the four-word header those builds carry costs a + // tracked type nothing over the two-word one, while an untracked leaf pays + // the whole two words. // - // Both terms are read off the type. Asking instead which heap the instance + // The term is read off the type. Asking instead which heap the instance // landed in would make the answer depend on the allocation that produced // it: a `str` folded into a code constant sits outside the collector's // ranges and one built at run time does not, so the same value reported two // different sizes. let word = std::mem::size_of::() as u64; - let gc_header = if crate::typedef::cpython_object_is_gc(current()) { - 2 * word - } else { - 0 - }; - let managed_prefix = crate::typedef::r#type(current()).map_or(0, |tp| unsafe { + let pre_header = crate::typedef::r#type(current()).map_or(0, |tp| unsafe { if pyre_object::w_type_is_heaptype(tp.as_ptr()) && (pyre_object::w_type_get_hasdict(tp.as_ptr()) || pyre_object::w_type_get_weakrefable(tp.as_ptr())) @@ -98,7 +95,6 @@ fn get_sizeof(w_obj: PyObjectRef) -> crate::PyResult { 0 } }); - let pre_header = gc_header + managed_prefix; let total = (size as u64) .checked_add(pre_header) .expect("Py_ssize_t plus the fixed pre-header fits in size_t"); @@ -1238,11 +1234,11 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { ); // sys.winver — the "major.minor" tag Windows uses for the per-user site // directory and the PythonCore registry keys. site.getusersitepackages - // reads it to build USER_SITE. It carries no `t`: that suffix is how - // Windows spells what `sys.abiflags` spells elsewhere, and pyre holds a - // global interpreter lock. + // reads it to build USER_SITE. A build without a global interpreter lock + // carries the `t` here, which is how Windows spells what `sys.abiflags` + // spells elsewhere. #[cfg(windows)] - module_ns_store(ns, "winver", w_str_new("3.14")); + module_ns_store(ns, "winver", w_str_new("3.14t")); // sys.dllhandle — the handle of the DLL exporting the Python C API, // published beside `winver` because both come from the same `MS_COREDLL` // block. `ctypes/__init__.py:562` builds `pythonapi` out of it with no @@ -2047,15 +2043,11 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { Err(unsafe { crate::PyError::from_exc_object(exc) }) }), ); - // sys.abiflags — empty, because pyre runs its mutators under a global - // interpreter lock (`majit-gc/src/rgil.rs`, the `thread_gil.c` port), so - // the `t` that spells a free-threaded ABI does not describe this build. - // `site.py:409` reads the flag to name the site-packages directory and - // `sysconfig` derives `abi_thread` from `Py_GIL_DISABLED` for the same - // name, so the two must agree. The attribute is absent on Windows, where - // the flag is spelled in `sys.winver`; every reader guards with `hasattr`, - // so an empty string answers the same. - module_ns_store(ns, "abiflags", w_str_new("")); + // sys.abiflags — `t` for a build without a global interpreter lock. The + // attribute is absent on Windows, where the flag is spelled in + // `sys.winver` and neither the `nt` scheme nor `site._get_path` reads it; + // every reader guards with `hasattr`, so an empty string answers the same. + module_ns_store(ns, "abiflags", w_str_new(if cfg!(windows) { "" } else { "t" })); // sys.argv — pick up pending argv from set_sys_argv if available. let pending = crate::importing::take_pending_sys_argv(); let argv = if pending.is_null() { diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index 32bb6904d38..c432b2d1796 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -4943,10 +4943,14 @@ fn list_descr_sizeof(args: &[PyObjectRef]) -> Result())); + // `list_sort_impl` temporarily writes -1 while the items are detached, and + // that sentinel is what the modification check reads back. It is not part + // of the size a build without a global interpreter lock reports: a list + // observed mid-sort answers its `tp_basicsize` and nothing more, so the + // detached state contributes no slots rather than wrapping in `size_t`. + let slots = allocated.max(0) as usize; + let size = + (basicsize as usize).wrapping_add(slots.wrapping_mul(std::mem::size_of::())); Ok(w_int_new(size as i64)) } @@ -5511,15 +5515,19 @@ fn init_str_type(ns: PyObjectRef) { 4 }; let word = std::mem::size_of::(); - // unicodeobject.c:unicode_sizeof_impl. Exact ASCII uses - // PyASCIIObject (5 words), exact non-ASCII uses - // PyCompactUnicodeObject (7 words), and Unicode - // subclasses use the two-block PyUnicodeObject (8 words). + // unicodeobject.c:unicode_sizeof_impl, over the four-word + // object header a build without a global interpreter lock + // carries (thread id, flags, mutex, gc bits, local and + // shared refcounts, type) rather than the two-word one. + // Exact ASCII then uses PyASCIIObject (7 words), exact + // non-ASCII uses PyCompactUnicodeObject (9 words), and + // Unicode subclasses the two-block PyUnicodeObject + // (10 words). let base = if pyre_object::pyobject::is_exact_type(args[0], &pyre_object::STR_TYPE) { - if maxchar < 0x80 { 5 * word } else { 7 * word } + if maxchar < 0x80 { 7 * word } else { 9 * word } } else { - 8 * word + 10 * word }; let size = base.checked_add((len + 1).checked_mul(kind).ok_or_else(|| { @@ -10702,10 +10710,16 @@ fn make_getset_property_full( /// `tp_is_gc` where the type installs one. /// /// This is a property of the type, not of the object's current collector -/// state: `()` is untracked yet `sys.getsizeof` still charges it a -/// `PyGC_Head`, because `tuple` carries the flag. Reading pyre's own GC -/// ownership instead answers a different question — pyre's nursery owns +/// state: `()` is untracked yet `tuple` carries the flag. Reading pyre's own +/// GC ownership instead answers a different question — pyre's nursery owns /// `object()` and `b""`, which `object` and `bytes` never declare. +/// +/// Nothing consumes it at present. `sys.getsizeof` used to, for the +/// `PyGC_Head` that `_PyType_PreHeaderSize` charges a tracked instance, but a +/// build without a global interpreter lock keeps the collector's bits in the +/// object header and so charges none. The flag itself remains the answer +/// `type.__flags__` owes for `Py_TPFLAGS_HAVE_GC`, which it does not yet +/// report. pub(crate) fn cpython_object_is_gc(w_obj: PyObjectRef) -> bool { let Some(tp) = r#type(w_obj) else { return false; @@ -10772,6 +10786,13 @@ fn cpython_type_has_gc_flag(w_type: PyObjectRef) -> bool { /// Logical CPython 3.14 `tp_basicsize` / `tp_itemsize` values ported so far. /// These belong to the type object, not to its Python namespace: CPython's /// `type_members` exposes both through read-only data descriptors. +/// +/// The sizes are the ones a build without a global interpreter lock reports, +/// which is what `sys.abiflags` and the `Py_GIL_DISABLED` config var advertise +/// here. Its object header is four words rather than two, so most of these run +/// two words above their counterpart in a build that keeps the lock; `type` +/// and `PyWeakReference` grow by three, carrying a field the lock makes +/// unnecessary. pub(crate) fn cpython_type_layout(w_type: PyObjectRef) -> Option<(i64, i64)> { if w_type.is_null() || !unsafe { pyre_object::is_type(w_type) } { return None; @@ -10780,59 +10801,62 @@ pub(crate) fn cpython_type_layout(w_type: PyObjectRef) -> Option<(i64, i64)> { let layout = unsafe { pyre_object::w_type_get_layout(w_type) }; let is = |candidate: *const PyType| std::ptr::eq(layout, candidate); let (base, item) = if is(&pyre_object::INSTANCE_TYPE) { - (2 * word, 0) + (4 * word, 0) } else if is(&pyre_object::TYPE_TYPE) { - (117 * word, 5 * word) + // `PyHeapTypeObject`, which keeps a unique-id word here that a build + // holding the lock has no use for. + (120 * word, 5 * word) } else if is(&pyre_object::INT_TYPE) || is(&pyre_object::LONG_TYPE) || is(&pyre_object::BOOL_TYPE) { - (3 * word, 4) + (5 * word, 4) } else if is(&pyre_object::FLOAT_TYPE) { - (3 * word, 0) + (5 * word, 0) } else if is(&pyre_object::COMPLEX_TYPE) { - (4 * word, 0) + (6 * word, 0) } else if is(&pyre_object::STR_TYPE) { - (8 * word, 0) + (10 * word, 0) } else if is(&pyre_object::bytesobject::BYTES_TYPE) { - (4 * word + 1, 1) + (6 * word + 1, 1) } else if is(&pyre_object::bytearrayobject::BYTEARRAY_TYPE) { - (7 * word, 0) + (9 * word, 0) } else if is(&pyre_object::LIST_TYPE) { - (5 * word, 0) + (7 * word, 0) } else if is(&pyre_object::TUPLE_TYPE) { - (4 * word, word) + (6 * word, word) } else if is(&pyre_object::DICT_TYPE) { - (6 * word, 0) + (8 * word, 0) } else if is(&pyre_object::setobject::SET_TYPE) || is(&pyre_object::setobject::FROZENSET_TYPE) { - (25 * word, 0) + (27 * word, 0) } else if is(&pyre_object::functional::RANGE_TYPE) { - (6 * word, 0) + (8 * word, 0) } else if is(&pyre_object::sliceobject::SLICE_TYPE) { - (5 * word, 0) + (7 * word, 0) } else if is(&pyre_object::memoryview::MEMORYVIEW_TYPE) { - (18 * word, word) + (20 * word, word) } else if is(&pyre_object::functional::MAP_TYPE) { - (5 * word, 0) + (7 * word, 0) } else if is(&pyre_object::functional::FILTER_TYPE) || is(&pyre_object::functional::REVERSED_TYPE) { - (4 * word, 0) - } else if is(&pyre_object::functional::ZIP_TYPE) { (6 * word, 0) + } else if is(&pyre_object::functional::ZIP_TYPE) { + (8 * word, 0) } else if is(&pyre_object::functional::ENUMERATE_TYPE) { - (7 * word, 0) + (9 * word, 0) } else if is(&pyre_object::weakref::WEAKREF_LAYOUT_TYPE) { // CPython 3.14 `PyWeakReference`: object header, doubly-linked - // weakref list, callback, hash/cache word and vectorcall slot. + // weakref list, callback, hash/cache word and vectorcall slot, plus + // the word that serialises the referent without the lock. // PyPy likewise gives W_WeakrefBase/W_Weakref their own typedef; // subclasses append their declared slots to this prefix. - (8 * word, 0) + (11 * word, 0) } else { // CPython's ordinary fixed-size heap instance begins with // PyObject_HEAD; user slots are appended below just like the // specialized builtin prefixes above. - (2 * word, 0) + (4 * word, 0) }; // PyPy typeobject.py:103-129 keeps the total slot count on Layout, whose // typedef identifies the fixed builtin prefix. CPython appends one pointer @@ -10867,23 +10891,26 @@ fn cpython_type_offsets(w_type: PyObjectRef) -> Option<(i64, i64)> { let layout = unsafe { pyre_object::w_type_get_layout(w_type) }; let is = |candidate: *const PyType| std::ptr::eq(layout, candidate); let (mut dict, mut weakref) = if is(&pyre_object::TYPE_TYPE) { - (33 * word, 46 * word) + (35 * word, 48 * word) } else if is(&pyre_object::setobject::SET_TYPE) || is(&pyre_object::setobject::FROZENSET_TYPE) { - (0, 24 * word) + (0, 26 * word) } else if is(&pyre_object::memoryview::MEMORYVIEW_TYPE) { - (0, 17 * word) + (0, 19 * word) } else { (0, 0) }; // Python 3.14 managed dict/weakref storage lives in the negative // pre-header. Preserve a builtin's positive inline offset when it already // owns the slot; otherwise heap types use the managed sentinel/offset. + // Without the lock that pre-header is the managed dict pair alone — the + // collector keeps its bits in the object header instead of a `PyGC_Head` + // ahead of it — so the weakref word sits two words back, not four. if unsafe { pyre_object::w_type_is_heaptype(w_type) } { if dict == 0 && unsafe { pyre_object::w_type_get_hasdict(w_type) } { dict = -1; } if weakref == 0 && unsafe { pyre_object::w_type_get_weakrefable(w_type) } { - weakref = -4 * word; + weakref = -2 * word; } } Some((dict, weakref)) @@ -10938,15 +10965,17 @@ fn init_type_type(ns: PyObjectRef) { let size = if pyre_object::w_type_is_heaptype(args[0]) { // CPython 3.14 typeobject.c:type___sizeof___impl: // PyHeapTypeObject plus the cached-keys table carried - // by a managed instance dictionary. - 117 * word + // by a managed instance dictionary. The struct sizes + // are the ones a build without a global interpreter + // lock reports; the keys table is the same either way. + 120 * word + if pyre_object::w_type_get_hasdict(args[0]) { 96 * word } else { 0 } } else { - 52 * word + 54 * word }; Ok(w_int_new(size)) }, @@ -19918,7 +19947,7 @@ fn init_object_type(ns: PyObjectRef) { .expect("every Python object has a type") .as_ptr(); let (basicsize, itemsize) = cpython_type_layout(w_type) - .unwrap_or((2 * std::mem::size_of::() as i64, 0)); + .unwrap_or((4 * std::mem::size_of::() as i64, 0)); let nitems = if itemsize == 0 { 0 } else if unsafe { @@ -30286,7 +30315,7 @@ mod tests { crate::module::_weakref::interp__weakref::proxy_type(), crate::module::_weakref::interp__weakref::callable_proxy_type(), ] { - assert_eq!(super::cpython_type_layout(w_type), Some((8 * word, 0))); + assert_eq!(super::cpython_type_layout(w_type), Some((11 * word, 0))); } } diff --git a/pyre/pyre-object/src/typeobject.rs b/pyre/pyre-object/src/typeobject.rs index 0bde55b2321..d0c67bfd342 100644 --- a/pyre/pyre-object/src/typeobject.rs +++ b/pyre/pyre-object/src/typeobject.rs @@ -240,9 +240,12 @@ pub struct W_TypeObject { /// `ModuleDictStrategy.version?`, the tree's other `?` declaration, the way /// upstream's one `QuasiImmut` class serves every quasi-immutable field. pub quasi_immut_watchers: crate::quasiimmut::QuasiImmutField, - /// `Py_TPFLAGS_HAVE_GC` (`1 << 14`) — whether instances of this type carry - /// the collector's two-word pre-header, which is what - /// `_PyType_PreHeaderSize` charges and `sys.getsizeof` therefore adds. + /// `Py_TPFLAGS_HAVE_GC` (`1 << 14`) — whether instances of this type join + /// the collector's traversal. A build that keeps the global interpreter + /// lock gives them a two-word `PyGC_Head` for it, which is what + /// `_PyType_PreHeaderSize` charges; one without the lock keeps the same + /// bits in the object header and charges nothing, so the flag records the + /// type property alone. /// /// A property of the type, not of the heap an instance landed in: the same /// `str` value must report one size whether it was folded into a code diff --git a/pyre/pyrex/tests/cpyext_smoke.rs b/pyre/pyrex/tests/cpyext_smoke.rs index 23b18342c9a..a443ba6bbb7 100644 --- a/pyre/pyrex/tests/cpyext_smoke.rs +++ b/pyre/pyrex/tests/cpyext_smoke.rs @@ -63,7 +63,7 @@ fn imports_single_phase_module_created_through_c_api() { let init_marker = out_dir.join("initialized"); let unload_marker = out_dir.join("unloaded"); let source = root.join("pyre/pyrex/tests/fixtures/cpyext_smoke.c"); - let include = root.join("pyre/pyre-interpreter/include/pyre3.14"); + let include = root.join("pyre/pyre-interpreter/include/pyre3.14t"); let mut cc = Command::new(std::env::var_os("CC").unwrap_or_else(|| "cc".into())); cc.arg(&source) .arg("-I") diff --git a/scripts/stage-stdlib.py b/scripts/stage-stdlib.py index 2b6e3d47e5f..77aba3b2208 100644 --- a/scripts/stage-stdlib.py +++ b/scripts/stage-stdlib.py @@ -30,12 +30,10 @@ ROOT = Path(__file__).resolve().parent.parent -# package.py:221 `IMPLEMENTATION = 'pypy{}'.format(python_ver)`. No trailing -# `t`: that is the free-threaded ABI flag, `sysconfig` derives `abi_thread` -# from `Py_GIL_DISABLED` and the posix schemes put it in the directory name, -# and pyre runs its mutators under a global interpreter lock -# (`majit-gc/src/rgil.rs`). -IMPLEMENTATION = "pyre3.14" +# package.py:221 `IMPLEMENTATION = 'pypy{}'.format(python_ver)`. The trailing +# `t` is the free-threaded ABI flag: `sysconfig` derives `abi_thread` from +# `Py_GIL_DISABLED` and the posix schemes put it in the directory name. +IMPLEMENTATION = "pyre3.14t" def ignored(_directory: str, names: list[str]) -> set[str]: