diff --git a/majit/majit-translate/src/front/mir.rs b/majit/majit-translate/src/front/mir.rs index b929a4d3802..e04a70d1360 100644 --- a/majit/majit-translate/src/front/mir.rs +++ b/majit/majit-translate/src/front/mir.rs @@ -10595,16 +10595,16 @@ impl<'a> Lowering<'a> { None } - /// `msg.into()` on a generic parameter bound `T: Into` — - /// a `CallKind::Trait` whose trait ref is a *clause* (no resolved - /// impl for [`Self::blanket_into_devirt`] to read). The blanket - /// `impl> Into for T` makes the result - /// `U::from(self)`; for a string-family target the conversion is - /// identity in the lifted value model (Rust `String` and `&str` - /// both lower to the immutable rpy_string), so the caller may - /// alias the destination to the argument. The callsite's `dest` - /// type *is* the trait ref's target type argument, so it is the - /// only payload field consulted besides the trait identity. + /// `msg.into()` on a generic parameter bound `T: Into` or + /// `T: Into` — a `CallKind::Trait` whose trait ref is a + /// *clause* (no resolved impl for [`Self::blanket_into_devirt`] to + /// read). The blanket `impl> Into for T` makes the + /// result `U::from(self)`; for a string-family target the conversion + /// is identity in the lifted value model (`String`, `&str`, `Wtf8` + /// and `Wtf8Buf` all lower to the immutable rpy_string), so the + /// caller may alias the destination to the argument. The callsite's + /// `dest` type *is* the trait ref's target type argument, so it is + /// the only payload field consulted besides the trait identity. fn trait_clause_into_string_identity(&self, reg: &RegularCall, dest_ty: &TyRef) -> bool { let CallKind::Trait(v) = ®.kind else { return false; @@ -10619,7 +10619,7 @@ impl<'a> Lowering<'a> { .llbc .trait_by_id(trait_id) .is_some_and(|td| td.item_meta.name_path() == "core::convert::Into"); - is_into && tyref_is_string_adt(dest_ty, self.llbc) + is_into && tyref_is_string_value(dest_ty, self.llbc) } /// Whether the FunDecl's first signature input is the given ADT, @@ -12675,14 +12675,16 @@ impl<'a> Lowering<'a> { /// Resolve the trait-spelled `Into::into` (`["Into", "into"]` — a /// generic-parameter receiver, so Charon cannot select the impl at - /// the call site) to its operand when the destination type is - /// `alloc::string::String`. `impl Into` message parameters - /// (the `PyError` constructor family) reach `msg.into()` inside the - /// generic body; the annotation model maps `String` and `str` to - /// the same string value (`project_pyre_field_type` — `s_unicode0`), - /// matching upstream's single string type (`rstr.py`), so the - /// conversion is an identity at the annotation level. Other - /// destination types keep the generic `Call` form. + /// the call site) to its operand when the destination is a + /// string-family value (`alloc::string::String` or the `Wtf8` / + /// `Wtf8Buf` wrappers). `impl Into` / `impl Into` + /// message parameters (the `PyError` constructor family) reach + /// `msg.into()` inside the generic body; the annotation model maps + /// `String`, `str`, `Wtf8` and `Wtf8Buf` to the same string value + /// (`project_pyre_field_type` — `s_unicode0`), matching upstream's + /// single string type (`rstr.py`), so the conversion is an identity + /// at the annotation level. Other destination types keep the + /// generic `Call` form. fn trait_into_string_alias( &self, segments: &[String], @@ -12698,20 +12700,24 @@ impl<'a> Lowering<'a> { let [arg] = args else { return None; }; - let dest_path = self.tyref_adt_name_path(dest_ty)?; - (dest_path == "alloc::string::String").then(|| arg.clone()) + tyref_is_string_value(dest_ty, self.llbc).then(|| arg.clone()) } /// Resolve the WTF-8 string wrappers `Wtf8::new(&str) -> &Wtf8` and - /// `Wtf8Buf::from_string(String) -> Wtf8Buf` to their sole string + /// `Wtf8Buf::from_string(String) -> Wtf8Buf`, and the reverse reinterpret + /// `wtf8_key_as_str_unchecked(&Wtf8) -> &str`, to their sole string /// argument. Rust's `&str` / `String` / `Wtf8` / `Wtf8Buf` all map /// to the single immutable rpy_string value (`project_pyre_field_type` /// — `s_unicode0`, matching upstream's one string type in `rstr.py`), /// so the wrap is an identity at the annotation level; the boxing the /// callers want (`box_str_constant`) happens downstream on the bound - /// value. Both bodies are Opaque in the LLBC (external + /// value. The `Wtf8` bodies are Opaque in the LLBC (external /// `rustpython_wtf8` crate), leaving the generic `Call` permanently - /// unliftable. + /// unliftable; `wtf8_key_as_str_unchecked` is a pyre free fn whose + /// `from_utf8_unchecked` body is likewise left unlifted by folding the + /// call away. It is matched by leaf name because its qualified path + /// (`pyre_object::dictmultiobject::wtf8_key_as_str_unchecked`) differs + /// between the defining-crate LLBC and a cross-crate caller. fn wtf8_string_identity_alias( &self, segments: &[String], @@ -12720,14 +12726,16 @@ impl<'a> Lowering<'a> { let [arg] = args else { return None; }; - matches!( + let is_wrap = matches!( segments, [a, b] if matches!( (a.as_str(), b.as_str()), ("Wtf8", "new") | ("Wtf8Buf", "from_string") ) - ) - .then(|| arg.clone()) + ); + let is_str_reinterpret = + matches!(segments, [.., leaf] if leaf == "wtf8_key_as_str_unchecked"); + (is_wrap || is_str_reinterpret).then(|| arg.clone()) } /// The unwrapped ADT body `Value` a [`TyRef`] resolves to, following @@ -12759,16 +12767,6 @@ impl<'a> Lowering<'a> { Some(v) } - /// The fully-qualified `name_path()` of the ADT a [`TyRef`] - /// resolves to, following `Deduplicated` / `HashConsedValue` - /// wrapper layers. `None` for non-ADT shapes. - fn tyref_adt_name_path(&self, ty: &TyRef) -> Option { - let v = self.tyref_adt_body(ty)?; - let def_id = inline_adt_def_id(v)?; - let td = self.llbc.type_by_id(def_id)?; - Some(td.item_meta.name_path()) - } - /// The struct-root canonical name of `ty` when it resolves to a /// zero-field unit struct — the shape of the prebuilt dict-strategy /// singletons (`EmptyDictStrategy`, `ObjectDictStrategy`, …). The diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 84595c59e7d..43c05140f39 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -7335,7 +7335,14 @@ pub(crate) fn exception_attr_get(obj: PyObjectRef, name: &str) -> PyResult { Ok(pyre_object::PY_NULL) } -fn object_getattr_miss(obj: PyObjectRef, name: &str, call_getattr: bool) -> PyResult { +/// The attribute-miss / special-attribute path of `getattribute`, reached only +/// after the fast descriptor and instance-dict lookups return nothing (a hot +/// attribute that resolves never enters here). Its `[Option; 2]` +/// metaclass walks (`.iter().flatten()`), array indexing, and `__getattr__` +/// callback are opaque to the annotator; it returns the blessed `PyResult` +/// carrier, so the whole cold path is residualised behind one boundary. +#[majit_macros::dont_look_inside] +pub(crate) fn object_getattr_miss(obj: PyObjectRef, name: &str, call_getattr: bool) -> PyResult { if name == "__dict__" && unsafe { is_module(obj) } { let dict = unsafe { pyre_object::w_module_get_w_dict(obj) }; if !dict.is_null() { @@ -9031,7 +9038,15 @@ pub(crate) unsafe fn lookup_where_pair_wtf8_uncached( lookup_where_wtf8(w_type, name) } -#[inline] +/// Mirror of the `&str` twin [`lookup_in_type_where_uncached`] and the +/// `objspace/std/mapdict.rs` slot residuals. The inner +/// `lookup_where_pair_wtf8_uncached` is already `#[dont_look_inside]`, +/// returning `Option<(PyObjectRef, PyObjectRef)>`; tracing this `.map` +/// projection over that tuple-payload `Option` walls the annotator on a +/// classdef-less payload read, so the projection also carries +/// `#[dont_look_inside]` and residualises to the single-word +/// `Option`. +#[majit_macros::dont_look_inside] pub(crate) unsafe fn lookup_in_type_wtf8_uncached( w_type: PyObjectRef, name: &Wtf8, diff --git a/pyre/pyre-interpreter/src/jit_fnaddr.rs b/pyre/pyre-interpreter/src/jit_fnaddr.rs index 46854eb2f0b..5d0688b54ab 100644 --- a/pyre/pyre-interpreter/src/jit_fnaddr.rs +++ b/pyre/pyre-interpreter/src/jit_fnaddr.rs @@ -895,6 +895,22 @@ pub fn jit_trace_fnaddrs() -> Vec<(&'static str, i64)> { "pyre_object::w_dict_setitem_str", pyre_object::dictmultiobject::w_dict_setitem_str as *const (), ); + // The wtf8-keyed dict adapters residualise their fallible `Wtf8::as_str` + // dispatch: `wtf8_key_is_utf8` is the `bool` validity probe, and + // `wtf8_surrogate_key_str_object` wraps the cold lone-surrogate + // `to_wtf8_buf` + `w_str_from_wtf8` into one objectptr call. + push_alias_pair( + &mut entries, + "pyre_object::dictmultiobject::wtf8_key_is_utf8", + "pyre_object::wtf8_key_is_utf8", + pyre_object::dictmultiobject::wtf8_key_is_utf8 as *const (), + ); + push_alias_pair( + &mut entries, + "pyre_object::dictmultiobject::wtf8_surrogate_key_str_object", + "pyre_object::wtf8_surrogate_key_str_object", + pyre_object::dictmultiobject::wtf8_surrogate_key_str_object as *const (), + ); // The typed int/bytes dict-storage leaves residualise their // `IndexMap::{insert,get}` (an external-crate heap store/lookup the tracer // cannot model): the stores return `()`, the lookups `Option`. @@ -1051,6 +1067,31 @@ pub fn jit_trace_fnaddrs() -> Vec<(&'static str, i64)> { "pyre_interpreter::lookup_in_type_where_uncached", lookup_in_type_where_uncached as *const (), ); + // #346: the WTF-8 twin of the projection above. `lookup_in_type_wtf8_uncached` + // maps the tuple-payload `Option<(PyObjectRef, PyObjectRef)>` of the already + // opaque `lookup_where_pair_wtf8_uncached` to the value; tracing that `.map` + // walls the annotator on the tuple's classdef-less payload, so it is + // `#[dont_look_inside]` and residualises to the single-word `Option`. + let lookup_in_type_wtf8_uncached: unsafe fn( + pyre_object::PyObjectRef, + &rustpython_wtf8::Wtf8, + ) -> Option = crate::baseobjspace::lookup_in_type_wtf8_uncached; + push_alias_pair( + &mut entries, + "pyre_interpreter::baseobjspace::lookup_in_type_wtf8_uncached", + "pyre_interpreter::lookup_in_type_wtf8_uncached", + lookup_in_type_wtf8_uncached as *const (), + ); + // #346: the getattribute attribute-miss / special-attribute path. Its cold + // `[Option; 2].iter().flatten()` metaclass walks and array indexing are + // opaque to the annotator, so it is `#[dont_look_inside]` and residualises + // over the blessed `PyResult` carrier; bind its `fn` by qualified path. + push_alias_pair( + &mut entries, + "pyre_interpreter::baseobjspace::object_getattr_miss", + "pyre_interpreter::object_getattr_miss", + crate::baseobjspace::object_getattr_miss as *const (), + ); // `gc_interp::enabled` reads (and lazily inits) the `STATE` atomic, and // `longobject::bigint_gc_type_id` / // `dictmultiobject::dict_view_iterator_gc_type_id` read the diff --git a/pyre/pyre-object/src/dictmultiobject.rs b/pyre/pyre-object/src/dictmultiobject.rs index 17dbaabfcc2..c1ff842cdc1 100644 --- a/pyre/pyre-object/src/dictmultiobject.rs +++ b/pyre/pyre-object/src/dictmultiobject.rs @@ -3433,13 +3433,48 @@ pub unsafe fn w_dict_delitem_str_no_proxy(obj: PyObjectRef, key: &str) -> bool { /// /// # Safety /// `obj` must point to a valid `W_DictObject`. +/// Whether `key`'s WTF-8 bytes are valid UTF-8 (no lone surrogate). +/// +/// Residualised (`dont_look_inside`) so the `Result<&str, Utf8Error>` that +/// `as_str` returns never crosses the trace boundary; the residual is a +/// single-word `bool`. The wtf8-keyed dict adapters branch on this in place +/// of `match key.as_str()`. +#[majit_macros::dont_look_inside] +pub unsafe fn wtf8_key_is_utf8(key: &rustpython_wtf8::Wtf8) -> bool { + key.as_str().is_ok() +} + +/// Reinterpret `key`'s bytes as `&str` without re-validating. +/// +/// Only ever called on the branch [`wtf8_key_is_utf8`] already validated, so +/// the reinterpret is infallible. Folded to identity in +/// `front/mir.rs::wtf8_string_identity_alias` (`&Wtf8` and `&str` share the +/// one immutable string value), so the call disappears and this body is never +/// lifted. +pub unsafe fn wtf8_key_as_str_unchecked(key: &rustpython_wtf8::Wtf8) -> &str { + core::str::from_utf8_unchecked(key.as_bytes()) +} + +/// Wrap a lone-surrogate `key` into a `W_UnicodeObject` for the object-keyed +/// slow path. +/// +/// Residualised (`dont_look_inside`) as one objectptr call wrapping both +/// `to_wtf8_buf` (external, unliftable) and `w_str_from_wtf8`. Deliberately +/// not a global `to_wtf8_buf` identity fold: other callers mutate the owned +/// `Wtf8Buf`. +#[majit_macros::dont_look_inside] +pub unsafe fn wtf8_surrogate_key_str_object(key: &rustpython_wtf8::Wtf8) -> PyObjectRef { + crate::w_str_from_wtf8(key.to_wtf8_buf()) +} + pub unsafe fn w_dict_getitem_wtf8( obj: PyObjectRef, key: &rustpython_wtf8::Wtf8, ) -> Option { - match key.as_str() { - Ok(s) => w_dict_getitem_str(obj, s), - Err(_) => w_dict_lookup(obj, crate::w_str_from_wtf8(key.to_wtf8_buf())), + if wtf8_key_is_utf8(key) { + w_dict_getitem_str(obj, wtf8_key_as_str_unchecked(key)) + } else { + w_dict_lookup(obj, wtf8_surrogate_key_str_object(key)) } } @@ -3473,9 +3508,10 @@ pub unsafe fn w_dict_getitem_wtf8_checked( obj: PyObjectRef, key: &rustpython_wtf8::Wtf8, ) -> Result, DictKeyError> { - match key.as_str() { - Ok(s) => w_dict_getitem_str_checked(obj, s), - Err(_) => w_dict_lookup_checked(obj, crate::w_str_from_wtf8(key.to_wtf8_buf())), + if wtf8_key_is_utf8(key) { + w_dict_getitem_str_checked(obj, wtf8_key_as_str_unchecked(key)) + } else { + w_dict_lookup_checked(obj, wtf8_surrogate_key_str_object(key)) } } @@ -3492,9 +3528,10 @@ pub unsafe fn w_dict_setitem_wtf8( key: &rustpython_wtf8::Wtf8, value: PyObjectRef, ) { - match key.as_str() { - Ok(s) => w_dict_setitem_str(obj, s, value), - Err(_) => w_dict_store(obj, crate::w_str_from_wtf8(key.to_wtf8_buf()), value), + if wtf8_key_is_utf8(key) { + w_dict_setitem_str(obj, wtf8_key_as_str_unchecked(key), value); + } else { + w_dict_store(obj, wtf8_surrogate_key_str_object(key), value); } }