Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-507 · Case file with fixtures · Case 479 of 694 · Compiler evidence

Recursive Rust Types Need an Indirection Boundary

A recursive value cannot contain itself inline without end. Put the recursive edge behind a fixed-size pointer such as Box, Rc, Arc, or a reference chosen for the ownership model.

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
The recursive variant contains its own complete inline layout, producing an equation whose size grows with every expansion and never reaches a fixed value.
First discriminating check
Locate the cycle and put a suitable fixed-size Box, shared pointer, reference, or arena index on one natural edge based on the real ownership model.

I defined Chain::Link(u8, Chain). Rust emitted E0072 because the enum would need to contain another complete Chain inline, which contains another complete Chain, without any finite end to its size calculation.

The failing fixture has an End variant, but that runtime possibility does not solve compile-time layout. Rust must reserve enough space for any variant before knowing which value will be stored.

Inline recursion creates an impossible size equation

An enum is large enough to hold its largest variant plus discriminant and alignment needs. If the largest variant includes the enum itself directly, its size depends on its own full size.

The official E0072 explanation presents this as an unbounded equation. Adding one more level never reaches a fixed number of bytes.

Rust needs a finite size for stack slots, function calling, moves, and containing types.

A pointer has a known size

The repaired fixture stores Box<Chain> on the recursive edge. The box handle has a fixed size, while the next node's allocation lives elsewhere.

Now the enum layout contains one byte value, a fixed-size pointer representation, and its enum metadata. The chain depth affects heap allocation count, not the size of one Chain value.

The fixture constructs one link and recursively measures depth to verify the usable repair.

Box is one ownership choice, not the only syntax fix

Box<T> expresses one owning pointer to T. It is a strong default for trees and linked structures with unique ownership.

Rc<T> supports shared ownership in one thread, while Arc<T> supports atomic reference counting across threads when the contained type and use satisfy thread-safety requirements. References borrow nodes owned elsewhere and require a lifetime model. Arena indexes store compact handles into separate storage.

I choose based on ownership, mutation, sharing, allocation, and traversal needs rather than selecting Box automatically.

Indirection can appear on any cycle edge

Mutually recursive types can fail even when neither directly names itself. If A contains B and B contains A, at least one edge in that cycle needs indirection.

The best edge is often the one that matches optionality or ownership. Moving a pointer can affect cache locality and allocation behaviour, so I consider the data's common shape.

One finite boundary breaks the layout equation; not every field must be boxed.

Option does not provide indirection

Option<Chain> is still inline because Option contains its payload within its own storage. It can represent absence at runtime, but the present case still contains a full Chain.

Option<Box<Chain>> works because the payload is the fixed-size box handle. This distinction is easy to miss when “optional next node” sounds conceptually finite.

Runtime termination and compile-time representation are separate questions.

Recursive drop and traversal deserve attention

Fixing layout makes the type valid, but a very deep recursively owned list can still use deep call stacks during recursive algorithms or destruction. Production limits may need iterative traversal, arenas, or explicit depth controls.

The Rust Book section on recursive types uses boxes to teach the representation boundary. I add workload tests for realistic depth rather than stopping at successful compilation.

Collections can replace one allocation per node

A linked Box structure is not always the fastest or simplest representation. If order is the main requirement, Vec<T> stores elements contiguously and usually improves locality. For graphs, a vector of nodes plus integer indexes can make ownership clearer and avoid reference-count cycles.

I keep recursive enums when their shape and variants carry meaning, such as syntax trees. I choose a collection when recursion is only an implementation of sequence or graph storage. The compiler error asks for finite layout; it does not insist that a pointer-based recursive structure is the final design.

My E0072 checklist

  • Where does the type cycle return to itself?
  • Is the recursive member stored inline or behind indirection?
  • Does Option only express absence without breaking layout recursion?
  • Should ownership be unique, shared, borrowed, or arena-based?
  • Which cycle edge is the natural indirection boundary?
  • What allocation and locality costs follow from that choice?
  • Can traversal or destruction become too deeply recursive?
  • Does the repaired test construct and inspect a real recursive value?

The core principle is that recursive meaning needs finite representation. A fixed-size handle breaks the layout cycle, and the handle type should also express the ownership model the data structure truly needs.