Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 35 additions & 37 deletions majit/majit-translate/src/front/mir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10595,16 +10595,16 @@ impl<'a> Lowering<'a> {
None
}

/// `msg.into()` on a generic parameter bound `T: Into<String>`
/// a `CallKind::Trait` whose trait ref is a *clause* (no resolved
/// impl for [`Self::blanket_into_devirt`] to read). The blanket
/// `impl<T, U: From<T>> Into<U> 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<String>` or
/// `T: Into<Wtf8Buf>` — a `CallKind::Trait` whose trait ref is a
/// *clause* (no resolved impl for [`Self::blanket_into_devirt`] to
/// read). The blanket `impl<T, U: From<T>> Into<U> 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) = &reg.kind else {
return false;
Expand All @@ -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,
Expand Down Expand Up @@ -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<String>` 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<String>` / `impl Into<Wtf8Buf>`
/// 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],
Expand All @@ -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],
Expand All @@ -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
Expand Down Expand Up @@ -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<String> {
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
Expand Down
19 changes: 17 additions & 2 deletions pyre/pyre-interpreter/src/baseobjspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PyObjectRef>; 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() {
Expand Down Expand Up @@ -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<PyObjectRef>`.
#[majit_macros::dont_look_inside]
pub(crate) unsafe fn lookup_in_type_wtf8_uncached(
w_type: PyObjectRef,
name: &Wtf8,
Expand Down
41 changes: 41 additions & 0 deletions pyre/pyre-interpreter/src/jit_fnaddr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PyObjectRef>`.
Expand Down Expand Up @@ -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<pyre_object::PyObjectRef> = 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
Expand Down
55 changes: 46 additions & 9 deletions pyre/pyre-object/src/dictmultiobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PyObjectRef> {
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))
}
}

Expand Down Expand Up @@ -3473,9 +3508,10 @@ pub unsafe fn w_dict_getitem_wtf8_checked(
obj: PyObjectRef,
key: &rustpython_wtf8::Wtf8,
) -> Result<Option<PyObjectRef>, 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))
}
}

Expand All @@ -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);
}
}

Expand Down
Loading