Skip to content

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

Open
mqqz wants to merge 4 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 4 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 Outdated
Comment thread internal/src/pin_data.rs Outdated
Comment thread internal/src/pin_data.rs Outdated
Comment thread internal/src/pin_data.rs Outdated
Comment thread internal/src/init.rs Outdated
Comment thread internal/src/init.rs Outdated
Comment thread internal/src/pin_data.rs Outdated
Comment thread internal/src/init.rs Outdated
@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.

@nbdd0121

Copy link
Copy Markdown
Member

Can you rebase on #161?

@mqqz
mqqz force-pushed the add_tuple_structs_v2 branch 2 times, most recently 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 Outdated
Comment thread internal/src/pin_data.rs Outdated
Comment thread internal/src/pin_data.rs Outdated
Comment thread internal/src/pin_data.rs Outdated
Comment thread internal/src/init.rs Outdated
Comment thread internal/src/init.rs Outdated
Comment thread internal/src/init.rs Outdated
Comment thread internal/src/init.rs Outdated
@nbdd0121

nbdd0121 commented Aug 3, 2026

Copy link
Copy Markdown
Member

Could you handle cfgs in a way similar to #165? Unlike #[pin_data], I am inclining not to always expand out cfg in init!() as there is no other need for this, unlike #[pin_data] where self-ref pin init also needs expanded cfgs, so #165 is probably not going to be merged, but the approach can work for tuple structs.

@mqqz

mqqz commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

I'll rethink this PR as a whole and double check (the last few fixes were rushed because they were slaving me away at work)

@nbdd0121

nbdd0121 commented Aug 4, 2026

Copy link
Copy Markdown
Member

Thanks for working on this!

@mqqz
mqqz force-pushed the add_tuple_structs_v2 branch from a61d14e to ec74456 Compare August 28, 2026 11:42
@mqqz

mqqz commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

oh its my absolute pleasure to work on this. I took my time to fix botched rebase and clean up commits. Also, I think CI is broken now.

@mqqz
mqqz force-pushed the add_tuple_structs_v2 branch from ec74456 to 3ec0b2f Compare August 28, 2026 15:19
nbdd0121 and others added 4 commits September 2, 2026 18:10
Create a new `util.rs` to host utility code that are generic and can be
shared by multiple macros.

Signed-off-by: Gary Guo <gary@garyguo.net>
`#[pin_data]` rejects tuple structs because it assumes every field has a
name, which it uses for the projection field, the `__Unpin` field and the
pin-data accessor.

Identify fields by `syn::Member` instead, so that tuple fields are referred
to by their index in generated field accesses. The names that generated
items still need are derived from the index as `_0`, `_1`, etc.

The projection of a tuple struct is a tuple struct itself, so projected
fields are accessed with the same `.0`, `.1` syntax as on the input
type rather than through synthesised names.

Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
[ Moved utility code to util.rs as extension trait - Gary ]
Signed-off-by: Gary Guo <gary@garyguo.net>
Extend the initializer syntax so that a field can be named by an index,
addressing tuple struct fields the same way a struct expression does:

    pin_init!(Foo { 0: value, 1 <- initializer })

Tuple fields are not exposed by a `let` binding to the fields after them,
since they have no name to bind; `_0` would shadow a user variable.

Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
[ Fixed incorrect index calculation and cleaned up the code - Gary ]
Signed-off-by: Gary Guo <gary@garyguo.net>
A tuple struct whose fields are all set to a value reads better written
like a call to its constructor than with the indices spelled out:

    pin_init!(Foo(value, value))

Parse the two forms into separate types and rewrite the constructor
arguments into the indexed fields they are shorthand for, so that only the
parser has to know about the second form.

The arguments have no names, so they cannot use `<-`. Parse it anyway and
reject it afterwards, which reports the position of every offending `<-`
rather than stopping at the first one.

`cfg` needs different treatment for tuple constructor syntax. As non-derive
proc macros are invoked before cfg is resolved, the macro cannot know
whether a field survives, and dropping a tuple field renumbers every field
after it. That cannot be expressed by attaching a `cfg` attribute to the
initializer of a single field. Thus, resolve tuple field cfgs up front
instead, by generating two cfg-gated invocations of the macro with one
field resolved in each. This is the approach of commit 3445a65
("internal: rework how `#[pin_data]` handles cfg"), and it is linear time
because only one of the two branches is ever expanded. Struct expression
syntax do not renumber, so using tuple structs with struct syntax can keep
using the existing attribute-based handling.

Suggested-by: Gary Guo <gary@garyguo.net>
Link: Rust-for-Linux#165
Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
Co-developed-by: Gary Guo <gary@garyguo.net> # cfg expansion
[ Use generics instead of separate types for normalization - Gary ]
Signed-off-by: Gary Guo <gary@garyguo.net>
Comment thread internal/src/init.rs Outdated
Comment on lines +278 to +285
// Removing a tuple field shifts every field after it down by one.
for field in init.fields.iter_mut() {
if let Some(Member::Unnamed(index)) = field.kind.member_mut() {
if index.index > removed_index {
index.index -= 1;
}
}
}

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.

Unfortunately this logic is completely broken, because index in struct expression are never renumbered.

@nbdd0121
nbdd0121 force-pushed the add_tuple_structs_v2 branch from 3ec0b2f to 5d04c89 Compare September 2, 2026 17:35
@nbdd0121

nbdd0121 commented Sep 2, 2026

Copy link
Copy Markdown
Member

I've updated the PR with tuple index renumbering fixed (and some misc code style improvements too). Can you check if you're happy with the outcome?

Comment thread internal/src/init.rs
Comment on lines +69 to +76
let span = field.value.span();
let field = InitializerField {
attrs: field.attrs,
kind: InitializerKind::Value {
member: Member::Unnamed(index.into()),
value: Some((Token![:](span), field.value)),
},
};

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.

why not keep the span since we already have it in scope

      kind: InitializerKind::Value {
          member: Member::Unnamed(Index {
              index: index as u32,
              span,
          }),
          value: Some((Token![:](span), field.value)),
      },

not that it really matters, it's only for nicer diagnostics like "available fields are: 0, 1"

Comment thread src/lib.rs
/// As already mentioned in the examples above, inside of `pin_init!` a struct initializer with the
/// following modifications is expected:
/// - Fields that you want to initialize in-place have to use `<-` instead of `:`.
/// - Tuple struct fields are named by their index, as in `0: value` or `0 <- initializer`. They

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.

maybe we should include a comment about how tuple indexes are post-cfg (same as Rust) I feel like it can a bit of a footgun, something like "note that disabled fields don't occupy an index"

@mqqz

mqqz commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

thank you Gary for the fixes, looks much better now! I have left some inconsequential nitpicks but apart from that it looks good

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