RFA-446 · Case file with fixtures · Case 418 of 694 · Compiler evidence
A Rust Range Pattern Cannot Have Its Bounds Reversed
Range patterns are compile-time structural tests and must describe a non-empty scalar interval. Put bounds in semantic order; for runtime or possibly inverted limits, validate or normalise before matching with comparisons.
- 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 range pattern denotes scalar set membership rather than descending traversal, and reversed bounds describe no valid compile-time interval.
- First discriminating check
- Write the semantic minimum before the maximum and add tests at both boundaries and immediately outside them.
I wrote an inclusive match range as 10..=5, thinking of a descending interval. Rust reported E0030 because a range pattern's lower bound must not exceed its upper bound.
The failing fixture uses a u8 scrutinee. Rust can evaluate both literal bounds during compilation and prove that the range contains no values.
Range patterns describe membership, not traversal
5..=10 in a pattern means the tested scalar lies between five and ten, inclusive. It does not create an iterator and has no direction of traversal.
The E0030 explanation notes that range patterns include both endpoints and must be non-empty. Equality is allowed: 5..=5 matches exactly one value.
Reversing bounds cannot express descending order because matching asks a yes-or-no membership question.
Put domain bounds in increasing order
The repaired fixture uses 5..=10 and confirms that seven enters the arm.
For named constants, I choose names such as MIN_RETRY and MAX_RETRY and test them. A typo can otherwise make domain intent less obvious than literal order.
Rust's compile-time rejection is helpful because an empty arm would never run and might hide a missing classification.
Runtime bounds do not belong directly in patterns
Range-pattern bounds are restricted forms, not arbitrary runtime expressions. If minimum and maximum come from configuration, I use comparisons or range APIs:
if min <= value && value <= max { /* ... */ }
Before that check I decide what inverted configuration means. It can be rejected, swapped deliberately, or interpreted as empty. Silently normalising may hide operator mistakes.
The Atlas case on Range::is_empty with NaN shows why runtime floating-point bounds add another policy.
Descending iteration is separate
To visit numbers from ten down to five, I construct a forward interval and reverse its iterator:
for value in (5..=10).rev() { /* ... */ }
That controls traversal order. It does not change which values belong to the interval.
I keep range membership, construction, and iteration direction as separate concepts in APIs and tests.
Adjacent arms need boundary tests
Classification often combines ranges:
0..=4 => Low,
5..=10 => Medium,
_ => High,
I test four, five, ten, and eleven. Inclusive versus exclusive notation can create overlaps or gaps, and later arms may become unreachable.
The compiler catches some unreachable patterns, but business boundaries deserve explicit assertions with domain names.
Scalar types keep matching decidable
The range-pattern Reference limits nonempty range patterns to numeric and char types with the required structural properties. String ordering, semantic versions, and custom partial orders need guards or ordinary comparisons.
For floats, NaN and structural equality introduce further restrictions. I normally classify floating values through explicit logic that names NaN policy.
Generated match tables need validated order
A generator can emit reversed bounds from a schema even when hand-written Rust looks correct. I validate min <= max, sort only when the schema defines unordered endpoints, and produce a source-level diagnostic before generating code.
Compiling a generated fixture remains useful because it catches output bugs and overlaps after transformation.
My E0030 checklist
For protocol fields, I write a boundary table next to the match before editing it: the first accepted value, last accepted value, and the first value of the next category. Then I test those exact edges. This catches a reversed range, but also the more subtle gap or overlap that still compiles. I avoid translating a descending business description directly into pattern syntax. “Versions ten down to seven” may describe an ordering in a document; membership is still written 7..=10. Keeping traversal order and set membership separate makes the code easier to audit.
An empty or reversed interval can sometimes appear after code generation or constant calculation. Because patterns require compile-time bounds, I validate the source data before emitting Rust and still compile the generated fixture in CI. That gives a useful error close to the generated artefact instead of a surprising missing branch at runtime.
- Are these bounds membership limits or an intended iteration direction?
- Is the lower endpoint semantically no greater than the upper?
- Are both endpoints inclusive?
- Could named constants have been swapped?
- Do runtime bounds need rejection or normalisation?
- Are adjacent classification edges tested?
- Does the scalar type have NaN or another incomparable value?
The core principle is that range patterns describe non-empty scalar sets. Direction belongs to iteration, not matching. I order compile-time bounds by membership and handle dynamic or potentially invalid intervals through explicit validation before classification.