Skip to content

Allow #[derive(IntoBytes)] to ignore unused const generic parameters - #3685

Open
barunaniket wants to merge 2 commits into
google:mainfrom
barunaniket:unused-const-generics
Open

barunaniket wants to merge 2 commits into
google:mainfrom
barunaniket:unused-const-generics

Conversation

@barunaniket

Copy link
Copy Markdown

Summary

Fixes #2723. Previously, deriving IntoBytes on a struct with any generic parameters -- including a const parameter that doesn't appear in any field's type, and thus can't affect layout -- forced every field type to implement Unaligned. This made code like the issue's repro impossible:

#[derive(IntoBytes, Immutable)]
#[repr(C)]
struct Tile<const L: usize>(u32, u32);

This detects structs whose only generic parameters are const parameters that don't appear anywhere in any field's type (conservatively, by token identity -- see the doc comment on generics_are_unused_consts for the shadowing edge case this over-approximates), and for those, proves the absence of padding via PaddingCheck::ReprCStruct instead of falling back to requiring Unaligned fields.

Why not just treat it like the no-generics case?

That was my first attempt, and it compiles the derive-macro-output-comparison tests fine, but fails on an actual real-world instantiation. PaddingCheck::Struct's struct_padding! expansion computes size_of::<Self>() inside an anonymous const (the PADDING_BYTES argument to PaddingFree<Self, { .. }>). Once the impl has a const generic parameter, Self is generic (e.g. Foo<{ N }>), and rustc rejects that:

error: generic `Self` types are currently not permitted in anonymous constants

PaddingCheck::ReprCStruct's repr_c_struct_has_padding! sidesteps this because its expansion never references its $t argument at all -- only the (concrete) field types and the align/packed literals go into the anonymous const. Self only appears as a standalone generic argument to DynamicPaddingFree, outside of the anonymous const, which rustc permits.

One limitation as a result: this currently only covers repr(C) structs, since repr_c_struct_has_padding! requires that repr. repr(Rust) structs with unused const generics still require Unaligned fields (same as before this change). Happy to discuss whether that's worth lifting in a follow-up if there's a similar trick available, but didn't want to hold up the common case on it.

Test plan

  • Added test_into_bytes_struct_unused_const_generic (derive-output snapshot test) covering the issue's exact shape
  • Added test_into_bytes_struct_used_const_generic (derive-output snapshot test) as a negative case, confirming a used const param still requires Unaligned (guards the "is it actually unused" detection)
  • Added test_into_bytes_unused_const_generic_derive in zerocopy's own test suite -- an actual compiling+running end-to-end test (cargo test --features derive), not just a token-stream comparison, since the snapshot tests alone wouldn't have caught the anonymous-const rustc limitation above
  • cargo test -p zerocopy-derive --lib -- full suite passes (36 tests)
  • cargo test -p zerocopy --lib --features derive -- full suite passes (117 tests)
  • cargo clippy -p zerocopy-derive --lib -- -D warnings -- clean
  • rustfmt +nightly --check on all touched files -- clean
  • Confirmed no existing tests/ui/*.rs compile-fail fixture exercises IntoBytes + const generics, so nothing regresses there

Previously, any generic parameter on a struct -- including a const
parameter that doesn't appear in any field's type, and thus cannot
affect layout -- forced all field types to implement Unaligned. This
made it impossible to derive IntoBytes on, e.g., a repr(C) struct with
an unused const generic tag, even though nothing about its layout
actually depends on that parameter.

Detect structs whose only generic parameters are const parameters not
referenced by any field, and for those, prove the absence of padding
via PaddingCheck::ReprCStruct rather than requiring Unaligned fields.

Struct::Struct's own padding check can't be reused here: its
struct_padding! expansion computes size_of::<Self>() inside an
anonymous const, and Self is generic once const parameters are
present (e.g. Foo<{ N }>), which rustc rejects ("generic `Self` types
are currently not permitted in anonymous constants").
ReprCStruct's repr_c_struct_has_padding! sidesteps this: its expansion
never references its $t argument, only the (concrete) field types, so
Self only appears as a standalone generic argument outside of the
anonymous const, which is permitted. This does mean the fix is
currently limited to repr(C) structs; repr(Rust) structs with unused
const generics still require Unaligned fields.

Fixes google#2723
@codecov-commenter

codecov-commenter commented Sep 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.90%. Comparing base (f2c99f9) to head (c48a9c7).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3685   +/-   ##
=======================================
  Coverage   91.90%   91.90%           
=======================================
  Files          20       20           
  Lines        6175     6179    +4     
=======================================
+ Hits         5675     5679    +4     
  Misses        500      500           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4eca67f9f4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


fn contains_ident(tokens: TokenStream, ident: &Ident) -> bool {
tokens.into_iter().any(|tt| match tt {
TokenTree::Ident(i) => i == *ident,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize raw const identifiers before comparing

When a const parameter is declared with raw spelling but referenced ordinarily, as in struct Foo<const r#N: usize>([u8; N], u8), proc_macro2::Ident equality does not consider r#N and N equal. This therefore misclassifies N as unused and emits the anonymous-const padding check, which stable Rust rejects because that check now contains the generic N; the same derive compiled against the parent revision. Compare normalized spellings, for example with the existing to_ident_str helper, rather than using i == *ident.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in c48a9c7 — compare identifiers via to_ident_str instead of raw Ident equality, so r#N and N are treated as the same parameter.

Comment on lines +121 to +123
!ast.generics
.const_params()
.any(|param| fields.iter().any(|(_, _, ty)| contains_ident(quote!(#ty), &param.ident)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat type macros as potential const-generic uses

A field type macro can reference the surrounding const parameter without placing its identifier in the derive input, for example macro_rules! field { () => { [u8; N] } } followed by struct Foo<const N: usize>(field!(), u8). This scan classifies that struct as having an unused N, but the macro subsequently expands inside repr_c_struct_has_padding!, where stable Rust rejects the generic parameter in the anonymous const; the parent revision instead emitted Unaligned bounds and compiled this input. Conservatively return false whenever a field type contains a macro invocation, including nested invocations.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in c48a9c7 — the token scan now conservatively treats any macro invocation (any ! token) in a field's type as risky, falling back to the Unaligned bounds.

//
// This requires `repr(C)`, which `repr_c_struct_has_padding!`
// requires of its input.
(Some(PaddingCheck::ReprCStruct), false, None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve support for unsized trailing field types

For an unused const parameter on a struct such as #[repr(C)] struct Foo<const N: usize>(u8, str), this branch replaces the previous valid IntoBytes + Unaligned bounds with repr_c_struct_has_padding!. That macro only recognizes syntactic slice fields as unsized, so it sends str, a slice alias, or a parenthesized slice to DstLayout::for_unpadded_type<T>(), whose implicit T: Sized bound makes the derive fail; the same str example compiles against the parent revision. Retain the old Unaligned fallback for potentially unsized trailing types or extend the padding check to obtain their DST layout.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in c48a9c7 — added a guard that bails out of the new branch when the trailing field is str. Noted in the comment that this doesn't cover every unsized type hiding behind a non-slice-syntax alias (a narrower, pre-existing limitation shared with the no-generics case's is_syntactic_dst check), which is now a known, documented gap rather than a silent compile failure.

Comment on lines +194 to +196
// `PaddingCheck::ReprCStruct`'s `repr_c_struct_has_padding!`
// expansion sidesteps this: it never mentions `$t` in its body, only
// the (concrete) field types and the `align`/`packed` literals. Its

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep generic Self out of the anonymous padding const

A field type can contain Self without spelling any const parameter, for example #[repr(C)] struct Foo<const N: usize>(PhantomData<Self>, u8). The predicate classifies N as unused, but copying PhantomData<Self> into the padding macro places the generic Self inside the anonymous const and stable Rust rejects the derive; this example compiled with the parent revision's Unaligned bounds. Treat any Self reference in a field type as preventing this optimization and fall back to the previous bounds.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in c48a9c7 — the token scan now also bails out when a field's type contains Self, since Self is generic over all of the struct's parameters regardless of whether it spells any of their identifiers.

} else {
(Some(PaddingCheck::Struct), false, None)
}
} else if is_c && generics_are_unused_consts(&ctx.ast, &strct.fields()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Document the unused-const analysis branch

The public IntoBytes derive analysis in zerocopy/src/lib.rs still says that a generic repr(C) struct outside the homogeneous case requires every field to implement Unaligned, which is directly contradicted by this new branch and its u32 regression test. Add the unused-const exception to that analysis so users do not rely on obsolete guidance when interpreting derive failures.

AGENTS.md reference: zerocopy/AGENTS.md:L37-L38

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in c48a9c7 — added a bullet to the IntoBytes derive's Analysis section documenting the new exception.

Four edge cases in the unused-const-generic-parameters detection could
misclassify a parameter as unused, causing the derive to attempt the
new padding check and fail to compile (previously-working code would
stop compiling, though never unsoundly):

- A const parameter declared with raw syntax (e.g. `r#N`) wasn't
  recognized as the same identifier as an ordinary reference to it
  (`N`) in a field type, since token equality doesn't normalize raw
  identifiers. Compare via the existing `to_ident_str` helper instead.
- A field mentioning `Self` implicitly depends on all of the struct's
  generic parameters (Self is the struct at the same arguments as the
  impl), even if it doesn't spell any of their identifiers.
- A field type built by a macro invocation could expand to something
  that depends on a const parameter without that parameter's
  identifier appearing anywhere in the derive input's tokens.
- A trailing field written as `str` (or any other unsized type not
  spelled with literal `[T]` syntax) isn't recognized by
  repr_c_struct_has_padding!'s slice-detecting macro arm, so it falls
  to a helper with an implicit `Sized` bound that `str` can't satisfy.
  This is a narrower, pre-existing limitation of that macro (shared
  with the no-generics case's `is_syntactic_dst` check) that the new
  branch would otherwise have newly exposed for generic structs.

Fold the first three checks into one conservative token scan and gate
the fourth at the call site, since it's about field shape rather than
the generic parameters themselves. Also note the new exception in the
IntoBytes derive's "Analysis" doc section, which still described the
pre-existing Unaligned-for-all-fields requirement unconditionally.

Add a regression test for each case, confirming the derive falls back
to requiring `Unaligned` fields rather than attempting (and failing)
the new padding check.
@barunaniket

Copy link
Copy Markdown
Author

@joshlf whenever you have time please review it and then merge it.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

#[derive(IntoBytes)] should ignore unused const arguments

2 participants