gleam-lang/gleam · error

check to already be a choice

Error message

check to already be a choice

What it means

Panic from `self.choices.get_mut(*index).expect("check to already be a choice")` while folding a branch into overlapping earlier choices. The `overlapping` indices come from `check_overlaps`, which is documented to only report indices that already exist in `self.choices`; if an index is missing, the overlap bookkeeping is broken.

Solutions

  1. Recompute overlaps immediately before the merge loop so indices cannot go stale.
  2. Check any code between `check_overlaps` and this loop that mutates `self.choices` (push/pop/retain).
  3. Soften to a skip-with-warning (`continue` when lookup fails) while debugging, then fix the root cause in `check_overlaps`.

Example fix

// before
let (overlapping_check, branches) = self.choices.get_mut(*index).expect("check to already be a choice");
// after
let Some((overlapping_check, branches)) = self.choices.get_mut(*index) else {
    panic!("overlap index {} not in {} choices", index, self.choices.len())
};
Defensive patterns

Strategy: validation

Validate before calling

for index in &overlapping {
    assert!((*index as usize) < self.choices.len(), "stale overlap index");
}

Type guard

fn choice_at(choices: &[(RuntimeCheck, Branches)], i: usize) -> Option<&(RuntimeCheck, Branches)> {
    choices.get(i)
}

Try / catch

std::panic::catch_unwind(|| merge_overlaps(overlapping)).unwrap_or_else(|_| recompute_overlaps_and_retry())

Prevention

When it happens

Trigger: Compiling a pattern that `check_overlaps` reported as overlapping some existing choice, then `self.choices.get_mut(index)` failing — e.g. choices were removed/reordered between overlap computation and this loop, or `check_overlaps` returned an index into the wrong vec.

Common situations: Hit by Gleam compiler contributors after changes to overlap detection, guard fall-through handling, or when choices are pruned before the merge step.

Related errors


AI-assisted analysis of gleam-lang/gleam@49f8762da5 (2026-09-14). Data as JSON: /api/errors/b7055abb22b35d48. Report an issue: GitHub.

Appendix: source

Thrown at compiler-core/src/exhaustiveness.rs:3139

            .to_runtime_check_kind()
            .expect("no unconditional patterns left");

        let CheckOverlaps {
            overlapping,
            needs_new_choice,
        } = self.check_overlaps(&kind);

        // The branch is relevant to every existing choice its check overlaps
        // with, so we add it (together with any newly discovered checks) to all
        // of those paths. For a string prefix this also includes any longer
        // prefixes and exact literals that start with it. When one of those
        // matched but its branch then failed, control falls through to this
        // shorter prefix.
        for index in overlapping.iter() {
            let (overlapping_check, branches) = self
                .choices
                .get_mut(*index)
                .expect("check to already be a choice");

            let mut branch = branch.clone();
            for new_check in compiler.new_checks(&pattern_check.var, &pattern, overlapping_check) {
                branch.add_check(new_check);
            }
            branches.push_back(branch);
        }

        // Unless the check is fully subsumed by an existing choice, we also add
        // it as a new path we might have to go down to in the decision tree.
        // A shorter prefix still needs its own choice even when it overlaps
        // earlier longer prefixes, so it can match values outside those.
        if needs_new_choice {
            self.save_index_of_new_choice(kind.clone());

            let check = compiler.fresh_runtime_check(kind, branch_mode);
            for new_check in compiler.new_checks(&pattern_check.var, &pattern, &check) {
                branch.add_check(new_check);

View on GitHub (pinned to 49f8762da5)