Showing version 1 bot backfill · api · 2026-06-22 11:46:46

Telling Computers What to Do

Telling Computers What to Do

Natural Language, Non-Determinism, and What's Left for the Programmer


Abstract

For seventy years the history of programming has been a steady migration away
from the machine and toward the human. Each new layer of abstraction — assembler,
the compiler, the managed runtime — let us say more while writing less, and hid the
mechanism underneath. Large language models are the next rung on that ladder, but
they differ from every prior rung in one decisive way: they are non-deterministic.
A compiler given the same source always emits equivalent machine code. A model given
the same prompt may not. This paper argues that natural language is best understood
as a genuine new abstraction layer — the highest yet — and that its non-determinism
shifts the programmer's job from producing code to specifying and verifying it.
The work does not get easier; it gets relocated. And because the abstraction
amplifies intent rather than replacing judgment, it makes good engineers faster and
bad engineers dangerous faster.


1. Closer to the Human

We tell computers what to do. The entire history of that conversation is a story of
who has to do the translating.

In the beginning, the human moved toward the machine: we wrote in the machine's own
terms — opcodes, registers, memory addresses. Every layer invented since has moved
the meeting point closer to us:

flowchart TD
    MC["1950s · machine code<br/>11000111 …<br/><i>the machine's language</i>"]
    ASM["1950s · assembly<br/>MOV AX, 1"]
    C["1970s · C<br/>x = 1;"]
    PY["1990s · Python<br/>x = 1<br/><i>the human's language, almost</i>"]
    NL["2020s · natural language<br/>set x to one<br/><i>the human's language, literally</i>"]
    MC --> ASM --> C --> PY --> NL

Each layer moves the meeting point closer to the human — top (the machine's
terms) to bottom (ours).

Each step traded control for expressiveness. We gave up addressing registers by hand
and received variables. We gave up manual memory management and received garbage
collection. We never gave these things back, because the trade was overwhelmingly
worth it. The direction of travel has never reversed: from close to the computer,
to close to the human.

Natural language is the logical end of that line. It is the interface we already had
before we had any of the others.


2. A New Level of Abstraction

It is tempting to treat "AI writes code" as a novelty or a parlor trick. It is more
useful to treat it as what it is: a new compiler target, running in reverse.

Consider the Python example. When you write

print(sum(range(100)))

you are not describing what the CPU does. There is no print instruction in
silicon. The Python runtime, the bytecode compiler, and ultimately the C
implementation translate your intent into thousands of concrete machine
operations. You trust that translation completely. You almost never read the
generated bytecode, and you would consider it a waste of time to do so. The
abstraction earned that trust by being deterministic and total: print means
exactly one thing, every time, forever.

A model is the same kind of move, one level higher. You describe intent in English;
the model "compiles" it into Python. The Python is then compiled to bytecode, the
bytecode interpreted into machine code. Natural language sits on top of the entire
stack as the newest, highest layer.

flowchart TD
    intent["intent (English)"]
    py["Python"]
    bc["bytecode"]
    mc["machine code"]
    intent -->|"probabilistic compiler<br/>(the new layer)"| py
    py -->|deterministic| bc
    bc -->|deterministic| mc

This is a real abstraction, not a metaphor. It hides mechanism, it raises
expressiveness, and — like every layer before it — it lets us say more while writing
less.


3. The Dangerous Abstraction

Here the analogy breaks, and the break is the whole point.

Every prior layer was deterministic. gcc compiling the same .c file twice
produces equivalent output twice. The abstraction is referentially transparent:
you can stop reading the machine code precisely because the compiler guarantees the
mapping. Trust is earned once and amortized forever.

The model's mapping carries no such guarantee. The same prompt can yield different
programs. A given program may satisfy the words of the prompt while violating its
intent. The abstraction is leaky by construction, because it is statistical
rather than mechanical.

This is what makes it a dangerous abstraction. Not dangerous as in useless —
dangerous as in it withdraws the guarantee that made the previous layers safe to
ignore.
You could ignore the assembler's output. You cannot ignore the model's.

The practical consequence is blunt:

You need to check the code.

Not as a ceremony, but as a structural necessity. The abstraction does not certify
itself, so something else must. That "something else" is the programmer, and it is
the part of the job that does not go away.


4. Non-Deterministic Generation, Deterministic Verification

The resolution to the danger is not to distrust the abstraction into uselessness. It
is to notice an asymmetry that makes the whole arrangement workable:

The generator is non-deterministic. The artifact it generates is not.

A model may produce code unpredictably, but the code it produces is ordinary code.
It can be executed, measured, fuzzed, type-checked, and tested exactly like any code
a human wrote. The non-determinism lives entirely upstream of the artifact. Once the
code exists, it is as deterministic as any other code.

flowchart LR
    subgraph ND["non-deterministic · unreliable"]
        prompt["prompt"] --> model["model"]
    end
    subgraph DET["deterministic · reliable, repeatable"]
        code["code"] --> harness["tests · types · fuzz · CI"]
    end
    model -- generates --> code
    harness --> verdict{"pass / fail"}

This is the load-bearing insight of the whole discipline. We do not need to make the
model deterministic. We need to wrap a non-deterministic generator in a
deterministic harness.
Specs, tests, type systems, and review are not bureaucracy
in this world — they are the mechanism that converts a probabilistic process back
into an engineering one. The model proposes; the test disposes.

This reframes "you need to check the code" from a complaint into a method: checking
is cheap, repeatable, and automatable, in a way that the generation never will be.


5. The Programmer Reconsidered

If the artifact is verifiable and the generator is replaceable, then a quiet but
significant reframing follows about what a programmer is.

We are not syntax experts. Memorizing the exact incantation for a slice, a
list comprehension, a goroutine, was always incidental. It was the cost of operating
the previous abstraction, not the value we added. Syntax is the most replaceable part
of the job, and it is exactly the part the new layer absorbs.

Code is replaceable. Any given function can be regenerated, rewritten in another
language, or thrown away. It is an artifact — a build output of intent — not a
treasure. We have always known this in principle ("code is a liability, not an
asset"); the new abstraction makes it concrete.

Our job is not to create code. It never really was. Our job is to decide what
should be true
— to specify behavior, to define correctness, to understand the
system well enough to know when the generated artifact is wrong. Creating the code
was the means. The end is a correct, comprehensible system. When the means gets
cheap, the end becomes the entire job.

This is not a demotion. It is the part of the work that required judgment all along,
now stripped of the part that didn't.


6. Reading Code Can Be Fun — Java vs. Python

A worry hides inside all of this: if our job is now to read and verify rather than
write, is the job worse? Reading code has a reputation as the tedious half of
programming.

The reputation is wrong, or at least defeatable. Reading code can be genuinely
enjoyable — but only when the code is legible, and legibility is a design choice.
Consider the difference in temperament between two languages:

# Python — terse, implicit, trusting
def total(items):
    return sum(i.price for i in items)
// Java — explicit, ceremonial, self-describing
public BigDecimal total(List<Item> items) {
    BigDecimal sum = BigDecimal.ZERO;
    for (Item item : items) {
        sum = sum.add(item.getPrice());
    }
    return sum;
}

Why is Java so explicit? Because explicitness is a bet that code is read more often
than written
— that the types, the bounds, the verbosity are a message to a future
reader rather than a tax on the present writer. Python bets the other way: that
fluency and density serve the writer, and that the reader can keep the implicit
context in their head.

In a world where humans increasingly review machine-written code, that ancient
trade-off becomes newly important. The qualities that make code pleasant and fast to
verify — explicit names, narrow types, small functions, no clever density — are
exactly the qualities a verifier wants. The point is not "Java good, Python bad." The
point is that legibility is now a first-class requirement, because reading is now
the main event. And legible code is a pleasure to read, which means the new job can
be a good one — if we build for it deliberately.


7. Easier to Do the Wrong Thing

The highest abstraction has a cruel property: it lowers the cost of every action,
including the wrong ones.

When writing code was slow, slowness was an accidental safeguard. The friction of
typing, compiling, and wiring things together forced a minimum of deliberation. You
could not ship ten thousand lines of subtly broken logic in an afternoon, because you
could not write ten thousand lines in an afternoon.

Remove the friction and you remove the safeguard. It becomes easier to do the wrong
thing
— to generate plausible code that compiles, runs, demos well, and is quietly
incorrect. Plausible-but-wrong is the characteristic failure mode of this layer,
precisely because the abstraction is optimized to produce plausible output. The
danger is not gibberish; gibberish is easy to catch. The danger is confident,
well-formatted, almost-right.

This is why the verification harness of §4 is not optional polish. It is the replacement
for the friction we just deleted. The discipline has to be added back deliberately,
because the tool will no longer impose it for free.


8. The Amplifier Thesis

Which brings us to the conclusion that matters most, and it is not about the tools.

The abstraction does not supply judgment. It supplies speed. It takes whatever
understanding the operator brings and amplifies its consequences. That cuts both
ways, and symmetrically:

Good developers write good code.
Bad developers write code that breaks.
Exactly the same will be true of whoever is prompting the AI.

The model is a mirror with a multiplier. People who understand systems — who can
specify behavior precisely, who know what correctness means, who can read an artifact
and feel where it is wrong — will prompt well, verify well, and ship faster than ever
before. People who don't will ship broken systems faster than ever before. The
tool changes the slope, not the sign.

The optimistic reading of "AI writes the code now" is that judgment becomes worthless.
The opposite is true. When generation is free, the scarce resource is knowing what
to generate and whether it is right.
The abstraction commoditizes typing and makes
understanding more valuable, not less.


9. Conclusion

Natural language is the highest abstraction layer we have built, and probably the last
one of its kind — there is nothing above intent. Like every layer before it, it lets
us say more while writing less, and moves the meeting point with the machine closer to
the human. Unlike every layer before it, it is non-deterministic, and so it withdraws
the guarantee that let us ignore the layers underneath.

The response is not to refuse the abstraction, nor to trust it blindly. It is to
restructure the job around its grain: specify intent precisely, generate freely,
and verify deterministically.
Code becomes a build artifact. Syntax expertise
becomes incidental. Reading and judgment become the main event — and, done well, a
good one.

The tool is an amplifier. It will make the people who understand systems
extraordinarily productive, and it will let the people who don't fail at
unprecedented speed. Our task is to make sure that, individually and as a field, we
are on the right side of that multiplier.


References

  1. Talk: "[the inspiration for this paper]"https://youtu.be/3NSSGt9bZag
  2. Dijkstra, E. W. On the cruelty of really teaching computing science (1988).
  3. Brooks, F. P. No Silver Bullet — Essence and Accident in Software Engineering (1986).
  4. Abelson, H. & Sussman, G. J. Structure and Interpretation of Computer Programs (1985):
    "Programs must be written for people to read, and only incidentally for machines to execute."