Language reference
Everything in the language, in the order you would meet it. This page follows the formal definition, which is the authority where the two disagree.
{ x } is zero or more, [ x ] is optional, and |
separates alternatives. Words in this colour
are literal;
is a placeholder. Round and square brackets are interchangeable as long as they match —
square brackets are used by convention where a form takes a list of clauses.
Lexical structure
A semicolon begins a comment that runs to the end of the line. Each bracket is
a token on its own. A string is "…" with the escapes
\", \\ and \n. Every other character joins a token that
extends as far as it can, stopping only at a bracket, a semicolon, whitespace or a quote.
| Token | Form | Examples | Meaning |
|---|---|---|---|
| numeral | digits, optional sign | 42 -7 | a 64-bit integer |
| money | $ digits, optionally . and up to six more | $0.50 $20 | integer micro-dollars; $0.50 is 500 000 µ$ |
| duration | digits then ms, s, min or h | 250ms 30s 10min | integer milliseconds |
| Boolean | #t or #f | ||
| symbol | ' then a name | 'declined | a symbol value |
| empty list | '() | ||
| name | starts lowercase or with a symbol character | react best-of + | a variable or function |
| Name | starts uppercase | Step Exec Text | a constructor, record or type |
They are distinct kinds of value. You may add money to money, and a duration to a duration, but never money to seconds. That rules out an entire class of budget bug at the level of values rather than by convention.
Program structure
A file is a sequence of definitions and extended definitions. Definitions are the language proper. Extended definitions arrange the world around it: loading another file, receiving authority from the host, and testing. As in Ramsey's interpreters, a file's unit tests run after the whole file has loaded, so a test may mention anything the file defines, in any order.
::= (val ) | | (define () ) | (datatype {[ {( )}]}) | (record {( )}) | ::= (use ) | (grant ) | (script {}) | (under ({}) {}) |
Definitions
- (define name (formals) body)
- A function. The formals must be distinct. Top-level names are mutually recursive.
- (val name exp)
- Evaluate once and bind the result. If it fails, nothing is bound and the failure is reported.
- (datatype Type [Con (field ty)…]…)
- A sum type: a value is one of its constructors. Constructor names must be distinct, and they share one namespace across the program.
- (record Record (field ty)…)
- A product type: a value has all of its fields.
The difference matters when you write code over them. A datatype is consumed with
case, one branch per constructor. A record is consumed with (. r field).
It also shows up in how you build them:
(Sell "Margins are compressing.") (Message [role User] [content "FY2025 10-K"])
Expressions
Core forms
- (if test then else)
- The test must evaluate to a Boolean; anything else is a checked run-time error.
- (let* ([x e]…) body)
- Sequential binding: each right-hand side sees the bindings before it. There is no simultaneous
let. - (lambda (formals) body)
- An anonymous function, closing over its environment.
- (f arg…)
- Application. Arguments are evaluated left to right.
- (begin e…)
- Evaluate in order, answering with the last. Empty
beginis#f. Sugar for nestedlet*. - (and e₁ e₂) · (or e₁ e₂)
- Short-circuiting, and sugar for
if. - (. e field)
- Select a field from a record.
Case analysis
case is how a program consumes a datatype, a list, or a model's answer. If no
branch matches, that is a checked run-time error, not a failure.
::= | ( { | _}) | '() | (cons ) | _
Patterns are flat. A constructor applies to variables or _, never
to another pattern, and no variable may repeat within one pattern. This keeps matching a
single step, which keeps every proof about case to a single step too. Nested
patterns could be added later as sugar.
Building values
( {}) ( {[ ]}) (list {}) (cons )
The six agentic forms
These are what the language adds. Everything a program can do to the outside world happens
through ask and call; there is no other door, which is why a budget,
a deadline, the trace and the ownership rules apply to everything.
ask
(ask [])
Consult a model once, for an answer of type ty. The context is an
ordinary list of Messages that you build and pass: nothing accumulates behind your
back. The site is a name for this place in the program; scripts and traces are keyed by it, and
it defaults to the source location.
The answer is validated against ty before it is bound. If it doesn't fit, the ask
produces Invalid rather than a value. Asking again is a separate, visible
decision — see retry.
Before the call, ask reserves the worst case: everything it is
about to send, plus the largest answer ty allows. If that doesn't fit the budget, the
model is never called and nothing is charged.
call
(call {})
Use a tool once. The capability comes from grant and can only
come from there. A tool result is currently always Text; unlike ask,
call does not yet name the type of its result.
fail and catch
(fail ) (catch )
A failure is a value, not a crash. catch binds it to name and
runs the handler. (fail 'sym) is sugar for (fail (Raised 'sym)).
A failure propagates left to right: everything before it still ran and still spent.
budget
(budget ([cost ] [time ]) )
Everything inside gets at most that much money and time. Either limit may be omitted, which means unlimited. Nesting takes the smaller of the two, and whatever the inside spends is charged to the outside. A budget scope has one pool and one absolute deadline shared by every branch running inside it.
workflow and par
(workflow ([ ]…) ) (par )
A set of named steps. Dependencies come from the names each step mentions: a step whose inputs are ready starts immediately, alongside the others. The graph must be acyclic, and the names must be distinct — both are checked before the program runs.
Steps may not share a stateful tool, because the outcome would depend on timing. Such a
workflow is rejected before any step starts. To work in parallel on a kernel, fork it first.
par is sugar for a two-step workflow and answers with a Pair.
Types
::= Text | (Text ) | Num | Bool | Sym | (List ) | (List ) | | | Any
Which types can be asked for
A type is askable when it is built only from Text,
Num, Bool, Sym, List, and datatypes or
records whose fields are all askable. Any is not askable.
That one rule is what keeps authority unforgeable: a capability is not an askable type, so no model answer can ever contain a tool. Authority comes only from the host.
Bounds, and why a type costs money twice
A bound caps the answer's size, which caps the reservation. A Verdict with
400-byte reasons reserves far less than the same type with unbounded text, so it fits budgets
the unbounded one is refused by.
The type is also sent with every ask, as a JSON schema, and billed as input. The project's first real API call spent 538 input tokens on a 175-character prompt, 478 of them on the schema. So bounding an answer shrinks the reservation, while adding a constructor or a field makes every ask dearer on input for as long as the program runs. No bound on the text inside those fields helps, because what is sent is the schema, not the answer.
How a type maps to JSON
| µNorman | JSON |
|---|---|
Text, (Text n) | a string, at most n bytes when bounded |
Num | an integer |
Bool | true / false |
Sym | a string |
(List τ), (List τ n) | an array, at most n elements |
a record R | an object with exactly R's fields |
constructor K with fields | {"tag": "K", "f₁": …}; a nullary K is {"tag": "K"} |
Failures
Every failure is a value of the predefined Failure datatype, so you can
case on it like anything else.
| Failure | Means |
|---|---|
(Invalid raw) | the reply did not fit the type; raw is what came back |
(ToolError message) | a tool or the provider failed — a 503, a Python NameError |
(Refused category) | the model declined |
OverBudget | the reservation did not fit the remaining money |
PastDeadline | the scope's deadline passed |
(Raised reason) | the program's own (fail 'reason) |
A checked run-time error is a different thing and cannot be caught: an unbound
name, an if on a non-Boolean, a case with no matching branch, an
exhausted script. Those mean the program is wrong, not that the world misbehaved.
The initial basis
Predefined types
(datatype Role [System] [User] [Assistant] [Tool]) (record Message (role Role) (content Text)) (datatype Failure [Invalid (raw Text)] [ToolError (message Text)] [Refused (category Sym)] [OverBudget] [PastDeadline] [Raised (reason Sym)]) (datatype Option [None] [Some (value Any)]) (record Pair (fst Any) (snd Any)) (record Resources (cost Any) (time Any))
Primitives
| Primitive | Notes |
|---|---|
| + - * / | on Num, and on money or durations of the same kind. Scaling takes a Num. |
| = < > | comparison; < and > also order strings |
| cons · list | list is variadic |
| string-append · string-length | length is in bytes. These are the only string primitives. |
| println | print a value |
| (remaining) | a Resources record for the current scope: (. (remaining) cost) and … time |
There is no number-to-string, no substring, and no way to render a value such as a
Verdict back into a prompt. A program can only put text into a context that it
already has as text. This is the first thing a real agent runs into.
Predefined functions
These are written in µNorman itself, and each is defined by algebraic laws.
- (not b) · (<= x y) · (>= x y)
- The obvious things.
- (append xs ys) · (length xs) · (filter p xs) · (drop k xs)
- Lists.
droptreats a count of zero or less as zero. - (retry n f)
- Call the thunk up to n+1 times, stopping at the first success. Retries
InvalidandToolErroronly — neverOverBudgetorPastDeadline, since asking again cannot help and would spend what little is left. - (repair n f ctx)
- Like
retry, but the bad reply and a correction are appended to the context. Costs more per attempt, because the context grows — a trade-off explicit context makes visible. - (best-of k f check)
- k attempts, concurrently, keeping one that check accepts. Built from a recursive
par, so the cost adds up and the time is the longest attempt. - (window n ctx)
- A context policy: every
Systemmessage, then the last n others. Note that it moves system messages to the front.
Hosts and capabilities
(grant (model [id ] [in ] [out ] [ceiling ] [think ])) (grant (kernel)) (grant (filesystem)) (grant (http))
Every model field is optional and falls back to a default. Prices are in micro-dollars per
token: [in 3] is $3 per million input tokens. Prices are stated here, not
looked up — the API does not report them, so a grant is where the program says what it
believes a token costs. If they are wrong, the budget arithmetic is wrong.
Scripts and tests
Tests run against scripts: canned replies keyed by site, with a simulated clock. Every test runs in a fresh world — a fresh pool, a fresh deadline, the script rewound, an empty trace — so tests are independent and exactly repeatable.
(script [ (reply ) (out ) (latency ) (reply ) (out ) (latency )] [ (result ) (latency )])
Faults are injected the same way, which is what makes failure paths testable at all:
(reply "…") | what the model says; pair with (out n) for output tokens |
(refusal "category") | the model declines |
(provider-error "503 …") | the provider fails |
(result "…") | a tool succeeds |
(error "…") | a tool fails |
(latency 2s) | how long it takes on the simulated clock |
Test forms
(under ([script ] [cost ] [time ]) {})
- (check-expect e expected)
- Both sides run in their own fresh world, so evaluating the expected value cannot consume the script.
- (check-assert e)
- The expression is true.
- (check-fail e pattern)
- It fails, with a failure matching the pattern:
(check-fail (ask-v) (Invalid _)). - (check-error e)
- It is a checked run-time error — the program is wrong, not the world.
- (check-within e ([cost c] [time t]))
- It spends at most c and takes at most t, whatever its result. That lets you measure failures as well as successes.
- (check-equiv e₁ e₂ 'grade)
- Two expressions agree, at
'exact(result and the whole world, trace included),'resource(result, spend, time, host state), or'value.
A complete, runnable example of all of it together:
(grant claude (model [in 3] [out 15] [ceiling 8000]))
(datatype Verdict
[Buy (reason (Text 400))]
[Sell (reason (Text 400))])
(define ask-v ()
(ask claude Verdict (list (Message [role User] [content "FY2025 10-K"])) 'analyst))
(script flaky
[analyst (provider-error "503 Service Unavailable") (latency 1s)
(reply "{\"tag\":\"Sell\",\"reason\":\"Margins fell.\"}") (out 12) (latency 2s)])
(under ([script flaky] [cost $1.00] [time 1min])
;; A provider outage is a ToolError, which retry handles.
(check-fail (ask-v) (ToolError _))
(check-expect (retry 1 ask-v) (Sell "Margins fell."))
(check-within (retry 1 ask-v) ([cost $0.01] [time 3s]))
;; Failures are values: catch one and look at it.
(check-expect (catch (ask-v) e (case e [(ToolError m) m] [_ "other"]))
"503 Service Unavailable"))
Syntactic sugar
Sugar is defined by translation, so it inherits every theorem already proved about the core forms, with no new cases to check.
(let* () ) = (let* ([ ] ) ) = LETX(, , (let* () )) (begin) = #f (begin ) = (let* ([ ] …) ) (par ) = (workflow ([ ] [ ]) (Pair [fst ] [snd ])) (and ) = (if #f) (or ) = (if #t ) (fail ) = (fail (Raised )) (ask ) = (ask ) (budget ([cost ]) ) = BUDGET(, ∞, )
What is checked before a program runs
Each of these is decidable from the syntax alone, so the parser rejects it outright.
- The formals of a
lambdaordefineare distinct; so are a datatype's constructors and a record's fields. - A
workflow's binding names are distinct. - A
workflow's dependency graph is acyclic. - Patterns are flat, and no variable repeats within one pattern.
- Forms are applied with the right number of parts.
Askability is checked when the ask runs rather than at parse time, because a
function may ask for a datatype defined later in the file. Everything else about types is a
run-time check too. Moving those to compile time is what the planned type and effect system is
for — see Status.
Reserved words
val define datatype record use
grant script under check-expect
check-assert check-error check-fail
check-within check-equiv if let*
lambda case ask call fail
catch budget workflow begin
par and or _
cost and time are not reserved. They appear only in
fixed bracket positions, and they are the field names of Resources. An earlier
draft did reserve them, which made (. (remaining) cost) a syntax error in the
language's own basis. Running the tests caught it.
Going deeper
- The formal definition — abstract syntax, big-step semantics, every rule, and the theorems.
- The machine — the event-by-event semantics behind concurrency and shared budgets.
- The laws — which refactors are safe, which are not, and the nine problems writing them found.