RSS Amplifier

Bit Maybe Wise · Jul 18, 2026

Tsonnet #44 - The devil in the details #0

0
Sign in to vote or save

This page did not load. You can still read it on the original site — the toolbar below keeps your place in the directory.

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.

a black rectangular device
Photo by Volodymyr Hryshchenko on Unsplash

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.mldeep_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.


Thanks for reading Bit Maybe Wise! Your inbox deserves exhaustive coverage — subscribe and never miss a variant.

Read on bitmaybewise.substack.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.