@@ -305,6 +305,85 @@ pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Resul
305305 Ok ( tokens)
306306}
307307
308+ /// Validates that when a base class is specified, the struct has the base type as its first field.
309+ /// This ensures proper memory layout for subclassing (required for #[repr(transparent)] to work correctly).
310+ fn validate_base_field ( item : & Item , base_path : & syn:: Path ) -> Result < ( ) > {
311+ let Item :: Struct ( item_struct) = item else {
312+ // Only validate structs - enums with base are already an error elsewhere
313+ return Ok ( ( ) ) ;
314+ } ;
315+
316+ // Get the base type name for error messages
317+ let base_name = base_path
318+ . segments
319+ . last ( )
320+ . map ( |s| s. ident . to_string ( ) )
321+ . unwrap_or_else ( || quote ! ( #base_path) . to_string ( ) ) ;
322+
323+ match & item_struct. fields {
324+ syn:: Fields :: Named ( fields) => {
325+ let Some ( first_field) = fields. named . first ( ) else {
326+ bail_span ! (
327+ item_struct,
328+ "#[pyclass] with base = {base_name} requires the first field to be of type {base_name}, but the struct has no fields"
329+ ) ;
330+ } ;
331+ if !type_matches_path ( & first_field. ty , base_path) {
332+ bail_span ! (
333+ first_field,
334+ "#[pyclass] with base = {base_name} requires the first field to be of type {base_name}"
335+ ) ;
336+ }
337+ }
338+ syn:: Fields :: Unnamed ( fields) => {
339+ let Some ( first_field) = fields. unnamed . first ( ) else {
340+ bail_span ! (
341+ item_struct,
342+ "#[pyclass] with base = {base_name} requires the first field to be of type {base_name}, but the struct has no fields"
343+ ) ;
344+ } ;
345+ if !type_matches_path ( & first_field. ty , base_path) {
346+ bail_span ! (
347+ first_field,
348+ "#[pyclass] with base = {base_name} requires the first field to be of type {base_name}"
349+ ) ;
350+ }
351+ }
352+ syn:: Fields :: Unit => {
353+ bail_span ! (
354+ item_struct,
355+ "#[pyclass] with base = {base_name} requires the first field to be of type {base_name}, but the struct is a unit struct"
356+ ) ;
357+ }
358+ }
359+
360+ Ok ( ( ) )
361+ }
362+
363+ /// Check if a type matches a given path (handles simple cases like `Foo` or `path::to::Foo`)
364+ fn type_matches_path ( ty : & syn:: Type , path : & syn:: Path ) -> bool {
365+ // Compare by converting both to string representation for macro hygiene
366+ let ty_str = quote ! ( #ty) . to_string ( ) . replace ( ' ' , "" ) ;
367+ let path_str = quote ! ( #path) . to_string ( ) . replace ( ' ' , "" ) ;
368+
369+ // Check if both are the same or if the type ends with the path's last segment
370+ if ty_str == path_str {
371+ return true ;
372+ }
373+
374+ // Also match if just the last segment matches (e.g., foo::Bar matches Bar)
375+ let syn:: Type :: Path ( type_path) = ty else {
376+ return false ;
377+ } ;
378+ let Some ( type_last) = type_path. path . segments . last ( ) else {
379+ return false ;
380+ } ;
381+ let Some ( path_last) = path. segments . last ( ) else {
382+ return false ;
383+ } ;
384+ type_last. ident == path_last. ident
385+ }
386+
308387fn generate_class_def (
309388 ident : & Ident ,
310389 name : & str ,
@@ -339,7 +418,6 @@ fn generate_class_def(
339418 } else {
340419 quote ! ( false )
341420 } ;
342- let basicsize = quote ! ( std:: mem:: size_of:: <#ident>( ) ) ;
343421 let is_pystruct = attrs. iter ( ) . any ( |attr| {
344422 attr. path ( ) . is_ident ( "derive" )
345423 && if let Ok ( Meta :: List ( l) ) = attr. parse_meta ( ) {
@@ -350,6 +428,25 @@ fn generate_class_def(
350428 false
351429 }
352430 } ) ;
431+ // Check if the type has #[repr(transparent)] - only then we can safely
432+ // generate PySubclass impl (requires same memory layout as base type)
433+ let is_repr_transparent = attrs. iter ( ) . any ( |attr| {
434+ attr. path ( ) . is_ident ( "repr" )
435+ && if let Ok ( Meta :: List ( l) ) = attr. parse_meta ( ) {
436+ l. nested
437+ . into_iter ( )
438+ . any ( |n| n. get_ident ( ) . is_some_and ( |p| p == "transparent" ) )
439+ } else {
440+ false
441+ }
442+ } ) ;
443+ // If repr(transparent) with a base, the type has the same memory layout as base,
444+ // so basicsize should be 0 (no additional space beyond the base type)
445+ let basicsize = if is_repr_transparent && base. is_some ( ) {
446+ quote ! ( 0 )
447+ } else {
448+ quote ! ( std:: mem:: size_of:: <#ident>( ) )
449+ } ;
353450 if base. is_some ( ) && is_pystruct {
354451 bail_span ! ( ident, "PyStructSequence cannot have `base` class attr" , ) ;
355452 }
@@ -379,12 +476,31 @@ fn generate_class_def(
379476 }
380477 } ) ;
381478
382- let base_or_object = if let Some ( base) = base {
479+ let base_or_object = if let Some ( ref base) = base {
383480 quote ! { #base }
384481 } else {
385482 quote ! { :: rustpython_vm:: builtins:: PyBaseObject }
386483 } ;
387484
485+ // Generate PySubclass impl for #[repr(transparent)] types with base class
486+ // (tuple struct assumed, so &self.0 works)
487+ let subclass_impl = if !is_pystruct && is_repr_transparent {
488+ base. as_ref ( ) . map ( |typ| {
489+ quote ! {
490+ impl :: rustpython_vm:: class:: PySubclass for #ident {
491+ type Base = #typ;
492+
493+ #[ inline]
494+ fn as_base( & self ) -> & Self :: Base {
495+ & self . 0
496+ }
497+ }
498+ }
499+ } )
500+ } else {
501+ None
502+ } ;
503+
388504 let tokens = quote ! {
389505 impl :: rustpython_vm:: class:: PyClassDef for #ident {
390506 const NAME : & ' static str = #name;
@@ -409,6 +525,8 @@ fn generate_class_def(
409525
410526 #base_class
411527 }
528+
529+ #subclass_impl
412530 } ;
413531 Ok ( tokens)
414532}
@@ -426,11 +544,16 @@ pub(crate) fn impl_pyclass(attr: PunctuatedNestedMeta, item: Item) -> Result<Tok
426544 let metaclass = class_meta. metaclass ( ) ?;
427545 let unhashable = class_meta. unhashable ( ) ?;
428546
547+ // Validate that if base is specified, the first field must be of the base type
548+ if let Some ( ref base_path) = base {
549+ validate_base_field ( & item, base_path) ?;
550+ }
551+
429552 let class_def = generate_class_def (
430553 ident,
431554 & class_name,
432555 module_name. as_deref ( ) ,
433- base,
556+ base. clone ( ) ,
434557 metaclass,
435558 unhashable,
436559 attrs,
@@ -485,19 +608,47 @@ pub(crate) fn impl_pyclass(attr: PunctuatedNestedMeta, item: Item) -> Result<Tok
485608 }
486609 } ;
487610
488- let impl_payload = if let Some ( ctx_type_name) = class_meta. ctx_name ( ) ? {
489- let ctx_type_ident = Ident :: new ( & ctx_type_name, ident. span ( ) ) ; // FIXME span
611+ // Generate PyPayload impl based on whether base exists
612+ #[ allow( clippy:: collapsible_else_if) ]
613+ let impl_payload = if let Some ( base_type) = & base {
614+ let class_fn = if let Some ( ctx_type_name) = class_meta. ctx_name ( ) ? {
615+ let ctx_type_ident = Ident :: new ( & ctx_type_name, ident. span ( ) ) ;
616+ quote ! { ctx. types. #ctx_type_ident }
617+ } else {
618+ quote ! { <Self as :: rustpython_vm:: class:: StaticType >:: static_type( ) }
619+ } ;
490620
491- // We need this to make extend mechanism work:
492621 quote ! {
622+ // static_assertions::const_assert!(std::mem::size_of::<#base_type>() <= std::mem::size_of::<#ident>());
493623 impl :: rustpython_vm:: PyPayload for #ident {
624+ #[ inline]
625+ fn payload_type_id( ) -> :: std:: any:: TypeId {
626+ <#base_type as :: rustpython_vm:: PyPayload >:: payload_type_id( )
627+ }
628+
629+ #[ inline]
630+ fn validate_downcastable_from( obj: & :: rustpython_vm:: PyObject ) -> bool {
631+ <Self as :: rustpython_vm:: class:: PyClassDef >:: BASICSIZE <= obj. class( ) . slots. basicsize && obj. class( ) . fast_issubclass( <Self as :: rustpython_vm:: class:: StaticType >:: static_type( ) )
632+ }
633+
494634 fn class( ctx: & :: rustpython_vm:: vm:: Context ) -> & ' static :: rustpython_vm:: Py <:: rustpython_vm:: builtins:: PyType > {
495- ctx . types . #ctx_type_ident
635+ #class_fn
496636 }
497637 }
498638 }
499639 } else {
500- quote ! { }
640+ if let Some ( ctx_type_name) = class_meta. ctx_name ( ) ? {
641+ let ctx_type_ident = Ident :: new ( & ctx_type_name, ident. span ( ) ) ;
642+ quote ! {
643+ impl :: rustpython_vm:: PyPayload for #ident {
644+ fn class( ctx: & :: rustpython_vm:: vm:: Context ) -> & ' static :: rustpython_vm:: Py <:: rustpython_vm:: builtins:: PyType > {
645+ ctx. types. #ctx_type_ident
646+ }
647+ }
648+ }
649+ } else {
650+ quote ! { }
651+ }
501652 } ;
502653
503654 let empty_impl = if let Some ( attrs) = class_meta. impl_attrs ( ) ? {
@@ -536,26 +687,6 @@ pub(crate) fn impl_pyexception(attr: PunctuatedNestedMeta, item: Item) -> Result
536687 let class_name = class_meta. class_name ( ) ?;
537688
538689 let base_class_name = class_meta. base ( ) ?;
539- let impl_payload = if let Some ( ctx_type_name) = class_meta. ctx_name ( ) ? {
540- let ctx_type_ident = Ident :: new ( & ctx_type_name, ident. span ( ) ) ; // FIXME span
541-
542- // We need this to make extend mechanism work:
543- quote ! {
544- impl :: rustpython_vm:: PyPayload for #ident {
545- fn class( ctx: & :: rustpython_vm:: vm:: Context ) -> & ' static :: rustpython_vm:: Py <:: rustpython_vm:: builtins:: PyType > {
546- ctx. exceptions. #ctx_type_ident
547- }
548- }
549- }
550- } else {
551- quote ! {
552- impl :: rustpython_vm:: PyPayload for #ident {
553- fn class( _ctx: & :: rustpython_vm:: vm:: Context ) -> & ' static :: rustpython_vm:: Py <:: rustpython_vm:: builtins:: PyType > {
554- <Self as :: rustpython_vm:: class:: StaticType >:: static_type( )
555- }
556- }
557- }
558- } ;
559690 let impl_pyclass = if class_meta. has_impl ( ) ? {
560691 quote ! {
561692 #[ pyexception]
@@ -568,7 +699,6 @@ pub(crate) fn impl_pyexception(attr: PunctuatedNestedMeta, item: Item) -> Result
568699 let ret = quote ! {
569700 #[ pyclass( module = false , name = #class_name, base = #base_class_name) ]
570701 #item
571- #impl_payload
572702 #impl_pyclass
573703 } ;
574704 Ok ( ret)
@@ -585,7 +715,8 @@ pub(crate) fn impl_pyexception_impl(attr: PunctuatedNestedMeta, item: Item) -> R
585715 let mut extra_attrs = Vec :: new ( ) ;
586716 for nested in & attr {
587717 if let NestedMeta :: Meta ( Meta :: List ( MetaList { path, nested, .. } ) ) = nested {
588- if path. is_ident ( "with" ) {
718+ // If we already found the constructor trait, no need to keep looking for it
719+ if !has_slot_new && path. is_ident ( "with" ) {
589720 // Check if Constructor is in the list
590721 for meta in nested {
591722 if let NestedMeta :: Meta ( Meta :: Path ( p) ) = meta
@@ -1078,9 +1209,8 @@ impl GetSetNursery {
10781209 item_ident : Ident ,
10791210 ) -> Result < ( ) > {
10801211 assert ! ( !self . validated, "new item is not allowed after validation" ) ;
1081- if !matches ! ( kind, GetSetItemKind :: Get ) && !cfgs. is_empty ( ) {
1082- bail_span ! ( item_ident, "Only the getter can have #[cfg]" , ) ;
1083- }
1212+ // Note: Both getter and setter can have #[cfg], but they must have matching cfgs
1213+ // since the map key is (name, cfgs). This ensures getter and setter are paired correctly.
10841214 let entry = self . map . entry ( ( name. clone ( ) , cfgs) ) . or_default ( ) ;
10851215 let func = match kind {
10861216 GetSetItemKind :: Get => & mut entry. 0 ,
0 commit comments