From 3ea880f2875c3a8a8272b14f92f28149366aac1a Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 12 Aug 2026 12:10:21 +0900 Subject: [PATCH 1/3] majit: alias generic Into message conversions to their operand The two frontend identity gates for a generic-clause `msg.into()` (`trait_clause_into_string_identity`, `trait_into_string_alias`) accepted only `alloc::string::String` as the destination. The `PyError` constructor family takes `impl Into`, whose `.into()` targets `Wtf8Buf`, so both gates rejected it and the call lowered to an unregistered `["Into", "into"]`, failing the lift of `PyError::new` / `type_error` and every graph raising through them. Widen both gates to `tyref_is_string_value` (`String` / `Wtf8` / `Wtf8Buf` / `str`, all projecting to the single immutable `s_unicode0`). Remove the now-unused `tyref_adt_name_path`. Prepass census (b9ac6970b13): phaseA 1709 -> 1381, phaseB 14 -> 4; 328 graphs lift, 0 new failures. cargo test --features dynasm clean; check.py bit-exact dynasm 420/420, cranelift 419/419, wasm 414/414. Assisted-by: Claude --- majit/majit-translate/src/front/mir.rs | 53 +++++++++++--------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/majit/majit-translate/src/front/mir.rs b/majit/majit-translate/src/front/mir.rs index b929a4d3802..45ed6d41c45 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,8 +12700,7 @@ 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 @@ -12759,16 +12760,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 From 5735933d692bcaeae226b2f9d1cca05da742946e Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 12 Aug 2026 19:59:22 +0900 Subject: [PATCH 2/3] jit: residualise the WTF-8 dict-key split and type-lookup projection Three WTF-8 dict adapters (w_dict_getitem_wtf8, w_dict_getitem_wtf8_checked, w_dict_setitem_wtf8) branched on `key.as_str()`, whose `Result<&str, Utf8Error>` cannot cross the residual-call boundary. Decompose each into a `#[dont_look_inside]` validity probe (wtf8_key_is_utf8), an identity-folded reinterpret (wtf8_key_as_str_unchecked, folded to its receiver by wtf8_string_identity_alias), and a `#[dont_look_inside]` lone-surrogate key object (wtf8_surrogate_key_str_object). Mark lookup_in_type_wtf8_uncached `#[dont_look_inside]`, matching its `&str` twin lookup_in_type_where_uncached: its `.map` over the opaque lookup_where_pair_wtf8_uncached's `Option<(PyObjectRef, PyObjectRef)>` kept a tuple-payload projection on the trace surface. It now residualises to the single-word `Option`. Register the three new residual helpers in jit_fnaddr. Assisted-by: Claude --- majit/majit-translate/src/front/mir.rs | 19 +++++--- pyre/pyre-interpreter/src/baseobjspace.rs | 10 ++++- pyre/pyre-interpreter/src/jit_fnaddr.rs | 31 +++++++++++++ pyre/pyre-object/src/dictmultiobject.rs | 55 +++++++++++++++++++---- 4 files changed, 99 insertions(+), 16 deletions(-) diff --git a/majit/majit-translate/src/front/mir.rs b/majit/majit-translate/src/front/mir.rs index 45ed6d41c45..e04a70d1360 100644 --- a/majit/majit-translate/src/front/mir.rs +++ b/majit/majit-translate/src/front/mir.rs @@ -12704,15 +12704,20 @@ impl<'a> Lowering<'a> { } /// 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], @@ -12721,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 diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 84595c59e7d..923b682e520 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -9031,7 +9031,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..b3358b74d97 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,21 @@ 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 (), + ); // `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); } } From bd4acf94387ca4a20a1ace5bdb738a1a22e9f60b Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 12 Aug 2026 23:19:22 +0900 Subject: [PATCH 3/3] jit: residualise the getattribute attribute-miss path Mark `object_getattr_miss` `#[dont_look_inside]` and register it in `jit_fnaddr`. It is the miss / special-attribute path of `getattribute`, reached only after the fast descriptor and instance-dict lookups return nothing. Its `[Option; 2].iter().flatten()` metaclass walks and array indexing are opaque to the annotator; it returns the blessed `PyResult` carrier, so the whole cold path residualises behind one boundary. The prepass census `Flatten::next` fallback clears; the getattr graphs relocate to their next wall rather than lifting (0 lift). Assisted-by: Claude --- pyre/pyre-interpreter/src/baseobjspace.rs | 9 ++++++++- pyre/pyre-interpreter/src/jit_fnaddr.rs | 10 ++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 923b682e520..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() { diff --git a/pyre/pyre-interpreter/src/jit_fnaddr.rs b/pyre/pyre-interpreter/src/jit_fnaddr.rs index b3358b74d97..5d0688b54ab 100644 --- a/pyre/pyre-interpreter/src/jit_fnaddr.rs +++ b/pyre/pyre-interpreter/src/jit_fnaddr.rs @@ -1082,6 +1082,16 @@ pub fn jit_trace_fnaddrs() -> Vec<(&'static str, i64)> { "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