A Domain in Records
Every probe so far had been an algorithm or a binary format — the one axis left unmeasured was what it feels like to model a business domain in records. A double-entry ledger, with record types for money, accounts, entries and transactions and plenty of nested updates, is a good stress test, and it surfaced a parser hole that had been hiding in plain sight. Mere follows the ML convention of lowercase type names and capitalized constructors, and record declarations honored it — but the record literal only parsed when the type name was capitalized, so a lowercase one fell through to a variable followed by a block and failed with an error far from its cause. Fixing it was one branch: a registered record name of any case followed by a brace is a record literal. The domain modeled cleanly after that, with one honest wrinkle left standing.
The probes had all leaned the same way — algorithms, numeric kernels, binary formats — and one thing they never touched was the plainest kind of program most software actually is: modeling a domain with records. Not a clever data structure, just a handful of record types with meaningful fields, constructed and updated and read. A double-entry ledger is a compact version of that shape: money, accounts, entries, transactions, each a record, with nested updates as postings move balances around. Writing it was the probe, and the point was less whether it worked than what it felt like — and where it rubbed.
A hole in the ML convention
It rubbed immediately, in the parser. Mere follows the ML tradition where type names are
lowercase and constructors are capitalized — type 'a list = Nil | Cons of .... Record types
declared that way were accepted without complaint: type addr = { city: str } is a valid
declaration. But the record literal — addr { city = "kyoto" } — only parsed when the type
name started with a capital letter. A lowercase name at the head of a brace fell through to the
general case, was read as a variable followed by a block, and failed with “expected ‘;’ or ‘}’
in block” — an error about block syntax, pointing nowhere near the actual problem, which was
that the parser did not recognize a lowercase record constructor. Variants, lists, and options
had always worked with lowercase type names; records were the one place the convention had a
hole, because the record-literal check tested for a leading capital.
One branch
The fix was small and located exactly where the misparse happened: before falling through to
“lowercase name means variable,” check whether the name is a registered record type, of any
case, and if the next token is an opening brace, parse a record literal. With that branch in,
the whole vocabulary of records worked regardless of capitalization — construction, field
access, and nested update, including the awkward-looking { p | home = { p.home | city = ... } }
that rebuilds a record with one deep field changed. That the fix was a single branch is the
tell that the design was right and only the parser had lagged; nothing about the type system or
the semantics needed to move, just the recognition of a lowercase name in one position.
The wrinkle that stayed
One rough edge did not get smoothed, because it is the same rule that shows up elsewhere and is not really a bug: a record parameter that gets updated must be annotated with its type. Without the annotation, the function is polymorphic in that parameter, and the update site has no record type to update against — the error is “record update base must be a record value.” This is the same demand the numeric overloads make, where a polymorphic parameter that flows into arithmetic has to be annotated so the operator knows which type it is working on. Nested update where the base is a concrete field access is fine; it is only the bare polymorphic parameter that needs the hint. Recording it as a standing wrinkle rather than fixing it is a deliberate choice — the workaround is one annotation, and the real fix is a larger inference change that the pain does not yet justify. The ledger balanced, revenue matching cash to the cent, and the record side of the language reads well now that the lowercase hole is closed.