Will ChenWill Chen
← Writingsystem design

Making prompts code-like without making them look like code

Prompts with the properties of code and none of its syntax. That rules out every control structure, because a conditional turns a paragraph into a program.

Will ChenWill Chen8 min

I wanted prompts to have the properties of code and to refuse to look like code. Every syntax proposal got measured against that one rule, and every one of them lost.

A prompt is the block of text you send a language model, and everything the model knows about your request lives in it. People write them fresh each time, like a note to an assistant. That works and does not scale:

  • the same request written twice produces two different runs
  • nothing in it fixes what order things happen in, or whether they happen at all

Code has the opposite properties: the same thing twice, structure you can point at, scopes that say which names are visible where, a defined order of execution.

The tempting resolution is to smuggle a programming language into the text box. If the thing you write looks like a program, you have not made prompts better, you have made a programming language with an unusually chatty runtime, and the people I wanted to write these are people who currently write prose.

Motivation

The question underneath is narrower than "can prompts be code": how can flow be predictable in a non deterministic system?

A language model samples its output. Ask it the same question twice and you get two answers, and that variability is the reason it can handle the cases you did not anticipate. Any structure you add is a bet that you would rather have predictability than adaptability at that particular spot.

So the target is a quantity rather than an absolute: "what we really want is MORE determinism not absolute determinism." As a pair of opposites, which names all four ways it can go wrong: "we don't want rigidity and we don't want verbosity or unpredictability, we want fluidity while keeping determinism."

That rules things out:

  • a programming language. Buys absolute determinism at the cost of everything the model is good at.
  • plain prose. Buys fluidity and gives no guarantees at all.

What is left is a document that is mostly prose with a few places where you have said exactly what you mean.

Design constraints

A document that is mostly prose with a few exact places in it needs some way of telling one from the other, and the obvious way is to make the exact parts look like code so that nobody can mistake them. That is the option I refused every time it was offered, and refusing it is the constraint the whole language came out of.

The mechanism is what a code construct does to the text around it. A conditional is a control structure with a body, so a paragraph containing one stops being a paragraph and becomes a program with prose inside it. The reader switches modes at that point, and the person I wanted writing these currently writes prose.

Three proposals were refused on that ground across ten weeks, each better-looking than anything I had at the time:

  • "i actualyl don't want any actual code elements in v1, the if statement u provided feels wrong."
  • "we don't want to make it look like code, focus on inline directives that are like structures that can be picked up on the parser, it must not read like code."
  • "remember our prompts don't look like code they look like notion documents."

A preference bends when the alternative is convenient, and this one did not bend once. The positive version is the whole design in one sentence: "it should be light stuff embedded into text."

Syntax design

A negative constraint decides nothing. Something had to say which constructs were allowed, or every proposal came down to whether I happened to like the look of it. The rule that made the question decidable: "we want to keep the inline deterministic prompting extensions natural language compatible, the grammar should make sense in terms of flow. so they have to be played off nouns, adjectives, adverbs, conjunctions, etc."

English already has slots. If every construct in the language occupies a slot that a sentence already has, the result reads as a sentence rather than as a sentence with machinery bolted to it.

  • @mention takes the proper-noun slot. It names the thing a clause is about.
  • #directive takes the modifier slot. It changes how an instruction is carried out without changing what the instruction refers to.

Both sit inside an ordinary sentence without breaking it. An if block does not occupy a slot English has, which is the same failure as before stated positively: there is no grammatical role for it to fill, so it has to bring its own structure and the paragraph reorganises around that.

Control flow

Removing if leaves a real gap, since a document that cannot branch cannot do very much. The replacement is the same abstraction one level up:

decision {
  case: there are too many documents {
    ...
  }
  case: if I am tired {
    ...
  }
}

The cases are sentences. Nothing in that block can be evaluated by a runtime, because "there are too many documents" has no truth value until something reads the situation and judges it. An earlier sketch of mine used arrows for the same shape, Urgent => ... and Just Interesting => ..., which is the same idea with less punctuation.

The pattern holds across the other constructs:

Programming constructPrompt-language equivalent
objects (pass by reference)semantic objects
reactive programmingwatch statements
pattern matchingdecision (LLM does the pattern matching)
scopescontext scopes
functionsAI interpolated tool calling
macrosAI dynamic JIT interpolation

The third row is where the parenthesis does the work, and it is what makes this a design rather than an analogy table.

  • A conditional evaluates a boolean. The runtime computes true or false and jumps accordingly.
  • A decision hands the branch to the model, which reads the cases and picks.

The runtime is not evaluating anything, so there is nothing for it to be wrong about, and the model is doing the only part it was ever going to do well.

The rest of the rows follow the same relocation. Reactive bindings become watch statements because something has to notice when a value changed. Lexical scope becomes context scope because "what is visible here" is the same question whether the answer is variables or paragraphs.

Runtime interpolation

One construct has no equivalent in any programming language. A full program:

@Load {5 - 20} latest messages from @ConversationHistory
@Telegram.SendMessage to reply to the user
Update @ConversationHistory
Look at all tokens I listed in @CoinsILike and check their prices on @Coinbase
@Telegram.SendMessage write {the best 5 performing tokens}
Update @ConversationHistory

It reads as a list of instructions you might leave for an assistant, and as a program every line has a defined effect. The @ names resolve to real things: a conversation history, a messaging integration, a list of coins, a price source.

The braces are the interesting part:

  • {5 - 20} is not a number between five and twenty chosen at random. The model decides how many messages this situation calls for.
  • {the best 5 performing tokens} is not a query in any query language. It is a sentence describing a selection, evaluated at the moment the line runs.

In a programming language that construct is meaningless, because there is nothing in the runtime capable of reading a description and returning the thing described. In a prompt language it is almost free, because interpretation is the one operation you have an abundance of. Finding the constructs that are cheap in your medium and impossible in the neighbouring one is most of what language design is.

Execution model

The architectural fork is between two ways of running the document, and they differ in what each demands of the writer before anything runs.

  • Precompiled. Parse the document ahead of time into a structure with fixed evaluation rules. Predictable, and it forces the writer to be precise before anything runs.
  • On-the-fly. Hand each part to the model as you reach it. Handles anything the writer left vague, guarantees nothing.

Choosing between them assumes the document is uniform, and a document under construction is not. It is partly structured and partly still prose, and those two states want opposite treatment. So the split runs along the content rather than along the program: "we should execute the plaintext sections agentically, basically if the user hasn't reified it into structured document blocks. this means we get a mix of dynamic interpretation and structured predictability."

That makes reification the deciding property. A block the writer has already structured runs predictably, and one still in prose gets handed to the model, with the syntax tree holding both as dynamic text blocks the agent runs alongside structured elements.

The determinism is therefore opt-in and local. You are not choosing a mode for your program, you are choosing per paragraph, by writing more precisely where precision matters and leaving prose where it does not.

Reification happens on demand rather than ahead of time, which is the timing decision that falls out of the same argument: a dynamic block should see the state that actually exists when it runs rather than the state predicted for it.

What it cost

The target is code-like, not code: "we don't need code as prompts we just need prompts to be code-like enough that people could treat it like code."

Code-like enough is a design target you can hit:

  • the reader can predict what happens
  • the writer can point at a piece and say what it does
  • the whole thing still reads as a document

It does not mean a compiler and it does not mean guarantees. Trying for either produces something nobody wants to write in.

The constraint that comes with it is the part that is easy to lose: people have to be able to understand the semantics of how prompts execute, and that has to be easy to learn. A prompt language whose execution model takes a week to learn has no advantage over the programming language it was avoiding.

The same move decides what to build.

  • By category: retrieval is a subsystem, so design a retrieval subsystem.
  • By destination: retrieval always ends up injected into a prompt, so what the language needs is a way to name a source inside a sentence, which @mention already was.

Asking what a feature eventually becomes, and building for that instead of for its category, is a move I have used on every project since.