Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-579 · Case file with fixtures · Case 551 of 694 · Compiler evidence

Rust Square-Bracket Indexing Requires an Index Contract

Index syntax is an explicit semantic promise provided by built-in containers or the Index trait. Prefer fields and checked access when they describe the domain better.

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
Field layout was mistaken for a public sequence contract, but bracket syntax requires built-in indexing or an explicit Index implementation.
First discriminating check
Decide whether the value is a named record or true collection, then use fields, checked get access, array storage, or a deliberate Index contract.

Square brackets are not generic field-selection syntax in Rust. Arrays, slices, and types implementing the indexing contract support them. The Point in the failing fixture exposes named x and y fields but no index operation, so rustc reports E0608 for point[0].

Positional access is a semantic promise

It is possible to imagine index zero meaning x and index one meaning y, but several questions immediately appear. What happens for index two? Is coordinate order stable public API? Should a point support iteration? Can a caller mutate through the index? Should an invalid dynamic index panic or return None?

Rust refuses to infer answers from field order. Struct field order may matter for some layout representations, but layout is not automatically a collection interface.

The E0608 explanation directs the user toward supported indexing. Custom behaviour is defined by std::ops::Index, whose index method returns a reference to an associated output type.

Named fields are the honest repair here

The repaired fixture reads point.x and point.y. Those names preserve meaning and cannot receive an out-of-range runtime integer. A caller reviewing point.x knows which coordinate is selected without remembering a convention.

For a fixed mathematical vector where algorithms naturally loop over dimensions, storing [f64; N] may be a better representation. The array already has positional semantics and supports indexing. A wrapper can then add domain methods while deciding deliberately whether to expose Index.

I avoid implementing Index merely to make syntax shorter. The trait's conventional failure mode is panic for an invalid index. If invalid input is expected, a named get method returning Option<&T> communicates that possibility and keeps untrusted indexes away from panicking syntax.

Index output has one associated type

An implementation chooses an index type and an output type. All results for that implementation must be reference-compatible with the same Output. A heterogeneous struct containing a name, count, and timestamp cannot naturally return each field through one integer index without erasure or an enum wrapper.

That pressure is useful. A record is not automatically a sequence. Serialisation order, database column number, and source field position are often separate external concerns that should be translated at a boundary rather than becoming the in-memory API.

For maps, index syntax can also panic when a key is absent, depending on the type. I prefer get when absence is part of normal control flow. Indexing is best when the index is expected to be valid by construction and a violation indicates a programmer bug.

IndexMut adds another invariant

Supporting reads through Index does not automatically support assignment. Mutable indexing requires IndexMut and must preserve the type's invariants. Exposing individual storage slots can bypass validation that a named setter would perform.

For example, independently mutating point coordinates may be fine, while independently changing fields in a normalised range or checksum-bearing packet may create invalid states. I keep IndexMut absent when mutation needs a coordinated operation.

Performance is rarely a reason to hide meaning. Field access and array indexing are both straightforward for the optimiser. I benchmark a real hot loop before changing a clear representation, and I distinguish bounds-check elimination from API syntax.

Slices can expose sequence access without exposing storage forever

When a type truly contains homogeneous values, I may provide as_slice or iteration before implementing indexing. This gives callers safe traversal and the full collection API while keeping the concrete backing container replaceable. A slice also makes the lifetime of the borrowed view explicit.

Iteration is often better than indexing for algorithms that only need every item. It avoids manufacturing integer positions, works with more container shapes, and makes accidental out-of-range access impossible. If callers repeatedly need random access, that evidence can justify a get or indexing contract later. I let usage establish the abstraction instead of adding every familiar operator on day one.

My E0608 checklist

  • Is the value conceptually a sequence, mapping, tuple, or named record?
  • Would a field or method communicate the selected value better?
  • Does the type already implement Index for a different key type?
  • What should happen when a dynamic index is invalid or a key is missing?
  • Would get and Option be safer at this boundary?
  • Can every index return the same associated output type?
  • Would mutable indexing bypass validation or cross-field invariants?
  • Should the representation be an array if dimension-wise iteration is fundamental?

The core principle is that concise syntax carries a contract. Square brackets say that positional or keyed access is fundamental to the type and that invalid access follows the type's indexing policy. I use E0608 to decide whether that promise is real, not simply to imitate another language's syntax.