Background
Several built-in attributes accept only integer literals where the argument is semantically a compile-time constant. The most common case is #[repr(align(N))], where N must be a literal even when the appropriate value is target-dependent and already exists as a const somewherein the crate graph.
Today this forces duplication across cfg branches:
#[cfg(target_arch = "x86_64")]
#[repr(align(64))]
pub struct CachePadded<T>(T);
#[cfg(target_arch = "aarch64")]
#[repr(align(128))]
pub struct CachePadded<T>(T);
#[cfg(any(target_arch = "arm", target_arch = "riscv32"))]
#[repr(align(32))]
pub struct CachePadded<T>(T);
The same cfg ladder appears across the ecosystem with concurrency primitives (cache-line padding), crypto (side-channel hardening), embedded HALs (DMA buffer alignment), and any crate that aligns to a target-dependent value. The constant is known at compile time; the attribute just refuses to accept it in non-literal form.
C and C++ already support this via _Alignas(expr) / alignas(expr). Zig supports it via align(expr).
Proposal
Allow #[repr(align(N))] and #[repr(packed(N))] to accept paths to const items and constant expressions composed of such paths, in addition to integer literals.
pub const CACHELINE: usize = {
#[cfg(target_arch = "x86_64")] { 64 }
#[cfg(target_arch = "aarch64")] { 128 }
#[cfg(any(target_arch = "arm", target_arch =
"riscv32"))] { 32 }
};
#[repr(align(CACHELINE))]
pub struct CachePadded<T>(T);
Proposed rules
- The argument must resolve to a const of the type the attribute already requires (for align and packed, a power-of-two usize).
- Resolution happens at attribute-processing time. The path must be visible in the dependency graph already established at that point — using a path that would require pulling in a new crate is an error. (I think this one is standard, I'm still learning the limitations of cargo dependency resolution.)
- Macro calls (align(some_macro!())) are out of scope. Only paths to already-defined constants and expressions over such paths are permitted.
- Errors at the attribute site (non-power-of-two, type mismatch, unresolved path) render at the attribute, not at the constant's definition.
Whatever form the backend machinery takes, the idea is to keep the semantics open and compatible with potential future expression resolution. I think it's important to be forward looking here.
Questions
- Syntactic vehicle. Should the surface syntax remain
#[repr(align(N))] (with N resolving to a constant), or should the feature land through a different syntactic form?
Options raised so far in PR #154708
- Keep
#[repr(align(N))] as-is, with N extended to accept constant paths and expressions. Argued for by me and @programmerjake on the grounds that existing attribute syntax extends naturally
- Treat the affected built-in attributes as active built-in attributes hardcoded in
rustc_expand (the model used by cfg / cfg_attr).
- Implement as a built-in proc macro modeled on
#[define_opaque], with the surface attribute
preserved as the macro's invocation form. Suggested by @petrochenkov
- A new dedicated syntax (e.g.,
align(CACHELINE) struct Foo;) — significantly larger scope, mentioned only for completeness.
- Backend mechanism. Independent of the syntactic vehicle, how should the constant payload be represented and resolved internally?
- (A) Extend the existing attribute IR to resolve constant paths during attribute lowering
- (B) Introduce a proper AST node (e.g.,
ast::Repr) carrying ast::AnonConst payloads, lowered through the usual AST → HIR → MIR pipeline.
The choice constrains and is constrained by the syntactic vehicle, which is why both questions appear here together.
- Scope. Is
#[repr(packed(N))] in scope alongside #[repr(align(N))] for the initial implementation, or should it be deferred?
- String-valued attributes.
#[link_section = "..."] and #[export_name = "..."] have analogous motivation (target-dependent values forced into cfg ladders). Should they be considered in the same proposal, a follow-up, or deliberately left out?
- Process. Is this an experiment-gated feature, or does it need a full RFC before implementation? @nnethercote raised this at the start of the PR review.
Prior Art
cc @rust-lang/lang
Note: This is my first PR/Issue in rust itself, though I've used it and many other languages all my life, I'm very new to contributing to projects of this scale. I've been told a couple times that this is unreasonable as a first-time contribution, but this started as just a private "what if?" purely at parse time, and it just exploded from there after discussions were had. Regardless, I'm happy to do the leg work, and I'm learning a hell of a lot in the process, so I appreciate all the insight I've been given by everyone!
Background
Several built-in attributes accept only integer literals where the argument is semantically a compile-time constant. The most common case is
#[repr(align(N))], whereNmust be a literal even when the appropriate value is target-dependent and already exists as aconstsomewherein the crate graph.Today this forces duplication across
cfgbranches:The same cfg ladder appears across the ecosystem with concurrency primitives (cache-line padding), crypto (side-channel hardening), embedded HALs (DMA buffer alignment), and any crate that aligns to a target-dependent value. The constant is known at compile time; the attribute just refuses to accept it in non-literal form.
C and C++ already support this via
_Alignas(expr)/alignas(expr). Zig supports it viaalign(expr).Proposal
Allow
#[repr(align(N))]and#[repr(packed(N))]to accept paths to const items and constant expressions composed of such paths, in addition to integer literals.Proposed rules
Whatever form the backend machinery takes, the idea is to keep the semantics open and compatible with potential future expression resolution. I think it's important to be forward looking here.
Questions
#[repr(align(N))](with N resolving to a constant), or should the feature land through a different syntactic form?Options raised so far in PR #154708
#[repr(align(N))]as-is, with N extended to accept constant paths and expressions. Argued for by me and @programmerjake on the grounds that existing attribute syntax extends naturallyrustc_expand(the model used by cfg / cfg_attr).#[define_opaque], with the surface attributepreserved as the macro's invocation form. Suggested by @petrochenkov
align(CACHELINE) struct Foo;) — significantly larger scope, mentioned only for completeness.ast::Repr) carryingast::AnonConstpayloads, lowered through the usual AST → HIR → MIR pipeline.The choice constrains and is constrained by the syntactic vehicle, which is why both questions appear here together.
#[repr(packed(N))]in scope alongside#[repr(align(N))]for the initial implementation, or should it be deferred?#[link_section = "..."]and#[export_name = "..."]have analogous motivation (target-dependent values forced into cfg ladders). Should they be considered in the same proposal, a follow-up, or deliberately left out?Prior Art
repr(tag = TypeAlias), currently S-waiting-on-team. Same tension between attributes-as-tokens and attributes-carrying-real-code, broader scope#[define_opaque](compiler/rustc_builtin_macros/src/define_opaque.rs) — existing built-in attribute that produces a proper AST node through a built-in proc macro. Reference implementationpattern
cfg/cfg_attr— existing precedent for active built-in attributes producing AST inrustc_expand.cc @rust-lang/lang
Note: This is my first PR/Issue in rust itself, though I've used it and many other languages all my life, I'm very new to contributing to projects of this scale. I've been told a couple times that this is unreasonable as a first-time contribution, but this started as just a private "what if?" purely at parse time, and it just exploded from there after discussions were had. Regardless, I'm happy to do the leg work, and I'm learning a hell of a lot in the process, so I appreciate all the insight I've been given by everyone!