Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-481 · Case file with fixtures · Case 453 of 694 · Compiler evidence

Rust Array Lengths Need Compile-time Constants, Not let Values

A let value is runtime storage even when initialised from a literal. Use const for a true compile-time size, a const generic for caller-selected static size, or Vec and slices for runtime lengths.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
An immutable let still creates runtime storage, while rustc needs the array's usize length during type checking and layout.
First discriminating check
Use a named const or const generic for a true static invariant, or use Vec and slices when the length arrives during execution.

I bound let width = 4 and used it in [u8; width]. Rust reported E0435 because the array length appears in a type and the local binding is not a compile-time constant.

The failing fixture surprises people because the initializer is a literal and never changes. But let still creates runtime storage; immutability is not the same as constant evaluation.

Array length is part of type identity

[u8; 4] and [u8; 8] are different Rust types with different sizes and layouts. The compiler must know the length while checking types and laying out stack values.

The array type Reference defines the length operand as a constant expression of type usize.

No execution of main has happened when rustc needs that answer.

Use const for one compile-time value

The repaired fixture declares:

const WIDTH: usize = 4;
let values: [u8; WIDTH] = [0; WIDTH];

The constant-evaluation Reference describes expressions allowed in constant contexts. Naming the constant also gives the dimension one source of truth.

I use an uppercase domain name such as HEADER_BYTES when the size belongs to a format contract.

Use Vec when the size arrives at runtime

If a request, file, command-line flag, or negotiated protocol determines width, it cannot honestly become a fixed array type in ordinary runtime code. I use:

let values = vec![0_u8; width];

The vector stores length at runtime and usually allocates its elements on the heap. A borrowed slice &[u8] can expose runtime-sized contiguous data without transferring ownership.

I choose representation from when the size becomes known, not from a preference for array syntax.

Const generics express caller-selected static sizes

A reusable function can take const N: usize and work with [u8; N]. Each monomorphised use still has a compile-time length, but callers may select different constants.

This is suitable for hashes, fixed blocks, and protocol fields where length is a type-level guarantee. It is not a way to convert arbitrary runtime integers into types.

Large const-generic variation may increase generated code, so I measure when many sizes are instantiated.

const evaluation is restricted execution

Changing let to const works only if the initializer is allowed in a constant context. Runtime I/O, heap allocation in ordinary forms, environment state, and many function calls cannot be evaluated there.

The E0435 explanation gives the direct constant repair, but I still verify whether compile time is the right semantic moment for the value.

A const fn can participate when all operations meet const-evaluation rules.

Stack size and binary design still matter

A compile-time array may live on the stack depending on use and optimisation. Making a user-controlled size into a huge generated const is not automatically safe or efficient.

For large buffers, I consider heap allocation, streaming, bounded chunks, and target stack constraints. Type correctness does not replace capacity design.

Embedded targets may prefer fixed arrays precisely because allocation is unavailable, which makes conservative size contracts important.

Build-time generation is another boundary

A Cargo build script can generate Rust source or constants from configuration known during the build. That value then becomes compile-time input for the crate, but changing it requires recompilation.

I distinguish build-time configuration from runtime configuration and include rerun directives so Cargo invalidates output correctly. Otherwise a “constant” can become stale evidence.

My E0435 checklist

I pay attention to memory initialisation too. [0; N] evaluates a repeat expression under array rules, while vec![0; n] allocates runtime storage and can fail indirectly through allocation pressure. For large or externally supplied sizes, I enforce an upper bound before allocation and return a domain error. Turning a local into a const may make the type compile, but it can also bake an unexpectedly large object into stack frames or generic instantiations. I measure the actual target constraints.

  • Does this value appear in a type or another constant context?
  • Is it known during compilation or only during execution?
  • Would a named const express a real format invariant?
  • Should a const generic let callers choose among static sizes?
  • Does runtime variability require Vec, a slice, or streaming?
  • Is the initializer valid under const-evaluation rules?
  • What stack, allocation, and code-size costs follow?
  • Is build-time generation properly invalidated?

The core principle is that immutable runtime data and compile-time constants answer different questions. I put fixed sizes in the type system only when the build can know them and use runtime-sized containers when the environment supplies the dimension later.