Exhaustiveness
Totality as a legal-safety property: every case of a determination handled.
The Legal Problem
A legal determination over a closed category must have an answer for every member of the category. A visa decision procedure that handles "granted" and "refused" but not "withdrawn" is not a smaller version of the right procedure — it is a procedure with a hole in it. In software, the hole surfaces as an unhandled case at the worst possible moment: on the first real input that falls into the forgotten category.
Law is unusually exposed to this failure mode, for two reasons:
- Legal categories are closed enumerations. Statutes define exhaustive lists ("'person' means (a)…, (b)…, or (c)…"), so "cover every case" is a well-defined, checkable demand. See Types as legal categories on why these lists become sum types.
- Legal categories change. Amendments add variants: a new visa class, a new entity type, a new ground of refusal. Every determination written against the old category is now potentially incomplete — and nothing about its text looks wrong.
Exhaustiveness checking (totality over the scrutinised type) turns both problems into compiler diagnostics.
What the Checker Does
When a CONSIDER expression scrutinises a value of an algebraic type, L4's type checker compares the branches against the type's full constructor set. A complete match is silent:
DECLARE `visa status` IS ONE OF
Granted
Refused HAS reason IS A STRING
Withdrawn
GIVEN status IS A `visa status`
GIVETH A STRING
`outcome letter` MEANS
CONSIDER status
WHEN Granted THEN "Your visa has been granted."
WHEN Refused r THEN r
WHEN Withdrawn THEN "Your application was withdrawn."
Missing branches
If a constructor is not covered, the checker emits a warning listing exactly what still needs to be handled:
GIVEN status IS A `visa status`
GIVETH A STRING
`incomplete letter` MEANS
CONSIDER status
WHEN Granted THEN "Your visa has been granted."
WHEN Refused r THEN r
Warning: The following branches still need to be considered:
WHEN Withdrawn THEN
Redundant branches
The dual check also fires: a branch that can never be reached (for example, a second WHEN Granted after the first) is flagged:
Warning: The following CONSIDER branch is redundant:
WHEN Granted THEN "duplicate branch - unreachable"
Redundancy matters legally too — an unreachable branch is usually a sign that the author believed some case was being handled there, when in fact an earlier branch is absorbing it.
Rules written as clauses
A rule written as several DECIDE clauses, one per case (see DECIDE), is checked the same way, as one group. The warning lists the clauses still needed, ready to paste in and finish:
This multi-clause definition does not cover all cases. The following clauses are still needed:
DECIDE `price` Blue IS
L4 tries clauses from the top, so a clause below one that matches every input is never reached. That is warned about too.
The amendment scenario
The payoff comes when a category grows. Add a fourth variant to `visa status` and every checked CONSIDER site that scrutinises it — every letter template, every fee rule, every appeal-rights determination — immediately warns that the new case "still needs to be considered". The legislature's amendment becomes a checklist of code sites to revisit. This is the property flattened boolean encodings cannot offer: a bundle of independent IF tests has no notion of "covering" a category, so nothing notices when the category changes.
Caveats and Escape Hatches
Numbers, text and dates
The check works by listing constructors, and a NUMBER, a STRING or a DATE has none to list: its values never run out. A WHEN 0 or a WHEN "Saturday" matches one value, so the only way to complete such a match is a branch that matches everything. In a CONSIDER that is OTHERWISE; in a rule written as clauses it is a clause written with the input's GIVEN name. The warning suggests exactly that:
GIVEN n IS A NUMBER
GIVETH A STRING
`describe count` MEANS
CONSIDER n
WHEN 0 THEN "none"
WHEN 1 THEN "one"
-- warns: OTHERWISE
GIVEN n IS A NUMBER
GIVETH A STRING
DECIDE `describe number` 0 IS "none"
DECIDE `describe number` 1 IS "one"
-- warns: DECIDE `describe number` n IS
The other inputs of a rule are still checked. Over DECLARE Colour IS ONE OF Red, Green, Blue, the clauses DECIDE charge Red 0 IS 0, DECIDE charge Red n IS n and DECIDE charge Green n IS n TIMES 2 draw a warning for DECIDE `charge` Blue n IS: the clause for 0 covers one value, the clause below it the rest of Red, and Blue is still missing.
A few more details:
EXACTLYof a number, a piece of text or a value of an enumeration is read as that value. AnEXACTLYof anything else cannot be counted: over a number, text or date it covers nothing as far as the check knows, and over an enumeration it stops the check for that match.- In a
CONSIDER, a number repeated in another spelling (WHEN 1.0afterWHEN 1) is reported as a redundant branch. A repeated value in a rule written as clauses is not reported. - A type declared as another name for a number, text or date (
DECLARE Age IS A NUMBER) is treated like that type.
The practical consequence: when a statutory category is modelled as a STRING (status codes, category letters), the check can only ask for an OTHERWISE; it cannot tell you which categories you left out. Declare an enumeration instead. It is precisely what makes the completeness of your determinations checkable.
Booleans, MAYBE, EITHER and lists
BOOLEAN is checked like an enumeration of TRUE and FALSE: a CONSIDER with only WHEN TRUE warns about the missing WHEN FALSE.
MAYBE, EITHER and lists are checked only inside. Their own constructors are not listed, so a missing WHEN NOTHING, WHEN RIGHT or WHEN EMPTY is not reported. What they hold is checked: WHEN JUST Basic over a MAYBE Tier asks for WHEN JUST Premium and WHEN JUST Gold. A rule written as clauses goes less far. Where a missing case could only be found by telling such values apart, it gets no warning: over a MAYBE Colour, the clauses DECIDE f (JUST Red) IS 1 and DECIDE f NOTHING IS 0 draw none, while the same two branches in a CONSIDER ask for WHEN JUST Green and WHEN JUST Blue.
Types from another file
An enumeration declared in another file and brought in with IMPORT is not analysed. A CONSIDER or a rule written as clauses over it gets no missing-case warning, so cover such a type with extra care, or test every value.
Very large tables
For a rule written as clauses, the check gives up without a warning when it would have to list more than 64 clauses, or needs more than 10,000 steps to find them. When the clauses match numbers or text, it does not fall silent at that limit, but gives a shorter, coarser list. A large CONSIDER still warns, but may list only some of the missing branches.
Where the check applies
The check applies wherever the CONSIDER appears, in a rule's main body and equally inside WHERE- or LET-bound local definitions. Rules written as clauses are only recognised at the top level of a file or section.
OTHERWISE trades checking for coverage
OTHERWISE is a catch-all branch. It makes any match complete, and thereby switches the missing-case warning off — including for variants added in the future, which it will absorb without comment. Use it when the remaining cases genuinely share one treatment; avoid it when each case of a determination deserves an explicit decision.
What happens if a hole is hit at runtime
Exhaustiveness warnings are compile-time advice, not a runtime guard. If evaluation actually reaches a CONSIDER with no matching branch, that evaluation fails with an error naming the offending value:
The value
Withdrawn
reached a CONSIDER that has no branch for it.
Add a WHEN branch for this case, or a catch-all OTHERWISE branch.
The typechecker's exhaustiveness warning lists all missing branches.
A rule written as clauses fails the same way, with "No clause of describe matches these inputs." and the value it could not match. l4 run exits non-zero when any directive fails this way. The warning exists so you find the hole before an applicant does.
Why Warnings, Not Errors
L4 reports incomplete matches as warnings: the program still compiles and runs. This is a deliberate choice about how legal encoding actually proceeds.
- Encoding is incremental. A statute is formalised provision by provision. While the work is in progress, determinations are legitimately partial — the encoder is often deliberately deferring the hard case (the one that needs a policy decision) while validating the easy ones with
#EVAL. A hard error would force placeholder branches everywhere, which are worse than an honest warning: a placeholder looks handled. - Existing code keeps working. Tightening the checker over time (covering more scrutinee types, sharper redundancy analysis) must not break rulebases that previously compiled. Warnings allow the analysis to improve without invalidating legacy encodings.
The intended discipline: treat the warnings as a completeness report. A finished, production-ready determination should compile with none — at that point, every case of every closed category it examines has an explicit, reviewed answer.
Summary
| Property | Behaviour |
|---|---|
Missing case in CONSIDER |
Compile-time warning listing the uncovered branches |
Missing case in a multi-clause DECIDE |
Compile-time warning listing the clauses still needed |
| Unreachable branch | Compile-time warning that the branch is redundant |
NUMBER / STRING / DATE scrutinee |
Analysed; OTHERWISE is suggested when it is missing |
Clauses over a NUMBER or STRING input |
Analysed; the clause with the input's name is suggested when it is missing |
BOOLEAN and declared enumerations (IS ONE OF) |
Fully analysed |
MAYBE / EITHER / LIST scrutinee |
Their contents analysed; their own constructors not listed |
| Enumeration declared in an imported file | Not analysed |
| More than 64 missing clauses | No warning for a rule written as clauses |
CONSIDER inside WHERE / LET |
Analysed like any other |
OTHERWISE |
Completes the match, disables missing-case warnings |
| Hole reached at runtime | Evaluation fails, naming the unmatched value; l4 run exits non-zero |
| Severity | Warning, not error — the file still evaluates and its #EVALs still run |
Further Reading
- Types as legal categories — Why legal categories become sum types in the first place
- CONSIDER — Full pattern-matching syntax
- Definitions and duties — Where determinations sit in the larger architecture