From 5ad536e030d0c3b9fe304a8cf674a4a1aee8df9c Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 5 Dec 2025 14:55:35 +0900 Subject: [PATCH 1/2] Fix local time --- crates/vm/src/stdlib/time.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/vm/src/stdlib/time.rs b/crates/vm/src/stdlib/time.rs index fb64e28b906..2e8743b1c2d 100644 --- a/crates/vm/src/stdlib/time.rs +++ b/crates/vm/src/stdlib/time.rs @@ -280,7 +280,9 @@ mod decl { /// Construct a localtime from the optional seconds, or get the current local time. fn naive_or_local(self, vm: &VirtualMachine) -> PyResult { Ok(match self { - Self::Present(secs) => pyobj_to_date_time(secs, vm)?.naive_utc(), + Self::Present(secs) => pyobj_to_date_time(secs, vm)? + .with_timezone(&chrono::Local) + .naive_local(), Self::Missing => chrono::offset::Local::now().naive_local(), }) } @@ -323,7 +325,12 @@ mod decl { #[pyfunction] fn mktime(t: PyStructTime, vm: &VirtualMachine) -> PyResult { let datetime = t.to_date_time(vm)?; - let seconds_since_epoch = datetime.and_utc().timestamp() as f64; + // mktime interprets struct_time as local time + let local_dt = chrono::Local + .from_local_datetime(&datetime) + .single() + .ok_or_else(|| vm.new_overflow_error("mktime argument out of range"))?; + let seconds_since_epoch = local_dt.timestamp() as f64; Ok(seconds_since_epoch) } From a5aa4094ef6657565fd127c99a06d7c58163dbcf Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 3 Dec 2025 21:38:33 +0900 Subject: [PATCH 2/2] PyStructSequence --- Lib/test/datetimetester.py | 2 - Lib/test/test_logging.py | 2 - Lib/test/test_structseq.py | 4 - crates/derive-impl/src/lib.rs | 8 +- crates/derive-impl/src/pymodule.rs | 107 +++++- crates/derive-impl/src/pystructseq.rs | 479 +++++++++++++++++++++++--- crates/derive/src/lib.rs | 57 ++- crates/stdlib/src/grp.rs | 30 +- crates/stdlib/src/resource.rs | 18 +- crates/vm/src/stdlib/nt.rs | 4 +- crates/vm/src/stdlib/os.rs | 89 +++-- crates/vm/src/stdlib/posix.rs | 8 +- crates/vm/src/stdlib/pwd.rs | 26 +- crates/vm/src/stdlib/sys.rs | 151 +++++--- crates/vm/src/stdlib/time.rs | 80 +++-- crates/vm/src/types/mod.rs | 2 +- crates/vm/src/types/structseq.rs | 344 ++++++++++++++++-- crates/vm/src/vm/context.rs | 3 + crates/vm/src/vm/mod.rs | 2 +- 19 files changed, 1136 insertions(+), 280 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 7c6c8d7dd2c..018b0c4d03a 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2943,8 +2943,6 @@ def newmeth(self, start): self.assertEqual(dt2.newmeth(-7), dt1.year + dt1.month + dt1.second - 7) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_subclass_alternate_constructors_datetime(self): # Test that alternate constructors call the constructor class DateTimeSubclass(self.theclass): diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 84a659ebe4b..3ec6c3c3a1d 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -4748,7 +4748,6 @@ def test_defaults_parameter(self): def test_invalid_style(self): self.assertRaises(ValueError, logging.Formatter, None, None, 'x') - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'struct_time' object has no attribute 'tm_gmtoff' def test_time(self): r = self.get_record() dt = datetime.datetime(1993, 4, 21, 8, 3, 0, 0, utc) @@ -4763,7 +4762,6 @@ def test_time(self): f.format(r) self.assertEqual(r.asctime, '1993-04-21 08:03:00,123') - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'struct_time' object has no attribute 'tm_gmtoff' def test_default_msec_format_none(self): class NoMsecFormatter(logging.Formatter): default_msec_format = None diff --git a/Lib/test/test_structseq.py b/Lib/test/test_structseq.py index 41095f63ad6..a9fe193028e 100644 --- a/Lib/test/test_structseq.py +++ b/Lib/test/test_structseq.py @@ -75,8 +75,6 @@ def test_cmp(self): self.assertTrue(t1 >= t2) self.assertTrue(not (t1 != t2)) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_fields(self): t = time.gmtime() self.assertEqual(len(t), t.n_sequence_fields) @@ -129,8 +127,6 @@ def test_match_args(self): 'tm_sec', 'tm_wday', 'tm_yday', 'tm_isdst') self.assertEqual(time.struct_time.__match_args__, expected_args) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_match_args_with_unnamed_fields(self): expected_args = ('st_mode', 'st_ino', 'st_dev', 'st_nlink', 'st_uid', 'st_gid', 'st_size') diff --git a/crates/derive-impl/src/lib.rs b/crates/derive-impl/src/lib.rs index 786c77e3212..51bb0af406f 100644 --- a/crates/derive-impl/src/lib.rs +++ b/crates/derive-impl/src/lib.rs @@ -58,12 +58,12 @@ pub fn pymodule(attr: PunctuatedNestedMeta, item: Item) -> TokenStream { result_to_tokens(pymodule::impl_pymodule(attr, item)) } -pub fn pystruct_sequence(input: DeriveInput) -> TokenStream { - result_to_tokens(pystructseq::impl_pystruct_sequence(input)) +pub fn pystruct_sequence(attr: PunctuatedNestedMeta, item: Item) -> TokenStream { + result_to_tokens(pystructseq::impl_pystruct_sequence(attr, item)) } -pub fn pystruct_sequence_try_from_object(input: DeriveInput) -> TokenStream { - result_to_tokens(pystructseq::impl_pystruct_sequence_try_from_object(input)) +pub fn pystruct_sequence_data(attr: PunctuatedNestedMeta, item: Item) -> TokenStream { + result_to_tokens(pystructseq::impl_pystruct_sequence_data(attr, item)) } pub fn py_compile(input: TokenStream, compiler: &dyn Compiler) -> TokenStream { diff --git a/crates/derive-impl/src/pymodule.rs b/crates/derive-impl/src/pymodule.rs index 9db7128b3a1..2d5ff7cb0c2 100644 --- a/crates/derive-impl/src/pymodule.rs +++ b/crates/derive-impl/src/pymodule.rs @@ -1,4 +1,5 @@ use crate::error::Diagnostic; +use crate::pystructseq::PyStructSequenceMeta; use crate::util::{ ALL_ALLOWED_NAMES, AttrItemMeta, AttributeExt, ClassItemMeta, ContentItem, ContentItemInner, ErrorVec, ItemMeta, ItemNursery, ModuleItemMeta, SimpleItemMeta, format_doc, iter_use_idents, @@ -18,6 +19,7 @@ enum AttrName { Attr, Class, Exception, + StructSequence, } impl std::fmt::Display for AttrName { @@ -27,6 +29,7 @@ impl std::fmt::Display for AttrName { Self::Attr => "pyattr", Self::Class => "pyclass", Self::Exception => "pyexception", + Self::StructSequence => "pystruct_sequence", }; s.fmt(f) } @@ -41,6 +44,7 @@ impl FromStr for AttrName { "pyattr" => Self::Attr, "pyclass" => Self::Class, "pyexception" => Self::Exception, + "pystruct_sequence" => Self::StructSequence, s => { return Err(s.to_owned()); } @@ -235,6 +239,10 @@ fn module_item_new( inner: ContentItemInner { index, attr_name }, py_attrs, }), + AttrName::StructSequence => Box::new(StructSequenceItem { + inner: ContentItemInner { index, attr_name }, + py_attrs, + }), } } @@ -301,13 +309,16 @@ where result.push(item_new(i, attr_name, Vec::new())); } else { match attr_name { - AttrName::Class | AttrName::Function | AttrName::Exception => { + AttrName::Class + | AttrName::Function + | AttrName::Exception + | AttrName::StructSequence => { result.push(item_new(i, attr_name, py_attrs.clone())); } _ => { bail_span!( attr, - "#[pyclass], #[pyfunction], or #[pyexception] can follow #[pyattr]", + "#[pyclass], #[pyfunction], #[pyexception], or #[pystruct_sequence] can follow #[pyattr]", ) } } @@ -402,6 +413,12 @@ struct AttributeItem { py_attrs: Vec, } +/// #[pystruct_sequence] +struct StructSequenceItem { + inner: ContentItemInner, + py_attrs: Vec, +} + impl ContentItem for FunctionItem { type AttrName = AttrName; @@ -426,6 +443,14 @@ impl ContentItem for AttributeItem { } } +impl ContentItem for StructSequenceItem { + type AttrName = AttrName; + + fn inner(&self) -> &ContentItemInner { + &self.inner + } +} + struct ModuleItemArgs<'a> { item: &'a mut Item, attrs: &'a mut Vec, @@ -602,6 +627,84 @@ impl ModuleItem for ClassItem { } } +impl ModuleItem for StructSequenceItem { + fn gen_module_item(&self, args: ModuleItemArgs<'_>) -> Result<()> { + // Get the struct identifier (this IS the Python type, e.g., PyStructTime) + let pytype_ident = match args.item { + Item::Struct(s) => s.ident.clone(), + other => bail_span!(other, "#[pystruct_sequence] can only be on a struct"), + }; + + // Parse the #[pystruct_sequence(name = "...", module = "...", no_attr)] attribute + let structseq_attr = &args.attrs[self.inner.index]; + let meta = PyStructSequenceMeta::from_attr(pytype_ident.clone(), structseq_attr)?; + + let class_name = meta.class_name()?.ok_or_else(|| { + syn::Error::new_spanned( + structseq_attr, + "#[pystruct_sequence] requires name parameter", + ) + })?; + let module_name = meta.module()?.unwrap_or_else(|| args.context.name.clone()); + let no_attr = meta.no_attr()?; + + // Generate the class creation code + let class_new = quote_spanned!(pytype_ident.span() => + let new_class = <#pytype_ident as ::rustpython_vm::class::PyClassImpl>::make_class(ctx); + new_class.set_attr(rustpython_vm::identifier!(ctx, __module__), vm.new_pyobj(#module_name)); + ); + + // Handle py_attrs for custom names, or use class_name as default + let mut py_names = Vec::new(); + for attr_index in self.py_attrs.iter().rev() { + let attr_attr = args.attrs.remove(*attr_index); + let item_meta = SimpleItemMeta::from_attr(pytype_ident.clone(), &attr_attr)?; + let py_name = item_meta + .optional_name() + .unwrap_or_else(|| class_name.clone()); + py_names.push(py_name); + } + + // Require explicit #[pyattr] or no_attr, like #[pyclass] + if self.py_attrs.is_empty() && !no_attr { + bail_span!( + pytype_ident, + "#[pystruct_sequence] requires #[pyattr] to be a module attribute. \ + To keep it free type, try #[pystruct_sequence(..., no_attr)]" + ) + } + + let set_attr = match py_names.len() { + 0 => quote! { + let _ = new_class; // suppress warning + }, + 1 => { + let py_name = &py_names[0]; + quote! { + vm.__module_set_attr(&module, vm.ctx.intern_str(#py_name), new_class).unwrap(); + } + } + _ => quote! { + for name in [#(#py_names,)*] { + vm.__module_set_attr(&module, vm.ctx.intern_str(name), new_class.clone()).unwrap(); + } + }, + }; + + args.context.attribute_items.add_item( + pytype_ident.clone(), + py_names, + args.cfgs.to_vec(), + quote_spanned! { pytype_ident.span() => + #class_new + #set_attr + }, + 0, + )?; + Ok(()) + } +} + impl ModuleItem for AttributeItem { fn gen_module_item(&self, args: ModuleItemArgs<'_>) -> Result<()> { let cfgs = args.cfgs.to_vec(); diff --git a/crates/derive-impl/src/pystructseq.rs b/crates/derive-impl/src/pystructseq.rs index 89dee8da106..c43673fe975 100644 --- a/crates/derive-impl/src/pystructseq.rs +++ b/crates/derive-impl/src/pystructseq.rs @@ -1,34 +1,90 @@ +use crate::util::{ItemMeta, ItemMetaInner}; use proc_macro2::TokenStream; -use quote::quote; -use syn::{DeriveInput, Ident, Result}; +use quote::{format_ident, quote}; +use syn::{DeriveInput, Ident, Item, Result}; use syn_ext::ext::{AttributeExt, GetIdent}; -use syn_ext::types::Meta; +use syn_ext::types::{Meta, PunctuatedNestedMeta}; -// returning a pair of not-skipped and skipped field names -fn field_names(input: &mut DeriveInput) -> Result<(Vec, Vec)> { +// #[pystruct_sequence_data] - For Data structs + +/// Field kind for struct sequence +#[derive(Clone, Copy, PartialEq, Eq)] +enum FieldKind { + /// Named visible field (has getter, shown in repr) + Named, + /// Unnamed visible field (index-only, no getter) + Unnamed, + /// Hidden/skipped field (stored in tuple, but hidden from repr/len/index) + Skipped, +} + +/// Parsed field with its kind +struct ParsedField { + ident: Ident, + kind: FieldKind, +} + +/// Parsed field info from struct +struct FieldInfo { + /// All fields in order with their kinds + fields: Vec, +} + +impl FieldInfo { + fn named_fields(&self) -> Vec { + self.fields + .iter() + .filter(|f| f.kind == FieldKind::Named) + .map(|f| f.ident.clone()) + .collect() + } + + fn visible_fields(&self) -> Vec { + self.fields + .iter() + .filter(|f| f.kind != FieldKind::Skipped) + .map(|f| f.ident.clone()) + .collect() + } + + fn skipped_fields(&self) -> Vec { + self.fields + .iter() + .filter(|f| f.kind == FieldKind::Skipped) + .map(|f| f.ident.clone()) + .collect() + } + + fn n_unnamed_fields(&self) -> usize { + self.fields + .iter() + .filter(|f| f.kind == FieldKind::Unnamed) + .count() + } +} + +/// Parse field info from struct +fn parse_fields(input: &mut DeriveInput) -> Result { let syn::Data::Struct(struc) = &mut input.data else { - bail_span!( - input, - "#[pystruct_sequence] can only be on a struct declaration" - ) + bail_span!(input, "#[pystruct_sequence_data] can only be on a struct") }; let syn::Fields::Named(fields) = &mut struc.fields else { bail_span!( input, - "#[pystruct_sequence] can only be on a struct with named fields" + "#[pystruct_sequence_data] can only be on a struct with named fields" ); }; - let mut not_skipped = Vec::with_capacity(fields.named.len()); - let mut skipped = Vec::with_capacity(fields.named.len()); + let mut parsed_fields = Vec::with_capacity(fields.named.len()); + for field in &mut fields.named { let mut skip = false; - // Collect all attributes with pystruct and their indices + let mut unnamed = false; let mut attrs_to_remove = Vec::new(); for (i, attr) in field.attrs.iter().enumerate() { - if !attr.path().is_ident("pystruct") { + if !attr.path().is_ident("pystruct_sequence") { continue; } @@ -37,7 +93,7 @@ fn field_names(input: &mut DeriveInput) -> Result<(Vec, Vec)> { }; let Meta::List(l) = meta else { - bail_span!(input, "Only #[pystruct(...)] form is allowed"); + bail_span!(input, "Only #[pystruct_sequence(...)] form is allowed"); }; let idents: Vec<_> = l @@ -47,15 +103,16 @@ fn field_names(input: &mut DeriveInput) -> Result<(Vec, Vec)> { .cloned() .collect(); - // Follow #[serde(skip)] convention. - // Consider to add skip_serializing and skip_deserializing if required. for ident in idents { match ident.to_string().as_str() { "skip" => { skip = true; } + "unnamed" => { + unnamed = true; + } _ => { - bail_span!(ident, "Unknown item for #[pystruct(...)]") + bail_span!(ident, "Unknown item for #[pystruct_sequence(...)]") } } } @@ -63,67 +120,381 @@ fn field_names(input: &mut DeriveInput) -> Result<(Vec, Vec)> { attrs_to_remove.push(i); } - // Remove attributes in reverse order to maintain valid indices - attrs_to_remove.sort_unstable_by(|a, b| b.cmp(a)); // Sort in descending order + // Remove attributes in reverse order + attrs_to_remove.sort_unstable_by(|a, b| b.cmp(a)); for index in attrs_to_remove { field.attrs.remove(index); } + let ident = field.ident.clone().unwrap(); - if skip { - skipped.push(ident.clone()); + let kind = if skip { + FieldKind::Skipped + } else if unnamed { + FieldKind::Unnamed } else { - not_skipped.push(ident.clone()); - } + FieldKind::Named + }; + + parsed_fields.push(ParsedField { ident, kind }); } - Ok((not_skipped, skipped)) + Ok(FieldInfo { + fields: parsed_fields, + }) } -pub(crate) fn impl_pystruct_sequence(mut input: DeriveInput) -> Result { - let (not_skipped_fields, skipped_fields) = field_names(&mut input)?; - let ty = &input.ident; - let ret = quote! { - impl ::rustpython_vm::types::PyStructSequence for #ty { - const REQUIRED_FIELD_NAMES: &'static [&'static str] = &[#(stringify!(#not_skipped_fields),)*]; +/// Check if `try_from_object` is present in attribute arguments +fn has_try_from_object(attr: &PunctuatedNestedMeta) -> bool { + attr.iter().any(|nested| { + nested + .get_ident() + .is_some_and(|ident| ident == "try_from_object") + }) +} + +/// Attribute macro for Data structs: #[pystruct_sequence_data(...)] +/// +/// Generates: +/// - `REQUIRED_FIELD_NAMES` constant (named visible fields) +/// - `OPTIONAL_FIELD_NAMES` constant (hidden/skipped fields) +/// - `UNNAMED_FIELDS_LEN` constant +/// - `into_tuple()` method +/// - Field index constants (e.g., `TM_YEAR_INDEX`) +/// +/// Options: +/// - `try_from_object`: Generate `try_from_elements()` method and `TryFromObject` impl +pub(crate) fn impl_pystruct_sequence_data( + attr: PunctuatedNestedMeta, + item: Item, +) -> Result { + let Item::Struct(item_struct) = item else { + bail_span!( + item, + "#[pystruct_sequence_data] can only be applied to structs" + ); + }; + + let try_from_object = has_try_from_object(&attr); + let mut input: DeriveInput = DeriveInput { + attrs: item_struct.attrs.clone(), + vis: item_struct.vis.clone(), + ident: item_struct.ident.clone(), + generics: item_struct.generics.clone(), + data: syn::Data::Struct(syn::DataStruct { + struct_token: item_struct.struct_token, + fields: item_struct.fields.clone(), + semi_token: item_struct.semi_token, + }), + }; + let field_info = parse_fields(&mut input)?; + let data_ident = &input.ident; + + let named_fields = field_info.named_fields(); + let visible_fields = field_info.visible_fields(); + let skipped_fields = field_info.skipped_fields(); + let n_unnamed_fields = field_info.n_unnamed_fields(); + + // Generate field index constants for visible fields + let field_indices: Vec<_> = visible_fields + .iter() + .enumerate() + .map(|(i, field)| { + let const_name = format_ident!("{}_INDEX", field.to_string().to_uppercase()); + quote! { + pub const #const_name: usize = #i; + } + }) + .collect(); + + // Generate TryFromObject impl only when try_from_object=true + let try_from_object_impl = if try_from_object { + let n_required = visible_fields.len(); + quote! { + impl ::rustpython_vm::TryFromObject for #data_ident { + fn try_from_object( + vm: &::rustpython_vm::VirtualMachine, + obj: ::rustpython_vm::PyObjectRef, + ) -> ::rustpython_vm::PyResult { + let seq: Vec<::rustpython_vm::PyObjectRef> = obj.try_into_value(vm)?; + if seq.len() < #n_required { + return Err(vm.new_type_error(format!( + "{} requires at least {} elements", + stringify!(#data_ident), + #n_required + ))); + } + ::try_from_elements(seq, vm) + } + } + } + } else { + quote! {} + }; + + // Generate try_from_elements trait override only when try_from_object=true + let try_from_elements_trait_override = if try_from_object { + quote! { + fn try_from_elements( + elements: Vec<::rustpython_vm::PyObjectRef>, + vm: &::rustpython_vm::VirtualMachine, + ) -> ::rustpython_vm::PyResult { + let mut iter = elements.into_iter(); + Ok(Self { + #(#visible_fields: iter.next().unwrap().clone().try_into_value(vm)?,)* + #(#skipped_fields: match iter.next() { + Some(v) => v.clone().try_into_value(vm)?, + None => vm.ctx.none(), + },)* + }) + } + } + } else { + quote! {} + }; + + let output = quote! { + impl #data_ident { + #(#field_indices)* + } + + // PyStructSequenceData trait impl + impl ::rustpython_vm::types::PyStructSequenceData for #data_ident { + const REQUIRED_FIELD_NAMES: &'static [&'static str] = &[#(stringify!(#named_fields),)*]; const OPTIONAL_FIELD_NAMES: &'static [&'static str] = &[#(stringify!(#skipped_fields),)*]; + const UNNAMED_FIELDS_LEN: usize = #n_unnamed_fields; + fn into_tuple(self, vm: &::rustpython_vm::VirtualMachine) -> ::rustpython_vm::builtins::PyTuple { let items = vec![ #(::rustpython_vm::convert::ToPyObject::to_pyobject( - self.#not_skipped_fields, + self.#visible_fields, + vm, + ),)* + #(::rustpython_vm::convert::ToPyObject::to_pyobject( + self.#skipped_fields, vm, ),)* ]; ::rustpython_vm::builtins::PyTuple::new_unchecked(items.into_boxed_slice()) } + + #try_from_elements_trait_override } - impl ::rustpython_vm::convert::ToPyObject for #ty { - fn to_pyobject(self, vm: &::rustpython_vm::VirtualMachine) -> ::rustpython_vm::PyObjectRef { - ::rustpython_vm::types::PyStructSequence::into_struct_sequence(self, vm).into() + + #try_from_object_impl + }; + + // For attribute macro, we need to output the original struct as well + // But first, strip #[pystruct_sequence] attributes from fields + let mut clean_struct = item_struct.clone(); + if let syn::Fields::Named(ref mut fields) = clean_struct.fields { + for field in &mut fields.named { + field + .attrs + .retain(|attr| !attr.path().is_ident("pystruct_sequence")); + } + } + + Ok(quote! { + #clean_struct + #output + }) +} + +// #[pystruct_sequence(...)] - For Python type structs + +/// Meta parser for #[pystruct_sequence(...)] +pub(crate) struct PyStructSequenceMeta { + inner: ItemMetaInner, +} + +impl ItemMeta for PyStructSequenceMeta { + const ALLOWED_NAMES: &'static [&'static str] = &["name", "module", "data", "no_attr"]; + + fn from_inner(inner: ItemMetaInner) -> Self { + Self { inner } + } + fn inner(&self) -> &ItemMetaInner { + &self.inner + } +} + +impl PyStructSequenceMeta { + pub fn class_name(&self) -> Result> { + const KEY: &str = "name"; + let inner = self.inner(); + if let Some((_, meta)) = inner.meta_map.get(KEY) { + if let Meta::NameValue(syn::MetaNameValue { + value: + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }), + .. + }) = meta + { + return Ok(Some(lit.value())); } + bail_span!( + inner.meta_ident, + "#[pystruct_sequence({KEY}=value)] expects a string value" + ) + } else { + Ok(None) } - }; - Ok(ret) + } + + pub fn module(&self) -> Result> { + const KEY: &str = "module"; + let inner = self.inner(); + if let Some((_, meta)) = inner.meta_map.get(KEY) { + if let Meta::NameValue(syn::MetaNameValue { + value: + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }), + .. + }) = meta + { + return Ok(Some(lit.value())); + } + bail_span!( + inner.meta_ident, + "#[pystruct_sequence({KEY}=value)] expects a string value" + ) + } else { + Ok(None) + } + } + + fn data_type(&self) -> Result { + const KEY: &str = "data"; + let inner = self.inner(); + if let Some((_, meta)) = inner.meta_map.get(KEY) { + if let Meta::NameValue(syn::MetaNameValue { + value: + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }), + .. + }) = meta + { + return Ok(format_ident!("{}", lit.value())); + } + bail_span!( + inner.meta_ident, + "#[pystruct_sequence({KEY}=value)] expects a string value" + ) + } else { + bail_span!( + inner.meta_ident, + "#[pystruct_sequence] requires data parameter (e.g., data = \"DataStructName\")" + ) + } + } + + pub fn no_attr(&self) -> Result { + self.inner()._bool("no_attr") + } } -pub(crate) fn impl_pystruct_sequence_try_from_object( - mut input: DeriveInput, +/// Attribute macro for struct sequences. +/// +/// Usage: +/// ```ignore +/// #[pystruct_sequence_data] +/// struct StructTimeData { ... } +/// +/// #[pystruct_sequence(name = "struct_time", module = "time", data = "StructTimeData")] +/// struct PyStructTime; +/// ``` +pub(crate) fn impl_pystruct_sequence( + attr: PunctuatedNestedMeta, + item: Item, ) -> Result { - let (not_skipped_fields, skipped_fields) = field_names(&mut input)?; - let ty = &input.ident; - let ret = quote! { - impl ::rustpython_vm::TryFromObject for #ty { - fn try_from_object(vm: &::rustpython_vm::VirtualMachine, seq: ::rustpython_vm::PyObjectRef) -> ::rustpython_vm::PyResult { - let seq = Self::try_elements_from(seq, vm)?; - let mut iter = seq.into_iter(); - Ok(Self { - #(#not_skipped_fields: iter.next().unwrap().clone().try_into_value(vm)?,)* - #(#skipped_fields: match iter.next() { - Some(v) => v.clone().try_into_value(vm)?, - None => vm.ctx.none(), - },)* - }) + let Item::Struct(struct_item) = item else { + bail_span!(item, "#[pystruct_sequence] can only be applied to a struct"); + }; + + let ident = struct_item.ident.clone(); + let fake_ident = Ident::new("pystruct_sequence", ident.span()); + let meta = PyStructSequenceMeta::from_nested(ident, fake_ident, attr.into_iter())?; + + let pytype_ident = struct_item.ident.clone(); + let pytype_vis = struct_item.vis.clone(); + let data_ident = meta.data_type()?; + + let class_name = meta.class_name()?.ok_or_else(|| { + syn::Error::new_spanned( + &struct_item.ident, + "#[pystruct_sequence] requires name parameter", + ) + })?; + let module_name = meta.module()?; + + // Module name handling + let module_name_tokens = match &module_name { + Some(m) => quote!(Some(#m)), + None => quote!(None), + }; + + let module_class_name = if let Some(ref m) = module_name { + format!("{}.{}", m, class_name) + } else { + class_name.clone() + }; + + let output = quote! { + // The Python type struct (user-defined, possibly empty) + #pytype_vis struct #pytype_ident; + + // PyClassDef for Python type + impl ::rustpython_vm::class::PyClassDef for #pytype_ident { + const NAME: &'static str = #class_name; + const MODULE_NAME: Option<&'static str> = #module_name_tokens; + const TP_NAME: &'static str = #module_class_name; + const DOC: Option<&'static str> = None; + const BASICSIZE: usize = 0; + const UNHASHABLE: bool = false; + + type Base = ::rustpython_vm::builtins::PyTuple; + } + + // StaticType for Python type + impl ::rustpython_vm::class::StaticType for #pytype_ident { + fn static_cell() -> &'static ::rustpython_vm::common::static_cell::StaticCell<::rustpython_vm::builtins::PyTypeRef> { + ::rustpython_vm::common::static_cell! { + static CELL: ::rustpython_vm::builtins::PyTypeRef; + } + &CELL + } + + fn static_baseclass() -> &'static ::rustpython_vm::Py<::rustpython_vm::builtins::PyType> { + use ::rustpython_vm::class::StaticType; + ::rustpython_vm::builtins::PyTuple::static_type() + } + } + + // MaybeTraverse (empty - no GC fields in empty struct) + impl ::rustpython_vm::object::MaybeTraverse for #pytype_ident { + fn try_traverse(&self, _traverse_fn: &mut ::rustpython_vm::object::TraverseFn<'_>) { + // Empty struct has no fields to traverse + } + } + + // PyStructSequence trait for Python type + impl ::rustpython_vm::types::PyStructSequence for #pytype_ident { + type Data = #data_ident; + } + + // ToPyObject for Data struct - uses PyStructSequence::from_data + impl ::rustpython_vm::convert::ToPyObject for #data_ident { + fn to_pyobject(self, vm: &::rustpython_vm::VirtualMachine) -> ::rustpython_vm::PyObjectRef { + <#pytype_ident as ::rustpython_vm::types::PyStructSequence>::from_data(self, vm).into() } } }; - Ok(ret) + + Ok(output) } diff --git a/crates/derive/src/lib.rs b/crates/derive/src/lib.rs index 6aef58a2aa3..655ad3b4c9e 100644 --- a/crates/derive/src/lib.rs +++ b/crates/derive/src/lib.rs @@ -214,16 +214,57 @@ pub fn pymodule(attr: TokenStream, item: TokenStream) -> TokenStream { derive_impl::pymodule(attr, item).into() } -#[proc_macro_derive(PyStructSequence, attributes(pystruct))] -pub fn pystruct_sequence(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input); - derive_impl::pystruct_sequence(input).into() +/// 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. +/// +/// # Example +/// ```ignore +/// #[pystruct_sequence_data] +/// struct StructTimeData { +/// pub tm_year: PyObjectRef, +/// #[pystruct_sequence(skip)] +/// pub tm_gmtoff: PyObjectRef, +/// } +/// +/// #[pystruct_sequence(name = "struct_time", module = "time", data = "StructTimeData")] +/// struct PyStructTime; +/// ``` +#[proc_macro_attribute] +pub fn pystruct_sequence(attr: TokenStream, item: TokenStream) -> TokenStream { + let attr = parse_macro_input!(attr with Punctuated::parse_terminated); + let item = parse_macro_input!(item); + derive_impl::pystruct_sequence(attr, item).into() } -#[proc_macro_derive(TryIntoPyStructSequence, attributes(pystruct))] -pub fn pystruct_sequence_try_from_object(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input); - derive_impl::pystruct_sequence_try_from_object(input).into() +/// Attribute macro for struct sequence Data structs. +/// +/// Generates field name constants, index constants, and `into_tuple()` method. +/// +/// # Example +/// ```ignore +/// #[pystruct_sequence_data] +/// struct StructTimeData { +/// pub tm_year: PyObjectRef, +/// pub tm_mon: PyObjectRef, +/// #[pystruct_sequence(skip)] // optional field, not included in tuple +/// pub tm_gmtoff: PyObjectRef, +/// } +/// ``` +/// +/// # Options +/// - `try_from_object`: Generate `try_from_elements()` method and `TryFromObject` impl +/// +/// ```ignore +/// #[pystruct_sequence_data(try_from_object)] +/// struct StructTimeData { ... } +/// ``` +#[proc_macro_attribute] +pub fn pystruct_sequence_data(attr: TokenStream, item: TokenStream) -> TokenStream { + let attr = parse_macro_input!(attr with Punctuated::parse_terminated); + let item = parse_macro_input!(item); + derive_impl::pystruct_sequence_data(attr, item).into() } struct Compiler; diff --git a/crates/stdlib/src/grp.rs b/crates/stdlib/src/grp.rs index b640494c13b..4664d5fc575 100644 --- a/crates/stdlib/src/grp.rs +++ b/crates/stdlib/src/grp.rs @@ -13,26 +13,28 @@ mod grp { use nix::unistd; use std::ptr::NonNull; - #[pyattr] - #[pyclass(module = "grp", name = "struct_group", traverse)] - #[derive(PyStructSequence)] - struct Group { - #[pytraverse(skip)] + #[pystruct_sequence_data] + struct GroupData { gr_name: String, - #[pytraverse(skip)] gr_passwd: String, - #[pytraverse(skip)] gr_gid: u32, gr_mem: PyListRef, } + + #[pyattr] + #[pystruct_sequence(name = "struct_group", module = "grp", data = "GroupData")] + struct PyGroup; + #[pyclass(with(PyStructSequence))] - impl Group { + impl PyGroup {} + + impl GroupData { fn from_unistd_group(group: unistd::Group, vm: &VirtualMachine) -> Self { let cstr_lossy = |s: std::ffi::CString| { s.into_string() .unwrap_or_else(|e| e.into_cstring().to_string_lossy().into_owned()) }; - Group { + GroupData { gr_name: group.name, gr_passwd: cstr_lossy(group.passwd), gr_gid: group.gid.as_raw(), @@ -44,7 +46,7 @@ mod grp { } #[pyfunction] - fn getgrgid(gid: PyIntRef, vm: &VirtualMachine) -> PyResult { + fn getgrgid(gid: PyIntRef, vm: &VirtualMachine) -> PyResult { let gr_gid = gid.as_bigint(); let gid = libc::gid_t::try_from(gr_gid) .map(unistd::Gid::from_raw) @@ -61,11 +63,11 @@ mod grp { .into(), ) })?; - Ok(Group::from_unistd_group(group, vm)) + Ok(GroupData::from_unistd_group(group, vm)) } #[pyfunction] - fn getgrnam(name: PyStrRef, vm: &VirtualMachine) -> PyResult { + fn getgrnam(name: PyStrRef, vm: &VirtualMachine) -> PyResult { let gr_name = name.as_str(); if gr_name.contains('\0') { return Err(exceptions::cstring_error(vm)); @@ -78,7 +80,7 @@ mod grp { .into(), ) })?; - Ok(Group::from_unistd_group(group, vm)) + Ok(GroupData::from_unistd_group(group, vm)) } #[pyfunction] @@ -91,7 +93,7 @@ mod grp { unsafe { libc::setgrent() }; while let Some(ptr) = NonNull::new(unsafe { libc::getgrent() }) { let group = unistd::Group::from(unsafe { ptr.as_ref() }); - let group = Group::from_unistd_group(group, vm).to_pyobject(vm); + let group = GroupData::from_unistd_group(group, vm).to_pyobject(vm); list.push(group); } unsafe { libc::endgrent() }; diff --git a/crates/stdlib/src/resource.rs b/crates/stdlib/src/resource.rs index 8bd60cee879..59205a4fa46 100644 --- a/crates/stdlib/src/resource.rs +++ b/crates/stdlib/src/resource.rs @@ -63,10 +63,8 @@ mod resource { #[pyattr] use libc::{RUSAGE_CHILDREN, RUSAGE_SELF}; - #[pyattr] - #[pyclass(name = "struct_rusage")] - #[derive(PyStructSequence)] - struct Rusage { + #[pystruct_sequence_data] + struct RUsageData { ru_utime: f64, ru_stime: f64, ru_maxrss: libc::c_long, @@ -85,10 +83,14 @@ mod resource { ru_nivcsw: libc::c_long, } + #[pyattr] + #[pystruct_sequence(name = "struct_rusage", module = "resource", data = "RUsageData")] + struct PyRUsage; + #[pyclass(with(PyStructSequence))] - impl Rusage {} + impl PyRUsage {} - impl From for Rusage { + impl From for RUsageData { fn from(rusage: libc::rusage) -> Self { let tv = |tv: libc::timeval| tv.tv_sec as f64 + (tv.tv_usec as f64 / 1_000_000.0); Self { @@ -113,7 +115,7 @@ mod resource { } #[pyfunction] - fn getrusage(who: i32, vm: &VirtualMachine) -> PyResult { + fn getrusage(who: i32, vm: &VirtualMachine) -> PyResult { let res = unsafe { let mut rusage = mem::MaybeUninit::::uninit(); if libc::getrusage(who, rusage.as_mut_ptr()) == -1 { @@ -122,7 +124,7 @@ mod resource { Ok(rusage.assume_init()) } }; - res.map(Rusage::from).map_err(|e| { + res.map(RUsageData::from).map_err(|e| { if e.kind() == io::ErrorKind::InvalidInput { vm.new_value_error("invalid who parameter") } else { diff --git a/crates/vm/src/stdlib/nt.rs b/crates/vm/src/stdlib/nt.rs index cd2ff476ff7..9ed89b0bab7 100644 --- a/crates/vm/src/stdlib/nt.rs +++ b/crates/vm/src/stdlib/nt.rs @@ -179,7 +179,7 @@ pub(crate) mod module { fn get_terminal_size( fd: OptionalArg, vm: &VirtualMachine, - ) -> PyResult<_os::PyTerminalSize> { + ) -> PyResult<_os::TerminalSizeData> { let (columns, lines) = { let stdhandle = match fd { OptionalArg::Present(0) => Console::STD_INPUT_HANDLE, @@ -206,7 +206,7 @@ pub(crate) mod module { (w.Bottom - w.Top + 1) as usize, ) }; - Ok(_os::PyTerminalSize { columns, lines }) + Ok(_os::TerminalSizeData { columns, lines }) } #[cfg(target_env = "msvc")] diff --git a/crates/vm/src/stdlib/os.rs b/crates/vm/src/stdlib/os.rs index 1c65d4d12cf..45d4e41bcba 100644 --- a/crates/vm/src/stdlib/os.rs +++ b/crates/vm/src/stdlib/os.rs @@ -735,10 +735,9 @@ pub(super) mod _os { .into()) } - #[pyattr] - #[pyclass(module = "os", name = "stat_result")] - #[derive(Debug, PyStructSequence, FromArgs)] - struct StatResult { + #[derive(Debug, FromArgs)] + #[pystruct_sequence_data] + struct StatResultData { pub st_mode: PyIntRef, pub st_ino: PyIntRef, pub st_dev: PyIntRef, @@ -746,31 +745,42 @@ pub(super) mod _os { pub st_uid: PyIntRef, pub st_gid: PyIntRef, pub st_size: PyIntRef, - // TODO: unnamed structsequence fields + // Indices 7-9: integer seconds #[pyarg(positional, default)] - pub __st_atime_int: libc::time_t, + #[pystruct_sequence(unnamed)] + pub st_atime_int: libc::time_t, #[pyarg(positional, default)] - pub __st_mtime_int: libc::time_t, + #[pystruct_sequence(unnamed)] + pub st_mtime_int: libc::time_t, #[pyarg(positional, default)] - pub __st_ctime_int: libc::time_t, + #[pystruct_sequence(unnamed)] + pub st_ctime_int: libc::time_t, + // Float time attributes #[pyarg(any, default)] + #[pystruct_sequence(skip)] pub st_atime: f64, #[pyarg(any, default)] + #[pystruct_sequence(skip)] pub st_mtime: f64, #[pyarg(any, default)] + #[pystruct_sequence(skip)] pub st_ctime: f64, + // Nanosecond attributes #[pyarg(any, default)] + #[pystruct_sequence(skip)] pub st_atime_ns: i128, #[pyarg(any, default)] + #[pystruct_sequence(skip)] pub st_mtime_ns: i128, #[pyarg(any, default)] + #[pystruct_sequence(skip)] pub st_ctime_ns: i128, #[pyarg(any, default)] + #[pystruct_sequence(skip)] pub st_reparse_tag: u32, } - #[pyclass(with(PyStructSequence))] - impl StatResult { + impl StatResultData { fn from_stat(stat: &StatStruct, vm: &VirtualMachine) -> Self { let (atime, mtime, ctime); #[cfg(any(unix, windows))] @@ -810,9 +820,9 @@ pub(super) mod _os { st_uid: vm.ctx.new_pyref(stat.st_uid), st_gid: vm.ctx.new_pyref(stat.st_gid), st_size: vm.ctx.new_pyref(stat.st_size), - __st_atime_int: atime.0, - __st_mtime_int: mtime.0, - __st_ctime_int: ctime.0, + st_atime_int: atime.0, + st_mtime_int: mtime.0, + st_ctime_int: ctime.0, st_atime: to_f64(atime), st_mtime: to_f64(mtime), st_ctime: to_f64(ctime), @@ -822,7 +832,14 @@ pub(super) mod _os { st_reparse_tag, } } + } + + #[pyattr] + #[pystruct_sequence(name = "stat_result", module = "os", data = "StatResultData")] + struct PyStatResult; + #[pyclass(with(PyStructSequence))] + impl PyStatResult { #[pyslot] fn slot_new(_cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { let flatten_args = |r: &[PyObjectRef]| { @@ -845,7 +862,7 @@ pub(super) mod _os { let args: FuncArgs = flatten_args(&args.args).into(); - let stat: Self = args.bind(vm)?; + let stat: StatResultData = args.bind(vm)?; Ok(stat.to_pyobject(vm)) } } @@ -920,7 +937,7 @@ pub(super) mod _os { let stat = stat_inner(file.clone(), dir_fd, follow_symlinks) .map_err(|err| IOErrorBuilder::with_filename(&err, file, vm))? .ok_or_else(|| crate::exceptions::cstring_error(vm))?; - Ok(StatResult::from_stat(&stat, vm).to_pyobject(vm)) + Ok(StatResultData::from_stat(&stat, vm).to_pyobject(vm)) } #[pyfunction] @@ -1215,10 +1232,9 @@ pub(super) mod _os { } #[cfg(all(any(unix, windows), not(target_os = "redox")))] - #[pyattr] - #[pyclass(module = "os", name = "times_result")] - #[derive(Debug, PyStructSequence)] - struct TimesResult { + #[derive(Debug)] + #[pystruct_sequence_data] + struct TimesResultData { pub user: f64, pub system: f64, pub children_user: f64, @@ -1226,9 +1242,14 @@ pub(super) mod _os { pub elapsed: f64, } + #[cfg(all(any(unix, windows), not(target_os = "redox")))] + #[pyattr] + #[pystruct_sequence(name = "times_result", module = "os", data = "TimesResultData")] + struct PyTimesResult; + #[cfg(all(any(unix, windows), not(target_os = "redox")))] #[pyclass(with(PyStructSequence))] - impl TimesResult {} + impl PyTimesResult {} #[cfg(all(any(unix, windows), not(target_os = "redox")))] #[pyfunction] @@ -1257,7 +1278,7 @@ pub(super) mod _os { let kernel = unsafe { kernel.assume_init() }; let user = unsafe { user.assume_init() }; - let times_result = TimesResult { + let times_result = TimesResultData { user: user.dwHighDateTime as f64 * 429.4967296 + user.dwLowDateTime as f64 * 1e-7, system: kernel.dwHighDateTime as f64 * 429.4967296 + kernel.dwLowDateTime as f64 * 1e-7, @@ -1285,7 +1306,7 @@ pub(super) mod _os { return Err(vm.new_os_error("Fail to get times".to_string())); } - let times_result = TimesResult { + let times_result = TimesResultData { user: t.tms_utime as f64 / tick_for_second, system: t.tms_stime as f64 / tick_for_second, children_user: t.tms_cutime as f64 / tick_for_second, @@ -1458,21 +1479,23 @@ pub(super) mod _os { } } - #[pyattr] - #[pyclass(module = "os", name = "terminal_size")] - #[derive(PyStructSequence)] + #[pystruct_sequence_data] #[allow(dead_code)] - pub(crate) struct PyTerminalSize { + pub(crate) struct TerminalSizeData { pub columns: usize, pub lines: usize, } + + #[pyattr] + #[pystruct_sequence(name = "terminal_size", module = "os", data = "TerminalSizeData")] + pub(crate) struct PyTerminalSize; + #[pyclass(with(PyStructSequence))] impl PyTerminalSize {} - #[pyattr] - #[pyclass(module = "os", name = "uname_result")] - #[derive(Debug, PyStructSequence)] - pub(crate) struct UnameResult { + #[derive(Debug)] + #[pystruct_sequence_data] + pub(crate) struct UnameResultData { pub sysname: String, pub nodename: String, pub release: String, @@ -1480,8 +1503,12 @@ pub(super) mod _os { pub machine: String, } + #[pyattr] + #[pystruct_sequence(name = "uname_result", module = "os", data = "UnameResultData")] + pub(crate) struct PyUnameResult; + #[pyclass(with(PyStructSequence))] - impl UnameResult {} + impl PyUnameResult {} pub(super) fn support_funcs() -> Vec { let mut supports = super::platform::module::support_funcs(); diff --git a/crates/vm/src/stdlib/posix.rs b/crates/vm/src/stdlib/posix.rs index 071f93d7ee0..7409064668c 100644 --- a/crates/vm/src/stdlib/posix.rs +++ b/crates/vm/src/stdlib/posix.rs @@ -1330,9 +1330,9 @@ pub mod module { } #[pyfunction] - fn uname(vm: &VirtualMachine) -> PyResult<_os::UnameResult> { + fn uname(vm: &VirtualMachine) -> PyResult<_os::UnameResultData> { let info = uname::uname().map_err(|err| err.into_pyexception(vm))?; - Ok(_os::UnameResult { + Ok(_os::UnameResultData { sysname: info.sysname, nodename: info.nodename, release: info.release, @@ -1730,7 +1730,7 @@ pub mod module { fn get_terminal_size( fd: OptionalArg, vm: &VirtualMachine, - ) -> PyResult<_os::PyTerminalSize> { + ) -> PyResult<_os::TerminalSizeData> { let (columns, lines) = { nix::ioctl_read_bad!(winsz, libc::TIOCGWINSZ, libc::winsize); let mut w = libc::winsize { @@ -1743,7 +1743,7 @@ pub mod module { .map_err(|err| err.into_pyexception(vm))?; (w.ws_col.into(), w.ws_row.into()) }; - Ok(_os::PyTerminalSize { columns, lines }) + Ok(_os::TerminalSizeData { columns, lines }) } // from libstd: diff --git a/crates/vm/src/stdlib/pwd.rs b/crates/vm/src/stdlib/pwd.rs index 525b957e56d..e4d7075dbc8 100644 --- a/crates/vm/src/stdlib/pwd.rs +++ b/crates/vm/src/stdlib/pwd.rs @@ -16,10 +16,8 @@ mod pwd { #[cfg(not(target_os = "android"))] use crate::{PyObjectRef, convert::ToPyObject}; - #[pyattr] - #[pyclass(module = "pwd", name = "struct_passwd")] - #[derive(PyStructSequence)] - struct Passwd { + #[pystruct_sequence_data] + struct PasswdData { pw_name: String, pw_passwd: String, pw_uid: u32, @@ -29,10 +27,14 @@ mod pwd { pw_shell: String, } + #[pyattr] + #[pystruct_sequence(name = "struct_passwd", module = "pwd", data = "PasswdData")] + struct PyPasswd; + #[pyclass(with(PyStructSequence))] - impl Passwd {} + impl PyPasswd {} - impl From for Passwd { + impl From for PasswdData { fn from(user: User) -> Self { // this is just a pain... let cstr_lossy = |s: std::ffi::CString| { @@ -44,7 +46,7 @@ mod pwd { .into_string() .unwrap_or_else(|s| s.to_string_lossy().into_owned()) }; - Passwd { + PasswdData { pw_name: user.name, pw_passwd: cstr_lossy(user.passwd), pw_uid: user.uid.as_raw(), @@ -57,7 +59,7 @@ mod pwd { } #[pyfunction] - fn getpwnam(name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { + fn getpwnam(name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { let pw_name = name.as_str(); if pw_name.contains('\0') { return Err(exceptions::cstring_error(vm)); @@ -70,11 +72,11 @@ mod pwd { .into(), ) })?; - Ok(Passwd::from(user)) + Ok(PasswdData::from(user)) } #[pyfunction] - fn getpwuid(uid: PyIntRef, vm: &VirtualMachine) -> PyResult { + fn getpwuid(uid: PyIntRef, vm: &VirtualMachine) -> PyResult { let uid_t = libc::uid_t::try_from(uid.as_bigint()) .map(unistd::Uid::from_raw) .ok(); @@ -90,7 +92,7 @@ mod pwd { .into(), ) })?; - Ok(Passwd::from(user)) + Ok(PasswdData::from(user)) } // TODO: maybe merge this functionality into nix? @@ -105,7 +107,7 @@ mod pwd { unsafe { libc::setpwent() }; while let Some(ptr) = std::ptr::NonNull::new(unsafe { libc::getpwent() }) { let user = User::from(unsafe { ptr.as_ref() }); - let passwd = Passwd::from(user).to_pyobject(vm); + let passwd = PasswdData::from(user).to_pyobject(vm); list.push(passwd); } unsafe { libc::endpwent() }; diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index 12a69c23138..602fdff1eb1 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -1,6 +1,6 @@ use crate::{Py, PyResult, VirtualMachine, builtins::PyModule, convert::ToPyObject}; -pub(crate) use sys::{__module_def, DOC, MAXSIZE, MULTIARCH, UnraisableHookArgs}; +pub(crate) use sys::{__module_def, DOC, MAXSIZE, MULTIARCH, UnraisableHookArgsData}; #[pymodule] mod sys { @@ -450,12 +450,12 @@ mod sys { #[pyattr] fn flags(vm: &VirtualMachine) -> PyTupleRef { - Flags::from_settings(&vm.state.settings).into_struct_sequence(vm) + PyFlags::from_data(FlagsData::from_settings(&vm.state.settings), vm) } #[pyattr] fn float_info(vm: &VirtualMachine) -> PyTupleRef { - PyFloatInfo::INFO.into_struct_sequence(vm) + PyFloatInfo::from_data(FloatInfoData::INFO, vm) } #[pyfunction] @@ -656,7 +656,7 @@ mod sys { .map_err(|_| vm.new_os_error("service pack is not ASCII".to_owned()))? }; let real_version = get_kernel32_version().map_err(|e| vm.new_os_error(e.to_string()))?; - Ok(WindowsVersion { + let winver = WindowsVersionData { major: real_version.0, minor: real_version.1, build: real_version.2, @@ -667,11 +667,11 @@ mod sys { suite_mask: version.wSuiteMask, product_type: version.wProductType, platform_version: (real_version.0, real_version.1, real_version.2), // TODO Provide accurate version, like CPython impl - } - .into_struct_sequence(vm)) + }; + Ok(PyWindowsVersion::from_data(winver, vm)) } - fn _unraisablehook(unraisable: UnraisableHookArgs, vm: &VirtualMachine) -> PyResult<()> { + fn _unraisablehook(unraisable: UnraisableHookArgsData, vm: &VirtualMachine) -> PyResult<()> { use super::PyStderr; let stderr = PyStderr(vm); @@ -727,7 +727,7 @@ mod sys { #[pyattr] #[pyfunction(name = "__unraisablehook__")] - fn unraisablehook(unraisable: UnraisableHookArgs, vm: &VirtualMachine) { + fn unraisablehook(unraisable: UnraisableHookArgsData, vm: &VirtualMachine) { if let Err(e) = _unraisablehook(unraisable, vm) { let stderr = super::PyStderr(vm); writeln!( @@ -742,7 +742,7 @@ mod sys { #[pyattr] fn hash_info(vm: &VirtualMachine) -> PyTupleRef { - PyHashInfo::INFO.into_struct_sequence(vm) + PyHashInfo::from_data(HashInfoData::INFO, vm) } #[pyfunction] @@ -752,7 +752,7 @@ mod sys { #[pyattr] fn int_info(vm: &VirtualMachine) -> PyTupleRef { - PyIntInfo::INFO.into_struct_sequence(vm) + PyIntInfo::from_data(IntInfoData::INFO, vm) } #[pyfunction] @@ -762,7 +762,7 @@ mod sys { #[pyfunction] fn set_int_max_str_digits(maxdigits: usize, vm: &VirtualMachine) -> PyResult<()> { - let threshold = PyIntInfo::INFO.str_digits_check_threshold; + let threshold = IntInfoData::INFO.str_digits_check_threshold; if maxdigits == 0 || maxdigits >= threshold { vm.state.int_max_str_digits.store(maxdigits); Ok(()) @@ -812,12 +812,12 @@ mod sys { #[cfg(feature = "threading")] #[pyattr] fn thread_info(vm: &VirtualMachine) -> PyTupleRef { - PyThreadInfo::INFO.into_struct_sequence(vm) + PyThreadInfo::from_data(ThreadInfoData::INFO, vm) } #[pyattr] fn version_info(vm: &VirtualMachine) -> PyTupleRef { - VersionInfo::VERSION.into_struct_sequence(vm) + PyVersionInfo::from_data(VersionInfoData::VERSION, vm) } fn update_use_tracing(vm: &VirtualMachine) { @@ -898,19 +898,22 @@ mod sys { Ok(()) } - #[pyclass(no_attr, name = "asyncgen_hooks")] - #[derive(PyStructSequence)] - pub(super) struct PyAsyncgenHooks { + #[pystruct_sequence_data] + pub(super) struct AsyncgenHooksData { firstiter: PyObjectRef, finalizer: PyObjectRef, } + #[pyattr] + #[pystruct_sequence(name = "asyncgen_hooks", data = "AsyncgenHooksData")] + pub(super) struct PyAsyncgenHooks; + #[pyclass(with(PyStructSequence))] impl PyAsyncgenHooks {} #[pyfunction] - fn get_asyncgen_hooks(vm: &VirtualMachine) -> PyAsyncgenHooks { - PyAsyncgenHooks { + fn get_asyncgen_hooks(vm: &VirtualMachine) -> AsyncgenHooksData { + AsyncgenHooksData { firstiter: crate::vm::thread::ASYNC_GEN_FIRSTITER .with_borrow(Clone::clone) .to_pyobject(vm), @@ -923,9 +926,9 @@ mod sys { /// sys.flags /// /// Flags provided through command line arguments or environment vars. - #[pyclass(no_attr, name = "flags", module = "sys")] - #[derive(Debug, PyStructSequence)] - pub(super) struct Flags { + #[derive(Debug)] + #[pystruct_sequence_data] + pub(super) struct FlagsData { /// -d debug: u8, /// -i @@ -964,8 +967,7 @@ mod sys { warn_default_encoding: u8, } - #[pyclass(with(PyStructSequence))] - impl Flags { + impl FlagsData { const fn from_settings(settings: &Settings) -> Self { Self { debug: settings.debug, @@ -988,7 +990,13 @@ mod sys { warn_default_encoding: settings.warn_default_encoding as u8, } } + } + + #[pystruct_sequence(name = "flags", module = "sys", data = "FlagsData", no_attr)] + pub(super) struct PyFlags; + #[pyclass(with(PyStructSequence))] + impl PyFlags { #[pyslot] fn slot_new(_cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult { Err(vm.new_type_error("cannot create 'sys.flags' instances")) @@ -996,17 +1004,15 @@ mod sys { } #[cfg(feature = "threading")] - #[pyclass(no_attr, name = "thread_info")] - #[derive(PyStructSequence)] - pub(super) struct PyThreadInfo { + #[pystruct_sequence_data] + pub(super) struct ThreadInfoData { name: Option<&'static str>, lock: Option<&'static str>, version: Option<&'static str>, } #[cfg(feature = "threading")] - #[pyclass(with(PyStructSequence))] - impl PyThreadInfo { + impl ThreadInfoData { const INFO: Self = Self { name: crate::stdlib::thread::_thread::PYTHREAD_NAME, // As I know, there's only way to use lock as "Mutex" in Rust @@ -1016,9 +1022,16 @@ mod sys { }; } - #[pyclass(no_attr, name = "float_info")] - #[derive(PyStructSequence)] - pub(super) struct PyFloatInfo { + #[cfg(feature = "threading")] + #[pystruct_sequence(name = "thread_info", data = "ThreadInfoData", no_attr)] + pub(super) struct PyThreadInfo; + + #[cfg(feature = "threading")] + #[pyclass(with(PyStructSequence))] + impl PyThreadInfo {} + + #[pystruct_sequence_data] + pub(super) struct FloatInfoData { max: f64, max_exp: i32, max_10_exp: i32, @@ -1032,8 +1045,7 @@ mod sys { rounds: i32, } - #[pyclass(with(PyStructSequence))] - impl PyFloatInfo { + impl FloatInfoData { const INFO: Self = Self { max: f64::MAX, max_exp: f64::MAX_EXP, @@ -1049,9 +1061,14 @@ mod sys { }; } - #[pyclass(no_attr, name = "hash_info")] - #[derive(PyStructSequence)] - pub(super) struct PyHashInfo { + #[pystruct_sequence(name = "float_info", data = "FloatInfoData", no_attr)] + pub(super) struct PyFloatInfo; + + #[pyclass(with(PyStructSequence))] + impl PyFloatInfo {} + + #[pystruct_sequence_data] + pub(super) struct HashInfoData { width: usize, modulus: PyUHash, inf: PyHash, @@ -1063,8 +1080,7 @@ mod sys { cutoff: usize, } - #[pyclass(with(PyStructSequence))] - impl PyHashInfo { + impl HashInfoData { const INFO: Self = { use rustpython_common::hash::*; Self { @@ -1081,17 +1097,21 @@ mod sys { }; } - #[pyclass(no_attr, name = "int_info")] - #[derive(PyStructSequence)] - pub(super) struct PyIntInfo { + #[pystruct_sequence(name = "hash_info", data = "HashInfoData", no_attr)] + pub(super) struct PyHashInfo; + + #[pyclass(with(PyStructSequence))] + impl PyHashInfo {} + + #[pystruct_sequence_data] + pub(super) struct IntInfoData { bits_per_digit: usize, sizeof_digit: usize, default_max_str_digits: usize, str_digits_check_threshold: usize, } - #[pyclass(with(PyStructSequence))] - impl PyIntInfo { + impl IntInfoData { const INFO: Self = Self { bits_per_digit: 30, //? sizeof_digit: std::mem::size_of::(), @@ -1100,9 +1120,15 @@ mod sys { }; } - #[pyclass(no_attr, name = "version_info")] - #[derive(Default, Debug, PyStructSequence)] - pub struct VersionInfo { + #[pystruct_sequence(name = "int_info", data = "IntInfoData", no_attr)] + pub(super) struct PyIntInfo; + + #[pyclass(with(PyStructSequence))] + impl PyIntInfo {} + + #[derive(Default, Debug)] + #[pystruct_sequence_data] + pub struct VersionInfoData { major: usize, minor: usize, micro: usize, @@ -1110,8 +1136,7 @@ mod sys { serial: usize, } - #[pyclass(with(PyStructSequence))] - impl VersionInfo { + impl VersionInfoData { pub const VERSION: Self = Self { major: version::MAJOR, minor: version::MINOR, @@ -1119,6 +1144,13 @@ mod sys { releaselevel: version::RELEASELEVEL, serial: version::SERIAL, }; + } + + #[pystruct_sequence(name = "version_info", data = "VersionInfoData", no_attr)] + pub struct PyVersionInfo; + + #[pyclass(with(PyStructSequence))] + impl PyVersionInfo { #[pyslot] fn slot_new( _cls: crate::builtins::type_::PyTypeRef, @@ -1130,9 +1162,9 @@ mod sys { } #[cfg(windows)] - #[pyclass(no_attr, name = "getwindowsversion")] - #[derive(Default, Debug, PyStructSequence)] - pub(super) struct WindowsVersion { + #[derive(Default, Debug)] + #[pystruct_sequence_data] + pub(super) struct WindowsVersionData { major: u32, minor: u32, build: u32, @@ -1145,13 +1177,17 @@ mod sys { platform_version: (u32, u32, u32), } + #[cfg(windows)] + #[pystruct_sequence(name = "getwindowsversion", data = "WindowsVersionData", no_attr)] + pub(super) struct PyWindowsVersion; + #[cfg(windows)] #[pyclass(with(PyStructSequence))] - impl WindowsVersion {} + impl PyWindowsVersion {} - #[pyclass(no_attr, name = "UnraisableHookArgs")] - #[derive(Debug, PyStructSequence, TryIntoPyStructSequence)] - pub struct UnraisableHookArgs { + #[derive(Debug)] + #[pystruct_sequence_data(try_from_object)] + pub struct UnraisableHookArgsData { pub exc_type: PyTypeRef, pub exc_value: PyObjectRef, pub exc_traceback: PyObjectRef, @@ -1159,8 +1195,11 @@ mod sys { pub object: PyObjectRef, } + #[pystruct_sequence(name = "UnraisableHookArgs", data = "UnraisableHookArgsData", no_attr)] + pub struct PyUnraisableHookArgs; + #[pyclass(with(PyStructSequence))] - impl UnraisableHookArgs {} + impl PyUnraisableHookArgs {} } pub(crate) fn init_module(vm: &VirtualMachine, module: &Py, builtins: &Py) { diff --git a/crates/vm/src/stdlib/time.rs b/crates/vm/src/stdlib/time.rs index 2e8743b1c2d..0f3b21a3d93 100644 --- a/crates/vm/src/stdlib/time.rs +++ b/crates/vm/src/stdlib/time.rs @@ -34,10 +34,10 @@ unsafe extern "C" { #[pymodule(name = "time", with(platform))] mod decl { use crate::{ - AsObject, PyObjectRef, PyResult, TryFromObject, VirtualMachine, + AsObject, PyObjectRef, PyResult, VirtualMachine, builtins::{PyStrRef, PyTypeRef, PyUtf8StrRef}, function::{Either, FuncArgs, OptionalArg}, - types::PyStructSequence, + types::{PyStructSequence, struct_sequence_new}, }; use chrono::{ DateTime, Datelike, TimeZone, Timelike, @@ -295,7 +295,7 @@ mod decl { } } - impl OptionalArg { + impl OptionalArg { fn naive_or_local(self, vm: &VirtualMachine) -> PyResult { Ok(match self { Self::Present(t) => t.to_date_time(vm)?, @@ -306,24 +306,27 @@ mod decl { /// https://docs.python.org/3/library/time.html?highlight=gmtime#time.gmtime #[pyfunction] - fn gmtime(secs: OptionalArg>, vm: &VirtualMachine) -> PyResult { + fn gmtime( + secs: OptionalArg>, + vm: &VirtualMachine, + ) -> PyResult { let instant = secs.naive_or_utc(vm)?; - Ok(PyStructTime::new(vm, instant, 0)) + Ok(StructTimeData::new(vm, instant, 0)) } #[pyfunction] fn localtime( secs: OptionalArg>, vm: &VirtualMachine, - ) -> PyResult { + ) -> PyResult { let instant = secs.naive_or_local(vm)?; // TODO: isdst flag must be valid value here // https://docs.python.org/3/library/time.html#time.localtime - Ok(PyStructTime::new(vm, instant, -1)) + Ok(StructTimeData::new(vm, instant, -1)) } #[pyfunction] - fn mktime(t: PyStructTime, vm: &VirtualMachine) -> PyResult { + fn mktime(t: StructTimeData, vm: &VirtualMachine) -> PyResult { let datetime = t.to_date_time(vm)?; // mktime interprets struct_time as local time let local_dt = chrono::Local @@ -337,7 +340,7 @@ mod decl { const CFMT: &str = "%a %b %e %H:%M:%S %Y"; #[pyfunction] - fn asctime(t: OptionalArg, vm: &VirtualMachine) -> PyResult { + fn asctime(t: OptionalArg, vm: &VirtualMachine) -> PyResult { let instant = t.naive_or_local(vm)?; let formatted_time = instant.format(CFMT).to_string(); Ok(vm.ctx.new_str(formatted_time).into()) @@ -352,7 +355,7 @@ mod decl { #[pyfunction] fn strftime( format: PyUtf8StrRef, - t: OptionalArg, + t: OptionalArg, vm: &VirtualMachine, ) -> PyResult { use std::fmt::Write; @@ -472,34 +475,31 @@ mod decl { Ok(get_process_time(vm)?.as_nanos() as u64) } - #[pyattr] - #[pyclass(name = "struct_time")] - #[derive(PyStructSequence, TryIntoPyStructSequence)] - #[allow(dead_code)] - struct PyStructTime { - tm_year: PyObjectRef, - tm_mon: PyObjectRef, - tm_mday: PyObjectRef, - tm_hour: PyObjectRef, - tm_min: PyObjectRef, - tm_sec: PyObjectRef, - tm_wday: PyObjectRef, - tm_yday: PyObjectRef, - tm_isdst: PyObjectRef, - #[pystruct(skip)] - tm_gmtoff: PyObjectRef, - #[pystruct(skip)] - tm_zone: PyObjectRef, - } - - impl std::fmt::Debug for PyStructTime { + /// Data struct for struct_time + #[pystruct_sequence_data(try_from_object)] + pub struct StructTimeData { + pub tm_year: PyObjectRef, + pub tm_mon: PyObjectRef, + pub tm_mday: PyObjectRef, + pub tm_hour: PyObjectRef, + pub tm_min: PyObjectRef, + pub tm_sec: PyObjectRef, + pub tm_wday: PyObjectRef, + pub tm_yday: PyObjectRef, + pub tm_isdst: PyObjectRef, + #[pystruct_sequence(skip)] + pub tm_gmtoff: PyObjectRef, + #[pystruct_sequence(skip)] + pub tm_zone: PyObjectRef, + } + + impl std::fmt::Debug for StructTimeData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "struct_time()") } } - #[pyclass(with(PyStructSequence))] - impl PyStructTime { + impl StructTimeData { fn new(vm: &VirtualMachine, tm: NaiveDateTime, isdst: i32) -> Self { let local_time = chrono::Local.from_local_datetime(&tm).unwrap(); let offset_seconds = @@ -538,12 +538,18 @@ mod decl { ); Ok(dt) } + } + + #[pyattr] + #[pystruct_sequence(name = "struct_time", module = "time", data = "StructTimeData")] + pub struct PyStructTime; + #[pyclass(with(PyStructSequence))] + impl PyStructTime { #[pyslot] - fn slot_new(_cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - // cls is ignorable because this is not a basetype - let seq = args.bind(vm)?; - Ok(vm.new_pyobj(Self::try_from_object(vm, seq)?)) + fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let seq: PyObjectRef = args.bind(vm)?; + struct_sequence_new(cls, seq, vm) } } diff --git a/crates/vm/src/types/mod.rs b/crates/vm/src/types/mod.rs index 56d925bfaf1..f19328cdd2d 100644 --- a/crates/vm/src/types/mod.rs +++ b/crates/vm/src/types/mod.rs @@ -3,5 +3,5 @@ mod structseq; mod zoo; pub use slot::*; -pub use structseq::PyStructSequence; +pub use structseq::{PyStructSequence, PyStructSequenceData, struct_sequence_new}; pub(crate) use zoo::TypeZoo; diff --git a/crates/vm/src/types/structseq.rs b/crates/vm/src/types/structseq.rs index 318280f8620..b2ff5868d45 100644 --- a/crates/vm/src/types/structseq.rs +++ b/crates/vm/src/types/structseq.rs @@ -1,50 +1,193 @@ use crate::{ - AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, - builtins::{PyBaseExceptionRef, PyStrRef, PyTuple, PyTupleRef, PyType}, + AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, atomic_func, + builtins::{ + PyBaseExceptionRef, PyStrRef, PyTuple, PyTupleRef, PyType, PyTypeRef, type_::PointerSlot, + }, class::{PyClassImpl, StaticType}, + function::{Either, PyComparisonValue}, + iter::PyExactSizeIterator, + protocol::{PyMappingMethods, PySequenceMethods}, + sliceable::{SequenceIndex, SliceableSequenceOp}, + types::PyComparisonOp, vm::Context, }; +use std::sync::LazyLock; -#[pyclass] -pub trait PyStructSequence: StaticType + PyClassImpl + Sized + 'static { +/// Create a new struct sequence instance from a sequence. +/// +/// The class must have `n_sequence_fields` and `n_fields` attributes set +/// (done automatically by `PyStructSequence::extend_pyclass`). +pub fn struct_sequence_new(cls: PyTypeRef, seq: PyObjectRef, vm: &VirtualMachine) -> PyResult { + // = structseq_new + + #[cold] + fn length_error( + tp_name: &str, + min_len: usize, + max_len: usize, + len: usize, + vm: &VirtualMachine, + ) -> PyBaseExceptionRef { + if min_len == max_len { + vm.new_type_error(format!( + "{tp_name}() takes a {min_len}-sequence ({len}-sequence given)" + )) + } else if len < min_len { + vm.new_type_error(format!( + "{tp_name}() takes an at least {min_len}-sequence ({len}-sequence given)" + )) + } else { + vm.new_type_error(format!( + "{tp_name}() takes an at most {max_len}-sequence ({len}-sequence given)" + )) + } + } + + let min_len: usize = cls + .get_attr(identifier!(vm.ctx, n_sequence_fields)) + .ok_or_else(|| vm.new_type_error("missing n_sequence_fields attribute"))? + .try_into_value(vm)?; + let max_len: usize = cls + .get_attr(identifier!(vm.ctx, n_fields)) + .ok_or_else(|| vm.new_type_error("missing n_fields attribute"))? + .try_into_value(vm)?; + + let seq: Vec = seq.try_into_value(vm)?; + let len = seq.len(); + + if len < min_len || len > max_len { + return Err(length_error(&cls.slot_name(), min_len, max_len, len, vm)); + } + + // Copy items and pad with None + let mut items = seq; + items.resize_with(max_len, || vm.ctx.none()); + + PyTuple::new_unchecked(items.into_boxed_slice()) + .into_ref_with_type(vm, cls) + .map(Into::into) +} + +fn get_visible_len(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + obj.class() + .get_attr(identifier!(vm.ctx, n_sequence_fields)) + .ok_or_else(|| vm.new_type_error("missing n_sequence_fields"))? + .try_into_value(vm) +} + +/// Sequence methods for struct sequences. +/// Uses n_sequence_fields to determine visible length. +static STRUCT_SEQUENCE_AS_SEQUENCE: LazyLock = + LazyLock::new(|| PySequenceMethods { + length: atomic_func!(|seq, vm| get_visible_len(seq.obj, vm)), + concat: atomic_func!(|seq, other, vm| { + // Convert to visible-only tuple, then use regular tuple concat + let n_seq = get_visible_len(seq.obj, vm)?; + let tuple = seq.obj.downcast_ref::().unwrap(); + let visible: Vec<_> = tuple.iter().take(n_seq).cloned().collect(); + let visible_tuple = PyTuple::new_ref(visible, &vm.ctx); + // Use tuple's concat implementation + visible_tuple.as_object().to_sequence().concat(other, vm) + }), + repeat: atomic_func!(|seq, n, vm| { + // Convert to visible-only tuple, then use regular tuple repeat + let n_seq = get_visible_len(seq.obj, vm)?; + let tuple = seq.obj.downcast_ref::().unwrap(); + let visible: Vec<_> = tuple.iter().take(n_seq).cloned().collect(); + let visible_tuple = PyTuple::new_ref(visible, &vm.ctx); + // Use tuple's repeat implementation + visible_tuple.as_object().to_sequence().repeat(n, vm) + }), + item: atomic_func!(|seq, i, vm| { + let n_seq = get_visible_len(seq.obj, vm)?; + let tuple = seq.obj.downcast_ref::().unwrap(); + let idx = if i < 0 { + let pos_i = n_seq as isize + i; + if pos_i < 0 { + return Err(vm.new_index_error("tuple index out of range")); + } + pos_i as usize + } else { + i as usize + }; + if idx >= n_seq { + return Err(vm.new_index_error("tuple index out of range")); + } + Ok(tuple[idx].clone()) + }), + contains: atomic_func!(|seq, needle, vm| { + let n_seq = get_visible_len(seq.obj, vm)?; + let tuple = seq.obj.downcast_ref::().unwrap(); + for item in tuple.iter().take(n_seq) { + if item.rich_compare_bool(needle, PyComparisonOp::Eq, vm)? { + return Ok(true); + } + } + Ok(false) + }), + ..PySequenceMethods::NOT_IMPLEMENTED + }); + +/// Mapping methods for struct sequences. +/// Handles subscript (indexing) with visible length bounds. +static STRUCT_SEQUENCE_AS_MAPPING: LazyLock = + LazyLock::new(|| PyMappingMethods { + length: atomic_func!(|mapping, vm| get_visible_len(mapping.obj, vm)), + subscript: atomic_func!(|mapping, needle, vm| { + let n_seq = get_visible_len(mapping.obj, vm)?; + let tuple = mapping.obj.downcast_ref::().unwrap(); + let visible_elements = &tuple.as_slice()[..n_seq]; + + match SequenceIndex::try_from_borrowed_object(vm, needle, "tuple")? { + SequenceIndex::Int(i) => visible_elements.getitem_by_index(vm, i), + SequenceIndex::Slice(slice) => visible_elements + .getitem_by_slice(vm, slice) + .map(|x| vm.ctx.new_tuple(x).into()), + } + }), + ..PyMappingMethods::NOT_IMPLEMENTED + }); + +/// Trait for Data structs that back a PyStructSequence. +/// +/// This trait is implemented by `#[pystruct_sequence_data]` on the Data struct. +/// It provides field information, tuple conversion, and element parsing. +pub trait PyStructSequenceData: Sized { + /// Names of required fields (in order). Shown in repr. const REQUIRED_FIELD_NAMES: &'static [&'static str]; + + /// Names of optional/skipped fields (in order, after required fields). const OPTIONAL_FIELD_NAMES: &'static [&'static str]; + /// Number of unnamed fields (visible but index-only access). + const UNNAMED_FIELDS_LEN: usize = 0; + + /// Convert this Data struct into a PyTuple. fn into_tuple(self, vm: &VirtualMachine) -> PyTuple; - fn into_struct_sequence(self, vm: &VirtualMachine) -> PyTupleRef { - self.into_tuple(vm) - .into_ref_with_type(vm, Self::static_type().to_owned()) - .unwrap() + /// Construct this Data struct from tuple elements. + /// Default implementation returns an error. + /// Override with `#[pystruct_sequence_data(try_from_object)]` to enable. + fn try_from_elements(_elements: Vec, vm: &VirtualMachine) -> PyResult { + Err(vm.new_type_error("This struct sequence does not support construction from elements")) } +} - fn try_elements_from(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult> { - #[cold] - fn sequence_length_error( - name: &str, - len: usize, - vm: &VirtualMachine, - ) -> PyBaseExceptionRef { - vm.new_type_error(format!("{name} takes a sequence of length {len}")) - } +/// Trait for Python struct sequence types. +/// +/// This trait is implemented by the `#[pystruct_sequence]` macro on the Python type struct. +/// It connects to the Data struct and provides Python-level functionality. +#[pyclass] +pub trait PyStructSequence: StaticType + PyClassImpl + Sized + 'static { + /// The Data struct that provides field definitions. + type Data: PyStructSequenceData; + /// Convert a Data struct into a PyStructSequence instance. + fn from_data(data: Self::Data, vm: &VirtualMachine) -> PyTupleRef { let typ = Self::static_type(); - // if !obj.fast_isinstance(typ) { - // return Err(vm.new_type_error(format!( - // "{} is not a subclass of {}", - // obj.class().name(), - // typ.name(), - // ))); - // } - let seq: Vec = obj.try_into_value(vm)?; - if seq.len() < Self::REQUIRED_FIELD_NAMES.len() { - return Err(sequence_length_error( - &typ.name(), - Self::REQUIRED_FIELD_NAMES.len(), - vm, - )); - } - Ok(seq) + data.into_tuple(vm) + .into_ref_with_type(vm, typ.to_owned()) + .unwrap() } #[pyslot] @@ -53,20 +196,21 @@ pub trait PyStructSequence: StaticType + PyClassImpl + Sized + 'static { .downcast_ref::() .ok_or_else(|| vm.new_type_error("unexpected payload for __repr__"))?; + let field_names = Self::Data::REQUIRED_FIELD_NAMES; let format_field = |(value, name): (&PyObjectRef, _)| { let s = value.repr(vm)?; Ok(format!("{name}={s}")) }; let (body, suffix) = if let Some(_guard) = rustpython_vm::recursion::ReprGuard::enter(vm, zelf.as_ref()) { - if Self::REQUIRED_FIELD_NAMES.len() == 1 { + if field_names.len() == 1 { let value = zelf.first().unwrap(); - let formatted = format_field((value, Self::REQUIRED_FIELD_NAMES[0]))?; + let formatted = format_field((value, field_names[0]))?; (formatted, ",") } else { let fields: PyResult> = zelf .iter() - .zip(Self::REQUIRED_FIELD_NAMES.iter().copied()) + .zip(field_names.iter().copied()) .map(format_field) .collect(); (fields?.join(", "), "") @@ -88,9 +232,23 @@ pub trait PyStructSequence: StaticType + PyClassImpl + Sized + 'static { vm.new_tuple((zelf.class().to_owned(), (vm.ctx.new_tuple(zelf.to_vec()),))) } + #[pymethod] + fn __getitem__(zelf: PyRef, needle: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let n_seq = get_visible_len(zelf.as_ref(), vm)?; + let visible_elements = &zelf.as_slice()[..n_seq]; + + match SequenceIndex::try_from_borrowed_object(vm, &needle, "tuple")? { + SequenceIndex::Int(i) => visible_elements.getitem_by_index(vm, i), + SequenceIndex::Slice(slice) => visible_elements + .getitem_by_slice(vm, slice) + .map(|x| vm.ctx.new_tuple(x).into()), + } + } + #[extend_class] fn extend_pyclass(ctx: &Context, class: &'static Py) { - for (i, &name) in Self::REQUIRED_FIELD_NAMES.iter().enumerate() { + // Getters for named visible fields (indices 0 to REQUIRED_FIELD_NAMES.len() - 1) + for (i, &name) in Self::Data::REQUIRED_FIELD_NAMES.iter().enumerate() { // cast i to a u8 so there's less to store in the getter closure. // Hopefully there's not struct sequences with >=256 elements :P let i = i as u8; @@ -103,15 +261,125 @@ pub trait PyStructSequence: StaticType + PyClassImpl + Sized + 'static { ); } + // Getters for hidden/skipped fields (indices after visible fields) + let visible_count = Self::Data::REQUIRED_FIELD_NAMES.len() + Self::Data::UNNAMED_FIELDS_LEN; + for (i, &name) in Self::Data::OPTIONAL_FIELD_NAMES.iter().enumerate() { + let idx = (visible_count + i) as u8; + class.set_attr( + ctx.intern_str(name), + ctx.new_readonly_getset(name, class, move |zelf: &PyTuple| { + zelf[idx as usize].to_owned() + }) + .into(), + ); + } + class.set_attr( identifier!(ctx, __match_args__), ctx.new_tuple( - Self::REQUIRED_FIELD_NAMES + Self::Data::REQUIRED_FIELD_NAMES .iter() .map(|&name| ctx.new_str(name).into()) .collect::>(), ) .into(), ); + + // special fields: + // n_sequence_fields = visible fields (named + unnamed) + // n_fields = all fields (visible + hidden/skipped) + // n_unnamed_fields + let n_unnamed_fields = Self::Data::UNNAMED_FIELDS_LEN; + let n_sequence_fields = Self::Data::REQUIRED_FIELD_NAMES.len() + n_unnamed_fields; + let n_fields = n_sequence_fields + Self::Data::OPTIONAL_FIELD_NAMES.len(); + class.set_attr( + identifier!(ctx, n_sequence_fields), + ctx.new_int(n_sequence_fields).into(), + ); + class.set_attr(identifier!(ctx, n_fields), ctx.new_int(n_fields).into()); + class.set_attr( + identifier!(ctx, n_unnamed_fields), + ctx.new_int(n_unnamed_fields).into(), + ); + + // Override as_sequence and as_mapping slots to use visible length + class.slots.as_sequence.store(Some(PointerSlot::from( + &*STRUCT_SEQUENCE_AS_SEQUENCE as &'static PySequenceMethods, + ))); + class.slots.as_mapping.store(Some(PointerSlot::from( + &*STRUCT_SEQUENCE_AS_MAPPING as &'static PyMappingMethods, + ))); + + // Override iter slot to return only visible elements + class.slots.iter.store(Some(struct_sequence_iter)); + + // Override hash slot to hash only visible elements + class.slots.hash.store(Some(struct_sequence_hash)); + + // Override richcompare slot to compare only visible elements + class + .slots + .richcompare + .store(Some(struct_sequence_richcompare)); } } + +/// Iterator function for struct sequences - returns only visible elements +fn struct_sequence_iter(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let tuple = zelf + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("expected tuple"))?; + let n_seq = get_visible_len(&zelf, vm)?; + let visible: Vec<_> = tuple.iter().take(n_seq).cloned().collect(); + let visible_tuple = PyTuple::new_ref(visible, &vm.ctx); + visible_tuple + .as_object() + .to_owned() + .get_iter(vm) + .map(Into::into) +} + +/// Hash function for struct sequences - hashes only visible elements +fn struct_sequence_hash( + zelf: &PyObject, + vm: &VirtualMachine, +) -> PyResult { + let tuple = zelf + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("expected tuple"))?; + let n_seq = get_visible_len(zelf, vm)?; + // Create a visible-only tuple and hash it + let visible: Vec<_> = tuple.iter().take(n_seq).cloned().collect(); + let visible_tuple = PyTuple::new_ref(visible, &vm.ctx); + visible_tuple.as_object().hash(vm) +} + +/// Rich comparison for struct sequences - compares only visible elements +fn struct_sequence_richcompare( + zelf: &PyObject, + other: &PyObject, + op: PyComparisonOp, + vm: &VirtualMachine, +) -> PyResult> { + let zelf_tuple = zelf + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("expected tuple"))?; + + // If other is not a tuple, return NotImplemented + let Some(other_tuple) = other.downcast_ref::() else { + return Ok(Either::B(PyComparisonValue::NotImplemented)); + }; + + let zelf_len = get_visible_len(zelf, vm)?; + // For other, try to get visible len; if it fails (not a struct sequence), use full length + let other_len = get_visible_len(other, vm).unwrap_or(other_tuple.len()); + + let zelf_visible = &zelf_tuple.as_slice()[..zelf_len]; + let other_visible = &other_tuple.as_slice()[..other_len]; + + // Use the same comparison logic as regular tuples + zelf_visible + .iter() + .richcompare(other_visible.iter(), op, vm) + .map(|v| Either::B(PyComparisonValue::Implemented(v))) +} diff --git a/crates/vm/src/vm/context.rs b/crates/vm/src/vm/context.rs index c75bfe18558..191c090f121 100644 --- a/crates/vm/src/vm/context.rs +++ b/crates/vm/src/vm/context.rs @@ -245,6 +245,9 @@ declare_const_name! { items, keys, modules, + n_fields, + n_sequence_fields, + n_unnamed_fields, namereplace, replace, strict, diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index b793a283525..e6217e3e35d 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -465,7 +465,7 @@ impl VirtualMachine { let exc_type = e.class().to_owned(); let exc_traceback = e.__traceback__().to_pyobject(self); // TODO: actual traceback let exc_value = e.into(); - let args = stdlib::sys::UnraisableHookArgs { + let args = stdlib::sys::UnraisableHookArgsData { exc_type, exc_value, exc_traceback,