From a84de5146f115ea888659aa2501739e2eaff3b57 Mon Sep 17 00:00:00 2001 From: Mohamad Alsadhan Date: Fri, 22 May 2026 21:00:16 +0300 Subject: [PATCH 1/4] internal: pin_data: support tuple struct projections Add plumbing for tuple struct support to `#[pin_data]` by generating tuple struct projections and tuple-aware pin-data metadata. Keep the projection API native to Rust tuple structs, so projected fields are accessed as `.0`, `.1`, etc. instead of introducing synthetic public field names. Signed-off-by: Mohamad Alsadhan --- CHANGELOG.md | 1 + internal/src/pin_data.rs | 357 ++++++++++++------ src/__internal.rs | 44 +++ .../tuple_struct_missing_pin_phantom.rs | 8 + .../tuple_struct_missing_pin_phantom.stderr | 13 + .../tuple_struct_pinned_mutex_not_unpin.rs | 17 + ...tuple_struct_pinned_mutex_not_unpin.stderr | 51 +++ tests/ui/expand/tuple_struct.expanded.rs | 105 ++++++ tests/ui/expand/tuple_struct.rs | 7 + 9 files changed, 491 insertions(+), 112 deletions(-) create mode 100644 tests/ui/compile-fail/pin_data/tuple_struct_missing_pin_phantom.rs create mode 100644 tests/ui/compile-fail/pin_data/tuple_struct_missing_pin_phantom.stderr create mode 100644 tests/ui/compile-fail/pin_data/tuple_struct_pinned_mutex_not_unpin.rs create mode 100644 tests/ui/compile-fail/pin_data/tuple_struct_pinned_mutex_not_unpin.stderr create mode 100644 tests/ui/expand/tuple_struct.expanded.rs create mode 100644 tests/ui/expand/tuple_struct.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9492a3e0..419b27f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Support tuple structs in `#[pin_data]`, including tuple struct projections. - `[pin_]init_scope` functions to run arbitrary code inside of an initializer. - `&'static mut MaybeUninit` now implements `InPlaceWrite`. This enables users to use external allocation mechanisms such as `static_cell`. diff --git a/internal/src/pin_data.rs b/internal/src/pin_data.rs index 2284256a..1834d894 100644 --- a/internal/src/pin_data.rs +++ b/internal/src/pin_data.rs @@ -7,7 +7,8 @@ use syn::{ parse_quote, parse_quote_spanned, spanned::Spanned, visit_mut::VisitMut, - Field, Generics, Ident, Item, PathSegment, Type, TypePath, Visibility, WhereClause, + Field, Fields, Generics, Ident, Index, Item, Member, PathSegment, Type, TypePath, Visibility, + WhereClause, }; use crate::diagnostics::{DiagCtxt, ErrorGuaranteed}; @@ -37,9 +38,24 @@ impl Parse for Args { struct FieldInfo<'a> { field: &'a Field, + member: Member, pinned: bool, } +fn member_ident(member: &Member) -> Ident { + match member { + Member::Named(ident) => ident.clone(), + Member::Unnamed(Index { index, .. }) => format_ident!("_{index}"), + } +} + +fn member_display_name(member: &Member) -> String { + match member { + Member::Named(ident) => format!("`{ident}`"), + Member::Unnamed(Index { index, .. }) => format!("index `{index}`"), + } +} + pub(crate) fn pin_data( args: Args, input: Item, @@ -78,30 +94,39 @@ pub(crate) fn pin_data( replacer.visit_generics_mut(&mut struct_.generics); replacer.visit_fields_mut(&mut struct_.fields); + let is_tuple_struct = matches!(struct_.fields, Fields::Unnamed(_)); let fields: Vec> = struct_ .fields .iter_mut() - .map(|field| { + .enumerate() + .map(|(index, field)| { let len = field.attrs.len(); field.attrs.retain(|a| !a.path().is_ident("pin")); let pinned = len != field.attrs.len(); + let member = match &field.ident { + Some(ident) => Member::Named(ident.clone()), + None => Member::Unnamed(Index { + index: index as u32, + span: field.span(), + }), + }; FieldInfo { field: &*field, + member, pinned, } }) .collect(); for field in &fields { - let ident = field.field.ident.as_ref().unwrap(); - if !field.pinned && is_phantom_pinned(&field.field.ty) { dcx.warn( field.field, format!( - "The field `{ident}` of type `PhantomPinned` only has an effect \ + "The field {} of type `PhantomPinned` only has an effect \ if it has the `#[pin]` attribute", + member_display_name(&field.member), ), ); } @@ -109,10 +134,20 @@ pub(crate) fn pin_data( let unpin_impl = generate_unpin_impl(&struct_.ident, &struct_.generics, &fields); let drop_impl = generate_drop_impl(&struct_.ident, &struct_.generics, args); - let projections = - generate_projections(&struct_.vis, &struct_.ident, &struct_.generics, &fields); - let the_pin_data = - generate_the_pin_data(&struct_.vis, &struct_.ident, &struct_.generics, &fields); + let projections = generate_projections( + &struct_.vis, + &struct_.ident, + &struct_.generics, + is_tuple_struct, + &fields, + ); + let the_pin_data = generate_the_pin_data( + &struct_.vis, + &struct_.ident, + &struct_.generics, + is_tuple_struct, + &fields, + ); Ok(quote! { #struct_ @@ -171,7 +206,14 @@ fn generate_unpin_impl( else { unreachable!() }; - let pinned_fields = fields.iter().filter(|f| f.pinned).map(|f| f.field); + let pinned_fields = fields.iter().filter(|f| f.pinned).map(|f| { + let Field { attrs, vis, ty, .. } = f.field; + let ident = member_ident(&f.member); + quote!( + #(#attrs)* + #vis #ident: #ty + ) + }); quote! { // This struct will be used for the unpin analysis. It is needed, because only structurally // pinned fields are relevant whether the struct should implement `Unpin`. @@ -247,6 +289,7 @@ fn generate_projections( vis: &Visibility, ident: &Ident, generics: &Generics, + is_tuple_struct: bool, fields: &[FieldInfo<'_>], ) -> TokenStream { let (impl_generics, ty_generics, _) = generics.split_for_impl(); @@ -256,68 +299,121 @@ fn generate_projections( let projection = format_ident!("{ident}Projection"); let this = format_ident!("this"); - let (fields_decl, fields_proj): (Vec<_>, Vec<_>) = fields - .iter() - .map(|field| { - let Field { - vis, - ident, - ty, - attrs, - .. - } = &field.field; - - let mut no_doc_attrs = attrs.clone(); - no_doc_attrs.retain(|a| !a.path().is_ident("doc")); - let ident = ident - .as_ref() - .expect("only structs with named fields are supported"); - if field.pinned { - ( - quote!( - #(#attrs)* - #vis #ident: ::core::pin::Pin<&'__pin mut #ty>, - ), - quote!( - #(#no_doc_attrs)* - // SAFETY: this field is structurally pinned. - #ident: unsafe { ::core::pin::Pin::new_unchecked(&mut #this.#ident) }, - ), - ) - } else { - ( - quote!( - #(#attrs)* - #vis #ident: &'__pin mut #ty, - ), - quote!( - #(#no_doc_attrs)* - #ident: &mut #this.#ident, - ), - ) - } - }) - .collect(); let structurally_pinned_fields_docs = fields .iter() .filter(|f| f.pinned) - .map(|f| format!(" - `{}`", f.field.ident.as_ref().unwrap())); + .map(|f| format!(" - {}", member_display_name(&f.member))); let not_structurally_pinned_fields_docs = fields .iter() .filter(|f| !f.pinned) - .map(|f| format!(" - `{}`", f.field.ident.as_ref().unwrap())); + .map(|f| format!(" - {}", member_display_name(&f.member))); let docs = format!(" Pin-projections of [`{ident}`]"); - quote! { - #[doc = #docs] - #[allow(dead_code)] - #[doc(hidden)] - #vis struct #projection #generics_with_pin_lt - #whr - { - #(#fields_decl)* - ___pin_phantom_data: ::core::marker::PhantomData<&'__pin mut ()>, + + let (projection_def, projection_init) = if !is_tuple_struct { + let (fields_decl, fields_proj): (Vec<_>, Vec<_>) = fields + .iter() + .map(|field| { + let Field { vis, ty, attrs, .. } = &field.field; + let ident = member_ident(&field.member); + let member = &field.member; + + let mut no_doc_attrs = attrs.clone(); + no_doc_attrs.retain(|a| !a.path().is_ident("doc")); + if field.pinned { + ( + quote!( + #(#attrs)* + #vis #ident: ::core::pin::Pin<&'__pin mut #ty>, + ), + quote!( + #(#no_doc_attrs)* + // SAFETY: this field is structurally pinned. + #ident: unsafe { ::core::pin::Pin::new_unchecked(&mut #this.#member) }, + ), + ) + } else { + ( + quote!( + #(#attrs)* + #vis #ident: &'__pin mut #ty, + ), + quote!( + #(#no_doc_attrs)* + #ident: &mut #this.#member, + ), + ) + } + }) + .collect(); + ( + quote! { + #[doc = #docs] + #[allow(dead_code)] + #[doc(hidden)] + #vis struct #projection #generics_with_pin_lt + #whr + { + #(#fields_decl)* + ___pin_phantom_data: ::core::marker::PhantomData<&'__pin mut ()>, + } + }, + quote! { + #projection { + #(#fields_proj)* + ___pin_phantom_data: ::core::marker::PhantomData, + } + }, + ) + } else { + let mut fields_decl = Vec::new(); + let mut field_bindings = Vec::new(); + let mut field_projections = Vec::new(); + for (index, field) in fields.iter().enumerate() { + let Field { vis, ty, attrs, .. } = &field.field; + let binding = format_ident!("__field_{index}"); + + if field.pinned { + fields_decl.push(quote!( + #(#attrs)* + #vis ::core::pin::Pin<&'__pin mut #ty>, + )); + field_projections.push(quote!( + // SAFETY: this field is structurally pinned. + unsafe { ::core::pin::Pin::new_unchecked(#binding) }, + )); + } else { + fields_decl.push(quote!( + #(#attrs)* + #vis &'__pin mut #ty, + )); + field_projections.push(quote!(#binding,)); + } + field_bindings.push(quote!(ref mut #binding,)); } + ( + quote! { + #[doc = #docs] + #[allow(dead_code)] + #[doc(hidden)] + #vis struct #projection #generics_with_pin_lt ( + #(#fields_decl)* + ::core::marker::PhantomData<&'__pin mut ()>, + ) #whr; + }, + quote! {{ + let #ident(#(#field_bindings)*) = *#this; + #projection( + #(#field_projections)* + ::core::marker::PhantomData, + ) + }}, + ) + }; + + quote! { + #projection_def + impl #impl_generics #ident #ty_generics #whr { @@ -334,10 +430,7 @@ fn generate_projections( ) -> #projection #ty_generics_with_pin_lt { // SAFETY: we only give access to `&mut` for fields not structurally pinned. let #this = unsafe { ::core::pin::Pin::get_unchecked_mut(self) }; - #projection { - #(#fields_proj)* - ___pin_phantom_data: ::core::marker::PhantomData, - } + #projection_init } } } @@ -347,64 +440,104 @@ fn generate_the_pin_data( vis: &Visibility, struct_name: &Ident, generics: &Generics, + is_tuple_struct: bool, fields: &[FieldInfo<'_>], ) -> TokenStream { let (impl_generics, ty_generics, whr) = generics.split_for_impl(); - // For every field, we create an initializing projection function according to its projection - // type. If a field is structurally pinned, we create a `Slot` with `Pinned` which must be - // initialized via `PinInit`; if it is not structurally pinned, then we create a `Slot` with - // `Unpinned` which allows initialization via `Init`. - let field_accessors = fields - .iter() - .map(|f| { - let Field { - vis, - ident, - ty, - attrs, - .. - } = f.field; - - let field_name = ident - .as_ref() - .expect("only structs with named fields are supported"); + let (pin_data_struct, pin_data_new, field_accessors) = if !is_tuple_struct { + // For every field, we create an initializing projection function according to its + // projection type. If a field is structurally pinned, we create a `Slot` with `Pinned` + // which must be initialized via `PinInit`; if it is not structurally pinned, then we + // create a `Slot` with `Unpinned` which allows initialization via `Init`. + let field_accessors = fields + .iter() + .map(|f| { + let Field { vis, ty, attrs, .. } = f.field; + let field_name = member_ident(&f.member); + let member = &f.member; + let pin_marker = if f.pinned { + quote!(Pinned) + } else { + quote!(Unpinned) + }; + quote! { + /// # Safety + /// + /// - `slot` is valid and properly aligned. + /// - `(*slot).#field_name` is properly aligned. + /// - `(*slot).#field_name` points to uninitialized and exclusively accessed + /// memory. + #(#attrs)* + #[inline(always)] + #vis unsafe fn #field_name( + self, + slot: *mut #struct_name #ty_generics, + ) -> ::pin_init::__internal::Slot<::pin_init::__internal::#pin_marker, #ty> { + // SAFETY: + // - If `#pin_marker` is `Pinned`, the corresponding field is structurally + // pinned. + // - Other safety requirements follows the safety requirement. + unsafe { ::pin_init::__internal::Slot::new(&raw mut (*slot).#member) } + } + } + }) + .collect::(); + ( + quote! { + #vis struct __ThePinData #generics + #whr + { + __phantom: ::pin_init::__internal::PhantomInvariant<#struct_name #ty_generics>, + } + }, + quote! { + __ThePinData { __phantom: ::pin_init::__internal::PhantomInvariant::new() } + }, + field_accessors, + ) + } else { + let mut field_data = Vec::new(); + let mut field_values = Vec::new(); + for f in fields { + let Field { vis, ty, attrs, .. } = f.field; let pin_marker = if f.pinned { quote!(Pinned) } else { quote!(Unpinned) }; - quote! { - /// # Safety - /// - /// - `slot` is valid and properly aligned. - /// - `(*slot).#field_name` is properly aligned. - /// - `(*slot).#field_name` points to uninitialized and exclusively accessed - /// memory. + field_data.push(quote! { #(#attrs)* - #[inline(always)] - #vis unsafe fn #field_name( - self, - slot: *mut #struct_name #ty_generics, - ) -> ::pin_init::__internal::Slot<::pin_init::__internal::#pin_marker, #ty> { - // SAFETY: - // - If `#pin_marker` is `Pinned`, the corresponding field is structurally - // pinned. - // - Other safety requirements follows the safety requirement. - unsafe { ::pin_init::__internal::Slot::new(&raw mut (*slot).#field_name) } - } - } - }) - .collect::(); + #vis ::pin_init::__internal::FieldData< + ::pin_init::__internal::#pin_marker, + #ty, + >, + }); + field_values.push(quote! { + #(#attrs)* + ::pin_init::__internal::FieldData::new(), + }); + } + ( + quote! { + #vis struct __ThePinData #generics ( + #(#field_data)* + ) #whr; + }, + quote! { + __ThePinData( + #(#field_values)* + ) + }, + TokenStream::new(), + ) + }; + quote! { // We declare this struct which will host all of the projection function for our type. It // will be invariant over all generic parameters which are inherited from the struct. #[doc(hidden)] - #vis struct __ThePinData #generics - #whr - { - __phantom: ::pin_init::__internal::PhantomInvariant<#struct_name #ty_generics>, - } + #pin_data_struct impl #impl_generics ::core::clone::Clone for __ThePinData #ty_generics #whr @@ -442,7 +575,7 @@ fn generate_the_pin_data( type PinData = __ThePinData #ty_generics; unsafe fn __pin_data() -> Self::PinData { - __ThePinData { __phantom: ::pin_init::__internal::PhantomInvariant::new() } + #pin_data_new } } } diff --git a/src/__internal.rs b/src/__internal.rs index 540add6c..491a8beb 100644 --- a/src/__internal.rs +++ b/src/__internal.rs @@ -262,6 +262,50 @@ pub struct Slot { _phantom: PhantomData

, } +/// Type-level metadata for a tuple field. +/// +/// This is used by generated tuple-struct pin data in place of a named accessor method. +pub struct FieldData { + _phantom_pin: PhantomData

, + _phantom_ty: PhantomInvariant, +} + +impl FieldData { + #[inline(always)] + pub const fn new() -> Self { + Self { + _phantom_pin: PhantomData, + _phantom_ty: PhantomInvariant::new(), + } + } + + /// # Safety + /// + /// - `ptr` is valid, properly aligned and points to uninitialized and exclusively accessed + /// memory. + /// - If `P` is [`Pinned`], then `ptr` is structurally pinned. + #[inline(always)] + pub unsafe fn slot(self, ptr: *mut T) -> Slot { + // SAFETY: The caller upholds `Slot::new`'s requirements. + unsafe { Slot::new(ptr) } + } +} + +impl Clone for FieldData { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for FieldData {} + +impl Default for FieldData { + #[inline(always)] + fn default() -> Self { + Self::new() + } +} + impl Slot { /// # Safety /// diff --git a/tests/ui/compile-fail/pin_data/tuple_struct_missing_pin_phantom.rs b/tests/ui/compile-fail/pin_data/tuple_struct_missing_pin_phantom.rs new file mode 100644 index 00000000..bdba06c3 --- /dev/null +++ b/tests/ui/compile-fail/pin_data/tuple_struct_missing_pin_phantom.rs @@ -0,0 +1,8 @@ +#![deny(warnings)] + +use pin_init::*; + +#[pin_data] +struct Tuple(T, core::marker::PhantomPinned); + +fn main() {} diff --git a/tests/ui/compile-fail/pin_data/tuple_struct_missing_pin_phantom.stderr b/tests/ui/compile-fail/pin_data/tuple_struct_missing_pin_phantom.stderr new file mode 100644 index 00000000..7dfbfdab --- /dev/null +++ b/tests/ui/compile-fail/pin_data/tuple_struct_missing_pin_phantom.stderr @@ -0,0 +1,13 @@ +error: use of deprecated function `_::warn`: + The field index `1` of type `PhantomPinned` only has an effect if it has the `#[pin]` attribute + --> tests/ui/compile-fail/pin_data/tuple_struct_missing_pin_phantom.rs:6:20 + | +6 | struct Tuple(T, core::marker::PhantomPinned); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +note: the lint level is defined here + --> tests/ui/compile-fail/pin_data/tuple_struct_missing_pin_phantom.rs:1:9 + | +1 | #![deny(warnings)] + | ^^^^^^^^ + = note: `#[deny(deprecated)]` implied by `#[deny(warnings)]` diff --git a/tests/ui/compile-fail/pin_data/tuple_struct_pinned_mutex_not_unpin.rs b/tests/ui/compile-fail/pin_data/tuple_struct_pinned_mutex_not_unpin.rs new file mode 100644 index 00000000..4edf2250 --- /dev/null +++ b/tests/ui/compile-fail/pin_data/tuple_struct_pinned_mutex_not_unpin.rs @@ -0,0 +1,17 @@ +#![cfg_attr(feature = "alloc", feature(allocator_api))] + +use pin_init::*; + +#[allow(unused_attributes)] +#[path = "../../../../examples/mutex.rs"] +mod mutex; +use mutex::CMutex; + +#[pin_data] +struct Tuple(#[pin] CMutex, usize); + +fn assert_unpin() {} + +fn main() { + assert_unpin::>(); +} diff --git a/tests/ui/compile-fail/pin_data/tuple_struct_pinned_mutex_not_unpin.stderr b/tests/ui/compile-fail/pin_data/tuple_struct_pinned_mutex_not_unpin.stderr new file mode 100644 index 00000000..2b240c75 --- /dev/null +++ b/tests/ui/compile-fail/pin_data/tuple_struct_pinned_mutex_not_unpin.stderr @@ -0,0 +1,51 @@ +error[E0277]: `PhantomPinned` cannot be unpinned + --> tests/ui/compile-fail/pin_data/tuple_struct_pinned_mutex_not_unpin.rs:16:20 + | +16 | assert_unpin::>(); + | ^^^^^^^^^^^^ within `mutex::linked_list::_::__Unpin<'_>`, the trait `Unpin` is not implemented for `PhantomPinned` + | + = note: consider using the `pin!` macro + consider using `Box::pin` if you need to access the pinned value outside of the current scope +note: required because it appears within the type `mutex::linked_list::_::__Unpin<'_>` + --> tests/ui/compile-fail/pin_data/../../../../examples/./linked_list.rs + | + | #[pin_data(PinnedDrop)] + | ^^^^^^^^^^^^^^^^^^^^^^^ +note: required for `mutex::linked_list::ListHead` to implement `Unpin` + --> tests/ui/compile-fail/pin_data/../../../../examples/./linked_list.rs + | + | #[pin_data(PinnedDrop)] + | ^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound introduced here +... + | pub struct ListHead { + | ^^^^^^^^ +note: required because it appears within the type `mutex::_::__Unpin<'_, usize>` + --> tests/ui/compile-fail/pin_data/../../../../examples/mutex.rs + | + | #[pin_data] + | ^^^^^^^^^^^ +note: required for `CMutex` to implement `Unpin` + --> tests/ui/compile-fail/pin_data/../../../../examples/mutex.rs + | + | #[pin_data] + | ^^^^^^^^^^^ unsatisfied trait bound introduced here + | pub struct CMutex { + | ^^^^^^^^^ +note: required because it appears within the type `_::__Unpin<'_, usize>` + --> tests/ui/compile-fail/pin_data/tuple_struct_pinned_mutex_not_unpin.rs:10:1 + | +10 | #[pin_data] + | ^^^^^^^^^^^ +note: required for `Tuple` to implement `Unpin` + --> tests/ui/compile-fail/pin_data/tuple_struct_pinned_mutex_not_unpin.rs:10:1 + | +10 | #[pin_data] + | ^^^^^^^^^^^ unsatisfied trait bound introduced here +11 | struct Tuple(#[pin] CMutex, usize); + | ^^^^^^^^ +note: required by a bound in `assert_unpin` + --> tests/ui/compile-fail/pin_data/tuple_struct_pinned_mutex_not_unpin.rs:13:20 + | +13 | fn assert_unpin() {} + | ^^^^^ required by this bound in `assert_unpin` + = note: this error originates in the attribute macro `pin_data` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/expand/tuple_struct.expanded.rs b/tests/ui/expand/tuple_struct.expanded.rs new file mode 100644 index 00000000..08068064 --- /dev/null +++ b/tests/ui/expand/tuple_struct.expanded.rs @@ -0,0 +1,105 @@ +use core::marker::PhantomPinned; +use pin_init::*; +struct Foo<'a, T: Copy, const N: usize>(&'a mut [T; N], PhantomPinned, usize); +/// Pin-projections of [`Foo`] +#[allow(dead_code)] +#[doc(hidden)] +struct FooProjection<'__pin, 'a, T: Copy, const N: usize>( + &'__pin mut &'a mut [T; N], + ::core::pin::Pin<&'__pin mut PhantomPinned>, + &'__pin mut usize, + ::core::marker::PhantomData<&'__pin mut ()>, +); +impl<'a, T: Copy, const N: usize> Foo<'a, T, N> { + /// Pin-projects all fields of `Self`. + /// + /// These fields are structurally pinned: + /// - index `1` + /// + /// These fields are **not** structurally pinned: + /// - index `0` + /// - index `2` + #[inline] + fn project<'__pin>( + self: ::core::pin::Pin<&'__pin mut Self>, + ) -> FooProjection<'__pin, 'a, T, N> { + let this = unsafe { ::core::pin::Pin::get_unchecked_mut(self) }; + { + let Foo(ref mut __field_0, ref mut __field_1, ref mut __field_2) = *this; + FooProjection( + __field_0, + unsafe { ::core::pin::Pin::new_unchecked(__field_1) }, + __field_2, + ::core::marker::PhantomData, + ) + } + } +} +const _: () = { + #[doc(hidden)] + struct __ThePinData<'a, T: Copy, const N: usize>( + ::pin_init::__internal::FieldData< + ::pin_init::__internal::Unpinned, + &'a mut [T; N], + >, + ::pin_init::__internal::FieldData<::pin_init::__internal::Pinned, PhantomPinned>, + ::pin_init::__internal::FieldData<::pin_init::__internal::Unpinned, usize>, + ); + impl<'a, T: Copy, const N: usize> ::core::clone::Clone for __ThePinData<'a, T, N> { + fn clone(&self) -> Self { + *self + } + } + impl<'a, T: Copy, const N: usize> ::core::marker::Copy for __ThePinData<'a, T, N> {} + #[allow(dead_code)] + #[expect(clippy::missing_safety_doc)] + impl<'a, T: Copy, const N: usize> __ThePinData<'a, T, N> { + /// Type inference helper function. + #[inline(always)] + fn __make_closure<__F, __E>(self, f: __F) -> __F + where + __F: FnOnce( + *mut Foo<'a, T, N>, + ) -> ::core::result::Result<::pin_init::__internal::InitOk, __E>, + { + f + } + } + unsafe impl<'a, T: Copy, const N: usize> ::pin_init::__internal::HasPinData + for Foo<'a, T, N> { + type PinData = __ThePinData<'a, T, N>; + unsafe fn __pin_data() -> Self::PinData { + __ThePinData( + ::pin_init::__internal::FieldData::new(), + ::pin_init::__internal::FieldData::new(), + ::pin_init::__internal::FieldData::new(), + ) + } + } + #[allow(dead_code)] + struct __Unpin<'__pin, 'a, T: Copy, const N: usize> { + __phantom_pin: ::pin_init::__internal::PhantomInvariantLifetime<'__pin>, + __phantom: ::pin_init::__internal::PhantomInvariant>, + _1: PhantomPinned, + } + #[doc(hidden)] + impl<'__pin, 'a, T: Copy, const N: usize> ::core::marker::Unpin for Foo<'a, T, N> + where + __Unpin<'__pin, 'a, T, N>: ::core::marker::Unpin, + {} + trait MustNotImplDrop {} + #[expect(drop_bounds)] + impl MustNotImplDrop for T {} + impl<'a, T: Copy, const N: usize> MustNotImplDrop for Foo<'a, T, N> {} + #[expect(non_camel_case_types)] + trait UselessPinnedDropImpl_you_need_to_specify_PinnedDrop {} + impl< + T: ::pin_init::PinnedDrop + ?::core::marker::Sized, + > UselessPinnedDropImpl_you_need_to_specify_PinnedDrop for T {} + impl< + 'a, + T: Copy, + const N: usize, + > UselessPinnedDropImpl_you_need_to_specify_PinnedDrop for Foo<'a, T, N> {} +}; +fn main() {} diff --git a/tests/ui/expand/tuple_struct.rs b/tests/ui/expand/tuple_struct.rs new file mode 100644 index 00000000..d81daa1e --- /dev/null +++ b/tests/ui/expand/tuple_struct.rs @@ -0,0 +1,7 @@ +use core::marker::PhantomPinned; +use pin_init::*; + +#[pin_data] +struct Foo<'a, T: Copy, const N: usize>(&'a mut [T; N], #[pin] PhantomPinned, usize); + +fn main() {} From 47a91b5947012122e6c0f911bed9f8e1d07d4a6d Mon Sep 17 00:00:00 2001 From: Mohamad Alsadhan Date: Fri, 22 May 2026 21:03:59 +0300 Subject: [PATCH 2/4] internal: init: add tuple struct field and constructor syntax Expand `init!` and `pin_init!` to initialize tuple structs using both indexed brace syntax and tuple-constructor syntax. Add tuple-specific runtime and UI coverage for the basic initializer forms, duplicate/missing fields, invalid indices, and syntax errors. Signed-off-by: Mohamad Alsadhan --- CHANGELOG.md | 4 +- internal/src/init.rs | 229 +++++++++++++----- tests/tuple_struct.rs | 67 +++++ .../compile-fail/init/no_tuple_paren_arrow.rs | 8 + .../init/no_tuple_paren_arrow.stderr | 5 + .../compile-fail/init/no_tuple_shorthand.rs | 8 + .../init/no_tuple_shorthand.stderr | 5 + .../init/no_tuple_syntax_mixing.rs | 8 + .../init/no_tuple_syntax_mixing.stderr | 5 + .../init/tuple_duplicate_field.rs | 8 + .../init/tuple_duplicate_field.stderr | 8 + .../compile-fail/init/tuple_invalid_field.rs | 8 + .../init/tuple_invalid_field.stderr | 30 +++ .../compile-fail/init/tuple_missing_field.rs | 9 + .../init/tuple_missing_field.stderr | 11 + tests/ui/expand/tuple_struct.expanded.rs | 119 ++++++++- tests/ui/expand/tuple_struct.rs | 12 +- 17 files changed, 480 insertions(+), 64 deletions(-) create mode 100644 tests/tuple_struct.rs create mode 100644 tests/ui/compile-fail/init/no_tuple_paren_arrow.rs create mode 100644 tests/ui/compile-fail/init/no_tuple_paren_arrow.stderr create mode 100644 tests/ui/compile-fail/init/no_tuple_shorthand.rs create mode 100644 tests/ui/compile-fail/init/no_tuple_shorthand.stderr create mode 100644 tests/ui/compile-fail/init/no_tuple_syntax_mixing.rs create mode 100644 tests/ui/compile-fail/init/no_tuple_syntax_mixing.stderr create mode 100644 tests/ui/compile-fail/init/tuple_duplicate_field.rs create mode 100644 tests/ui/compile-fail/init/tuple_duplicate_field.stderr create mode 100644 tests/ui/compile-fail/init/tuple_invalid_field.rs create mode 100644 tests/ui/compile-fail/init/tuple_invalid_field.stderr create mode 100644 tests/ui/compile-fail/init/tuple_missing_field.rs create mode 100644 tests/ui/compile-fail/init/tuple_missing_field.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index 419b27f8..98e7772b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - -- Support tuple structs in `#[pin_data]`, including tuple struct projections. +- Support tuple structs in `#[pin_data]`, including tuple struct projections, and tuple struct + initialization in `init!` and `pin_init!`. - `[pin_]init_scope` functions to run arbitrary code inside of an initializer. - `&'static mut MaybeUninit` now implements `InPlaceWrite`. This enables users to use external allocation mechanisms such as `static_cell`. diff --git a/internal/src/init.rs b/internal/src/init.rs index 699b1055..b65c52fc 100644 --- a/internal/src/init.rs +++ b/internal/src/init.rs @@ -3,12 +3,13 @@ use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote}; use syn::{ - braced, + braced, parenthesized, parse::{End, Parse}, parse_quote, punctuated::Punctuated, spanned::Spanned, - token, Attribute, Block, Expr, ExprCall, ExprPath, Ident, Path, Token, Type, + token, Attribute, Block, Expr, ExprCall, ExprPath, Ident, Index, LitInt, Member, Path, Token, + Type, }; use crate::diagnostics::{DiagCtxt, ErrorGuaranteed}; @@ -17,7 +18,7 @@ pub(crate) struct Initializer { attrs: Vec, this: Option, path: Path, - brace_token: token::Brace, + close_span: Span, fields: Punctuated, rest: Option<(Token![..], Expr)>, error: Option<(Token![?], Type)>, @@ -34,13 +35,18 @@ struct InitializerField { kind: InitializerKind, } +struct TupleInitializerField { + attrs: Vec, + value: Expr, +} + enum InitializerKind { Value { - ident: Ident, + member: Member, value: Option<(Token![:], Expr)>, }, Init { - ident: Ident, + member: Member, _left_arrow_token: Token![<-], value: Expr, }, @@ -52,14 +58,28 @@ enum InitializerKind { } impl InitializerKind { - fn ident(&self) -> Option<&Ident> { + fn member(&self) -> Option<&Member> { match self { - Self::Value { ident, .. } | Self::Init { ident, .. } => Some(ident), + Self::Value { member, .. } | Self::Init { member, .. } => Some(member), Self::Code { .. } => None, } } } +fn member_ident(member: &Member) -> Ident { + match member { + Member::Named(ident) => ident.clone(), + Member::Unnamed(Index { index, .. }) => format_ident!("_{index}"), + } +} + +fn member_binding(member: &Member) -> Option { + match member { + Member::Named(ident) => Some(ident.clone()), + Member::Unnamed(_) => None, + } +} + enum InitializerAttribute { DefaultError(DefaultErrorAttribute), } @@ -73,7 +93,7 @@ pub(crate) fn expand( attrs, this, path, - brace_token, + close_span, fields, rest, error, @@ -96,7 +116,7 @@ pub(crate) fn expand( } else if let Some(default_error) = default_error { syn::parse_str(default_error).unwrap() } else { - dcx.error(brace_token.span.close(), "expected `? ` after `}`"); + dcx.error(close_span, "expected `? ` after initializer"); parse_quote!(::core::convert::Infallible) } }, @@ -229,9 +249,9 @@ fn init_fields( cfgs }; - let ident = match kind { - InitializerKind::Value { ident, .. } => ident, - InitializerKind::Init { ident, .. } => ident, + let member = match kind { + InitializerKind::Value { member, .. } => member, + InitializerKind::Init { member, .. } => member, InitializerKind::Code { block, .. } => { res.extend(quote! { #(#attrs)* @@ -241,27 +261,38 @@ fn init_fields( continue; } }; + let ident = member_ident(member); let slot = if pinned { - quote! { - // SAFETY: - // - `slot` is valid and properly aligned. - // - `make_field_check` checks that `&raw mut (*slot).#ident` is properly aligned. - // - `make_field_check` prevents `#ident` from being used twice, therefore - // `(*slot).#ident` is exclusively accessed and has not been initialized. - (unsafe { #data.#ident(#slot) }) + match member { + Member::Named(_) => quote! { + // SAFETY: + // - `slot` is valid and properly aligned. + // - `make_field_check` checks that the field is properly aligned. + // - `make_field_check` prevents the field from being used twice, therefore + // it is exclusively accessed and has not been initialized. + (unsafe { #data.#ident(#slot) }) + }, + Member::Unnamed(_) => quote! { + // SAFETY: + // - `slot` is valid and properly aligned. + // - `make_field_check` checks that the field is properly aligned. + // - `make_field_check` prevents the field from being used twice, therefore + // it is exclusively accessed and has not been initialized. + (unsafe { #data.#member.slot(&raw mut (*#slot).#member) }) + }, } } else { quote! { // For `init!()` macro, everything is unpinned. // SAFETY: - // - `&raw mut (*slot).#ident` is valid. - // - `make_field_check` checks that `&raw mut (*slot).#ident` is properly aligned. - // - `make_field_check` prevents `#ident` from being used twice, therefore - // `(*slot).#ident` is exclusively accessed and has not been initialized. + // - The field pointer is valid. + // - `make_field_check` checks that the field is properly aligned. + // - `make_field_check` prevents the field from being used twice, therefore + // it is exclusively accessed and has not been initialized. (unsafe { ::pin_init::__internal::Slot::<::pin_init::__internal::Unpinned, _>::new( - &raw mut (*#slot).#ident + &raw mut (*#slot).#member ) }) } @@ -271,7 +302,7 @@ fn init_fields( let guard = format_ident!("__{ident}_guard", span = Span::mixed_site()); let init = match kind { - InitializerKind::Value { ident, value } => { + InitializerKind::Value { value, .. } => { let value = value .as_ref() .map(|(_, value)| quote!(#value)) @@ -291,13 +322,18 @@ fn init_fields( } InitializerKind::Code { .. } => unreachable!(), }; + let binding = member_binding(member).map(|ident| { + quote! { + #(#cfgs)* + #[allow(unused_variables)] + let #ident = #guard.let_binding(); + } + }); res.extend(quote! { #init - #(#cfgs)* - #[allow(unused_variables)] - let #ident = #guard.let_binding(); + #binding }); guards.push(guard); @@ -322,9 +358,9 @@ fn make_field_check( ) -> TokenStream { let field_attrs: Vec<_> = fields .iter() - .filter_map(|f| f.kind.ident().map(|_| &f.attrs)) + .filter_map(|f| f.kind.member().map(|_| &f.attrs)) .collect(); - let field_name: Vec<_> = fields.iter().filter_map(|f| f.kind.ident()).collect(); + let field_name: Vec<_> = fields.iter().filter_map(|f| f.kind.member()).collect(); let zeroing_trailer = match init_kind { InitKind::Normal => None, InitKind::Zeroing => Some(quote! { @@ -360,36 +396,73 @@ fn make_field_check( } } -impl Parse for Initializer { - fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { - let attrs = input.call(Attribute::parse_outer)?; - let this = input.peek(Token![&]).then(|| input.parse()).transpose()?; - let path = input.parse()?; - let content; - let brace_token = braced!(content in input); - let mut fields = Punctuated::new(); - loop { +type InitFields = Punctuated; +type InitRest = Option<(Token![..], Expr)>; + +fn parse_brace_initializer( + input: syn::parse::ParseStream<'_>, +) -> syn::Result<(Span, InitFields, InitRest)> { + let content; + let brace_token = braced!(content in input); + let mut fields = Punctuated::new(); + loop { + let lh = content.lookahead1(); + if lh.peek(End) || lh.peek(Token![..]) { + break; + } else if lh.peek(Ident) || lh.peek(LitInt) || lh.peek(Token![_]) || lh.peek(Token![#]) { + fields.push_value(content.parse()?); let lh = content.lookahead1(); - if lh.peek(End) || lh.peek(Token![..]) { + if lh.peek(End) { break; - } else if lh.peek(Ident) || lh.peek(Token![_]) || lh.peek(Token![#]) { - fields.push_value(content.parse()?); - let lh = content.lookahead1(); - if lh.peek(End) { - break; - } else if lh.peek(Token![,]) { - fields.push_punct(content.parse()?); - } else { - return Err(lh.error()); - } + } else if lh.peek(Token![,]) { + fields.push_punct(content.parse()?); } else { return Err(lh.error()); } + } else { + return Err(lh.error()); } - let rest = content - .peek(Token![..]) - .then(|| Ok::<_, syn::Error>((content.parse()?, content.parse()?))) - .transpose()?; + } + let rest = content + .peek(Token![..]) + .then(|| Ok::<_, syn::Error>((content.parse()?, content.parse()?))) + .transpose()?; + Ok((brace_token.span.close(), fields, rest)) +} + +fn parse_paren_initializer(input: syn::parse::ParseStream<'_>) -> syn::Result<(Span, InitFields)> { + let content; + let paren_token = parenthesized!(content in input); + let tuple_fields = content.parse_terminated(TupleInitializerField::parse, Token![,])?; + let mut fields = Punctuated::new(); + for (index, tuple_field) in tuple_fields.into_iter().enumerate() { + fields.push(InitializerField { + attrs: tuple_field.attrs, + kind: InitializerKind::Value { + member: Member::Unnamed(Index { + index: index as u32, + span: tuple_field.value.span(), + }), + value: Some((Token![:](tuple_field.value.span()), tuple_field.value)), + }, + }); + } + Ok((paren_token.span.close(), fields)) +} + +impl Parse for Initializer { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let attrs = input.call(Attribute::parse_outer)?; + let this = input.peek(Token![&]).then(|| input.parse()).transpose()?; + let path = input.parse()?; + let (close_span, fields, rest) = if input.peek(token::Brace) { + parse_brace_initializer(input)? + } else if input.peek(token::Paren) { + let (close_span, fields) = parse_paren_initializer(input)?; + (close_span, fields, None) + } else { + return Err(input.error("expected curly braces or parentheses")); + }; let error = input .peek(Token![?]) .then(|| Ok::<_, syn::Error>((input.parse()?, input.parse()?))) @@ -409,7 +482,7 @@ impl Parse for Initializer { attrs, this, path, - brace_token, + close_span, fields, rest, error, @@ -452,22 +525,43 @@ impl Parse for InitializerKind { _colon_token: input.parse()?, block: input.parse()?, }) - } else if lh.peek(Ident) { - let ident = input.parse()?; + } else if lh.peek(Ident) || lh.peek(LitInt) { + let member = if lh.peek(Ident) { + Member::Named(input.parse()?) + } else { + let lit: LitInt = input.parse()?; + let index: u32 = lit.base10_parse().map_err(|_| { + syn::Error::new( + lit.span(), + "tuple field index must be a non-negative integer", + ) + })?; + Member::Unnamed(Index { + index, + span: lit.span(), + }) + }; let lh = input.lookahead1(); if lh.peek(Token![<-]) { Ok(Self::Init { - ident, + member, _left_arrow_token: input.parse()?, value: input.parse()?, }) } else if lh.peek(Token![:]) { Ok(Self::Value { - ident, + member, value: Some((input.parse()?, input.parse()?)), }) } else if lh.peek(Token![,]) || lh.peek(End) { - Ok(Self::Value { ident, value: None }) + if matches!(member, Member::Unnamed(_)) { + Err(lh.error()) + } else { + Ok(Self::Value { + member, + value: None, + }) + } } else { Err(lh.error()) } @@ -476,3 +570,18 @@ impl Parse for InitializerKind { } } } + +impl Parse for TupleInitializerField { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let attrs = input.call(Attribute::parse_outer)?; + if input.peek(Token![<-]) { + return Err(input.error( + "`<-` is not supported in tuple constructor syntax; use braces with indices, e.g. `Type { 0 <- init, 1: value }`", + )); + } + Ok(Self { + attrs, + value: input.parse()?, + }) + } +} diff --git a/tests/tuple_struct.rs b/tests/tuple_struct.rs new file mode 100644 index 00000000..48ec89ee --- /dev/null +++ b/tests/tuple_struct.rs @@ -0,0 +1,67 @@ +#![cfg_attr(feature = "alloc", feature(allocator_api))] + +use core::pin::Pin; +use pin_init::*; + +#[allow(unused_attributes)] +#[path = "../examples/mutex.rs"] +mod mutex; +use mutex::*; + +#[pin_data] +struct TupleStruct(#[pin] CMutex, i32); + +fn assert_pinned_mutex(_: &Pin<&mut CMutex>) {} + +#[test] +fn tuple_struct_brace_init_and_projection() { + stack_pin_init!(let tuple = pin_init!(TupleStruct:: { 0 <- CMutex::new(7), 1: 13 })); + + let projected = tuple.as_mut().project(); + assert_pinned_mutex(&projected.0); + assert_eq!(*projected.0.as_ref().get_ref().lock(), 7); + assert_eq!(*projected.1, 13); +} + +#[pin_data] +struct Triple(i32, i32, i32); + +#[pin_data] +struct ValueTuple(T, i32); + +#[pin_data] +struct DualPinned(#[pin] CMutex, #[pin] CMutex, usize); + +#[test] +fn tuple_struct_constructor_form() { + stack_pin_init!(let triple = pin_init!(Triple(11, 29, 31))); + assert_eq!(triple.as_ref().get_ref().0, 11); + assert_eq!(triple.as_ref().get_ref().1, 29); + assert_eq!(triple.as_ref().get_ref().2, 31); +} + +#[test] +fn tuple_struct_constructor_infers_generics() { + stack_pin_init!(let tuple = pin_init!(ValueTuple(9u32, 6))); + assert_eq!(tuple.as_ref().get_ref().0, 9u32); + assert_eq!(tuple.as_ref().get_ref().1, 6); +} + +#[test] +fn tuple_struct_multi_pinned_fields_projection() { + stack_pin_init!( + let tuple = pin_init!(DualPinned:: { 0 <- CMutex::new(1), 1 <- CMutex::new(2), 2: 3 }) + ); + + let projected = tuple.as_mut().project(); + assert_pinned_mutex(&projected.0); + assert_pinned_mutex(&projected.1); + + *projected.0.as_ref().get_ref().lock() = 10; + *projected.1.as_ref().get_ref().lock() = 20; + *projected.2 = 30; + + assert_eq!(*tuple.as_ref().get_ref().0.lock(), 10); + assert_eq!(*tuple.as_ref().get_ref().1.lock(), 20); + assert_eq!(tuple.as_ref().get_ref().2, 30); +} diff --git a/tests/ui/compile-fail/init/no_tuple_paren_arrow.rs b/tests/ui/compile-fail/init/no_tuple_paren_arrow.rs new file mode 100644 index 00000000..2df60080 --- /dev/null +++ b/tests/ui/compile-fail/init/no_tuple_paren_arrow.rs @@ -0,0 +1,8 @@ +use pin_init::*; + +#[pin_data] +struct Tuple(#[pin] i32, i32); + +fn main() { + let _ = pin_init!(Tuple(<- 1, 2)); +} diff --git a/tests/ui/compile-fail/init/no_tuple_paren_arrow.stderr b/tests/ui/compile-fail/init/no_tuple_paren_arrow.stderr new file mode 100644 index 00000000..55ddc3dc --- /dev/null +++ b/tests/ui/compile-fail/init/no_tuple_paren_arrow.stderr @@ -0,0 +1,5 @@ +error: `<-` is not supported in tuple constructor syntax; use braces with indices, e.g. `Type { 0 <- init, 1: value }` + --> tests/ui/compile-fail/init/no_tuple_paren_arrow.rs:7:29 + | +7 | let _ = pin_init!(Tuple(<- 1, 2)); + | ^ diff --git a/tests/ui/compile-fail/init/no_tuple_shorthand.rs b/tests/ui/compile-fail/init/no_tuple_shorthand.rs new file mode 100644 index 00000000..4b0297f4 --- /dev/null +++ b/tests/ui/compile-fail/init/no_tuple_shorthand.rs @@ -0,0 +1,8 @@ +use pin_init::*; + +#[pin_data] +struct Tuple(#[pin] i32, i32); + +fn main() { + let _ = pin_init!(Tuple { 0, 1: 24 }); +} diff --git a/tests/ui/compile-fail/init/no_tuple_shorthand.stderr b/tests/ui/compile-fail/init/no_tuple_shorthand.stderr new file mode 100644 index 00000000..f78d85fa --- /dev/null +++ b/tests/ui/compile-fail/init/no_tuple_shorthand.stderr @@ -0,0 +1,5 @@ +error: expected `<-` or `:` + --> tests/ui/compile-fail/init/no_tuple_shorthand.rs:7:32 + | +7 | let _ = pin_init!(Tuple { 0, 1: 24 }); + | ^ diff --git a/tests/ui/compile-fail/init/no_tuple_syntax_mixing.rs b/tests/ui/compile-fail/init/no_tuple_syntax_mixing.rs new file mode 100644 index 00000000..9a7e3c84 --- /dev/null +++ b/tests/ui/compile-fail/init/no_tuple_syntax_mixing.rs @@ -0,0 +1,8 @@ +use pin_init::*; + +#[pin_data] +struct Tuple(#[pin] i32, i32); + +fn main() { + let _ = pin_init!(Tuple (0, 1: 24)); +} diff --git a/tests/ui/compile-fail/init/no_tuple_syntax_mixing.stderr b/tests/ui/compile-fail/init/no_tuple_syntax_mixing.stderr new file mode 100644 index 00000000..81c8fe02 --- /dev/null +++ b/tests/ui/compile-fail/init/no_tuple_syntax_mixing.stderr @@ -0,0 +1,5 @@ +error: expected `,` + --> tests/ui/compile-fail/init/no_tuple_syntax_mixing.rs:7:34 + | +7 | let _ = pin_init!(Tuple (0, 1: 24)); + | ^ diff --git a/tests/ui/compile-fail/init/tuple_duplicate_field.rs b/tests/ui/compile-fail/init/tuple_duplicate_field.rs new file mode 100644 index 00000000..971b3f97 --- /dev/null +++ b/tests/ui/compile-fail/init/tuple_duplicate_field.rs @@ -0,0 +1,8 @@ +use pin_init::*; + +#[pin_data] +struct Tuple(#[pin] i32, i32); + +fn main() { + let _ = pin_init!(Tuple { 0: 1, 0: 2, 1: 3 }); +} diff --git a/tests/ui/compile-fail/init/tuple_duplicate_field.stderr b/tests/ui/compile-fail/init/tuple_duplicate_field.stderr new file mode 100644 index 00000000..dd57ac30 --- /dev/null +++ b/tests/ui/compile-fail/init/tuple_duplicate_field.stderr @@ -0,0 +1,8 @@ +error[E0062]: field `0` specified more than once + --> tests/ui/compile-fail/init/tuple_duplicate_field.rs:7:37 + | +7 | let _ = pin_init!(Tuple { 0: 1, 0: 2, 1: 3 }); + | ------------------------^------------ + | | | + | | used more than once + | first use of `0` diff --git a/tests/ui/compile-fail/init/tuple_invalid_field.rs b/tests/ui/compile-fail/init/tuple_invalid_field.rs new file mode 100644 index 00000000..19663284 --- /dev/null +++ b/tests/ui/compile-fail/init/tuple_invalid_field.rs @@ -0,0 +1,8 @@ +use pin_init::*; + +#[pin_data] +struct Tuple(#[pin] i32, i32); + +fn main() { + let _ = pin_init!(Tuple { 0: 1, 1: 2, 2: 3 }); +} diff --git a/tests/ui/compile-fail/init/tuple_invalid_field.stderr b/tests/ui/compile-fail/init/tuple_invalid_field.stderr new file mode 100644 index 00000000..f705adaf --- /dev/null +++ b/tests/ui/compile-fail/init/tuple_invalid_field.stderr @@ -0,0 +1,30 @@ +error[E0609]: no field `2` on type `__ThePinData` + --> tests/ui/compile-fail/init/tuple_invalid_field.rs:7:43 + | +7 | let _ = pin_init!(Tuple { 0: 1, 1: 2, 2: 3 }); + | ^ unknown field + | + = note: available fields are: `0`, `1` + +error[E0609]: no field `2` on type `Tuple` + --> tests/ui/compile-fail/init/tuple_invalid_field.rs:7:43 + | +7 | let _ = pin_init!(Tuple { 0: 1, 1: 2, 2: 3 }); + | ^ unknown field + | + = note: available fields are: `0`, `1` + +error[E0560]: struct `Tuple` has no field named `2` + --> tests/ui/compile-fail/init/tuple_invalid_field.rs:7:43 + | +4 | struct Tuple(#[pin] i32, i32); + | ----- `Tuple` defined here +... +7 | let _ = pin_init!(Tuple { 0: 1, 1: 2, 2: 3 }); + | ^ field does not exist + | +help: `Tuple` is a tuple struct, use the appropriate syntax + | +7 - let _ = pin_init!(Tuple { 0: 1, 1: 2, 2: 3 }); +7 + let _ = Tuple(/* i32 */, /* i32 */); + | diff --git a/tests/ui/compile-fail/init/tuple_missing_field.rs b/tests/ui/compile-fail/init/tuple_missing_field.rs new file mode 100644 index 00000000..401ded40 --- /dev/null +++ b/tests/ui/compile-fail/init/tuple_missing_field.rs @@ -0,0 +1,9 @@ +use pin_init::*; + +#[pin_data] +struct Tuple(#[pin] i32, i32); + +fn main() { + let _ = pin_init!(Tuple { 0: 1 }); + let _ = init!(Tuple { 0: 1 }); +} diff --git a/tests/ui/compile-fail/init/tuple_missing_field.stderr b/tests/ui/compile-fail/init/tuple_missing_field.stderr new file mode 100644 index 00000000..4e5ad4c8 --- /dev/null +++ b/tests/ui/compile-fail/init/tuple_missing_field.stderr @@ -0,0 +1,11 @@ +error[E0063]: missing field `1` in initializer of `Tuple` + --> tests/ui/compile-fail/init/tuple_missing_field.rs:7:23 + | +7 | let _ = pin_init!(Tuple { 0: 1 }); + | ^^^^^ missing `1` + +error[E0063]: missing field `1` in initializer of `Tuple` + --> tests/ui/compile-fail/init/tuple_missing_field.rs:8:19 + | +8 | let _ = init!(Tuple { 0: 1 }); + | ^^^^^ missing `1` diff --git a/tests/ui/expand/tuple_struct.expanded.rs b/tests/ui/expand/tuple_struct.expanded.rs index 08068064..4c8ba6cb 100644 --- a/tests/ui/expand/tuple_struct.expanded.rs +++ b/tests/ui/expand/tuple_struct.expanded.rs @@ -102,4 +102,121 @@ const _: () = { const N: usize, > UselessPinnedDropImpl_you_need_to_specify_PinnedDrop for Foo<'a, T, N> {} }; -fn main() {} +fn main() { + let mut first = [1u8, 2, 3]; + let _ = { + let __data = unsafe { + use ::pin_init::__internal::HasInitData; + Foo::__init_data() + }; + let init = __data + .__make_closure::< + _, + ::core::convert::Infallible, + >(move |slot| { + let mut ___0_guard = (unsafe { + ::pin_init::__internal::Slot::< + ::pin_init::__internal::Unpinned, + _, + >::new(&raw mut (*slot).0) + }) + .write(&mut first); + let mut ___1_guard = (unsafe { + ::pin_init::__internal::Slot::< + ::pin_init::__internal::Unpinned, + _, + >::new(&raw mut (*slot).1) + }) + .write(PhantomPinned); + let mut ___2_guard = (unsafe { + ::pin_init::__internal::Slot::< + ::pin_init::__internal::Unpinned, + _, + >::new(&raw mut (*slot).2) + }) + .init(10)?; + ::core::mem::forget(___0_guard); + ::core::mem::forget(___1_guard); + ::core::mem::forget(___2_guard); + #[allow(unreachable_code, clippy::diverging_sub_expression)] + let _ = || unsafe { + let _ = &(*slot).0; + let _ = &(*slot).1; + let _ = &(*slot).2; + ::core::ptr::write( + slot, + Foo { + 0: ::core::panicking::panic("explicit panic"), + 1: ::core::panicking::panic("explicit panic"), + 2: ::core::panicking::panic("explicit panic"), + }, + ) + }; + Ok(unsafe { ::pin_init::__internal::InitOk::new() }) + }); + let init = move | + slot, + | -> ::core::result::Result<(), ::core::convert::Infallible> { + init(slot).map(|__InitOk| ()) + }; + unsafe { ::pin_init::init_from_closure::<_, ::core::convert::Infallible>(init) } + }; + let mut second = [4u8, 5, 6]; + let _ = { + let __data = unsafe { + use ::pin_init::__internal::HasInitData; + Foo::__init_data() + }; + let init = __data + .__make_closure::< + _, + ::core::convert::Infallible, + >(move |slot| { + let mut ___0_guard = (unsafe { + ::pin_init::__internal::Slot::< + ::pin_init::__internal::Unpinned, + _, + >::new(&raw mut (*slot).0) + }) + .write(&mut second); + let mut ___1_guard = (unsafe { + ::pin_init::__internal::Slot::< + ::pin_init::__internal::Unpinned, + _, + >::new(&raw mut (*slot).1) + }) + .write(PhantomPinned); + let mut ___2_guard = (unsafe { + ::pin_init::__internal::Slot::< + ::pin_init::__internal::Unpinned, + _, + >::new(&raw mut (*slot).2) + }) + .write(20); + ::core::mem::forget(___0_guard); + ::core::mem::forget(___1_guard); + ::core::mem::forget(___2_guard); + #[allow(unreachable_code, clippy::diverging_sub_expression)] + let _ = || unsafe { + let _ = &(*slot).0; + let _ = &(*slot).1; + let _ = &(*slot).2; + ::core::ptr::write( + slot, + Foo { + 0: ::core::panicking::panic("explicit panic"), + 1: ::core::panicking::panic("explicit panic"), + 2: ::core::panicking::panic("explicit panic"), + }, + ) + }; + Ok(unsafe { ::pin_init::__internal::InitOk::new() }) + }); + let init = move | + slot, + | -> ::core::result::Result<(), ::core::convert::Infallible> { + init(slot).map(|__InitOk| ()) + }; + unsafe { ::pin_init::init_from_closure::<_, ::core::convert::Infallible>(init) } + }; +} diff --git a/tests/ui/expand/tuple_struct.rs b/tests/ui/expand/tuple_struct.rs index d81daa1e..27d8c7f7 100644 --- a/tests/ui/expand/tuple_struct.rs +++ b/tests/ui/expand/tuple_struct.rs @@ -4,4 +4,14 @@ use pin_init::*; #[pin_data] struct Foo<'a, T: Copy, const N: usize>(&'a mut [T; N], #[pin] PhantomPinned, usize); -fn main() {} +fn main() { + let mut first = [1u8, 2, 3]; + let _ = init!(Foo { + 0: &mut first, + 1: PhantomPinned, + 2 <- 10, + }); + + let mut second = [4u8, 5, 6]; + let _ = init!(Foo(&mut second, PhantomPinned, 20)); +} From 02d105f3ad525216bf7849f26fc1f6ef9d32be87 Mon Sep 17 00:00:00 2001 From: Mohamad Alsadhan Date: Fri, 22 May 2026 21:04:29 +0300 Subject: [PATCH 3/4] tests: add tuple struct generic and drop coverage Extend tuple-struct coverage with the semantic cases that are easiest to review independently from the implementation changes. Add runtime coverage for generic and const-generic tuple structs, `Unpin` behaviour with `!Unpin` field types, pinned drop, and fallible partial-init rollback. Signed-off-by: Mohamad Alsadhan --- tests/tuple_struct.rs | 108 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/tests/tuple_struct.rs b/tests/tuple_struct.rs index 48ec89ee..c2508056 100644 --- a/tests/tuple_struct.rs +++ b/tests/tuple_struct.rs @@ -65,3 +65,111 @@ fn tuple_struct_multi_pinned_fields_projection() { assert_eq!(*tuple.as_ref().get_ref().1.lock(), 20); assert_eq!(tuple.as_ref().get_ref().2, 30); } + +#[pin_data] +struct GenericTuple<'a, T, const N: usize>(#[pin] CMutex<(&'a T, [u8; N])>, usize); + +#[pin_data] +#[allow(dead_code)] +struct UnpinnedMutexTuple(CMutex, usize); + +#[pin_data] +struct TupleConst(#[pin] CMutex<[T; N]>, usize); + +fn assert_unpin() {} + +#[test] +fn tuple_struct_generics_are_supported() { + let value = 77u16; + let payload = (&value, [1, 2, 3, 4]); + stack_pin_init!( + let tuple = pin_init!(GenericTuple { 0 <- CMutex::new(payload), 1: 12 }) + ); + + let projected = tuple.as_mut().project(); + assert_pinned_mutex(&projected.0); + let locked = projected.0.as_ref().get_ref().lock(); + assert_eq!(*locked.0, 77u16); + assert_eq!(locked.1, [1, 2, 3, 4]); + assert_eq!(*projected.1, 12); +} + +#[test] +fn tuple_struct_unpin_ignores_unpinned_non_unpin_field() { + assert_unpin::>(); +} + +#[test] +fn tuple_struct_const_generics_support_explicit_arguments() { + stack_pin_init!(let tuple = pin_init!(TupleConst:: { 0 <- CMutex::new([1, 2, 3]), 1: 9 })); + + let projected = tuple.as_mut().project(); + assert_pinned_mutex(&projected.0); + assert_eq!(*projected.0.as_ref().get_ref().lock(), [1, 2, 3]); + assert_eq!(*projected.1, 9); +} + +#[pin_data(PinnedDrop)] +struct DropTuple(#[pin] CMutex, usize); + +static PINNED_DROP_TUPLE_DROPS: core::sync::atomic::AtomicUsize = + core::sync::atomic::AtomicUsize::new(0); + +struct DropCounter; + +static FALLIBLE_TUPLE_DROPS: core::sync::atomic::AtomicUsize = + core::sync::atomic::AtomicUsize::new(0); + +impl Drop for DropCounter { + fn drop(&mut self) { + FALLIBLE_TUPLE_DROPS.fetch_add(1, core::sync::atomic::Ordering::SeqCst); + } +} + +fn tuple_failing_init() -> impl PinInit, ()> { + // SAFETY: The closure initializes field 0, explicitly rolls it back before returning `Err`, + // and leaves the slot otherwise untouched. + unsafe { + pin_init_from_closure(|slot: *mut TupleStruct| { + let field0 = core::ptr::addr_of_mut!((*slot).0); + let init0 = CMutex::new(DropCounter); + match init0.__pinned_init(field0) { + Ok(()) => {} + Err(infallible) => match infallible {}, + } + core::ptr::drop_in_place(field0); + Err(()) + }) + } +} + +#[pinned_drop] +impl PinnedDrop for DropTuple { + fn drop(self: Pin<&mut Self>) { + let _ = self; + PINNED_DROP_TUPLE_DROPS.fetch_add(1, core::sync::atomic::Ordering::SeqCst); + } +} + +#[test] +fn tuple_struct_pinned_drop_delegates_from_drop() { + PINNED_DROP_TUPLE_DROPS.store(0, core::sync::atomic::Ordering::SeqCst); + { + stack_pin_init!(let _tuple = pin_init!(DropTuple { 0 <- CMutex::new(5usize), 1: 1 })); + } + assert_eq!( + PINNED_DROP_TUPLE_DROPS.load(core::sync::atomic::Ordering::SeqCst), + 1 + ); +} + +#[test] +fn tuple_struct_fallible_init_drops_initialized_fields() { + FALLIBLE_TUPLE_DROPS.store(0, core::sync::atomic::Ordering::SeqCst); + stack_try_pin_init!(let tuple: TupleStruct = tuple_failing_init()); + assert!(matches!(tuple, Err(()))); + assert_eq!( + FALLIBLE_TUPLE_DROPS.load(core::sync::atomic::Ordering::SeqCst), + 1 + ); +} From 2d2042eb7bc48e683e33ee5397ab9f3243b37100 Mon Sep 17 00:00:00 2001 From: Mohamad Alsadhan Date: Fri, 22 May 2026 21:05:02 +0300 Subject: [PATCH 4/4] internal: support cfg on final tuple fields and arguments Support tuple structs whose final field or final constructor argument is removed by #[cfg]. This preserves tuple indices without needing to evaluate user cfgs in the proc macro. Reject #[cfg] on earlier tuple fields and tuple constructor arguments, because those cases would require reindexing the remaining fields after cfg stripping. That is possible to generate, but the extra complexity is not justified for the tuple struct support added here. Add regression tests for the supported final-field case and UI diagnostics for the unsupported index-shifting cases. Signed-off-by: Mohamad Alsadhan --- internal/src/init.rs | 18 +++++ internal/src/pin_data.rs | 74 ++++++++++++++++--- tests/cfgs.rs | 73 +++++++++++++++++- .../init/tuple_constructor_cfg_non_last.rs | 12 +++ .../tuple_constructor_cfg_non_last.stderr | 5 ++ .../pin_data/tuple_struct_cfg_non_last.rs | 6 ++ .../pin_data/tuple_struct_cfg_non_last.stderr | 5 ++ 7 files changed, 183 insertions(+), 10 deletions(-) create mode 100644 tests/ui/compile-fail/init/tuple_constructor_cfg_non_last.rs create mode 100644 tests/ui/compile-fail/init/tuple_constructor_cfg_non_last.stderr create mode 100644 tests/ui/compile-fail/pin_data/tuple_struct_cfg_non_last.rs create mode 100644 tests/ui/compile-fail/pin_data/tuple_struct_cfg_non_last.stderr diff --git a/internal/src/init.rs b/internal/src/init.rs index b65c52fc..01799c9c 100644 --- a/internal/src/init.rs +++ b/internal/src/init.rs @@ -434,7 +434,25 @@ fn parse_paren_initializer(input: syn::parse::ParseStream<'_>) -> syn::Result<(S let content; let paren_token = parenthesized!(content in input); let tuple_fields = content.parse_terminated(TupleInitializerField::parse, Token![,])?; + let tuple_fields: Vec<_> = tuple_fields.into_iter().collect(); let mut fields = Punctuated::new(); + + for tuple_field in tuple_fields + .iter() + .take(tuple_fields.len().saturating_sub(1)) + { + if let Some(attr) = tuple_field + .attrs + .iter() + .find(|attr| attr.path().is_ident("cfg")) + { + return Err(syn::Error::new_spanned( + attr, + "`#[cfg]` on tuple constructor arguments is only supported on the last argument", + )); + } + } + for (index, tuple_field) in tuple_fields.into_iter().enumerate() { fields.push(InitializerField { attrs: tuple_field.attrs, diff --git a/internal/src/pin_data.rs b/internal/src/pin_data.rs index 1834d894..04131dfa 100644 --- a/internal/src/pin_data.rs +++ b/internal/src/pin_data.rs @@ -7,8 +7,8 @@ use syn::{ parse_quote, parse_quote_spanned, spanned::Spanned, visit_mut::VisitMut, - Field, Fields, Generics, Ident, Index, Item, Member, PathSegment, Type, TypePath, Visibility, - WhereClause, + Attribute, Field, Fields, Generics, Ident, Index, Item, Member, Meta, PathSegment, Type, + TypePath, Visibility, WhereClause, }; use crate::diagnostics::{DiagCtxt, ErrorGuaranteed}; @@ -56,6 +56,19 @@ fn member_display_name(member: &Member) -> String { } } +fn has_cfg_attr(attrs: &[Attribute]) -> bool { + attrs.iter().any(|attr| attr.path().is_ident("cfg")) +} + +fn cfg_condition(attrs: &[Attribute]) -> Option { + let cfgs: Vec<_> = attrs + .iter() + .filter(|attr| attr.path().is_ident("cfg")) + .filter_map(|attr| attr.parse_args::().ok()) + .collect(); + (!cfgs.is_empty()).then(|| quote!(all(#(#cfgs),*))) +} + pub(crate) fn pin_data( args: Args, input: Item, @@ -119,6 +132,17 @@ pub(crate) fn pin_data( }) .collect(); + if is_tuple_struct { + for field in fields.iter().take(fields.len().saturating_sub(1)) { + if has_cfg_attr(&field.field.attrs) { + return Err(dcx.error( + field.field, + "`#[cfg]` on tuple struct fields is only supported on the last field", + )); + } + } + } + for field in &fields { if !field.pinned && is_phantom_pinned(&field.field.ty) { dcx.warn( @@ -368,6 +392,7 @@ fn generate_projections( let mut fields_decl = Vec::new(); let mut field_bindings = Vec::new(); let mut field_projections = Vec::new(); + for (index, field) in fields.iter().enumerate() { let Field { vis, ty, attrs, .. } = &field.field; let binding = format_ident!("__field_{index}"); @@ -390,6 +415,43 @@ fn generate_projections( } field_bindings.push(quote!(ref mut #binding,)); } + let projection_init = if let Some(last_field) = fields.last() { + if let Some(cfg) = cfg_condition(&last_field.field.attrs) { + let field_bindings_without_last = &field_bindings[..field_bindings.len() - 1]; + let field_projections_without_last = + &field_projections[..field_projections.len() - 1]; + quote! {{ + #[cfg(#cfg)] + { + let #ident(#(#field_bindings)*) = *#this; + #projection( + #(#field_projections)* + ::core::marker::PhantomData, + ) + } + #[cfg(not(#cfg))] + { + let #ident(#(#field_bindings_without_last)*) = *#this; + #projection( + #(#field_projections_without_last)* + ::core::marker::PhantomData, + ) + } + }} + } else { + quote! {{ + let #ident(#(#field_bindings)*) = *#this; + #projection( + #(#field_projections)* + ::core::marker::PhantomData, + ) + }} + } + } else { + quote! { + #projection(::core::marker::PhantomData) + } + }; ( quote! { @@ -401,13 +463,7 @@ fn generate_projections( ::core::marker::PhantomData<&'__pin mut ()>, ) #whr; }, - quote! {{ - let #ident(#(#field_bindings)*) = *#this; - #projection( - #(#field_projections)* - ::core::marker::PhantomData, - ) - }}, + projection_init, ) }; diff --git a/tests/cfgs.rs b/tests/cfgs.rs index f1be1bc2..5b67b275 100644 --- a/tests/cfgs.rs +++ b/tests/cfgs.rs @@ -1,4 +1,4 @@ -use pin_init::{pin_data, pin_init, PinInit}; +use pin_init::{pin_data, pin_init, stack_pin_init, PinInit}; #[pin_data] pub struct Struct { @@ -21,9 +21,80 @@ impl Struct { struct Field {} +#[cfg(not(feature = "std"))] +fn assert_pinned(_: core::pin::Pin<&mut T>) {} + #[pin_data] pub struct Struct2 { // Test for cases where the type is not even defined when cfg is not satisfied. #[cfg(any())] non_exist: NonExistentType, } + +#[pin_data] +pub struct TupleStruct(Field, #[cfg(any())] HiddenField); + +impl TupleStruct { + pub fn new() -> impl PinInit { + pin_init!(Self(Field {})) + } +} + +#[allow(dead_code)] +struct HiddenField; + +#[test] +fn tuple_struct_allows_cfgd_out_last_field() { + stack_pin_init!(let value = TupleStruct::new()); + let projected = value.as_mut().project(); + let _ = projected.0; +} + +#[pin_data] +pub struct ConstructorCfgTuple(Field, #[cfg(any())] HiddenField); + +impl ConstructorCfgTuple { + pub fn new() -> impl PinInit { + pin_init!(Self( + Field {}, + #[cfg(any())] + HiddenField + )) + } +} + +#[test] +fn tuple_constructor_allows_cfgd_out_last_argument() { + stack_pin_init!(let value = ConstructorCfgTuple::new()); + let projected = value.as_mut().project(); + let _ = projected.0; +} + +#[pin_data] +pub struct FeatureTupleStruct( + Field, + #[cfg(not(feature = "std"))] + #[pin] + core::marker::PhantomPinned, +); + +impl FeatureTupleStruct { + pub fn new() -> impl PinInit { + pin_init!(Self( + Field {}, + #[cfg(not(feature = "std"))] + core::marker::PhantomPinned + )) + } +} + +#[test] +fn tuple_struct_allows_feature_cfgd_out_last_field() { + stack_pin_init!(let value = FeatureTupleStruct::new()); + let projected = value.as_mut().project(); + let _ = projected.0; + #[cfg(not(feature = "std"))] + { + assert_pinned(projected.1); + } +} diff --git a/tests/ui/compile-fail/init/tuple_constructor_cfg_non_last.rs b/tests/ui/compile-fail/init/tuple_constructor_cfg_non_last.rs new file mode 100644 index 00000000..6b17ebd3 --- /dev/null +++ b/tests/ui/compile-fail/init/tuple_constructor_cfg_non_last.rs @@ -0,0 +1,12 @@ +use pin_init::*; + +#[pin_data] +struct Tuple(Option, i32); + +fn main() { + let _ = pin_init!(Tuple( + #[cfg(any())] + None, + 1 + )); +} diff --git a/tests/ui/compile-fail/init/tuple_constructor_cfg_non_last.stderr b/tests/ui/compile-fail/init/tuple_constructor_cfg_non_last.stderr new file mode 100644 index 00000000..d31f2057 --- /dev/null +++ b/tests/ui/compile-fail/init/tuple_constructor_cfg_non_last.stderr @@ -0,0 +1,5 @@ +error: `#[cfg]` on tuple constructor arguments is only supported on the last argument + --> tests/ui/compile-fail/init/tuple_constructor_cfg_non_last.rs:8:9 + | +8 | #[cfg(any())] + | ^^^^^^^^^^^^^ diff --git a/tests/ui/compile-fail/pin_data/tuple_struct_cfg_non_last.rs b/tests/ui/compile-fail/pin_data/tuple_struct_cfg_non_last.rs new file mode 100644 index 00000000..d6077c31 --- /dev/null +++ b/tests/ui/compile-fail/pin_data/tuple_struct_cfg_non_last.rs @@ -0,0 +1,6 @@ +use pin_init::*; + +#[pin_data] +struct Tuple(#[cfg(any())] HiddenField, i32); + +fn main() {} diff --git a/tests/ui/compile-fail/pin_data/tuple_struct_cfg_non_last.stderr b/tests/ui/compile-fail/pin_data/tuple_struct_cfg_non_last.stderr new file mode 100644 index 00000000..ea3fac67 --- /dev/null +++ b/tests/ui/compile-fail/pin_data/tuple_struct_cfg_non_last.stderr @@ -0,0 +1,5 @@ +error: `#[cfg]` on tuple struct fields is only supported on the last field + --> tests/ui/compile-fail/pin_data/tuple_struct_cfg_non_last.rs:4:14 + | +4 | struct Tuple(#[cfg(any())] HiddenField, i32); + | ^^^^^^^^^^^^^^^^^^^^^^^^^