ADR-203: An enum’s Cases Are a Column of the Ledger, and a Unit-Only enum Compares

Status: Accepted Date: September 23, 2026 Related: ADR-291 D19 (a match covers every case, and why an else is the expensive way out), ADR-295 D12 (the table of ledger entries), ADR-288 D11 (the ledger records what a consumer may reach), ADR-280 D18 (a match over a catch’s error), ADR-280 D5 (the variant is the kind and it carries the thing), ADR-289 D20 (the request’s Method), ADR-204 D1 and D2 (whether a type compares)

1. Context

A [type."X"] entry carries a struct’s fields. Without an entry for an enum, a consumer of a package cannot see that its cases are Yes and No, and

match verdict::of(word) {
    verdict::Answer::Yes => { println("yes") }
    verdict::Answer::No  => { println("no") }
}

would be NK1151, this match does not cover else. Two things are wrong with that at once. It is a correct program refused (Part III C.4) — the two arms are the whole type. And the way out it offers is the one ADR-291 D19 exists to prevent: an else makes the type gaining a variant silent in that program forever after, which is the whole reason the rule is a refusal rather than a warning.

Two smaller things belong with it.

== on a declared type has no lowering. k == Kind::Post — for an enum declared in the same file — would reach rustc as binary operation == cannot be applied to type Kind, with consider annotating Kind with #[derive(PartialEq)] as the help: the backend’s words about a file nobody wrote, and a way out the source cannot take (Part III C.1 and C.2).

And a match over a catch’s error would be the backend’s too. A handler that takes io::IoError apart and misses a case would get non-exhaustive patterns: IoError::NotText(_) not covered, about the generated file.

2. Decisions

D1 — A [type."X"] entry carries an enum’s cases, in a variants column.

[type."Answer"]
pub = true
variants = ["Yes", "No"]

[type."io::IoError"]
variants = ["NotFound(String)", "PermissionDenied(String)", "NotText(String)", "Outside(String)", "Other(String)"]

One line per case, written the way the source writes it: a bare name, a positional payload Name(T, U), or named fields Name { a: T }. A struct has fields and an enum has variants, and neither has the other’s.

A column and not a record of its own, because it is the same fact as fields for the other shape of type, in the same table (ADR-295 D12). There is no second credible spelling: fields cannot be widened to hold a case, and a [variants."X"] table would be a second place to look for what a type is.

This does not answer the bound question. How does a bound reach a caller across a package boundary? is on the former open-decisions.md because it has two credible spellings — a key of its own, or the signature language widened — and the choice costs a grammar every ledger reader parses. This one had one spelling, and the column beside it already existed.

Read from every ledger, and this unit’s own source wins. A package’s ledger is absorbed under qualified keys, so verdict::Answer and a locally declared Answer cannot collide by accident; where they would, the source is what the file being checked declares.

D2 — An enum whose variants hold nothing derives PartialEq and Eq.

Changed by ADR-204 D1 and D2: whether any type compares, and whether it is an equivalence, is a structural walk over its parts; a unit-only enum has none, so it compares and is Eq by that rule, with no special case.

Method::Get == Method::Post is a comparison of two values of one type, and it lowers to Rust’s. Such a type has no part that could fail to be comparable, so the derive cannot fail — which is what ADR-204 D1 answers for every type, read at the case with no parts.

D3 — A match over a catch’s error is a match over the type that arrived, where exactly one arrives.

error is a binding with no written type, and it stays one. What this adds is the answer to one question: whether the arms cover every case. The one type comes from the callee’s throws column, over both ledgers, and only where that column names exactly one thing that is not "?".

It can only move a refusal, never add one. Where the arms miss a case the words would otherwise be rustc’s about a generated file, and they are this compiler’s; where the arms cover everything, nothing is said either way. That is why it does not need the sweep a typed error would: giving a binding a type where it had none can turn a program that compiles into one that is refused, and this gives it none.

ADR-280 D18 is untouched: where several error types arrive the set is open and else is still the only answer.

3. Consequences

4. What this does not decide

This site is open source. Improve this page.