Skip to content

Lang proposal: const expressions in #[repr(align)] and #[repr(packed)] and friends. #155390

@BarronKane

Description

@BarronKane

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

  1. The argument must resolve to a const of the type the attribute already requires (for align and packed, a power-of-two usize).
  2. 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.)
  3. Macro calls (align(some_macro!())) are out of scope. Only paths to already-defined constants and expressions over such paths are permitted.
  4. 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

  1. 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.
  1. 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.

  1. Scope. Is #[repr(packed(N))] in scope alongside #[repr(align(N))] for the initial implementation, or should it be deferred?
  2. 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?
  3. 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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageThis issue may need triage. Remove it if it has been sufficiently triaged.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions