enum’s Cases Are a Column of the Ledger, and a Unit-Only enum ComparesStatus: 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)
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.
[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.
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.
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.
Method is such an enum. examples/http publishes
enum Method { Get, Post } and a handler asks request.method(). Two
variants are the whole type because std refuses the rest: http1 answers
400 to any other method before a handler runs, which is what makes the enum
total rather than merely short.std failure apart is checked here. Part III 17.1’s own
sentence — one that takes it apart writes use std::io and
io::IoError::NotFound(p) — describes a match that can be complete.error binding, which is what would let every other check see a
handler’s value. D3 deliberately answers one question instead.< on an enum would be PartialOrd, and what an order over
cases means is a question nobody has asked.