Skip to content

Matching on usize requires half-open range #146476

Description

@mkeeter

usize ranges in match statements are surprising:

fn test(i: usize) {
    match i {
        0..=usize::MAX => todo!(),
    }
}

I expected this to compile; instead, rustc returns an error:

error[E0004]: non-exhaustive patterns: `usize::MAX..` not covered
 --> src/lib.rs:2:11
  |
2 |     match i {
  |           ^ pattern `usize::MAX..` not covered
  |
  = note: the matched value is of type `usize`
  = note: `usize` does not have a fixed maximum value, so half-open ranges are necessary to match exhaustively
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
  |
3 ~         0..=usize::MAX => todo!(),
4 ~         usize::MAX.. => todo!(),
  |

For more information about this error, try `rustc --explain E0004`.

This is obviously a known edge case, given the custom error message (which is great!), but it's still a weird paper cut. It's especially weird to declare "usize does not have a fixed maximum value" when usize::MAX is right there!

Doing a little digging, it looks like it dates back to at least #118598

Meta

rustc --version --verbose:

rustc 1.89.0 (29483883e 2025-08-04)
binary: rustc
commit-hash: 29483883eed69d5fb4db01964cdf2af4d86e9cb2
commit-date: 2025-08-04
host: aarch64-apple-darwin
release: 1.89.0
LLVM version: 20.1.7

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Sep 12, 2025
  2. added
    T-langRelevant to the language team
    C-discussionCategory: Discussion or questions that doesn't represent real issues.
    A-patternsRelating to patterns and pattern matching
    A-exhaustiveness-checkingRelating to exhaustiveness / usefulness checking of patterns
    and removed
    C-bugCategory: This is a bug.
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Sep 12, 2025
  3. workingjubilee commented on Sep 12, 2025

    @workingjubilee
    Member

    This is an interesting quirk of the way the type is defined. Technically, the compiler has no special knowledge of the usize::MAX constant: it just is a value. The fact that it will, across all compilation sessions, mystically always be the largest value of usize (well, unless the stdlib is implemented incorrectly) is a mystery to the compiler.

  4. fmease commented on Sep 12, 2025

    @fmease
    Member

    Doing a little digging, it looks like it dates back to at least #118598

    I mean the PR description basically explains why it is the way it is and I don't see how we would 'fix' this in a principled and performant manner.

    Of course, we could slap some #[lang] or #[rustc_*] attr on the MAX const in the stdlib but is that really principled? Unless we somehow track this metadata in the const value itself (similar to initializedness and provenance I guess; probably extremely expensive to maintain), this wouldn't be "closed under" normalization (evaluation). Consider MY_MAX set to usize::MAX - 1 + 1, (usize::MAX,).0, etc.

    And if it "doesn't work with" normalization, it would just be a super weird special case that's equivalent to just using a half-open range.

    In any case, there might be merit in clarifying that diagnostic note even further.

  5. fmease commented on Sep 12, 2025

    @fmease
    Member

    It is unfortunate but not world shattering imo. In my mind, it compares to constructs like loop {} chosen over while KNOWN_TO_EVAL_TO_TRUE {} as a more restricted version of another construct that allows for more static guarantees. Does that make sense?

  6. added
    A-diagnosticsArea: Messages for errors, warnings, and lints
    C-bugCategory: This is a bug.
    and removed
    C-discussionCategory: Discussion or questions that doesn't represent real issues.
    on Sep 14, 2025
  7. workingjubilee commented on Sep 14, 2025

    @workingjubilee
    Member

    seems like a clear diagnostic bug, tbh, even if it is mostly going to be about choosing different words.

  8. added
    E-easyCall for participation: Easy difficulty. Experience needed to fix: Not much. Good first issue.
    on Sep 14, 2025
  9. workingjubilee commented on Sep 14, 2025

    @workingjubilee
    Member

    This just requires updating the text here:

    if ty.inner() == cx.tcx.types.usize {
    err.note(format!(
    "`{ty}` does not have a fixed maximum value, so half-open ranges are \
    necessary to match exhaustively",
    ));
    } else if ty.inner() == cx.tcx.types.isize {
    err.note(format!(
    "`{ty}` does not have fixed minimum and maximum values, so half-open \
    ranges are necessary to match exhaustively",
    ));
    }

    Word choice is going to be the greatest obstacle here, so it requires fairly little programming knowledge to do the update1 but it does require very thoughtful wordsmithing.

    Footnotes

    1. The process of committing the change, running tests, updating those, opening a PR, and rebasing it after review will admittedly require a little more. ↩

  10. added
    E-mentorCall for participation: This issue has a mentor. Use #t-compiler/help on Zulip for discussion.
    on Sep 14, 2025
  11. hkBst commented on Sep 19, 2025

    @hkBst
    Member

    I'm not following why the compiler does not or cannot know that 0..=usize::MAX is exhaustive for matching on a usize.

    Surely the compiler knows the target ABI which specifies the range of usize for the current target, and surely the compiler knows the value of usize::MAX (it's a constant with a different value for each target, through the magic of conditional compilation, right?), so surely the compiler has enough information to deduce exhaustiveness for the current target. Right?

  12. fmease commented on Sep 19, 2025

    @fmease
    Member

    Was my explanation not sufficient?

  13. fmease commented on Sep 19, 2025

    @fmease
    Member

    If the compiler sees usize::MAX it looks like any other user-defined constant. It just so happens to have the correct max value for each supported target_pointer_width via cfgs which expand very early on during the compilation process.

    We still want to treat 0usize..=MY_MAX_CONST as non-exhaustive where MY_MAX_CONST is a constant set to a (concrete and non-portable) usize corresponding to the logical maximum. There's no principled way of letting the compiler know if that value is portable or not because the cfg system is only slightly better than a preprocessor.

    As I've already explained above, we could make std's usize::MAX special by slapping an internal marker attribute on each of its (per target pointer width) definitions. Now, how would the compiler then recognize it?

    Assuming the compiler doesn't try to to evaluate/normalize the const paths before exhaustiveness/relevancy checking (I don't remember), then the simplest and most unprincipled solution would be to look for said marker attribute on the definition "corresponding to" the unevaluated constant beforehand and if present suppress this error (simplifying).

    As I've alluded to above already, what about 0usize..=K then where const K: usize = usize::MAX;? That wouldn't work with the impl I described above. And even if you were to hack it together and made it work, what about K set to usize::MAX + 0;? Every const op would need to recompute "is this logical max"? That's too expensive and unmaintainable.

    Okay, what if we didn't care about normalization and only accepted the simplest case? Then that would still require a language change, a hard-or-annoying-to-describe-in-spec-lang one at that. And what does it buy you? We already have 0usize...

  14. hkBst commented on Sep 19, 2025

    @hkBst
    Member

    @fmease thanks! My thinking was, there are already things the compiler only checks for the current target arch, so this could be one more. But from your explanation, it seems like the exhaustiveness check is trying to be target arch-independent. It seems the only difference between these two cases is when a manual upper bound with a value equal to usize::MAX is specified. If you go arch-dependent, then we can accept the given code, and if arch-independent, then it's problematic. So why not do this check arch-dependently?

  15. workingjubilee commented on Sep 24, 2025

    @workingjubilee
    Member

    @Nadrieril vibes?

  16. fmease commented on Sep 25, 2025

    @fmease
    Member

    So basically reverting the decision made by Nadrieril's PR? Idk, open a PR and lang-nominate it, I don't have an opinion on this.

  17. added a commit that references this issue on Sep 28, 2025
    0e675b2
  18. added a commit that references this issue on Sep 28, 2025
    4eddf64
  19. added a commit that references this issue on Sep 28, 2025
    9f136dd
  20. hkBst commented on Sep 28, 2025

    @hkBst
    Member

    The clarification of the diagnostic is actually very satisfactory.

  21. Nadrieril commented on Oct 3, 2025

    @Nadrieril
    Member

    it seems like the exhaustiveness check is trying to be target arch-independent

    That's exactly what's happening. For context, removing a feature gate doesn't require input from t-lang so not sure what t-lang thinks of this today. When I removed it I just assumed there was a good reason for having this special behavior but I didn't really question it. And this behavior was added in 2018; I feel like we've matured in our view of portability since.

    At this point I think this feature isn't particularly helpful. There's no way someone accidentally writes this and acts surprised if it fails to compile on a different target:

    const MY_USIZE_MAX: usize = u64::MAX as usize;
    // or
    const MY_USIZE_MAX: usize = 18446744073709551615;
    
    match ... {
      0..MY_USIZE_MAX => ...,
    }

    which is what this special case is supposed to be for. Plus it requires a bit of weirdnesses in the implementation.

    I would approve a PR that reinstates the precise_pointer_size_matching feature gate and help with the t-lang discussions, if anyone's motivated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-diagnosticsArea: Messages for errors, warnings, and lintsA-exhaustiveness-checkingRelating to exhaustiveness / usefulness checking of patternsA-patternsRelating to patterns and pattern matchingC-bugCategory: This is a bug.E-easyCall for participation: Easy difficulty. Experience needed to fix: Not much. Good first issue.E-mentorCall for participation: This issue has a mentor. Use #t-compiler/help on Zulip for discussion.T-langRelevant to the language team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions