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
4 changes: 3 additions & 1 deletion pyre/bench/synth/closure_per_call.wasm.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_rolled_back_with_effects=0
guard_failures=470
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=468
internal_compile_panics=0
loops_aborted=0
loops_compiled=4
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_rolled_back_with_effects=0
guard_failures=820
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=819
internal_compile_panics=0
loops_aborted=0
loops_compiled=17
4 changes: 3 additions & 1 deletion pyre/bench/synth/gc_iterator_source_drop.wasm.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_rolled_back_with_effects=0
guard_failures=613
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=614
internal_compile_panics=0
loops_aborted=0
loops_compiled=4
8 changes: 5 additions & 3 deletions pyre/bench/synth/pickle_ctor_args.cranelift.jitstats
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
bridges_compiled=1
bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_rolled_back_with_effects=0
guard_failures=201
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=1
internal_compile_panics=0
loops_aborted=0
loops_compiled=4
loops_compiled=2
4 changes: 3 additions & 1 deletion pyre/bench/synth/pickle_ctor_args.dynasm.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_rolled_back_with_effects=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=1
internal_compile_panics=0
loops_aborted=0
loops_compiled=4
loops_compiled=2
4 changes: 3 additions & 1 deletion pyre/bench/synth/pickle_ctor_args.wasm.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_rolled_back_with_effects=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=1
internal_compile_panics=0
loops_aborted=0
loops_compiled=4
loops_compiled=2
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_rolled_back_with_effects=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=656
internal_compile_panics=0
loops_aborted=1
loops_compiled=36
loops_compiled=31
6 changes: 4 additions & 2 deletions pyre/bench/synth/pickle_terminal_raise_resume.dynasm.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_rolled_back_with_effects=0
guard_failures=463
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=354
internal_compile_panics=0
loops_aborted=1
loops_compiled=36
loops_compiled=31
6 changes: 4 additions & 2 deletions pyre/bench/synth/pickle_terminal_raise_resume.wasm.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_rolled_back_with_effects=5
guard_failures=464
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=355
internal_compile_panics=0
loops_aborted=13
loops_compiled=73
loops_compiled=68
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_rolled_back_with_effects=0
guard_failures=649
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=648
internal_compile_panics=0
loops_aborted=0
loops_compiled=3
2 changes: 2 additions & 0 deletions pyre/bench/synth/type_metatype_method_call.cranelift.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_rolled_back_with_effects=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=1
internal_compile_panics=0
loops_aborted=0
Expand Down
2 changes: 2 additions & 0 deletions pyre/bench/synth/type_metatype_method_call.dynasm.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_rolled_back_with_effects=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=1
internal_compile_panics=0
loops_aborted=0
Expand Down
4 changes: 3 additions & 1 deletion pyre/bench/synth/type_metatype_method_call.wasm.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_rolled_back_with_effects=0
guard_failures=2
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=1
internal_compile_panics=0
loops_aborted=0
loops_compiled=2
161 changes: 105 additions & 56 deletions pyre/pyre-interpreter/src/baseobjspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5739,21 +5739,29 @@ fn getattr_str_impl(obj: PyObjectRef, name: &str, call_getattr: bool, suppress:
// lookup without re-dispatching to the override.
if let Some(w_metatype) = crate::typedef::r#type(obj) {
if let Some(slot) = getattribute_if_not_from_object(w_metatype.as_ptr()) {
let name_obj = w_str_new(name);
// objspace.py:666 — bind the metaclass `__getattribute__`
// through `__get__` and call it with the attribute name.
match get_and_call_function(slot, obj, w_metatype.as_ptr(), &[name_obj]) {
Ok(v) => return Ok(v),
Err(e) if e.kind == PyErrorKind::AttributeError => {
return type_getattr_hook_or_err(
obj,
&[Some(w_metatype.as_ptr()), None],
name,
e,
call_getattr,
);
// typeobject.py:811-828 `W_TypeObject.descr_getattribute`
// is the body inlined by `object_getattr_miss` below. Keep
// a metaclass override on the descriptor-call path, but do
// not wrap and then unwrap an already validated name merely
// to re-enter that same canonical body.
if !is_type_getattribute_descr(slot) {
let name_obj = w_str_new(name);
// objspace.py:666 — bind the metaclass
// `__getattribute__` through `__get__` and call it with
// the attribute name.
match get_and_call_function(slot, obj, w_metatype.as_ptr(), &[name_obj]) {
Ok(v) => return Ok(v),
Err(e) if e.kind == PyErrorKind::AttributeError => {
return type_getattr_hook_or_err(
obj,
&[Some(w_metatype.as_ptr()), None],
name,
e,
call_getattr,
);
}
Err(e) => return Err(e),
}
Err(e) => return Err(e),
}
}
}
Expand Down Expand Up @@ -6280,34 +6288,55 @@ pub(crate) unsafe fn object_delattr_surrogate(
}
}

/// `raiseattrerror` for a lone-surrogate name. descroperation.py:58-64
/// renders the name with `%R` (its repr), so a lone surrogate prints as
/// `\udcXX` rather than a lossy replacement char. The repr already
/// supplies the surrounding quotes (`format_wtf8_repr`), matching the
/// `%R` substitution in `"'%T' object has no attribute %R"`.
/// `raiseattrerror` for a lone-surrogate name. descroperation.py:58-64 keeps
/// the original name object in the formatted AttributeError, so build the
/// exception argument as WTF-8 instead of reducing it to a Rust string.
fn attr_error_wtf8(obj: PyObjectRef, name: &Wtf8) -> PyError {
let tp_name = unsafe {
match crate::typedef::r#type(obj) {
Some(tp) => pyre_object::w_type_get_name(tp.as_ptr()).to_string(),
None => (*(*obj).ob_type).name.to_string(),
}
};
let name_repr = crate::display::format_wtf8_repr(name);
let mut err = PyError::new(
PyErrorKind::AttributeError,
format!("'{tp_name}' object has no attribute {name_repr}"),
let mut message = Wtf8Buf::from_string(format!(
"{} has no attribute '",
missing_attribute_subject(obj)
));
message.push_wtf8(name);
message.push_wtf8(Wtf8::new("'"));
Comment on lines +6295 to +6300

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve repr escaping in surrogate AttributeErrors

Format the name with its WTF-8-aware repr instead of inserting it verbatim. This path also accepts names containing a lone surrogate plus quotes, backslashes, or control characters; for those inputs the new message is malformed (for example, an embedded quote is left unescaped), while PyPy's %R path and the removed format_wtf8_repr call produce a valid escaped representation.

AGENTS.md reference: AGENTS.md:L231-L233

Useful? React with 👍 / 👎.


let _roots = pyre_object::gc_roots::push_roots();
let obj_slot = pyre_object::gc_roots::shadow_stack_len();
pyre_object::gc_roots::pin_root(obj);
let w_name = pyre_object::w_str_from_wtf8(name.to_wtf8_buf());
let name_slot = pyre_object::gc_roots::shadow_stack_len();
pyre_object::gc_roots::pin_root(w_name);
let exc = pyre_object::interp_exceptions::w_exception_new_wtf8(
pyre_object::interp_exceptions::ExcKind::AttributeError,
&message,
);
err.w_name_context = pyre_object::w_str_from_wtf8(name.to_wtf8_buf());
err.w_obj_context = obj;
err
unsafe {
pyre_object::interp_exceptions::w_exception_set_name(
exc,
pyre_object::gc_roots::shadow_stack_get(name_slot),
);
pyre_object::interp_exceptions::w_exception_set_attr_obj(
exc,
pyre_object::gc_roots::shadow_stack_get(obj_slot),
);
PyError::from_exc_object(exc)
}
}

/// `object.__getattribute__` terminal — the default descriptor protocol
/// without the user `__getattribute__` override check.
pub fn object_getattribute(obj: PyObjectRef, name: &str) -> PyResult {
unsafe {
if is_instance(obj) {
let w_type = w_instance_get_type(obj);
if is_instance(obj) || is_type(obj) {
// descroperation.py:88-112 `Object.descr__getattribute__` uses
// `space.lookup(w_obj, name)`, hence the receiver's class (the
// metatype for a type object), and reads only
// `w_obj.getdictvalue`, never the receiver type's own MRO.
let instance = is_instance(obj);
let w_type = if instance {
w_instance_get_type(obj)
} else {
crate::typedef::r#type(obj).map_or(PY_NULL, |p| p.as_ptr())
};
let w_descr = lookup_in_type_where(w_type, name);
if let Some(descr) = w_descr {
if is_data_descr(descr) {
Expand All @@ -6316,13 +6345,15 @@ pub fn object_getattribute(obj: PyObjectRef, name: &str) -> PyResult {
}
}
}
// Instance dict is the sole authority for instance attributes:
// read the mapdict node directly (getdictvalue, mapdict.py:846-847)
// rather than materialising the MapDictStrategy `__dict__` view, which
// MapDictStrategy.getitem_str (mapdict.py:1168-1175) delegates to
// anyway. No side-table fallback.
let value =
crate::objspace::std::mapdict::instance_node_getdictvalue(obj, Wtf8::new(name));
// The receiver namespace is the sole authority at this stage.
// Read a user instance's mapdict node directly (getdictvalue,
// mapdict.py:846-847); a type receiver uses only its canonical
// dictionary, which is the corresponding `getdictvalue` result.
let value = if instance {
crate::objspace::std::mapdict::instance_node_getdictvalue(obj, Wtf8::new(name))
} else {
crate::type_dict_lookup(obj, name)
};
if let Some(value) = value {
return Ok(value);
}
Expand All @@ -6339,7 +6370,7 @@ pub fn object_getattribute(obj: PyObjectRef, name: &str) -> PyResult {
}
return Ok(descr);
}
if name == "__class__" {
if instance && name == "__class__" {
return Ok(w_type);
}
// descroperation.py:88 — object.__getattribute__ raises
Expand All @@ -6354,12 +6385,19 @@ pub fn object_getattribute(obj: PyObjectRef, name: &str) -> PyResult {
));
}
}
// Non-instance receiver (module, type, builtin object): the pure descriptor
// protocol with no `__getattr__` fallback — that belongs to space.getattr,
// not the bare object.__getattribute__ slot (descroperation.py:88).
// Remaining non-instance receivers (module and builtin objects): preserve
// their pure descriptor protocol with no `__getattr__` fallback — that
// belongs to space.getattr, not the bare object.__getattribute__ slot
// (descroperation.py:88).
getattr_str_impl(obj, name, false, false)
}

/// typeobject.py:811-828 `W_TypeObject.descr_getattribute` — the canonical
/// metatype-data-descriptor, class-MRO, metatype-non-data-descriptor lookup.
pub(crate) fn type_getattribute(obj: PyObjectRef, name: &str) -> PyResult {
object_getattr_miss(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 {
Expand Down Expand Up @@ -9528,6 +9566,13 @@ unsafe fn is_object_getattribute_descr(w_descr: PyObjectRef) -> bool {
}
}

/// `typeobject.py:1322` — identity anchor for the canonical
/// `W_TypeObject.descr_getattribute` wrapper installed on `type`.
unsafe fn is_type_getattribute_descr(w_descr: PyObjectRef) -> bool {
lookup_in_type_where(crate::typedef::w_type(), "__getattribute__")
.is_some_and(|d| std::ptr::eq(w_descr, d))
}

/// module.py `Module.descr_getattribute` is the default attribute slot for
/// module objects. Module subclasses inherit it unless they explicitly
/// replace `__getattribute__`.
Expand Down Expand Up @@ -11727,17 +11772,7 @@ pub(crate) fn raiseattrerror(
"'{tp_name}' object attribute '{name}' is read-only"
));
}
let subject = unsafe {
if is_type(obj) {
format!("type object '{}'", pyre_object::w_type_get_name(obj))
} else {
let tp_name = match crate::typedef::r#type(obj) {
Some(tp) => pyre_object::w_type_get_name(tp.as_ptr()).to_string(),
None => (*(*obj).ob_type).name.to_string(),
};
format!("'{}' object", tp_name)
}
};
let subject = missing_attribute_subject(obj);
// `object.c _PyObject_GenericSetAttrWithDict` appends the suffix when the
// receiver has no dict *slot*. A raising `getdict` says nothing about
// whether the object could hold a dict, so the suffix is only added on a
Expand All @@ -11754,6 +11789,20 @@ pub(crate) fn raiseattrerror(
)
}

fn missing_attribute_subject(obj: PyObjectRef) -> String {
unsafe {
if is_type(obj) {
format!("type object '{}'", pyre_object::w_type_get_name(obj))
} else {
let tp_name = match crate::typedef::r#type(obj) {
Some(tp) => pyre_object::w_type_get_name(tp.as_ptr()).to_string(),
None => (*(*obj).ob_type).name.to_string(),
};
format!("'{tp_name}' object")
}
}
}

/// Delete an attribute: `del obj.name`.
///
/// PyPy: descroperation.py descr__delattr__
Expand Down
4 changes: 4 additions & 0 deletions pyre/pyre-interpreter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,10 @@ pub fn all_subclass_range_aliases() -> Vec<pyre_object::pyobject::SubclassRangeA
158,
typed::<crate::module::__pypy__::interp_buffer::bufferable_impl::W_Bufferable>(),
),
// `_io.BytesIO` registers after the `rbigint` result pair, which takes
// 159 as a bare `with_gc_ptrs` id and carries no vtable of its own.
subclass_range_alias(160, typed::<crate::module::_io::W_BytesIO>()),
subclass_range_alias(161, typed::<crate::module::_io::W_StringIO>()),
]
}

Expand Down
Loading
Loading