diff --git a/majit/majit-translate/src/annotator/builtin.rs b/majit/majit-translate/src/annotator/builtin.rs index c055fd1a64e..9ec16b985c9 100644 --- a/majit/majit-translate/src/annotator/builtin.rs +++ b/majit/majit-translate/src/annotator/builtin.rs @@ -300,6 +300,11 @@ fn register_builtins() -> HashMap { "pyre_object.lltype.malloc_typed", malloc_typed_alloc, ); + analyzer_for( + &mut reg, + "pyre_object.lltype.malloc_typed_managed", + malloc_typed_alloc, + ); // `pyre_object::lltype::malloc_raw` — the raw (non-GC) allocation // intrinsic (`lltype.malloc(T, flavor='raw')` parity). Recognising it // as a builtin keeps its `Box::new` / `Box::into_raw` body out of the diff --git a/majit/majit-translate/src/flowspace/model.rs b/majit/majit-translate/src/flowspace/model.rs index 73d93f79ab4..84dd42bda7f 100644 --- a/majit/majit-translate/src/flowspace/model.rs +++ b/majit/majit-translate/src/flowspace/model.rs @@ -2209,6 +2209,10 @@ impl HostEnv { "malloc_typed", HostObject::new_builtin_callable("pyre_object.lltype.malloc_typed"), ); + pyre_object_lltype.module_set( + "malloc_typed_managed", + HostObject::new_builtin_callable("pyre_object.lltype.malloc_typed_managed"), + ); // `pyre_object::lltype::malloc_raw` — the raw (non-GC) allocation // intrinsic (`lltype.malloc(T, flavor='raw')` parity). Exposed as a // host builtin so its `Box::new` body is never looked-inside; the diff --git a/majit/majit-translate/src/model.rs b/majit/majit-translate/src/model.rs index 9027db44e68..c323033a3f1 100644 --- a/majit/majit-translate/src/model.rs +++ b/majit/majit-translate/src/model.rs @@ -2677,7 +2677,10 @@ pub fn fuse_boxing_alloc( let is_malloc_typed = |target: &CallTarget| -> bool { matches!(target, CallTarget::FunctionPath { segments } if segments.len() >= 2 - && segments[segments.len() - 1] == "malloc_typed" + && matches!( + segments[segments.len() - 1].as_str(), + "malloc_typed" | "malloc_typed_managed" + ) && segments[segments.len() - 2] == "lltype") }; diff --git a/majit/majit-translate/src/translator/rtyper/cutover.rs b/majit/majit-translate/src/translator/rtyper/cutover.rs index 08a2dd7e687..423db7920c3 100644 --- a/majit/majit-translate/src/translator/rtyper/cutover.rs +++ b/majit/majit-translate/src/translator/rtyper/cutover.rs @@ -1565,7 +1565,9 @@ pub(crate) fn populate_call_registry_from_call_graphs( // `is_known_unported`) so the graph census-Skips to the legacy walker // instead of silently matching a wrong residual call. Tracked by the // boxing-lowering epic (#134/#142). - if canonical_strip == ["lltype", "malloc_typed"] { + if canonical_strip == ["lltype", "malloc_typed"] + || canonical_strip == ["lltype", "malloc_typed_managed"] + { continue; } // `pyre_object::lltype::malloc_raw` is the raw (non-GC) allocation diff --git a/majit/majit-translate/src/translator/rtyper/flowspace_adapter.rs b/majit/majit-translate/src/translator/rtyper/flowspace_adapter.rs index 41e4e9847f8..1fb32e18909 100644 --- a/majit/majit-translate/src/translator/rtyper/flowspace_adapter.rs +++ b/majit/majit-translate/src/translator/rtyper/flowspace_adapter.rs @@ -1695,10 +1695,13 @@ pub fn translate_op( // `malloc_typed` registration skip). if segments.len() >= 2 && segments[segments.len() - 2] == "lltype" - && segments[segments.len() - 1] == "malloc_typed" + && matches!( + segments[segments.len() - 1].as_str(), + "malloc_typed" | "malloc_typed_managed" + ) { return Err(TyperError::message( - "`lltype::malloc_typed` survived fuse_boxing_alloc unfused; \ + "`lltype::malloc_typed[_managed]` survived fuse_boxing_alloc unfused; \ only the numeric boxing structs fuse_boxing_alloc rewrites \ (W_Float/W_Int/W_Complex/W_Long) have a NewWithVtable \ lowering; no general malloc->new lowering ported" diff --git a/pyre/pyre-interpreter/src/_structseq.rs b/pyre/pyre-interpreter/src/_structseq.rs index b6801ac97e8..6e47279093f 100644 --- a/pyre/pyre-interpreter/src/_structseq.rs +++ b/pyre/pyre-interpreter/src/_structseq.rs @@ -26,7 +26,7 @@ //! f1=v1, ...)"` rendering. use indexmap::IndexMap; -use std::cell::RefCell; +use std::sync::{Mutex, OnceLock}; use pyre_object::PyObjectRef; @@ -39,7 +39,7 @@ use crate::PyError; struct StructSeqDescr { name: String, /// Field names in positional order. Names starting with `_` are - /// CPython's "unnamed" placeholders (`_structseq.py:67-69`). + /// unnamed placeholders (`_structseq.py:67-69`). fields: Vec, /// Named-only fields stored in the instance `__dict__` rather than the /// tuple body (`_structseq.py:31-37` — the `obj.__dict__[name]` arm). @@ -52,12 +52,13 @@ struct StructSeqDescr { extra_fields: Vec, } -thread_local! { - /// `class_ptr → StructSeqDescr`. Pyre keys by the subclass type - /// pointer because the GetSetProperty descriptor only carries a - /// `name` slot (`typedef.rs:174`), not the owning class. - static STRUCTSEQ_REGISTRY: RefCell> = - RefCell::new(IndexMap::new()); +/// `class_ptr → StructSeqDescr`. Pyre keys by the subclass type +/// pointer because the GetSetProperty descriptor only carries a +/// `name` slot (`typedef.rs:174`), not the owning class. +static STRUCTSEQ_REGISTRY: OnceLock>> = OnceLock::new(); + +fn structseq_registry() -> &'static Mutex> { + STRUCTSEQ_REGISTRY.get_or_init(|| Mutex::new(IndexMap::new())) } /// `lib_pypy/_structseq.py:31-37 structseqfield.__get__` — @@ -94,10 +95,12 @@ fn structseq_field_get(args: &[PyObjectRef]) -> Result { } // `_structseq.py:31-37` — an extra (dict-backed) field shadows a // same-named positional slot, so resolve those first. - let resolved = STRUCTSEQ_REGISTRY.with(|r| { - let map = r.borrow(); + let resolved = { + let map = structseq_registry().lock().unwrap(); let Some(entry) = map.get(&(cls as usize)) else { - return Resolved::Missing; + return Err(PyError::attribute_error(format!( + "structseq object has no field {name}" + ))); }; if entry.extra_fields.iter().any(|n| n == &name) { Resolved::Extra @@ -106,7 +109,7 @@ fn structseq_field_get(args: &[PyObjectRef]) -> Result { } else { Resolved::Missing } - }); + }; match resolved { Resolved::Extra => { let w_dict = crate::baseobjspace::getdict(inst); @@ -137,12 +140,12 @@ fn structseq_repr(args: &[PyObjectRef]) -> Result { return Err(PyError::type_error("structseq __repr__ missing self")); } let cls = unsafe { (*inst).w_class }; - let (name, fields) = STRUCTSEQ_REGISTRY.with(|r| -> (String, Vec) { - let map = r.borrow(); + let (name, fields) = { + let map = structseq_registry().lock().unwrap(); map.get(&(cls as usize)) .map(|d| (d.name.clone(), d.fields.clone())) .unwrap_or_default() - }); + }; let n = unsafe { pyre_object::w_tuple_len(inst) }; let mut parts: Vec = Vec::with_capacity(n); for i in 0..n { @@ -227,12 +230,12 @@ fn structseq_descr_new(args: &[PyObjectRef]) -> Result { let cls = args[0]; let n_seq = read_class_int(cls, "n_sequence_fields").unwrap_or(0) as usize; let n_fields = read_class_int(cls, "n_fields").unwrap_or(n_seq as i64) as usize; - let (name, extra_names) = STRUCTSEQ_REGISTRY.with(|r| { - let map = r.borrow(); + let (name, extra_names) = { + let map = structseq_registry().lock().unwrap(); map.get(&(cls as usize)) .map(|d| (d.name.clone(), d.extra_fields.clone())) .unwrap_or_else(|| ("structseq".to_string(), Vec::new())) - }); + }; // `_structseq.py:95-101` — the optional second arg is a dict supplying // values for the named-only extra fields. @@ -523,8 +526,8 @@ fn make_struct_seq_impl( unsafe { pyre_object::typeobject::w_type_set_hasdict(cls, true) }; } - STRUCTSEQ_REGISTRY.with(|r| { - r.borrow_mut().insert( + { + structseq_registry().lock().unwrap().insert( cls as usize, StructSeqDescr { name: name.to_string(), @@ -532,7 +535,7 @@ fn make_struct_seq_impl( extra_fields: owned_extra, }, ); - }); + } cls } diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index bc54e2508e3..753b2ec966c 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -4661,6 +4661,24 @@ fn getattr_str_impl(obj: PyObjectRef, name: &str, call_getattr: bool) -> PyResul unsafe { if is_module(obj) { let w_type = crate::typedef::r#type(obj).unwrap_or(PY_NULL); + // module.py Module.descr_getattribute is the default module slot + // inlined below. A retagged module may replace that slot (for + // example importlib.util._LazyModule), in which case space.getattr + // must dispatch the replacement before touching the module dict. + // The default result is memoized on the live, possibly retagged + // type so ordinary modules retain the hot path. + if call_getattr { + if let Some(slot) = module_getattribute_if_not_from_default(w_type) { + let name_obj = w_str_new(name); + match get_and_call_function(slot, obj, w_type, &[name_obj]) { + Ok(v) => return Ok(v), + Err(e) if e.kind == PyErrorKind::AttributeError => { + return module_getattr_hook_or_err(obj, name, e, call_getattr); + } + Err(e) => return Err(e), + } + } + } let w_descr = if w_type.is_null() { None } else { @@ -4893,29 +4911,8 @@ fn getattr_str_impl(obj: PyObjectRef, name: &str, call_getattr: bool) -> PyResul // normal lookup misses with AttributeError, a module-level `__getattr__` // stored in the module's own dict gets the final say, called with just the // attribute name. Only `space.getattr` consults it. - if call_getattr && err.kind == PyErrorKind::AttributeError && unsafe { is_module(obj) } { - let w_dict = unsafe { pyre_object::w_module_get_w_dict(obj) }; - if !w_dict.is_null() { - if let Some(mod_getattr) = finditem_str(w_dict, "__getattr__")? { - if !mod_getattr.is_null() { - let name_obj = w_str_new(name); - return crate::call::call_function_impl_result(mod_getattr, &[name_obj]); - } - } - // No module `__getattr__`: phrase the miss with the module's - // `__name__` (`module '' has no attribute ''`, the - // `'%U'` form), which requires a str `__name__` and falls back - // to the bare form otherwise. (The `__spec__`-based - // circular-import diagnostics are not ported.) - let msg = match finditem_str(w_dict, "__name__")? { - Some(w) if !w.is_null() && unsafe { pyre_object::is_str(w) } => { - let nm = unsafe { pyre_object::w_str_get_wtf8(w) }; - format!("module '{nm}' has no attribute '{name}'") - } - _ => format!("module has no attribute '{name}'"), - }; - return Err(PyError::new(PyErrorKind::AttributeError, msg)); - } + if err.kind == PyErrorKind::AttributeError && unsafe { is_module(obj) } { + return unsafe { module_getattr_hook_or_err(obj, name, err, call_getattr) }; } Err(err) } @@ -5143,11 +5140,14 @@ unsafe fn setattr_surrogate( ) -> PyResult { let obj = crate::module::_weakref::interp__weakref::force(obj)?; unsafe { - if is_instance(obj) { - let w_type = w_instance_get_type(obj); - if let Some(sa) = lookup_in_type(w_type, "__setattr__") { - return crate::call::call_function_impl_result(sa, &[obj, w_name, value]) - .map(|_| w_none()); + let w_type = if is_instance(obj) { + w_instance_get_type(obj) + } else { + crate::typedef::r#type(obj).unwrap_or(PY_NULL) + }; + if !w_type.is_null() { + if let Some(sa) = setattr_if_not_from_object(w_type) { + return get_and_call_function(sa, obj, w_type, &[w_name, value]).map(|_| w_none()); } } } @@ -5234,19 +5234,18 @@ pub(crate) unsafe fn object_setattr_surrogate( unsafe fn delattr_surrogate(obj: PyObjectRef, w_name: PyObjectRef, name: &Wtf8) -> PyResult { let obj = crate::module::_weakref::interp__weakref::force(obj)?; unsafe { - if is_instance(obj) { - let w_type = w_instance_get_type(obj); - if let Some(da) = lookup_in_type(w_type, "__delattr__") { - return crate::call::call_function_impl_result(da, &[obj, w_name]) - .map(|_| w_none()); - } - } else if let Some(w_type) = crate::typedef::r#type(obj) { - // descroperation.py:254 dispatches through space.lookup for every - // receiver kind. A class is an instance of its metaclass, so its - // __delattr__ override precedes direct type-dict removal too. + let w_type = if is_instance(obj) { + w_instance_get_type(obj) + } else { + crate::typedef::r#type(obj).unwrap_or(PY_NULL) + }; + if !w_type.is_null() { if let Some(da) = lookup_in_type(w_type, "__delattr__") { - return crate::call::call_function_impl_result(da, &[obj, w_name]) - .map(|_| w_none()); + let is_default = lookup_in_type(crate::typedef::w_object(), "__delattr__") + .is_some_and(|d| std::ptr::eq(da, d)); + if !is_default { + return get_and_call_function(da, obj, w_type, &[w_name]).map(|_| w_none()); + } } } } @@ -5392,6 +5391,51 @@ pub fn object_getattribute(obj: PyObjectRef, name: &str) -> PyResult { getattr_str_impl(obj, name, false) } +/// module.py `Module.descr_getattribute` — run the object-default descriptor +/// protocol, then the module-dict `__getattr__` hook on AttributeError. +pub(crate) fn module_getattribute(obj: PyObjectRef, name: &str) -> PyResult { + match object_getattribute(obj, name) { + Ok(value) => Ok(value), + Err(err) if err.kind == PyErrorKind::AttributeError => unsafe { + module_getattr_hook_or_err(obj, name, err, true) + }, + Err(err) => Err(err), + } +} + +/// module.py `Module.descr_getattribute` tail. A module-level `__getattr__` +/// is a namespace value called with the name alone, not a type descriptor. +unsafe fn module_getattr_hook_or_err( + obj: PyObjectRef, + name: &str, + err: PyError, + call_getattr: bool, +) -> PyResult { + if !call_getattr { + return Err(err); + } + let w_dict = pyre_object::w_module_get_w_dict(obj); + if w_dict.is_null() { + return Err(err); + } + if let Some(mod_getattr) = finditem_str(w_dict, "__getattr__")? { + if !mod_getattr.is_null() { + return crate::call::call_function_impl_result(mod_getattr, &[w_str_new(name)]); + } + } + // No module `__getattr__`: phrase the miss with the module's `__name__` + // when it is a string, falling back to the bare form otherwise. The + // `__spec__`-based circular-import diagnostics are not ported. + let msg = match finditem_str(w_dict, "__name__")? { + Some(w) if !w.is_null() && pyre_object::is_str(w) => { + let nm = pyre_object::w_str_get_wtf8(w); + format!("module '{nm}' has no attribute '{name}'") + } + _ => format!("module has no attribute '{name}'"), + }; + Err(PyError::new(PyErrorKind::AttributeError, msg)) +} + /// `descroperation.py:242-245` `_handle_getattribute` tail: on an /// AttributeError from the descriptor protocol (a custom `__getattribute__`, /// a descriptor `__get__`, or the terminal miss), look up `__getattr__` on the @@ -7730,6 +7774,41 @@ unsafe fn is_object_getattribute_descr(w_descr: PyObjectRef) -> bool { } } +/// module.py `Module.descr_getattribute` is the default attribute slot for +/// module objects. Module subclasses inherit it unless they explicitly +/// replace `__getattribute__`. +unsafe fn is_module_getattribute_descr(w_descr: PyObjectRef) -> bool { + let w_module_type = + crate::typedef::gettypefor(&pyre_object::MODULE_TYPE as *const pyre_object::PyType) + .unwrap_or(PY_NULL); + !w_module_type.is_null() + && lookup_in_type_where(w_module_type, "__getattribute__") + .is_some_and(|d| std::ptr::eq(w_descr, d)) +} + +/// Module-specialized companion of `getattribute_if_not_from_object`. +/// +/// The module default is `Module.descr_getattribute`, not +/// `object.__getattribute__`, but it uses the same per-type memoized flag and +/// mutation invalidation as the object-default path. +unsafe fn module_getattribute_if_not_from_default(w_type: PyObjectRef) -> Option { + if majit_metainterp::jit::we_are_jitted() { + return lookup_in_type_where(w_type, "__getattribute__") + .filter(|&w_descr| !is_module_getattribute_descr(w_descr)); + } + if pyre_object::typeobject::w_type_get_uses_object_getattribute(w_type) { + return None; + } + if let Some(w_descr) = lookup_in_type_where(w_type, "__getattribute__") { + if is_module_getattribute_descr(w_descr) { + pyre_object::typeobject::w_type_set_uses_object_getattribute(w_type, true); + return None; + } + return Some(w_descr); + } + None +} + /// descroperation.py:17-20 `object_setattr(space)` — the canonical /// `object.__setattr__` descriptor anchor (see /// [`is_object_getattribute_descr`]). @@ -8577,10 +8656,16 @@ pub(crate) fn descr_set___class__(w_obj: PyObjectRef, w_newcls: PyObjectRef) -> pyre_object::type_name_of(w_newcls), ))); } - // objectobject.py:143-145 — w_newcls must be a heap type. - if !w_type_is_heaptype(w_newcls) { + // objectobject.py:166-171 — assignment targets are heap types or the + // exact module type. The latter lets a ModuleType subclass restore + // its receiver to ModuleType after temporarily overriding the slots. + let w_module_type = + crate::typedef::gettypefor(&pyre_object::MODULE_TYPE as *const pyre_object::PyType) + .unwrap_or(PY_NULL); + if !w_type_is_heaptype(w_newcls) && !std::ptr::eq(w_newcls, w_module_type) { return Err(crate::PyError::type_error( - "__class__ assignment: only for heap types".to_string(), + "__class__ assignment only supported for heap types or ModuleType subclasses" + .to_string(), )); } // objectobject.py:146-147 — get the old class @@ -8647,14 +8732,9 @@ pub fn setattr_str(obj: PyObjectRef, name: &str, value: PyObjectRef) -> PyResult // (e.g. structseq tuple subclasses) may install a non-default // __setattr__; only a real override (≠ object.__setattr__) // needs invoking — the default terminal path is object_setattr. - if let Some(sa) = lookup_in_type(w_type, "__setattr__") { - let is_default = lookup_in_type(crate::typedef::w_object(), "__setattr__") - .is_some_and(|d| std::ptr::eq(sa, d)); - if !is_default { - let w_name = w_str_new(name); - return crate::call::call_function_impl_result(sa, &[obj, w_name, value]) - .map(|_| w_none()); - } + if let Some(sa) = setattr_if_not_from_object(w_type) { + let w_name = w_str_new(name); + return get_and_call_function(sa, obj, w_type, &[w_name, value]).map(|_| w_none()); } } } @@ -9375,30 +9455,18 @@ pub fn delattr_str(obj: PyObjectRef, name: &str) -> PyResult { return get_and_call_function(da, obj, w_type, &[w_name]).map(|_| w_none()); } } - } else if is_type(obj) { - // descroperation.py:254 looks up __delattr__ on the receiver type - // regardless of receiver kind. For a type receiver that is the - // metaclass; a metaclass overriding __delattr__ customises - // `del C.x`. Only a real override (≠ object.__delattr__) needs - // invoking — the default terminal path is object_delattr. - if let Some(w_metatype) = crate::typedef::r#type(obj) { - if let Some(da) = lookup_in_type(w_metatype, "__delattr__") { - let is_default = lookup_in_type(crate::typedef::w_object(), "__delattr__") - .is_some_and(|d| std::ptr::eq(da, d)); - if !is_default { - let w_name = w_str_new(name); - return get_and_call_function(da, obj, w_metatype, &[w_name]) - .map(|_| w_none()); - } - } - } } else if let Some(w_type) = crate::typedef::r#type(obj) { - // A class object's attribute operations dispatch through its - // metaclass in PyPy (`space.lookup(w_obj, '__delattr__')`). + // descroperation.py:254 looks up __delattr__ on the receiver type + // regardless of receiver kind. This includes a module retagged + // to a subclass as well as a type receiver whose metaclass + // customises deletion. if let Some(da) = lookup_in_type(w_type, "__delattr__") { - let w_name = w_str_new(name); - return crate::call::call_function_impl_result(da, &[obj, w_name]) - .map(|_| w_none()); + let is_default = lookup_in_type(crate::typedef::w_object(), "__delattr__") + .is_some_and(|d| std::ptr::eq(da, d)); + if !is_default { + let w_name = w_str_new(name); + return get_and_call_function(da, obj, w_type, &[w_name]).map(|_| w_none()); + } } } } diff --git a/pyre/pyre-interpreter/src/builtins.rs b/pyre/pyre-interpreter/src/builtins.rs index bc5bbe40085..4ae2cf3d9e0 100644 --- a/pyre/pyre-interpreter/src/builtins.rs +++ b/pyre/pyre-interpreter/src/builtins.rs @@ -2711,7 +2711,7 @@ pub fn install_default_builtins(ns: PyObjectRef) { make_module_builtin_function_with_arity("issubclass", builtin_issubclass, 2) }); crate::module_ns_get_or_insert_with(ns, "__import__", || { - make_module_builtin_function("__import__", builtin_import_stub) + make_module_builtin_function("__import__", builtin_dunder_import) }); // Descriptor types @@ -5353,6 +5353,9 @@ fn make_exc_type_with_init( init_fn: Option, base: PyObjectRef, ) -> PyObjectRef { + if let Some(cls) = lookup_exc_class(name) { + return cls; + } let cls = crate::typedef::make_builtin_type_with_base( name, move |ns| { @@ -5554,8 +5557,7 @@ fn make_exc_type_with_init( ); // Record the class so typedef::r#type can map a raised exception // back to its specific builtin class (TypeError, ValueError, ...). - register_exc_class(name, cls); - cls + register_exc_class(name, cls) } /// Build a builtin exception class with more than one base, e.g. @@ -5569,6 +5571,9 @@ pub(crate) fn make_exc_type_multi( new_fn: crate::gateway::BuiltinCodeFn, bases: &[PyObjectRef], ) -> PyObjectRef { + if let Some(cls) = lookup_exc_class(name) { + return cls; + } let cls = crate::typedef::make_builtin_type_with_bases( name, move |ns| { @@ -5582,8 +5587,7 @@ pub(crate) fn make_exc_type_multi( }, bases, ); - register_exc_class(name, cls); - cls + register_exc_class(name, cls) } const EG_MESSAGE_KEY: &str = "__pyre_exception_group_message"; @@ -6071,6 +6075,9 @@ fn exception_group_repr(args: &[PyObjectRef]) -> Result PyObjectRef { + if let Some(cls) = lookup_exc_class(name) { + return cls; + } let cls = crate::typedef::make_builtin_type_with_bases( name, move |ns| { @@ -6130,13 +6137,14 @@ fn make_exception_group_type(name: &'static str, bases: &[PyObjectRef]) -> PyObj }, bases, ); - register_exc_class(name, cls); - cls + register_exc_class(name, cls) } -/// Thread-local registry from exception class name (as used by +/// Process-global registry from exception class name (as used by /// `ExcKind → exc_kind_name`) to the W_TypeObject exposed in the builtins -/// namespace. Populated at init-builtins time via `make_exc_type`. +/// namespace. Populated at init-builtins time via `make_exc_type`. Entries are +/// first-writer-wins so every execution context installs the same canonical +/// class hierarchy in its builtins dictionary. /// /// Also propagates into `pyre_object::interp_exceptions`'s kind-indexed /// registry so `w_exception_new(kind, ...)` populates @@ -6144,20 +6152,29 @@ fn make_exception_group_type(name: &'static str, bases: &[PyObjectRef]) -> PyObj /// builtin-raised exception then satisfies /// `space.type(w_exc) == registered class` per `baseobjspace.py /// exception_getclass`. -fn register_exc_class(name: &'static str, cls: PyObjectRef) { - EXC_CLASS_REGISTRY.with(|r| { - r.borrow_mut().insert(name, cls); - }); +fn register_exc_class(name: &'static str, cls: PyObjectRef) -> PyObjectRef { + let registry = + EXC_CLASS_REGISTRY.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new())); + let mut registry = registry + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let canonical = *registry.entry(name).or_insert(cls as usize) as PyObjectRef; if let Some(kind) = pyre_object::interp_exceptions::exc_kind_from_name(name) { - pyre_object::interp_exceptions::register_exc_class_for_kind(kind, cls); + let by_kind = pyre_object::interp_exceptions::register_exc_class_for_kind(kind, canonical); + debug_assert_eq!(by_kind, canonical); } + canonical } /// Look up a builtin exception class by its `ExcKind` name. Returns /// `None` if the registry hasn't been populated yet (e.g. before /// install_default_builtins). pub fn lookup_exc_class(name: &str) -> Option { - EXC_CLASS_REGISTRY.with(|r| r.borrow().get(name).copied()) + let registry = EXC_CLASS_REGISTRY.get()?; + let registry = registry + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + registry.get(name).copied().map(|cls| cls as PyObjectRef) } /// Look up the reusable prebuilt instance for a builtin exception @@ -6178,10 +6195,9 @@ pub fn lookup_exc_instance(name: &str) -> Option { Some(pyre_object::interp_exceptions::standard_exc_instance(kind)) } -thread_local! { - static EXC_CLASS_REGISTRY: std::cell::RefCell> - = std::cell::RefCell::new(std::collections::HashMap::new()); -} +static EXC_CLASS_REGISTRY: std::sync::OnceLock< + std::sync::Mutex>, +> = std::sync::OnceLock::new(); /// `__build_class__(body, name, *bases)` — class creation. /// @@ -12428,10 +12444,10 @@ fn builtin_format(args: &[PyObjectRef]) -> Result { } /// `__import__(name, globals=None, locals=None, fromlist=(), level=0)` -/// — PyPy: `pypy/module/imp/importing.py:importhook`. -fn builtin_import_stub(args: &[PyObjectRef]) -> Result { +/// — PyPy: `_frozen_importlib/interp_import.py:interp___import__`. +fn builtin_dunder_import(args: &[PyObjectRef]) -> Result { // `__import__(name, globals, locals, fromlist, level)` — PyPy's gateway - // binds the five named slots before `importhook` runs. Use the shared + // binds the five named slots before the import runs. Use the shared // flat-ABI equivalent so duplicate positional/keyword values, unknown // keywords, and surplus positionals raise at the same boundary. let scope = bind_builtin_kwargs( @@ -12446,6 +12462,7 @@ fn builtin_import_stub(args: &[PyObjectRef]) -> Result Result PyObjectRef { - thread_local! { - static TYPE: OnceLock = const { OnceLock::new() }; - } - TYPE.with(|cell| { - *cell.get_or_init(|| { - crate::_structseq::make_struct_seq( - "sys.UnraisableHookArgs", - &[ - "exc_type", - "exc_value", - "exc_traceback", - "err_msg", - "object", - ], - ) - }) - }) + static TYPE: OnceLock = OnceLock::new(); + *TYPE.get_or_init(|| { + crate::_structseq::make_struct_seq( + "sys.UnraisableHookArgs", + &[ + "exc_type", + "exc_value", + "exc_traceback", + "err_msg", + "object", + ], + ) as usize + }) as PyObjectRef } /// Resolve an exception instance's actual Python class name for display. diff --git a/pyre/pyre-interpreter/src/importing.rs b/pyre/pyre-interpreter/src/importing.rs index 0e47ed133c1..7b24cadb5db 100644 --- a/pyre/pyre-interpreter/src/importing.rs +++ b/pyre/pyre-interpreter/src/importing.rs @@ -941,12 +941,13 @@ pub(crate) fn create_builtin_module( execution_context: *const PyExecutionContext, ) -> Result, crate::PyError> { // `import builtins` must resolve to `space.builtin`, the one Module every - // frame uses for its LOAD_GLOBAL fallback. A fresh `load_builtin_module` - // would rerun `install_default_builtins`, minting a second exception - // hierarchy and overwriting the name→class registry, so a caught - // `except KeyError` would then compare against the wrong `BaseException`. - // `load_part` routes the name this way; the `_imp.create_builtin` entry - // point must too. + // frame uses for its LOAD_GLOBAL fallback. Historically a fresh + // `load_builtin_module` reran `install_default_builtins`, minted a second + // exception hierarchy, and overwrote the name→class registry. The + // process-global get-or-mint registry now prevents that identity + // clobbering even on another fresh-dictionary path, while this guard still + // preserves the builtins Module identity. `load_part` routes the name this + // way; the `_imp.create_builtin` entry point must too. if name == "builtins" && !execution_context.is_null() { let module = unsafe { (*execution_context).get_builtin() }; set_sys_module(name, module); @@ -2141,7 +2142,20 @@ fn load_source_module( // a young module while only the dict entry is updated. if modulename == "importlib._bootstrap" { if let Some(loaded) = check_sys_modules(modulename) { - install_importlib_bootstrap(loaded, execution_context)?; + if let Err(e) = install_importlib_bootstrap(loaded, execution_context) { + // Unwind the partial install: `dunder_import` routes through + // `_bootstrap.__import__` whenever `importlib._bootstrap` is + // in `sys.modules`, and a half-installed bootstrap (module + // registered, PathFinder missing — e.g. `_bootstrap_external` + // needs the `nt` builtin on Windows) would then answer every + // import with no file finder installed. Dropping the entries + // keeps the native importer authoritative, the minimal- + // importer role the boot sequence already documents. + remove_sys_module(modulename); + remove_sys_module("_frozen_importlib"); + remove_sys_module("_frozen_importlib_external"); + return Err(e); + } } } @@ -2192,12 +2206,69 @@ fn install_importlib_bootstrap( )?; crate::call::call_function_impl_result(install_external, &[])?; + if let Some(external) = check_sys_modules("_frozen_importlib_external") { + set_frozen_alias_metadata( + external, + "_frozen_importlib_external", + shadow_stack_get(module_slot), + )?; + } + // `_install_external_importers` imports `_frozen_importlib_external`, // which aliases that name onto the loaded submodule; the bootstrap module // itself is only reached under its submodule name, so it never picks up // the matching alias. Register it once both installs have succeeded, so a // body that raised leaves no alias behind. set_sys_module("_frozen_importlib", shadow_stack_get(module_slot)); + set_frozen_alias_metadata( + shadow_stack_get(module_slot), + "_frozen_importlib", + shadow_stack_get(module_slot), + )?; + Ok(()) +} + +#[cfg(feature = "host_env")] +fn set_frozen_alias_metadata( + module: PyObjectRef, + name: &str, + bootstrap: PyObjectRef, +) -> Result<(), crate::PyError> { + use pyre_object::gc_roots::{pin_root, push_roots, shadow_stack_get, shadow_stack_len}; + + let _roots = push_roots(); + let module_slot = shadow_stack_len(); + pin_root(module); + let bootstrap_slot = shadow_stack_len(); + pin_root(bootstrap); + + let loader = + crate::baseobjspace::getattr_str(shadow_stack_get(bootstrap_slot), "FrozenImporter")?; + let loader_slot = shadow_stack_len(); + pin_root(loader); + let find_spec = crate::baseobjspace::getattr_str(shadow_stack_get(loader_slot), "find_spec")?; + let find_spec_slot = shadow_stack_len(); + pin_root(find_spec); + let w_name = pyre_object::w_str_new(name); + let name_slot = shadow_stack_len(); + pin_root(w_name); + let spec = crate::call::call_function_impl_result( + shadow_stack_get(find_spec_slot), + &[shadow_stack_get(name_slot)], + )?; + let spec_slot = shadow_stack_len(); + pin_root(spec); + + crate::baseobjspace::setattr_str( + shadow_stack_get(module_slot), + "__loader__", + shadow_stack_get(loader_slot), + )?; + crate::baseobjspace::setattr_str( + shadow_stack_get(module_slot), + "__spec__", + shadow_stack_get(spec_slot), + )?; Ok(()) } @@ -2489,6 +2560,191 @@ pub fn import_name( ) } +// ── __import__ ─────────────────────────────────────────────────────── +// PyPy equivalent: _frozen_importlib/interp_import.py `interp___import__` + +/// `_gcd_import` fast path: the already-imported, fully-initialised module +/// for `name`, or `None` when the slow path must run — a missing +/// `sys.modules` entry, a missing `__spec__`, or a module whose +/// `__spec__._initializing` is still true. A `__spec__` without +/// `_initializing` counts as initialised (a builtin module). +fn gcd_import_fast(name: &str) -> Result, crate::PyError> { + use pyre_object::gc_roots::{pin_root, push_roots, shadow_stack_get, shadow_stack_len}; + + // A `None` sentinel blocks the name; `check_sys_modules` skips it and + // would fall back to the interpreter cache, resurrecting a builtin the + // sentinel is meant to block. Give up so the slow path raises + // `import of {name} halted; None in sys.modules`. + if sys_modules_blocks(name) { + return Ok(None); + } + let Some(w_module) = check_sys_modules(name) else { + return Ok(None); + }; + let _roots = push_roots(); + let mod_slot = shadow_stack_len(); + pin_root(w_module); + let Some(w_spec) = + crate::baseobjspace::findattr_result(shadow_stack_get(mod_slot), "__spec__")? + else { + return Ok(None); + }; + let spec_slot = shadow_stack_len(); + pin_root(w_spec); + if let Some(w_initializing) = + crate::baseobjspace::findattr_result(shadow_stack_get(spec_slot), "_initializing")? + { + if crate::baseobjspace::is_true(w_initializing)? { + return Ok(None); + } + } + Ok(Some(shadow_stack_get(mod_slot))) +} + +/// `builtins.__import__` — `interp___import__`: a fast path answering +/// absolute imports from initialised `sys.modules` entries, the app-level +/// `_bootstrap.__import__` (the full `sys.meta_path` / `sys.path_hooks` +/// protocol) otherwise. While the importlib bootstrap is not installed — +/// during startup, or when no stdlib is reachable — the native `importhook` +/// stands in, the role of PyPy's minimal `importing.py` importer. +pub fn dunder_import( + name: &str, + w_globals: PyObjectRef, + w_locals: PyObjectRef, + w_fromlist: PyObjectRef, + level: i64, + execution_context: *const PyExecutionContext, +) -> Result { + use pyre_object::gc_roots::{pin_root, push_roots, shadow_stack_get, shadow_stack_len}; + + // Captured before any Python can run below (`is_true` may call a + // `__bool__`); the raw argument pointers are stale after that. + let fromlist_missing = w_fromlist.is_null() || unsafe { is_none(w_fromlist) }; + + let _roots = push_roots(); + let globals_slot = shadow_stack_len(); + pin_root(if w_globals.is_null() { + pyre_object::w_none() + } else { + w_globals + }); + let locals_slot = shadow_stack_len(); + pin_root(if w_locals.is_null() { + pyre_object::w_none() + } else { + w_locals + }); + let fromlist_slot = shadow_stack_len(); + pin_root(if w_fromlist.is_null() { + pyre_object::w_none() + } else { + w_fromlist + }); + + if level == 0 { + // Fast path only for absolute imports (interp_import.py). + // A package with a fromlist needs `_handle_fromlist`, which the + // slow path runs. + let have_fromlist = + !fromlist_missing && crate::baseobjspace::is_true(shadow_stack_get(fromlist_slot))?; + if let Some(w_mod) = gcd_import_fast(name)? { + let mod_slot = shadow_stack_len(); + pin_root(w_mod); + if !have_fromlist { + match name.find('.') { + None => return Ok(shadow_stack_get(mod_slot)), + Some(dot) => { + // `import a.b` returns `a`; give up when the + // top-level ancestor is not initialised yet. + if let Some(w_top) = gcd_import_fast(&name[..dot])? { + return Ok(w_top); + } + } + } + } else if crate::baseobjspace::findattr_result(shadow_stack_get(mod_slot), "__path__")? + .is_none() + { + return Ok(shadow_stack_get(mod_slot)); + } + } + } + + // The frozen bootstrap aliases stay on the native importer: + // `_install_external_importers` imports `_frozen_importlib_external` + // while installing PathFinder, so no finder can serve it yet — + // `absolute_import` maps the alias onto the on-disk bootstrap sources. + if matches!(name, "_frozen_importlib" | "_frozen_importlib_external") { + return importhook( + name, + if w_globals.is_null() { + pyre_object::PY_NULL + } else { + shadow_stack_get(globals_slot) + }, + if w_fromlist.is_null() { + pyre_object::PY_NULL + } else { + shadow_stack_get(fromlist_slot) + }, + level, + execution_context, + ); + } + + // Slow path: the app-level `_bootstrap.__import__`. + if let Some(w_bootstrap) = get_sys_module("importlib._bootstrap") { + let bootstrap_slot = shadow_stack_len(); + pin_root(w_bootstrap); + if let Some(w_import) = + crate::baseobjspace::findattr_result(shadow_stack_get(bootstrap_slot), "__import__")? + { + let import_slot = shadow_stack_len(); + pin_root(w_import); + let w_name = pyre_object::w_str_new(name); + let name_slot = shadow_stack_len(); + pin_root(w_name); + let w_level = pyre_object::w_int_new(level); + let level_slot = shadow_stack_len(); + pin_root(w_level); + // An omitted fromlist reaches `__import__` as its `()` default, + // not `None` (interp_import.py `WrappedDefault(())`). + let call_fromlist_slot = if fromlist_missing { + let w_empty = pyre_object::w_tuple_new(vec![]); + let slot = shadow_stack_len(); + pin_root(w_empty); + slot + } else { + fromlist_slot + }; + return crate::call::call_function_impl_result( + shadow_stack_get(import_slot), + &[ + shadow_stack_get(name_slot), + shadow_stack_get(globals_slot), + shadow_stack_get(locals_slot), + shadow_stack_get(call_fromlist_slot), + shadow_stack_get(level_slot), + ], + ); + } + } + importhook( + name, + if w_globals.is_null() { + pyre_object::PY_NULL + } else { + shadow_stack_get(globals_slot) + }, + if w_fromlist.is_null() { + pyre_object::PY_NULL + } else { + shadow_stack_get(fromlist_slot) + }, + level, + execution_context, + ) +} + // ── importhook ─────────────────────────────────────────────────────── // PyPy equivalent: importing.py `importhook()` diff --git a/pyre/pyre-interpreter/src/jit_fnaddr.rs b/pyre/pyre-interpreter/src/jit_fnaddr.rs index d3c78a013a2..3477c6a4217 100644 --- a/pyre/pyre-interpreter/src/jit_fnaddr.rs +++ b/pyre/pyre-interpreter/src/jit_fnaddr.rs @@ -529,7 +529,7 @@ pub fn jit_trace_fnaddrs() -> Vec<(&'static str, i64)> { "pyre_object::w_type_issubtype", w_type_issubtype as *const (), ); - // `lookup_exc_class_for_kind` reads the TLS `EXC_CLASS_BY_KIND` + // `lookup_exc_class_for_kind` reads the process-global `EXC_CLASS_BY_KIND` // registry the tracer cannot model; its residual call rides a C-ABI // bridge that reconstructs the `ExcKind` from the integer arg slot. push_alias_pair( diff --git a/pyre/pyre-interpreter/src/lib.rs b/pyre/pyre-interpreter/src/lib.rs index a2c23af4f52..e06ac0e999b 100644 --- a/pyre/pyre-interpreter/src/lib.rs +++ b/pyre/pyre-interpreter/src/lib.rs @@ -366,8 +366,8 @@ macro_rules! pyre_count_typed_args { /// /// ```ignore /// pub fn type_object() -> ::pyre_object::PyObjectRef { -/// thread_local! { static CELL: ... = const { ... }; } -/// CELL.with(|c| *c.get_or_init(|| { +/// static CELL: ::std::sync::OnceLock = ::std::sync::OnceLock::new(); +/// *CELL.get_or_init(|| { /// let tp = crate::typedef::make_builtin_type("_random.Random", |ns| { /// #[crate::pyre_function] /// fn __init__(self_obj: PyObjectRef, seed: i64) -> Result<(), crate::PyError> { ... } @@ -376,8 +376,8 @@ macro_rules! pyre_count_typed_args { /// // ... more methods /// }); /// unsafe { ::pyre_object::typeobject::w_type_set_hasdict(tp, true) }; -/// tp -/// })) +/// tp as usize +/// }) as ::pyre_object::PyObjectRef /// } /// ``` #[macro_export] @@ -397,54 +397,49 @@ macro_rules! py_class { $(,)? ) => { pub fn type_object() -> ::pyre_object::PyObjectRef { - thread_local! { - static CELL: ::std::cell::OnceCell<::pyre_object::PyObjectRef> - = const { ::std::cell::OnceCell::new() }; - } - CELL.with(|c| { - *c.get_or_init(|| { - let tp = $crate::typedef::make_builtin_type($name, |ns| { - // `make_builtin_function` (varargs, no arity check) is - // used here rather than `_with_arity` because methods - // with `Option` parameters need to accept calls with - // fewer args (PyPy `def f(self, s=None)`). The - // `#[pyre_function]` wrapper uses bounds-checked - // `args.len()` for Option arms so missing-arg → None, - // while required args still index `args[N]` directly. - $($( - { - #[$crate::pyre_function] - fn $mname ( $($margs)* ) $(-> $mret)? $mbody - unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( - ns, - stringify!($mname), - $crate::make_builtin_function(stringify!($mname), $mname), - ) }; - } - )*)? - // `properties:` — each fn registered as a - // `GetSetProperty` descriptor so `obj.name` - // returns the value directly (PyPy - // `GetSetProperty(W_X.fget_name)`). - $($( - { - #[$crate::pyre_function] - fn $pname ( $($pargs)* ) $(-> $pret)? $pbody - unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( - ns, + static CELL: ::std::sync::OnceLock = ::std::sync::OnceLock::new(); + *CELL.get_or_init(|| { + let tp = $crate::typedef::make_builtin_type($name, |ns| { + // `make_builtin_function` (varargs, no arity check) is + // used here rather than `_with_arity` because methods + // with `Option` parameters need to accept calls with + // fewer args (PyPy `def f(self, s=None)`). The + // `#[pyre_function]` wrapper uses bounds-checked + // `args.len()` for Option arms so missing-arg → None, + // while required args still index `args[N]` directly. + $($( + { + #[$crate::pyre_function] + fn $mname ( $($margs)* ) $(-> $mret)? $mbody + unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + stringify!($mname), + $crate::make_builtin_function(stringify!($mname), $mname), + ) }; + } + )*)? + // `properties:` — each fn registered as a + // `GetSetProperty` descriptor so `obj.name` + // returns the value directly (PyPy + // `GetSetProperty(W_X.fget_name)`). + $($( + { + #[$crate::pyre_function] + fn $pname ( $($pargs)* ) $(-> $pret)? $pbody + unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + stringify!($pname), + $crate::typedef::make_getset_descriptor_named( + $crate::make_builtin_function(stringify!($pname), $pname), stringify!($pname), - $crate::typedef::make_getset_descriptor_named( - $crate::make_builtin_function(stringify!($pname), $pname), - stringify!($pname), - ), - ) }; - } - )*)? - }); - unsafe { ::pyre_object::typeobject::w_type_set_hasdict(tp, true) }; - tp - }) - }) + ), + ) }; + } + )*)? + }); + unsafe { ::pyre_object::typeobject::w_type_set_hasdict(tp, true) }; + tp as usize + }) as ::pyre_object::PyObjectRef } }; } @@ -493,56 +488,51 @@ macro_rules! py_class_typed { $(,)? ) => { pub fn type_object() -> ::pyre_object::PyObjectRef { - thread_local! { - static CELL: ::std::cell::OnceCell<::pyre_object::PyObjectRef> - = const { ::std::cell::OnceCell::new() }; - } - CELL.with(|c| { - *c.get_or_init(|| { - let tp = $crate::typedef::make_builtin_type_with_layout( - $name, - |ns| { - $($( - { - #[$crate::pyre_function] - fn $mname ( $($margs)* ) $(-> $mret)? $mbody - unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( - ns, - stringify!($mname), - $crate::make_builtin_function(stringify!($mname), $mname), - ) }; - } - )*)? - $($( - { - #[$crate::pyre_function] - fn $pname ( $($pargs)* ) $(-> $pret)? $pbody - unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( - ns, + static CELL: ::std::sync::OnceLock = ::std::sync::OnceLock::new(); + *CELL.get_or_init(|| { + let tp = $crate::typedef::make_builtin_type_with_layout( + $name, + |ns| { + $($( + { + #[$crate::pyre_function] + fn $mname ( $($margs)* ) $(-> $mret)? $mbody + unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + stringify!($mname), + $crate::make_builtin_function(stringify!($mname), $mname), + ) }; + } + )*)? + $($( + { + #[$crate::pyre_function] + fn $pname ( $($pargs)* ) $(-> $pret)? $pbody + unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + stringify!($pname), + $crate::typedef::make_getset_descriptor_named( + $crate::make_builtin_function(stringify!($pname), $pname), stringify!($pname), - $crate::typedef::make_getset_descriptor_named( - $crate::make_builtin_function(stringify!($pname), $pname), - stringify!($pname), - ), - ) }; - } - )*)? - }, - $crate::typedef::w_object(), - <$struct as $crate::PyreClassPyTypeOf>::PYTYPE, - ); - // Eagerly bind the W_TypeObject to the static - // `PyType` so `<$struct>::allocate(...)` can stamp - // `ob_header.w_class` at construction without racing - // the post-init typedef pass (matches - // `getset_descriptor_type()`'s eager `set_instantiate`). - ::pyre_object::pyobject::set_instantiate( - unsafe { &*<$struct as $crate::PyreClassPyTypeOf>::PYTYPE }, - tp, - ); - tp - }) - }) + ), + ) }; + } + )*)? + }, + $crate::typedef::w_object(), + <$struct as $crate::PyreClassPyTypeOf>::PYTYPE, + ); + // Eagerly bind the W_TypeObject to the static + // `PyType` so `<$struct>::allocate(...)` can stamp + // `ob_header.w_class` at construction without racing + // the post-init typedef pass (matches + // `getset_descriptor_type()`'s eager `set_instantiate`). + ::pyre_object::pyobject::set_instantiate( + unsafe { &*<$struct as $crate::PyreClassPyTypeOf>::PYTYPE }, + tp, + ); + tp as usize + }) as ::pyre_object::PyObjectRef } }; } diff --git a/pyre/pyre-interpreter/src/module/grp/grp.rs b/pyre/pyre-interpreter/src/module/grp/grp.rs index 33feb6f0677..c0dab6bbcc6 100644 --- a/pyre/pyre-interpreter/src/module/grp/grp.rs +++ b/pyre/pyre-interpreter/src/module/grp/grp.rs @@ -3,24 +3,19 @@ //! Verbatim move of the inline block previously in importing.rs. #[cfg(unix)] -thread_local! { - /// `lib_pypy/grp.py:14-20 class struct_group(metaclass=structseqtype)` - /// — process-wide cached subclass-of-tuple type so every getgrgid / - /// getgrnam / getgrall call materialises into the same structseq. - static STRUCT_GROUP_TYPE: std::cell::OnceCell = - const { std::cell::OnceCell::new() }; -} +/// `lib_pypy/grp.py:14-20 class struct_group(metaclass=structseqtype)` +/// — process-wide cached subclass-of-tuple type so every getgrgid / +/// getgrnam / getgrall call materialises into the same structseq. +static STRUCT_GROUP_TYPE: std::sync::OnceLock = std::sync::OnceLock::new(); #[cfg(unix)] fn struct_group_type() -> pyre_object::PyObjectRef { - STRUCT_GROUP_TYPE.with(|c| { - *c.get_or_init(|| { - crate::_structseq::make_struct_seq( - "grp.struct_group", - &["gr_name", "gr_passwd", "gr_gid", "gr_mem"], - ) - }) - }) + *STRUCT_GROUP_TYPE.get_or_init(|| { + crate::_structseq::make_struct_seq( + "grp.struct_group", + &["gr_name", "gr_passwd", "gr_gid", "gr_mem"], + ) as usize + }) as pyre_object::PyObjectRef } /// grp module — `lib_pypy/grp.py` (PyPy keeps it app-level via diff --git a/pyre/pyre-interpreter/src/module/imp/interp_imp.rs b/pyre/pyre-interpreter/src/module/imp/interp_imp.rs index 40cee5414fb..4f18a401a4c 100644 --- a/pyre/pyre-interpreter/src/module/imp/interp_imp.rs +++ b/pyre/pyre-interpreter/src/module/imp/interp_imp.rs @@ -3,12 +3,189 @@ //! Verbatim move of the inline block previously in importing.rs. use crate::importing::BUILTIN_MODULES; +use std::sync::atomic::{AtomicI64, Ordering}; + +struct FrozenModule { + name: &'static str, + origname: Option<&'static str>, + is_package: bool, + source: FrozenSource, +} + +enum FrozenSource { + Stdlib(&'static str), + Literal(&'static str), +} + +static FROZEN_MODULES: &[FrozenModule] = &[ + FrozenModule { + name: "_frozen_importlib", + origname: Some("importlib._bootstrap"), + is_package: false, + source: FrozenSource::Stdlib("importlib/_bootstrap.py"), + }, + FrozenModule { + name: "_frozen_importlib_external", + origname: Some("importlib._bootstrap_external"), + is_package: false, + source: FrozenSource::Stdlib("importlib/_bootstrap_external.py"), + }, + FrozenModule { + name: "zipimport", + origname: Some("zipimport"), + is_package: false, + source: FrozenSource::Stdlib("zipimport.py"), + }, + FrozenModule { + name: "__hello__", + origname: Some("__hello__"), + is_package: false, + source: FrozenSource::Stdlib("__hello__.py"), + }, + FrozenModule { + name: "__hello_alias__", + origname: Some("__hello__"), + is_package: false, + source: FrozenSource::Stdlib("__hello__.py"), + }, + FrozenModule { + name: "__phello_alias__", + origname: Some("__hello__"), + is_package: true, + source: FrozenSource::Stdlib("__hello__.py"), + }, + FrozenModule { + name: "__phello_alias__.spam", + origname: Some("__hello__"), + is_package: false, + source: FrozenSource::Stdlib("__hello__.py"), + }, + FrozenModule { + name: "__phello__", + origname: Some("__phello__"), + is_package: true, + source: FrozenSource::Stdlib("__phello__/__init__.py"), + }, + FrozenModule { + name: "__phello__.__init__", + origname: Some("<__phello__"), + is_package: false, + source: FrozenSource::Stdlib("__phello__/__init__.py"), + }, + FrozenModule { + name: "__phello__.ham", + origname: Some("__phello__.ham"), + is_package: true, + source: FrozenSource::Stdlib("__phello__/ham/__init__.py"), + }, + FrozenModule { + name: "__phello__.ham.__init__", + origname: Some("<__phello__.ham"), + is_package: false, + source: FrozenSource::Stdlib("__phello__/ham/__init__.py"), + }, + FrozenModule { + name: "__phello__.ham.eggs", + origname: Some("__phello__.ham.eggs"), + is_package: false, + source: FrozenSource::Stdlib("__phello__/ham/eggs.py"), + }, + FrozenModule { + name: "__phello__.spam", + origname: Some("__phello__.spam"), + is_package: false, + source: FrozenSource::Stdlib("__phello__/spam.py"), + }, + FrozenModule { + name: "__hello_only__", + origname: None, + is_package: false, + source: FrozenSource::Literal("initialized = True\n"), + }, +]; + +static FROZEN_OVERRIDE: AtomicI64 = AtomicI64::new(0); + +fn frozen_module(name: &str) -> Option<&'static FrozenModule> { + FROZEN_MODULES.iter().find(|entry| entry.name == name) +} + +fn is_bootstrap_frozen(name: &str) -> bool { + matches!( + name, + "_frozen_importlib" | "_frozen_importlib_external" | "zipimport" + ) +} + +fn frozen_module_served(entry: &FrozenModule) -> bool { + let mode = FROZEN_OVERRIDE.load(Ordering::Relaxed); + mode > 0 || (mode <= 0 && is_bootstrap_frozen(entry.name)) +} + +fn served_frozen_module(name: &str) -> Option<&'static FrozenModule> { + frozen_module(name).filter(|entry| frozen_module_served(entry)) +} + +fn frozen_name(args: &[pyre_object::PyObjectRef], function: &str) -> Result { + let Some(&name) = args.first() else { + return Err(crate::PyError::type_error(format!( + "{function} expected at least 1 argument, got 0" + ))); + }; + if !unsafe { pyre_object::is_str(name) } { + return Err(crate::PyError::type_error(format!( + "{function}() argument 1 must be str" + ))); + } + Ok(unsafe { pyre_object::w_str_get_value(name) }.to_owned()) +} + +fn missing_frozen_error(name: &str) -> crate::PyError { + crate::PyError::import_error_name_path( + format!("No such frozen object named {name:?}"), + pyre_object::w_str_new(name), + pyre_object::w_none(), + ) +} + +fn frozen_source(entry: &FrozenModule) -> Result<(String, String), crate::PyError> { + match entry.source { + FrozenSource::Literal(source) => Ok((source.to_owned(), "frozen_only".to_owned())), + FrozenSource::Stdlib(relative) => { + #[cfg(feature = "host_env")] + { + let stdlib = crate::importing::detect_stdlib_path().ok_or_else(|| { + crate::PyError::new( + crate::PyErrorKind::ImportError, + format!("cannot resolve source for frozen module {:?}", entry.name), + ) + })?; + let path = stdlib.join(relative); + let source = crate::importing::read_source_to_string(&path).map_err(|error| { + crate::PyError::new( + crate::PyErrorKind::ImportError, + format!("cannot read '{}': {error}", path.display()), + ) + })?; + let code_name = entry + .origname + .unwrap_or(entry.name) + .strip_prefix('<') + .unwrap_or(entry.origname.unwrap_or(entry.name)); + Ok((source, code_name.to_owned())) + } + #[cfg(not(feature = "host_env"))] + { + let _ = relative; + Err(crate::PyError::new( + crate::PyErrorKind::ImportError, + format!("cannot resolve source for frozen module {:?}", entry.name), + )) + } + } + } +} -/// _imp stub — PyPy: pypy/module/imp/ -/// -/// Minimal subset required by importlib._bootstrap to decide which loader -/// handles a name. We report every name we know about as a builtin so -/// pyre's own registrations remain authoritative. pub fn register_module(ns: pyre_object::PyObjectRef) { crate::module_ns_store( ns, @@ -37,7 +214,12 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { "is_frozen", crate::make_builtin_function_with_arity( "is_frozen", - |_| Ok(pyre_object::w_bool_from(false)), + |args| { + let name = frozen_name(args, "is_frozen")?; + Ok(pyre_object::w_bool_from( + served_frozen_module(&name).is_some(), + )) + }, 1, ), ); @@ -46,7 +228,12 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { "is_frozen_package", crate::make_builtin_function_with_arity( "is_frozen_package", - |_| Ok(pyre_object::w_bool_from(false)), + |args| { + let name = frozen_name(args, "is_frozen_package")?; + let entry = + served_frozen_module(&name).ok_or_else(|| missing_frozen_error(&name))?; + Ok(pyre_object::w_bool_from(entry.is_package)) + }, 1, ), ); @@ -54,51 +241,71 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { ns, "_frozen_module_names", crate::make_builtin_function("_frozen_module_names", |_| { - Ok(pyre_object::w_list_new(Vec::new())) + Ok(pyre_object::w_list_new( + FROZEN_MODULES + .iter() + .filter(|entry| frozen_module_served(entry)) + .map(|entry| pyre_object::w_str_new(entry.name)) + .collect(), + )) }), ); - // `_imp.find_frozen(name)` — `FrozenImporter.find_spec` calls this and - // treats None as "not a frozen module". Pyre has no frozen modules, so - // every name resolves to None and the import falls through to the next - // finder on `sys.meta_path`. crate::module_ns_store( ns, "find_frozen", crate::make_builtin_function_with_arity( "find_frozen", - |_| Ok(pyre_object::w_none()), + |args| { + let name = frozen_name(args, "find_frozen")?; + let Some(entry) = served_frozen_module(&name) else { + return Ok(pyre_object::w_none()); + }; + let origname = entry + .origname + .map(pyre_object::w_str_new) + .unwrap_or_else(pyre_object::w_none); + Ok(pyre_object::w_tuple_new(vec![ + pyre_object::w_none(), + pyre_object::w_bool_from(entry.is_package), + origname, + ])) + }, 1, ), ); - // `_imp._override_frozen_modules_for_tests(value)` — the CPython test - // harness (`test.support.import_helper`) toggles frozen-module - // overriding. Pyre has no frozen modules, so accept and ignore. crate::module_ns_store( ns, "_override_frozen_modules_for_tests", - crate::make_builtin_function("_override_frozen_modules_for_tests", |_| { + crate::make_builtin_function("_override_frozen_modules_for_tests", |args| { + let Some(&value) = args.first() else { + return Err(crate::PyError::type_error( + "_override_frozen_modules_for_tests expected at least 1 argument, got 0", + )); + }; + let value = crate::baseobjspace::gateway_int_w(value)?; + FROZEN_OVERRIDE.store(value, Ordering::Relaxed); Ok(pyre_object::w_none()) }), ); - // `_imp.get_frozen_object(name, data=None)` — pyre has no frozen modules, - // so every name is unknown and `set_frozen_error(FROZEN_NOT_FOUND)` raises - // `ImportError("No such frozen object named %R")`. crate::module_ns_store( ns, "get_frozen_object", crate::make_builtin_function_with_arity( "get_frozen_object", |args| { - let Some(&name) = args.first() else { - return Err(crate::PyError::new( - crate::PyErrorKind::TypeError, - "get_frozen_object expected at least 1 argument, got 0".to_string(), - )); - }; - let name_repr = unsafe { crate::display::py_repr(name)? }; - Err(crate::PyError::new( - crate::PyErrorKind::ImportError, - format!("No such frozen object named {name_repr}"), + let name = frozen_name(args, "get_frozen_object")?; + let entry = + served_frozen_module(&name).ok_or_else(|| missing_frozen_error(&name))?; + let (source, code_name) = frozen_source(entry)?; + let filename = format!(""); + let code = crate::compile::compile_source_with_filename( + &source, + crate::compile::Mode::Exec, + &filename, + ) + .map_err(|error| crate::builtins::compile_err_to_syntax_error(error, &source))?; + Ok(crate::w_code_new( + Box::into_raw(Box::new(code)) as *const () )) }, 1, @@ -210,5 +417,13 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { "check_hash_based_pycs", pyre_object::w_str_new("default"), ); - crate::module_ns_store(ns, "pyc_magic_number_token", pyre_object::w_int_new(3495)); + // `MAGIC_NUMBER = _imp.pyc_magic_number_token.to_bytes(4, 'little')` + // (_bootstrap_external.py) — low half is the 3.14 magic 3627, high half + // the `\r\n` marker so the number breaks when read as text. Cache + // files are already segregated by `sys.implementation.cache_tag`. + crate::module_ns_store( + ns, + "pyc_magic_number_token", + pyre_object::w_int_new(0x0A0D_0E2B), + ); } diff --git a/pyre/pyre-interpreter/src/module/mmap/interp_mmap.rs b/pyre/pyre-interpreter/src/module/mmap/interp_mmap.rs index f12dd0ce54c..e2f0baba258 100644 --- a/pyre/pyre-interpreter/src/module/mmap/interp_mmap.rs +++ b/pyre/pyre-interpreter/src/module/mmap/interp_mmap.rs @@ -86,23 +86,18 @@ fn mmap_io_err(e: std::io::Error, ctx: &str) -> crate::PyError { } #[cfg(unix)] -thread_local! { - static MMAP_TYPE_OBJ: std::cell::OnceCell = - const { std::cell::OnceCell::new() }; -} +static MMAP_TYPE_OBJ: std::sync::OnceLock = std::sync::OnceLock::new(); #[cfg(unix)] fn mmap_type() -> pyre_object::PyObjectRef { - MMAP_TYPE_OBJ.with(|c| { - *c.get_or_init(|| { - let tp = crate::typedef::make_builtin_type("mmap", init_mmap_type); - unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; - // A view dropped by the collector never reaches `__release_buffer__`, - // so the buffer layer needs a way back here to drop the count. - unsafe { pyre_object::buffer::set_external_release_hook(mmap_exports_decref) }; - tp - }) - }) + *MMAP_TYPE_OBJ.get_or_init(|| { + let tp = crate::typedef::make_builtin_type("mmap", init_mmap_type); + unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; + // A view dropped by the collector never reaches `__release_buffer__`, + // so the buffer layer needs a way back here to drop the count. + unsafe { pyre_object::buffer::set_external_release_hook(mmap_exports_decref) }; + tp as usize + }) as pyre_object::PyObjectRef } #[cfg(unix)] @@ -207,20 +202,15 @@ fn mmap_get_attr_obj(obj: pyre_object::PyObjectRef, key: &str) -> pyre_object::P // generator as a dedicated iterator object holding the source mmap, a // cursor, and a step (`+1` forwards, `-1` for `reversed`). #[cfg(unix)] -thread_local! { - static MMAP_ITER_TYPE_OBJ: std::cell::OnceCell = - const { std::cell::OnceCell::new() }; -} +static MMAP_ITER_TYPE_OBJ: std::sync::OnceLock = std::sync::OnceLock::new(); #[cfg(unix)] fn mmap_iterator_type() -> pyre_object::PyObjectRef { - MMAP_ITER_TYPE_OBJ.with(|c| { - *c.get_or_init(|| { - let tp = crate::typedef::make_builtin_type("mmap_iterator", init_mmap_iterator_type); - unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; - tp - }) - }) + *MMAP_ITER_TYPE_OBJ.get_or_init(|| { + let tp = crate::typedef::make_builtin_type("mmap_iterator", init_mmap_iterator_type); + unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; + tp as usize + }) as pyre_object::PyObjectRef } #[cfg(unix)] diff --git a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs index c73de4a2263..947c4bf8f70 100644 --- a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs +++ b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs @@ -20,11 +20,8 @@ use crate::host_seam::sys as libc; /// `st_blksize`/`st_blocks`/`st_rdev` block-device fields are named-only /// extras. fn stat_result_seq_type() -> PyObjectRef { - thread_local! { - static STAT_RESULT_SEQ_TYPE: std::cell::OnceCell = const { std::cell::OnceCell::new() }; - } - STAT_RESULT_SEQ_TYPE.with(|c| { - *c.get_or_init(|| { + static STAT_RESULT_SEQ_TYPE: std::sync::OnceLock = std::sync::OnceLock::new(); + *STAT_RESULT_SEQ_TYPE.get_or_init(|| { crate::_structseq::make_struct_seq_with_extra( // Dotted name → `__name__` "stat_result", repr "os.stat_result(...)". "os.stat_result", @@ -69,68 +66,52 @@ fn stat_result_seq_type() -> PyObjectRef { "st_mtime_ns", "st_ctime_ns", ], - ) - }) - }) + ) as usize + }) as PyObjectRef } /// `os.terminal_size` structseq — `(columns, lines)`. fn terminal_size_seq_type() -> PyObjectRef { - thread_local! { - static T: std::cell::OnceCell = const { std::cell::OnceCell::new() }; - } - T.with(|c| { - *c.get_or_init(|| { - crate::_structseq::make_struct_seq("os.terminal_size", &["columns", "lines"]) - }) - }) + static T: std::sync::OnceLock = std::sync::OnceLock::new(); + *T.get_or_init(|| { + crate::_structseq::make_struct_seq("os.terminal_size", &["columns", "lines"]) as usize + }) as PyObjectRef } /// `os.uname_result` structseq — `(sysname, nodename, release, version, /// machine)`; repr renders "posix.uname_result(...)". fn uname_result_seq_type() -> PyObjectRef { - thread_local! { - static T: std::cell::OnceCell = const { std::cell::OnceCell::new() }; - } - T.with(|c| { - *c.get_or_init(|| { - crate::_structseq::make_struct_seq( + static T: std::sync::OnceLock = std::sync::OnceLock::new(); + *T.get_or_init(|| { + crate::_structseq::make_struct_seq( "posix.uname_result", &["sysname", "nodename", "release", "version", "machine"], - ) - }) - }) + ) as usize + }) as PyObjectRef } /// `os.statvfs_result` structseq — 10 sequence slots with `f_fsid` as an /// extra named field (`n_sequence_fields=10`, `n_fields=11`). fn statvfs_result_seq_type() -> PyObjectRef { - thread_local! { - static T: std::cell::OnceCell = const { std::cell::OnceCell::new() }; - } - T.with(|c| { - *c.get_or_init(|| { - crate::_structseq::make_struct_seq_with_extra( + static T: std::sync::OnceLock = std::sync::OnceLock::new(); + *T.get_or_init(|| { + crate::_structseq::make_struct_seq_with_extra( "os.statvfs_result", &[ "f_bsize", "f_frsize", "f_blocks", "f_bfree", "f_bavail", "f_files", "f_ffree", "f_favail", "f_flag", "f_namemax", ], &["f_fsid"], - ) - }) - }) + ) as usize + }) as PyObjectRef } /// `os.times_result` structseq — `(user, system, children_user, /// children_system, elapsed)`; repr renders "posix.times_result(...)". fn times_result_seq_type() -> PyObjectRef { - thread_local! { - static T: std::cell::OnceCell = const { std::cell::OnceCell::new() }; - } - T.with(|c| { - *c.get_or_init(|| { - crate::_structseq::make_struct_seq( + static T: std::sync::OnceLock = std::sync::OnceLock::new(); + *T.get_or_init(|| { + crate::_structseq::make_struct_seq( "posix.times_result", &[ "user", @@ -139,9 +120,8 @@ fn times_result_seq_type() -> PyObjectRef { "children_system", "elapsed", ], - ) - }) - }) + ) as usize + }) as PyObjectRef } /// Split `path` into the root and everything after it, the way @@ -1537,29 +1517,25 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { Ok(pyre_object::w_str_new(&format!(""))) } fn dir_entry_type() -> PyObjectRef { - thread_local! { - static CELL: std::cell::OnceCell = const { std::cell::OnceCell::new() }; - } - CELL.with(|c| { - *c.get_or_init(|| { - let tp = crate::typedef::make_builtin_type("DirEntry", |ns| { - for (name, f) in [ - ("is_dir", dir_entry_is_dir as crate::gateway::BuiltinCodeFn), - ("is_file", dir_entry_is_file), - ("is_symlink", dir_entry_is_symlink), - ("is_junction", dir_entry_is_junction), - ("inode", dir_entry_inode), - ("stat", dir_entry_stat), - ("__fspath__", dir_entry_fspath), - ("__repr__", dir_entry_repr), - ] { - unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy(ns, name, crate::make_builtin_function(name, f)) }; - } - }); - unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; - tp - }) - }) + static CELL: std::sync::OnceLock = std::sync::OnceLock::new(); + *CELL.get_or_init(|| { + let tp = crate::typedef::make_builtin_type("DirEntry", |ns| { + for (name, f) in [ + ("is_dir", dir_entry_is_dir as crate::gateway::BuiltinCodeFn), + ("is_file", dir_entry_is_file), + ("is_symlink", dir_entry_is_symlink), + ("is_junction", dir_entry_is_junction), + ("inode", dir_entry_inode), + ("stat", dir_entry_stat), + ("__fspath__", dir_entry_fspath), + ("__repr__", dir_entry_repr), + ] { + unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy(ns, name, crate::make_builtin_function(name, f)) }; + } + }); + unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; + tp as usize + }) as PyObjectRef } fn scandir_iter_self(args: &[PyObjectRef]) -> Result { @@ -1584,26 +1560,22 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { Ok(item) } fn scandir_iter_type() -> PyObjectRef { - thread_local! { - static CELL: std::cell::OnceCell = const { std::cell::OnceCell::new() }; - } - CELL.with(|c| { - *c.get_or_init(|| { - let tp = crate::typedef::make_builtin_type("ScandirIterator", |ns| { - for (name, f) in [ - ("__iter__", scandir_iter_self as crate::gateway::BuiltinCodeFn), - ("__next__", scandir_iter_next), - ("__enter__", scandir_iter_self), - ("__exit__", scandir_iter_close), - ("close", scandir_iter_close), - ] { - unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy(ns, name, crate::make_builtin_function(name, f)) }; - } - }); - unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; - tp - }) - }) + static CELL: std::sync::OnceLock = std::sync::OnceLock::new(); + *CELL.get_or_init(|| { + let tp = crate::typedef::make_builtin_type("ScandirIterator", |ns| { + for (name, f) in [ + ("__iter__", scandir_iter_self as crate::gateway::BuiltinCodeFn), + ("__next__", scandir_iter_next), + ("__enter__", scandir_iter_self), + ("__exit__", scandir_iter_close), + ("close", scandir_iter_close), + ] { + unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy(ns, name, crate::make_builtin_function(name, f)) }; + } + }); + unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; + tp as usize + }) as PyObjectRef } fn scandir_fn(args: &[PyObjectRef]) -> Result { diff --git a/pyre/pyre-interpreter/src/module/pwd/interp_pwd.rs b/pyre/pyre-interpreter/src/module/pwd/interp_pwd.rs index 4be7b27a030..d2232d41c4c 100644 --- a/pyre/pyre-interpreter/src/module/pwd/interp_pwd.rs +++ b/pyre/pyre-interpreter/src/module/pwd/interp_pwd.rs @@ -3,32 +3,27 @@ //! Verbatim move of the inline block previously in importing.rs. #[cfg(unix)] -thread_local! { - /// `app_pwd.py:3-19 class struct_passwd(metaclass=structseqtype)`. - /// Process-wide cached subclass-of-tuple type so every getpwuid / - /// getpwnam / getpwall result materialises into the same structseq. - static STRUCT_PASSWD_TYPE: std::cell::OnceCell = - const { std::cell::OnceCell::new() }; -} +/// `app_pwd.py:3-19 class struct_passwd(metaclass=structseqtype)`. +/// Process-wide cached subclass-of-tuple type so every getpwuid / +/// getpwnam / getpwall result materialises into the same structseq. +static STRUCT_PASSWD_TYPE: std::sync::OnceLock = std::sync::OnceLock::new(); #[cfg(unix)] fn struct_passwd_type() -> pyre_object::PyObjectRef { - STRUCT_PASSWD_TYPE.with(|c| { - *c.get_or_init(|| { - crate::_structseq::make_struct_seq( - "pwd.struct_passwd", - &[ - "pw_name", - "pw_passwd", - "pw_uid", - "pw_gid", - "pw_gecos", - "pw_dir", - "pw_shell", - ], - ) - }) - }) + *STRUCT_PASSWD_TYPE.get_or_init(|| { + crate::_structseq::make_struct_seq( + "pwd.struct_passwd", + &[ + "pw_name", + "pw_passwd", + "pw_uid", + "pw_gid", + "pw_gecos", + "pw_dir", + "pw_shell", + ], + ) as usize + }) as pyre_object::PyObjectRef } /// `interp_pwd.py:50-73 uid_converter` — narrow a python int to `uid_t`. diff --git a/pyre/pyre-interpreter/src/module/resource/resource.rs b/pyre/pyre-interpreter/src/module/resource/resource.rs index c639132d1ea..c7892692019 100644 --- a/pyre/pyre-interpreter/src/module/resource/resource.rs +++ b/pyre/pyre-interpreter/src/module/resource/resource.rs @@ -3,18 +3,14 @@ //! Verbatim move of the inline block previously in importing.rs. -thread_local! { - /// `lib_pypy/resource.py:15-37 class struct_rusage( - /// metaclass=structseqtype)` — process-wide cached subclass-of-tuple - /// type. - static STRUCT_RUSAGE_TYPE: std::cell::OnceCell = - const { std::cell::OnceCell::new() }; -} +/// `lib_pypy/resource.py:15-37 class struct_rusage( +/// metaclass=structseqtype)` — process-wide cached subclass-of-tuple +/// type. +static STRUCT_RUSAGE_TYPE: std::sync::OnceLock = std::sync::OnceLock::new(); fn struct_rusage_type() -> pyre_object::PyObjectRef { - STRUCT_RUSAGE_TYPE.with(|c| { - *c.get_or_init(|| { - crate::_structseq::make_struct_seq( + *STRUCT_RUSAGE_TYPE.get_or_init(|| { + crate::_structseq::make_struct_seq( "resource.struct_rusage", &[ "ru_utime", @@ -34,9 +30,8 @@ fn struct_rusage_type() -> pyre_object::PyObjectRef { "ru_nvcsw", "ru_nivcsw", ], - ) - }) - }) + ) as usize + }) as pyre_object::PyObjectRef } /// resource module — `lib_pypy/resource.py` (PyPy keeps it app-level diff --git a/pyre/pyre-interpreter/src/module/sys/vm.rs b/pyre/pyre-interpreter/src/module/sys/vm.rs index 70773b06970..8ec5297cfc1 100644 --- a/pyre/pyre-interpreter/src/module/sys/vm.rs +++ b/pyre/pyre-interpreter/src/module/sys/vm.rs @@ -1056,6 +1056,15 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { module_ns_store(ns, "exec_prefix", w_str_new("")); module_ns_store(ns, "base_prefix", w_str_new("")); module_ns_store(ns, "base_exec_prefix", w_str_new("")); + // FrozenImporter uses the resolved stdlib root to reconstruct source + // filenames for frozen stdlib modules. + #[cfg(feature = "host_env")] + let stdlib_dir = crate::importing::detect_stdlib_path() + .and_then(|path| path.to_str().map(w_str_new)) + .unwrap_or_else(w_none); + #[cfg(not(feature = "host_env"))] + let stdlib_dir = w_none(); + module_ns_store(ns, "_stdlib_dir", stdlib_dir); // sys._framework — macOS framework name (empty string on non-framework builds) module_ns_store(ns, "_framework", w_str_new("")); // sys._jit — namespace with is_enabled/is_available methods. diff --git a/pyre/pyre-interpreter/src/module/time/interp_time.rs b/pyre/pyre-interpreter/src/module/time/interp_time.rs index e0dfd46f336..248b620a974 100644 --- a/pyre/pyre-interpreter/src/module/time/interp_time.rs +++ b/pyre/pyre-interpreter/src/module/time/interp_time.rs @@ -709,32 +709,27 @@ fn c_tm_to_msvc_tm(tm: &c_tm) -> MsvcTm { /// process-wide cached subclass-of-tuple type. The 9-field positional /// core; on Unix (`HAS_TM_ZONE`) `tm_zone` / `tm_gmtoff` are named-only /// extras so `n_fields == _STRUCT_TM_ITEMS == 11`. -thread_local! { - static STRUCT_TIME_TYPE: std::cell::OnceCell = - const { std::cell::OnceCell::new() }; -} +static STRUCT_TIME_TYPE: std::sync::OnceLock = std::sync::OnceLock::new(); pub(crate) fn struct_time_type() -> PyObjectRef { const SEQ: &[&str] = &[ "tm_year", "tm_mon", "tm_mday", "tm_hour", "tm_min", "tm_sec", "tm_wday", "tm_yday", "tm_isdst", ]; - STRUCT_TIME_TYPE.with(|c| { - *c.get_or_init(|| { - #[cfg(unix)] - { - crate::_structseq::make_struct_seq_with_extra( - "time.struct_time", - SEQ, - &["tm_zone", "tm_gmtoff"], - ) - } - #[cfg(not(unix))] - { - crate::_structseq::make_struct_seq("time.struct_time", SEQ) - } - }) - }) + *STRUCT_TIME_TYPE.get_or_init(|| { + #[cfg(unix)] + { + crate::_structseq::make_struct_seq_with_extra( + "time.struct_time", + SEQ, + &["tm_zone", "tm_gmtoff"], + ) as usize + } + #[cfg(not(unix))] + { + crate::_structseq::make_struct_seq("time.struct_time", SEQ) as usize + } + }) as PyObjectRef } /// Build a `time.struct_time` from our portable `c_tm`. diff --git a/pyre/pyre-interpreter/src/module/zlib/mod.rs b/pyre/pyre-interpreter/src/module/zlib/mod.rs index dfa3c4af44e..378936ea65f 100644 --- a/pyre/pyre-interpreter/src/module/zlib/mod.rs +++ b/pyre/pyre-interpreter/src/module/zlib/mod.rs @@ -146,20 +146,16 @@ fn adler32_compute(buf: &[u8], start: u32) -> u32 { // ── Compress (compressobj) ────────────────────────────────────────────── -thread_local! { - static COMPRESS_TYPE: std::cell::OnceCell = const { std::cell::OnceCell::new() }; - static DECOMPRESS_TYPE: std::cell::OnceCell = const { std::cell::OnceCell::new() }; - static ZDECOMPRESS_TYPE: std::cell::OnceCell = const { std::cell::OnceCell::new() }; -} +static COMPRESS_TYPE: std::sync::OnceLock = std::sync::OnceLock::new(); +static DECOMPRESS_TYPE: std::sync::OnceLock = std::sync::OnceLock::new(); +static ZDECOMPRESS_TYPE: std::sync::OnceLock = std::sync::OnceLock::new(); fn compress_type() -> PyObjectRef { - COMPRESS_TYPE.with(|c| { - *c.get_or_init(|| { - let tp = crate::typedef::make_builtin_type("Compress", init_compress_type); - unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; - tp - }) - }) + *COMPRESS_TYPE.get_or_init(|| { + let tp = crate::typedef::make_builtin_type("Compress", init_compress_type); + unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; + tp as usize + }) as PyObjectRef } fn init_compress_type(ns: PyObjectRef) { @@ -226,13 +222,11 @@ fn make_compress( // ── Decompress (decompressobj) ────────────────────────────────────────── fn decompress_type() -> PyObjectRef { - DECOMPRESS_TYPE.with(|c| { - *c.get_or_init(|| { - let tp = crate::typedef::make_builtin_type("Decompress", init_decompress_type); - unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; - tp - }) - }) + *DECOMPRESS_TYPE.get_or_init(|| { + let tp = crate::typedef::make_builtin_type("Decompress", init_decompress_type); + unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; + tp as usize + }) as PyObjectRef } fn decompress_getset(ns: PyObjectRef, name: &'static str, f: crate::gateway::BuiltinCodeFn) { @@ -347,13 +341,11 @@ fn make_decompress(wbits: i8, zdict: Option>) -> Result PyObjectRef { - ZDECOMPRESS_TYPE.with(|c| { - *c.get_or_init(|| { - let tp = crate::typedef::make_builtin_type("_ZlibDecompressor", init_zdecompress_type); - unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; - tp - }) - }) + *ZDECOMPRESS_TYPE.get_or_init(|| { + let tp = crate::typedef::make_builtin_type("_ZlibDecompressor", init_zdecompress_type); + unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; + tp as usize + }) as PyObjectRef } fn zdecompress_getset(ns: PyObjectRef, name: &'static str, f: crate::gateway::BuiltinCodeFn) { diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index ceed4424a12..88c5d441157 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -1821,7 +1821,7 @@ fn make_maketrans_descr( /// `__new__` ignores its arguments. A subclass instance is retagged /// with the actual class. fn module_descr_new(args: &[PyObjectRef]) -> Result { - let w_module = pyre_object::w_module_new(""); + let w_module = pyre_object::w_module_new_managed(""); if let Some(cls) = args.first().copied() { if !cls.is_null() { unsafe { (*w_module).w_class = cls }; @@ -2000,7 +2000,7 @@ pub(crate) fn module_repr_string(module: PyObjectRef) -> Result Result { let module = module_require(args.first().copied().unwrap_or(PY_NULL), "__getattribute__")?; let name = crate::baseobjspace::text_w(args[1])?; - crate::baseobjspace::getattr_str(module, name) + crate::baseobjspace::module_getattribute(module, name) } /// module.py:164-173 `Module.descr_module__dir__`. @@ -8392,46 +8392,38 @@ fn init_union_type(ns: PyObjectRef) { }; } -thread_local! { - static GETSET_DESCRIPTOR_TYPE: std::cell::OnceCell - = const { std::cell::OnceCell::new() }; -} +static GETSET_DESCRIPTOR_TYPE: std::sync::OnceLock = std::sync::OnceLock::new(); fn getset_descriptor_type() -> pyre_object::PyObjectRef { - GETSET_DESCRIPTOR_TYPE.with(|cell| { - *cell.get_or_init(|| { - // `typedef.py:444 GetSetProperty.typedef = TypeDef( - // "getset_descriptor", ...)`. Pyre owns the static - // `GETSET_DESCRIPTOR_TYPE` PyType so GetSetProperty - // instances carry it as `ob_type` (not the catch-all - // `INSTANCE_TYPE`). `make_builtin_type_with_layout` - // wires the layout so `setup_builtin_type` records the - // explicit typedef per `typeobject.py:1273-1280`. - let tp = make_builtin_type_with_layout( - "getset_descriptor", - init_getset_descriptor_type, - w_object(), - &pyre_object::typedef::GETSET_DESCRIPTOR_TYPE as *const PyType, - ); - // typedef.py:446 assert not GetSetProperty.typedef.acceptable_as_base_class - unsafe { pyre_object::w_type_set_acceptable_as_base_class(tp, false) }; - // `init_typeobjects` would normally hand the W_TypeObject - // to `set_instantiate(pytype, w_typeobject)` so allocators - // can stamp `ob_header.w_class` at construction time - // (see typedef.rs around `for (pytype, w_type) in reg`). - // `getset_descriptor_type()` is called from inside the - // init loop *as* a builder for descriptors that other - // typedefs install, so the post-loop `set_instantiate` - // pass can race the first GetSetProperty alloc. - // Setting it eagerly here keeps `w_class` non-null for - // every descriptor regardless of allocation order. - pyre_object::pyobject::set_instantiate( - &pyre_object::typedef::GETSET_DESCRIPTOR_TYPE, - tp, - ); - tp - }) - }) + *GETSET_DESCRIPTOR_TYPE.get_or_init(|| { + // `typedef.py:444 GetSetProperty.typedef = TypeDef( + // "getset_descriptor", ...)`. Pyre owns the static + // `GETSET_DESCRIPTOR_TYPE` PyType so GetSetProperty + // instances carry it as `ob_type` (not the catch-all + // `INSTANCE_TYPE`). `make_builtin_type_with_layout` + // wires the layout so `setup_builtin_type` records the + // explicit typedef per `typeobject.py:1273-1280`. + let tp = make_builtin_type_with_layout( + "getset_descriptor", + init_getset_descriptor_type, + w_object(), + &pyre_object::typedef::GETSET_DESCRIPTOR_TYPE as *const PyType, + ); + // typedef.py:446 assert not GetSetProperty.typedef.acceptable_as_base_class + unsafe { pyre_object::w_type_set_acceptable_as_base_class(tp, false) }; + // `init_typeobjects` would normally hand the W_TypeObject + // to `set_instantiate(pytype, w_typeobject)` so allocators + // can stamp `ob_header.w_class` at construction time + // (see typedef.rs around `for (pytype, w_type) in reg`). + // `getset_descriptor_type()` is called from inside the + // init loop *as* a builder for descriptors that other + // typedefs install, so the post-loop `set_instantiate` + // pass can race the first GetSetProperty alloc. + // Setting it eagerly here keeps `w_class` non-null for + // every descriptor regardless of allocation order. + pyre_object::pyobject::set_instantiate(&pyre_object::typedef::GETSET_DESCRIPTOR_TYPE, tp); + tp as usize + }) as pyre_object::PyObjectRef } /// typedef.py:378-382 readonly_attribute @@ -8694,12 +8686,11 @@ fn init_getset_descriptor_type(ns: PyObjectRef) { // __name__/__qualname__/__objclass__/__doc__) cannot be // installed inside this function — each one allocates a fresh // `GetSetProperty` via `make_getset_descriptor`, which - // funnels through `getset_descriptor_type()`'s OnceCell, and we - // are currently *inside* that OnceCell's init closure. - // Re-entering `OnceCell::get_or_init` is undefined behaviour - // (the cell is already mutably borrowed), so the post-init + // funnels through `getset_descriptor_type()`'s OnceLock, and we + // are currently *inside* that OnceLock's init closure. + // Re-entering `OnceLock::get_or_init` would deadlock, so the post-init // helper `patch_getset_descriptor_metadata` stamps them after - // the OnceCell finishes, mirroring how + // the OnceLock finishes, mirroring how // `patch_builtin_function_descriptors` patches the // BuiltinFunction `reqcls` slot. } diff --git a/pyre/pyre-object/src/interp_exceptions.rs b/pyre/pyre-object/src/interp_exceptions.rs index e96450a6d8e..ccb12a3f64f 100644 --- a/pyre/pyre-object/src/interp_exceptions.rs +++ b/pyre/pyre-object/src/interp_exceptions.rs @@ -614,13 +614,13 @@ fn w_exception_new_empty_impl(kind: ExcKind, immortal: bool) -> PyObjectRef { /// PyPy's equivalent is the `space.w_TypeError` / `space.w_ValueError` /// / ... attributes on `ObjSpace`. /// -/// Stored as `thread_local!` because pyre's `W_TypeObject` identities -/// are also per-thread (each cargo test thread re-runs -/// `init_typeobjects` and gets its own `W_TypeObject` pointers via -/// `TYPEOBJECT_CACHE`). A global `AtomicPtr` cache on -/// `PyType.instantiate` would let one test thread's write race ahead -/// of another's, causing `exception_match` on thread A to compare -/// against thread B's W_TypeObject identity — they'd never match. +/// The builtin `W_TypeObject` identities and this registry are process-global. +/// A class installed by one execution-context thread must therefore be the +/// same class used to stamp and match exceptions on every other thread. +/// Registration is first-writer-wins so rebuilding a builtins dictionary +/// cannot replace a canonical class. The pointer is stored as `usize` because +/// `PyObjectRef` itself is neither `Send` nor `Sync`; builtin type objects are +/// immortal and process-global. /// One slot per `ExcKind` variant. Indexed by `kind as u8 as usize`, /// so `EXC_KIND_COUNT - 1` is the largest valid index. Public so /// downstream crates (e.g. pyre-jit's GC init) can size per-kind @@ -629,33 +629,41 @@ fn w_exception_new_empty_impl(kind: ExcKind, immortal: bool) -> PyObjectRef { /// enum extends the bound automatically. pub const EXC_KIND_COUNT: usize = (ExcKind::StopAsyncIteration as u8 as usize) + 1; -thread_local! { - static EXC_CLASS_BY_KIND: std::cell::Cell<[PyObjectRef; EXC_KIND_COUNT]> = - const { std::cell::Cell::new([PY_NULL; EXC_KIND_COUNT]) }; -} - -pub fn register_exc_class_for_kind(kind: ExcKind, cls: PyObjectRef) { - EXC_CLASS_BY_KIND.with(|cell| { - let mut table = cell.get(); - table[kind as u8 as usize] = cls; - cell.set(table); - }); +static EXC_CLASS_BY_KIND: [std::sync::atomic::AtomicUsize; EXC_KIND_COUNT] = + [const { std::sync::atomic::AtomicUsize::new(0) }; EXC_KIND_COUNT]; + +/// Register `cls` for `kind` if the process-global slot is empty and return +/// the canonical class selected by the first writer. +pub fn register_exc_class_for_kind(kind: ExcKind, cls: PyObjectRef) -> PyObjectRef { + let slot = &EXC_CLASS_BY_KIND[kind as u8 as usize]; + match slot.compare_exchange( + 0, + cls as usize, + std::sync::atomic::Ordering::AcqRel, + std::sync::atomic::Ordering::Acquire, + ) { + Ok(_) => cls, + Err(canonical) => canonical as PyObjectRef, + } } -/// Reads the thread-local `EXC_CLASS_BY_KIND`, a runtime-mutable root -/// the tracer cannot type; the JIT residualises the read instead of -/// tracing into it (`@dont_look_inside`, `rlib/jit.py:139`). The residual -/// call resolves its address by qualified path in `jit_trace_fnaddrs`. +/// Reads the process-global `EXC_CLASS_BY_KIND`, a runtime-mutable root the +/// tracer cannot type; the JIT residualises the read instead of tracing into +/// it (`@dont_look_inside`, `rlib/jit.py:139`). The residual call resolves its +/// address by qualified path in `jit_trace_fnaddrs`. #[majit_macros::dont_look_inside] pub fn lookup_exc_class_for_kind(kind: ExcKind) -> PyObjectRef { - EXC_CLASS_BY_KIND.with(|cell| cell.get()[kind as u8 as usize]) + EXC_CLASS_BY_KIND[kind as u8 as usize].load(std::sync::atomic::Ordering::Acquire) as PyObjectRef } -/// True when `cls` is one of the canonical per-kind builtin exception +/// True when `cls` is one of the canonical process-global builtin exception /// classes registered via `register_exc_class_for_kind` — i.e. its /// constructor is the Rust `descr_init` (no Python `__init__`). pub fn is_canonical_exc_class(cls: PyObjectRef) -> bool { - !cls.is_null() && EXC_CLASS_BY_KIND.with(|cell| cell.get().contains(&cls)) + !cls.is_null() + && EXC_CLASS_BY_KIND + .iter() + .any(|slot| slot.load(std::sync::atomic::Ordering::Acquire) == cls as usize) } /// `interp_exceptions.py:153 W_BaseException.descr_getargs` parity — diff --git a/pyre/pyre-object/src/lltype.rs b/pyre/pyre-object/src/lltype.rs index 0bd22147b13..ae4b5c72531 100644 --- a/pyre/pyre-object/src/lltype.rs +++ b/pyre/pyre-object/src/lltype.rs @@ -223,11 +223,12 @@ pub fn malloc(value: T) -> *mut T { majit_gc::header::alloc_with_gc_header(value, 0) } -/// Typed variant of [`malloc`]: `T: GcType` lets the allocator stamp the -/// header with `T::type_id()` and assert `T::SIZE` without a runtime registry -/// lookup. Same body as [`malloc`] (the `alloc_with_gc_header` prepend), -/// passing the real GC type id — `init_gc_object(result, typeid, flags=0)` -/// (`framework.py:807-811`). +/// Legacy typed allocation outside the managed heap. +/// +/// This retains the bootstrap/immortal behavior required by existing leaf +/// boxes and explicit raw-root walkers. New ordinary GC objects must use +/// [`malloc_typed_managed`], whose allocation belongs to the collector and +/// therefore receives its registered trace shape. /// /// `T::type_id()` is `TypeIdCell::UNASSIGNED` (`u32::MAX`) until the JIT /// driver registers an auto-id type. That sentinel is not a real id and is @@ -250,6 +251,40 @@ pub fn malloc_typed(value: T) -> *mut T { majit_gc::header::alloc_with_gc_header(value, type_id) } +/// Managed typed allocation. +/// +/// `gct_fv_gc_malloc` / `init_gc_object(result, typeid, flags=0)` +/// (`framework.py:807-856`): route through the installed GC allocator so the +/// type id's registered fixed offsets or custom trace hook are applied. Falls +/// back to [`malloc_typed`] only before the runtime hook is installed (unit +/// tests and bootstrap tools). +#[inline] +pub fn malloc_typed_managed(value: T) -> *mut T { + debug_assert_eq!( + std::mem::size_of::(), + T::SIZE, + "GcType::SIZE drift from std::mem::size_of" + ); + let type_id = match T::type_id() { + TypeIdCell::UNASSIGNED => 0, + id => id, + }; + if let Some(raw) = crate::gc_hook::try_gc_alloc(type_id, T::SIZE) { + if !raw.is_null() { + unsafe { + std::ptr::write(raw as *mut T, value); + // The no-collect allocator may fall back to old-gen when the + // nursery is full. In that case the freshly-written payload + // can already contain nursery references and must enter the + // remembered set. Nursery allocations ignore this barrier. + crate::gc_hook::try_gc_write_barrier(raw); + } + return raw as *mut T; + } + } + malloc_typed(value) +} + /// Stable-address variant of [`malloc_typed`]: routes through the /// non-moving old-gen allocator (`try_gc_alloc_stable_raw`) so the object /// never relocates across a later collection. A self-mutating typed payload diff --git a/pyre/pyre-object/src/module.rs b/pyre/pyre-object/src/module.rs index e4c5844ee71..5b94ea2de08 100644 --- a/pyre/pyre-object/src/module.rs +++ b/pyre/pyre-object/src/module.rs @@ -59,7 +59,8 @@ impl crate::lltype::GcType for Module { /// the anonymous-name sentinel for `pick_builtin`'s default Module /// case (`moduledef.py:106-108`, PyPy `Module(space, None, ...)`) /// in which `Module.__init__` skips the `__name__` setitem. -pub fn w_module_new(name: &str) -> PyObjectRef { +/// +fn module_value(name: &str) -> Module { // `pypy/interpreter/module.py:18 Module.__init__` opens // `w_dict = space.newdict(module=True)` per `dictmultiobject.py:440-451 // _newdict(module=True)`, which lands on `W_ModuleDictObject` @@ -74,14 +75,32 @@ pub fn w_module_new(name: &str) -> PyObjectRef { crate::dictmultiobject::w_dict_setitem_str(w_dict, "__name__", crate::w_str_new(name)); } } - crate::lltype::malloc_typed(Module { + Module { ob_header: PyObject { ob_type: &MODULE_TYPE as *const PyType, w_class: get_instantiate(&MODULE_TYPE), }, name: name_box, w_dict, - }) as PyObjectRef + } +} + +/// Bootstrap/import allocation. Native owners of these modules still keep +/// stable raw pointers, so retain the legacy immortal allocation until those +/// owners are migrated to ordinary GC roots. +pub fn w_module_new(name: &str) -> PyObjectRef { + crate::lltype::malloc_typed(module_value(name)) as PyObjectRef +} + +/// Python-visible module allocation (`types.ModuleType.__new__`). +/// +/// The holder belongs to the collector, carries `W_MODULE_GC_TYPE_ID`, and is +/// traced through `W_MODULE_GC_PTR_OFFSETS`, so a minor collection forwards +/// `w_dict`. The allocation itself is stable (non-moving old gen): module +/// objects flow into JIT traces as promoted constants (globals lookups, +/// attribute caches), and a baked pointer must survive later collections. +pub fn w_module_new_managed(name: &str) -> PyObjectRef { + crate::lltype::malloc_typed_stable(module_value(name)) as PyObjectRef } /// Allocate a `Module` aliasing a user-supplied `W_DictObject`. @@ -145,8 +164,7 @@ pub unsafe fn w_module_get_name(obj: PyObjectRef) -> &'static str { /// Replace the module name (`module.py:24` re-seeding). Used by /// `module.__init__(name, doc)` after `module.__new__` allocates an -/// anonymous module. A Module holder is immortal (`malloc_typed`, never -/// swept), so its `name` stays a `malloc_raw` box that no collector reclaims; +/// anonymous module. `name` stays a `malloc_raw` box outside the collector; /// free the previous box before installing the new one to avoid leaking it. /// /// # Safety diff --git a/pyre/pyrex/src/lib.rs b/pyre/pyrex/src/lib.rs index 9ca0a3b9b6d..d9e104dbeec 100644 --- a/pyre/pyrex/src/lib.rs +++ b/pyre/pyrex/src/lib.rs @@ -692,68 +692,27 @@ pub(crate) fn import_site( /// /// `_bootstrap._setup` (importlib/_bootstrap.py) reads the bootstrap builtins /// `_thread`/`_warnings`/`_weakref` from `sys.modules`, so import them first to -/// seed `sys.modules` (otherwise `_setup` falls into `_builtin_from_name` → -/// `_imp.create_builtin`, which the native importer does not implement). -fn init_importlib_bootstrap( +/// seed `sys.modules`; a name already present skips `_builtin_from_name` and +/// keeps the natively-registered module object authoritative. +pub(crate) fn init_importlib_bootstrap( canonical: pyre_object::PyObjectRef, ec_ptr: *const pyre_interpreter::PyExecutionContext, ) -> Result<(), pyre_interpreter::PyError> { let import = |name: &str| importing::importhook(name, canonical, pyre_object::PY_NULL, 0, ec_ptr); - let call_checked = |func: pyre_object::PyObjectRef, - args: &[pyre_object::PyObjectRef]| - -> Result { - let res = pyre_interpreter::call_function(func, args); - if res.is_null() { - return Err( - pyre_interpreter::call::take_call_error().unwrap_or_else(|| { - pyre_interpreter::PyError::new( - pyre_interpreter::PyErrorKind::RuntimeError, - "importlib bootstrap _install returned NULL without an exception", - ) - }), - ); - } - Ok(res) - }; for name in ["_thread", "_warnings", "_weakref"] { import(name)?; } - let sys_mod = import("sys")?; - let imp_mod = import("_imp")?; + import("sys")?; + import("_imp")?; + // Importing the bootstrap module fires `install_importlib_bootstrap` + // (the native load hook) as its body finishes: `_install(sys, _imp)`, + // `_install_external_importers()` — which imports and links + // `_frozen_importlib_external` — and the `_frozen_importlib` alias. + // A cached module skips the hook, so running this again (`-i` reaches + // the REPL after `run_source`) does not re-append the importers. import("importlib._bootstrap")?; - import("importlib._bootstrap_external")?; - let bootstrap = importing::get_sys_module("importlib._bootstrap").ok_or_else(|| { - pyre_interpreter::PyError::new( - pyre_interpreter::PyErrorKind::RuntimeError, - "importlib._bootstrap missing from sys.modules after import", - ) - })?; - let bootstrap_ext = - importing::get_sys_module("importlib._bootstrap_external").ok_or_else(|| { - pyre_interpreter::PyError::new( - pyre_interpreter::PyErrorKind::RuntimeError, - "importlib._bootstrap_external missing from sys.modules after import", - ) - })?; - - // init_importlib: importlib._bootstrap._install(sys, _imp) - let install = pyre_interpreter::getattr(bootstrap, pyre_object::w_str_new("_install"))?; - call_checked(install, &[sys_mod, imp_mod])?; - // init_importlib_external: importlib._bootstrap_external._install(_bootstrap) - let install_ext = pyre_interpreter::getattr(bootstrap_ext, pyre_object::w_str_new("_install"))?; - call_checked(install_ext, &[bootstrap])?; - // importlib/__init__.py: _bootstrap._bootstrap_external = _bootstrap_external - // (`_install` only calls `_set_bootstrap_module`; the reverse link that - // `ModuleSpec.cached` / `_get_cached` reads is wired by importlib's package - // init, which the native importer does not run for `_bootstrap`). - pyre_interpreter::baseobjspace::setattr_str(bootstrap, "_bootstrap_external", bootstrap_ext)?; - // The bootstrap modules are exposed under their frozen names; modules such - // as `zipimport` import `_frozen_importlib{,_external}` directly. Register - // the same objects under the frozen names once `_install` has wired them. - importing::set_sys_module("_frozen_importlib", bootstrap); - importing::set_sys_module("_frozen_importlib_external", bootstrap_ext); Ok(()) } @@ -864,6 +823,15 @@ fn run_source(source: &str, mode: Mode, filename: &str, no_site: bool) { // into `sys.path` before `site` and user code read it. let _ = importing::importhook("sys", canonical, pyre_object::PY_NULL, 0, ec_ptr); + // pylifecycle.c init_importlib before site: install the importlib + // bootstrap so `builtins.__import__` routes imports through + // `sys.meta_path` / `sys.path_hooks` from the first user statement. + // A failure (no reachable stdlib) is non-fatal — the native importer + // keeps serving imports, the minimal-importer role. + if let Err(e) = init_importlib_bootstrap(canonical, ec_ptr) { + eprintln!("pyre: importlib bootstrap failed: {}", e.message_text()); + } + import_site(no_site, canonical, ec_ptr); match eval_with_jit(&mut frame) { diff --git a/pyre/pyrex/src/repl.rs b/pyre/pyrex/src/repl.rs index 256bb6979b8..96e10587db5 100644 --- a/pyre/pyrex/src/repl.rs +++ b/pyre/pyrex/src/repl.rs @@ -87,6 +87,11 @@ pub fn run_repl(quiet: bool, no_site: bool) { }; configure_sys_for_repl(sys_module); + // pylifecycle.c init_importlib before site — see run_source. + if let Err(e) = crate::init_importlib_bootstrap(canonical, Rc::as_ptr(&execution_context)) { + eprintln!("pyre: importlib bootstrap failed: {}", e.message_text()); + } + crate::import_site(no_site, canonical, Rc::as_ptr(&execution_context)); let runtime = ReplRuntime {