diff --git a/AGENTS.md b/AGENTS.md index 80a79d61c..ae24d4167 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -161,7 +161,7 @@ Run `./scripts/whats_left.py` to get a list of unimplemented methods, which is h - Do not delete or rewrite existing comments unless they are factually wrong or directly contradict the new code. - Do not add decorative section separators (e.g. `// -----------`, `// ===`, `/* *** */`). Use `///` doc-comments or short `//` comments only when they add value. -- Do not put `///` doc comments on items annotated with `#[pyattr]`, `#[pyclass]`, or `#[pyfunction]`. The derive macros pull authoritative docstrings from CPython via the `rustpython-doc` crate; a Rust doc comment overrides that source, and on `#[pyattr]` it is silently dropped. +- A `///` doc comment on a Python-exposed item (`#[pyattr]`, `#[pyclass]`, `#[pyfunction]`, `#[pymethod]`, `#[pyclassmethod]`, `#[pystaticmethod]`, `#[pygetset]`, `#[pymember]`, `#[pyslot]`, `#[pyexception]`, `#[pymodule]`, `#[pystruct_sequence]`) is that item's docstring and overrides the stored docs in `rustpython-doc` (`crates/doc`). Those stored docs are used only when the item has no `///` comment. Developer notes on these items must be `//` comments. #### Avoid Duplicate Code in Branches diff --git a/crates/capi/src/descrobject.rs b/crates/capi/src/descrobject.rs index 18e18acde..60cbe492c 100644 --- a/crates/capi/src/descrobject.rs +++ b/crates/capi/src/descrobject.rs @@ -149,8 +149,8 @@ impl PyMemberDef { flags, doc, }, - // `offset` is a byte offset from the object to a pointer cell. - access: MemberAccess::Slot, + // `offset` is a byte offset from the object to the field. + access: MemberAccess::Offset, }; Ok(descriptor.into_ref(&vm.ctx)) diff --git a/crates/common/src/atomic.rs b/crates/common/src/atomic.rs index 15c95c3e1..85398e817 100644 --- a/crates/common/src/atomic.rs +++ b/crates/common/src/atomic.rs @@ -52,6 +52,33 @@ impl PyAtomicScalar for *mut T { type Radium = atomic_ty!(*mut T, AtomicPtr); } +/// Atomic `f64`. The value is stored as its bits in an `AtomicU64`. +#[repr(transparent)] +pub struct AtomicF64 { + bits: AtomicU64, +} + +impl AtomicF64 { + #[inline] + #[must_use] + pub const fn new(value: f64) -> Self { + Self { + bits: AtomicU64::new(value.to_bits()), + } + } + + #[inline] + #[must_use] + pub fn load(&self, order: Ordering) -> f64 { + f64::from_bits(self.bits.load(order)) + } + + #[inline] + pub fn store(&self, value: f64, order: Ordering) { + self.bits.store(value.to_bits(), order); + } +} + pub struct OncePtr { inner: PyAtomic<*mut T>, } diff --git a/crates/derive-impl/src/pyclass.rs b/crates/derive-impl/src/pyclass.rs index 224e8a795..6b21a03ce 100644 --- a/crates/derive-impl/src/pyclass.rs +++ b/crates/derive-impl/src/pyclass.rs @@ -66,7 +66,6 @@ struct ImplContext { attribute_items: ItemNursery, method_items: MethodNursery, getset_items: GetSetNursery, - member_items: MemberNursery, extend_slots_items: ItemNursery, class_extensions: Vec, errors: Vec, @@ -99,7 +98,6 @@ fn extract_items_into_context<'a, Item>( } context.errors.ok_or_push(context.method_items.validate()); context.errors.ok_or_push(context.getset_items.validate()); - context.errors.ok_or_push(context.member_items.validate()); } pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Result { @@ -109,7 +107,6 @@ pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Resul if imp.generics.params.is_empty() { context.self_ty_subst = Some((*imp.self_ty).clone()); context.getset_items.class_ty = context.self_ty_subst.clone(); - context.member_items.class_ty = context.self_ty_subst.clone(); } extract_items_into_context(&mut context, imp.items.iter_mut()); @@ -182,7 +179,6 @@ pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Resul let payload_ty = attr_payload.unwrap_or(payload_guess); let method_def = &context.method_items; let getset_impl = &context.getset_items; - let member_impl = &context.member_items; let extend_impl = context.attribute_items.validate()?; let slots_impl = context.extend_slots_items.validate()?; let class_extensions = &context.class_extensions; @@ -197,7 +193,6 @@ pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Resul class: &'static ::rustpython_vm::Py<::rustpython_vm::builtins::PyType>, ) { #getset_impl - #member_impl #extend_impl #(#class_extensions)* } @@ -295,7 +290,6 @@ pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Resul let method_def = &context.method_items; let getset_impl = &context.getset_items; - let member_impl = &context.member_items; let extend_impl = &context.attribute_items.validate()?; let slots_impl = &context.extend_slots_items.validate()?; let class_extensions = &context.class_extensions; @@ -316,7 +310,6 @@ pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Resul class: &'static ::rustpython_vm::Py<::rustpython_vm::builtins::PyType>, ) { #getset_impl - #member_impl #extend_impl #with_impl #(#class_extensions)* @@ -433,15 +426,15 @@ fn class_def_ty(self_ty: Option<&syn::Type>) -> Option { Some(quote!(#ty)) } -/// Const `Option<&'static str>`: table entry, else the Rust doc. -/// Generic impls cannot name `Self` from a nested const, so they keep the Rust doc. +/// Const `Option<&'static str>`. A non-empty Rust doc is the docstring. +/// The attribute table is used only when there is no Rust doc. Generic impls +/// cannot name `Self` from a nested const, so without a Rust doc they store none. fn attr_doc_expr(self_ty: Option<&syn::Type>, attr: &str, rust_doc: Option) -> TokenStream { - let fallback = match rust_doc { - Some(doc) => quote!(Some(#doc)), - None => quote!(None), - }; + if let Some(doc) = rust_doc.filter(|doc| !doc.is_empty()) { + return quote!(Some(#doc)); + } let Some(ty) = class_def_ty(self_ty) else { - return fallback; + return quote!(None); }; quote! { { @@ -449,11 +442,10 @@ fn attr_doc_expr(self_ty: Option<&syn::Type>, attr: &str, rust_doc: Option::ATTR_DOCS, #attr, ); - const RUST_DOC: Option<&str> = #fallback; if let Some(doc) = FOUND { if doc.is_empty() { None } else { Some(doc) } } else { - RUST_DOC + None } } } @@ -482,6 +474,17 @@ fn type_matches_path(ty: &syn::Type, path: &syn::Path) -> bool { type_last.ident == path_last.ident } +struct MemberTableTokens<'a> { + members: &'a TokenStream, + parts: &'a TokenStream, + checks: &'a TokenStream, +} + +struct ClassDefExtras<'a> { + attrs: &'a [Attribute], + member_table: MemberTableTokens<'a>, +} + fn generate_class_def( ident: &Ident, name: &str, @@ -489,14 +492,16 @@ fn generate_class_def( base: Option, metaclass: Option, unhashable: bool, - attrs: &[Attribute], + extras: ClassDefExtras<'_>, ) -> Result { + let attrs = extras.attrs; let module_key = module_name.unwrap_or("builtins"); let attr_docs = crate::class_docs::attr_docs_tokens(module_name, name); - let doc = rustpython_doc::get_qualified(module_key, name, None, true) - .filter(|doc| !doc.is_empty()) - .map(str::to_owned) - .or_else(|| attrs.doc()); + let doc = attrs.doc().filter(|doc| !doc.is_empty()).or_else(|| { + rustpython_doc::get_qualified(module_key, name, None, true) + .filter(|doc| !doc.is_empty()) + .map(str::to_owned) + }); let doc = if let Some(doc) = doc { quote!(Some(#doc)) } else { @@ -600,6 +605,12 @@ fn generate_class_def( None }; + let MemberTableTokens { + members, + parts: member_parts_impl, + checks: member_checks, + } = extras.member_table; + let tokens = quote! { impl ::rustpython_vm::class::PyClassDef for #ident { const NAME: &'static str = #name; @@ -609,10 +620,17 @@ fn generate_class_def( const ATTR_DOCS: &'static [(&'static str, &'static str)] = #attr_docs; const BASICSIZE: usize = #basicsize; const UNHASHABLE: bool = #unhashable; + const MEMBERS: &'static [::rustpython_vm::builtins::descriptor::PyMemberSpec] = #members; type Base = #base_or_object; + + fn assert_member_layout() { + #member_checks + } } + #member_parts_impl + impl ::rustpython_vm::class::StaticType for #ident { fn static_cell() -> &'static ::rustpython_vm::common::static_cell::StaticCell<::rustpython_vm::builtins::PyTypeRef> { ::rustpython_vm::common::static_cell! { @@ -667,11 +685,12 @@ pub(crate) fn impl_pyclass(attr: PunctuatedNestedMeta, item: Item) -> Result Result) -> Result<()> { - let func = args - .item - .function_or_method() - .map_err(|_| self.new_syn_error(args.item.span(), "can only be on a method"))?; - let ident = &func.sig().ident; - - let item_attr = args.attrs.remove(self.index()); - let item_meta = MemberItemMeta::from_attr(ident.clone(), &item_attr)?; - - let (py_name, member_item_kind) = item_meta.member_name()?; - let member_kind = item_meta.member_kind()?; - if let Some(ref s) = member_kind { - match s.as_str() { - "bool" | "object" => {} - other => { - return Err(self.new_syn_error( - args.item.span(), - &format!("unknown member type '{other}'"), - )); - } - } - } - - // Add #[allow(non_snake_case)] for setter methods - if matches!(member_item_kind, MemberItemKind::Set) { - let allow_attr: Attribute = parse_quote!(#[allow(non_snake_case)]); - args.attrs.push(allow_attr); - } - - let doc = args.attrs.doc(); - args.context.member_items.add_item( - &py_name, - member_item_kind, - member_kind, - ident.clone(), - doc, - )?; - Ok(()) + Err(self.new_syn_error( + args.item.span(), + "belongs on a payload field, not in an impl. Put #[pymember(...)] on the struct field", + )) } } @@ -1597,7 +1589,7 @@ impl ToTokens for GetSetNursery { #( #cfgs )* { const DOC: Option<&str> = #doc; - let mut getset = ::rustpython_vm::builtins::PyGetSet::new(#name.into(), class) + let mut getset = ::rustpython_vm::builtins::PyGetSet::new(#name, class, ctx) .with_get(Self::#getter) #setter; if let Some(doc) = DOC { @@ -1619,124 +1611,6 @@ impl ToTokens for GetSetNursery { } } -/// Member type as string, matching `Py_T_*` codes. -/// None means `MemberKind::ObjectEx`. Valid values: "bool", "object". -type MemberKindStr = Option; - -#[derive(Default)] -struct MemberNursery { - map: HashMap, - validated: bool, - class_ty: Option, -} - -struct MemberNurseryEntry { - kind: MemberKindStr, - getter: Option, - setter: Option, - doc: Option, -} - -#[derive(Clone, Copy, Eq, PartialEq)] -enum MemberItemKind { - Get, - Set, -} - -impl MemberNursery { - fn add_item( - &mut self, - name: &str, - kind: MemberItemKind, - member_kind: MemberKindStr, - item_ident: Ident, - doc: Option, - ) -> Result<()> { - assert!(!self.validated, "new item is not allowed after validation"); - let entry = self - .map - .entry(name.to_string()) - .or_insert_with(|| MemberNurseryEntry { - kind: member_kind, - getter: None, - setter: None, - doc: None, - }); - let func = match kind { - MemberItemKind::Get => &mut entry.getter, - MemberItemKind::Set => &mut entry.setter, - }; - if func.is_some() { - bail_span!(item_ident, "Multiple member accessors with name '{}'", name); - } - *func = Some(item_ident); - if matches!(kind, MemberItemKind::Get) - && let Some(doc) = doc - { - entry.doc = Some(doc); - } - Ok(()) - } - - fn validate(&mut self) -> Result<()> { - let mut errors = Vec::new(); - - #[expect( - clippy::iter_over_hash_type, - reason = "Iteration order doesn't matter here" - )] - for (name, entry) in &self.map { - if entry.getter.is_none() { - errors.push(err_span!( - entry.setter.as_ref().unwrap(), - "Member '{}' is missing a getter", - name - )); - }; - } - - errors.into_result()?; - self.validated = true; - Ok(()) - } -} - -impl ToTokens for MemberNursery { - fn to_tokens(&self, tokens: &mut TokenStream) { - assert!(self.validated, "Call `validate()` before token generation"); - let properties = self.map.iter().map(|(name, entry)| { - let setter = match &entry.setter { - Some(setter) => quote_spanned! { setter.span() => Some(Self::#setter)}, - None => quote! { None }, - }; - let member_kind = match entry.kind.as_deref() { - Some("bool") => { - quote!(::rustpython_vm::builtins::descriptor::MemberKind::Bool) - } - Some("object") => { - quote!(::rustpython_vm::builtins::descriptor::MemberKind::Object) - } - _ => { - quote!(::rustpython_vm::builtins::descriptor::MemberKind::ObjectEx) - } - }; - let getter = entry.getter.as_ref().unwrap(); - let doc = attr_doc_expr(self.class_ty.as_ref(), name, entry.doc.clone()); - quote_spanned! { getter.span() => - { - const DOC: Option<&str> = #doc; - class.set_str_attr( - #name, - ctx.new_member(#name, #member_kind, Self::#getter, #setter, class, DOC), - ctx, - ); - } - } - }); - tokens.extend(properties); - } -} - struct MethodItemMeta(ItemMetaInner); impl ItemMeta for MethodItemMeta { @@ -1918,7 +1792,15 @@ impl SlotItemMeta { struct MemberItemMeta(ItemMetaInner); impl ItemMeta for MemberItemMeta { - const ALLOWED_NAMES: &'static [&'static str] = &["type", "setter"]; + const ALLOWED_NAMES: &'static [&'static str] = &[ + "type", + "writable", + "audit_read", + "name", + "path", + "doc", + "offset", + ]; fn from_inner(inner: ItemMetaInner) -> Self { Self(inner) @@ -1930,48 +1812,527 @@ impl ItemMeta for MemberItemMeta { } impl MemberItemMeta { - fn member_name(&self) -> Result<(String, MemberItemKind)> { - let inner = self.inner(); - let sig_name = inner.item_name(); - let extract_prefix_name = |prefix, item_typ| { - if let Some(name) = sig_name.strip_prefix(prefix) { - if name.is_empty() { - Err(err_span!( - inner.meta_ident, - r#"A #[{}({typ})] fn with a {prefix}* name must \ - have something after "{prefix}""#, - inner.meta_name(), - typ = item_typ, - prefix = prefix - )) - } else { - Ok(name.to_owned()) - } - } else { - Err(err_span!( - inner.meta_ident, - r#"A #[{}(setter)] fn must either have a `name` \ - parameter or a fn name along the lines of "set_*""#, - inner.meta_name() - )) - } - }; - let kind = if inner._bool("setter")? { - MemberItemKind::Set - } else { - MemberItemKind::Get - }; - let name = match kind { - MemberItemKind::Get => sig_name, - MemberItemKind::Set => extract_prefix_name("set_", "setter")?, - }; - Ok((name, kind)) + fn explicit_name(&self) -> Result> { + self.inner()._optional_str("name") } fn member_kind(&self) -> Result> { - let inner = self.inner(); - inner._optional_str("type") + let kind = self.inner()._optional_str("type")?; + if let Some(value) = &kind { + let span = self + .inner() + .meta_map + .get("type") + .map_or_else(|| self.inner().meta_ident.span(), |(_, meta)| meta.span()); + match value.as_str() { + "object_ex" => {} + "object" | "bool" | "double" | "int" | "uint" | "py_ssize_t" => { + return Err(syn::Error::new( + span, + format!( + "member kind is inferred from the field type; remove `type = \"{value}\"`" + ), + )); + } + other => { + return Err(syn::Error::new( + span, + format!("unknown member type '{other}'"), + )); + } + } + } + Ok(kind) } + + fn path_tokens(&self) -> Result> { + let inner = self.inner(); + let Some(value) = inner._optional_str("path")? else { + return Ok(None); + }; + let span = inner + .meta_map + .get("path") + .map_or_else(|| inner.meta_ident.span(), |(_, meta)| meta.span()); + parse_member_subpath(&value, span).map(Some) + } + + fn writable(&self) -> Result { + self.inner()._bool("writable") + } + + fn audit_read(&self) -> Result { + self.inner()._bool("audit_read") + } + + fn offset_tokens(&self) -> Result> { + let Some((_, meta)) = self.inner().meta_map.get("offset") else { + return Ok(None); + }; + let Meta::NameValue(name_value) = meta else { + return Err(syn::Error::new( + meta.span(), + "#[pymember(offset = ...)] must be an expression", + )); + }; + Ok(Some(name_value.value.to_token_stream())) + } + + /// `doc = false` suppresses the stored docstring. The member docstring is + /// the field's `///` comment (or the `///` lines immediately above a + /// struct-level `#[pymember]`). A string `doc` is rejected. + fn suppress_doc(&self) -> Result { + let Some((_, meta)) = self.inner().meta_map.get("doc") else { + return Ok(false); + }; + match meta { + Meta::NameValue(syn::MetaNameValue { + value: + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Bool(lit), + .. + }), + .. + }) if !lit.value => Ok(true), + _ => Err(syn::Error::new( + meta.span(), + "#[pymember] docstring is the `///` comment; only `doc = false` is accepted", + )), + } + } +} + +fn parse_member_subpath(value: &str, span: Span) -> Result { + if value.is_empty() { + return Err(syn::Error::new( + span, + "#[pymember(path = ...)] must name a subfield", + )); + } + let stream: TokenStream = value.parse().map_err(|err| syn::Error::new(span, err))?; + let trees: Vec = stream.into_iter().collect(); + if trees.is_empty() { + return Err(syn::Error::new( + span, + "#[pymember(path = ...)] must name a subfield", + )); + } + let mut expect_ident = true; + for tree in &trees { + match tree { + TokenTree::Ident(_) if expect_ident => expect_ident = false, + TokenTree::Punct(punct) if !expect_ident && punct.as_char() == '.' => { + expect_ident = true; + } + _ => { + return Err(syn::Error::new( + span, + "#[pymember(path = ...)] must be a field path", + )); + } + } + } + if expect_ident { + return Err(syn::Error::new( + span, + "#[pymember(path = ...)] must be a field path", + )); + } + Ok(trees.into_iter().collect()) +} + +struct BuiltMember { + name: String, + cfgs: Vec, + kind: TokenStream, + offset: TokenStream, + flags: TokenStream, + doc: TokenStream, + check: TokenStream, + span: Span, +} + +fn is_doc_attr(attr: &Attribute) -> bool { + attr.path().is_ident("doc") +} + +fn doc_attr_line(attr: &Attribute) -> Option { + let syn::Meta::NameValue(name_value) = &attr.meta else { + return None; + }; + let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }) = &name_value.value + else { + return None; + }; + Some(lit.value().trim().to_owned()) +} + +/// `#[pymember]` on a struct has no field. `///` lines placed immediately +/// above that attribute (after any other attribute) are its docstring and are +/// removed so they are not also the class docstring. Several struct-level +/// members each take the `///` lines directly above them. +fn split_struct_pymembers(attrs: &mut Vec) -> Vec<(Attribute, Option)> { + let mut kept = Vec::with_capacity(attrs.len()); + let mut members = Vec::new(); + let mut pending_docs = Vec::new(); + let mut pending_doc_attrs = Vec::new(); + for attr in attrs.drain(..) { + if is_doc_attr(&attr) + && let Some(text) = doc_attr_line(&attr) + { + pending_docs.push(text); + pending_doc_attrs.push(attr); + continue; + } + if attr.path().is_ident("pymember") { + let doc = if pending_docs.is_empty() { + None + } else { + Some(pending_docs.join("\n")).filter(|doc| !doc.is_empty()) + }; + pending_docs.clear(); + pending_doc_attrs.clear(); + members.push((attr, doc)); + continue; + } + kept.append(&mut pending_doc_attrs); + pending_docs.clear(); + kept.push(attr); + } + kept.append(&mut pending_doc_attrs); + *attrs = kept; + members +} + +fn split_pymember_attrs(attrs: &mut Vec) -> Vec { + let mut kept = Vec::with_capacity(attrs.len()); + let mut members = Vec::new(); + for attr in attrs.drain(..) { + if attr.path().is_ident("pymember") { + members.push(attr); + } else { + kept.push(attr); + } + } + *attrs = kept; + members +} + +fn field_cfg_attrs(attrs: &[Attribute]) -> Result> { + let mut cfgs = Vec::new(); + for attr in attrs { + if attr.path().is_ident("cfg") { + if !matches!(attr.meta, syn::Meta::List(_)) { + return Err(syn::Error::new( + attr.span(), + "#[cfg] on a #[pymember] field must have a predicate", + )); + } + cfgs.push(attr.clone()); + } + } + Ok(cfgs) +} + +fn cfg_not_all(cfgs: &[Attribute]) -> TokenStream { + let preds = cfgs.iter().map(|attr| match &attr.meta { + syn::Meta::List(list) => list.tokens.clone(), + _ => quote!(all()), + }); + quote!(#[cfg(not(all(#(#preds),*)))] ) +} + +fn member_kind_tokens( + kind: Option<&str>, + field_tokens: Option<&TokenStream>, + span: Span, +) -> Result { + let Some(field_tokens) = field_tokens else { + let name = match kind { + None => "Object", + Some("object_ex") => "ObjectEx", + Some(other) => { + return Err(syn::Error::new( + span, + format!("unknown member type '{other}'"), + )); + } + }; + let ident = Ident::new(name, span); + return Ok(quote!(::rustpython_vm::builtins::descriptor::MemberKind::#ident)); + }; + let inferred = quote_spanned! { span => + ::rustpython_vm::builtins::descriptor::member_kind_of( + |payload: &Self| &payload.#field_tokens, + ) + }; + let kind = match kind { + None => inferred, + Some("object_ex") => quote_spanned! { span => + { + let inferred = #inferred; + ::core::assert!( + matches!( + inferred, + ::rustpython_vm::builtins::descriptor::MemberKind::Object + ), + "`type = \"object_ex\"` requires an object field", + ); + ::rustpython_vm::builtins::descriptor::MemberKind::ObjectEx + } + }, + Some(other) => { + return Err(syn::Error::new( + span, + format!("unknown member type '{other}'"), + )); + } + }; + Ok(kind) +} + +fn member_layout_tokens(readonly: bool) -> TokenStream { + let name = if readonly { + "MemberLayout" + } else { + "MemberCell" + }; + let ident = Ident::new(name, Span::call_site()); + quote!(::rustpython_vm::builtins::descriptor::#ident) +} + +fn member_flag_tokens(readonly: bool, audit_read: bool) -> TokenStream { + let mut flag_parts = Vec::new(); + if readonly { + flag_parts.push(quote!(::rustpython_vm::builtins::descriptor::PY_READONLY)); + } + if audit_read { + flag_parts.push(quote!(::rustpython_vm::builtins::descriptor::PY_AUDIT_READ)); + } + if flag_parts.is_empty() { + quote!(0) + } else { + quote!(#(#flag_parts)|*) + } +} + +fn build_member( + attr: &Attribute, + field: Option<(&syn::Field, usize)>, + cfgs: &[Attribute], + class_ty: &syn::Type, + rust_doc: Option, + seen: &mut Vec<(String, Vec)>, +) -> Result { + let fallback_ident = field + .and_then(|(field, _)| field.ident.clone()) + .unwrap_or_else(|| Ident::new("member", attr.span())); + let meta = MemberItemMeta::from_attr(fallback_ident, attr)?; + let explicit_name = meta.explicit_name()?; + let name = if let Some(name) = explicit_name { + name + } else if let Some((field, _)) = field { + field + .ident + .as_ref() + .map(|ident| ident.to_string()) + .ok_or_else(|| { + syn::Error::new( + attr.span(), + "#[pymember] on an unnamed field requires `name`", + ) + })? + } else { + return Err(syn::Error::new( + attr.span(), + "#[pymember] on a struct requires `name`", + )); + }; + if seen + .iter() + .any(|(seen_name, seen_cfgs)| seen_name == &name && seen_cfgs == cfgs) + { + return Err(syn::Error::new( + attr.span(), + format!("Multiple members with name '{name}'"), + )); + } + seen.push((name.clone(), cfgs.to_vec())); + + let kind = meta.member_kind()?; + let path = meta.path_tokens()?; + let readonly = !meta.writable()?; + let audit_read = meta.audit_read()?; + let offset_expr = meta.offset_tokens()?; + if field.is_some() && offset_expr.is_some() { + return Err(syn::Error::new( + attr.span(), + "#[pymember(offset = ...)] is only valid on a struct, not on a field", + )); + } + if field.is_none() && path.is_some() { + return Err(syn::Error::new( + attr.span(), + "#[pymember(path = ...)] is only valid on a field", + )); + } + let (offset, check, kind_tokens) = if let Some((field, index)) = field { + let index_tokens = match &field.ident { + Some(ident) => ident.to_token_stream(), + None => syn::Index::from(index).into_token_stream(), + }; + let field_tokens = if let Some(path) = &path { + quote!(#index_tokens.#path) + } else { + index_tokens + }; + let offset = quote_spanned! { attr.span() => + ::rustpython_vm::object::payload_offset::() as isize + + ::core::mem::offset_of!(Self, #field_tokens) as isize + }; + let layout = member_layout_tokens(readonly); + let check = quote_spanned! { attr.span() => + let _ = |payload: *const Self| { + fn assert_member_field(_: *const T) {} + // SAFETY: `payload` is not dereferenced. `addr_of` + // only names the field so its type can be checked. + let field = unsafe { core::ptr::addr_of!((*payload).#field_tokens) }; + assert_member_field(field); + }; + }; + let kind_tokens = member_kind_tokens(kind.as_deref(), Some(&field_tokens), attr.span())?; + (offset, check, kind_tokens) + } else { + let Some(offset_expr) = offset_expr else { + return Err(syn::Error::new( + attr.span(), + "#[pymember] on a struct requires `offset`", + )); + }; + let kind_tokens = member_kind_tokens(kind.as_deref(), None, attr.span())?; + (offset_expr, quote!(), kind_tokens) + }; + let doc = if meta.suppress_doc()? { + quote!(None) + } else { + attr_doc_expr(Some(class_ty), &name, rust_doc) + }; + Ok(BuiltMember { + name, + cfgs: cfgs.to_vec(), + kind: kind_tokens, + offset, + flags: member_flag_tokens(readonly, audit_read), + doc, + check, + span: attr.span(), + }) +} + +fn emit_member_table( + ident: &Ident, + members: &[BuiltMember], +) -> (TokenStream, TokenStream, TokenStream) { + if members.is_empty() { + return (quote!(&[]), quote!(), quote!()); + } + let mut parts = TokenStream::new(); + let mut checks = TokenStream::new(); + let mut names = Vec::new(); + for (index, member) in members.iter().enumerate() { + let const_name = Ident::new(&format!("__PYMEMBER_{index}"), member.span); + let cfgs = &member.cfgs; + let not_cfg = cfg_not_all(cfgs); + let kind = &member.kind; + let offset = &member.offset; + let flags = &member.flags; + let doc = &member.doc; + let name = &member.name; + let check = &member.check; + parts.extend(quote_spanned! { member.span => + #(#cfgs)* + const #const_name: &'static [::rustpython_vm::builtins::descriptor::PyMemberSpec] = &[ + ::rustpython_vm::builtins::descriptor::PyMemberSpec { + name: #name, + kind: #kind, + offset: #offset, + flags: #flags, + doc: #doc, + }, + ]; + #not_cfg + const #const_name: &'static [::rustpython_vm::builtins::descriptor::PyMemberSpec] = &[]; + }); + if !check.is_empty() { + checks.extend(quote_spanned! { member.span => + #(#cfgs)* + #check + }); + } + names.push(const_name); + } + let len_terms = names.iter().map(|name| quote!(#ident::#name.len())); + let members_expr = quote! { + &::rustpython_vm::builtins::descriptor::PyMemberSpec::concat::<{ #(#len_terms)+* }>( + &[#(#ident::#names),*], + ) + }; + let parts_impl = quote! { + impl #ident { + #parts + } + }; + (members_expr, parts_impl, checks) +} + +fn extract_py_members(item: &mut Item) -> Result<(TokenStream, TokenStream, TokenStream)> { + let Item::Struct(item_struct) = item else { + return Ok((quote!(&[]), quote!(), quote!())); + }; + let struct_ident = item_struct.ident.clone(); + let class_ty: syn::Type = syn::parse_quote!(#struct_ident); + let mut seen = Vec::new(); + let mut built = Vec::new(); + for (attr, rust_doc) in split_struct_pymembers(&mut item_struct.attrs) { + built.push(build_member( + &attr, + None, + &[], + &class_ty, + rust_doc, + &mut seen, + )?); + } + let field_count = match &item_struct.fields { + syn::Fields::Named(fields) => fields.named.len(), + syn::Fields::Unnamed(fields) => fields.unnamed.len(), + syn::Fields::Unit => 0, + }; + for index in 0..field_count { + let field = match &mut item_struct.fields { + syn::Fields::Named(fields) => &mut fields.named[index], + syn::Fields::Unnamed(fields) => &mut fields.unnamed[index], + syn::Fields::Unit => unreachable!(), + }; + let cfgs = field_cfg_attrs(&field.attrs)?; + // Every #[pymember] on the field shares that field's `///` comment. + let rust_doc = field.attrs.doc().filter(|doc| !doc.is_empty()); + let attrs = split_pymember_attrs(&mut field.attrs); + for attr in &attrs { + built.push(build_member( + attr, + Some((field, index)), + &cfgs, + &class_ty, + rust_doc.clone(), + &mut seen, + )?); + } + } + Ok(emit_member_table(&struct_ident, &built)) } struct ExtractedImplAttrs { diff --git a/crates/derive-impl/src/pymodule.rs b/crates/derive-impl/src/pymodule.rs index 5e3c68e54..c38fd6f79 100644 --- a/crates/derive-impl/src/pymodule.rs +++ b/crates/derive-impl/src/pymodule.rs @@ -255,10 +255,11 @@ pub(crate) fn impl_pymodule(args: PyModuleArgs, module_item: Item) -> Result quote!(Some(#doc)), None => quote!(None), diff --git a/crates/derive-impl/src/pystructseq.rs b/crates/derive-impl/src/pystructseq.rs index 24ee687b8..56f2998d5 100644 --- a/crates/derive-impl/src/pystructseq.rs +++ b/crates/derive-impl/src/pystructseq.rs @@ -2,7 +2,7 @@ use crate::util::{ItemMeta, ItemMetaInner}; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{DeriveInput, Ident, Item, Result}; -use syn_ext::ext::{AttributeExt, GetIdent}; +use syn_ext::ext::{AttributeExt, AttributeIteratorExt, GetIdent}; use syn_ext::types::{Meta, PunctuatedNestedMeta}; // #[pystruct_sequence_data] - For Data structs @@ -524,13 +524,18 @@ pub(crate) fn impl_pystruct_sequence( } else { class_name.clone() }; - let doc = rustpython_doc::get(module_class_name.as_str()).or_else(|| { + let db_doc = rustpython_doc::get(module_class_name.as_str()).or_else(|| { // os re-exports the posix/nt struct sequences. let class_name = module_class_name.strip_prefix("os.")?; rustpython_doc::get(&format!("posix.{class_name}")) .or_else(|| rustpython_doc::get(&format!("nt.{class_name}"))) }); - let doc = doc.filter(|doc| !doc.is_empty()); + let db_doc = db_doc.filter(|doc| !doc.is_empty()).map(str::to_owned); + let doc = struct_item + .attrs + .doc() + .filter(|doc| !doc.is_empty()) + .or(db_doc); let doc = match doc { Some(doc) => quote!(Some(#doc)), None => quote!(None), diff --git a/crates/derive-impl/src/util.rs b/crates/derive-impl/src/util.rs index 006f87631..557106ba0 100644 --- a/crates/derive-impl/src/util.rs +++ b/crates/derive-impl/src/util.rs @@ -867,8 +867,9 @@ fn args_const(pieces: &[SigPiece]) -> TokenStream { /// Expression of type `Option<&'static str>`: the internal doc, or the plain /// doc when the arguments cannot form a signature. -/// `doc` is a const `Option<&'static str>`. A table entry wins; an empty -/// string is no docstring. The text is composed into the internal doc so +/// `doc` is a const `Option<&'static str>` already chosen by the caller +/// (a Rust doc comment wins over the stored docstring). An empty string is +/// no docstring. The text is composed into the internal doc so /// `__text_signature__` stays on the signature half. pub(crate) fn internal_doc_tokens( sig: &Signature, diff --git a/crates/derive/src/lib.rs b/crates/derive/src/lib.rs index 4fcdad91e..c130fcd72 100644 --- a/crates/derive/src/lib.rs +++ b/crates/derive/src/lib.rs @@ -142,6 +142,11 @@ pub fn derive_from_args(input: TokenStream) -> TokenStream { /// ... /// } /// ``` +/// ## Docstrings +/// A `///` doc comment on the struct, or on a `#[pymethod]` / `#[pyclassmethod]` / +/// `#[pystaticmethod]` / `#[pygetset]` / `#[pymember]` item, is the Python +/// docstring and overrides the stored documentation. The stored documentation +/// is used only when that item has no `///` comment. Use `//` for developer notes. /// ## Inner markers /// ### pymethod/pyclassmethod/pystaticmethod /// `pymethod` is used to mark a method of the Python class. @@ -210,6 +215,41 @@ pub fn derive_from_args(input: TokenStream) -> TokenStream { /// } /// ``` /// ### pymember +/// Declares an offset member on a payload field. The struct `#[pyclass]` builds +/// a `PyClassDef::MEMBERS` table and `extend_class` registers one +/// `member_descriptor` per entry. The member kind is inferred from the field +/// type: `bool` / `AtomicBool`, `i32` / `AtomicI32`, `u32` / `AtomicU32`, +/// `isize` / `AtomicIsize`, `f64` / `AtomicF64`, or an object pointer +/// (`PyObjectRef`, `PyRef`, `Option` of those, `PyAtomicRef`, +/// `PyAtomicRef>`, `PyAtomicRef>`, +/// `&'static Py`, `&'static PyStrInterned`). +/// +/// - `type`: only `"object_ex"`, on an object field or a struct-level offset +/// member. `"object"`, `"bool"`, `"int"`, `"uint"`, `"double"`, and +/// `"py_ssize_t"` are rejected because those kinds are inferred. +/// - `writable`: accept stores. Members are readonly without it. A writable +/// object member must be `PyAtomicRef` (never null) or +/// `PyAtomicRef>` (nullable), a writable bool must be +/// `AtomicBool`, a writable int must be `AtomicI32`, a writable uint must be +/// `AtomicU32`, a writable double must be `AtomicF64`, and a writable +/// py_ssize_t must be `AtomicIsize`. +/// - `audit_read`: audit `object.__getattr__` before the load. +/// - `name`: Python attribute name. Defaults to the field name. +/// - `path`: subfield of the annotated field (`value` with `path = "re"`). +/// - `doc`: only `doc = false`, which stores no docstring. Otherwise the +/// docstring is the field's `///` comment when present, and the stored +/// attribute documentation when the field has no `///` comment. Every +/// `#[pymember]` on one field shares that comment. A string `doc` is rejected. +/// +/// A struct-level `#[pymember]` (after `#[pyclass]`) has no field. `offset` is +/// required there and rejected on a field. `///` lines placed immediately above +/// that attribute are its docstring; otherwise the stored attribute +/// documentation is used. One field may carry several `#[pymember]` attributes. +/// A `#[cfg]` on the field gates that entry. +/// ```rust, ignore +/// #[pymember(name = "fget")] +/// getter: PyAtomicRef>, +/// ``` /// # Trait /// `#[pyclass]` on traits functions a lot like `#[pyclass]` on `impl` blocks. /// Note that associated functions that are annotated with `#[pymethod]` or similar **must** @@ -223,6 +263,8 @@ pub fn pyclass(attr: TokenStream, item: TokenStream) -> TokenStream { /// Helper macro to define `Exception` types. /// More-or-less is an alias to `pyclass` macro. +/// A `///` comment on the exception type is its docstring and overrides the +/// stored documentation. Use `//` for developer notes. /// /// This macro serves a goal of generating multiple /// `BaseException` / `Exception` @@ -240,6 +282,10 @@ pub fn pyexception(attr: TokenStream, item: TokenStream) -> TokenStream { /// It defines a Python module in the form of a `module_def` function in the module; /// this has to be used in a `add_native_module` to properly register the module. /// Additionally, this macro defines 'MODULE_NAME' and 'DOC' in the module. +/// A `///` comment on the module is its docstring and overrides the stored +/// module documentation. `#[pyfunction]` works the same way: its `///` comment +/// is the function docstring and overrides the stored function documentation. +/// Use `//` for developer notes. /// # Arguments /// - `name`: the name of the python module, /// by default, it is the name of the module, but this can be configured. @@ -311,7 +357,8 @@ pub fn pymodule(attr: TokenStream, item: TokenStream) -> TokenStream { /// Attribute macro for defining Python struct sequence types. /// /// This macro is applied to an empty struct to create a Python type -/// that wraps a Data struct. +/// that wraps a Data struct. A `///` comment on that struct is the type's +/// docstring and overrides the stored documentation. Use `//` for developer notes. /// /// # Example /// ```ignore diff --git a/crates/stdlib/src/_asyncio.rs b/crates/stdlib/src/_asyncio.rs index 9432111d1..4d1ec084c 100644 --- a/crates/stdlib/src/_asyncio.rs +++ b/crates/stdlib/src/_asyncio.rs @@ -111,7 +111,6 @@ pub(crate) mod _asyncio { } } - /// asyncio.Future implementation #[pyattr] #[pyclass(name = "Future", module = "_asyncio", traverse)] #[derive(Debug, PyPayload)] diff --git a/crates/stdlib/src/_sqlite3.rs b/crates/stdlib/src/_sqlite3.rs index 309c76c6f..f5b02422f 100644 --- a/crates/stdlib/src/_sqlite3.rs +++ b/crates/stdlib/src/_sqlite3.rs @@ -1093,7 +1093,7 @@ mod _sqlite3 { } if let Some(cursor_ref) = cursor.downcast_ref::() - && let Some(factory) = zelf.row_factory.to_owned() + && let Some(factory) = zelf.row_factory.load_owned() { let _ = unsafe { cursor_ref.row_factory.swap(Some(factory)) }; } @@ -1187,7 +1187,7 @@ mod _sqlite3 { parameters: OptionalArg, vm: &VirtualMachine, ) -> PyResult> { - let row_factory = zelf.row_factory.to_owned(); + let row_factory = zelf.row_factory.load_owned(); let cursor = Cursor::new(zelf, row_factory, vm).into_ref(&vm.ctx); Cursor::execute(cursor, sql, parameters, vm) } @@ -1199,7 +1199,7 @@ mod _sqlite3 { seq_of_params: ArgIterable, vm: &VirtualMachine, ) -> PyResult> { - let row_factory = zelf.row_factory.to_owned(); + let row_factory = zelf.row_factory.load_owned(); let cursor = Cursor::new(zelf, row_factory, vm).into_ref(&vm.ctx); Cursor::executemany(cursor, sql, seq_of_params, vm) } @@ -1210,7 +1210,7 @@ mod _sqlite3 { script: PyUtf8StrRef, vm: &VirtualMachine, ) -> PyResult> { - let row_factory = zelf.row_factory.to_owned(); + let row_factory = zelf.row_factory.load_owned(); Cursor::executescript( Cursor::new(zelf, row_factory, vm).into_ref(&vm.ctx), script, @@ -1669,7 +1669,7 @@ mod _sqlite3 { #[pygetset] fn row_factory(&self) -> Option { - self.row_factory.to_owned() + self.row_factory.load_owned() } #[pygetset(setter)] fn set_row_factory( @@ -2146,7 +2146,7 @@ mod _sqlite3 { #[pygetset] fn row_factory(&self) -> Option { - self.row_factory.to_owned() + self.row_factory.load_owned() } #[pygetset(setter)] @@ -2346,7 +2346,7 @@ mod _sqlite3 { let row = vm.ctx.new_tuple(row); - if let Some(row_factory) = zelf.row_factory.to_owned() { + if let Some(row_factory) = zelf.row_factory.load_owned() { row_factory .call((zelf.to_owned(), row), vm) .map(PyIterReturn::Return) diff --git a/crates/stdlib/src/multiprocessing.rs b/crates/stdlib/src/multiprocessing.rs index d29c0d89e..1c98e7e0e 100644 --- a/crates/stdlib/src/multiprocessing.rs +++ b/crates/stdlib/src/multiprocessing.rs @@ -496,7 +496,6 @@ mod _multiprocessing { self.name.clone() } - /// Acquire the semaphore/lock. // _multiprocessing_SemLock_acquire_impl #[pymethod] fn acquire(&self, args: AcquireArgs, vm: &VirtualMachine) -> PyResult { @@ -609,7 +608,6 @@ mod _multiprocessing { .store(host_multiprocessing::current_thread_id(), Ordering::Release); } - /// Release the semaphore/lock. // _multiprocessing_SemLock_release_impl #[pymethod] fn release(&self, vm: &VirtualMachine) -> PyResult<()> { @@ -670,7 +668,6 @@ mod _multiprocessing { Ok(()) } - /// Enter the semaphore/lock (context manager). // _multiprocessing_SemLock___enter___impl #[pymethod(name = "__enter__")] fn enter(&self, vm: &VirtualMachine) -> PyResult { @@ -684,7 +681,6 @@ mod _multiprocessing { ) } - /// Exit the semaphore/lock (context manager). // _multiprocessing_SemLock___exit___impl #[pymethod] fn __exit__(&self, _args: ExitArgs, vm: &VirtualMachine) -> PyResult<()> { @@ -719,7 +715,6 @@ mod _multiprocessing { zelf.into_ref_with_type(vm, cls).map(Into::into) } - /// Rezero the net acquisition count after fork(). // _multiprocessing_SemLock__after_fork_impl #[pymethod] fn _after_fork(&self) { @@ -729,14 +724,13 @@ mod _multiprocessing { self.last_tid.store(0, Ordering::Release); } - /// SemLock objects cannot be pickled directly. - /// Use multiprocessing.synchronize.SemLock wrapper which handles pickling. + // SemLock objects cannot be pickled directly. + // Use multiprocessing.synchronize.SemLock wrapper which handles pickling. #[pymethod] fn __reduce__(&self, vm: &VirtualMachine) -> PyResult { Err(vm.new_type_error("cannot pickle 'SemLock' object")) } - /// Num of `acquire()`s minus num of `release()`s for this process. // _multiprocessing_SemLock__count_impl #[pymethod] fn _count(&self) -> i32 { @@ -744,14 +738,12 @@ mod _multiprocessing { self.count.load(Ordering::Acquire) } - /// Whether the lock is owned by this thread. // _multiprocessing_SemLock__is_mine_impl #[pymethod] fn _is_mine(&self) -> bool { ismine!(self) } - /// Get the value of the semaphore. // _multiprocessing_SemLock__get_value_impl #[pymethod] fn _get_value(&self, vm: &VirtualMachine) -> PyResult { @@ -767,7 +759,6 @@ mod _multiprocessing { } } - /// Return whether semaphore has value zero. // _multiprocessing_SemLock__is_zero_impl #[pymethod] fn _is_zero(&self, vm: &VirtualMachine) -> PyResult { diff --git a/crates/stdlib/src/pickle.rs b/crates/stdlib/src/pickle.rs index 1b29062e6..9fbf73498 100644 --- a/crates/stdlib/src/pickle.rs +++ b/crates/stdlib/src/pickle.rs @@ -96,8 +96,6 @@ mod _pickle { }) } - /// Return a memoryview of the raw memory underlying this buffer. - /// Will raise BufferError if the buffer isn't contiguous. #[pymethod] fn raw(&self, vm: &VirtualMachine) -> PyResult> { let buffer = self.get(vm)?; @@ -109,7 +107,6 @@ mod _pickle { Ok(crate::vm::builtins::PyMemoryView::from_buffer(buffer, vm)?.into_ref(&vm.ctx)) } - /// Release the underlying buffer exposed by the PickleBuffer object. #[pymethod] fn release(&self) { *self.buffer.write() = None; diff --git a/crates/stdlib/src/pyexpat.rs b/crates/stdlib/src/pyexpat.rs index 99e638019..5f8c4f6af 100644 --- a/crates/stdlib/src/pyexpat.rs +++ b/crates/stdlib/src/pyexpat.rs @@ -132,7 +132,7 @@ macro_rules! create_property { macro_rules! create_readonly_int_property { ($ctx: expr, $attributes: expr, $name: expr, $class: expr, $element: ident) => { - let getset = crate::vm::builtins::PyGetSet::new($name, $class).with_get( + let getset = crate::vm::builtins::PyGetSet::new($name, $class, $ctx).with_get( move |this: &Py, vm: &VirtualMachine| -> PyObjectRef { vm.ctx.new_int(*this.$element.read()).into() }, diff --git a/crates/stdlib/src/pystruct.rs b/crates/stdlib/src/pystruct.rs index 44d0ed2de..b217e2005 100644 --- a/crates/stdlib/src/pystruct.rs +++ b/crates/stdlib/src/pystruct.rs @@ -309,8 +309,8 @@ pub(crate) mod _struct { Ok(self.ready(vm)?.format.clone()) } - /// The size an uninitialized `Struct` reports, which no format has - /// yet given a value. + // The size an uninitialized `Struct` reports, which no format has + // yet given a value. #[pygetset] fn size(&self) -> isize { self.inner diff --git a/crates/stdlib/src/re.rs b/crates/stdlib/src/re.rs index c72039f10..31ce8912f 100644 --- a/crates/stdlib/src/re.rs +++ b/crates/stdlib/src/re.rs @@ -75,7 +75,7 @@ mod re { } } - /// Inner data for a match object. + // Inner data for a match object. #[pyattr] #[pyclass(module = "re", name = "Match", traverse)] #[derive(PyPayload, Traverse)] diff --git a/crates/stdlib/src/socket.rs b/crates/stdlib/src/socket.rs index d97bc4780..283ccce57 100644 --- a/crates/stdlib/src/socket.rs +++ b/crates/stdlib/src/socket.rs @@ -2155,9 +2155,6 @@ mod _socket { .map_err(|e| e.into_pyexception(vm)) } - /// sendmsg_afalg([msg], *, op[, iv[, assoclen[, flags]]]) -> int - /// - /// Set operation mode and target IV for an AF_ALG socket. #[cfg(target_os = "linux")] #[pymethod] fn sendmsg_afalg(&self, args: SendmsgAfalgArgs, vm: &VirtualMachine) -> PyResult { @@ -2192,9 +2189,6 @@ mod _socket { .map_err(|e| e.into_pyexception(vm)) } - /// recvmsg(bufsize[, ancbufsize[, flags]]) -> (data, ancdata, msg_flags, address) - /// - /// Receive normal data and ancillary data from the socket. #[cfg(all(unix, not(target_os = "redox")))] #[pymethod] fn recvmsg( diff --git a/crates/vm/src/builtins/asyncgenerator.rs b/crates/vm/src/builtins/asyncgenerator.rs index 7e5d596be..788725621 100644 --- a/crates/vm/src/builtins/asyncgenerator.rs +++ b/crates/vm/src/builtins/asyncgenerator.rs @@ -12,6 +12,7 @@ use crate::{ types::{Destructor, IterNext, Iterable, Representable, SelfIter}, }; +use core::sync::atomic::{AtomicBool, Ordering}; use crossbeam_utils::atomic::AtomicCell; fn warn_unawaited_asyncgen_method(ag: &Py, method: &str, vm: &VirtualMachine) { @@ -26,7 +27,8 @@ fn warn_unawaited_asyncgen_method(ag: &Py, method: &str, vm: &Virtua #[derive(Debug)] pub struct PyAsyncGen { inner: Coro, - running_async: AtomicCell, + #[pymember(name = "ag_running")] + running_async: AtomicBool, // whether hooks have been initialized ag_hooks_inited: AtomicCell, // Distinct from the frame being finished: aclose() sets this @@ -70,7 +72,7 @@ impl PyAsyncGen { pub fn new(frame: FrameObjectRef, name: PyStrRef, qualname: PyStrRef) -> Self { Self { inner: Coro::new(frame, name, qualname), - running_async: AtomicCell::new(false), + running_async: AtomicBool::new(false), ag_hooks_inited: AtomicCell::new(false), ag_closed: AtomicCell::new(false), ag_finalizer: PyMutex::new(None), @@ -153,10 +155,6 @@ impl PyAsyncGen { } } #[pygetset] - fn ag_running(&self, _vm: &VirtualMachine) -> bool { - self.running_async.load() - } - #[pygetset] fn ag_code(&self, _vm: &VirtualMachine) -> PyRef { self.inner.code() } @@ -289,12 +287,12 @@ impl PyAsyncGenWrappedValue { ag.ag_closed.store(true); } if async_done { - ag.running_async.store(false); + ag.running_async.store(false, Ordering::Relaxed); } let val = val?.into_async_pyresult(vm)?; match_class!(match val { val @ Self => { - ag.running_async.store(false); + ag.running_async.store(false, Ordering::Relaxed); Err(vm.new_stop_iteration(Some(val.0.clone()))) } val => Ok(val), @@ -348,13 +346,13 @@ impl PyAsyncGenASend { } AwaitableState::Iter => val, // already running, all good AwaitableState::Init => { - if self.ag.running_async.load() { + if self.ag.running_async.load(Ordering::Relaxed) { self.state.store(AwaitableState::Closed); return Err( vm.new_runtime_error("anext(): asynchronous generator is already running") ); } - self.ag.running_async.store(true); + self.ag.running_async.store(true, Ordering::Relaxed); self.state.store(AwaitableState::Iter); if vm.is_none(&val) { self.value.clone() @@ -386,13 +384,13 @@ impl PyAsyncGenASend { ); } AwaitableState::Init => { - if self.ag.running_async.load() { + if self.ag.running_async.load(Ordering::Relaxed) { self.state.store(AwaitableState::Closed); return Err( vm.new_runtime_error("anext(): asynchronous generator is already running") ); } - self.ag.running_async.store(true); + self.ag.running_async.store(true, Ordering::Relaxed); self.state.store(AwaitableState::Iter); } AwaitableState::Iter => {} @@ -498,7 +496,7 @@ impl PyAsyncGenAThrow { return Err(vm.new_stop_iteration(None)); } if matches!(self.state.load(), AwaitableState::Init) { - if self.ag.running_async.load() { + if self.ag.running_async.load(Ordering::Relaxed) { self.state.store(AwaitableState::Closed); let msg = if self.aclose { "aclose(): asynchronous generator is already running" @@ -519,7 +517,7 @@ impl PyAsyncGenAThrow { ); } self.state.store(AwaitableState::Iter); - self.ag.running_async.store(true); + self.ag.running_async.store(true, Ordering::Relaxed); if self.aclose { self.ag.ag_closed.store(true); } @@ -565,7 +563,7 @@ impl PyAsyncGenAThrow { return Err(vm.new_runtime_error("cannot reuse already awaited aclose()/athrow()")); } AwaitableState::Init => { - if self.ag.running_async.load() { + if self.ag.running_async.load(Ordering::Relaxed) { self.state.store(AwaitableState::Closed); let msg = if self.aclose { "aclose(): asynchronous generator is already running" @@ -578,7 +576,7 @@ impl PyAsyncGenAThrow { self.state.store(AwaitableState::Closed); return Err(vm.new_stop_iteration(None)); } - self.ag.running_async.store(true); + self.ag.running_async.store(true, Ordering::Relaxed); self.state.store(AwaitableState::Iter); } AwaitableState::Iter => {} @@ -634,12 +632,12 @@ impl PyAsyncGenAThrow { }) } fn yield_close(&self, vm: &VirtualMachine) -> PyBaseExceptionRef { - self.ag.running_async.store(false); + self.ag.running_async.store(false, Ordering::Relaxed); self.state.store(AwaitableState::Closed); vm.new_runtime_error("async generator ignored GeneratorExit") } fn check_error(&self, exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyBaseExceptionRef { - self.ag.running_async.store(false); + self.ag.running_async.store(false, Ordering::Relaxed); self.state.store(AwaitableState::Closed); if self.aclose && (exc.fast_isinstance(vm.ctx.exceptions.stop_async_iteration) diff --git a/crates/vm/src/builtins/bytes.rs b/crates/vm/src/builtins/bytes.rs index c36422e68..d087ed8d5 100644 --- a/crates/vm/src/builtins/bytes.rs +++ b/crates/vm/src/builtins/bytes.rs @@ -607,13 +607,7 @@ impl PyRef { } } - /// Return a string decoded from the given bytes. - /// Default encoding is 'utf-8'. - /// Default errors is 'strict', meaning that encoding errors raise a UnicodeError. - /// Other possible values are 'ignore', 'replace' - /// For a list of possible encodings, - /// see https://docs.python.org/3/library/codecs.html#standard-encodings - /// currently, only 'utf-8' and 'ascii' implemented + // currently, only 'utf-8' and 'ascii' implemented #[pymethod] fn decode(self, args: DecodeArgs, vm: &VirtualMachine) -> PyResult { bytes_decode(self.into(), args, vm) diff --git a/crates/vm/src/builtins/capsule.rs b/crates/vm/src/builtins/capsule.rs index 19260dba8..a432ae6a2 100644 --- a/crates/vm/src/builtins/capsule.rs +++ b/crates/vm/src/builtins/capsule.rs @@ -7,8 +7,7 @@ use crate::{ use core::ffi::{CStr, c_void}; use core::sync::atomic::AtomicPtr; -/// PyCapsule - a container for C pointers. -/// In RustPython, this is a minimal implementation for compatibility. +// In RustPython, this is a minimal implementation for compatibility. #[pyclass(module = false, name = "PyCapsule")] #[derive(Debug)] pub struct PyCapsule { diff --git a/crates/vm/src/builtins/classmethod.rs b/crates/vm/src/builtins/classmethod.rs index 522dae064..46c0c7d76 100644 --- a/crates/vm/src/builtins/classmethod.rs +++ b/crates/vm/src/builtins/classmethod.rs @@ -4,41 +4,23 @@ use super::{ use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, class::{PyClassDef, PyClassImpl}, - common::lock::PyMutex, function::{FuncArgs, PySetterValue}, + object::PyAtomicRef, types::{Constructor, GetDescriptor, Initializer, Representable}, }; -/// classmethod(function) -> method -/// -/// Convert a function to be a class method. -/// -/// A class method receives the class as implicit first argument, -/// just like an instance method receives the instance. -/// To declare a class method, use this idiom: -/// -/// class C: -/// @classmethod -/// def f(cls, arg1, arg2, ...): -/// ... -/// -/// It can be called either on the class (e.g. C.f()) or on an instance -/// (e.g. C().f()). The instance is ignored except for its class. -/// If a class method is called for a derived class, the derived class -/// object is passed as the implied first argument. -/// -/// Class methods are different than C++ or Java static methods. -/// If you want those, see the staticmethod builtin. #[pyclass(module = false, name = "classmethod", traverse)] #[derive(Debug)] pub struct PyClassMethod { - callable: PyMutex, + #[pymember(name = "__func__")] + #[pymember(name = "__wrapped__")] + callable: PyAtomicRef, } impl From for PyClassMethod { fn from(callable: PyObjectRef) -> Self { Self { - callable: PyMutex::new(callable), + callable: PyAtomicRef::from(callable), } } } @@ -62,7 +44,7 @@ impl GetDescriptor for PyClassMethod { Some(cls) => cls.to_owned(), None => _obj.class().to_owned().into(), }; - let callable = zelf.callable.lock().clone(); + let callable = zelf.callable.load_owned(); Ok(PyBoundMethod::new(cls, callable).into_ref(&vm.ctx).into()) } } @@ -83,7 +65,7 @@ impl Constructor for PyClassMethod { // `None`, matching CPython. let _: ClassMethodArgs = args.bind_for(vm, Self::NAME)?; let classmethod = Self { - callable: PyMutex::new(vm.ctx.none()), + callable: PyAtomicRef::from(vm.ctx.none()), }; let result = PyRef::new_ref(classmethod, cls, Some(vm.ctx.new_dict())); Ok(PyObjectRef::from(result)) @@ -99,7 +81,7 @@ impl Initializer for PyClassMethod { fn init(zelf: &Py, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> { let callable = args.function; - *zelf.callable.lock() = callable.clone(); + zelf.callable.store(callable.clone()); functools_wraps(zelf.as_object(), &callable, vm) } } @@ -116,21 +98,9 @@ impl PyClassMethod { flags(BASETYPE, HAS_DICT, HAS_WEAKREF) )] impl PyClassMethod { - #[pymember] - fn __func__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: &Py = zelf.try_to_value(vm)?; - Ok(zelf.callable.lock().clone()) - } - - #[pymember] - fn __wrapped__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: &Py = zelf.try_to_value(vm)?; - Ok(zelf.callable.lock().clone()) - } - #[pygetset] fn __annotations__(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let callable = zelf.callable.lock(); + let callable = zelf.callable.load_owned(); descriptor_get_wrapped_attribute( &callable, zelf.as_object(), @@ -156,7 +126,7 @@ impl PyClassMethod { #[pygetset] fn __annotate__(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let callable = zelf.callable.lock(); + let callable = zelf.callable.load_owned(); descriptor_get_wrapped_attribute( &callable, zelf.as_object(), @@ -182,7 +152,7 @@ impl PyClassMethod { #[pygetset] fn __isabstractmethod__(&self, vm: &VirtualMachine) -> PyObjectRef { - let callable = self.callable.lock().clone(); + let callable = self.callable.load_owned(); if let Ok(Some(is_abstract)) = vm.get_attribute_opt(&callable, "__isabstractmethod__") { is_abstract } else { @@ -193,7 +163,7 @@ impl PyClassMethod { #[pygetset(setter)] fn set___isabstractmethod__(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { self.callable - .lock() + .load_owned() .set_attr("__isabstractmethod__", value, vm)?; Ok(()) } @@ -211,7 +181,7 @@ impl PyClassMethod { impl Representable for PyClassMethod { #[inline] fn repr_str(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let callable = zelf.callable.lock().repr(vm)?; + let callable = zelf.callable.load_owned().repr(vm)?; let class = Self::class(&vm.ctx); let repr = match ( diff --git a/crates/vm/src/builtins/code.rs b/crates/vm/src/builtins/code.rs index 98e6d344a..d2ea714d8 100644 --- a/crates/vm/src/builtins/code.rs +++ b/crates/vm/src/builtins/code.rs @@ -471,6 +471,12 @@ pub struct CoMonitoringData { #[pyclass(module = false, name = "code")] pub struct PyCode { + #[pymember(name = "co_argcount", path = "arg_count")] + #[pymember(name = "co_posonlyargcount", path = "posonlyarg_count")] + #[pymember(name = "co_kwonlyargcount", path = "kwonlyarg_count")] + #[pymember(name = "co_stacksize", path = "max_stackdepth")] + #[pymember(name = "co_name", path = "obj_name")] + #[pymember(name = "co_qualname", path = "qualname")] pub code: CodeObject, /// Slot-indexed names, equivalent to CPython's `co_localsplusnames`. /// Derived once so frame-local proxy operations do not repeatedly scan @@ -1028,21 +1034,6 @@ impl Constructor for PyCode { flags(HAS_WEAKREF) )] impl PyCode { - #[pygetset] - const fn co_posonlyargcount(&self) -> usize { - self.code.posonlyarg_count as usize - } - - #[pygetset] - const fn co_argcount(&self) -> usize { - self.code.arg_count as usize - } - - #[pygetset] - const fn co_stacksize(&self) -> u32 { - self.code.max_stackdepth - } - #[pygetset] pub fn co_filename(&self) -> PyStrRef { self.source_path().to_owned() @@ -1068,26 +1059,12 @@ impl PyCode { self.code.first_line_number.map_or(0, |n| n.get() as _) } - #[pygetset] - const fn co_kwonlyargcount(&self) -> usize { - self.code.kwonlyarg_count as usize - } - #[pygetset] fn co_consts(&self, vm: &VirtualMachine) -> PyTupleRef { let consts = self.code.constants.iter().map(|x| x.0.clone()).collect(); vm.ctx.new_tuple(consts) } - #[pygetset] - fn co_name(&self) -> PyStrRef { - self.code.obj_name.to_owned() - } - #[pygetset] - fn co_qualname(&self) -> PyStrRef { - self.code.qualname.to_owned() - } - #[pygetset] fn co_names(&self, vm: &VirtualMachine) -> PyTupleRef { let names = self diff --git a/crates/vm/src/builtins/complex.rs b/crates/vm/src/builtins/complex.rs index 48e8cf4b1..bde54d4e9 100644 --- a/crates/vm/src/builtins/complex.rs +++ b/crates/vm/src/builtins/complex.rs @@ -17,12 +17,11 @@ use num_complex::Complex64; use num_traits::Zero; use rustpython_common::hash; -/// Create a complex number from a real part and an optional imaginary part. -/// -/// This is equivalent to (real + imag*1j) where imag defaults to 0. #[pyclass(module = false, name = "complex")] #[derive(Debug, Copy, Clone, PartialEq)] pub struct PyComplex { + #[pymember(name = "real", path = "re")] + #[pymember(name = "imag", path = "im")] value: Complex64, } @@ -551,18 +550,6 @@ impl PyComplex { with(PyRef, Comparable, Hashable, Constructor, AsNumber, Representable) )] impl PyComplex { - #[pymember] - fn real(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: &Py = zelf.try_to_value(vm)?; - Ok(vm.ctx.new_float(zelf.as_complex().re).into()) - } - - #[pymember] - fn imag(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: &Py = zelf.try_to_value(vm)?; - Ok(vm.ctx.new_float(zelf.as_complex().im).into()) - } - #[pymethod] fn conjugate(&self) -> Complex64 { self.value.conj() diff --git a/crates/vm/src/builtins/coroutine.rs b/crates/vm/src/builtins/coroutine.rs index d0a6547c3..60ee5eac2 100644 --- a/crates/vm/src/builtins/coroutine.rs +++ b/crates/vm/src/builtins/coroutine.rs @@ -16,6 +16,7 @@ use crossbeam_utils::atomic::AtomicCell; // PyCoro_Type in CPython pub struct PyCoroutine { inner: Coro, + #[pymember(name = "cr_origin")] origin: Option, } @@ -113,10 +114,6 @@ impl PyCoroutine { self.inner.code() } #[pygetset] - fn cr_origin(&self, _vm: &VirtualMachine) -> Option { - self.origin.clone() - } - #[pygetset] fn cr_suspended(&self, _vm: &VirtualMachine) -> bool { self.inner.suspended() } @@ -134,20 +131,11 @@ impl PyCoroutine { #[pyclass] impl Py { #[pymethod] - /// send(arg) -> send 'arg' into coroutine, - /// return next iterated value or raise StopIteration. fn send(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult { self.inner.send(self.as_object(), value, vm) } #[pymethod] - /// throw(value) - /// throw(type[,value[,traceback]]) - /// - /// Raise exception in coroutine, return next iterated value or raise - /// StopIteration. - /// the (type, val, tb) signature is deprecated, - /// and may be removed in a future version of Python. fn throw( &self, exc_type: PyObjectRef, @@ -166,7 +154,6 @@ impl Py { } #[pymethod] - /// close() -> raise GeneratorExit inside coroutine. fn close(&self, vm: &VirtualMachine) -> PyResult { self.inner.close(self.as_object(), vm) } diff --git a/crates/vm/src/builtins/descriptor.rs b/crates/vm/src/builtins/descriptor.rs index 9c6972e27..a523d3c57 100644 --- a/crates/vm/src/builtins/descriptor.rs +++ b/crates/vm/src/builtins/descriptor.rs @@ -33,10 +33,11 @@ pub struct PyDescriptorOwned { #[pyclass(name = "method_descriptor", module = false)] pub struct PyMethodDescriptor { + #[pymember(name = "__objclass__", path = "typ")] + #[pymember(name = "__name__", path = "name")] pub common: PyDescriptor, pub method: &'static PyMethodDef, // vectorcall: vector_call_func, - pub objclass: &'static Py, // TODO: move to tp_members /// Prevent HeapMethodDef from being freed while this descriptor references it pub(crate) _method_def_owner: Option, } @@ -50,7 +51,6 @@ impl PyMethodDescriptor { qualname: PyRwLock::new(None), }, method, - objclass: typ, _method_def_owner: None, } } @@ -78,6 +78,7 @@ impl GetDescriptor for PyMethodDescriptor { let descr = Self::_as_pyref(zelf, vm).unwrap(); let bound = match obj { Some(obj) => { + method_descr_typecheck(descr, obj, vm)?; if descr.method.flags.contains(PyMethodFlags::METHOD) { if cls .as_ref() @@ -108,6 +109,9 @@ impl Callable for PyMethodDescriptor { type Args = FuncArgs; #[inline] fn call(zelf: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + if let Some(obj) = args.args.first() { + method_descr_typecheck(zelf, obj, vm)?; + } (zelf.method.func)( vm, args, @@ -127,11 +131,6 @@ impl PyMethodDescriptor { flags(METHOD_DESCRIPTOR, DISALLOW_INSTANTIATION) )] impl PyMethodDescriptor { - #[pygetset] - const fn __name__(&self) -> &'static PyStrInterned { - self.common.name - } - #[pygetset] fn __qualname__(&self) -> String { format!("{}.{}", self.common.typ.name(), self.common.name) @@ -149,11 +148,6 @@ impl PyMethodDescriptor { type_::get_text_signature_from_internal_doc(self.method.name, doc) } - #[pygetset] - fn __objclass__(&self) -> PyTypeRef { - self.objclass.to_owned() - } - #[pymethod] fn __reduce__( &self, @@ -179,9 +173,10 @@ impl Representable for PyMethodDescriptor { /// METH_CLASS descriptors. Same layout as method_descriptor; a distinct type. #[pyclass(name = "classmethod_descriptor", module = false)] pub struct PyClassMethodDescriptor { + #[pymember(name = "__objclass__", path = "typ")] + #[pymember(name = "__name__", path = "name")] pub common: PyDescriptor, pub method: &'static PyMethodDef, - pub objclass: &'static Py, pub(crate) _method_def_owner: Option, } @@ -194,7 +189,6 @@ impl PyClassMethodDescriptor { qualname: PyRwLock::new(None), }, method, - objclass: typ, _method_def_owner: None, } } @@ -286,11 +280,6 @@ impl Callable for PyClassMethodDescriptor { flags(DISALLOW_INSTANTIATION) )] impl PyClassMethodDescriptor { - #[pygetset] - const fn __name__(&self) -> &'static PyStrInterned { - self.common.name - } - #[pygetset] fn __qualname__(&self) -> String { format!("{}.{}", self.common.typ.name(), self.common.name) @@ -307,11 +296,6 @@ impl PyClassMethodDescriptor { let doc = self.method.doc?; type_::get_text_signature_from_internal_doc(self.method.name, doc) } - - #[pygetset] - fn __objclass__(&self) -> PyTypeRef { - self.objclass.to_owned() - } } impl Representable for PyClassMethodDescriptor { @@ -329,55 +313,140 @@ impl Representable for PyClassMethodDescriptor { #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(i32)] pub enum MemberKind { + Int = 1, + Double = 4, Object = 6, + Uint = 11, Bool = 14, ObjectEx = 16, + /// `Py_ssize_t`. Writable cells are `AtomicIsize`. + PySsizeT = 19, } impl MemberKind { #[must_use] pub fn from_i32(value: i32) -> Option { match value { + 1 => Some(Self::Int), + 4 => Some(Self::Double), 6 => Some(Self::Object), + 11 => Some(Self::Uint), 14 => Some(Self::Bool), 16 => Some(Self::ObjectEx), + 19 => Some(Self::PySsizeT), _ => None, } } } pub const PY_READONLY: i32 = 1; -pub(crate) const PY_AUDIT_READ: i32 = 2; +#[doc(hidden)] +pub const PY_AUDIT_READ: i32 = 2; pub const PY_RELATIVE_OFFSET: i32 = 8; -pub(crate) type MemberSetterFunc = - Option PyResult<()>>; +/// Kind of a `#[pymember]` field. The macro reads [`MemberLayout::KIND`]. +#[doc(hidden)] +pub trait MemberLayout { + const KIND: MemberKind; +} + +/// Writable `#[pymember]` field. Only an atomic cell may change after publication. +#[doc(hidden)] +pub trait MemberCell: MemberLayout {} + +/// `KIND` of the field named by `probe`. `probe` is not called. +#[doc(hidden)] +#[must_use] +pub const fn member_kind_of( + _: for<'a> fn(&'a Owner) -> &'a T, +) -> MemberKind { + ::KIND +} + +impl MemberLayout for bool { + const KIND: MemberKind = MemberKind::Bool; +} +impl MemberLayout for core::sync::atomic::AtomicBool { + const KIND: MemberKind = MemberKind::Bool; +} +impl MemberCell for core::sync::atomic::AtomicBool {} + +impl MemberLayout for i32 { + const KIND: MemberKind = MemberKind::Int; +} +impl MemberLayout for core::sync::atomic::AtomicI32 { + const KIND: MemberKind = MemberKind::Int; +} +impl MemberCell for core::sync::atomic::AtomicI32 {} + +impl MemberLayout for u32 { + const KIND: MemberKind = MemberKind::Uint; +} +impl MemberLayout for core::sync::atomic::AtomicU32 { + const KIND: MemberKind = MemberKind::Uint; +} +impl MemberCell for core::sync::atomic::AtomicU32 {} + +// One object pointer. Readonly members may be a plain pointer. Writable +// members are an atomic cell: `PyAtomicRef` when the pointer is +// never null, or `PyAtomicRef>` when it may be. +impl MemberLayout for PyObjectRef { + const KIND: MemberKind = MemberKind::Object; +} +impl MemberLayout for Option { + const KIND: MemberKind = MemberKind::Object; +} +impl MemberLayout for PyRef { + const KIND: MemberKind = MemberKind::Object; +} +impl MemberLayout for Option> { + const KIND: MemberKind = MemberKind::Object; +} +impl MemberLayout for crate::object::PyAtomicRef { + const KIND: MemberKind = MemberKind::Object; +} +impl MemberLayout for crate::object::PyAtomicRef> { + const KIND: MemberKind = MemberKind::Object; +} +impl MemberLayout for crate::object::PyAtomicRef> { + const KIND: MemberKind = MemberKind::Object; +} +impl MemberLayout for &'static crate::builtins::PyStrInterned { + const KIND: MemberKind = MemberKind::Object; +} +impl MemberLayout for &'static Py { + const KIND: MemberKind = MemberKind::Object; +} +impl MemberCell for crate::object::PyAtomicRef {} +impl MemberCell for crate::object::PyAtomicRef> {} + +impl MemberLayout for f64 { + const KIND: MemberKind = MemberKind::Double; +} +impl MemberLayout for crate::common::atomic::AtomicF64 { + const KIND: MemberKind = MemberKind::Double; +} +impl MemberCell for crate::common::atomic::AtomicF64 {} + +impl MemberLayout for isize { + const KIND: MemberKind = MemberKind::PySsizeT; +} +impl MemberLayout for core::sync::atomic::AtomicIsize { + const KIND: MemberKind = MemberKind::PySsizeT; +} +impl MemberCell for core::sync::atomic::AtomicIsize {} /// Where `PyMemberDef.offset` points. /// -/// Slot members use a byte offset from the object to an inline pointer cell. -/// Builtin payloads keep fields behind locks, so those members use a function -/// and ignore `offset`. -#[derive(Clone, Copy)] +/// `Offset` is a byte offset from the object to the field. +/// `TupleItem` is a struct-sequence element index: the elements live in the +/// tuple payload, not as separately addressable fields. +#[derive(Clone, Copy, Debug)] pub enum MemberAccess { - Func { - get: fn(&VirtualMachine, PyObjectRef) -> PyResult, - set: MemberSetterFunc, - }, - Slot, + Offset, TupleItem, } -impl core::fmt::Debug for MemberAccess { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Func { set, .. } => f.debug_struct("Func").field("set", &set.is_some()).finish(), - Self::Slot => write!(f, "Slot"), - Self::TupleItem => write!(f, "TupleItem"), - } - } -} - /// Same fields as `PyMemberDef`: name, type, offset, flags, doc. pub struct PyMemberDef { pub name: String, @@ -391,6 +460,10 @@ impl PyMemberDef { pub(crate) fn readonly(&self) -> bool { self.flags & PY_READONLY != 0 } + + pub(crate) fn audit_read(&self) -> bool { + self.flags & PY_AUDIT_READ != 0 + } } impl core::fmt::Debug for PyMemberDef { @@ -405,21 +478,69 @@ impl core::fmt::Debug for PyMemberDef { } } +/// Const-constructible member spec. Registered as a `PyMemberDef`. +#[derive(Clone, Copy)] +pub struct PyMemberSpec { + pub name: &'static str, + pub kind: MemberKind, + pub offset: isize, + pub flags: i32, + pub doc: Option<&'static str>, +} + +impl PyMemberSpec { + /// Concatenate cfg-gated member groups into one table. + #[must_use] + pub const fn concat(parts: &[&[Self]]) -> [Self; N] { + const EMPTY: PyMemberSpec = PyMemberSpec { + name: "", + kind: MemberKind::Object, + offset: 0, + flags: 0, + doc: None, + }; + let mut out = [EMPTY; N]; + let mut index = 0; + let mut part_index = 0; + while part_index < parts.len() { + let part = parts[part_index]; + let mut item_index = 0; + while item_index < part.len() { + out[index] = part[item_index]; + index += 1; + item_index += 1; + } + part_index += 1; + } + out + } +} + // = PyMemberDescrObject #[pyclass(name = "member_descriptor", module = false)] #[derive(Debug)] pub struct PyMemberDescriptor { + #[pymember(name = "__objclass__", path = "typ")] + #[pymember(name = "__name__", path = "name")] pub common: PyDescriptorOwned, pub member: PyMemberDef, pub access: MemberAccess, } impl PyMemberDescriptor { - /// Byte offset of an instance-slot member. `None` when this is not a slot. + /// Byte offset of an object-pointer member. `None` for bool, float, and + /// struct-sequence indexes, which slot specialization must not treat as cells. pub(crate) fn slot_offset(&self) -> Option { - match self.access { - MemberAccess::Slot => Some(self.member.offset), - _ => None, + if matches!(self.access, MemberAccess::TupleItem) { + return None; + } + match self.member.kind { + MemberKind::Object | MemberKind::ObjectEx => Some(self.member.offset), + MemberKind::Bool + | MemberKind::Double + | MemberKind::Int + | MemberKind::Uint + | MemberKind::PySsizeT => None, } } @@ -430,8 +551,7 @@ impl PyMemberDescriptor { })?; } match self.access { - MemberAccess::Func { get, .. } => get(vm, obj), - MemberAccess::Slot => get_slot_from_object(&obj, self.member.offset, &self.member, vm), + MemberAccess::Offset => member_get_one(&obj, self.member.offset, &self.member, vm), MemberAccess::TupleItem => { let index = self.member.offset as usize; let tuple = obj.downcast_ref::().ok_or_else(|| { @@ -456,12 +576,8 @@ impl PyMemberDescriptor { return Err(vm.new_attribute_error("readonly attribute")); } match self.access { - MemberAccess::Func { set, .. } => match set { - Some(set) => set(vm, obj, value), - None => Err(vm.new_attribute_error("readonly attribute")), - }, - MemberAccess::Slot => { - set_slot_at_object(&obj, self.member.offset, &self.member, value, vm) + MemberAccess::Offset => { + member_set_one(&obj, self.member.offset, &self.member, value, vm) } MemberAccess::TupleItem => Err(vm.new_attribute_error("readonly attribute")), } @@ -487,18 +603,6 @@ fn calculate_qualname(descr: &PyDescriptorOwned, vm: &VirtualMachine) -> PyResul #[pyclass(with(GetDescriptor, Representable), flags(DISALLOW_INSTANTIATION))] impl PyMemberDescriptor { - #[pymember] - fn __objclass__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: &Py = zelf.try_to_value(vm)?; - Ok(zelf.common.typ.clone().into()) - } - - #[pymember] - fn __name__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: &Py = zelf.try_to_value(vm)?; - Ok(zelf.common.name.to_owned().into()) - } - #[pygetset] fn __doc__(&self) -> Option { self.member.doc.to_owned() @@ -556,18 +660,86 @@ impl PyMemberDescriptor { } } -// PyMember_GetOne for a value stored in the instance slot array. -fn get_slot_from_object( +fn member_addr(obj: &PyObject, offset: isize) -> *mut u8 { + (obj as *const PyObject as *const u8).wrapping_add(offset as usize) as *mut u8 +} + +fn warn_member(vm: &VirtualMachine, message: &str) -> PyResult<()> { + crate::warn::warn( + vm.ctx.new_str(message).into(), + Some(vm.ctx.exceptions.runtime_warning.to_owned()), + 1, + None, + vm, + ) +} + +fn load_i32(obj: &PyObject, offset: isize, readonly: bool) -> i32 { + let addr = member_addr(obj, offset); + if readonly { + // SAFETY: a readonly int member addresses an `i32` that is not written + // after publication. + unsafe { addr.cast::().read() } + } else { + // SAFETY: a writable int member addresses an aligned `AtomicI32`. + unsafe { + (*addr.cast::()) + .load(core::sync::atomic::Ordering::Relaxed) + } + } +} + +fn load_u32(obj: &PyObject, offset: isize, readonly: bool) -> u32 { + let addr = member_addr(obj, offset); + if readonly { + // SAFETY: a readonly uint member addresses a `u32` that is not written + // after publication. + unsafe { addr.cast::().read() } + } else { + // SAFETY: a writable uint member addresses an aligned `AtomicU32`. + unsafe { + (*addr.cast::()) + .load(core::sync::atomic::Ordering::Relaxed) + } + } +} + +fn member_as_c_long(value: &PyObject, vm: &VirtualMachine) -> PyResult { + let int_obj = value.try_index(vm)?; + core::ffi::c_long::try_from(int_obj.as_bigint()) + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C long")) +} + +fn member_uint_value( + value: &PyObject, + vm: &VirtualMachine, +) -> PyResult<(u32, Option<&'static str>)> { + let int_obj = value.try_index(vm)?; + let big = int_obj.as_bigint(); + if big.sign() == malachite_bigint::Sign::Minus { + let long_val = core::ffi::c_long::try_from(big) + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C long"))?; + // Keeps the low 32 bits whether `c_long` is 32 or 64 bits wide. + let stored = long_val as u32; + return Ok((stored, Some("Writing negative value into unsigned field"))); + } + core::ffi::c_ulong::try_from(big) + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C unsigned long"))?; + let wide = u64::try_from(big).expect("a value that fits c_ulong fits u64"); + let stored = wide as u32; + let warning = (wide > u64::from(u32::MAX)).then_some("Truncation of value to unsigned int"); + Ok((stored, warning)) +} + +// PyMember_GetOne. `offset` is a byte offset from the object to the field. +fn member_get_one( obj: &PyObject, offset: isize, member: &PyMemberDef, vm: &VirtualMachine, ) -> PyResult { - let slot = match member.kind { + let value = match member.kind { MemberKind::Object => obj.get_slot(offset).unwrap_or_else(|| vm.ctx.none()), - MemberKind::Bool => obj - .get_slot(offset) - .unwrap_or_else(|| vm.ctx.new_bool(false).into()), MemberKind::ObjectEx => obj.get_slot(offset).ok_or_else(|| { vm.new_attribute_error(format!( "'{}' object has no attribute '{}'", @@ -575,12 +747,62 @@ fn get_slot_from_object( member.name )) })?, + MemberKind::Bool => { + // SAFETY: a bool member addresses an `AtomicBool` or a `bool` + // (one byte, naturally aligned). The macro rejects any other field + // type. A plain `bool` is not written after the object is published. + let raw = unsafe { + (*member_addr(obj, offset).cast::()) + .load(core::sync::atomic::Ordering::Relaxed) + }; + vm.ctx.new_bool(raw).into() + } + MemberKind::Int => vm + .ctx + .new_int(load_i32(obj, offset, member.readonly())) + .into(), + MemberKind::Uint => vm + .ctx + .new_int(load_u32(obj, offset, member.readonly())) + .into(), + MemberKind::Double => { + let raw = if member.readonly() { + // SAFETY: a readonly double member addresses an `f64` that is + // not written after publication. A plain `f64` may be only + // 4-byte aligned, so this is a plain read and not an atomic + // access. + unsafe { member_addr(obj, offset).cast::().read() } + } else { + // SAFETY: a writable double member addresses an aligned + // `AtomicF64`. The macro accepts only `MemberCell`. + unsafe { + (*member_addr(obj, offset).cast::()) + .load(core::sync::atomic::Ordering::Relaxed) + } + }; + vm.ctx.new_float(raw).into() + } + MemberKind::PySsizeT => { + let raw = if member.readonly() { + // SAFETY: a readonly `Py_ssize_t` member addresses an `isize` + // that is not written after publication. + unsafe { member_addr(obj, offset).cast::().read() } + } else { + // SAFETY: a writable `Py_ssize_t` member addresses an aligned + // `AtomicIsize`. The macro accepts only `MemberCell`. + unsafe { + (*member_addr(obj, offset).cast::()) + .load(core::sync::atomic::Ordering::Relaxed) + } + }; + vm.ctx.new_int(raw).into() + } }; - Ok(slot) + Ok(value) } -// PyMember_SetOne for a value stored in the instance slot array. -fn set_slot_at_object( +// PyMember_SetOne. +fn member_set_one( obj: &PyObject, offset: isize, member: &PyMemberDef, @@ -588,35 +810,17 @@ fn set_slot_at_object( vm: &VirtualMachine, ) -> PyResult<()> { if matches!(value, PySetterValue::Delete) - && member.kind != MemberKind::ObjectEx - && member.kind != MemberKind::Object + && !matches!(member.kind, MemberKind::Object | MemberKind::ObjectEx) { return Err(vm.new_type_error("can't delete numeric/char attribute")); } match member.kind { MemberKind::Object => match value { - PySetterValue::Assign(v) => { - obj.set_slot(offset, Some(v)); - } - PySetterValue::Delete => { - obj.set_slot(offset, None); - } - }, - MemberKind::Bool => match value { - PySetterValue::Assign(v) => { - if !v.class().is(vm.ctx.types.bool_type) { - return Err(vm.new_type_error("attribute value type must be bool")); - } - obj.set_slot(offset, Some(v)) - } - PySetterValue::Delete => { - return Err(vm.new_type_error("can't delete numeric/char attribute")); - } + PySetterValue::Assign(v) => obj.set_slot(offset, Some(v)), + PySetterValue::Delete => obj.set_slot(offset, None), }, MemberKind::ObjectEx => match value { - PySetterValue::Assign(v) => { - obj.set_slot(offset, Some(v)); - } + PySetterValue::Assign(v) => obj.set_slot(offset, Some(v)), PySetterValue::Delete => { if obj.get_slot(offset).is_none() { return Err(vm.new_attribute_error(member.name.clone())); @@ -624,8 +828,87 @@ fn set_slot_at_object( obj.set_slot(offset, None); } }, + MemberKind::Bool => { + let PySetterValue::Assign(value) = value else { + return Err(vm.new_type_error("can't delete numeric/char attribute")); + }; + if !value.class().is(vm.ctx.types.bool_type) { + return Err(vm.new_type_error("attribute value type must be bool")); + } + let stored = value.is(&vm.ctx.true_value); + // SAFETY: a writable bool member addresses an `AtomicBool`. + unsafe { + (*member_addr(obj, offset).cast::()) + .store(stored, core::sync::atomic::Ordering::Relaxed); + } + } + MemberKind::Int => { + let PySetterValue::Assign(value) = value else { + return Err(vm.new_type_error("can't delete numeric/char attribute")); + }; + let long_val = member_as_c_long(&value, vm)?; + let stored = long_val as i32; + // SAFETY: a writable int member addresses an aligned `AtomicI32`. + // Readonly members are rejected before this call. + unsafe { + (*member_addr(obj, offset).cast::()) + .store(stored, core::sync::atomic::Ordering::Relaxed); + } + let truncated = long_val > i32::MAX as core::ffi::c_long + || long_val < i32::MIN as core::ffi::c_long; + if truncated { + warn_member(vm, "Truncation of value to int")?; + } + } + MemberKind::Uint => { + let PySetterValue::Assign(value) = value else { + return Err(vm.new_type_error("can't delete numeric/char attribute")); + }; + let (stored, warning) = member_uint_value(&value, vm)?; + // SAFETY: a writable uint member addresses an aligned `AtomicU32`. + // Readonly members are rejected before this call. + unsafe { + (*member_addr(obj, offset).cast::()) + .store(stored, core::sync::atomic::Ordering::Relaxed); + } + if let Some(warning) = warning { + warn_member(vm, warning)?; + } + } + MemberKind::Double => { + let PySetterValue::Assign(value) = value else { + return Err(vm.new_type_error("can't delete numeric/char attribute")); + }; + let number = value.try_float(vm)?.to_f64(); + // SAFETY: a writable double member addresses an aligned `AtomicF64`. + // Readonly members are rejected before this call. + unsafe { + (*member_addr(obj, offset).cast::()) + .store(number, core::sync::atomic::Ordering::Relaxed); + } + } + MemberKind::PySsizeT => { + let PySetterValue::Assign(value) = value else { + return Err(vm.new_type_error("can't delete numeric/char attribute")); + }; + // PyLong_AsSsize_t: an int (bool included). No `__index__`. + if !value.fast_isinstance(vm.ctx.types.int_type) { + return Err(vm.new_type_error("an integer is required")); + } + let Some(int_obj) = value.downcast_ref::() else { + return Err(vm.new_type_error("an integer is required")); + }; + let stored = isize::try_from(int_obj.as_bigint()).map_err(|_| { + vm.new_overflow_error("Python int too large to convert to C ssize_t") + })?; + // SAFETY: a writable `Py_ssize_t` member addresses an aligned + // `AtomicIsize`. Readonly members are rejected before this call. + unsafe { + (*member_addr(obj, offset).cast::()) + .store(stored, core::sync::atomic::Ordering::Relaxed); + } + } } - Ok(()) } @@ -665,6 +948,25 @@ impl GetDescriptor for PyMemberDescriptor { } } +fn method_descr_typecheck( + descr: &PyMethodDescriptor, + obj: &PyObject, + vm: &VirtualMachine, +) -> PyResult<()> { + if descr.method.flags.contains(PyMethodFlags::STATIC) + || descr.method.flags.contains(PyMethodFlags::CLASS) + || obj.fast_isinstance(descr.common.typ) + { + return Ok(()); + } + Err(vm.new_type_error(format!( + "descriptor '{}' for '{}' objects doesn't apply to a '{}' object", + descr.common.name.as_str(), + descr.common.typ.name(), + obj.class().name() + ))) +} + /// Vectorcall for method_descriptor: calls native method directly fn vectorcall_method_descriptor( zelf_obj: &PyObject, @@ -674,6 +976,11 @@ fn vectorcall_method_descriptor( vm: &VirtualMachine, ) -> PyResult { let zelf: &Py = zelf_obj.downcast_ref().unwrap(); + if nargs > 0 + && let Some(obj) = args.first() + { + method_descr_typecheck(zelf, obj, vm)?; + } let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames); (zelf.method.func)( vm, @@ -1058,7 +1365,9 @@ fn parse_buffer_flags( #[pyclass(name = "wrapper_descriptor", module = false)] #[derive(Debug)] pub(crate) struct PyWrapper { + #[pymember(name = "__objclass__")] pub typ: &'static Py, + #[pymember(name = "__name__")] pub name: &'static PyStrInterned, pub wrapped: SlotFunc, /// Slot text, including the text signature. @@ -1119,21 +1428,11 @@ impl Callable for PyWrapper { flags(DISALLOW_INSTANTIATION) )] impl PyWrapper { - #[pygetset] - fn __name__(&self) -> &'static PyStrInterned { - self.name - } - #[pygetset] fn __qualname__(&self) -> String { format!("{}.{}", self.typ.name(), self.name) } - #[pygetset] - fn __objclass__(&self) -> PyTypeRef { - self.typ.to_owned() - } - #[pygetset] fn __doc__(&self) -> Option<&'static str> { if let Some(doc) = self.plain_doc { @@ -1171,6 +1470,7 @@ impl Representable for PyWrapper { #[derive(Debug)] pub(crate) struct PyMethodWrapper { pub wrapper: PyRef, + #[pymember(name = "__self__")] #[pytraverse(skip)] pub obj: PyObjectRef, } @@ -1203,11 +1503,6 @@ impl Callable for PyMethodWrapper { flags(DISALLOW_INSTANTIATION) )] impl PyMethodWrapper { - #[pygetset] - fn __self__(&self) -> PyObjectRef { - self.obj.clone() - } - #[pygetset] fn __name__(&self) -> &'static PyStrInterned { self.wrapper.name diff --git a/crates/vm/src/builtins/dict.rs b/crates/vm/src/builtins/dict.rs index ebd43cab5..2bed68c59 100644 --- a/crates/vm/src/builtins/dict.rs +++ b/crates/vm/src/builtins/dict.rs @@ -590,7 +590,6 @@ impl Py { Ok(PyArithmeticValue::Implemented(true)) } - /// Return self[key]. #[cfg_attr(feature = "flame-it", flame("PyDictRef"))] #[pymethod(coexist)] fn __getitem__(&self, key: PyObjectRef, vm: &VirtualMachine) -> PyResult { diff --git a/crates/vm/src/builtins/frame.rs b/crates/vm/src/builtins/frame.rs index 045375f1a..ceb0779a4 100644 --- a/crates/vm/src/builtins/frame.rs +++ b/crates/vm/src/builtins/frame.rs @@ -6,7 +6,7 @@ use super::{PyAsyncGen, PyCode, PyCoroutine, PyDictRef, PyGenerator, PyIntRef}; use crate::{ AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, class::PyClassImpl, - frame::{FrameObject, FrameObjectRef, FrameOwner}, + frame::{FrameObject, FrameOwner}, function::PySetterValue, types::{Destructor, Representable}, }; @@ -771,69 +771,24 @@ impl FrameObject { } } - #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")] - #[pymember(type = "bool")] - fn f_trace_lines(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: FrameObjectRef = zelf.downcast().unwrap_or_else(|_| unreachable!()); - - let boxed = zelf.iframe().cold().trace_lines.lock(); - Ok(vm.ctx.new_bool(*boxed).into()) + #[pygetset] + fn f_trace_opcodes(&self, vm: &VirtualMachine) -> PyObjectRef { + let trace_opcodes = self.iframe().cold().trace_opcodes.lock(); + vm.ctx.new_bool(*trace_opcodes).into() } - #[pymember(type = "bool", setter)] - fn set_f_trace_lines( - vm: &VirtualMachine, - zelf: PyObjectRef, - value: PySetterValue, - ) -> PyResult<()> { + #[pygetset(setter)] + fn set_f_trace_opcodes(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> { match value { PySetterValue::Assign(value) => { - let zelf: FrameObjectRef = zelf.downcast().unwrap_or_else(|_| unreachable!()); - let value: PyIntRef = value .downcast() .map_err(|_| vm.new_type_error("attribute value type must be bool"))?; let val = !value.as_bigint().is_zero(); - *zelf.iframe().cold().trace_lines.lock() = val; + *self.iframe().cold().trace_opcodes.lock() = val; // Propagate to live source iframe. - let live = zelf.find_live_source_iframe(); - if !live.is_null() { - *unsafe { &*live }.cold().trace_lines.lock() = val; - } - - Ok(()) - } - PySetterValue::Delete => Err(vm.new_type_error("can't delete numeric/char attribute")), - } - } - - #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")] - #[pymember(type = "bool")] - fn f_trace_opcodes(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: FrameObjectRef = zelf.downcast().unwrap_or_else(|_| unreachable!()); - let trace_opcodes = zelf.iframe().cold().trace_opcodes.lock(); - Ok(vm.ctx.new_bool(*trace_opcodes).into()) - } - - #[pymember(type = "bool", setter)] - fn set_f_trace_opcodes( - vm: &VirtualMachine, - zelf: PyObjectRef, - value: PySetterValue, - ) -> PyResult<()> { - match value { - PySetterValue::Assign(value) => { - let zelf: FrameObjectRef = zelf.downcast().unwrap_or_else(|_| unreachable!()); - - let value: PyIntRef = value - .downcast() - .map_err(|_| vm.new_type_error("attribute value type must be bool"))?; - - let val = !value.as_bigint().is_zero(); - *zelf.iframe().cold().trace_opcodes.lock() = val; - // Propagate to live source iframe. - let live = zelf.find_live_source_iframe(); + let live = self.find_live_source_iframe(); if !live.is_null() { *unsafe { &*live }.cold().trace_opcodes.lock() = val; } diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index c8a574185..a63e67f2b 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -65,8 +65,11 @@ fn format_missing_args( #[derive(Debug)] pub struct PyFunction { pub(crate) code: PyAtomicRef, + #[pymember(name = "__globals__")] pub(crate) globals: PyDictRef, + #[pymember(name = "__builtins__")] pub(crate) builtins: PyObjectRef, + #[pymember(name = "__closure__")] pub(crate) closure: Option>>, defaults_and_kwdefaults: PyMutex<(Option, Option)>, name: PyMutex, @@ -74,8 +77,10 @@ pub struct PyFunction { type_params: PyMutex, annotations: PyMutex>, annotate: PyMutex>, - module: PyMutex, - doc: PyMutex, + #[pymember(name = "__module__", writable)] + module: PyAtomicRef>, + #[pymember(name = "__doc__", writable)] + doc: PyAtomicRef>, func_version: AtomicU32, #[cfg(feature = "jit")] jitted_code: PyMutex>, @@ -106,8 +111,8 @@ unsafe impl Traverse for PyFunction { // Traverse additional fields that may contain references self.type_params.lock().traverse(tracer_fn); self.annotations.lock().traverse(tracer_fn); - self.module.lock().traverse(tracer_fn); - self.doc.lock().traverse(tracer_fn); + self.module.traverse(tracer_fn); + self.doc.traverse(tracer_fn); } fn clear(&mut self, out: &mut Vec) { @@ -139,14 +144,10 @@ unsafe impl Traverse for PyFunction { } // Clear module, doc, and type_params (Py_CLEAR) - if let Some(mut guard) = self.module.try_lock() { - let old_module = - core::mem::replace(&mut *guard, Context::genesis().none.to_owned().into()); + if let Some(old_module) = self.module.store(Some(Context::genesis().none())) { out.push(old_module); } - if let Some(mut guard) = self.doc.try_lock() { - let old_doc = - core::mem::replace(&mut *guard, Context::genesis().none.to_owned().into()); + if let Some(old_doc) = self.doc.store(Some(Context::genesis().none())) { out.push(old_doc); } if let Some(mut guard) = self.type_params.try_lock() { @@ -213,8 +214,8 @@ impl PyFunction { type_params: PyMutex::new(vm.ctx.empty_tuple.clone()), annotations: PyMutex::new(None), annotate: PyMutex::new(None), - module: PyMutex::new(module), - doc: PyMutex::new(doc), + module: PyAtomicRef::from(Some(module)), + doc: PyAtomicRef::from(Some(doc)), func_version: AtomicU32::new(next_func_version()), #[cfg(feature = "jit")] jitted_code: PyMutex::new(None), @@ -536,7 +537,8 @@ impl PyFunction { })? .into_pyref(); - self.closure = Some(closure_tuple.try_into_typed::(vm)?); + let typed = closure_tuple.try_into_typed::(vm)?; + self.closure = Some(typed); } bytecode::MakeFunctionFlag::TypeParams => { let type_params = attr_value.clone().downcast::().map_err(|_| { @@ -1051,29 +1053,6 @@ impl PyFunction { crate::stdlib::_testinternalcapi::note_func_modification(); } - // {"__closure__", T_OBJECT, OFF(func_closure), READONLY}, - // {"__doc__", T_OBJECT, OFF(func_doc), 0}, - // {"__globals__", T_OBJECT, OFF(func_globals), READONLY}, - // {"__module__", T_OBJECT, OFF(func_module), 0}, - // {"__builtins__", T_OBJECT, OFF(func_builtins), READONLY}, - #[pymember] - fn __globals__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf = Self::_as_pyref(&zelf, vm)?; - Ok(zelf.globals.clone().into()) - } - - #[pymember] - fn __closure__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf = Self::_as_pyref(&zelf, vm)?; - Ok(vm.unwrap_or_none(zelf.closure.clone().map(|x| x.into()))) - } - - #[pymember] - fn __builtins__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf = Self::_as_pyref(&zelf, vm)?; - Ok(zelf.builtins.clone()) - } - #[pygetset] fn __name__(&self) -> PyStrRef { self.name.lock().clone() @@ -1084,38 +1063,6 @@ impl PyFunction { *self.name.lock() = name; } - #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")] - #[pymember] - fn __doc__(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { - // When accessed from instance, obj is the PyFunction instance - if let Ok(func) = obj.downcast::() { - let doc = func.doc.lock(); - Ok(doc.clone()) - } else { - // When accessed from class, return None as there's no instance - Ok(vm.ctx.none()) - } - } - - #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")] - #[pymember(setter)] - fn set___doc__(vm: &VirtualMachine, zelf: PyObjectRef, value: PySetterValue) -> PyResult<()> { - let zelf: PyRef = zelf.downcast().unwrap_or_else(|_| unreachable!()); - let value = value.unwrap_or_none(vm); - *zelf.doc.lock() = value; - Ok(()) - } - - #[pygetset] - fn __module__(&self) -> PyObjectRef { - self.module.lock().clone() - } - - #[pygetset(setter)] - fn set___module__(&self, module: PySetterValue, vm: &VirtualMachine) { - *self.module.lock() = module.unwrap_or_none(vm); - } - #[pygetset] fn __annotations__(&self, vm: &VirtualMachine) -> PyResult { // First check if we have cached annotations @@ -1393,7 +1340,9 @@ impl Constructor for PyFunction { #[pyclass(module = false, name = "method", traverse)] #[derive(Debug)] pub struct PyBoundMethod { + #[pymember(name = "__self__")] object: PyObjectRef, + #[pymember(name = "__func__")] function: PyObjectRef, } @@ -1531,16 +1480,6 @@ impl PyBoundMethod { self.function.get_attr("__doc__", vm) } - #[pygetset] - fn __func__(&self) -> PyObjectRef { - self.function.clone() - } - - #[pygetset(name = "__self__")] - fn get_self(&self) -> PyObjectRef { - self.object.clone() - } - #[pygetset] fn __module__(&self, vm: &VirtualMachine) -> Option { self.function.get_attr("__module__", vm).ok() diff --git a/crates/vm/src/builtins/genericalias.rs b/crates/vm/src/builtins/genericalias.rs index 3a9a7f0cc..46de0bec6 100644 --- a/crates/vm/src/builtins/genericalias.rs +++ b/crates/vm/src/builtins/genericalias.rs @@ -36,9 +36,12 @@ static ATTR_BLOCKED: [&str; 3] = ["__bases__", "__copy__", "__deepcopy__"]; #[pyclass(module = "types", name = "GenericAlias")] pub struct PyGenericAlias { + #[pymember(name = "__origin__")] origin: PyObjectRef, + #[pymember(name = "__args__")] args: PyTupleRef, parameters: PyTupleRef, + #[pymember(name = "__unpacked__")] starred: bool, // for __unpacked__ attribute } @@ -221,21 +224,6 @@ impl PyGenericAlias { self.parameters.clone().into() } - #[pygetset] - fn __args__(&self) -> PyObjectRef { - self.args.clone().into() - } - - #[pygetset] - fn __origin__(&self) -> PyObjectRef { - self.origin.clone() - } - - #[pygetset] - const fn __unpacked__(&self) -> bool { - self.starred - } - #[pygetset] fn __typing_unpacked_tuple_args__(&self, vm: &VirtualMachine) -> PyObjectRef { if self.starred && self.origin.is(vm.ctx.types.tuple_type.as_object()) { @@ -253,7 +241,7 @@ impl PyGenericAlias { #[pymethod] fn __dir__(&self, vm: &VirtualMachine) -> PyResult { - let dir = vm.dir(Some(self.__origin__()))?; + let dir = vm.dir(Some(self.origin.clone()))?; for exc in &ATTR_EXCEPTIONS { let exc_obj = (*exc).to_pyobject(vm); if !dir.__contains__(&exc_obj, vm)? { @@ -291,7 +279,7 @@ impl PyGenericAlias { #[pymethod] fn __mro_entries__(&self, _object: PyObjectRef, vm: &VirtualMachine) -> PyTupleRef { - PyTuple::new_ref(vec![self.__origin__()], &vm.ctx) + PyTuple::new_ref(vec![self.origin.clone()], &vm.ctx) } #[pymethod] @@ -643,10 +631,10 @@ impl Comparable for PyGenericAlias { return Ok(PyComparisonValue::Implemented(false)); } Ok(PyComparisonValue::Implemented( - zelf.__origin__() - .rich_compare_bool(&other.__origin__(), PyComparisonOp::Eq, vm)? - && zelf.__args__().rich_compare_bool( - &other.__args__(), + zelf.origin + .rich_compare_bool(&other.origin, PyComparisonOp::Eq, vm)? + && zelf.args.as_object().rich_compare_bool( + other.args.as_object(), PyComparisonOp::Eq, vm, )?, @@ -675,7 +663,7 @@ impl GetAttr for PyGenericAlias { return zelf.as_object().generic_getattr(attr, vm); } } - zelf.__origin__().get_attr(attr, vm) + zelf.origin.get_attr(attr, vm) } } diff --git a/crates/vm/src/builtins/getset.rs b/crates/vm/src/builtins/getset.rs index 6b2699d3a..effdeabd4 100644 --- a/crates/vm/src/builtins/getset.rs +++ b/crates/vm/src/builtins/getset.rs @@ -11,9 +11,11 @@ use crate::{ #[pyclass(module = false, name = "getset_descriptor", traverse = "manual")] pub struct PyGetSet { - name: String, - /// `d_type`. Owned: a type's namespace can outlive the type, and the - /// descriptors it holds have to stay valid for as long as it does. + #[pymember(name = "__name__")] + name: &'static crate::builtins::PyStrInterned, + // `d_type`. Owned: a type's namespace can outlive the type, and the + // descriptors it holds have to stay valid for as long as it does. + #[pymember(name = "__objclass__")] class: PyRef, getter: Option, setter: Option, @@ -25,7 +27,7 @@ impl core::fmt::Debug for PyGetSet { write!( f, "PyGetSet {{ name: {}, getter: {}, setter: {} }}", - self.name, + self.name.as_str(), if self.getter.is_some() { "Some" } else { @@ -70,7 +72,7 @@ impl GetDescriptor for PyGetSet { } else { Err(vm.new_attribute_error(format!( "attribute '{}' of '{}' objects is not readable", - zelf.name, + zelf.name.as_str(), Self::class(&vm.ctx).name() ))) } @@ -79,9 +81,9 @@ impl GetDescriptor for PyGetSet { impl PyGetSet { #[must_use] - pub fn new(name: &str, class: &Py) -> Self { + pub fn new(name: &str, class: &Py, ctx: &Context) -> Self { Self { - name: name.into(), + name: ctx.intern_str(name), class: class.to_owned(), getter: None, setter: None, @@ -131,32 +133,21 @@ impl PyGetSet { } else { Err(vm.new_attribute_error(format!( "attribute '{}' of '{}' objects is not writable", - zelf.name, + zelf.name.as_str(), obj.class().name() ))) } } - #[pygetset] - fn __name__(&self) -> String { - self.name.clone() - } - #[pygetset] fn __qualname__(&self) -> String { - format!("{}.{}", self.class.slot_name(), self.name.clone()) + format!("{}.{}", self.class.slot_name(), self.name.as_str()) } #[pygetset] fn __doc__(&self) -> Option { self.doc.clone() } - - #[pymember] - fn __objclass__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: &Py = zelf.try_to_value(vm)?; - Ok(zelf.class.clone().into()) - } } impl Representable for PyGetSet { @@ -165,11 +156,11 @@ impl Representable for PyGetSet { let class = &zelf.class; // Special case for object type if class.is(vm.ctx.types.object_type) { - Ok(format!("", zelf.name)) + Ok(format!("", zelf.name.as_str())) } else { Ok(format!( "", - zelf.name, + zelf.name.as_str(), class.name() )) } diff --git a/crates/vm/src/builtins/interpolation.rs b/crates/vm/src/builtins/interpolation.rs index 8f088a007..73126102e 100644 --- a/crates/vm/src/builtins/interpolation.rs +++ b/crates/vm/src/builtins/interpolation.rs @@ -13,15 +13,16 @@ use crate::{ use itertools::Itertools; use rustpython_common::wtf8::Wtf8Buf; -/// Interpolation object for t-strings (PEP 750). -/// -/// Represents an interpolated expression within a template string. #[pyclass(module = "string.templatelib", name = "Interpolation")] #[derive(Debug, Clone)] pub struct PyInterpolation { + #[pymember(type = "object_ex")] pub value: PyObjectRef, + #[pymember(type = "object_ex")] pub expression: PyStrRef, + #[pymember(type = "object_ex")] pub conversion: PyObjectRef, // None or 's', 'r', 'a' + #[pymember(type = "object_ex")] pub format_spec: PyStrRef, } @@ -119,26 +120,6 @@ impl PyInterpolation { ]) } - #[pygetset] - fn value(&self) -> PyObjectRef { - self.value.clone() - } - - #[pygetset] - fn expression(&self) -> PyStrRef { - self.expression.clone() - } - - #[pygetset] - fn conversion(&self) -> PyObjectRef { - self.conversion.clone() - } - - #[pygetset] - fn format_spec(&self) -> PyStrRef { - self.format_spec.clone() - } - #[pyclassmethod] fn __class_getitem__( cls: PyTypeRef, diff --git a/crates/vm/src/builtins/list.rs b/crates/vm/src/builtins/list.rs index 90c7e4f9d..2e478e17f 100644 --- a/crates/vm/src/builtins/list.rs +++ b/crates/vm/src/builtins/list.rs @@ -306,7 +306,6 @@ impl PyList { } } - /// Return self[index]. #[pymethod(coexist)] fn __getitem__(&self, index: PyObjectRef, vm: &VirtualMachine) -> PyResult { self._getitem(&index, vm) diff --git a/crates/vm/src/builtins/mod.rs b/crates/vm/src/builtins/mod.rs index f6e9f686d..f74660c3a 100644 --- a/crates/vm/src/builtins/mod.rs +++ b/crates/vm/src/builtins/mod.rs @@ -98,7 +98,7 @@ pub use zip::PyZip; #[path = "union.rs"] pub(crate) mod union_; pub use union_::{PyUnion, make_union}; -pub(crate) mod descriptor; +pub mod descriptor; pub use descriptor::{ MemberAccess, MemberKind, PY_READONLY, PY_RELATIVE_OFFSET, PyDescriptorOwned, PyMemberDef as DescriptorMemberDef, PyMemberDescriptor, diff --git a/crates/vm/src/builtins/module.rs b/crates/vm/src/builtins/module.rs index e37d5a2ba..aae75da81 100644 --- a/crates/vm/src/builtins/module.rs +++ b/crates/vm/src/builtins/module.rs @@ -83,6 +83,9 @@ impl PyModuleDef { } #[pyclass(module = false, name = "module")] +// The dict lives in the object extension, not the payload. The offset is +// the dict cell at the front of that extension. +#[pymember(name = "__dict__", offset = ::rustpython_vm::object::dict_member_offset())] #[derive(Debug)] pub struct PyModule { // PyObject *md_dict; @@ -292,13 +295,6 @@ impl PyModule { Self::new().into_ref_with_type(vm, cls).map(Into::into) } - #[pymember] - fn __dict__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - zelf.dict() - .map(Into::into) - .ok_or_else(|| vm.new_attribute_error("module has no __dict__")) - } - #[pymethod] fn __dir__(zelf: &Py, vm: &VirtualMachine) -> PyResult> { // First check if __dict__ attribute exists and is actually a dictionary diff --git a/crates/vm/src/builtins/namespace.rs b/crates/vm/src/builtins/namespace.rs index 182891fdf..f47caf818 100644 --- a/crates/vm/src/builtins/namespace.rs +++ b/crates/vm/src/builtins/namespace.rs @@ -11,9 +11,6 @@ use crate::{ }; use rustpython_common::wtf8::Wtf8Buf; -/// A simple attribute-based namespace. -/// -/// SimpleNamespace(**kwargs) #[pyclass(module = "types", name = "SimpleNamespace")] #[derive(Debug, Default)] pub struct PyNamespace {} diff --git a/crates/vm/src/builtins/object.rs b/crates/vm/src/builtins/object.rs index 7f33c23b9..dc76d29c1 100644 --- a/crates/vm/src/builtins/object.rs +++ b/crates/vm/src/builtins/object.rs @@ -13,13 +13,6 @@ use crate::{ }; use itertools::Itertools; -/// object() -/// -- -/// -/// The base class of the class hierarchy. -/// -/// When called, it accepts no arguments and returns a new featureless -/// instance that has no instance attributes and cannot be given any. #[pyclass(module = false, name = "object")] #[derive(Debug)] pub struct PyBaseObject; @@ -218,37 +211,10 @@ fn object_getstate_default(obj: &PyObject, required: bool, vm: &VirtualMachine) type_slot_names(obj.class(), vm).map_err(|_| vm.new_type_error("cannot pickle object"))?; if required { - // Start with PyBaseObject_Type's basicsize - let mut basicsize = vm.ctx.types.object_type.slots().basicsize; - - // Add __dict__ size if type has dict - if obj.class().slots().flags.has_feature(PyTypeFlags::HAS_DICT) { - basicsize += core::mem::size_of::(); - } - - // Add __weakref__ size if type has weakref support - let has_weakref = if let Some(ext) = obj.class().heaptype_ext() { - match &ext.slots { - None => true, // Heap type without __slots__ has automatic weakref - Some(slots) => slots - .as_slice() - .iter() - .any(|s| s.as_bytes() == b"__weakref__"), - } - } else { - let weakref_name = vm.ctx.intern_str("__weakref__"); - obj.class().attributes().contains(weakref_name) - }; - if has_weakref { - basicsize += core::mem::size_of::(); - } - - // Add slots size - if let Some(ref slot_names) = slot_names { - basicsize += core::mem::size_of::() * slot_names.__len__(); - } - - // Fail if actual type's basicsize > expected basicsize + // Dict, weakref list, and slot cells sit in the prefix in front of + // the payload, so they are not part of `slots.basicsize`. Only state + // stored inside the payload counts. + let basicsize = vm.ctx.types.object_type.slots().basicsize; if obj.class().slots().basicsize > basicsize { return Err(vm.new_type_error(format!("cannot pickle '{}' object", obj.class().name()))); } diff --git a/crates/vm/src/builtins/property.rs b/crates/vm/src/builtins/property.rs index 7def6977c..1da2e24c9 100644 --- a/crates/vm/src/builtins/property.rs +++ b/crates/vm/src/builtins/property.rs @@ -3,6 +3,7 @@ */ use super::PyType; use crate::common::lock::PyRwLock; +use crate::object::PyAtomicRef; use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, class::PyClassImpl, @@ -42,10 +43,14 @@ impl FromArgs for SetNameArgs { #[pyclass(module = false, name = "property", traverse)] #[derive(Debug)] pub struct PyProperty { - getter: PyRwLock>, - setter: PyRwLock>, - deleter: PyRwLock>, - doc: PyRwLock>, + #[pymember(name = "fget")] + getter: PyAtomicRef>, + #[pymember(name = "fset")] + setter: PyAtomicRef>, + #[pymember(name = "fdel")] + deleter: PyAtomicRef>, + #[pymember(name = "__doc__", writable)] + doc: PyAtomicRef>, name: PyRwLock>, #[pytraverse(skip)] getter_doc: core::sync::atomic::AtomicBool, @@ -83,7 +88,7 @@ impl GetDescriptor for PyProperty { } // Clone and release lock before calling Python code to prevent deadlock - let value = zelf.getter.read().clone(); + let value = zelf.getter.load_owned(); if let Some(getter) = value { getter.call((obj.to_owned(),), vm) } else { @@ -108,7 +113,7 @@ impl PyProperty { } // Clone and release lock before calling Python code to prevent deadlock - let Some(getter) = self.getter.read().clone() else { + let Some(getter) = self.getter.load_owned() else { return Ok(None); }; @@ -139,7 +144,7 @@ impl PyProperty { match value { PySetterValue::Assign(value) => { // Clone and release lock before calling Python code to prevent deadlock - let set = zelf.setter.read().clone(); + let set = zelf.setter.load_owned(); if let Some(setter) = set { setter.call((obj, value), vm).map(drop) } else { @@ -149,7 +154,7 @@ impl PyProperty { } PySetterValue::Delete => { // Clone and release lock before calling Python code to prevent deadlock - let del = zelf.deleter.read().clone(); + let del = zelf.deleter.load_owned(); if let Some(deleter) = del { deleter.call((obj,), vm).map(drop) } else { @@ -160,36 +165,16 @@ impl PyProperty { } } - // Access functions - - #[pymember] - fn fget(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: &Py = zelf.try_to_value(vm)?; - Ok(vm.unwrap_or_none(zelf.getter.read().clone())) - } - pub(crate) fn get_fget(&self) -> Option { - self.getter.read().clone() - } - - #[pymember] - fn fset(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: &Py = zelf.try_to_value(vm)?; - Ok(vm.unwrap_or_none(zelf.setter.read().clone())) + self.getter.load_owned() } fn get_fset(&self) -> Option { - self.setter.read().clone() - } - - #[pymember] - fn fdel(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: &Py = zelf.try_to_value(vm)?; - Ok(vm.unwrap_or_none(zelf.deleter.read().clone())) + self.setter.load_owned() } fn get_fdel(&self) -> Option { - self.deleter.read().clone() + self.deleter.load_owned() } #[pygetset(name = "__name__")] @@ -206,10 +191,7 @@ impl PyProperty { } fn doc_getter(&self) -> Option { - self.doc.read().clone() - } - fn doc_setter(&self, value: Option) { - *self.doc.write() = value; + self.doc.load_owned() } #[pymethod] @@ -302,21 +284,21 @@ impl PyProperty { // Clone and release lock before calling Python code to prevent deadlock // Check getter - if let Some(getter) = self.getter.read().clone() + if let Some(getter) = self.getter.load_owned() && is_abstract(&getter)? { return Ok(vm.ctx.new_bool(true).into()); } // Check setter - if let Some(setter) = self.setter.read().clone() + if let Some(setter) = self.setter.load_owned() && is_abstract(&setter)? { return Ok(vm.ctx.new_bool(true).into()); } // Check deleter - if let Some(deleter) = self.deleter.read().clone() + if let Some(deleter) = self.deleter.load_owned() && is_abstract(&deleter)? { return Ok(vm.ctx.new_bool(true).into()); @@ -328,7 +310,7 @@ impl PyProperty { #[pygetset(setter)] fn set___isabstractmethod__(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { // Clone and release lock before calling Python code to prevent deadlock - let maybe_getter = self.getter.read().clone(); + let maybe_getter = self.getter.load_owned(); if let Some(getter) = maybe_getter { getter.set_attr("__isabstractmethod__", value, vm)?; } @@ -368,10 +350,10 @@ impl Constructor for PyProperty { fn py_new(_cls: &Py, _args: FuncArgs, _vm: &VirtualMachine) -> PyResult { Ok(Self { - getter: PyRwLock::new(None), - setter: PyRwLock::new(None), - deleter: PyRwLock::new(None), - doc: PyRwLock::new(None), + getter: PyAtomicRef::new_empty(), + setter: PyAtomicRef::new_empty(), + deleter: PyAtomicRef::new_empty(), + doc: PyAtomicRef::new_empty(), name: PyRwLock::new(None), getter_doc: AtomicBool::new(false), }) @@ -409,7 +391,7 @@ impl Initializer for PyProperty { if is_exact_property { // For exact property type, store doc in the field - *zelf.doc.write() = doc; + zelf.doc.store(doc); } else { // For property subclass, set __doc__ as an attribute let doc_to_set = doc.unwrap_or_else(|| vm.ctx.none()); @@ -423,9 +405,9 @@ impl Initializer for PyProperty { } } - *zelf.getter.write() = args.fget; - *zelf.setter.write() = args.fset; - *zelf.deleter.write() = args.fdel; + zelf.getter.store(args.fget); + zelf.setter.store(args.fset); + zelf.deleter.store(args.fdel); zelf.getter_doc.store(getter_doc, Ordering::Relaxed); Ok(()) @@ -434,15 +416,4 @@ impl Initializer for PyProperty { pub(crate) fn init(context: &'static Context) { PyProperty::extend_class(context, context.types.property_type); - - // This is a bit unfortunate, but this instance attribute overlaps with the - // class __doc__ string.. - extend_class!(context, context.types.property_type, { - "__doc__" => context.new_static_getset( - "__doc__", - context.types.property_type, - PyProperty::doc_getter, - PyProperty::doc_setter, - ), - }); } diff --git a/crates/vm/src/builtins/range.rs b/crates/vm/src/builtins/range.rs index 85ac1f3bd..082797c1c 100644 --- a/crates/vm/src/builtins/range.rs +++ b/crates/vm/src/builtins/range.rs @@ -64,8 +64,11 @@ fn iter_search( #[pyclass(module = false, name = "range")] #[derive(Debug, Clone)] pub struct PyRange { + #[pymember(type = "object_ex")] pub start: PyIntRef, + #[pymember(type = "object_ex")] pub stop: PyIntRef, + #[pymember(type = "object_ex")] pub step: PyIntRef, } @@ -259,21 +262,6 @@ impl PyRange { .into_ref_with_type(vm, cls) } - #[pygetset] - fn start(&self) -> PyIntRef { - self.start.clone() - } - - #[pygetset] - fn stop(&self) -> PyIntRef { - self.stop.clone() - } - - #[pygetset] - fn step(&self) -> PyIntRef { - self.step.clone() - } - #[pymethod] fn __reversed__(&self, vm: &VirtualMachine) -> PyObjectRef { let start = self.start.as_bigint(); @@ -486,14 +474,14 @@ impl Hashable for PyRange { } else if length.is_one() { [ vm.ctx.new_int(length).into(), - zelf.start().into(), + zelf.start.clone().into(), vm.ctx.none(), ] } else { [ vm.ctx.new_int(length).into(), - zelf.start().into(), - zelf.step().into(), + zelf.start.clone().into(), + zelf.step.clone().into(), ] }; tuple_hash(&elements, vm) diff --git a/crates/vm/src/builtins/slice.rs b/crates/vm/src/builtins/slice.rs index fc5c157e5..465e02b96 100644 --- a/crates/vm/src/builtins/slice.rs +++ b/crates/vm/src/builtins/slice.rs @@ -9,7 +9,6 @@ use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, class::{PyClassDef, PyClassImpl}, common::hash::{PyHash, PyUHash}, - convert::ToPyObject, function::{ArgIndex, FuncArgs, OptionalArg, PyComparisonValue}, sliceable::SaturatedSlice, types::{Comparable, Constructor, Hashable, PyComparisonOp, Representable}, @@ -20,8 +19,11 @@ use num_traits::{One, Signed, Zero}; #[pyclass(module = false, name = "slice", unhashable = true, traverse = "manual")] #[derive(Debug)] pub struct PySlice { + #[pymember] pub start: Option, + #[pymember] pub stop: PyObjectRef, + #[pymember] pub step: Option, } @@ -91,11 +93,6 @@ impl PyPayload for PySlice { #[pyclass(with(Comparable, Representable, Hashable))] impl PySlice { - #[pygetset] - fn start(&self, vm: &VirtualMachine) -> PyObjectRef { - self.start.clone().to_pyobject(vm) - } - pub(crate) fn start_ref<'a>(&'a self, vm: &'a VirtualMachine) -> &'a PyObject { match &self.start { Some(v) => v, @@ -103,16 +100,6 @@ impl PySlice { } } - #[pygetset] - pub(crate) fn stop(&self, _vm: &VirtualMachine) -> PyObjectRef { - self.stop.clone() - } - - #[pygetset] - fn step(&self, vm: &VirtualMachine) -> PyObjectRef { - self.step.clone().to_pyobject(vm) - } - pub(crate) fn step_ref<'a>(&'a self, vm: &'a VirtualMachine) -> &'a PyObject { match &self.step { Some(v) => v, @@ -158,7 +145,7 @@ impl PySlice { step = One::one(); } else { // Clone the value, not the reference. - let this_step = self.step(vm).try_index(vm)?; + let this_step = self.step_ref(vm).try_index(vm)?; step = this_step.as_bigint().clone(); if step.is_zero() { @@ -192,7 +179,7 @@ impl PySlice { lower.clone() }; } else { - let this_start = self.start(vm).try_index(vm)?; + let this_start = self.start_ref(vm).try_index(vm)?; start = this_start.as_bigint().clone(); if start < Zero::zero() { @@ -212,7 +199,7 @@ impl PySlice { if vm.is_none(&self.stop) { stop = if backwards { lower } else { upper }; } else { - let this_stop = self.stop(vm).try_index(vm)?; + let this_stop = self.stop.try_index(vm)?; stop = this_stop.as_bigint().clone(); if stop < Zero::zero() { diff --git a/crates/vm/src/builtins/staticmethod.rs b/crates/vm/src/builtins/staticmethod.rs index 3f366a53f..8198c35c4 100644 --- a/crates/vm/src/builtins/staticmethod.rs +++ b/crates/vm/src/builtins/staticmethod.rs @@ -7,15 +7,17 @@ use super::{ use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, class::{PyClassDef, PyClassImpl}, - common::lock::PyMutex, function::{FuncArgs, PySetterValue}, + object::PyAtomicRef, types::{Callable, Constructor, GetDescriptor, Initializer, Representable}, }; #[pyclass(module = false, name = "staticmethod", traverse)] #[derive(Debug)] pub struct PyStaticMethod { - pub callable: PyMutex, + #[pymember(name = "__func__")] + #[pymember(name = "__wrapped__")] + pub callable: PyAtomicRef, } impl PyPayload for PyStaticMethod { @@ -33,14 +35,14 @@ impl GetDescriptor for PyStaticMethod { vm: &VirtualMachine, ) -> PyResult { let (zelf, _obj) = Self::_unwrap(zelf, obj, vm)?; - Ok(zelf.callable.lock().clone()) + Ok(zelf.callable.load_owned()) } } impl From for PyStaticMethod { fn from(callable: PyObjectRef) -> Self { Self { - callable: PyMutex::new(callable), + callable: PyAtomicRef::from(callable), } } } @@ -61,7 +63,7 @@ impl Constructor for PyStaticMethod { // `None`, matching CPython. let _: StaticMethodArgs = args.bind_for(vm, Self::NAME)?; let result = Self { - callable: PyMutex::new(vm.ctx.none()), + callable: PyAtomicRef::from(vm.ctx.none()), } .into_ref_with_type(vm, cls)?; Ok(PyObjectRef::from(result)) @@ -76,7 +78,7 @@ impl PyStaticMethod { #[must_use] pub fn new(callable: PyObjectRef) -> Self { Self { - callable: PyMutex::new(callable), + callable: PyAtomicRef::from(callable), } } @@ -91,7 +93,7 @@ impl Initializer for PyStaticMethod { fn init(zelf: &Py, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> { let callable = args.function; - *zelf.callable.lock() = callable.clone(); + zelf.callable.store(callable.clone()); functools_wraps(zelf.as_object(), &callable, vm) } } @@ -101,21 +103,9 @@ impl Initializer for PyStaticMethod { flags(BASETYPE, HAS_DICT, HAS_WEAKREF) )] impl PyStaticMethod { - #[pymember] - fn __func__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: &Py = zelf.try_to_value(vm)?; - Ok(zelf.callable.lock().clone()) - } - - #[pymember] - fn __wrapped__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: &Py = zelf.try_to_value(vm)?; - Ok(zelf.callable.lock().clone()) - } - #[pygetset] fn __annotations__(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let callable = zelf.callable.lock(); + let callable = zelf.callable.load_owned(); descriptor_get_wrapped_attribute( &callable, zelf.as_object(), @@ -141,7 +131,7 @@ impl PyStaticMethod { #[pygetset] fn __annotate__(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let callable = zelf.callable.lock(); + let callable = zelf.callable.load_owned(); descriptor_get_wrapped_attribute( &callable, zelf.as_object(), @@ -167,7 +157,7 @@ impl PyStaticMethod { #[pygetset] fn __isabstractmethod__(&self, vm: &VirtualMachine) -> PyObjectRef { - let callable = self.callable.lock().clone(); + let callable = self.callable.load_owned(); if let Ok(Some(is_abstract)) = vm.get_attribute_opt(&callable, "__isabstractmethod__") { is_abstract @@ -179,7 +169,7 @@ impl PyStaticMethod { #[pygetset(setter)] fn set___isabstractmethod__(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { self.callable - .lock() + .load_owned() .set_attr("__isabstractmethod__", value, vm)?; Ok(()) } @@ -198,14 +188,14 @@ impl Callable for PyStaticMethod { type Args = FuncArgs; #[inline] fn call(zelf: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - let callable = zelf.callable.lock().clone(); + let callable = zelf.callable.load_owned(); callable.call(args, vm) } } impl Representable for PyStaticMethod { fn repr_str(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let callable = zelf.callable.lock().repr(vm)?; + let callable = zelf.callable.load_owned().repr(vm)?; let class = Self::class(&vm.ctx); match ( diff --git a/crates/vm/src/builtins/template.rs b/crates/vm/src/builtins/template.rs index 471fa1edb..08b7cd928 100644 --- a/crates/vm/src/builtins/template.rs +++ b/crates/vm/src/builtins/template.rs @@ -16,13 +16,12 @@ use crate::{ }; use rustpython_common::wtf8::{Wtf8Buf, wtf8_concat}; -/// Template object for t-strings (PEP 750). -/// -/// Represents a template string with interpolated expressions. #[pyclass(module = "string.templatelib", name = "Template")] #[derive(Debug, Clone)] pub struct PyTemplate { + #[pymember(type = "object_ex")] pub strings: PyTupleRef, + #[pymember(type = "object_ex")] pub interpolations: PyTupleRef, } @@ -98,16 +97,6 @@ impl Constructor for PyTemplate { #[pyclass(with(Constructor, Comparable, Iterable, Representable, AsSequence))] impl PyTemplate { - #[pygetset] - fn strings(&self) -> PyTupleRef { - self.strings.clone() - } - - #[pygetset] - fn interpolations(&self) -> PyTupleRef { - self.interpolations.clone() - } - #[pygetset] fn values(&self, vm: &VirtualMachine) -> PyTupleRef { let values: Vec = self @@ -271,7 +260,6 @@ impl Representable for PyTemplate { } } -/// Iterator for Template objects #[pyclass(module = "string.templatelib", name = "TemplateIter")] #[derive(Debug)] pub struct PyTemplateIter { diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index e7f4ee3f3..2d6b7c9e7 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -46,7 +46,8 @@ pub(crate) type PyTypeTupleRef = PyRef>; #[pyclass(module = false, name = "type", traverse = "manual")] pub struct PyType { - /// tp_base. Written under the type lock (see `set_bases`); read lock-free. + // tp_base. Written under the type lock (see `set_bases`); read lock-free. + #[pymember(name = "__base__", doc = false)] pub base: PyAtomicRef>, pub bases: PyRwLock, pub mro: PyRwLock>, @@ -329,7 +330,7 @@ impl TypeSpecializationCache { new.as_object().mark_cache_published(); } // SAFETY: reclamation of published objects is deferred via QSBR; - // racing try_to_owned readers never touch freed memory. + // racing load_owned readers never touch freed memory. let old = unsafe { self.init.swap(new_init) }; if let Some(old) = old { // Dropping may run arbitrary Python; defer past the type lock. @@ -1398,10 +1399,7 @@ impl PyType { return None; } // Check order: pointer (Acquire) then function version. - let init = ext - .specialization_cache - .init - .try_to_owned(Ordering::Acquire)?; + let init = ext.specialization_cache.init.load_owned()?; let cached_version = ext .specialization_cache .init_version @@ -1446,10 +1444,7 @@ impl PyType { pub(crate) fn get_cached_getitem_for_specialization(&self) -> Option<(PyRef, u32)> { let ext = self.heaptype_ext.as_ref()?; // Check order: pointer (Acquire) then function version. - let getitem = ext - .specialization_cache - .getitem - .try_to_owned(Ordering::Acquire)?; + let getitem = ext.specialization_cache.getitem.load_owned()?; let cached_version = ext .specialization_cache .getitem_version @@ -1775,7 +1770,10 @@ impl PyType { // Reject reparenting onto a base whose instances have an incompatible // object layout. - let old_base = zelf.base.deref().unwrap_or(vm.ctx.types.object_type); + let old_base_owned = zelf.base.load_owned(); + let old_base = old_base_owned + .as_deref() + .unwrap_or(vm.ctx.types.object_type); compatible_for_assignment(old_base, &new_base, "__bases__", vm)?; // References released inside the critical section are collected here @@ -1922,11 +1920,6 @@ impl PyType { result } - #[pygetset] - fn __base__(&self) -> Option { - self.base.to_owned() - } - #[pygetset] fn __flags__(&self) -> u64 { self.slots.flags.bits() @@ -2831,7 +2824,7 @@ impl Constructor for PyType { qualname: PyRwLock::new(None), }, member: member_def, - access: MemberAccess::Slot, + access: MemberAccess::Offset, }); // __slots__ attributes always get a member descriptor // (this overrides any inherited attribute from MRO) @@ -3329,7 +3322,7 @@ fn get_builtin_base_with_dict(typ: &Py, vm: &VirtualMachine) -> Option

staticbase = base, None => break, } diff --git a/crates/vm/src/builtins/union.rs b/crates/vm/src/builtins/union.rs index 137dbfb4e..5b73e3332 100644 --- a/crates/vm/src/builtins/union.rs +++ b/crates/vm/src/builtins/union.rs @@ -18,6 +18,7 @@ const CLS_ATTRS: &[&str] = &["__module__"]; #[pyclass(module = "typing", name = "Union", traverse)] pub struct PyUnion { + #[pymember(name = "__args__")] args: PyTupleRef, /// Frozenset of hashable args, or None if all args were hashable hashable_args: Option>, @@ -124,11 +125,6 @@ impl PyUnion { self.parameters.clone().into() } - #[pygetset] - fn __args__(&self) -> PyObjectRef { - self.args.clone().into() - } - #[pymethod] fn __instancecheck__( zelf: PyRef, @@ -143,7 +139,7 @@ impl PyUnion { { Err(vm.new_type_error("isinstance() argument 2 cannot be a parameterized generic")) } else { - obj.is_instance(zelf.__args__().as_object(), vm) + obj.is_instance(zelf.args.as_object(), vm) } } @@ -161,7 +157,7 @@ impl PyUnion { { Err(vm.new_type_error("issubclass() argument 2 cannot be a parameterized generic")) } else { - obj.is_subclass(zelf.__args__().as_object(), vm) + obj.is_subclass(zelf.args.as_object(), vm) } } diff --git a/crates/vm/src/class.rs b/crates/vm/src/class.rs index 076e97c3b..c3a3cc513 100644 --- a/crates/vm/src/class.rs +++ b/crates/vm/src/class.rs @@ -140,6 +140,9 @@ pub trait PyClassDef { const BASICSIZE: usize; const ITEMSIZE: usize = 0; const UNHASHABLE: bool = false; + const MEMBERS: &'static [crate::builtins::descriptor::PyMemberSpec] = &[]; + + fn assert_member_layout() {} // due to restriction of rust trait system, object.__base__ is None // but PyBaseObject::Base will be PyBaseObject. @@ -240,6 +243,22 @@ pub trait PyClassImpl: PyClassDef { ); } + Self::assert_member_layout(); + for member in Self::MEMBERS { + class.set_str_attr( + member.name, + ctx.new_member( + member.name, + member.kind, + member.offset, + member.flags, + class, + member.doc, + ), + ctx, + ); + } + Self::impl_extend_class(ctx, class); if let Some(doc) = Self::DOC { @@ -284,8 +303,14 @@ pub trait PyClassImpl: PyClassDef { // Add slot wrappers using SLOT_DEFS array add_operators::(class, ctx); - // Inherit slots from base types after slots are fully initialized - for base in class.bases.read().as_slice() { + // Same walk as init_slots: a slot such as tp_init is copied only from + // a base that defines it, so a static grandchild must see that base + // in the MRO, not only its direct bases. + let mro = { + let guard = class.mro.read(); + guard[1..].to_vec() + }; + for base in &mro { class.inherit_slots(base); } diff --git a/crates/vm/src/coroutine.rs b/crates/vm/src/coroutine.rs index 1b7dbfe95..510580607 100644 --- a/crates/vm/src/coroutine.rs +++ b/crates/vm/src/coroutine.rs @@ -9,7 +9,6 @@ use crate::{ protocol::PyIterReturn, vm::GenFrameLink, }; -use core::sync::atomic::Ordering; use crossbeam_utils::atomic::AtomicCell; impl ExecutionResult { @@ -522,7 +521,7 @@ impl Coro { } pub fn frame_opt(&self) -> Option { - self.frame.try_to_owned(Ordering::Acquire) + self.frame.load_owned() } pub fn code(&self) -> PyRef { diff --git a/crates/vm/src/exception_group.rs b/crates/vm/src/exception_group.rs index 5232cbe98..ffac0b387 100644 --- a/crates/vm/src/exception_group.rs +++ b/crates/vm/src/exception_group.rs @@ -53,7 +53,9 @@ pub(super) mod types { #[repr(C)] pub struct PyBaseExceptionGroup { base: PyBaseException, + #[pymember(name = "message")] msg: PyAtomicRef, + #[pymember(name = "exceptions")] excs: PyAtomicRef, excs_str: PyAtomicRef>, } @@ -85,16 +87,6 @@ pub(super) mod types { #[pyexception(with(Constructor, Initializer))] impl PyBaseExceptionGroup { - #[pygetset] - fn message(&self) -> PyObjectRef { - self.msg.to_owned() - } - - #[pygetset] - fn exceptions(&self) -> PyObjectRef { - self.excs.to_owned() - } - #[pyclassmethod] fn __class_getitem__( cls: PyTypeRef, @@ -228,8 +220,11 @@ pub(super) mod types { Ok(vm.ctx.new_tuple(vec![match_group, rest_group])) } - #[pymethod] - fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult { + #[pyslot] + fn slot_str(zelf: &PyObject, vm: &VirtualMachine) -> PyResult { + let zelf: &Py = zelf + .downcast_ref() + .expect("slot wrapper checked BaseExceptionGroup"); let message = zelf.msg.str(vm)?; let num_excs = zelf .excs @@ -238,7 +233,8 @@ pub(super) mod types { let suffix = if num_excs == 1 { "" } else { "s" }; let mut result = message.as_wtf8().to_owned(); - write!(result, " ({num_excs} sub-exception{suffix})").unwrap(); + write!(result, " ({num_excs} sub-exception{suffix})") + .expect("formatting into a string buffer cannot fail"); Ok(vm.ctx.new_str(result)) } @@ -250,7 +246,7 @@ pub(super) mod types { let class_name = zelf.class().name().to_owned(); let message = zelf.msg.repr(vm)?; - let exceptions_str = if let Some(saved) = zelf.excs_str.to_owned() { + let exceptions_str = if let Some(saved) = zelf.excs_str.load_owned() { saved .downcast::() .map_err(|_| vm.new_type_error("__repr__ returned non-string"))? diff --git a/crates/vm/src/exceptions.rs b/crates/vm/src/exceptions.rs index c692ca21e..588754bb3 100644 --- a/crates/vm/src/exceptions.rs +++ b/crates/vm/src/exceptions.rs @@ -18,7 +18,7 @@ use crate::{ types::{Callable, Constructor, Initializer, Representable}, }; use core::fmt::{self, Display, Formatter}; -use crossbeam_utils::atomic::AtomicCell; +use core::sync::atomic::{AtomicBool, Ordering}; use itertools::Itertools; #[cfg(feature = "host_env")] use std::io::{BufRead, BufReader}; @@ -634,7 +634,7 @@ impl PyBaseException { traceback: PyRwLock::new(None), cause: PyRwLock::new(None), context: PyRwLock::new(None), - suppress_context: AtomicCell::new(false), + suppress_context: AtomicBool::new(false), args: PyRwLock::new(PyTuple::new_ref(args, &vm.ctx)), } } @@ -765,14 +765,13 @@ impl PyBaseException { *self.context.write() = context; } - #[pygetset] pub(super) fn __suppress_context__(&self) -> bool { - self.suppress_context.load() + self.suppress_context.load(Ordering::Relaxed) } - #[pygetset(name = "__suppress_context__", setter)] - fn set_suppress_context(&self, suppress_context: bool) { - self.suppress_context.store(suppress_context); + pub(super) fn set_suppress_context(&self, suppress_context: bool) { + self.suppress_context + .store(suppress_context, Ordering::Relaxed); } } @@ -1085,19 +1084,11 @@ impl ExceptionZoo { extend_exception!(PyZeroDivisionError, ctx, excs.zero_division_error); extend_exception!(PyAssertionError, ctx, excs.assertion_error); - extend_exception!(PyAttributeError, ctx, excs.attribute_error, { - "name" => ctx.none(), - "obj" => ctx.none(), - }); + extend_exception!(PyAttributeError, ctx, excs.attribute_error); extend_exception!(PyBufferError, ctx, excs.buffer_error); extend_exception!(PyEOFError, ctx, excs.eof_error); - extend_exception!(PyImportError, ctx, excs.import_error, { - "msg" => ctx.new_readonly_getset("msg", excs.import_error, make_arg_getter(0)), - "name" => ctx.none(), - "path" => ctx.none(), - "name_from" => ctx.none(), - }); + extend_exception!(PyImportError, ctx, excs.import_error); extend_exception!(PyModuleNotFoundError, ctx, excs.module_not_found_error); extend_exception!(PyLookupError, ctx, excs.lookup_error); @@ -1106,9 +1097,7 @@ impl ExceptionZoo { extend_exception!(PyKeyError, ctx, excs.key_error); extend_exception!(PyMemoryError, ctx, excs.memory_error); - extend_exception!(PyNameError, ctx, excs.name_error, { - "name" => ctx.none(), - }); + extend_exception!(PyNameError, ctx, excs.name_error); extend_exception!(PyUnboundLocalError, ctx, excs.unbound_local_error); // os errors: @@ -1143,21 +1132,7 @@ impl ExceptionZoo { excs.python_finalization_error ); - extend_exception!(PySyntaxError, ctx, excs.syntax_error, { - "msg" => ctx.new_static_getset( - "msg", - excs.syntax_error, - make_arg_getter(0), - syntax_error_set_msg, - ), - // TODO: members - "filename" => ctx.none(), - "lineno" => ctx.none(), - "end_lineno" => ctx.none(), - "offset" => ctx.none(), - "end_offset" => ctx.none(), - "text" => ctx.none(), - }); + extend_exception!(PySyntaxError, ctx, excs.syntax_error); extend_exception!(PyIncompleteInputError, ctx, excs.incomplete_input_error); extend_exception!(PyIndentationError, ctx, excs.indentation_error); extend_exception!(PyTabError, ctx, excs.tab_error); @@ -1192,24 +1167,6 @@ impl ExceptionZoo { } } -fn make_arg_getter(idx: usize) -> impl Fn(PyBaseExceptionRef) -> Option { - move |exc| exc.get_arg(idx) -} - -fn syntax_error_set_msg(exc: &Py, value: PySetterValue, vm: &VirtualMachine) { - let mut args = exc.args.write(); - let mut new_args = args.as_slice().to_vec(); - // Ensure the message slot at index 0 always exists for SyntaxError.args. - if new_args.is_empty() { - new_args.push(vm.ctx.none()); - } - match value { - PySetterValue::Assign(value) => new_args[0] = value, - PySetterValue::Delete => new_args[0] = vm.ctx.none(), - } - *args = PyTuple::new_ref(new_args, &vm.ctx); -} - #[cfg(feature = "serde")] pub struct SerializeException<'vm, 's> { vm: &'vm VirtualMachine, @@ -1739,9 +1696,9 @@ pub(super) mod types { }, convert::ToPyResult, function::{ArgBytesLike, FuncArgs, KwArgs, PySetterValue}, - set_attrs, types::{Constructor, Initializer}, }; + use core::sync::atomic::{AtomicBool, AtomicIsize, Ordering}; use crossbeam_utils::atomic::AtomicCell; use itertools::Itertools; use rustpython_common::{ @@ -1765,7 +1722,8 @@ pub(super) mod types { pub(super) traceback: PyRwLock>, pub(super) cause: PyRwLock>>, pub(super) context: PyRwLock>>, - pub(super) suppress_context: AtomicCell, + #[pymember(name = "__suppress_context__", writable)] + pub(super) suppress_context: AtomicBool, pub(super) args: PyRwLock, } @@ -1773,6 +1731,7 @@ pub(super) mod types { #[repr(C)] pub struct PySystemExit { base: PyBaseException, + #[pymember(writable)] code: PyAtomicRef>, } @@ -1799,21 +1758,7 @@ pub(super) mod types { } #[pyexception(with(Constructor, Initializer))] - impl PySystemExit { - #[pygetset] - fn code(&self) -> Option { - self.code.to_owned() - } - - #[pygetset(setter)] - fn set_code(&self, value: PySetterValue, vm: &VirtualMachine) { - let code = match value { - PySetterValue::Assign(v) => Some(v), - PySetterValue::Delete => None, - }; - self.code.swap_to_temporary_refs(code, vm); - } - } + impl PySystemExit {} impl Initializer for PySystemExit { type Args = FuncArgs; @@ -1866,6 +1811,7 @@ pub(super) mod types { #[repr(C)] pub struct PyStopIteration { base: PyException, + #[pymember(writable)] value: PyAtomicRef>, } @@ -1922,21 +1868,7 @@ pub(super) mod types { } #[pyexception(with(Constructor, Initializer))] - impl PyStopIteration { - #[pygetset] - fn value(&self) -> Option { - self.value.to_owned() - } - - #[pygetset(setter)] - fn set_value(&self, setter_value: PySetterValue, vm: &VirtualMachine) { - let value = match setter_value { - PySetterValue::Assign(v) => Some(v), - PySetterValue::Delete => None, - }; - self.value.swap_to_temporary_refs(value, vm); - } - } + impl PyStopIteration {} #[pyexception(name, base = PyException, ctx = "stop_async_iteration", impl)] #[derive(Debug)] @@ -1968,35 +1900,79 @@ pub(super) mod types { #[repr(transparent)] pub struct PyAssertionError(PyException); - #[pyexception(name, base = PyException, ctx = "attribute_error")] - #[derive(Debug)] - #[repr(transparent)] - pub struct PyAttributeError(PyException); + #[pyexception(name, base = PyException, ctx = "attribute_error", traverse = "manual")] + #[repr(C)] + pub struct PyAttributeError { + base: PyException, + #[pymember(writable)] + name: PyAtomicRef>, + #[pymember(writable)] + obj: PyAtomicRef>, + } - #[pyexception(with(Initializer))] + impl crate::class::PySubclass for PyAttributeError { + type Base = PyException; + fn as_base(&self) -> &Self::Base { + &self.base + } + } + + impl core::fmt::Debug for PyAttributeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PyAttributeError").finish_non_exhaustive() + } + } + + unsafe impl Traverse for PyAttributeError { + fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { + self.base.0.traverse(tracer_fn); + if let Some(obj) = self.name.deref() { + tracer_fn(obj); + } + if let Some(obj) = self.obj.deref() { + tracer_fn(obj); + } + } + } + + impl Constructor for PyAttributeError { + type Args = FuncArgs; + + fn py_new(_cls: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + Ok(Self { + base: PyException(PyBaseException::new(args.args, vm)), + name: None.into(), + obj: None.into(), + }) + } + } + + #[pyexception(with(Constructor, Initializer))] impl PyAttributeError { #[pymethod] - fn __getstate__(zelf: PyBaseExceptionRef, vm: &VirtualMachine) -> PyResult { + fn __getstate__(zelf: &Py, vm: &VirtualMachine) -> PyResult { let obj = zelf.as_object(); let state = match obj.dict() { Some(dict) => dict.copy().into_ref(&vm.ctx), None => vm.ctx.new_dict(), }; let _ = state.del_item("obj", vm); - if let Ok(name) = obj.get_attr("name", vm) - && !vm.is_none(&name) - { + if let Some(name) = zelf.name.load_owned() { state.set_item("name", name, vm)?; } Ok(state.into()) } #[pymethod] - fn __reduce__(zelf: PyBaseExceptionRef, vm: &VirtualMachine) -> PyResult { - let state = Self::__getstate__(zelf.clone(), vm)?; + fn __reduce__(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let state = Self::__getstate__(zelf, vm)?; + let base: &Py = zelf + .as_object() + .downcast_ref() + .expect("AttributeError is a BaseException"); Ok(vm.ctx.new_tuple(vec![ zelf.class().to_owned().into(), - zelf.args().into(), + base.args().into(), state, ])) } @@ -2023,11 +1999,11 @@ pub(super) mod types { let base_args = FuncArgs::new(args.args, KwArgs::default()); PyBaseException::slot_init(zelf, base_args, vm)?; - // Set attributes - set_attrs!(zelf, vm, - "name" => vm.unwrap_or_none(name), - "obj" => vm.unwrap_or_none(obj), - ); + let exc: &Py = zelf + .downcast_ref() + .expect("AttributeError instance has AttributeError payload"); + exc.name.swap_to_temporary_refs(name, vm); + exc.obj.swap_to_temporary_refs(obj, vm); Ok(()) } @@ -2046,27 +2022,113 @@ pub(super) mod types { #[repr(transparent)] pub struct PyEOFError(PyException); - #[pyexception(name, base = PyException, ctx = "import_error")] - #[derive(Debug)] - #[repr(transparent)] - pub struct PyImportError(PyException); + #[pyexception(name, base = PyException, ctx = "import_error", traverse = "manual")] + #[repr(C)] + pub struct PyImportError { + base: PyException, + #[pymember(writable)] + msg: PyAtomicRef>, + #[pymember(writable)] + name: PyAtomicRef>, + #[pymember(writable)] + path: PyAtomicRef>, + #[pymember(writable)] + name_from: PyAtomicRef>, + } - #[pyexception(with(Initializer))] + impl crate::class::PySubclass for PyImportError { + type Base = PyException; + fn as_base(&self) -> &Self::Base { + &self.base + } + } + + impl core::fmt::Debug for PyImportError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PyImportError").finish_non_exhaustive() + } + } + + unsafe impl Traverse for PyImportError { + fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { + self.base.0.traverse(tracer_fn); + if let Some(obj) = self.msg.deref() { + tracer_fn(obj); + } + if let Some(obj) = self.name.deref() { + tracer_fn(obj); + } + if let Some(obj) = self.path.deref() { + tracer_fn(obj); + } + if let Some(obj) = self.name_from.deref() { + tracer_fn(obj); + } + } + } + + impl Constructor for PyImportError { + type Args = FuncArgs; + + fn py_new(_cls: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + Ok(Self { + base: PyException(PyBaseException::new(args.args, vm)), + msg: None.into(), + name: None.into(), + path: None.into(), + name_from: None.into(), + }) + } + } + + #[pyexception(with(Constructor, Initializer))] impl PyImportError { + #[pyslot] + fn slot_str(zelf: &PyObject, vm: &VirtualMachine) -> PyResult { + let exc: &Py = zelf + .downcast_ref() + .expect("slot wrapper checked ImportError"); + if let Some(msg) = exc.msg.load_owned() + && msg.class().is(vm.ctx.types.str_type) + { + return msg.str(vm); + } + let base: &Py = + zelf.downcast_ref().expect("ImportError is a BaseException"); + Ok(base.__str__(vm)) + } + #[pymethod] - fn __reduce__(zelf: PyBaseExceptionRef, vm: &VirtualMachine) -> PyTupleRef { - let obj = zelf.as_object().to_owned(); - let args: PyObjectRef = match zelf.get_arg(0) { + fn __reduce__(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let obj = zelf.as_object(); + let exc = zelf; + let base: &Py = + obj.downcast_ref().expect("ImportError is a BaseException"); + let args: PyObjectRef = match base.get_arg(0) { Some(arg) => vm.new_tuple((arg,)).into(), - None => zelf.args().into(), + None => base.args().into(), }; let mut result: Vec = vec![obj.class().to_owned().into(), args]; - if let Some(dict) = obj.dict().filter(|x| !x.is_empty()) { - result.push(dict.into()); + let mut state = obj + .dict() + .filter(|dict| !dict.is_empty()) + .map(|dict| dict.copy().into_ref(&vm.ctx)); + let mut insert = |key: &str, value: Option| -> PyResult<()> { + let Some(value) = value else { + return Ok(()); + }; + let dict = state.get_or_insert_with(|| vm.ctx.new_dict()); + dict.set_item(key, value, vm)?; + Ok(()) + }; + insert("name", exc.name.load_owned())?; + insert("path", exc.path.load_owned())?; + insert("name_from", exc.name_from.load_owned())?; + if let Some(state) = state { + result.push(state.into()); } - - result.into_pytuple(vm) + Ok(result.into_pytuple(vm)) } } @@ -2088,22 +2150,21 @@ pub(super) mod types { )); } - if let Some(name) = name { - zelf.set_attr("name", name, vm)?; - } else if let Some(dict) = zelf.dict() { - dict.del_item("name", vm).ok(); - } - if let Some(path) = path { - zelf.set_attr("path", path, vm)?; - } else if let Some(dict) = zelf.dict() { - dict.del_item("path", vm).ok(); - } - if let Some(name_from) = name_from { - zelf.set_attr("name_from", name_from, vm)?; - } else if let Some(dict) = zelf.dict() { - dict.del_item("name_from", vm).ok(); - } - PyBaseException::slot_init(zelf, args, vm) + let msg = if args.args.len() == 1 { + Some(args.args[0].clone()) + } else { + None + }; + let base_args = FuncArgs::new(args.args, KwArgs::default()); + PyBaseException::slot_init(zelf, base_args, vm)?; + let exc: &Py = zelf + .downcast_ref() + .expect("ImportError instance has ImportError payload"); + exc.name.swap_to_temporary_refs(name, vm); + exc.path.swap_to_temporary_refs(path, vm); + exc.name_from.swap_to_temporary_refs(name_from, vm); + exc.msg.swap_to_temporary_refs(msg, vm); + Ok(()) } fn init(_zelf: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> { @@ -2179,13 +2240,49 @@ pub(super) mod types { } } - #[pyexception(name, base = PyException, ctx = "name_error")] - #[derive(Debug)] - #[repr(transparent)] - pub struct PyNameError(PyException); + #[pyexception(name, base = PyException, ctx = "name_error", traverse = "manual")] + #[repr(C)] + pub struct PyNameError { + base: PyException, + #[pymember(writable)] + name: PyAtomicRef>, + } - // NameError_init: handles the .name. kwarg - #[pyexception(with(Initializer))] + impl crate::class::PySubclass for PyNameError { + type Base = PyException; + fn as_base(&self) -> &Self::Base { + &self.base + } + } + + impl core::fmt::Debug for PyNameError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PyNameError").finish_non_exhaustive() + } + } + + unsafe impl Traverse for PyNameError { + fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { + self.base.0.traverse(tracer_fn); + if let Some(obj) = self.name.deref() { + tracer_fn(obj); + } + } + } + + impl Constructor for PyNameError { + type Args = FuncArgs; + + fn py_new(_cls: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + Ok(Self { + base: PyException(PyBaseException::new(args.args, vm)), + name: None.into(), + }) + } + } + + // NameError_init: handles the .name kwarg + #[pyexception(with(Constructor, Initializer))] impl PyNameError {} impl Initializer for PyNameError { @@ -2207,10 +2304,11 @@ pub(super) mod types { let base_args = FuncArgs::new(args.args, KwArgs::default()); PyBaseException::slot_init(zelf, base_args, vm)?; - // Set name attribute if provided - if let Some(name) = name { - zelf.set_attr("name", name, vm)?; - } + let exc: &Py = zelf + .downcast_ref() + .expect("NameError instance has NameError payload"); + // Absent keyword clears the field. Re-init must not keep the old name. + exc.name.swap_to_temporary_refs(name, vm); Ok(()) } @@ -2228,11 +2326,16 @@ pub(super) mod types { #[repr(C)] pub struct PyOSError { base: PyException, + #[pymember(writable)] errno: PyAtomicRef>, + #[pymember(writable)] strerror: PyAtomicRef>, + #[pymember(writable)] filename: PyAtomicRef>, + #[pymember(writable)] filename2: PyAtomicRef>, #[cfg(windows)] + #[pymember(writable)] winerror: PyAtomicRef>, // For BlockingIOError: characters written before blocking occurred // -1 means not set (AttributeError when accessed) @@ -2448,7 +2551,7 @@ pub(super) mod types { // to, both follow the filename itself having been taken. let has_filename = exc .filename - .to_owned() + .load_owned() .as_ref() .is_some_and(|f| !vm.is_none(f)); if (3..=5).contains(&len) && has_filename { @@ -2485,9 +2588,9 @@ pub(super) mod types { #[pyexception(with(Constructor, Initializer))] impl PyOSError { - #[pymethod] - fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let obj = zelf.as_object(); + #[pyslot] + fn slot_str(zelf: &PyObject, vm: &VirtualMachine) -> PyResult { + let obj = zelf; // Get OSError fields directly let errno_field = obj.get_attr("errno", vm).ok().filter(|v| !vm.is_none(v)); @@ -2572,19 +2675,25 @@ pub(super) mod types { } // fallback to BaseException.__str__ - Ok(zelf.__str__(vm)) + let base: &Py = + zelf.downcast_ref().expect("OSError is a BaseException"); + Ok(base.__str__(vm)) } #[pymethod] - fn __reduce__(zelf: PyBaseExceptionRef, vm: &VirtualMachine) -> PyTupleRef { - let args = zelf.args(); + fn __reduce__(zelf: &Py, vm: &VirtualMachine) -> PyTupleRef { + let base: &Py = zelf + .as_object() + .downcast_ref() + .expect("OSError is a BaseException"); + let args = base.args(); let obj = zelf.as_object().to_owned(); let mut result: Vec = vec![obj.class().to_owned().into()]; if args.as_slice().len() >= 2 && args.as_slice().len() <= 5 { // SAFETY: len() == 2 is checked so get_arg 1 or 2 won't panic - let errno = zelf.get_arg(0).unwrap(); - let msg = zelf.get_arg(1).unwrap(); + let errno = base.get_arg(0).expect("args len checked"); + let msg = base.get_arg(1).expect("args len checked"); if let Ok(filename) = obj.get_attr("filename", vm) { if !vm.is_none(&filename) { @@ -2630,59 +2739,6 @@ pub(super) mod types { result.into_pytuple(vm) } - // Getters and setters for OSError fields - #[pygetset] - fn errno(&self) -> Option { - self.errno.to_owned() - } - - #[pygetset(setter)] - fn set_errno(&self, value: Option, vm: &VirtualMachine) { - self.errno.swap_to_temporary_refs(value, vm); - } - - #[pygetset] - fn strerror(&self) -> Option { - self.strerror.to_owned() - } - - #[pygetset(setter, name = "strerror")] - fn set_strerror(&self, value: Option, vm: &VirtualMachine) { - self.strerror.swap_to_temporary_refs(value, vm); - } - - #[pygetset] - fn filename(&self) -> Option { - self.filename.to_owned() - } - - #[pygetset(setter)] - fn set_filename(&self, value: Option, vm: &VirtualMachine) { - self.filename.swap_to_temporary_refs(value, vm); - } - - #[pygetset] - fn filename2(&self) -> Option { - self.filename2.to_owned() - } - - #[pygetset(setter)] - fn set_filename2(&self, value: Option, vm: &VirtualMachine) { - self.filename2.swap_to_temporary_refs(value, vm); - } - - #[cfg(windows)] - #[pygetset] - fn winerror(&self) -> Option { - self.winerror.to_owned() - } - - #[cfg(windows)] - #[pygetset(setter)] - fn set_winerror(&self, value: Option, vm: &VirtualMachine) { - self.winerror.swap_to_temporary_refs(value, vm); - } - #[pygetset] fn characters_written(&self, vm: &VirtualMachine) -> PyResult { let written = self.written.load(); @@ -2832,15 +2888,90 @@ pub(super) mod types { #[repr(transparent)] pub struct PyPythonFinalizationError(PyRuntimeError); - #[pyexception(name, base = PyException, ctx = "syntax_error")] - #[derive(Debug)] - #[repr(transparent)] - pub struct PySyntaxError(PyException); + #[pyexception(name, base = PyException, ctx = "syntax_error", traverse = "manual")] + #[repr(C)] + pub struct PySyntaxError { + base: PyException, + #[pymember(writable)] + msg: PyAtomicRef>, + #[pymember(writable)] + filename: PyAtomicRef>, + #[pymember(writable)] + lineno: PyAtomicRef>, + #[pymember(writable)] + offset: PyAtomicRef>, + #[pymember(writable)] + text: PyAtomicRef>, + #[pymember(writable)] + end_lineno: PyAtomicRef>, + #[pymember(writable)] + end_offset: PyAtomicRef>, + #[pymember(writable)] + print_file_and_line: PyAtomicRef>, + #[pymember(name = "_metadata", writable)] + metadata: PyAtomicRef>, + } - #[pyexception(with(Initializer))] + impl crate::class::PySubclass for PySyntaxError { + type Base = PyException; + fn as_base(&self) -> &Self::Base { + &self.base + } + } + + impl core::fmt::Debug for PySyntaxError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PySyntaxError").finish_non_exhaustive() + } + } + + unsafe impl Traverse for PySyntaxError { + fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { + self.base.0.traverse(tracer_fn); + for cell in [ + &self.msg, + &self.filename, + &self.lineno, + &self.offset, + &self.text, + &self.end_lineno, + &self.end_offset, + &self.print_file_and_line, + &self.metadata, + ] { + if let Some(obj) = cell.deref() { + tracer_fn(obj); + } + } + } + } + + impl Constructor for PySyntaxError { + type Args = FuncArgs; + + fn py_new(_cls: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + Ok(Self { + base: PyException(PyBaseException::new(args.args, vm)), + msg: None.into(), + filename: None.into(), + lineno: None.into(), + offset: None.into(), + text: None.into(), + end_lineno: None.into(), + end_offset: None.into(), + print_file_and_line: None.into(), + metadata: None.into(), + }) + } + } + + #[pyexception(with(Constructor, Initializer))] impl PySyntaxError { - #[pymethod] - fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyStrRef { + #[pyslot] + fn slot_str(zelf: &PyObject, vm: &VirtualMachine) -> PyResult { + let exc: &Py = zelf + .downcast_ref() + .expect("slot wrapper checked SyntaxError"); fn basename(filename: &Wtf8) -> &Wtf8 { let bytes = filename.as_bytes(); let pos = if cfg!(windows) { @@ -2855,30 +2986,28 @@ pub(super) mod types { } } - let maybe_lineno = zelf - .as_object() - .get_attr("lineno", vm) - .ok() - .filter(|obj| !vm.is_none(obj)) - .and_then(|obj| obj.str_utf8(vm).ok()); - let maybe_filename = zelf - .as_object() - .get_attr("filename", vm) - .ok() - .filter(|obj| !vm.is_none(obj)) + let maybe_lineno = exc + .lineno + .load_owned() + .filter(|obj| obj.class().is(vm.ctx.types.int_type)) + .and_then(|obj| { + obj.downcast_ref::() + .map(|int| int.as_bigint().to_string()) + }); + let maybe_filename = exc + .filename + .load_owned() + .filter(|obj| obj.fast_isinstance(vm.ctx.types.str_type)) .map(|obj| { obj.str(vm) .unwrap_or_else(|_| vm.ctx.new_str("")) }); - let msg = match zelf.as_object().get_attr("msg", vm) { - Ok(obj) => obj + let msg = match exc.msg.load_owned() { + Some(obj) => obj .str(vm) .unwrap_or_else(|_| vm.ctx.new_str("")), - Err(_) => { - // Fallback to the base formatting if the msg attribute was deleted or attribute lookup fails for any reason. - return Py::::__str__(zelf, vm); - } + None => vm.ctx.none().str(vm)?, }; let msg_with_location_info: Wtf8Buf = match (maybe_lineno, maybe_filename) { @@ -2899,7 +3028,7 @@ pub(super) mod types { (None, None) => msg.as_wtf8().to_owned(), }; - vm.ctx.new_str(msg_with_location_info) + Ok(vm.ctx.new_str(msg_with_location_info)) } } @@ -2910,9 +3039,15 @@ pub(super) mod types { let len = args.args.len(); let new_args = args; - zelf.set_attr("print_file_and_line", vm.ctx.none(), vm)?; + let mut filename = None; + let mut lineno = None; + let mut offset = None; + let mut text = None; + let mut end_lineno = None; + let mut end_offset = None; + let mut metadata = None; - if len == 2 + let have_location = if len == 2 && let Ok(location_tuple) = new_args.args[1] .clone() .downcast::() @@ -2938,27 +3073,43 @@ pub(super) mod types { } } - for (i, &attr) in [ - "filename", - "lineno", - "offset", - "text", - "end_lineno", - "end_offset", - ] - .iter() - .enumerate() - { - let value = if location_tup_len > i { - location_tuple.as_slice()[i].to_owned() - } else { - vm.ctx.none() - }; - zelf.set_attr(attr, value, vm)?; - } - } + let at = |index: usize| -> Option { + (location_tup_len > index).then(|| location_tuple.as_slice()[index].to_owned()) + }; + filename = at(0); + lineno = at(1); + offset = at(2); + text = at(3); + end_lineno = at(4); + end_offset = at(5); + metadata = at(6); + true + } else { + false + }; - PyBaseException::slot_init(zelf, new_args, vm) + PyBaseException::slot_init(zelf, new_args, vm)?; + let exc: &Py = zelf + .downcast_ref() + .expect("SyntaxError instance has SyntaxError payload"); + if len >= 1 { + exc.msg.swap_to_temporary_refs( + Some(exc.base.0.get_arg(0).expect("args len >= 1")), + vm, + ); + } + exc.print_file_and_line + .swap_to_temporary_refs(Some(vm.ctx.none()), vm); + if have_location { + exc.filename.swap_to_temporary_refs(filename, vm); + exc.lineno.swap_to_temporary_refs(lineno, vm); + exc.offset.swap_to_temporary_refs(offset, vm); + exc.text.swap_to_temporary_refs(text, vm); + exc.end_lineno.swap_to_temporary_refs(end_lineno, vm); + exc.end_offset.swap_to_temporary_refs(end_offset, vm); + exc.metadata.swap_to_temporary_refs(metadata, vm); + } + Ok(()) } fn init(_zelf: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> { @@ -3014,41 +3165,54 @@ pub(super) mod types { ))) } - fn unicode_error_attr_str( - exc: &Py, - name: &'static str, + fn unicode_error_payload(exc: &PyObject) -> &Py { + exc.downcast_ref() + .expect("slot wrapper checked the unicode error type") + } + + /// `PyObject_Str` of a nullable object member. A null pointer stringifies + /// as ``; a stored object uses its own `__str__`. + fn unicode_cell_str( + cell: &PyAtomicRef>, vm: &VirtualMachine, ) -> PyResult { - exc.as_object().get_attr(name, vm)?.str(vm) - } - - fn unicode_error_attr_isize( - exc: &Py, - name: &'static str, - vm: &VirtualMachine, - ) -> PyResult { - let value = exc.as_object().get_attr(name, vm)?; - match value.try_index(vm) { - Ok(n) => Ok(n.try_to_primitive::(vm).unwrap_or(0)), - Err(_) => Ok(0), + match cell.load_owned() { + Some(obj) => obj.str(vm), + None => Ok(vm.ctx.new_str("")), } } - fn unicode_error_require_object( - exc: &Py, + fn unicode_error_object( + exc: &Py, + as_bytes: bool, vm: &VirtualMachine, ) -> PyResult { - match exc.as_object().get_attr("object", vm) { - Ok(obj) if !vm.is_none(&obj) => Ok(obj), - _ => Err(vm.new_type_error("UnicodeError 'object' attribute is not set")), + let Some(obj) = exc.object.load_owned() else { + return Err(vm.new_type_error("UnicodeError 'object' attribute is not set")); + }; + let ok = if as_bytes { + obj.fast_isinstance(vm.ctx.types.bytes_type) + } else { + obj.fast_isinstance(vm.ctx.types.str_type) + }; + if !ok { + let kind = if as_bytes { "bytes" } else { "string" }; + return Err( + vm.new_type_error(format!("UnicodeError 'object' attribute must be a {kind}")) + ); } + Ok(obj) } #[pyexception(name, base = PyValueError, ctx = "unicode_error", traverse = "manual")] #[repr(C)] pub struct PyUnicodeError { base: PyValueError, + encoding: PyAtomicRef>, object: PyAtomicRef>, + start: AtomicIsize, + end: AtomicIsize, + reason: PyAtomicRef>, } impl crate::class::PySubclass for PyUnicodeError { @@ -3067,36 +3231,51 @@ pub(super) mod types { unsafe impl Traverse for PyUnicodeError { fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { self.base.0.0.traverse(tracer_fn); + if let Some(obj) = self.encoding.deref() { + tracer_fn(obj); + } if let Some(obj) = self.object.deref() { tracer_fn(obj); } + if let Some(obj) = self.reason.deref() { + tracer_fn(obj); + } } } - #[pyexception(with(Constructor))] - impl PyUnicodeError { - #[pygetset] - fn object(&self) -> Option { - self.object.to_owned() - } - - #[pygetset(setter)] - fn set_object(&self, value: PySetterValue, vm: &VirtualMachine) { - let object = match value { - PySetterValue::Assign(v) => Some(v), - PySetterValue::Delete => None, - }; - self.object.swap_to_temporary_refs(object, vm); - } + fn store_unicode_error( + zelf: &PyObject, + encoding: Option, + object: Option, + start: isize, + end: isize, + reason: Option, + vm: &VirtualMachine, + ) { + let exc: &Py = zelf + .downcast_ref() + .expect("unicode error subclass has PyUnicodeError payload"); + exc.encoding.swap_to_temporary_refs(encoding, vm); + exc.object.swap_to_temporary_refs(object, vm); + exc.start.store(start, Ordering::Relaxed); + exc.end.store(end, Ordering::Relaxed); + exc.reason.swap_to_temporary_refs(reason, vm); } + #[pyexception(with(Constructor))] + impl PyUnicodeError {} + impl Constructor for PyUnicodeError { type Args = FuncArgs; fn py_new(_cls: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { Ok(Self { base: PyValueError(PyException(PyBaseException::new(args.args, vm))), + encoding: None.into(), object: None.into(), + start: AtomicIsize::new(0), + end: AtomicIsize::new(0), + reason: None.into(), }) } } @@ -3104,22 +3283,29 @@ pub(super) mod types { #[pyexception(name, base = PyUnicodeError, ctx = "unicode_decode_error")] #[derive(Debug)] #[repr(transparent)] - pub struct PyUnicodeDecodeError(PyUnicodeError); + pub struct PyUnicodeDecodeError( + #[pymember(name = "encoding", path = "encoding", writable)] + #[pymember(name = "object", path = "object", writable)] + #[pymember(name = "start", path = "start", writable)] + #[pymember(name = "end", path = "end", writable)] + #[pymember(name = "reason", path = "reason", writable)] + PyUnicodeError, + ); #[pyexception(with(Initializer))] impl PyUnicodeDecodeError { - #[pymethod] - fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult { - match zelf.as_object().get_attr("object", vm) { - Ok(obj) if !vm.is_none(&obj) => {} - _ => return Ok(vm.ctx.empty_str.to_owned()), + #[pyslot] + fn slot_str(zelf: &PyObject, vm: &VirtualMachine) -> PyResult { + let payload = unicode_error_payload(zelf); + if payload.object.deref().is_none() { + return Ok(vm.ctx.empty_str.to_owned()); } - let encoding = unicode_error_attr_str(zelf, "encoding", vm)?; - let reason = unicode_error_attr_str(zelf, "reason", vm)?; + let reason = unicode_cell_str(&payload.reason, vm)?; + let encoding = unicode_cell_str(&payload.encoding, vm)?; let object: ArgBytesLike = - unicode_error_require_object(zelf, vm)?.try_into_value(vm)?; - let start = unicode_error_attr_isize(zelf, "start", vm)?; - let end = unicode_error_attr_isize(zelf, "end", vm)?; + unicode_error_object(payload, true, vm)?.try_into_value(vm)?; + let start = payload.start.load(Ordering::Relaxed); + let end = payload.end.load(Ordering::Relaxed); let start_u = usize::try_from(start).ok(); let end_u = usize::try_from(end).ok(); Ok(vm.ctx.new_str( @@ -3149,12 +3335,14 @@ pub(super) mod types { parse_tuple_arity(&args, 5, vm)?; type Args = (PyStrRef, ArgBytesLike, isize, isize, PyStrRef); let (encoding, object, start, end, reason): Args = args.bind(vm)?; - set_attrs!(zelf, vm, - "encoding" => encoding, - "object" => vm.ctx.new_bytes(object.borrow_buf().to_vec()), - "start" => vm.ctx.new_int(start), - "end" => vm.ctx.new_int(end), - "reason" => reason, + store_unicode_error( + zelf, + Some(encoding.into()), + Some(vm.ctx.new_bytes(object.borrow_buf().to_vec()).into()), + start, + end, + Some(reason.into()), + vm, ); Ok(()) } @@ -3167,21 +3355,28 @@ pub(super) mod types { #[pyexception(name, base = PyUnicodeError, ctx = "unicode_encode_error")] #[derive(Debug)] #[repr(transparent)] - pub struct PyUnicodeEncodeError(PyUnicodeError); + pub struct PyUnicodeEncodeError( + #[pymember(name = "encoding", path = "encoding", writable)] + #[pymember(name = "object", path = "object", writable)] + #[pymember(name = "start", path = "start", writable)] + #[pymember(name = "end", path = "end", writable)] + #[pymember(name = "reason", path = "reason", writable)] + PyUnicodeError, + ); #[pyexception(with(Initializer))] impl PyUnicodeEncodeError { - #[pymethod] - fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult { - match zelf.as_object().get_attr("object", vm) { - Ok(obj) if !vm.is_none(&obj) => {} - _ => return Ok(vm.ctx.empty_str.to_owned()), + #[pyslot] + fn slot_str(zelf: &PyObject, vm: &VirtualMachine) -> PyResult { + let payload = unicode_error_payload(zelf); + if payload.object.deref().is_none() { + return Ok(vm.ctx.empty_str.to_owned()); } - let encoding = unicode_error_attr_str(zelf, "encoding", vm)?; - let reason = unicode_error_attr_str(zelf, "reason", vm)?; - let object: PyStrRef = unicode_error_require_object(zelf, vm)?.try_into_value(vm)?; - let start = unicode_error_attr_isize(zelf, "start", vm)?; - let end = unicode_error_attr_isize(zelf, "end", vm)?; + let reason = unicode_cell_str(&payload.reason, vm)?; + let encoding = unicode_cell_str(&payload.encoding, vm)?; + let object: PyStrRef = unicode_error_object(payload, false, vm)?.try_into_value(vm)?; + let start = payload.start.load(Ordering::Relaxed); + let end = payload.end.load(Ordering::Relaxed); let start_u = usize::try_from(start).ok(); let end_u = usize::try_from(end).ok(); Ok(vm.ctx.new_str( @@ -3212,12 +3407,14 @@ pub(super) mod types { parse_tuple_arity(&args, 5, vm)?; type Args = (PyStrRef, PyStrRef, isize, isize, PyStrRef); let (encoding, object, start, end, reason): Args = args.bind(vm)?; - set_attrs!(zelf, vm, - "encoding" => encoding, - "object" => object, - "start" => vm.ctx.new_int(start), - "end" => vm.ctx.new_int(end), - "reason" => reason, + store_unicode_error( + zelf, + Some(encoding.into()), + Some(object.into()), + start, + end, + Some(reason.into()), + vm, ); Ok(()) } @@ -3230,20 +3427,27 @@ pub(super) mod types { #[pyexception(name, base = PyUnicodeError, ctx = "unicode_translate_error")] #[derive(Debug)] #[repr(transparent)] - pub struct PyUnicodeTranslateError(PyUnicodeError); + pub struct PyUnicodeTranslateError( + #[pymember(name = "encoding", path = "encoding", writable)] + #[pymember(name = "object", path = "object", writable)] + #[pymember(name = "start", path = "start", writable)] + #[pymember(name = "end", path = "end", writable)] + #[pymember(name = "reason", path = "reason", writable)] + PyUnicodeError, + ); #[pyexception(with(Initializer))] impl PyUnicodeTranslateError { - #[pymethod] - fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult { - match zelf.as_object().get_attr("object", vm) { - Ok(obj) if !vm.is_none(&obj) => {} - _ => return Ok(vm.ctx.empty_str.to_owned()), + #[pyslot] + fn slot_str(zelf: &PyObject, vm: &VirtualMachine) -> PyResult { + let payload = unicode_error_payload(zelf); + if payload.object.deref().is_none() { + return Ok(vm.ctx.empty_str.to_owned()); } - let reason = unicode_error_attr_str(zelf, "reason", vm)?; - let object: PyStrRef = unicode_error_require_object(zelf, vm)?.try_into_value(vm)?; - let start = unicode_error_attr_isize(zelf, "start", vm)?; - let end = unicode_error_attr_isize(zelf, "end", vm)?; + let reason = unicode_cell_str(&payload.reason, vm)?; + let object: PyStrRef = unicode_error_object(payload, false, vm)?.try_into_value(vm)?; + let start = payload.start.load(Ordering::Relaxed); + let end = payload.end.load(Ordering::Relaxed); let start_u = usize::try_from(start).ok(); let end_u = usize::try_from(end).ok(); Ok(vm.ctx.new_str( @@ -3274,11 +3478,14 @@ pub(super) mod types { parse_tuple_arity(&args, 4, vm)?; type Args = (PyStrRef, isize, isize, PyStrRef); let (object, start, end, reason): Args = args.bind(vm)?; - set_attrs!(zelf, vm, - "object" => object, - "start" => vm.ctx.new_int(start), - "end" => vm.ctx.new_int(end), - "reason" => reason, + store_unicode_error( + zelf, + None, + Some(object.into()), + start, + end, + Some(reason.into()), + vm, ); Ok(()) } diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index c1c53cabb..5af8be7b4 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -874,7 +874,6 @@ unsafe impl Traverse for FrameLocals { /// to keep the hot InterpreterFrame small. pub(crate) struct FrameColdData { pub trace: PyMutex>, - pub trace_lines: PyMutex, pub trace_opcodes: PyMutex, pub temporary_refs: PyMutex>, pub f_extra_locals: PyMutex>, @@ -897,7 +896,6 @@ impl Default for FrameColdData { fn default() -> Self { Self { trace: PyMutex::new(None), - trace_lines: PyMutex::new(true), trace_opcodes: PyMutex::new(false), temporary_refs: PyMutex::new(Vec::new()), f_extra_locals: PyMutex::new(None), @@ -1320,6 +1318,7 @@ impl InterpreterFrame { }; let frame_obj = FrameObject { + f_trace_lines: core::sync::atomic::AtomicBool::new(self.trace_lines_flag()), owned_code: Some(code), owned_globals: Some(globals), owned_builtins: Some(builtins), @@ -1397,6 +1396,7 @@ impl InterpreterFrame { }; let frame_obj = FrameObject { + f_trace_lines: core::sync::atomic::AtomicBool::new(self.trace_lines_flag()), owned_code: Some(code), owned_globals: Some(globals), owned_builtins: Some(builtins), @@ -1492,6 +1492,20 @@ impl InterpreterFrame { self.cold.get().map(|b| &**b) } + /// `f_trace_lines` of the materialized frame object. True when the frame + /// has not been materialized. + pub(crate) fn trace_lines_flag(&self) -> bool { + let mat = self.materialized.load(atomic::Ordering::Relaxed); + if mat != 0 { + // SAFETY: `materialized` holds a `Py` that stays + // allocated while the pointer is published. + return unsafe { &*(mat as *const Py) } + .f_trace_lines + .load(core::sync::atomic::Ordering::Relaxed); + } + true + } + /// Thread still running the frame this one was materialized from, or 0. #[inline] pub(crate) fn attached_tid(&self) -> u64 { @@ -1513,6 +1527,9 @@ impl InterpreterFrame { /// Analogous to CPython's `PyFrameObject`. #[pyclass(module = false, name = "frame", traverse = "manual")] pub struct FrameObject { + // The executing iframe reads this when it points at the frame object. + #[pymember(name = "f_trace_lines", writable)] + pub(crate) f_trace_lines: core::sync::atomic::AtomicBool, // Owned references — keep the pointed-to objects alive for InterpreterFrame's // raw pointers. Wrapped in Option so Traverse::clear can release them, // allowing GC cycle collection to reclaim referenced objects. @@ -1821,6 +1838,7 @@ impl FrameObject { ) }; Self { + f_trace_lines: core::sync::atomic::AtomicBool::new(true), owned_code: Some(code), owned_globals: Some(scope.globals), owned_builtins: Some(builtins), @@ -3353,12 +3371,10 @@ impl ExecutingFrame<'_> { .is_some_and(|c| *c.trace_opcodes.lock()) } - /// f_trace_lines, defaulting to true when cold data is not allocated. + /// f_trace_lines, defaulting to true when no frame object exists. #[inline] fn trace_lines_is_set(&self) -> bool { - self.iframe() - .cold_opt() - .is_none_or(|c| *c.trace_lines.lock()) + self.iframe().trace_lines_flag() } /// Get pending_stack_pops from the frame. @@ -7035,7 +7051,7 @@ impl ExecutingFrame<'_> { .localsplus .stack_index(self_index) .as_ref() - .is_some_and(|self_obj| self_obj.class().is(descr.objclass)) + .is_some_and(|self_obj| self_obj.class().is(descr.common.typ)) { let func = descr.method.func; let callee = Callee::named(descr.method.name).with_instance_arg(true); @@ -7076,7 +7092,7 @@ impl ExecutingFrame<'_> { .localsplus .stack_index(self_index) .as_ref() - .is_some_and(|self_obj| self_obj.class().is(descr.objclass)) + .is_some_and(|self_obj| self_obj.class().is(descr.common.typ)) { let func = descr.method.func; let callee = Callee::named(descr.method.name).with_instance_arg(true); @@ -7117,7 +7133,7 @@ impl ExecutingFrame<'_> { .localsplus .stack_index(self_index) .as_ref() - .is_some_and(|self_obj| self_obj.class().is(descr.objclass)) + .is_some_and(|self_obj| self_obj.class().is(descr.common.typ)) { let func = descr.method.func; let callee = Callee::named(descr.method.name).with_instance_arg(true); @@ -7223,7 +7239,7 @@ impl ExecutingFrame<'_> { .localsplus .stack_index(self_index) .as_ref() - .is_some_and(|self_obj| self_obj.class().is(descr.objclass)) + .is_some_and(|self_obj| self_obj.class().is(descr.common.typ)) { let func = descr.method.func; let callee = Callee::named(descr.method.name).with_instance_arg(true); @@ -10212,6 +10228,7 @@ impl ExecutingFrame<'_> { if let Some(ref descr) = cls_attr && let Some(member_descr) = descr.downcast_ref::() && let Some(offset) = member_descr.slot_offset() + && !member_descr.member.audit_read() && cls.fast_issubclass(&member_descr.common.typ) { unsafe { diff --git a/crates/vm/src/object/core.rs b/crates/vm/src/object/core.rs index 1e5946cd0..2b5f9cab1 100644 --- a/crates/vm/src/object/core.rs +++ b/crates/vm/src/object/core.rs @@ -239,9 +239,13 @@ pub(super) unsafe fn default_dealloc(obj: *mut PyObject) { && !obj_ref.0.ref_count.is_published() { if let Some(ext) = obj_ref.0.ext_ref() { - if let Some(dict) = &ext.dict { - let old = dict.d.write().take(); - drop(old); + if obj_ref + .class() + .slots + .flags + .has_feature(crate::types::PyTypeFlags::HAS_DICT) + { + drop(ext.dict.replace(None)); } for slot in obj_ref.0.slot_cells() { drop(slot.store(None)); @@ -365,16 +369,19 @@ unsafe impl Link for GcLink { /// but some payloads like PyWeak have align 8 due to i64 fields). #[repr(C, align(8))] pub(super) struct ObjExt { - pub(super) dict: Option, + /// Always present. The dict cell is its first field, so + /// `dict_member_offset` addresses that pointer. Null when the type has + /// no dict slot. + pub(super) dict: InstanceDict, } impl ObjExt { fn new(dict: Option, has_dict: bool, inline_values: bool) -> Self { Self { dict: if has_dict { - Some(InstanceDict::from_opt(dict, inline_values)) + InstanceDict::from_opt(dict, inline_values) } else { - None + InstanceDict::from_opt(None, false) }, } } @@ -400,7 +407,7 @@ const EXT_OFFSET: usize = core::mem::size_of::(); pub(crate) fn slot_member_offset(index: usize) -> isize { // Cell 0 sits directly in front of ObjExt. Higher indexes extend further // forward, so a base class offset stays valid on a subclass with more slots. - let cell = core::mem::size_of::>(); + let cell = core::mem::size_of::>>(); -((EXT_OFFSET + (index + 1) * cell) as isize) } @@ -408,7 +415,7 @@ fn slot_region_layout(member_count: usize) -> Option { if member_count == 0 { return None; } - let cell = core::mem::size_of::>(); + let cell = core::mem::size_of::>>(); let bytes = member_count * cell; let align = core::mem::align_of::(); // Padding goes in front of the cells so cell 0 stays flush with ObjExt. @@ -465,6 +472,23 @@ pub struct Py { } pub const SIZEOF_PYOBJECT_HEAD: usize = core::mem::size_of::>(); +/// Byte offset of the payload inside `Py`. +#[must_use] +#[inline] +pub const fn payload_offset() -> usize { + core::mem::offset_of!(Py, payload) +} + +/// Byte offset of the instance-dict pointer from the start of `Py`. +/// +/// The pointer is the first field of `ObjExt`, which sits immediately in front +/// of `Py`. The cell owns the dict. +#[must_use] +#[inline] +pub const fn dict_member_offset() -> isize { + -(core::mem::size_of::() as isize) +} + // ref_count, vtable, gc_pointers (two) and typ are one word each; the gc bits, // generation, owner and refs take eight bytes between them. A 64-bit header had // those eight as the padding its alignment forces, so they cost it nothing; a @@ -514,8 +538,8 @@ impl Py { } /// Member cells are the pointer array immediately before [`ObjExt`]. - /// Layout: `[WeakRefList?][PyAtomicRef; N][ObjExt?][Py]`. - pub(super) fn slot_cells(&self) -> &[PyAtomicRef] { + /// Layout: `[WeakRefList?][PyAtomicRef>; N][ObjExt?][Py]`. + pub(super) fn slot_cells(&self) -> &[PyAtomicRef>] { let Some(ext) = self.ext_ref() else { return &[]; }; @@ -523,12 +547,12 @@ impl Py { if member_count == 0 { return &[]; } - let cell = core::mem::size_of::>(); + let cell = core::mem::size_of::>>(); // Index 0 is the cell adjacent to ObjExt; index i is i cells before it. let first = (ext as *const ObjExt) .addr() .wrapping_sub(member_count * cell); - let ptr = core::ptr::with_exposed_provenance::>(first); + let ptr = core::ptr::with_exposed_provenance::>>(first); unsafe { core::slice::from_raw_parts(ptr, member_count) } } @@ -1108,11 +1132,19 @@ impl Py { pub(crate) const SHARED_KEYS_MAX_SIZE: usize = 30; #[derive(Debug)] +#[repr(C)] pub(crate) struct InstanceDict { - pub(crate) d: PyRwLock>, + /// Owned dict pointer. First field so `dict_member_offset` addresses it. + /// Null when this object has no dict. + pub(crate) dict: PyAtomicRef>, + /// Separate from the pointer: shared-key inline values are valid only + /// while this stays true. Replacing the dict does not change it. inline_values_valid: PyAtomic, } +const _: () = assert!(core::mem::offset_of!(InstanceDict, dict) == 0); +const _: () = assert!(core::mem::offset_of!(ObjExt, dict) == 0); + impl From for InstanceDict { #[inline(always)] fn from(d: PyDictRef) -> Self { @@ -1129,7 +1161,7 @@ impl InstanceDict { #[inline] pub(crate) fn from_opt(d: Option, inline_values: bool) -> Self { Self { - d: PyRwLock::new(d), + dict: PyAtomicRef::from(d), inline_values_valid: Radium::new(inline_values), } } @@ -1156,17 +1188,18 @@ impl InstanceDict { #[inline] pub(crate) fn get(&self) -> Option { - self.d.read().clone() + self.dict.load_owned() } - /// Run `f` on the dict without cloning it. + /// Run `f` on the current dict. /// - /// For callers that only need to look at the dict — a predicate, a version - /// stamp — this drops the refcount round-trip [`Self::get`] pays. `f` runs - /// under the read guard, so it must not run Python or take this lock again. + /// The dict is held by a strong reference for the call, so a concurrent + /// replace cannot free it. `f` must not re-enter `get_or_insert` on this + /// cell. #[inline] pub(crate) fn with(&self, f: impl FnOnce(Option<&Py>) -> R) -> R { - f(self.d.read().as_deref()) + let owned = self.get(); + f(owned.as_deref()) } #[inline] @@ -1176,20 +1209,22 @@ impl InstanceDict { #[inline] pub(crate) fn replace(&self, d: Option) -> Option { - core::mem::replace(&mut self.d.write(), d) + self.dict.store(d) } pub(crate) fn get_or_insert(&self, vm: &VirtualMachine) -> PyDictRef { - if let Some(existing) = self.d.read().as_ref() { - return existing.clone(); - } - let dict = vm.ctx.new_dict(); - let mut d = self.d.write(); - if let Some(existing) = d.as_ref() { - existing.clone() - } else { - *d = Some(dict.clone()); - dict + loop { + if let Some(existing) = self.get() { + return existing; + } + let dict = vm.ctx.new_dict(); + match self.dict.compare_exchange_empty(dict.clone()) { + Ok(()) => return dict, + Err(rejected) => { + drop(rejected); + drop(dict); + } + } } } } @@ -1210,7 +1245,7 @@ impl Py { } /// Deallocate a Py, handling optional prefix(es). - /// Layout: `[WeakRefList?][PyAtomicRef; N][ObjExt?][Py]` + /// Layout: `[WeakRefList?][PyAtomicRef>; N][ObjExt?][Py]` /// /// # Safety /// `ptr` must be a valid pointer from `Py::new` and must not be used after this call. @@ -1257,9 +1292,9 @@ impl Py { cursor = cursor.add(core::mem::size_of::()); } if let Some(region) = slot_region_layout(member_count) { - let cell = core::mem::size_of::>(); + let cell = core::mem::size_of::>>(); let first = cursor.add(region.size() - member_count * cell); - let cells = first.cast::>(); + let cells = first.cast::>>(); for i in 0..member_count { core::ptr::drop_in_place(cells.add(i)); } @@ -1292,7 +1327,7 @@ impl Py { impl Py { /// Allocate a new Py, optionally with prefix(es). /// Returns a raw pointer to the Py (NOT the allocation start). - /// Layout: `[WeakRefList?][PyAtomicRef; N][ObjExt?][Py]` + /// Layout: `[WeakRefList?][PyAtomicRef>; N][ObjExt?][Py]` fn new(payload: T, typ: PyTypeRef, dict: Option) -> *mut Self { let member_count = typ.slots.member_count; let needs_ext = typ @@ -1355,11 +1390,13 @@ impl Py { unsafe { if let Some(offset) = slots_start { let region = slot_region_layout(member_count).unwrap(); - let cell = core::mem::size_of::>(); + let cell = core::mem::size_of::>>(); let first = alloc_ptr.add(offset + region.size() - member_count * cell); - let cells = first.cast::>(); + let cells = first.cast::>>(); for i in 0..member_count { - cells.add(i).write(PyAtomicRef::::new_empty()); + cells + .add(i) + .write(PyAtomicRef::>::new_empty()); } } @@ -1778,7 +1815,13 @@ impl PyObject { #[inline(always)] pub(crate) fn instance_dict(&self) -> Option<&InstanceDict> { - self.0.ext_ref().and_then(|ext| ext.dict.as_ref()) + let ext = self.0.ext_ref()?; + let (flags, _) = self.0.read_type_flags(); + if flags.has_feature(crate::types::PyTypeFlags::HAS_DICT) { + Some(&ext.dict) + } else { + None + } } /// `_PyObject_InlineValues(obj)->valid` when the type has INLINE_VALUES. @@ -2180,11 +2223,13 @@ impl PyObject { drop(self.slot_cell_at(byte_offset).store(value)); } - fn slot_cell_at(&self, byte_offset: isize) -> &PyAtomicRef { + fn slot_cell_at(&self, byte_offset: isize) -> &PyAtomicRef> { let addr = (self as *const Self as *const u8) .addr() .wrapping_add(byte_offset as usize); - let ptr = core::ptr::with_exposed_provenance::>(addr); + let ptr = core::ptr::with_exposed_provenance::>>(addr); + // SAFETY: `byte_offset` addresses an object-pointer cell. Nullable and + // non-null cells share this layout; member loads go through the nullable view. unsafe { &*ptr } } @@ -2302,7 +2347,9 @@ impl PyObject { self_addr.wrapping_sub(EXT_OFFSET), ); let ext = unsafe { &mut *ext_ptr }; - if let Some(dict_ref) = ext.dict.as_ref().and_then(|d| d.replace(None)) { + if flags.has_feature(crate::types::PyTypeFlags::HAS_DICT) + && let Some(dict_ref) = ext.dict.replace(None) + { result.push(dict_ref.into()); } for slot in obj.0.slot_cells() { @@ -2333,7 +2380,11 @@ impl PyObject { // Py_TPFLAGS_HAVE_GC types have tp_clear pub fn gc_has_clear(&self) -> bool { self.0.vtable.clear.is_some() - || self.0.ext_ref().is_some_and(|ext| ext.dict.is_some()) + || self + .0 + .read_type_flags() + .0 + .has_feature(crate::types::PyTypeFlags::HAS_DICT) || self.0.read_type_flags().1 > 0 } } diff --git a/crates/vm/src/object/ext.rs b/crates/vm/src/object/ext.rs index da68c1820..c0b7a66db 100644 --- a/crates/vm/src/object/ext.rs +++ b/crates/vm/src/object/ext.rs @@ -239,6 +239,21 @@ pub struct PyAtomicRef { _phantom: PhantomData, } +// The cell stores a pointer, not an inline `T`. `PhantomData` would +// otherwise make `PyAtomicRef` `!Unpin` because `PyObject` is pinned. +impl Unpin for PyAtomicRef {} + +// Typed and untyped cells are the same pointer-sized slot. A typed nullable +// CAS forwards to the untyped one through this layout. +const _: () = assert!( + core::mem::size_of::>>() + == core::mem::size_of::>() + && core::mem::align_of::>>() + == core::mem::align_of::>() + && core::mem::offset_of!(PyAtomicRef>, inner) + == core::mem::offset_of!(PyAtomicRef<()>, inner) +); + impl Drop for PyAtomicRef { fn drop(&mut self) { // SAFETY: We are dropping the atomic reference, so we can safely @@ -389,43 +404,6 @@ impl PyAtomicRef> { unsafe { self.inner.load(ordering).cast::>().as_ref() } } - pub fn to_owned(&self) -> Option> { - self.to_owned_ordering(Ordering::Relaxed) - } - - pub fn to_owned_ordering(&self, ordering: Ordering) -> Option> { - self.deref_ordering(ordering).map(|x| x.to_owned()) - } - - /// Try-incref read of the current value. - /// - /// Unlike [`Self::to_owned`], this never increfs a destructed object: - /// it uses a conditional incref and revalidates that the slot still - /// holds the same pointer. Returns `None` when the slot is empty. - /// - /// Soundness relies on published-object memory being reclaimed only - /// after a QSBR grace period (see `object::qsbr`), so the refcount - /// word of a concurrently swapped-out value stays readable. - pub fn try_to_owned(&self, ordering: Ordering) -> Option> { - loop { - let ptr = self.inner.load(ordering); - if ptr.is_null() { - return None; - } - if let Some(obj) = unsafe { PyObject::try_to_owned_from_ptr(ptr.cast::()) } { - if core::ptr::eq(self.inner.load(Ordering::Acquire), ptr) { - // SAFETY: the slot only ever stores `PyRef` values. - return Some(unsafe { obj.downcast_unchecked::() }); - } - drop(obj); - } - // Slot changed or the value was torn down mid-read; a failed - // incref with an unchanged slot is impossible (the slot's own - // strong ref keeps the value alive), so this loop progresses. - core::hint::spin_loop(); - } - } - /// # Safety /// The caller is responsible to keep the returned PyRef alive /// until no more reference can be used via PyAtomicRef::deref() @@ -444,81 +422,140 @@ impl PyAtomicRef> { frame.iframe().cold().temporary_refs.lock().push(old.into()); } } -} -impl PyAtomicRef { - /// Empty slot. The pointer is null and owns no reference. - pub(crate) fn new_empty() -> Self { - Self { - inner: Radium::new(null_mut()), - _phantom: PhantomData, - } - } - - /// Borrowed pointer currently stored. Null when the slot is empty. + /// Strong reference to the current value, or `None` when the slot is empty. /// - /// The slot owns the reference, so this stays valid while the slot is - /// unchanged. Traversal calls it with other threads stopped. - pub(crate) fn load_ptr(&self) -> *mut PyObject { - self.inner.load(Ordering::Acquire).cast() - } - - /// Strong reference to the current value. - /// - /// A concurrent store may drop the previous value. The incref is retried - /// until it applies to the pointer still in the slot, and a value placed - /// here is published so its memory outlives that race. - pub(crate) fn load_owned(&self) -> Option { - let ptr = self.inner.load(Ordering::Acquire); - if ptr.is_null() { - return None; - } - // Without threading the slot's own reference keeps the object alive, - // so one incref is enough. With threading, retry when a store retires - // the pointer between the load and the incref. - #[cfg(not(feature = "threading"))] - { - unsafe { PyObject::try_to_owned_from_ptr(ptr.cast()) } - } - #[cfg(feature = "threading")] - { - let mut ptr = ptr; - loop { - if let Some(obj) = unsafe { PyObject::try_to_owned_from_ptr(ptr.cast()) } - && core::ptr::eq(self.inner.load(Ordering::Acquire), ptr) - { - return Some(obj); - } - ptr = self.inner.load(Ordering::Acquire); - if ptr.is_null() { - return None; - } - core::hint::spin_loop(); - } - } + /// This is the owned read for a nullable cell. A concurrent store may drop + /// the previous value; the incref is retried until it applies to the + /// pointer still in the slot. Published-object memory is reclaimed only + /// after a QSBR grace period (see `object::qsbr`), so the refcount word of + /// a swapped-out value stays readable. + pub fn load_owned(&self) -> Option> { + cell_load_owned(&self.inner).map(|obj| { + // SAFETY: a typed cell only stores references of payload `T`. + unsafe { obj.downcast_unchecked() } + }) } /// Replace the stored reference. Returns the previous one, still owned. + pub(crate) fn store(&self, value: Option>) -> Option> { + cell_store(&self.inner, value.map(PyObjectRef::from)).map(|obj| { + // SAFETY: a typed cell only stores references of payload `T`. + unsafe { obj.downcast_unchecked() } + }) + } + + /// Store `value` only when the cell is empty. /// - /// The value placed in the slot is not marked published, so it can still - /// return to the freelist. With threading, the value that leaves the slot - /// is marked so its free waits out a reader that already loaded it. - pub(crate) fn store(&self, value: Option) -> Option { - let new_ptr = match value { - Some(obj) => { - let ptr = obj.into_raw().as_ptr(); - ptr.expose_provenance(); - ptr.cast() + /// On failure the cell is unchanged and `value` is returned still owned. + pub(crate) fn compare_exchange_empty(&self, value: PyRef) -> Result<(), PyRef> { + // SAFETY: every `PyAtomicRef<_>` is an atomic pointer plus a + // zero-sized marker, so the layouts match. The untyped cell API only + // loads and stores that pointer. + let cell = unsafe { &*core::ptr::from_ref(self).cast::>>() }; + cell.compare_exchange_empty(value.into()).map_err(|obj| { + // SAFETY: a typed cell only stores references of payload `T`. + unsafe { obj.downcast_unchecked() } + }) + } +} + +fn empty_cell() -> PyAtomicRef { + PyAtomicRef { + inner: Radium::new(null_mut()), + _phantom: PhantomData, + } +} + +fn cell_load_ptr(inner: &PyAtomic<*mut u8>) -> *mut PyObject { + inner.load(Ordering::Acquire).cast() +} + +/// Try-incref the pointer in `inner`. +/// +/// A concurrent store may drop the previous value. The incref is retried +/// until it applies to the pointer still in the slot. Returns `None` when +/// the slot is empty. +fn cell_load_owned(inner: &PyAtomic<*mut u8>) -> Option { + let ptr = inner.load(Ordering::Acquire); + if ptr.is_null() { + return None; + } + // Without threading the slot's own reference keeps the object alive, + // so one incref is enough. With threading, retry when a store retires + // the pointer between the load and the incref. + #[cfg(not(feature = "threading"))] + { + // SAFETY: `ptr` is non-null and the cell's own reference keeps the + // object alive for this incref. + unsafe { PyObject::try_to_owned_from_ptr(ptr.cast()) } + } + #[cfg(feature = "threading")] + { + let mut ptr = ptr; + loop { + // SAFETY: `ptr` is non-null. A value that left the cell was marked + // published, so its refcount word stays readable until QSBR. + if let Some(obj) = unsafe { PyObject::try_to_owned_from_ptr(ptr.cast()) } + && core::ptr::eq(inner.load(Ordering::Acquire), ptr) + { + return Some(obj); } - None => null_mut(), - }; - let old = Radium::swap(&self.inner, new_ptr, Ordering::AcqRel); - let old = NonNull::new(old.cast()).map(|ptr| unsafe { PyObjectRef::from_raw(ptr) }); - #[cfg(feature = "threading")] - if let Some(old) = old.as_ref() { - old.mark_cache_published(); + ptr = inner.load(Ordering::Acquire); + if ptr.is_null() { + return None; + } + core::hint::spin_loop(); + } + } +} + +/// Replace the stored reference. Returns the previous one, still owned. +/// +/// The value placed in the slot is not marked published, so it can still +/// return to the freelist. With threading, the value that leaves the slot +/// is marked so its free waits out a reader that already loaded it. +fn cell_store(inner: &PyAtomic<*mut u8>, value: Option) -> Option { + let new_ptr = match value { + Some(obj) => { + let ptr = obj.into_raw().as_ptr(); + ptr.expose_provenance(); + ptr.cast() + } + None => null_mut(), + }; + let old = Radium::swap(inner, new_ptr, Ordering::AcqRel); + // SAFETY: a non-null slot pointer is an owning reference the cell just released. + let old = NonNull::new(old.cast()).map(|ptr| unsafe { PyObjectRef::from_raw(ptr) }); + #[cfg(feature = "threading")] + if let Some(old) = old.as_ref() { + old.mark_cache_published(); + } + old +} + +/// Store `value` only when the cell is empty. +/// +/// On failure the cell is unchanged and `value` is returned still owned. +fn cell_compare_exchange_empty( + inner: &PyAtomic<*mut u8>, + value: PyObjectRef, +) -> Result<(), PyObjectRef> { + let raw = value.into_raw(); + let ptr = raw.as_ptr(); + ptr.expose_provenance(); + match inner.compare_exchange( + core::ptr::null_mut(), + ptr.cast(), + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_) => Ok(()), + Err(_) => { + // SAFETY: the exchange did not take the pointer, so `raw` is + // still the unique owning reference. + Err(unsafe { PyObjectRef::from_raw(raw) }) } - old } } @@ -547,6 +584,21 @@ impl Deref for PyAtomicRef { } impl PyAtomicRef { + /// Strong reference to the current value. + /// + /// The cell is never null. A concurrent store may drop the previous value; + /// the incref is retried until it applies to the pointer still in the slot. + pub(crate) fn load_owned(&self) -> PyObjectRef { + cell_load_owned(&self.inner).expect("non-null atomic cell") + } + + /// Replace the stored reference. Returns the previous one, still owned. + /// + /// The cell is never null, before and after the store. + pub(crate) fn store(&self, value: PyObjectRef) -> PyObjectRef { + cell_store(&self.inner, Some(value)).expect("non-null atomic cell") + } + /// # Safety /// The caller is responsible to keep the returned PyRef alive /// until no more reference can be used via PyAtomicRef::deref() @@ -576,6 +628,42 @@ impl From> for PyAtomicRef> { } impl PyAtomicRef> { + /// Empty slot. The pointer is null and owns no reference. + pub(crate) fn new_empty() -> Self { + empty_cell() + } + + /// Borrowed pointer currently stored. Null when the slot is empty. + /// + /// The slot owns the reference, so this stays valid while the slot is + /// unchanged. Traversal calls it with other threads stopped. + pub(crate) fn load_ptr(&self) -> *mut PyObject { + cell_load_ptr(&self.inner) + } + + /// Strong reference to the current value, or `None` when the slot is empty. + /// + /// This is the owned read for a nullable cell. A concurrent store may drop + /// the previous value; the incref is retried until it applies to the + /// pointer still in the slot. Published-object memory is reclaimed only + /// after a QSBR grace period (see `object::qsbr`), so the refcount word of + /// a swapped-out value stays readable. + pub fn load_owned(&self) -> Option { + cell_load_owned(&self.inner) + } + + /// Replace the stored reference. Returns the previous one, still owned. + pub(crate) fn store(&self, value: Option) -> Option { + cell_store(&self.inner, value) + } + + /// Store `value` only when the cell is empty. + /// + /// On failure the cell is unchanged and `value` is returned still owned. + pub(crate) fn compare_exchange_empty(&self, value: PyObjectRef) -> Result<(), PyObjectRef> { + cell_compare_exchange_empty(&self.inner, value) + } + pub fn deref(&self) -> Option<&PyObject> { self.deref_ordering(Ordering::Relaxed) } @@ -584,14 +672,6 @@ impl PyAtomicRef> { unsafe { self.inner.load(ordering).cast::().as_ref() } } - pub fn to_owned(&self) -> Option { - self.to_owned_ordering(Ordering::Relaxed) - } - - pub fn to_owned_ordering(&self, ordering: Ordering) -> Option { - self.deref_ordering(ordering).map(|x| x.to_owned()) - } - /// # Safety /// The caller is responsible to keep the returned PyRef alive /// until no more reference can be used via PyAtomicRef::deref() diff --git a/crates/vm/src/object/traverse.rs b/crates/vm/src/object/traverse.rs index 4f83e77b2..0f7b13d16 100644 --- a/crates/vm/src/object/traverse.rs +++ b/crates/vm/src/object/traverse.rs @@ -116,10 +116,28 @@ where } } +unsafe impl Traverse for super::ext::PyAtomicRef> { + #[inline] + fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) { + if let Some(obj) = self.deref() { + traverse_fn(obj.as_object()); + } + } +} + unsafe impl Traverse for super::ext::PyAtomicRef { + #[inline] + fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) { + traverse_fn(self); + } +} + +unsafe impl Traverse for super::ext::PyAtomicRef> { #[inline] fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) { let ptr = self.load_ptr(); + // SAFETY: traversal runs with other threads stopped, so a non-null + // slot pointer stays allocated for this borrow. if let Some(obj) = unsafe { ptr.as_ref() } { traverse_fn(obj); } diff --git a/crates/vm/src/object/traverse_object.rs b/crates/vm/src/object/traverse_object.rs index bb427590f..a2db83c7e 100644 --- a/crates/vm/src/object/traverse_object.rs +++ b/crates/vm/src/object/traverse_object.rs @@ -48,7 +48,7 @@ impl PyObjVTable { unsafe impl Traverse for InstanceDict { fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { - self.d.traverse(tracer_fn) + self.dict.traverse(tracer_fn) } } diff --git a/crates/vm/src/sliceable.rs b/crates/vm/src/sliceable.rs index 6311c2993..e859c7a3a 100644 --- a/crates/vm/src/sliceable.rs +++ b/crates/vm/src/sliceable.rs @@ -395,7 +395,7 @@ impl SaturatedSlice { let start = to_isize_index(vm, slice.start_ref(vm))? .unwrap_or_else(|| if step.is_negative() { isize::MAX } else { 0 }); - let stop = to_isize_index(vm, &slice.stop(vm))?.unwrap_or_else(|| { + let stop = to_isize_index(vm, &slice.stop)?.unwrap_or_else(|| { if step.is_negative() { isize::MIN } else { diff --git a/crates/vm/src/stdlib/_abc.rs b/crates/vm/src/stdlib/_abc.rs index 99e1d1e65..e759e2fc4 100644 --- a/crates/vm/src/stdlib/_abc.rs +++ b/crates/vm/src/stdlib/_abc.rs @@ -134,7 +134,6 @@ mod _abc { Ok(()) } - /// Returns the current ABC cache token. #[pyfunction] fn get_cache_token() -> u64 { get_invalidation_counter() @@ -203,7 +202,6 @@ mod _abc { cls: PyObjectRef, } - /// Internal ABC helper for class set-up. Should be never used outside abc module. #[pyfunction] fn _abc_init(AbcSelf { cls }: AbcSelf, vm: &VirtualMachine) -> PyResult<()> { compute_abstract_methods(&cls, vm)?; @@ -220,7 +218,6 @@ mod _abc { Ok(()) } - /// Internal ABC helper for subclass registration. Should be never used outside abc module. #[pyfunction] fn _abc_register( AbcSelf { cls }: AbcSelf, @@ -269,7 +266,6 @@ mod _abc { Ok(subclass) } - /// Internal ABC helper for instance checks. Should be never used outside abc module. #[pyfunction] fn _abc_instancecheck( AbcSelf { cls }: AbcSelf, @@ -345,7 +341,6 @@ mod _abc { Ok(None) } - /// Internal ABC helper for subclass checks. Should be never used outside abc module. #[pyfunction] fn _abc_subclasscheck( AbcSelf { cls }: AbcSelf, @@ -431,7 +426,6 @@ mod _abc { Ok(false) } - /// Internal ABC helper for cache and registry debugging. #[pyfunction] fn _get_dump(AbcSelf { cls }: AbcSelf, vm: &VirtualMachine) -> PyResult { let impl_data = get_impl(&cls, vm)?; @@ -473,7 +467,6 @@ mod _abc { ])) } - /// Internal ABC helper to reset registry of a given class. #[pyfunction] fn _reset_registry(AbcSelf { cls }: AbcSelf, vm: &VirtualMachine) -> PyResult<()> { let impl_data = get_impl(&cls, vm)?; @@ -485,7 +478,6 @@ mod _abc { Ok(()) } - /// Internal ABC helper to reset both caches of a given class. #[pyfunction] fn _reset_caches(AbcSelf { cls }: AbcSelf, vm: &VirtualMachine) -> PyResult<()> { let impl_data = get_impl(&cls, vm)?; diff --git a/crates/vm/src/stdlib/_collections.rs b/crates/vm/src/stdlib/_collections.rs index 3e5be342d..c0b0fd180 100644 --- a/crates/vm/src/stdlib/_collections.rs +++ b/crates/vm/src/stdlib/_collections.rs @@ -19,7 +19,7 @@ mod _collections { function::{ ArgIterable, FuncArgs, KwArgs, OptionalArg, PyComparisonValue, PySetterValue, PySsize, }, - object::{Traverse, TraverseFn}, + object::{PyAtomicRef, Traverse, TraverseFn}, protocol::{PyIter, PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods}, recursion::ReprGuard, sequence::{MutObjectSequenceOp, OptionalRangeArgs}, @@ -925,10 +925,20 @@ mod _collections { unhashable = true, traverse = "manual" )] - #[derive(Debug, Default)] + #[derive(Debug)] struct PyDefaultDict { dict: PyDict, - default_factory: PyRwLock>, + #[pymember(writable)] + default_factory: PyAtomicRef>, + } + + impl Default for PyDefaultDict { + fn default() -> Self { + Self { + dict: PyDict::default(), + default_factory: PyAtomicRef::new_empty(), + } + } } // SAFETY: Traverse visits each owned Python reference at most once. @@ -940,7 +950,7 @@ mod _collections { fn clear(&mut self, out: &mut Vec) { Traverse::clear(&mut self.dict, out); - if let Some(factory) = self.default_factory.get_mut().take() { + if let Some(factory) = self.default_factory.store(None) { out.push(factory); } } @@ -951,33 +961,12 @@ mod _collections { flags(BASETYPE, MAPPING, HAS_DICT) )] impl PyDefaultDict { - #[pymember(type = "object")] - fn default_factory(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: &Py = zelf.try_to_value(vm)?; - Ok(zelf - .default_factory - .read() - .clone() - .unwrap_or_else(|| vm.ctx.none())) - } - - #[pymember(type = "object", setter)] - fn set_default_factory( - vm: &VirtualMachine, - zelf: PyObjectRef, - value: PySetterValue, - ) -> PyResult<()> { - let zelf: &Py = zelf.try_to_value(vm)?; - *zelf.default_factory.write() = match value { - PySetterValue::Assign(v) if !v.is(&vm.ctx.none()) => Some(v), - _ => None, - }; - Ok(()) - } - #[pymethod] fn __missing__(&self, object: PyObjectRef, vm: &VirtualMachine) -> PyResult { - let factory = self.default_factory.read().clone(); + let factory = self + .default_factory + .load_owned() + .filter(|factory| !vm.is_none(factory)); if let Some(f) = factory { let value = f.call((), vm)?; @@ -990,11 +979,14 @@ mod _collections { #[pymethod] #[pymethod(name = "__copy__")] fn copy(&self) -> Self { - let default_factory = self.default_factory.read().clone(); + let default_factory = match self.default_factory.load_owned() { + Some(factory) => PyAtomicRef::from(Some(factory)), + None => PyAtomicRef::new_empty(), + }; Self { dict: self.dict.copy(), - default_factory: PyRwLock::new(default_factory), + default_factory, } } @@ -1002,7 +994,11 @@ mod _collections { fn __reduce__(zelf: PyRef, vm: &VirtualMachine) -> PyResult { let cls = zelf.class().to_owned(); - let default_factory = zelf.default_factory.read().clone(); + // NULL and None both mean "no factory": the args tuple is empty. + let default_factory = zelf + .default_factory + .load_owned() + .filter(|factory| !vm.is_none(factory)); let factory_tuple_elements = default_factory.map_or_else(Vec::new, |factory| vec![factory]); let factory_tuple = vm.ctx.new_tuple(factory_tuple_elements); @@ -1034,13 +1030,13 @@ mod _collections { return not_implemented(); } - (zelf.default_factory.read().clone(), zelf.dict.copy()) + (zelf.default_factory.load_owned(), zelf.dict.copy()) } else if let Some(zelf) = rhs.downcast_ref::() { let Some(dict) = lhs.downcast_ref::() else { return not_implemented(); }; - (zelf.default_factory.read().clone(), dict.copy()) + (zelf.default_factory.load_owned(), dict.copy()) } else { return Err(vm.new_type_error(format!( "unsupported operand type(s) for |: '{}' and '{}'", @@ -1053,7 +1049,10 @@ mod _collections { Ok(Self { dict, - default_factory: PyRwLock::new(default_factory), + default_factory: match default_factory { + Some(factory) => PyAtomicRef::from(Some(factory)), + None => PyAtomicRef::new_empty(), + }, } .to_pyobject(vm)) } @@ -1077,7 +1076,7 @@ mod _collections { } })?; - *zelf.default_factory.write() = default_factory; + zelf.default_factory.store(default_factory); zelf.dict.update( OptionalArg::from_option(args.take_positional()), @@ -1091,7 +1090,7 @@ mod _collections { impl Representable for PyDefaultDict { fn repr_str(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let default_factory = zelf.default_factory.read(); + let default_factory = zelf.default_factory.load_owned(); let factory_repr = match default_factory.as_ref() { Some(factory) => { @@ -1183,7 +1182,8 @@ mod _collections { struct PyTupleGetter { #[pytraverse(skip)] index: isize, - doc: PyRwLock>, + #[pymember(name = "__doc__", writable)] + doc: PyAtomicRef>, } impl Constructor for PyTupleGetter { @@ -1196,7 +1196,7 @@ mod _collections { ) -> PyResult { Ok(Self { index, - doc: PyRwLock::new(Some(doc)), + doc: PyAtomicRef::from(Some(doc)), }) } } @@ -1204,27 +1204,7 @@ mod _collections { #[pyclass(with(Constructor, GetDescriptor, Representable))] impl PyTupleGetter { fn doc(&self, vm: &VirtualMachine) -> PyObjectRef { - self.doc.read().clone().unwrap_or_else(|| vm.ctx.none()) - } - - #[pymember(type = "object")] - fn __doc__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: &Py = zelf.try_to_value(vm)?; - Ok(zelf.doc(vm)) - } - - #[pymember(type = "object", setter)] - fn set___doc__( - vm: &VirtualMachine, - zelf: PyObjectRef, - value: PySetterValue, - ) -> PyResult<()> { - let zelf: &Py = zelf.try_to_value(vm)?; - *zelf.doc.write() = match value { - PySetterValue::Assign(doc) => Some(doc), - PySetterValue::Delete => None, - }; - Ok(()) + self.doc.load_owned().unwrap_or_else(|| vm.ctx.none()) } #[pyslot] diff --git a/crates/vm/src/stdlib/_ctypes.rs b/crates/vm/src/stdlib/_ctypes.rs index 54187d292..f89742a32 100644 --- a/crates/vm/src/stdlib/_ctypes.rs +++ b/crates/vm/src/stdlib/_ctypes.rs @@ -335,7 +335,6 @@ pub(crate) mod _ctypes { exc_type } - /// Get the size of a ctypes type or instance #[pyfunction] pub(crate) fn sizeof(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { use super::structure::PyCStructType; @@ -847,11 +846,6 @@ pub(crate) mod _ctypes { super::function::cast_impl(&obj, obj.clone(), &typ, vm) } - /// Return buffer interface information for a ctypes type or object. - /// Returns a tuple (format, ndim, shape) where: - /// - format: PEP 3118 format string - /// - ndim: number of dimensions - /// - shape: tuple of dimension sizes #[pyfunction] fn buffer_info(object: PyObjectRef, vm: &VirtualMachine) -> PyResult { // Determine if object is a type or an instance diff --git a/crates/vm/src/stdlib/_ctypes/array.rs b/crates/vm/src/stdlib/_ctypes/array.rs index 85668bb62..b0d4695e9 100644 --- a/crates/vm/src/stdlib/_ctypes/array.rs +++ b/crates/vm/src/stdlib/_ctypes/array.rs @@ -388,8 +388,7 @@ impl AsNumber for PyCArrayType { } } -/// PyCArray - Array instance -/// All array metadata (element_type, length, element_size) is stored in the type's StgInfo +// All array metadata (element_type, length, element_size) is stored in the type's StgInfo #[pyclass( name = "Array", base = PyCData, diff --git a/crates/vm/src/stdlib/_ctypes/base.rs b/crates/vm/src/stdlib/_ctypes/base.rs index 715967cb3..01a25480d 100644 --- a/crates/vm/src/stdlib/_ctypes/base.rs +++ b/crates/vm/src/stdlib/_ctypes/base.rs @@ -1245,7 +1245,6 @@ impl PyCData { // PyCField - Field descriptor for Structure/Union types -/// CField descriptor for Structure/Union field access #[pyclass(name = "CField", module = "_ctypes")] #[derive(Debug, PyPayload)] pub struct PyCField { diff --git a/crates/vm/src/stdlib/_ctypes/function.rs b/crates/vm/src/stdlib/_ctypes/function.rs index 77a66aca7..d99ceadae 100644 --- a/crates/vm/src/stdlib/_ctypes/function.rs +++ b/crates/vm/src/stdlib/_ctypes/function.rs @@ -344,8 +344,7 @@ impl PyCFuncPtrType { } } -/// PyCFuncPtr - Function pointer instance -/// Saved in _base.buffer +// Saved in _base.buffer #[pyclass( module = "_ctypes", name = "CFuncPtr", diff --git a/crates/vm/src/stdlib/_ctypes/pointer.rs b/crates/vm/src/stdlib/_ctypes/pointer.rs index fe57c3b09..d85f969af 100644 --- a/crates/vm/src/stdlib/_ctypes/pointer.rs +++ b/crates/vm/src/stdlib/_ctypes/pointer.rs @@ -249,8 +249,7 @@ impl AsNumber for PyCPointerType { } } -/// PyCPointer - Pointer instance -/// `contents` is a computed property, not a stored field. +// `contents` is a computed property, not a stored field. #[pyclass( name = "_Pointer", base = PyCData, diff --git a/crates/vm/src/stdlib/_ctypes/structure.rs b/crates/vm/src/stdlib/_ctypes/structure.rs index 6486e1dd7..959fff41a 100644 --- a/crates/vm/src/stdlib/_ctypes/structure.rs +++ b/crates/vm/src/stdlib/_ctypes/structure.rs @@ -662,7 +662,6 @@ impl SetAttr for PyCStructType { } } -/// PyCStructure - base class for Structure instances #[pyclass( module = "_ctypes", name = "Structure", diff --git a/crates/vm/src/stdlib/_ctypes/union.rs b/crates/vm/src/stdlib/_ctypes/union.rs index 7e424294f..2b19aa59c 100644 --- a/crates/vm/src/stdlib/_ctypes/union.rs +++ b/crates/vm/src/stdlib/_ctypes/union.rs @@ -534,7 +534,6 @@ impl SetAttr for PyCUnionType { } } -/// PyCUnion - base class for Union #[pyclass(module = "_ctypes", name = "Union", base = PyCData, metaclass = "PyCUnionType")] #[repr(transparent)] pub(crate) struct PyCUnion(pub PyCData); diff --git a/crates/vm/src/stdlib/_functools.rs b/crates/vm/src/stdlib/_functools.rs index 3d2ebf3f6..91cc1d530 100644 --- a/crates/vm/src/stdlib/_functools.rs +++ b/crates/vm/src/stdlib/_functools.rs @@ -692,9 +692,9 @@ mod _functools { #[pyclass(flags(IMMUTABLETYPE, DISALLOW_INSTANTIATION))] impl PyLruListElem {} - /// Native implementation of `functools._lru_cache_wrapper`, mirroring CPython's - /// `_functools` accelerator so `functools.lru_cache` doesn't fall back to the much - /// slower pure-Python implementation in `Lib/functools.py`. + // Native implementation of `functools._lru_cache_wrapper`, mirroring CPython's + // `_functools` accelerator so `functools.lru_cache` doesn't fall back to the much + // slower pure-Python implementation in `Lib/functools.py`. #[pyattr] #[pyclass(name = "_lru_cache_wrapper", module = "functools")] #[derive(PyPayload)] diff --git a/crates/vm/src/stdlib/_operator.rs b/crates/vm/src/stdlib/_operator.rs index df5794c3b..535bef0b7 100644 --- a/crates/vm/src/stdlib/_operator.rs +++ b/crates/vm/src/stdlib/_operator.rs @@ -350,14 +350,6 @@ mod _operator { Ok(res) } - /// attrgetter(attr, /, *attrs) - /// -- - /// - /// Return a callable object that fetches the given attribute(s) from its operand. - /// After f = attrgetter('name'), the call f(r) returns r.name. - /// After g = attrgetter('name', 'date'), the call g(r) returns (r.name, r.date). - /// After h = attrgetter('name.first', 'name.last'), the call h(r) returns - /// (r.name.first, r.name.last). #[pyattr] #[pyclass(name = "attrgetter")] #[derive(Debug, PyPayload)] @@ -475,12 +467,6 @@ mod _operator { } } - /// itemgetter(item, /, *items) - /// -- - /// - /// Return a callable object that fetches the given item(s) from its operand. - /// After f = itemgetter(2), the call f(r) returns r[2]. - /// After g = itemgetter(2, 5, 3), the call g(r) returns (r[2], r[5], r[3]) #[pyattr] #[pyclass(name = "itemgetter")] #[derive(Debug, PyPayload)] @@ -567,13 +553,6 @@ mod _operator { } } - /// methodcaller(name, /, *args, **kwargs) - /// -- - /// - /// Return a callable object that calls the given method on its operand. - /// After f = methodcaller('name'), the call f(r) returns r.name(). - /// After g = methodcaller('name', 'date', foo=1), the call g(r) returns - /// r.name('date', foo=1). #[pyattr] #[pyclass(name = "methodcaller")] #[derive(Debug, PyPayload)] diff --git a/crates/vm/src/stdlib/_thread.rs b/crates/vm/src/stdlib/_thread.rs index ce7983ea1..5721fab56 100644 --- a/crates/vm/src/stdlib/_thread.rs +++ b/crates/vm/src/stdlib/_thread.rs @@ -373,7 +373,7 @@ pub(crate) mod _thread { } } - /// Get thread identity - uses pthread_self() on Unix for fork compatibility + // uses pthread_self() on Unix for fork compatibility #[pyfunction] #[must_use] pub fn get_ident() -> u64 { @@ -999,8 +999,7 @@ pub(crate) mod _thread { vm.state.main_thread_ident.store(ident); } - /// ExceptHookArgs - simple class to hold exception hook arguments - /// This allows threading.py to import _excepthook and _ExceptHookArgs from _thread + // This allows threading.py to import _excepthook and _ExceptHookArgs from _thread #[pyattr] #[pyclass(module = "_thread", name = "_ExceptHookArgs")] #[derive(Debug, PyPayload)] @@ -1065,7 +1064,6 @@ pub(crate) mod _thread { } } - /// Handle uncaught exception in Thread.run() #[pyfunction] fn _excepthook(args: crate::PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { // Type check: args must be _ExceptHookArgs diff --git a/crates/vm/src/stdlib/_typing.rs b/crates/vm/src/stdlib/_typing.rs index 377e79346..bd158c1a1 100644 --- a/crates/vm/src/stdlib/_typing.rs +++ b/crates/vm/src/stdlib/_typing.rs @@ -195,6 +195,7 @@ pub(crate) mod decl { #[pyclass(name, module = "typing")] #[derive(Debug, PyPayload)] pub(crate) struct TypeAliasType { + #[pymember(name = "__name__")] name: PyStrRef, type_params: PyTupleRef, compute_value: PyObjectRef, @@ -240,11 +241,6 @@ pub(crate) mod decl { } } - #[pygetset] - fn __name__(&self) -> PyObjectRef { - self.name.clone().into() - } - #[pygetset] fn __value__(&self, vm: &VirtualMachine) -> PyResult { let cached = self.cached_value.lock().clone(); diff --git a/crates/vm/src/stdlib/_winapi.rs b/crates/vm/src/stdlib/_winapi.rs index 9d8a51c26..2fd640be2 100644 --- a/crates/vm/src/stdlib/_winapi.rs +++ b/crates/vm/src/stdlib/_winapi.rs @@ -674,7 +674,6 @@ mod _winapi { vm.ctx.new_str(result_str) } - /// GetShortPathName - Return the short version of the provided path. #[pyfunction] fn GetShortPathName(path: PyStrRef, vm: &VirtualMachine) -> PyResult { let path = path @@ -685,7 +684,6 @@ mod _winapi { Ok(path_name_result_to_pystr(wide, vm)) } - /// GetLongPathName - Return the long version of the provided path. #[pyfunction] fn GetLongPathName(path: PyStrRef, vm: &VirtualMachine) -> PyResult { let path = path @@ -915,7 +913,6 @@ mod _winapi { const MAXIMUM_WAIT_OBJECTS: usize = host_winapi::MAXIMUM_WAIT_OBJECTS as usize; - /// BatchedWaitForMultipleObjects - Wait for multiple handles, supporting more than 64. #[pyfunction] fn BatchedWaitForMultipleObjects( handle_seq: PyObjectRef, @@ -1070,7 +1067,6 @@ mod _winapi { host_winapi::virtual_query_size(address).map_err(|e| e.to_pyexception(vm)) } - /// CopyFile2 - Copy a file with extended parameters. #[pyfunction] fn CopyFile2( existing_file_name: PyStrRef, @@ -1090,7 +1086,6 @@ mod _winapi { host_winapi::copy_file2(&src_wide, &dst_wide, flags).map_err(|e| e.to_pyexception(vm)) } - /// _mimetypes_read_windows_registry - Read MIME type associations from registry. #[pyfunction] fn _mimetypes_read_windows_registry( on_type_read: PyObjectRef, diff --git a/crates/vm/src/stdlib/gc.rs b/crates/vm/src/stdlib/gc.rs index 31f46408f..2ebfd7928 100644 --- a/crates/vm/src/stdlib/gc.rs +++ b/crates/vm/src/stdlib/gc.rs @@ -21,19 +21,16 @@ mod gc { #[pyattr] const DEBUG_LEAK: u32 = gc_state::GcDebugFlags::LEAK.bits(); - /// Enable automatic garbage collection. #[pyfunction] fn enable(vm: &VirtualMachine) { vm.state.gc.enable(); } - /// Disable automatic garbage collection. #[pyfunction] fn disable(vm: &VirtualMachine) { vm.state.gc.disable(); } - /// Return true if automatic gc is enabled. #[pyfunction] fn isenabled(vm: &VirtualMachine) -> bool { vm.state.gc.is_enabled() @@ -77,7 +74,6 @@ mod gc { Ok((result.collected + result.uncollectable) as i32) } - /// Return the current collection thresholds as a tuple. #[pyfunction] fn get_threshold(vm: &VirtualMachine) -> PyObjectRef { let (t0, t1, t2) = vm.state.gc.get_threshold(); @@ -90,7 +86,6 @@ mod gc { .into() } - /// Set the collection thresholds. #[pyfunction] fn set_threshold( threshold0: u32, @@ -105,7 +100,6 @@ mod gc { ); } - /// Return the current collection counts as a tuple. #[pyfunction] fn get_count(vm: &VirtualMachine) -> PyObjectRef { let (c0, c1, c2) = gc_state::gc_state().get_count(); @@ -118,13 +112,11 @@ mod gc { .into() } - /// Return the current debugging flags. #[pyfunction] fn get_debug(vm: &VirtualMachine) -> u32 { vm.state.gc.get_debug().bits() } - /// Set the debugging flags. #[pyfunction] fn set_debug(flags: u32, vm: &VirtualMachine) { vm.state @@ -132,7 +124,6 @@ mod gc { .set_debug(gc_state::GcDebugFlags::from_bits_truncate(flags)); } - /// Return a list of per-generation gc stats. #[pyfunction] fn get_stats(vm: &VirtualMachine) -> PyResult { let stats = vm.state.gc.get_stats(); @@ -174,7 +165,6 @@ mod gc { Ok(vm.ctx.new_list(objects)) } - /// Return the list of objects directly referred to by any of the arguments. #[pyfunction] fn get_referents(objs: PosArgs, vm: &VirtualMachine) -> PyListRef { let mut result = Vec::new(); @@ -187,7 +177,6 @@ mod gc { vm.ctx.new_list(result) } - /// Return the list of objects that directly refer to any of the arguments. #[pyfunction] fn get_referrers(objs: PosArgs, vm: &VirtualMachine) -> PyListRef { use std::collections::HashSet; @@ -243,32 +232,27 @@ mod gc { vm.ctx.new_list(result) } - /// Return True if the object is tracked by the garbage collector. #[pyfunction] fn is_tracked(obj: PyObjectRef) -> bool { // An object is tracked if it has IS_TRACE = true (has a trace function) obj.is_gc_tracked() } - /// Return True if the object has been finalized by the garbage collector. #[pyfunction] fn is_finalized(obj: PyObjectRef) -> bool { obj.gc_finalized() } - /// Freeze all objects tracked by gc. #[pyfunction] fn freeze(vm: &VirtualMachine) { vm.state.gc.freeze(); } - /// Unfreeze all objects in the permanent generation. #[pyfunction] fn unfreeze(vm: &VirtualMachine) { vm.state.gc.unfreeze(); } - /// Return the number of objects in the permanent generation. #[pyfunction] fn get_freeze_count() -> usize { gc_state::gc_state().get_freeze_count() diff --git a/crates/vm/src/stdlib/nt.rs b/crates/vm/src/stdlib/nt.rs index 2283a4693..d4f3751b7 100644 --- a/crates/vm/src/stdlib/nt.rs +++ b/crates/vm/src/stdlib/nt.rs @@ -203,8 +203,7 @@ pub(crate) mod module { } } - /// Get the real file name (with correct case) without accessing the file. - /// Uses FindFirstFileW to get the name as stored on the filesystem. + // Uses FindFirstFileW to get the name as stored on the filesystem. #[pyfunction] fn _findfirstfile(path: OsPath, vm: &VirtualMachine) -> PyResult { let wide = path.to_wide_cstring(vm)?; @@ -305,55 +304,48 @@ pub(crate) mod module { } } - /// Check if a path is a directory. - /// return _testFileType(path, PY_IFDIR) + // return _testFileType(path, PY_IFDIR) #[pyfunction] fn _path_isdir(args: PathArg, vm: &VirtualMachine) -> bool { args.to_path_or_fd(vm) .is_some_and(|p| _test_file_type(&p, PY_IFDIR)) } - /// Check if a path is a regular file. - /// return _testFileType(path, PY_IFREG) + // return _testFileType(path, PY_IFREG) #[pyfunction] fn _path_isfile(args: PathArg, vm: &VirtualMachine) -> bool { args.to_path_or_fd(vm) .is_some_and(|p| _test_file_type(&p, PY_IFREG)) } - /// Check if a path is a symbolic link. - /// return _testFileType(path, PY_IFLNK) + // return _testFileType(path, PY_IFLNK) #[pyfunction] fn _path_islink(args: PathArg, vm: &VirtualMachine) -> bool { args.to_path_or_fd(vm) .is_some_and(|p| _test_file_type(&p, PY_IFLNK)) } - /// Check if a path is a junction (mount point). - /// return _testFileType(path, PY_IFMNT) + // return _testFileType(path, PY_IFMNT) #[pyfunction] fn _path_isjunction(args: PathArg, vm: &VirtualMachine) -> bool { args.to_path_or_fd(vm) .is_some_and(|p| _test_file_type(&p, PY_IFMNT)) } - /// Check if a path exists (follows symlinks). - /// return _testFileExists(path, TRUE) + // return _testFileExists(path, TRUE) #[pyfunction] fn _path_exists(args: PathArg, vm: &VirtualMachine) -> bool { args.to_path_or_fd(vm) .is_some_and(|p| _test_file_exists(&p, true)) } - /// Check if a path exists (does not follow symlinks). - /// return _testFileExists(path, FALSE) + // return _testFileExists(path, FALSE) #[pyfunction] fn _path_lexists(args: PathArg, vm: &VirtualMachine) -> bool { args.to_path_or_fd(vm) .is_some_and(|p| _test_file_exists(&p, false)) } - /// Check if a path is on a Windows Dev Drive. #[pyfunction] fn _path_isdevdrive(path: OsPath, vm: &VirtualMachine) -> PyResult { let path = path.to_wide_cstring(vm)?; @@ -1037,8 +1029,7 @@ pub(crate) mod module { host_nt::dup2(args.fd, args.fd2, args.inheritable).map_err(|e| e.to_pyexception(vm)) } - /// Windows-specific readlink that preserves \\?\ prefix for junctions - /// returns the substitute name from reparse data which includes the prefix + // returns the substitute name from reparse data which includes the prefix #[pyfunction] fn readlink(path: OsPath, vm: &VirtualMachine) -> PyResult { let mode = path.mode(); diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index 11e869e33..1660ca750 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -764,7 +764,6 @@ pub mod sys { false // RustPython has no GIL (like free-threaded Python) } - /// Return True if remote debugging is enabled, False otherwise. #[pyfunction] const fn is_remote_debug_enabled() -> bool { false // RustPython does not support remote debugging @@ -1063,8 +1062,6 @@ pub mod sys { }) } - /// Return a dictionary mapping each thread's identifier to the topmost stack frame - /// currently active in that thread at the time the function is called. #[cfg(feature = "threading")] #[pyfunction] fn _current_frames(vm: &VirtualMachine) -> PyResult { @@ -1082,8 +1079,6 @@ pub mod sys { Ok(dict) } - /// Return a dictionary mapping each thread's identifier to its currently - /// active exception, or None if no exception is active. #[cfg(feature = "threading")] #[pyfunction] fn _current_exceptions(vm: &VirtualMachine) -> PyResult { @@ -1109,7 +1104,7 @@ pub mod sys { Ok(dict) } - /// Stub for non-threading builds - returns empty dict + // Stub for non-threading builds - returns empty dict #[cfg(not(feature = "threading"))] #[pyfunction] fn _current_frames(vm: &VirtualMachine) -> PyDictRef { @@ -1261,7 +1256,7 @@ pub mod sys { PyIntInfo::from_data(IntInfoData::INFO, vm) } - /// Private function for getting PyConfig.cpu_count + // Private function for getting PyConfig.cpu_count #[pyfunction] fn _get_cpu_count_config(vm: &VirtualMachine) -> i32 { vm.state.config.settings.cpu_count.map_or(-1, |n| n.get()) diff --git a/crates/vm/src/stdlib/time.rs b/crates/vm/src/stdlib/time.rs index 482114f36..4216227df 100644 --- a/crates/vm/src/stdlib/time.rs +++ b/crates/vm/src/stdlib/time.rs @@ -467,7 +467,7 @@ mod decl { } } - /// https://docs.python.org/3/library/time.html?highlight=gmtime#time.gmtime + // https://docs.python.org/3/library/time.html?highlight=gmtime#time.gmtime #[pyfunction] fn gmtime( secs: OptionalOption>, diff --git a/crates/vm/src/stdlib/typevar.rs b/crates/vm/src/stdlib/typevar.rs index ef62a689d..4557b77dc 100644 --- a/crates/vm/src/stdlib/typevar.rs +++ b/crates/vm/src/stdlib/typevar.rs @@ -79,6 +79,7 @@ pub(crate) mod typevar { #[pyclass(name = "TypeVar", module = "typing")] #[derive(Debug, PyPayload)] pub struct TypeVar { + #[pymember(name = "__name__")] name: PyObjectRef, // TODO PyStrRef? bound: PyMutex, evaluate_bound: PyObjectRef, @@ -86,8 +87,11 @@ pub(crate) mod typevar { evaluate_constraints: PyObjectRef, default_value: PyMutex, evaluate_default: PyMutex, + #[pymember(name = "__covariant__")] covariant: bool, + #[pymember(name = "__contravariant__")] contravariant: bool, + #[pymember(name = "__infer_variance__")] infer_variance: bool, } @@ -101,11 +105,6 @@ pub(crate) mod typevar { Err(vm.new_type_error("Cannot subclass an instance of TypeVar")) } - #[pygetset] - fn __name__(&self) -> PyObjectRef { - self.name.clone() - } - #[pygetset] fn __constraints__(&self, vm: &VirtualMachine) -> PyResult { let mut constraints = self.constraints.lock(); @@ -136,21 +135,6 @@ pub(crate) mod typevar { Ok(r) } - #[pygetset] - const fn __covariant__(&self) -> bool { - self.covariant - } - - #[pygetset] - const fn __contravariant__(&self) -> bool { - self.contravariant - } - - #[pygetset] - const fn __infer_variance__(&self) -> bool { - self.infer_variance - } - #[pygetset] fn __default__(&self, vm: &VirtualMachine) -> PyResult { { @@ -455,12 +439,17 @@ pub(crate) mod typevar { #[pyclass(name = "ParamSpec", module = "typing")] #[derive(Debug, PyPayload)] pub struct ParamSpec { + #[pymember(name = "__name__")] name: PyObjectRef, + #[pymember(name = "__bound__")] bound: Option, default_value: PyMutex, evaluate_default: PyMutex, + #[pymember(name = "__covariant__")] covariant: bool, + #[pymember(name = "__contravariant__")] contravariant: bool, + #[pymember(name = "__infer_variance__")] infer_variance: bool, } @@ -474,11 +463,6 @@ pub(crate) mod typevar { Err(vm.new_type_error("Cannot subclass an instance of ParamSpec")) } - #[pygetset] - fn __name__(&self) -> PyObjectRef { - self.name.clone() - } - #[pygetset] fn args(zelf: crate::PyRef, vm: &VirtualMachine) -> PyObjectRef { let self_obj: PyObjectRef = zelf.into(); @@ -497,29 +481,6 @@ pub(crate) mod typevar { psk.into_ref(&vm.ctx).into() } - #[pygetset] - fn __bound__(&self, vm: &VirtualMachine) -> PyObjectRef { - if let Some(bound) = self.bound.clone() { - return bound; - } - vm.ctx.none() - } - - #[pygetset] - const fn __covariant__(&self) -> bool { - self.covariant - } - - #[pygetset] - const fn __contravariant__(&self) -> bool { - self.contravariant - } - - #[pygetset] - const fn __infer_variance__(&self) -> bool { - self.infer_variance - } - #[pygetset] fn __default__(&self, vm: &VirtualMachine) -> PyResult { { @@ -687,7 +648,7 @@ pub(crate) mod typevar { impl Representable for ParamSpec { #[inline(always)] fn repr_str(zelf: &crate::Py, vm: &VirtualMachine) -> PyResult { - let name = zelf.__name__().str_utf8(vm)?; + let name = zelf.name.str_utf8(vm)?; Ok(variance_repr( name.as_str(), zelf.infer_variance, @@ -715,6 +676,7 @@ pub(crate) mod typevar { #[pyclass(name = "TypeVarTuple", module = "typing")] #[derive(Debug, PyPayload)] pub struct TypeVarTuple { + #[pymember(name = "__name__")] name: PyObjectRef, default_value: PyMutex, evaluate_default: PyMutex, @@ -725,11 +687,6 @@ pub(crate) mod typevar { with(Constructor, Representable, Iterable) )] impl TypeVarTuple { - #[pygetset] - fn __name__(&self) -> PyObjectRef { - self.name.clone() - } - #[pygetset] fn __default__(&self, vm: &VirtualMachine) -> PyResult { { @@ -891,6 +848,7 @@ pub(crate) mod typevar { #[pyclass(name = "ParamSpecArgs", module = "typing")] #[derive(Debug, PyPayload)] pub struct ParamSpecArgs { + #[pymember] __origin__: PyObjectRef, } @@ -900,11 +858,6 @@ pub(crate) mod typevar { fn __mro_entries__(&self, _object: PyObjectRef, vm: &VirtualMachine) -> PyResult { Err(vm.new_type_error("Cannot subclass an instance of ParamSpecArgs")) } - - #[pygetset] - fn __origin__(&self) -> PyObjectRef { - self.__origin__.clone() - } } impl Constructor for ParamSpecArgs { @@ -922,7 +875,7 @@ pub(crate) mod typevar { // A ParamSpec origin is named; anything else is shown by its repr, // which carries the recursion guard a Rust `{:?}` walk does not. if let Some(param_spec) = zelf.__origin__.downcast_ref::() { - return Ok(format!("{}.args", param_spec.__name__().str_utf8(vm)?)); + return Ok(format!("{}.args", param_spec.name.str_utf8(vm)?)); } Ok(format!("{}.args", zelf.__origin__.repr(vm)?)) } @@ -955,6 +908,7 @@ pub(crate) mod typevar { #[pyclass(name = "ParamSpecKwargs", module = "typing")] #[derive(Debug, PyPayload)] pub struct ParamSpecKwargs { + #[pymember] __origin__: PyObjectRef, } @@ -964,11 +918,6 @@ pub(crate) mod typevar { fn __mro_entries__(&self, _object: PyObjectRef, vm: &VirtualMachine) -> PyResult { Err(vm.new_type_error("Cannot subclass an instance of ParamSpecKwargs")) } - - #[pygetset] - fn __origin__(&self) -> PyObjectRef { - self.__origin__.clone() - } } impl Constructor for ParamSpecKwargs { @@ -986,7 +935,7 @@ pub(crate) mod typevar { // A ParamSpec origin is named; anything else is shown by its repr, // which carries the recursion guard a Rust `{:?}` walk does not. if let Some(param_spec) = zelf.__origin__.downcast_ref::() { - return Ok(format!("{}.kwargs", param_spec.__name__().str_utf8(vm)?)); + return Ok(format!("{}.kwargs", param_spec.name.str_utf8(vm)?)); } Ok(format!("{}.kwargs", zelf.__origin__.repr(vm)?)) } diff --git a/crates/vm/src/types/slot.rs b/crates/vm/src/types/slot.rs index d187c93ff..1235ef8bf 100644 --- a/crates/vm/src/types/slot.rs +++ b/crates/vm/src/types/slot.rs @@ -599,7 +599,7 @@ pub(crate) fn hackcheck_setattro( obj_cls.slot_name() ))); } - base = b.base.deref().map(|cls| cls.to_owned()); + base = b.base.load_owned(); } Ok(()) } diff --git a/crates/vm/src/vm/context.rs b/crates/vm/src/vm/context.rs index 21c29a846..93917ef30 100644 --- a/crates/vm/src/vm/context.rs +++ b/crates/vm/src/vm/context.rs @@ -1,5 +1,5 @@ use crate::{ - AsObject, PyObject, PyResult, VirtualMachine, + AsObject, PyObject, builtins::{ PyByteArray, PyBytes, PyCapsule, PyComplex, PyDict, PyDictRef, PyEllipsis, PyFloat, PyFrozenSet, PyInt, PyIntRef, PyList, PyListRef, PyNone, PyNotImplemented, PyStr, @@ -7,8 +7,8 @@ use crate::{ bool_::PyBool, code::{self, PyCode}, descriptor::{ - MemberAccess, MemberKind, MemberSetterFunc, PY_READONLY, PyDescriptorOwned, - PyMemberDef, PyMemberDescriptor, + MemberAccess, MemberKind, PY_READONLY, PyDescriptorOwned, PyMemberDef, + PyMemberDescriptor, }, getset::PyGetSet, object, pystr, @@ -661,12 +661,11 @@ impl Context { &self, name: &str, kind: MemberKind, - getter: fn(&VirtualMachine, PyObjectRef) -> PyResult, - setter: MemberSetterFunc, + offset: isize, + flags: i32, class: &'static Py, doc: Option<&str>, ) -> PyRef { - let flags = if setter.is_none() { PY_READONLY } else { 0 }; let member_descriptor = PyMemberDescriptor { common: PyDescriptorOwned { typ: class.to_owned(), @@ -676,14 +675,11 @@ impl Context { member: PyMemberDef { name: name.to_owned(), kind, - offset: 0, + offset, flags, doc: doc.map(str::to_owned), }, - access: MemberAccess::Func { - get: getter, - set: setter, - }, + access: MemberAccess::Offset, }; member_descriptor.into_ref(self) } @@ -721,7 +717,7 @@ impl Context { where F: IntoPyGetterFunc, { - let getset = PyGetSet::new(name, class).with_get(f); + let getset = PyGetSet::new(name, class, self).with_get(f); PyRef::new_ref(getset, self.types.getset_type.to_owned(), None) } @@ -736,7 +732,7 @@ impl Context { G: IntoPyGetterFunc, S: IntoPySetterFunc, { - let getset = PyGetSet::new(name, class).with_get(g).with_set(s); + let getset = PyGetSet::new(name, class, self).with_get(g).with_set(s); PyRef::new_ref(getset, self.types.getset_type.to_owned(), None) } @@ -752,7 +748,7 @@ impl Context { G: IntoPyGetterFunc, S: IntoPySetterFunc, { - let getset = PyGetSet::new(name, class).with_get(g).with_set(s); + let getset = PyGetSet::new(name, class, self).with_get(g).with_set(s); PyRef::new_ref(getset, self.types.getset_type.to_owned(), None) } diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index fb03e0ce6..f95690616 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -1627,7 +1627,9 @@ impl VirtualMachine { // Create a function object for module code, similar to PyEval_EvalCode let mut func = PyFunction::new(code, scope.globals.clone(), self)?; - func.closure = closure; + if let Some(closure) = closure { + func.closure = Some(closure); + } let func = func.into_ref(&self.ctx); func.invoke_with_locals(FuncArgs::default(), scope.locals, self) } diff --git a/crates/vm/src/vm/thread.rs b/crates/vm/src/vm/thread.rs index 527c99366..7e6b1a5b6 100644 --- a/crates/vm/src/vm/thread.rs +++ b/crates/vm/src/vm/thread.rs @@ -1110,7 +1110,7 @@ pub fn get_all_current_exceptions(vm: &VirtualMachine) -> Vec<(u64, Option, name: PyStrRef) -> PyBaseExceptionRef { - let name_error_type = self.ctx.exceptions.name_error.to_owned(); - let name_error = self.new_exception_msg(name_error_type, msg.into()); + let name_error = self + .new_payload_exception::( + self.ctx.exceptions.name_error.to_owned(), + vec![self.ctx.new_str(msg.into()).into()].into(), + ) + .expect("NameError construction from internal args is infallible"); set_attrs!( name_error.as_object(), self, unwrap, "name" => name, ); - name_error + name_error.upcast() } /// The error a call that passed the wrong number of arguments gets. The @@ -961,7 +966,13 @@ impl VirtualMachine { _ => false, }; - let syntax_error = self.new_exception_msg(syntax_error_type, msg.into()); + let syntax_error: PyBaseExceptionRef = self + .new_payload_exception::( + syntax_error_type, + vec![self.ctx.new_str(msg).into()].into(), + ) + .expect("SyntaxError construction from internal args is infallible") + .upcast(); let (lineno_raw, offset_raw) = error.python_location(); let lineno = self.ctx.new_int(lineno_raw); @@ -1043,14 +1054,18 @@ impl VirtualMachine { msg: impl Into, name: impl Into, ) -> PyBaseExceptionRef { - let import_error = self.ctx.exceptions.import_error.to_owned(); - let exc = self.new_exception_msg(import_error, msg.into()); + let exc = self + .new_payload_exception::( + self.ctx.exceptions.import_error.to_owned(), + vec![self.ctx.new_str(msg.into()).into()].into(), + ) + .expect("ImportError construction from internal args is infallible"); set_attrs!( exc.as_object(), self, unwrap, "name" => name.into(), ); - exc + exc.upcast() } pub fn new_system_exit(&self, args: FuncArgs) -> PyBaseExceptionRef { @@ -1138,7 +1153,15 @@ impl VirtualMachine { define_exception_fn!(fn new_lookup_error, lookup_error, LookupError); define_exception_fn!(fn new_eof_error, eof_error, EOFError); - define_exception_fn!(fn new_attribute_error, attribute_error, AttributeError); + pub fn new_attribute_error(&self, msg: impl Into) -> PyBaseExceptionRef { + self.new_payload_exception::( + self.ctx.exceptions.attribute_error.to_owned(), + vec![self.ctx.new_str(msg.into()).into()].into(), + ) + .expect("AttributeError construction from internal args is infallible") + .upcast() + } + define_exception_fn!(fn new_type_error, type_error, TypeError); define_exception_fn!(fn new_system_error, system_error, SystemError); @@ -1158,7 +1181,14 @@ impl VirtualMachine { define_exception_fn!(fn new_python_finalization_error, python_finalization_error, PythonFinalizationError); define_exception_fn!(fn new_memory_error, memory_error, MemoryError); define_exception_fn!(fn new_assertion_error, assertion_error, AssertionError); - define_exception_fn!(fn new_unbound_local_error, unbound_local_error, UnboundLocalError); + pub fn new_unbound_local_error(&self, msg: impl Into) -> PyBaseExceptionRef { + self.new_payload_exception::( + self.ctx.exceptions.unbound_local_error.to_owned(), + vec![self.ctx.new_str(msg.into()).into()].into(), + ) + .expect("UnboundLocalError construction from internal args is infallible") + .upcast() + } } #[cfg(feature = "parser")]