Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-449 · Case file with fixtures · Case 421 of 694 · Compiler evidence

A Rust match Binding Cannot Shadow a static Item

A bare identifier in a pattern is resolved as a path or a new binding. Constants can be structural value patterns, but statics cannot be shadowed this way; use const for a fixed match value or a differently named binding and guard for runtime comparison.

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
A bare identifier pattern resolves as a constant path or introduces a binding, but a static item is runtime storage and cannot be shadowed at that pattern position.
First discriminating check
Use a const for a structural fixed value, qualify constant paths, or choose a different binding name and compare against static data in a guard.

I defined static LIMIT: u8 = 10 and wrote LIMIT as a match arm, expecting it to match ten. Rust reported E0530 because a match binding cannot shadow a static.

The failing fixture exposes an important ambiguity: a bare identifier in a pattern can refer to an existing path pattern or introduce a new binding that matches anything. Name resolution decides which meaning applies.

A new binding would match every value

Without an existing item named candidate, this arm binds any input:

candidate => use_value(candidate)

It is not a comparison with some earlier local variable named candidate. Pattern bindings introduce names and can shadow ordinary local variables.

Allowing LIMIT to become such a binding would make the arm match seven, ten, or any other u8, which is the opposite of the apparent constant-like intent.

A static is not a constant pattern

A static item has a stable memory location and represents stored global data. A const is an inlined constant value subject to structural pattern rules.

The E0530 explanation shows that match bindings cannot shadow statics and presents a const as the fixed-value alternative.

The repaired fixture declares const LIMIT: u8 = 10, then matches it. Seven reaches the wildcard and ten reaches the constant arm.

Constants still need structural equality

Not every value that implements PartialEq is permitted as a const pattern. The compiler must be able to reason structurally about the value for pattern matching and exhaustiveness.

For integers and characters, named const patterns are straightforward. For custom types, interior mutability, manual equality, floating fields, and other details can make a const unsuitable.

I compile the exact constant type and use guards when runtime equality is the intended operation.

Use a guard to compare runtime or static data

If a threshold truly comes from mutable or addressable static state, I bind the input under another name and compare in a guard or arm body:

match value {
    candidate if candidate == current_limit() => "limit",
    _ => "other",
}

The guard executes at runtime and is not considered guaranteed coverage. A fallback remains required.

For mutable global state, synchronization and consistency across the read matter. Pattern syntax would not solve those operational concerns.

Qualified paths reduce ambiguity

Enum variants and associated constants can be written with qualified paths such as State::Ready or Limits::MAX. This makes their path-pattern role obvious.

Imports that bring variants into scope shorten patterns but can create collisions with binding names. I favour qualification in dense protocol matches where meaning is more valuable than a few characters.

The identifier-pattern Reference explains that path patterns take precedence and that resolution determines whether a single identifier is a path or binding.

Uppercase style is only a clue

Rust naming conventions suggest uppercase names for constants and statics, but capitalization does not itself change grammar. Resolution does.

A misspelled variant can sometimes be read as a new binding in contexts where no item resolves, leading to an unreachable following arm warning rather than the comparison intended. Qualified variant paths and exhaustive tests reduce that risk.

I treat unexpected “unreachable pattern” warnings as possible name-resolution mistakes.

Match values, not storage identity

Even when a static stores the wanted scalar, matching normally asks whether the input value equals a fixed pattern value. The static's address and identity are separate concepts.

If identity matters, I compare pointers or references under the correct safety and lifetime rules. Converting a static into a const just to compile would lose address identity, so I confirm the domain question first.

My E0530 checklist

For codebases with many imported constants and statics, I prefer qualified paths in patterns even when an unqualified constant would compile. protocol::READY tells a reviewer that the arm compares against an existing value; a lowercase identifier normally introduces a binding and matches anything. This visual difference prevents a dangerous class of “specific” arms that are actually catch-alls. Compiler errors cover some collisions, but naming and qualification make the intended resolution understandable before compilation. I reserve guards for values that truly need runtime access, and I keep an explicit fallback because a guard cannot prove exhaustiveness.

  • Does the identifier resolve to a static, const, variant, associated const, or nothing?
  • Was the arm intended to compare or to bind?
  • Is the expected value fixed enough to be a const?
  • Does its type support structural const patterns?
  • Should runtime comparison live in a guard?
  • Would a qualified path make intent unmistakable?
  • Is storage identity relevant rather than value equality?

The core principle is that a pattern identifier participates in name resolution. A static cannot silently become a structural constant or a shadowing catch-all binding. I use constants for fixed pattern values, distinct binding names for capture, and guards for comparisons that must execute at runtime.