Skip to content

internal: add tuple struct support in pin_data and pin_init! (V2) - #155

Open
mqqz wants to merge 5 commits into
Rust-for-Linux:mainfrom
mqqz:add_tuple_structs_v2
Open

internal: add tuple struct support in pin_data and pin_init! (V2)#155
mqqz wants to merge 5 commits into
Rust-for-Linux:mainfrom
mqqz:add_tuple_structs_v2

Conversation

@mqqz

@mqqz mqqz commented May 22, 2026

Copy link
Copy Markdown
Contributor

Replaces #113.

This adds tuple struct support to #[pin_data], init!, and pin_init!.

The projected form of a tuple struct is also a tuple struct, so projected fields are accessed with native tuple syntax (.0, .1, ...), which keeps the generated API closer to
ordinary Rust and avoids exposing synthetic field names (also makes it easier fields are cfg'd out).

  • tuple struct projections from #[pin_data]
  • tuple struct initialization with indexed brace syntax:
    • pin_init!(Foo { 0 <- ..., 1: ... })
  • tuple struct initialization with constructor syntax:
    • pin_init!(Foo(...)) (but no <- e.g. Foo(a, <- b, c) is rejected)
  • support for tuple structs with generics, const generics, pinned drop, and fallible partial initialization rollback
  • support for tuple structs when fields or constructor arguments are removed by #[cfg] (Error out unless #[cfg] is on the final tuple field)

Notes

  • tuple projections use native tuple field access rather than synthetic names e.g. ._0 like last time.
  • cfg handling does not try to evaluate user #[cfg] conditions in the proc macro
  • instead, the macro generates the necessary cfg-dependent layouts and lets rustc select the active branch
  • #[cfg] is supported only on the final tuple field / constructor argument, when it does not change numbering.

Tests

Added coverage for:

  • basic tuple struct projection
  • brace and constructor tuple initialization syntax
  • duplicate / missing / invalid tuple field UI failures
  • generic and const-generic tuple structs
  • #[pin] interaction with !Unpin field types
  • pinned drop
  • fallible partial-init rollback
  • cfg-stripped tuple fields (last or otherwise)
  • cfg-stripped tuple constructor arguments
  • feature-dependent cfg field layouts

Comparison with the old PR

This replaces the earlier attempt in #113.

Compared with that PR, this version is intentionally narrower and easier to review:

  • less code motion/churn overall (my code always got motion 😎)
  • fewer refactors mixed into the feature work
  • tuple projections are tuple structs instead of relying on synthetic internal/public field names
  • cfg handling was redesigned to correctly handle cfg-stripped tuple fields and constructor arguments simplified to accept only on last field/arg. or error out
  • the test set was trimmed to the non-redundant cases, while keeping the important regressions and restoring the fallible partial-init rollback coverage

Closes: #85

Comment thread internal/src/init.rs Outdated
@mqqz
mqqz force-pushed the add_tuple_structs_v2 branch from 1f2c279 to 2d2042e Compare May 25, 2026 16:32
Comment thread internal/src/init.rs
Comment thread internal/src/pin_data.rs Outdated
Comment on lines +45 to +58
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}`"),
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be methods of FieldInfo

Comment thread internal/src/pin_data.rs Outdated
));
field_projections.push(quote!(#binding,));
}
field_bindings.push(quote!(ref mut #binding,));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to use this instead of .0, .1, etc?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree; direct .0, .1 is nicer and more reasonable. Will fix

Comment thread internal/src/pin_data.rs Outdated
#ty,
>,
});
field_values.push(quote! {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is changing how tuple struct is handled to be completely different from normal structs?

There's a reason that methods are used, because they support HRTB, so it makes it possible to add self-reference support later (#156)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.. I'll make tuple fields use the same methods

Comment thread internal/src/init.rs Outdated
this: Option<This>,
path: Path,
brace_token: token::Brace,
close_span: Span,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
close_span: Span,
delim_close_span: Span,

Comment thread internal/src/init.rs Outdated
Comment on lines +577 to +580
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 }`",
));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd move this into parse_paren_initializer and drop the type.

Comment thread internal/src/pin_data.rs Outdated
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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should just unconditionally apply cfgs?

Comment thread internal/src/init.rs Outdated
let tuple_fields: Vec<_> = tuple_fields.into_iter().collect();
let mut fields = Punctuated::new();

for tuple_field in tuple_fields

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cfg rejection should happen before tuple struct is accepted at all, so there is no intermediate commit that allows it but performs incorrectly.

@mqqz
mqqz force-pushed the add_tuple_structs_v2 branch 4 times, most recently from 5a4e625 to 65d2dc5 Compare July 11, 2026 15:05
@mqqz

mqqz commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

I pushed new changes that addressed your comments and rebased.

@mqqz
mqqz requested a review from nbdd0121 July 11, 2026 15:24
Attribute macros are invoked without cfg being resolved. This adds quite a
bit complexity to the macro because all of the macro needs to be careful to
attach necessary cfgs. This becomes especially tricky for tuple structs. Thus,
it is convenient if cfgs are all resolved like derive macros.

The most optimal way to handle this is via `TokenStream::expand_expr`, but that is still unstable.
Implement an approach where we generate two cfg-gated macro invocations with cfg
resolved within the invocation. This would mean when the loop falls through, all field cfgs
are resolved, so remove all handling of cfg_attrs for the rest of the macro.

Signed-off-by: Gary Guo <gary@garyguo.net>
@nbdd0121

Copy link
Copy Markdown
Member

Can you rebase on #161?

@mqqz
mqqz force-pushed the add_tuple_structs_v2 branch from 65d2dc5 to 9a5c70e Compare July 24, 2026 23:51
mqqz added 4 commits July 25, 2026 03:04
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 <mo@sdhn.cc>
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 <mo@sdhn.cc>
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 <mo@sdhn.cc>
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 <mo@sdhn.cc>
@mqqz
mqqz force-pushed the add_tuple_structs_v2 branch from 9a5c70e to a61d14e Compare July 25, 2026 00:08
@mqqz
mqqz changed the base branch from main to dev/cfg July 25, 2026 00:15
@nbdd0121
nbdd0121 changed the base branch from dev/cfg to main July 27, 2026 10:53
Comment thread internal/src/pin_data.rs
else {
unreachable!()
};
let unpin_allow = if fields.iter().any(|f| matches!(f.member, Member::Named(_))) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather unconditionally allow non_snake_case. Same elsewhere.

Comment thread internal/src/pin_data.rs
// 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the comment changed?

Comment thread internal/src/pin_data.rs
Comment on lines +522 to +532
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.
},
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Comment thread internal/src/pin_data.rs
Comment on lines +567 to +569
let pin_data_new = quote! {
__ThePinData { __phantom: ::pin_init::__internal::PhantomInvariant::new() }
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Comment thread internal/src/init.rs

fn member_binding(member: &Member) -> Option<Ident> {
match member {
Member::Named(ident) => Some(ident.clone()),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to clone here.

Comment thread internal/src/init.rs
// - `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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why updating the comment here?

Comment thread internal/src/init.rs
path: Path,
delim_close_span: Span,
fields: Punctuated<InitializerField, Token![,]>,
is_tuple_constructor: bool,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the code wouldn't be over-complicated, I would change this the parsing to give two distinct types for tuple syntax and struct syntax.

pub(crate) struct InitStruct {
    path: Path,
    brace: token::Brace,
    fields: Punctuated<InitializerField, Token![,]>,
    rest: Option<(Token![..], Expr)>,
}

pub(crate) struct InitTuple {
    path: Path,
    paren: token::Paren,
    fields: Punctuated<Expr, Token![,]>,
}

impl InitTuple {
    fn to_init_struct(self) -> InitStruct { ... }
}

pub(crate) enum InitKind { .. }

pub(crate) struct Initializer { ... }

Comment thread internal/src/init.rs
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",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"`#[cfg]` on tuple constructor arguments is only supported on the last argument",
"`#[cfg]` is not supported in this position",

this whole commit should be folded into commit 2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Support for initializing tuple-structs

2 participants