Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bf25f77
Address builtin members by field offset
youknowone Sep 25, 2026
6e359ec
Address member review findings
youknowone Sep 25, 2026
77354be
Declare members on payload fields
youknowone Sep 25, 2026
73ab3c6
Split nullable and non-null atomic cells
youknowone Sep 25, 2026
1643d90
Drop the unused cold trace_lines flag
youknowone Sep 25, 2026
6343798
Compute payload offsets from the merged Py header
youknowone Sep 25, 2026
a7107ae
Expose descriptor names and owner types as members
youknowone Sep 26, 2026
1e11d57
Expose remaining builtin tp_members as members
youknowone Sep 26, 2026
a7fed73
Spell a docless member as doc = false
youknowone Sep 26, 2026
45fca72
Make pymember readonly by default
youknowone Sep 26, 2026
2c6228c
Check the receiver of method descriptors
youknowone Sep 26, 2026
1a57108
Expose exception attributes as members
youknowone Sep 26, 2026
149922f
Avoid same-type casts in unsigned member conversion
youknowone Sep 26, 2026
e4897b1
Prefer an explicit pymember doc over the doc table
youknowone Sep 26, 2026
9413c06
Infer pymember kinds from field types
youknowone Sep 26, 2026
9d33366
Read nullable atomic cells only through load_owned
youknowone Sep 26, 2026
eaeb57a
Inherit static type slots from the whole MRO
youknowone Sep 26, 2026
947e203
Let Rust doc comments win over stored docstrings
youknowone Sep 27, 2026
0c40a56
Adapt members to upstream changes
youknowone Sep 27, 2026
cb242d6
Drop docstring text left as comments and inline downcast_cell
youknowone Sep 27, 2026
72e8f8d
Read sqlite row factories through load_owned
youknowone Sep 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Infer pymember kinds from field types
MemberLayout gives the member kind of each accepted field type, and the
macro reads it from the field or path subfield, so type = "bool" and
the other numeric spellings are gone. type only selects object_ex.
Writable doubles use a new AtomicF64 cell instead of AtomicU64 bits.
MemberLayout and MemberCell replace the per-kind marker traits.

Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5
  • Loading branch information
youknowone committed Sep 27, 2026
commit 9413c06c345b225113cfc1fee2ccc52d73d22120
27 changes: 27 additions & 0 deletions crates/common/src/atomic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,33 @@ impl<T> PyAtomicScalar for *mut T {
type Radium = atomic_ty!(*mut T, AtomicPtr<T>);
}

/// 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<T> {
inner: PyAtomic<*mut T>,
}
Expand Down
102 changes: 66 additions & 36 deletions crates/derive-impl/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1819,14 +1819,22 @@ impl MemberItemMeta {
fn member_kind(&self) -> Result<Option<String>> {
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" | "object_ex" | "bool" | "double" | "int" | "uint" | "py_ssize_t" => {}
"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 => {
let span = self
.inner()
.meta_map
.get("type")
.map_or_else(|| self.inner().meta_ident.span(), |(_, meta)| meta.span());
return Err(syn::Error::new(
span,
format!("unknown member type '{other}'"),
Expand Down Expand Up @@ -1984,40 +1992,60 @@ fn cfg_not_all(cfgs: &[Attribute]) -> TokenStream {
quote!(#[cfg(not(all(#(#preds),*)))] )
}

fn member_kind_tokens(kind: Option<&str>, span: Span) -> Result<TokenStream> {
let name = match kind {
None | Some("object") => "Object",
Some("object_ex") => "ObjectEx",
Some("bool") => "Bool",
Some("double") => "Double",
Some("int") => "Int",
Some("uint") => "Uint",
Some("py_ssize_t") => "PySsizeT",
fn member_kind_tokens(
kind: Option<&str>,
field_tokens: Option<&TokenStream>,
span: Span,
) -> Result<TokenStream> {
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}'"),
));
}
};
let ident = Ident::new(name, span);
Ok(quote!(::rustpython_vm::builtins::descriptor::MemberKind::#ident))
Ok(kind)
}

fn member_layout_tokens(kind: Option<&str>, readonly: bool) -> TokenStream {
let name = match (kind, readonly) {
(Some("bool"), false) => "BoolCell",
(Some("bool"), true) => "BoolMember",
(Some("double"), false) => "DoubleCell",
(Some("double"), true) => "DoubleMember",
(Some("int"), false) => "IntCell",
(Some("int"), true) => "IntMember",
(Some("uint"), false) => "UintCell",
(Some("uint"), true) => "UintMember",
(Some("py_ssize_t"), false) => "PySsizeCell",
(Some("py_ssize_t"), true) => "PySsizeMember",
(_, false) => "ObjectCell",
(_, true) => "ObjectMember",
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)
Expand Down Expand Up @@ -2097,7 +2125,7 @@ fn build_member(
"#[pymember(path = ...)] is only valid on a field",
));
}
let (offset, check) = if let Some((field, index)) = 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(),
Expand All @@ -2111,7 +2139,7 @@ fn build_member(
::rustpython_vm::object::payload_offset::<Self>() as isize
+ ::core::mem::offset_of!(Self, #field_tokens) as isize
};
let layout = member_layout_tokens(kind.as_deref(), readonly);
let layout = member_layout_tokens(readonly);
let check = quote_spanned! { attr.span() =>
let _ = |payload: *const Self| {
fn assert_member_field<T: #layout>(_: *const T) {}
Expand All @@ -2121,15 +2149,17 @@ fn build_member(
assert_member_field(field);
};
};
(offset, check)
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`",
));
};
(offset_expr, quote!())
let kind_tokens = member_kind_tokens(kind.as_deref(), None, attr.span())?;
(offset_expr, quote!(), kind_tokens)
};
let doc = match meta.doc()? {
// An explicit string is the docstring. The attribute-doc table is not
Expand All @@ -2141,7 +2171,7 @@ fn build_member(
Ok(BuiltMember {
name,
cfgs: cfgs.to_vec(),
kind: member_kind_tokens(kind.as_deref(), attr.span())?,
kind: kind_tokens,
offset,
flags: member_flag_tokens(readonly, audit_read),
doc,
Expand Down
17 changes: 11 additions & 6 deletions crates/derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,17 +212,22 @@ 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 field type is checked against the member kind.
/// `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<T>`, `Option` of those, `PyAtomicRef<PyObject>`,
/// `PyAtomicRef<Option<PyObject>>`, `PyAtomicRef<Option<T>>`,
/// `&'static Py<T>`, `&'static PyStrInterned`).
///
/// - `type`: `"object"` (default), `"object_ex"`, `"bool"`, `"double"`,
/// `"int"` (`i32`), `"uint"` (`u32`), or `"py_ssize_t"` (`isize`, `Py_ssize_t`).
/// - `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<PyObject>` (never null) or
/// `PyAtomicRef<Option<PyObject>>` (nullable), a writable bool must be
/// `AtomicBool`, a writable int must be `AtomicI32`, a writable uint must be
/// `AtomicU32`, a writable double must be an atomic 64-bit cell
/// (`AtomicU64`) holding the `f64` bits, and a writable py_ssize_t must be
/// `AtomicIsize`.
/// `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"`).
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/builtins/asyncgenerator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fn warn_unawaited_asyncgen_method(ag: &Py<PyAsyncGen>, method: &str, vm: &Virtua
#[derive(Debug)]
pub struct PyAsyncGen {
inner: Coro,
#[pymember(name = "ag_running", type = "bool")]
#[pymember(name = "ag_running")]
running_async: AtomicBool,
// whether hooks have been initialized
ag_hooks_inited: AtomicCell<bool>,
Expand Down
8 changes: 4 additions & 4 deletions crates/vm/src/builtins/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,10 +471,10 @@ pub struct CoMonitoringData {

#[pyclass(module = false, name = "code")]
pub struct PyCode {
#[pymember(name = "co_argcount", type = "uint", path = "arg_count")]
#[pymember(name = "co_posonlyargcount", type = "uint", path = "posonlyarg_count")]
#[pymember(name = "co_kwonlyargcount", type = "uint", path = "kwonlyarg_count")]
#[pymember(name = "co_stacksize", type = "uint", path = "max_stackdepth")]
#[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,
Expand Down
4 changes: 2 additions & 2 deletions crates/vm/src/builtins/complex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ use rustpython_common::hash;
#[pyclass(module = false, name = "complex")]
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct PyComplex {
#[pymember(type = "double", name = "real", path = "re")]
#[pymember(type = "double", name = "imag", path = "im")]
#[pymember(name = "real", path = "re")]
#[pymember(name = "imag", path = "im")]
value: Complex64,
}

Expand Down
Loading