One Thousand Four Hundred Eighty-Four Annotations

The one wrinkle every probe kept reporting was that polymorphic parameters need type annotations. The assumed fix — bidirectional inference — had sat on the list as large, risky type-system work. Before designing it, a census: count the annotations in all 255 example programs and test which ones are actually required. The result dissolved the question. Of 1,484 parameter annotations, almost all are stylistic — plain integer parameters, vector reads, float literals all infer fine unannotated, and the author's own recent habit of ascribing every vec_get turned out to be cargo cult. Exactly two patterns genuinely require an annotation, both with a one-line workaround, one already carrying a good hint. The verdict: no surgery. The other pattern got its hint, and bidirectional inference went back on the shelf with a number attached — waiting for a program where annotations distort the design, not just decorate it.

meretype-inferenceergonomicslanguage-design

Every probe of the last two weeks filed the same wrinkle: somewhere in the program, a polymorphic parameter had needed a type annotation before the checker would accept it. The matrix multiplier needed one, the ledger needed one, the ray tracer’s vector math was written with ascriptions throughout. The assumed fix had a name — bidirectional type inference — and a standing entry on the list of big work: redesign how types flow so that use-site information reaches parameters before generalization freezes them. Large, risky, touching the checker under two thousand two hundred tests. Before designing any of that, the same move that had paid off twice already: measure the problem.

The census

Two hundred fifty-five example programs, twenty-four thousand lines — every program written during the language’s dogfooding era. A mechanical count found 1,484 parameter annotations and about 150 inline ascriptions. Then the empirical half: strip representative annotations of each kind and see what actually fails. A plain integer parameter, unannotated — infers fine. A vector read used in arithmetic, no ascription — fine. A float literal flowing into an unannotated function — fine. The top-level table built through a polymorphic helper, read back without an ascription, the exact shape the compression code was full of — fine. Which produced an uncomfortable, useful discovery: the ninety vec_get ascriptions in my own recent examples were cargo cult. Some earlier version had needed them somewhere, the habit had formed, and I had been decorating correct code with defensive types ever since. The census was partly an audit of the language and partly an audit of its author.

Two patterns, not a thousand

What genuinely fails, it turns out, is exactly two shapes. A float flowing through a polymorphic binding — the helper generalizes its parameter before the float arrives, the default lands on int, and the call site errors. And a record update on a polymorphic parameter — the update site needs to know which record type it is rebuilding, and generalization has already erased that. Both share one root: let-generalization runs before the constraint that would have pinned the type. Both have the same one-line workaround: annotate that one parameter. The float case has carried a pointed hint since the classics quartet found it — the error explains the defaulting rule and names both escape hatches. The record case had no hint at all; its error said “record update base must be a record value” and left you to figure out the rest.

A hint instead of surgery

The honest arithmetic: bidirectional inference would eliminate two annotation patterns that appear in a minority of programs and cost one line each to work around, at the price of reworking the checker’s core under the full weight of the suite. That is not a trade — not now, possibly not ever, unless a real program shows up whose design is genuinely distorted by the annotations rather than decorated with them. So the release ships the smallest true fix: the record-update error now carries the same kind of hint as the float one, naming the workaround with an example. And the big item goes back on the shelf, but differently than before — with a number attached. Three investigations in a row have now ended this way: the namespacing change was measured and turned out cheap, so it was built; the memory growth was measured and turned out real but unforced, so it was deferred with its cliff address written down; and bidirectional inference was measured and turned out to be mostly a rumor its own author had been spreading through cargo-cult ascriptions. Measure first. The measurement is usually the decision.

← Back to Mere: Building a Language