Tsonnet #44 - The devil in the details #0
Removing catch-all pattern matches to expose unhandled expression variants across the compiler pipeline.
Welcome to the Tsonnet series!
If you’re not following along, check out how it all started in the first post of the series.
In the previous post, we got computed field names working:
Turns out the catch-all patterns I’d been relying on were defeating the one thing that makes pattern-matching useful.
The problem
Compilers are amazing co-pilots when you’re working with strongly typed languages. They catch invariants when we forget. But that only works if we enforce checking all pattern-matching cases, one by one. A catch-all | _ -> or | expr -> is the compiler equivalent of duct tape — it shuts it up, but it also hides every variant you forgot to handle.
So far, I’d been mistakenly ignoring new type variants in pattern-matching across lib/scope.ml, lib/type.ml, and lib/interpreter.ml because of catch-all cases. Every time a new expr variant was added, the compiler stayed quiet.
The fix: explicit exhaustiveness
The first thing to do is remove the catch-all cases and list every variant explicitly. This way, when a new variant gets added to the AST, OCaml will refuse to compile until every match is updated.
lib/scope.ml — _validate
lib/type.ml — three matches
The type checker had the most catch-all cases. Three separate matches needed the treatment:
lib/interpreter.ml — deep_eval
Every catch-all is gone. Now when I add a new variant to the expr type, the compiler will tell me exactly which function needs a new arm — silently swallowing it is no longer an option.
Does it change anything? (No)
This improves nothing in the runtime behaviour. Every explicit list above matches exactly what the catch-all was doing. But now the compiler can do its job — if I forget to update a match when adding a new variant, the build breaks.
The existing cram tests pass without changes. No output changed — as expected, since the semantics are identical.
What’s next
This post was just the tip of the iceberg — removing the catch-all exposed the loose ends, but now I actually need to deal with them. In the next one, I’ll start with the scope check, where there are variants in _validate that are silently passing through when they probably shouldn’t. But first, I needed to see them.
The diff is small — the habit it breaks is the point.
The entire diff can be seen here.





