context-free

Formal languages and natural ones aren’t opposite designs. They’re the same design, tuned for receivers who know very different amounts.

Here is a line of C. The shape of it has been ruining afternoons since 1960:

if (a) if (b) x(); else y();

Which if owns the else? The grammar allows both readings. C settles it by rule: the else binds to the nearest if, whatever the indentation says. ALGOL 60 settled it by refusing to let an if follow a then at all without brackets around it. Different fixes, same instinct. Two parse trees for one input is a defect, and you fix it before you ship.

Now here’s a sentence in English: I saw her duck. A bird, or a dodge. Nobody files a bug. Nobody even notices, because by the time you hear it in real life you already know whether you’re at a pond or a bar fight.

The easy version of this essay says these are opposite designs. Code is built to remove ambiguity and language is built on it. That’s true and it’s boring. The version I think is right is harder to defend: they’re the same design, optimizing the same cost, and the only thing that changes between them is who is on the receiving end.

the name is a description of the receiver

Every language, formal or not, trades two costs against each other. One is the cost of saying the thing. The other is the cost of getting it wrong on the other end.

In 2012 three MIT cognitive scientists, Steven Piantadosi, Harry Tily and Edward Gibson, published a paper with a result that should be more famous than it is. They argued, from information theory, that any efficient communication system will be ambiguous as long as context carries information about meaning. If the listener can already work out which duck you mean, spending extra syllables to say so is waste. So the cheap words get reused. They checked this against English, Dutch and German corpora and found what the theory predicts: the short, frequent, easy-to-say words are the ones carrying the most meanings.

So English isn’t sloppy. It’s compressed. It offloads disambiguation to a receiver who has eyes, a memory, a model of you, and the option to say wait, what?

Now look at the receiver a programming language is built for. A parser has no context at all. It can’t see the pond. It doesn’t know you. It reads one token at a time and has to decide, mechanically, what the next one means.

Computer science has a name for the grammars we use to build those parsers. We call them context-free. That’s a precise technical term, and it’s also the most honest name in the field. It describes the receiver, not the language. Put a receiver with no context into the Piantadosi math and the cost of a misread goes to infinity, so the optimal amount of ambiguity goes to zero.

Same equation. Different receiver. Opposite answer.

the places code chose to sound like English

If that’s right, you’d expect to find natural language leaking back into programming languages wherever the designer decided the writer’s comfort mattered more than the reader’s certainty. You’d also expect the bugs to live exactly there.

They do.

Yacc knows. When you hand a parser generator an ambiguous grammar, it reports a shift/reduce conflict: it reached a point where it could either keep reading or close off what it has, and the grammar allows both. Yacc then picks shift by default, which happens to give you the C answer to the dangling else. Bison, its successor, has a directive, %expect, for declaring how many of these conflicts you’re planning to live with. It’s a waiver, and it’s in the grammar file.

C++ shipped one. Widget w(); looks like it makes a Widget. It declares a function named w that returns one. Scott Meyers called this the most vexing parse in Effective STL back in 2001, and C++11 added a whole second initialization syntax with braces partly so people could stop hitting it.

Implicit conversion is the language guessing what you meant. In JavaScript, "5" - 2 is 3 and "5" + 2 is "52". [10, 1, 2].sort() gives you [1, 10, 2], because the default sort quietly turns numbers into strings first. Each of these is a resolution rule, written down and deterministic. Each one is also the language behaving like a listener who decided what you probably meant and didn’t check.

Whitespace is prosody. JavaScript’s automatic semicolon insertion will take

return
  { ok: true };

and return undefined, because a line break after return is read as the end of the statement. That’s the language reading your pause like a human would, and getting it wrong like a human would. Python went the other way and made indentation the syntax, which removes the gap between what the code looks like and what it does. Then it had to forbid mixing tabs and spaces, because two invisible characters that look identical are a homophone.

Operator overloading is polysemy, deliberately. + adds numbers, joins strings, concatenates lists, and in some libraries builds a query. << in C++ shifts bits or writes to a stream depending on the type on the left. That’s the duck problem, imported on purpose, because one short familiar symbol reused across contexts is exactly the efficiency Piantadosi says natural language buys. It works for the same reason it works in English: the context (here, the types) usually settles it.

Every one of these was a deliberate trade. A designer looked at the writer, decided the writer was human and tired, and moved the dial a notch toward English. Convenience features are where natural language leaks back into code, and they bring their ambiguity with them.

the type system is a context machine

Which explains what a type system is. It isn’t mainly a safety feature. It’s a way to give a context-free receiver some context.

Look at what Rust does with this:

let n = "5".parse().unwrap();

It won’t compile. parse can produce an integer, a float, an IP address, anything that knows how to read itself from a string, and nothing on this line says which. The compiler stops and says type annotations needed. It doesn’t guess. That error message is a clarifying question. It’s the compiler doing the one thing a human listener does that a context-free parser can’t: noticing there are two readings and asking which one you meant.

Write let n: u32 = ... and the question is answered. The annotation is the pond. And the more of the program the type checker can see, the less you have to spell out on each line. That’s type inference: the receiver using context to resolve ambiguity, the way English does, but only where the context is provable.

That’s the formal-language move in one sentence. Ambiguity is fine when the receiver can resolve it with certainty. Everywhere else, make the writer say it.

where English gets a bill

Natural language has its own version of the compiler error. It’s called a lawsuit.

Contract law is the place where English is asked to behave like code and charged when it doesn’t. The classic case is Raffles v Wichelhaus, 1864: a contract for cotton arriving in Liverpool from Bombay on a ship called the Peerless. There were two ships called Peerless making that run, one sailing in October and one in December. The buyer meant one and the seller meant the other. The court found no contract at all. There had been no meeting of the minds, because the words resolved to two different things in two different heads. A name collision voided a deal.

The law even has a rule for who pays when this happens. Contra proferentem: ambiguous terms are read against whoever drafted them. That’s a loss function written into case law. It sends the cost of ambiguity to the party who could have removed it most cheaply.

In 2017 the First Circuit ruled on a Maine overtime exemption covering the “canning, processing, preserving, freezing, drying, marketing, storing, packing for shipment or distribution” of perishable foods. Was distribution its own exempt activity, or the object of packing for? No serial comma, no way to tell. The court went with the drivers, and the opinion opens with For want of a comma. The dairy settled for five million dollars. That’s what a shift/reduce conflict costs when the parser is a judge.

Engineers ran into this too. The IETF got tired of specs where should sometimes meant must and sometimes meant would be nice, and in 1997 published RFC 2119, a short document that does nothing except pin down what a handful of English words mean when they appear in capital letters. MUST. SHOULD. MAY. It’s a type system bolted onto English, and every RFC since has started by importing it.

Then there’s the cost when there’s no court and no RFC. Poe’s law, coined in 2005 on a forum argument about creationism: without some clear signal of intent, a parody of an extreme view is indistinguishable from the real thing. Text strips out the tone, the face and the room, everything that told the listener which reading to take. Strip the context from natural language and it starts failing like code, except nobody gets an error message. People just believe the wrong thing.

where the loss function flips

I wrote last month about the other end of this, the bar where week and weak collide on purpose. In verse the loss function doesn’t just tolerate a second reading. It pays for one. An extra reading that both makes sense and makes a point is the whole product.

So you get a spectrum, not a binary. At one end is the parser, where every extra reading costs infinitely much. Then the contract, where it costs money. Then conversation, where it costs a second of confusion and mostly saves effort. Then the pun, where it costs nothing. At the far end is the poem, where the extra reading is the payment. Ambiguity has a price everywhere, and it’s set by the receiver.

That’s the defense of the harder claim. These aren’t two kinds of language. It’s one problem, move a meaning from one head to another as cheaply as possible, solved for receivers that run from knowing nothing to knowing you very well.

the model in the middle

Which brings me to the strange new receiver.

A language model was trained on the far end of that spectrum. Billions of words of human text, where ambiguity is how the system stays efficient, and resolving it from context is the skill that makes you fluent. Then we ask it to write the near end: code, where one leftover ambiguity is a bug.

It’s good at this, and it’s good at it for a reason that should make you a little nervous. The model is the first code generator that behaves like a human listener. Hand it write a function that sorts the users and it doesn’t stop and ask which field, which direction, stable or not. It does what English trained it to do: it picks the most probable reading and keeps going.

Yacc picked shift and printed a warning. The model picks a reading and doesn’t print anything.

The ambiguity doesn’t go away. It gets compiled. The code that comes out is perfectly unambiguous. It sorts by created_at, ascending, full stop. But the question of whether that’s what you meant was settled by a guess, and the guess is now syntactically valid, typechecked, and sitting in a pull request looking exactly as confident as the parts you specified.

That’s why prompting feels like the work it is. It isn’t magic words. It’s disambiguation. You are doing for the model what the type annotation does for Rust and what RFC 2119 does for a spec: supplying the context that collapses the readings you don’t want, before a receiver that will never ask settles them for you. The good prompts look like specs for the same reason good specs look like code. They’re all heading toward the same zero.

The tests are the type system for this. A test suite is a set of readings you’ve ruled out in advance, in a language the receiver can’t misread.

where this leaves me

I don’t think we get a model that refuses to guess, and I’m not sure we’d want one. A collaborator that answered every vague sentence with type annotations needed would be correct and exhausting, which is a fair description of most compilers.

What I want is the warning. Yacc had the decency to tell you it had picked shift. Bison made you write down how many conflicts you’d agreed to. Every receiver that resolves ambiguity on your behalf should say where it did. The model sits in the middle of this spectrum, trained by the poets and graded by the parser, and the thing it’s missing is the shift/reduce report.

Until then, that’s your job. Read the generated code the way the First Circuit read the Maine statute: as a document that means exactly what it says, written by something that wasn’t sure what you meant.