From 376d4f758038e5d87e3fe3dfa755fcaf58e0a132 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 | 337 +++++++++++++----- .../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 | 139 ++++++++ tests/ui/expand/tuple_struct.rs | 7 + 8 files changed, 486 insertions(+), 87 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 4ae9f971..3862bd3d 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 263f6730..045f2ec4 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, - Attribute, Field, Generics, Ident, Item, PathSegment, Type, TypePath, Visibility, WhereClause, + Attribute, Field, Fields, Generics, Ident, Index, Item, Member, Meta, PathSegment, Type, + TypePath, Visibility, WhereClause, }; use crate::diagnostics::{DiagCtxt, ErrorGuaranteed}; @@ -37,10 +38,40 @@ impl Parse for Args { struct FieldInfo<'a> { field: &'a Field, + member: Member, pinned: bool, cfg_attrs: Vec<&'a Attribute>, } +impl FieldInfo<'_> { + fn ident(&self) -> Ident { + match &self.member { + Member::Named(ident) => ident.clone(), + Member::Unnamed(Index { index, .. }) => format_ident!("_{index}"), + } + } + + fn display_name(&self) -> String { + match &self.member { + Member::Named(ident) => format!("`{ident}`"), + Member::Unnamed(Index { index, .. }) => format!("index `{index}`"), + } + } +} + +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, @@ -79,40 +110,59 @@ 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_count = len - field.attrs.len(); if pinned_count > 1 { dcx.error(&field, "#[pin] attribute specified more than once"); } - let cfg_attrs = field .attrs .iter() - .filter(|a| a.path().is_ident("cfg")) + .filter(|attr| attr.path().is_ident("cfg")) .collect(); + 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: pinned_count != 0, cfg_attrs, } }) .collect(); - for field in &fields { - let ident = field.field.ident.as_ref().unwrap(); + 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( 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", + field.display_name(), ), ); } @@ -120,8 +170,13 @@ 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 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, &fields); @@ -182,9 +237,19 @@ fn generate_unpin_impl( else { unreachable!() }; + let unpin_allow = if fields.iter().any(|f| matches!(f.member, Member::Named(_))) { + quote! { + #[allow( + dead_code, // The fields below are never used. + non_snake_case // The warning will be emitted on the struct definition. + )] + } + } else { + quote!(#[allow(dead_code)]) // The fields below are never used. + }; let pinned_fields = fields.iter().filter(|f| f.pinned).map(|f| { - let ident = f.field.ident.as_ref().unwrap(); let ty = &f.field.ty; + let ident = f.ident(); let cfg_attrs = &f.cfg_attrs; quote!( #(#cfg_attrs)* @@ -194,10 +259,7 @@ fn generate_unpin_impl( 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`. - #[allow( - dead_code, // The fields below are never used. - non_snake_case // The warning will be emitted on the struct definition. - )] + #unpin_allow struct __Unpin #generics_with_pin_lt #where_token #predicates @@ -269,6 +331,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(); @@ -278,62 +341,146 @@ 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, .. } = &field.field; - let cfg_attrs = &field.cfg_attrs; - - let ident = ident - .as_ref() - .expect("only structs with named fields are supported"); - if field.pinned { - ( - quote!( - #(#cfg_attrs)* - #vis #ident: ::core::pin::Pin<&'__pin mut #ty>, - ), - quote!( - #(#cfg_attrs)* - // SAFETY: this field is structurally pinned. - #ident: unsafe { ::core::pin::Pin::new_unchecked(&mut #this.#ident) }, - ), - ) - } else { - ( - quote!( - #(#cfg_attrs)* - #vis #ident: &'__pin mut #ty, - ), - quote!( - #(#cfg_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!(" - {}", f.display_name())); let not_structurally_pinned_fields_docs = fields .iter() .filter(|f| !f.pinned) - .map(|f| format!(" - `{}`", f.field.ident.as_ref().unwrap())); + .map(|f| format!(" - {}", f.display_name())); let docs = format!(" Pin-projections of [`{ident}`]"); - quote! { - #[doc = #docs] - // Allow `non_snake_case` since the same warning will be emitted on - // the struct definition. - #[allow(dead_code, non_snake_case)] - #[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, .. } = &field.field; + let ident = field.ident(); + let member = &field.member; + let cfg_attrs = &field.cfg_attrs; + + if field.pinned { + ( + quote!( + #(#cfg_attrs)* + #vis #ident: ::core::pin::Pin<&'__pin mut #ty>, + ), + quote!( + #(#cfg_attrs)* + // SAFETY: this field is structurally pinned. + #ident: unsafe { ::core::pin::Pin::new_unchecked(&mut #this.#member) }, + ), + ) + } else { + ( + quote!( + #(#cfg_attrs)* + #vis #ident: &'__pin mut #ty, + ), + quote!( + #(#cfg_attrs)* + #ident: &mut #this.#member, + ), + ) + } + }) + .collect(); + ( + quote! { + #[doc = #docs] + // Allow `non_snake_case` since the same warning will be emitted on + // the struct definition. + #[allow(dead_code, non_snake_case)] + #[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_projections = Vec::new(); + for field in fields { + let Field { vis, ty, attrs, .. } = &field.field; + let member = &field.member; + + 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(&mut #this.#member) }, + )); + } else { + fields_decl.push(quote!( + #(#attrs)* + #vis &'__pin mut #ty, + )); + field_projections.push(quote!(&mut #this.#member,)); + } } + let projection_init = if let Some(last_field) = fields.last() { + if let Some(cfg) = cfg_condition(&last_field.field.attrs) { + let field_projections_without_last = + &field_projections[..field_projections.len() - 1]; + quote! {{ + #[cfg(#cfg)] + { + #projection( + #(#field_projections)* + ::core::marker::PhantomData, + ) + } + #[cfg(not(#cfg))] + { + #projection( + #(#field_projections_without_last)* + ::core::marker::PhantomData, + ) + } + }} + } else { + quote! {{ + #projection( + #(#field_projections)* + ::core::marker::PhantomData, + ) + }} + } + } else { + quote! { + #projection(::core::marker::PhantomData) + } + }; + + ( + quote! { + #[doc = #docs] + #[allow(dead_code)] + #[doc(hidden)] + #vis struct #projection #generics_with_pin_lt ( + #(#fields_decl)* + ::core::marker::PhantomData<&'__pin mut ()>, + ) #whr; + }, + projection_init, + ) + }; + + quote! { + #projection_def impl #impl_generics #ident #ty_generics #whr @@ -351,10 +498,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 } } } @@ -368,35 +512,47 @@ fn generate_the_pin_data( ) -> 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`. + // 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, .. } = f.field; + let Field { vis, ty, .. } = f.field; + let field_name = f.ident(); + let member = &f.member; let cfg_attrs = &f.cfg_attrs; - - let field_name = ident - .as_ref() - .expect("only structs with named fields are supported"); let pin_marker = if f.pinned { quote!(Pinned) } else { quote!(Unpinned) }; + let field_safety_docs = match member { + Member::Named(_) => quote! { + /// - `(*slot).#field_name` is properly aligned. + /// - `(*slot).#field_name` points to uninitialized and exclusively accessed + /// memory. + }, + Member::Unnamed(_) => quote! { + /// - The field is properly aligned. + /// - The field points to uninitialized and exclusively accessed memory. + }, + }; + let non_snake_case_allow = matches!(member, Member::Named(_)).then(|| { + quote! { + // Allow `non_snake_case` since the same warning will be emitted on + // the struct definition. + #[allow(non_snake_case)] + } + }); 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_safety_docs #(#cfg_attrs)* - // Allow `non_snake_case` since the same warning will be emitted on - // the struct definition. - #[allow(non_snake_case)] + #non_snake_case_allow #[inline(always)] #vis unsafe fn #field_name( self, @@ -406,20 +562,27 @@ fn generate_the_pin_data( // - 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) } + unsafe { ::pin_init::__internal::Slot::new(&raw mut (*slot).#member) } } } }) .collect::(); - 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)] + let pin_data_struct = quote! { #vis struct __ThePinData #generics #whr { __phantom: ::pin_init::__internal::PhantomInvariant<#struct_name #ty_generics>, } + }; + let pin_data_new = quote! { + __ThePinData { __phantom: ::pin_init::__internal::PhantomInvariant::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)] + #pin_data_struct impl #impl_generics ::core::clone::Clone for __ThePinData #ty_generics #whr @@ -457,7 +620,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/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..48b3444c --- /dev/null +++ b/tests/ui/expand/tuple_struct.expanded.rs @@ -0,0 +1,139 @@ +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) }; + { + FooProjection( + &mut this.0, + unsafe { ::core::pin::Pin::new_unchecked(&mut this.1) }, + &mut this.2, + ::core::marker::PhantomData, + ) + } + } +} +const _: () = { + #[doc(hidden)] + struct __ThePinData<'a, T: Copy, const N: usize> { + __phantom: ::pin_init::__internal::PhantomInvariant>, + } + 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 + } + /// # Safety + /// + /// - `slot` is valid and properly aligned. + /// - The field is properly aligned. + /// - The field points to uninitialized and exclusively accessed memory. + #[inline(always)] + unsafe fn _0( + self, + slot: *mut Foo<'a, T, N>, + ) -> ::pin_init::__internal::Slot< + ::pin_init::__internal::Unpinned, + &'a mut [T; N], + > { + unsafe { ::pin_init::__internal::Slot::new(&raw mut (*slot).0) } + } + /// # Safety + /// + /// - `slot` is valid and properly aligned. + /// - The field is properly aligned. + /// - The field points to uninitialized and exclusively accessed memory. + #[inline(always)] + unsafe fn _1( + self, + slot: *mut Foo<'a, T, N>, + ) -> ::pin_init::__internal::Slot< + ::pin_init::__internal::Pinned, + PhantomPinned, + > { + unsafe { ::pin_init::__internal::Slot::new(&raw mut (*slot).1) } + } + /// # Safety + /// + /// - `slot` is valid and properly aligned. + /// - The field is properly aligned. + /// - The field points to uninitialized and exclusively accessed memory. + #[inline(always)] + unsafe fn _2( + self, + slot: *mut Foo<'a, T, N>, + ) -> ::pin_init::__internal::Slot<::pin_init::__internal::Unpinned, usize> { + unsafe { ::pin_init::__internal::Slot::new(&raw mut (*slot).2) } + } + } + 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 { + __phantom: ::pin_init::__internal::PhantomInvariant::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 93d4fe851af8c703ccbf4e38e45d5356a4bfc2f1 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 | 213 +++++++++++++----- 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_constructor_cfg_non_last.rs | 12 + .../tuple_constructor_cfg_non_last.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 | 33 +++ .../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 +- 19 files changed, 486 insertions(+), 62 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_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/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 3862bd3d..66d0e417 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 28d30805..2c1b1f7f 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, + delim_close_span: Span, fields: Punctuated, rest: Option<(Token![..], Expr)>, error: Option<(Token![?], Type)>, @@ -36,11 +37,11 @@ struct InitializerField { enum InitializerKind { Value { - ident: Ident, + member: Member, value: Option<(Token![:], Expr)>, }, Init { - ident: Ident, + member: Member, _left_arrow_token: Token![<-], value: Expr, }, @@ -52,14 +53,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 +88,7 @@ pub(crate) fn expand( attrs, this, path, - brace_token, + delim_close_span, fields, rest, error, @@ -96,7 +111,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(delim_close_span, "expected `? ` after initializer"); parse_quote!(::core::convert::Infallible) } }, @@ -229,9 +244,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 +256,28 @@ 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. + // - `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) }) } } 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 +287,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,15 +307,20 @@ fn init_fields( } InitializerKind::Code { .. } => unreachable!(), }; + let binding = member_binding(member).map(|ident| { + quote! { + #(#cfgs)* + // Allow `non_snake_case` since the same warning is going to be reported for the + // struct field. + #[allow(unused_variables, non_snake_case)] + let #ident = #guard.let_binding(); + } + }); res.extend(quote! { #init - #(#cfgs)* - // Allow `non_snake_case` since the same warning is going to be reported for the struct - // field. - #[allow(unused_variables, non_snake_case)] - let #ident = #guard.let_binding(); + #binding }); guards.push(guard); @@ -324,9 +345,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! { @@ -362,36 +383,91 @@ 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 mut fields = Punctuated::new(); + + while !content.is_empty() { + let attrs = content.call(Attribute::parse_outer)?; + if let Some(cfg) = attrs.iter().find(|attr| attr.path().is_ident("cfg")) { + return Err(syn::Error::new_spanned( + cfg, + "`#[cfg]` is not supported in tuple constructor syntax", + )); + } + if content.peek(Token![<-]) { + return Err(content.error( + "`<-` is not supported in tuple constructor syntax; use braces with indices, e.g. `Type { 0 <- init, 1: value }`", + )); + } + let value: Expr = content.parse()?; + let index = fields.len() as u32; + fields.push_value(InitializerField { + attrs, + kind: InitializerKind::Value { + member: Member::Unnamed(Index { + index, + span: value.span(), + }), + value: Some((Token![:](value.span()), value)), + }, + }); + if content.is_empty() { + break; + } + fields.push_punct(content.parse()?); + } + 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 (delim_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()?))) @@ -411,7 +487,7 @@ impl Parse for Initializer { attrs, this, path, - brace_token, + delim_close_span, fields, rest, error, @@ -454,22 +530,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()) } 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_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..13737f32 --- /dev/null +++ b/tests/ui/compile-fail/init/tuple_constructor_cfg_non_last.stderr @@ -0,0 +1,5 @@ +error: `#[cfg]` is not supported in tuple constructor syntax + --> tests/ui/compile-fail/init/tuple_constructor_cfg_non_last.rs:8:9 + | +8 | #[cfg(any())] + | ^^^^^^^^^^^^^ 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..f18d6e88 --- /dev/null +++ b/tests/ui/compile-fail/init/tuple_invalid_field.stderr @@ -0,0 +1,33 @@ +error[E0599]: no method named `_2` found for struct `__ThePinData` in the current scope + --> tests/ui/compile-fail/init/tuple_invalid_field.rs:7:13 + | +3 | #[pin_data] + | ----------- method `_2` not found for this struct +... +7 | let _ = pin_init!(Tuple { 0: 1, 1: 2, 2: 3 }); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `pin_init` (in Nightly builds, run with -Z macro-backtrace for more info) + +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 48b3444c..79517faf 100644 --- a/tests/ui/expand/tuple_struct.expanded.rs +++ b/tests/ui/expand/tuple_struct.expanded.rs @@ -136,4 +136,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: loop {}, + 1: loop {}, + 2: loop {}, + }, + ) + }; + 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: loop {}, + 1: loop {}, + 2: loop {}, + }, + ) + }; + 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 01734c5e3a240ee7158802c0beba2572cb725b44 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 65d2dc5047a14381e35220dc78bc529d1bce7f45 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 | 35 ++++++--- internal/src/pin_data.rs | 63 +++++----------- tests/cfgs.rs | 73 ++++++++++++++++++- .../tuple_constructor_cfg_non_last.stderr | 2 +- .../pin_data/tuple_struct_cfg_non_last.rs | 6 ++ .../pin_data/tuple_struct_cfg_non_last.stderr | 5 ++ 6 files changed, 127 insertions(+), 57 deletions(-) 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 2c1b1f7f..8ae5639b 100644 --- a/internal/src/init.rs +++ b/internal/src/init.rs @@ -20,6 +20,7 @@ pub(crate) struct Initializer { path: Path, delim_close_span: Span, fields: Punctuated, + is_tuple_constructor: bool, rest: Option<(Token![..], Expr)>, error: Option<(Token![?], Type)>, } @@ -90,6 +91,7 @@ pub(crate) fn expand( path, delim_close_span, fields, + is_tuple_constructor, rest, error, }: Initializer, @@ -97,6 +99,10 @@ pub(crate) fn expand( pinned: bool, dcx: &mut DiagCtxt, ) -> Result { + if is_tuple_constructor { + check_tuple_constructor_cfgs(&fields, dcx)?; + } + let error = error.map_or_else( || { if let Some(default_error) = attrs.iter().fold(None, |acc, attr| { @@ -424,12 +430,6 @@ fn parse_paren_initializer(input: syn::parse::ParseStream<'_>) -> syn::Result<(S while !content.is_empty() { let attrs = content.call(Attribute::parse_outer)?; - if let Some(cfg) = attrs.iter().find(|attr| attr.path().is_ident("cfg")) { - return Err(syn::Error::new_spanned( - cfg, - "`#[cfg]` is not supported in tuple constructor syntax", - )); - } if content.peek(Token![<-]) { return Err(content.error( "`<-` is not supported in tuple constructor syntax; use braces with indices, e.g. `Type { 0 <- init, 1: value }`", @@ -455,16 +455,32 @@ fn parse_paren_initializer(input: syn::parse::ParseStream<'_>) -> syn::Result<(S Ok((paren_token.span.close(), fields)) } +fn check_tuple_constructor_cfgs( + fields: &Punctuated, + dcx: &mut DiagCtxt, +) -> Result<(), ErrorGuaranteed> { + for field in fields.iter().take(fields.len().saturating_sub(1)) { + if let Some(attr) = field.attrs.iter().find(|attr| attr.path().is_ident("cfg")) { + return Err(dcx.error( + attr, + "`#[cfg]` on tuple constructor arguments is only supported on the last argument", + )); + } + } + Ok(()) +} + 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 (delim_close_span, fields, rest) = if input.peek(token::Brace) { - parse_brace_initializer(input)? + let (delim_close_span, fields, is_tuple_constructor, rest) = if input.peek(token::Brace) { + let (close_span, fields, rest) = parse_brace_initializer(input)?; + (close_span, fields, false, rest) } else if input.peek(token::Paren) { let (close_span, fields) = parse_paren_initializer(input)?; - (close_span, fields, None) + (close_span, fields, true, None) } else { return Err(input.error("expected curly braces or parentheses")); }; @@ -489,6 +505,7 @@ impl Parse for Initializer { path, delim_close_span, fields, + is_tuple_constructor, rest, error, }) diff --git a/internal/src/pin_data.rs b/internal/src/pin_data.rs index 045f2ec4..dca59a0e 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, - Attribute, Field, Fields, Generics, Ident, Index, Item, Member, Meta, PathSegment, Type, - TypePath, Visibility, WhereClause, + Attribute, Field, Fields, Generics, Ident, Index, Item, Member, PathSegment, Type, TypePath, + Visibility, WhereClause, }; use crate::diagnostics::{DiagCtxt, ErrorGuaranteed}; @@ -63,15 +63,6 @@ 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, @@ -413,6 +404,10 @@ fn generate_projections( for field in fields { let Field { vis, ty, attrs, .. } = &field.field; let member = &field.member; + let cfgs: Vec<_> = attrs + .iter() + .filter(|attr| attr.path().is_ident("cfg")) + .collect(); if field.pinned { fields_decl.push(quote!( @@ -420,6 +415,7 @@ fn generate_projections( #vis ::core::pin::Pin<&'__pin mut #ty>, )); field_projections.push(quote!( + #(#cfgs)* // SAFETY: this field is structurally pinned. unsafe { ::core::pin::Pin::new_unchecked(&mut #this.#member) }, )); @@ -428,42 +424,12 @@ fn generate_projections( #(#attrs)* #vis &'__pin mut #ty, )); - field_projections.push(quote!(&mut #this.#member,)); + field_projections.push(quote!( + #(#cfgs)* + &mut #this.#member, + )); } } - let projection_init = if let Some(last_field) = fields.last() { - if let Some(cfg) = cfg_condition(&last_field.field.attrs) { - let field_projections_without_last = - &field_projections[..field_projections.len() - 1]; - quote! {{ - #[cfg(#cfg)] - { - #projection( - #(#field_projections)* - ::core::marker::PhantomData, - ) - } - #[cfg(not(#cfg))] - { - #projection( - #(#field_projections_without_last)* - ::core::marker::PhantomData, - ) - } - }} - } else { - quote! {{ - #projection( - #(#field_projections)* - ::core::marker::PhantomData, - ) - }} - } - } else { - quote! { - #projection(::core::marker::PhantomData) - } - }; ( quote! { @@ -475,7 +441,12 @@ fn generate_projections( ::core::marker::PhantomData<&'__pin mut ()>, ) #whr; }, - projection_init, + quote! {{ + #projection( + #(#field_projections)* + ::core::marker::PhantomData, + ) + }}, ) }; 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.stderr b/tests/ui/compile-fail/init/tuple_constructor_cfg_non_last.stderr index 13737f32..d31f2057 100644 --- a/tests/ui/compile-fail/init/tuple_constructor_cfg_non_last.stderr +++ b/tests/ui/compile-fail/init/tuple_constructor_cfg_non_last.stderr @@ -1,4 +1,4 @@ -error: `#[cfg]` is not supported in tuple constructor syntax +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); + | ^^^^^^^^^^^^^^^^^^^^^^^^^