Does a grammar declared per module pay?
targets.md names the question this project exists to answer — whether a grammar declared per module is a good idea — and then leaves it to be answered somewhere else. Seven programs have answered parts of it, each in its own README, each quoting the one before. This is the seven of them weighed together.
Four are tabulated below; the fifth, sixth and seventh have sections of their own, because each was written after this page existed and against the question this page ended on at the time.
And four readers who did not write any of it have since been measured against the question the seven programs could not touch. Their sections are at the foot, and they are the only evidence here not produced by the author of the thing being judged — see second-reader.md.
Nothing here is new evidence. What is new is that it is in one place, and that the numbers were re-measured rather than carried across.
The short answer
It pays per line it removes, and it pays most when it removes a rule rather than a spelling.
programs/grammar found the first half and it has held for two programs since:
notation earns in a domain that is flat and repetitive and earns little in one
that is small and structured. programs/digest found the second half, and it is
the stronger finding — a dialect there is not shorter code, it is code that
cannot express the mistake.
Against that: a declared grammar is silent in four ways where a fixed one is loud, and every silence was found by running a program rather than by reading the design.
What the seven declared
| operators | forms | dialect | program | |
|---|---|---|---|---|
ember — an assembler |
0 | 16 | 18 | 220 |
grammar — a PEG toolkit |
0 | 8 | 9 | 29 |
digest — SHA-256 |
17 | 3 | 20 | 96 |
ledger — fixed-point decimal |
10 | 6 | 17 | 44 |
prose — a document |
0 | 7 | 9 | 64 |
minibasic — a BASIC interpreter, basic until 2026-09-14 |
0 | 15 | 16 | 251 |
bignum — arbitrary precision, in two modules |
1 | 5 | 5 | 153 |
Lines are non-blank, non-comment. program is the module, dialect the file
it uses.
The split down the middle went unnoticed until the four were tabulated.
The two programs about another language declare no operators at all and
nothing but forms. The two about a value domain declare mostly operators.
prose was written afterwards to look for a third case and did not find one —
it declares no operators and seven forms, and a document turns out to be a
domain of steps like the other two. basic is the fourth with no operators,
and it is the one that says why the split is not a taxonomy of programs — see
The sixth program below.
That is not a coincidence and it has a name already. ember found that the call
shape and the pattern shape “divide by what the form is, not by taste” — a
pattern “reads as a step in a procedure”. So:
A domain of steps wants forms. A domain of values wants operators.
Nobody chose that. Four programs arrived at it independently and two more have arrived since, and the two halves of Parasol’s extension mechanism turn out to serve two different kinds of domain rather than being two spellings of one thing. What the sixth adds is a limit on the claim, not a sixth data point: it is a taxonomy of domains a dialect can see.
Where it pays most, measured
digest is the one case with an independent comparison, because Solveig has its
own SHA-256 and the two can be counted.
bitAnd in Solveig’s sha256sum.sol, in code |
24 |
of those, bitAnd(mask) — the mod-2³² discipline, written by hand |
18 |
the same discipline in Parasol’s sha256.psol |
0 |
where it went: masks inside sha2.psol’s declarations |
5 |
Eighteen hand-written masks became five declarations, and the six remaining
bitAnds in the Solveig version are real algorithm ANDs, which Parasol’s version
writes as &.
That is the whole argument in one table, and the saving is not the thirteen
lines. It is that after the header, forgetting a mask is not a thing the
program can do. + is addition modulo 2³². ember’s and grammar’s
dialects only ever saved typing; this one enforces a correctness discipline, and
that is different in kind.
The other end of the range is ledger, and it is honest about it: three of
its functions are format, padLeft and padRight, and none of the three is
money arithmetic. The dialect covers the domain and the program is mostly the
scaffolding around the domain.
What it costs
Four silences, each found by a program and none by a test.
| found by | |
|---|---|
| Choosing the form’s shape wrongly is silent. A pattern where a call was meant parses, quietly takes what came after, and fails at run time in generated code. Both readings are legal and nothing at the declaration can say otherwise. | ember, then grammar — the same bug twice, the second time by the author of the sentence that says how to choose |
A wrong precedence is silent. A module declares its own ladder, so there is nothing for @infix * 60 to be wrong against. digest put * on +’s rung, compiled, ran, and failed as an array index four calls deep. |
digest |
A template naming a hole twice evaluates it twice. >>> expands left four times. Parasol has hygiene and no way to say bind this once. |
digest |
A dialect ends at its domain and cannot say where. + as addition modulo 2³² is right for every line of SHA-256 and a trap for the loop counter beside it; / as rounding to the nearest hundredth is right for splitting a bill and wrong for taking a number apart. |
digest, then ledger |
And two more that only the fourth program could find, because it was the first with a value type rather than a machine word:
One precision is not enough. ratio interest to subtotal answers 0.07
where the exact value is 0.074995…. A ledger has amounts wanting two places
and rates wanting five, and a dialect has one scale to give. The domain boundary
is not only at the domain’s edge.
A spelling gets one meaning. Amount ÷ amount is a ratio; amount ÷ count is
an amount. Both are /, and nothing can dispatch on what the operands turn out
to be. One of the two has to become a form and read worse for it.
What it cannot do at all
A rule cannot be a form. grammar is the sharp case: a template cannot
declare a form, so a grammar’s rules cannot be notation. They go into a table as
data, and apply 'term is a lookup. ember’s README had said notation is not
recursive even when the program using it is; grammar is the counterexample
that says what happens next — the recursive part stops being notation and
becomes data.
That is the ceiling. Everything a dialect can do, it does to one expression at a time.
And a claim that was true and is not
A form is not a method that costs nothing at run time. It was predicted to
be, and digest measured it: a template saves 2.03 instructions per rotation by
not calling and spends 2.00 recomputing a constant nothing folds — 98% given
back. ledger then measured the same shape at 0.19% rather than 5.4%,
because a dialect’s constants cost per use and ledger’s uses sit outside its
loop.
Two measurements, and the second argues against the first’s significance. The case for folding rests on the claim being made true, not on the number. ROADMAP.md carries it.
The fifth program, and what it settled
The four above were all arithmetic or instructions, and this page used to end by
asking for a domain that was neither. prose is that: a
document language, with the document itself written in the dialect.
It found no third category. No operators, seven forms, and every content
line a step that appends — ember’s and grammar’s shape. The taxonomy
absorbed the new domain instead of being extended by it.
It found the ceiling one level below where grammar left it. A form takes a
block and a block holds statements, so a document nests as deeply as it likes
and the notation follows it down; Solveig’s braces carry the structure.
grammar’s wall was narrower than nesting — a template cannot declare a
form, and a grammar’s rules are definitions. A document’s are not. What a form
still cannot do is contain part of a line:
A form can contain content. A form cannot contain half a line.
Emphasis inside a sentence, a link mid-clause: a paragraph is one string, and no arrangement of words and holes reaches inside it. That is the answer to could Parasol do a markup language — the block structure yes, the inline structure no.
And it split “carrying a rule” in two. prose.psol’s indent <b: block>
cannot be left unbalanced, because a block cannot be left unclosed. But the
dialect did not invent that rule; it borrowed one Solveig already enforces.
sha2.psol invented its own — nothing in Solveig makes + mask to 2³². Only
the invented kind is evidence that a declared grammar does something a fixed one
cannot, and it remains the one clear instance in seven programs.
What was still unknown was a dialect used by somebody who did not write it — every dialect here having been written by the author of the file that uses it, an hour before, and a notation’s real cost being paid by the second reader.
That was measured on 2026-09-03, and it is the section at the foot of this page.
The sixth program, and the ceiling stated properly
The five above are all a pass over an input: they walk it from one end to
the other, in the order it is written. minibasic is a BASIC
interpreter, and it is the first with a program counter that can go backwards,
an environment outliving every statement, and statements running a number of
times the source does not say.
It found the ceiling is one sentence and not two. grammar said a rule
cannot be a form, so a grammar’s rules become data, and that was read as a limit
on recursion. It is not about recursion. An interpreter meets the same wall
twice with none in sight:
| what it wants to write | why it cannot |
|---|---|
| a form per statement kind | which kind it is arrives with the input |
an operator for BASIC’s + |
which operator it is arrives with the input |
Notation is fixed when a file is read. An interpreter’s every decision is made after that.
So an interpreter uses the least notation of any program here, and for one reason covering both halves of Parasol rather than two reasons covering one each.
And it dissolves the two-domain question rather than answering it. BASIC has
a real domain of values — + is add-or-concat, exactly what digest’s and
ledger’s operator dialects are for. It never reaches the header. The
interpreter never writes a + b on two BASIC values anywhere: it writes
binop:value(op, a, b) where op is a string that came from the input, and
each branch is a send that already knows its operation. Two + survive in 251
lines and both are pc + #1.
A program can contain a domain without being one, and the split above is a taxonomy of domains a dialect can see. A domain one level down is invisible to every header there will ever be.
What it bought anyway
Fifteen forms used ninety-three times, and the clearest of them is the counter:
fallThrough, step and jump to are the only places a position moves, so
two + on integers survive in the whole interpreter. That is locality and not
enforcement — nothing stops a hand-written pc := pc + #2, where sha2.psol
makes forgetting a mask impossible. The one clear instance of a dialect carrying
an invented rule is still digest’s, and it is now one in six.
And it is the first program that needed hygiene
take’s template has a temporary t; parseAtom has a local t and calls
take into it. The generated source renames the template’s, so the parser keeps
its token. Nobody noticed while writing it, which is exactly what the
argument for putting hygiene in with forms in 0.2.0 predicted, and the only
evidence that argument could ever have.
The seventh program, and why the invented kind traps
Every value domain here had been Solveig’s own integer wearing a rule: a
32-bit word, an amount in hundredths. bignum is a value
domain whose values are objects, and it is in two modules that were meant
to disagree about +, to find where a dialect ends. They did not disagree.
limbs.psol is five lines, declares no operator, and takes lib/control.psol
unchanged; the driver takes the same file and adds ^. One @infix + 60 add.
adds two limbs in the library and two bignums in the driver, and inside the
library one * is an integer product on one line and a bignum product two
methods down.
So the first domain dialect with nothing invented is also the first with no
trap, and the two facts are one. digest’s + and ledger’s * are
templates because their values are integers and a rule on an integer has
nowhere to live but the spelling; a template cannot look at its receiver, so
the rule reaches the loop counter beside the domain as surely as the domain.
A bignum’s rule lives in big:add, dispatch reads it off the receiver, and the
header has nothing to add.
A dialect traps its scaffolding exactly when its domain’s values are the substrate’s own, because then the spelling is the only place the rule can go.
That narrows the table above from the other side. The invented kind is the only
evidence that a declared grammar does something a fixed one cannot, and it is
also the only kind that costs a silence: the two are the same property. A
domain that is an object gets its notation from arithmetic’s spellings for
free and gets nothing else from the header, which is why this dialect pays
least of the seven: five lines, and what they buy is digit(t) and
carry(t).
And it measured the other question, the one from outside. Solveig’s own
ideas.md wants a large-number-math library one day. This one is 126 lines
of generated Solveig, correct against bc, and 170× slower than CPython’s
int at 1000!, 44 instructions per limb product with the one-based index
arithmetic costing as much as the array access. It is a library. The ratio is
what a C extension would buy, and nothing has waited for it yet.
—
The second reader, measured
second-reader.md has the design, the six predictions committed before the run, and the full result. What it changes here:
The notation cost nothing, and that is the headline. A reader who had never
seen the language wrote a correct program on the first compile-and-run,
taking =, %, &&, while, nested if and the precedence rungs from one
example and one table, and never opening the README in full.
And they declared an operator of their own, unprompted, in that first
program — @infix ++ 55 concat., at a precedence chosen to sit under +,
mixed into a module that also @uses a dialect. The claim this project exists
to test is that a programmer can declare notation. The first stranger to touch
it did, unasked, and got it right.
Every cost was on the other side of the compiler, which turns the cost table above inside out. Seven programs found four silences, and all four are things a dialect does — a wrong precedence, a hole’s shape, a domain boundary, a hole named twice. A reader met none of them. What they met was:
| A dialect is documented and its substrate is not | asString appears nowhere in anything a stranger is given; display and concat once each, one of those as filler inside a warning example. Every non-syntactic decision was guess-then-run, and string does not understand 'show' names a wrong message without naming a right one. |
print is a repr and the example taught it wrongly |
Seven string :prints in examples/clike.psol and the output of none shown; its only two output comments are on integer prints, where print and display are indistinguishable. The example demonstrated the message exactly where its trap is invisible. |
Both are fixed. Neither is about notation at all.
A declared grammar’s cost to its author is the dialect. Its cost to a reader is the substrate. Seven programs measured the first and could not have found the second, because an author already knows what sends exist.
And the proxy flattered it. The design said in advance that a session with
no context still knows Smalltalk, so anything produced untraceable to a document
counts as an assist and the bias runs one way. It ran that way: concat and
asString were guessed correctly from zero documentation. A reader without
that background does not guess them. So the one real finding is worse than the
run makes it look, which is the correction the design was built to allow.
What is still unknown, and it is the only thing left on this page’s list: the other half of a second reader’s cost — reading somebody else’s Parasol a year later. This measured learning a notation once. There is no year-old Parasol and no second author, so nothing can measure the rest yet.
A second run followed, below, and did not change that.
And a second reader, run to force the one rough edge
The first run never reached lib/clike.psol’s known rough edge, so a second was
designed around it: a three-way classification, which in C is else if.
The reader never wrote the chain. They wrote the nested braces on the first attempt, correct output first compile-and-run, no diagnostic emitted at any stage — because the dialect file explains the limitation in eleven lines at the declaration itself.
A limitation explained where it is declared is not a limitation a reader pays for.
Run 3 narrowed this on 2026-09-04, and it needed narrowing. The
print-is-a-repr trap is explained at the line it is about and the third
reader read that comment, quoted it back, wrote :print anyway and paid the
cycle. The rule holds for a refusal and not for a silence: else if is
something a reader must choose to type, so a warning read beforehand removes
the option; :print compiles, runs, and looks right, so the warning describes
something they cannot recognise until they already have it.
A warning prevents a failure you would have chosen. It does not prevent one you would have walked into believing you had succeeded.
It is kept rather than replaced, because it is true of what it was measured on. POSTMORTEM.md 26 is why it should not have been stated wider than that on one reader.
And it measured the first run’s two fixes. Both were one line, neither
touched the compiler, and the second run is the control: six does not
understand probes became zero, and the print-is-a-repr trap that cost run
1 a cycle was caught before it fired. The reader named REFERENCE.md’s new
What is not here: the messages section as the decisive signpost.
Which sharpens the rule this page ended on rather than replacing it:
A declared grammar’s cost to its author is the dialect, and its cost to a reader is the substrate — and both are paid in documentation, not in syntax. Two one-line sentences removed every cost the first reader met.
Neither of the first two readers opened the README. One grepped it after the fact, one never opened it at all. Six programs’ worth of evidence sat behind a front page that neither stranger read.
That was acted on, and the third reader measured the fix the same day.
README.md gained a Where to start section on 2026-09-04 — twenty-five lines
naming the dialect file, the example and the reference, opening with Not here.
Run 3 opened the front page first, met that section, went where it pointed,
and stopped reading README past that point and did not go back to it later.
Which is the front page working rather than the front page being skipped, and it is the first evidence any of it has ever been load-bearing. The entry point is still the dialect file, the example and the reference — a front page’s job is to be read once, briefly, and left.
What four readers say about the diagnostics, which is nothing
No reader has ever seen one. Four runs, four tasks, and the two built
specifically to force an error produced none — the first stopped by the eleven
lines lib/clike.psol spends at its else declaration, the second by
examples/dialect.psol showing a one-statement body wearing braces.
The costs a reader actually met came from three places, and Parasol’s diagnostics are not among them:
| the example | supplied nearly 100% of run 3’s program, by its author’s account, and taught run 4 the braces it copied |
| the dialect file | supplied the shape of the form run 4 declared, from repeat <n> times <b: block> |
| Solveig’s runtime errors | got run 4 out of the one real failure in four runs, from the message alone |
Run 4 met the failure this whole project is built against — solvm naming
banner.sol:8, a line in a generated file, after seven lines of correct output
— and the map that recovers it had not been written, being opt-in. The
substrate’s error text was good enough to self-diagnose without it.
A declared grammar’s cost to a reader is paid in the substrate, and so is the rescue. Four readers have been served by Solveig’s documentation and Solveig’s diagnostics at every point where this project’s own would have had to work.
That is the sharpest thing four runs have to say, and it is not what any of them was designed to ask.