Solveig

Parasol’s journal

Frozen on 2026-09-15: Parasol’s journal from its first day to the day it became a member of Solveig’s toolkit. From that day a day of work on Parasol is a day of work on Solveig and is in Solveig’s journal. Nothing is appended below this note.

What a day of work on Parasol actually consisted of, newest first.

CHANGELOG.md records what landed, per feature, with the commit that carried it. COMPLETED.md records the case for each piece of work as it was argued before the work was done. POSTMORTEM.md records the failures. Neither of the first two holds the shape of a day — what was picked up and why, what turned out to be wrong, and the decisions that produced no code because they were decisions.


2026-09-14: the project changed its name a second time, a hole was asked to be a selector, the compiler learnt to run solas, and the Makefile went to the root

Two things reached this directory from Solveig’s morning, and both came from a reading of engine.sol in the SDL extension. Hans proposed a region for Solveig, @with rect { x := #0. ... }, that reads every statement as a slot write on one receiver; it was scoped and held there, in Solveig’s ideas.md. Then he asked whether Parasol could carry it instead.

The block form cannot be a template, and the line form found the recorded limit

A @syntax with <o> <body: block> places the block whole; nothing walks the statements inside a hole, and repetition has been declined three times on the roadmap. The line form was tried, on <o> => it := o and slot <n: name> is <e> => it:n := e, and emitted it:n := #0 for every slot: instantiate replaces a parameter only where it stands as a name node, and a selector is text on the send node. That is programs/grammar’s prediction 4, a template cannot build a selector out of a hole, met a second time. The roadmap has the entry, and it says why the second customer does not get what it came for even if the feature were built: the output would say it:x, and the greppable .sol that made this the attractive home is lost either way. Count stays at one.

The name

Hans said he was not totally sold on Proto and offered pretex. The answer was no, on pretext, on PreTeXt, and on preprocessor being the design this compiler avoids. Three rounds of names followed, none taken, until he offered Parasol, which was better than it first sounded: plain English, sol in the tail, para- as beside and paraphrase as what the output is. COMPLETED.md 18 has the table. The extension went to .psol in the same breath, since .pro had been half the objection.

What the rename was checked against, and what it left alone

The same shape as 2026-09-01: a survey first, 90 tracked files under the directory and 24 .pro, then git mv for the directory, the C source directory, the headers, one document and every module, then a respelling over the tree in four rules, then the residue read line by line. Two things the rules could not decide were done by hand. The .sol.map header and the editor’s scope names moved with the name; the object model’s proto, in Solveig’s own sources and documents, did not, and the rules were written to miss it. And the records of the first rename were put back after the respelling had reached them, because they had started to say that .psol is four characters and that Pro keeps a three-letter abbreviation: a record of a choice keeps the words of the choice.

default_output_path counts five now; 13’s luck about .phx and .pro being the same length was recorded as luck, and this is where it ran out. Both suites and the editor’s test were green before a record was written.

--sob, and the forty lines not written

Hans asked in the evening whether Parasol could output a .sob directly, essentially, add solas into parasol, with -I and the rest of solas’s options and --expr off, so that it can act as an advanced solas without replacing it. Two shapes were put: link libsol.a and call sol_compile_options on the emitted text, forty lines; or write the .sol and run the solas beside the binary. The first is not hard. It is that the Makefile’s first sentence, main.c’s header, CLAUDE.md and the root Makefile’s note all say the build needs no Solveig and that this is the arrangement being tested, and a link would make all four false to save a fork. Hans took the second, and said where it is going: Parasol as a member of the toolkit, laid out as solas/ and solid/ are, its records and build folded in, not some outside experiment any more. Not now; on the roadmap beside the version entry, which is its gate. The driver was built so as not to care which way that goes.

What had to be decided was smaller than expected and all at the edges. One -I for both compilers, since every program so far keeps its dialect and its generated library in one directory. Under --sob, -o names the .sob and the .sol goes beside it rather than beside the source, so the pair and the map stay together. --dump alone is a usage error. Which solas: argv[0]’s directory first, PATH second, which a copy of the binary in a temporary directory with PATH taken away proves by failing with

  1. test_sob.c runs the binary, the first test here that does, because a fork cannot be checked from inside the library; its load-bearing check is that the driver’s .sob is cmp-identical to solas’s on the same .sol. Run against the old compiler from a clean build, 21 of 27 failed, and the six that passed were the ones asserting a file is absent, which is the control the conventions ask for and the reason to have read its output rather than its count.

The version stayed at 0.17.0. A bump was typed and taken back: the roadmap’s first entry says the next release is what decides one version or two, and a number from here would have decided it sideways. The changelog entry is dated instead, and says so.

The version decided, the plan written, and step 2 the same evening

Hans agreed one version, the tree’s, from the next release, and asked for the order in which Parasol becomes a member of the toolkit: documents first, or Makefile, or examples, or source? The rule that answered it is that docs/ is counted and expect.sol runs what is fenced there, so the documents go last and describe the layout the other steps made. Five steps went into the roadmap entry, and he said step 2.

The Makefile moved as a section, above the generic object rule, and the thing it was for turned out to be enforceable by the build rather than by a file boundary. The scoping had planned a grep over the sources for a Solveig include; the compile rule is that check already, since a Parasol object is built with Parasol’s include path and nothing else, and a probe file holding #include "solum/common.h" failed to compile as it should. What a grep could not have seen, the link can: bin/parasol links libparasol.a alone, and test reads its symbol table with nm -g and refuses a sol_ export, a pattern checked against bin/solas (matches) and against parasol_ (does not, because the space or the underscore has to come right before sol_).

One thing the scoping did not know. The first build compiled a Parasol source with Solveig’s include path, because the generic $(BUILD)/%.o rule matched first. GNU make 4 prefers the shortest stem, which would have chosen the Parasol rule; macOS ships 3.81, which takes the first match. The section sits above the generic rule and its comment says so, and the order satisfies both.

Decisions on the way: sanitize and examples became root targets for the whole tree rather than Parasol’s alone, since a sanitised run of Solveig’s suite was documented in the root Makefile as an invocation and never as a target, which is the exact shape of POSTMORTEM.md 15; run and check went; the dialects install beside Solveig’s library rather than under lib/parasol, so that step 4, where parasol learns to look beside its own binary, finds one directory and not two; Parasol’s dist went with the file, a release earlier than the plan said, because a dist that cuts a tarball under a number the roadmap has already retired was not worth carrying for a week. install and uninstall were run into a staging directory and read; clean was run and git status read after it. The root README’s pipeline line still said proto, a survivor of the rename the conventions call a defect, and it says parasol now.

Step 3: the C where the other tools keep theirs

git mv, three directories and five files, and one parasol/ fewer in the Makefile’s include path. The one decision the plan had left open was taken both ways: every test links both libraries under the one rule, because a second rule for five files would have been the make 3.81 ordering question again for nothing, and the five carry a test_parasol_ prefix, because test_map and test_use beside forty Solveig tests would not say whose map or whose @use. The documents needed almost nothing: they had been naming parasol/src/emit.c relative to their own directory since the first version, and from the root those paths are now simply the paths.

What the move found was not in the move. test_use cleans up from a list of the files it wrote, and the list was seven files short of what the cycle and collision cases add, so rmdir had been failing quietly since those cases went in and there were fifty-five parasol-test-* directories in /tmp, the oldest from this morning’s rename run. It reads the directory now. A hand-kept list of what a test wrote is the same stale list as every other hand-kept list in this project, and the conventions already say so about text replacements.

Step 4: beside Solveig’s, and what is generated goes under build/

The examples to examples/, the dialects to lib/, the programs to programs/; every @use "../../lib/control.psol" kept its shape because the directories kept theirs relative to each other. The one name that collided was basic, against SolaBasic’s basic.sol and its NBS corpus basic/, which were there first and are linked from everywhere; three names were put and Hans took minibasic, which says what the program is: line numbers and ten statements, and not the historic Tiny BASIC either.

The trap the plan named was removed rather than labelled. The scoping had reached for a naming rule so that a generated .sol beside a hand-written one could be told apart by an ignore pattern. Looking at what examples/ would then hold settled it another way: programs/expect.sol reads every examples/*.sol as a file of claims and runs it, and a generated vectors.sol, whose ; #14 comments are that syntax exactly, would have been counted, checked, and reported as documentation. So nothing parasol generates lands beside a source. build/examples/ and build/programs/<name>/ hold the .sol, the map and the .sob together, which is also where bignum’s @include "bignum.sol" looks first, and clean is rm -rf build again. parasol/.gitignore went: there was nothing left for it to ignore.

And parasol finds its dialects the way solas finds its library. bin/../lib after PARASOL_PATH, then PARASOL_LIB_DIR from the generated config.h, which is the Makefile’s file and not Solveig’s, so the section’s claim holds and the nm check still passes. A @use "arith.psol" from a scratch directory compiled and ran through --sob; a copy of the binary in that directory, with no lib/ beside it and nothing installed, failed with the cannot find note, which now names the fourth place it looked. The editor’s test.py, which is not in make test, had the old example path in two lines and was run by hand: 81 cases, 24 headers, 0 problems.

Two questions after step 4, and what scoping the last step found

Hans asked whether examples/ and programs/ should now be grouped into kinds, lang, utils and so on. No for the first, which the guide already organises one file per concept; not now for the second, because 400 links in the records name programs/<name>.sol and the checker verifies each one, so a move would mean rewriting history or teaching the checker a moved-from table. Held on the roadmap with that as the precondition and a trigger, and the map, docs/programs.md, named as the place to group instead.

Then the plan for the documents, scoped in five sub-steps, and the scoping found two things. About 150 fences in these pages carry no language word, and Solveig’s checker runs every such fence as Solveig, so no page can move until each fence is tagged or checkable; the answer is to teach the checker a parasol fence first, through --sob, which makes the examples in the prose claims rather than pictures and is the customer --sob was built without. And the count of links a move would break found forty-four already broken: step 4 had left every README under programs/ reaching Parasol’s documents as ../../docs/, which from there is Solveig’s, two of them resolving to the wrong roadmap and grammar rather than to nothing, with the suite green because none of those pages is one the checker reads. Repointed the same hour, and POSTMORTEM.md 33 says why it is the reason step 5 starts with the checker. The evening’s other three defects, 30 to 32, are there too: the leaked test directories, the proto survivor on Solveig’s front page, and the make the scoping assumed. All four found beside the work, none by the suite.

The day closed with steps 2, 3 and 4 done, 1 waiting on the release, and 5 scoped, with two calls left to Hans before its second sub-step: whether the records fold into Solveig’s or stay whole under a prefix, and whether the postmortem survives at all.

2026-09-13: a program written to find a boundary, and the boundary was not there

Hans asked what was outstanding, then for a program to write, then for the bignum, with one instruction beside it: pay attention to whether Solveig wants a maths extension in the shape of net, and whether Python’s math library is the measure of what advanced maths would mean. The program was chosen because every one of the six before it is one code module and one dialect file, and the roadmap’s only entry with no proposal, a dialect ends at its domain and cannot say where, has the design’s own answer sitting untested in examples/vectors.psol: a second file may declare + to mean something else, and neither file has to know.

The predictions went in first, with bc’s answers beside them

Six predictions, committed with bignum.expected before a line of code, the expected file produced by bc so that the program could not be its own oracle. The sharpest was the second: that lib/arith.psol would serve the driver unchanged for integers and bignums alike, because + names a message and a message dispatches. The one that was going to be wrong was the sixth, which put a number on the speed.

The program ran right the first time, and the two modules did not disagree

limbs.psol came out at five lines: the base, its width, digit(t) and carry(t), over lib/control.psol unchanged. The library is 126 lines, the driver 27, and the first run matched bc on every line. Prediction 2 reached further than it was written: inside the library, ai * b:at(j) is an integer product and result * b two methods down is a bignum one, under the same declaration in the same file. The two modules were built to disagree about + and had nothing to disagree about, and that is the finding. digest and ledger invented a + and a * because their values are Solveig’s own integers wearing a rule, and a rule on an integer has nowhere to live but a template on the spelling, which cannot look at its receiver and so reaches the loop counter beside the domain. A bignum is an object, so the rule lives in big:add and the header has nothing to add. A dialect traps its scaffolding exactly when its domain’s values are the substrate’s own. The roadmap entry has that as its proposal now, and it is a question to ask before writing a domain dialect rather than a feature to build.

What was measured

By binary search on --steps: 11,291,573 instructions for 1000! and 444,565 for one product of two 100-limb numbers, which is 44 per limb product. The disassembly says fifteen of the 44 are sends, four on the array and eleven arithmetic, and four of the eleven are i + j - #1 computed twice. Prediction 6 had said the cost was the array access; the index arithmetic costs the same. By wall clock, 1000! is 40 ms at -O2, the build performance.md measures, and 190 ms at the default -g build; CPython’s int takes 0.24 ms. That is 170×, and the prediction said two orders of magnitude. The -O2 measurement meant a clean rebuild of the parent and a second one to put -g back, which is what the memory about benchmarks says to do and why.

So the answer to Hans’s question is in two halves, and both are in the README. A large-number library is a library: correct with nothing missing from the machine, and @include reaches it. The case for a C extension is the ratio and nothing else, and no program has waited 40 ms for it. Python’s math is a different question from a bignum, and solveig-notes.md 5 sorts its contents into what would be primitives by 3.14’s own argument and what would be lib/math.sol; nothing in it was wanted by this program.

What the exercise found that nobody predicted

A misspelled message in a copy of the library reported [bignum.sol:27] over [calc.sol:16], one frame per generated file, and each map took its line back: 27 to bignum.psol:55, 16 to calc.psol:51. Prediction 5 held and Parasol was not changed. But 27 is also lines 54, 56 and 57, because a while body is emitted on one generated line and a run-time trace carries a line and no column, so the map’s column, which tests/test_map.c checks harder than anything else here, had nothing to apply itself to. Sixteen of the library’s 103 generated lines are spans of the module’s own lines, every one a loop body or an if arm. A four-line span is a recovery and not the exact one the map was built for. Two fixes, neither built: Parasol keeping a line break inside an expanded hole, or Solveig’s trace carrying the column its compile errors already have. A row under Rough edges, checked on the day, and solveig-notes.md 4.

n(#2) had to be a form, because f(x) on a name is x:f in Solveig. And nothing in Solveig bit, for the third program in a row.

What moved in the records

Seven programs where there were six, everywhere a running count is kept; does-it-pay.md gained a row and a section; the roadmap’s folding entry a third customer declining, its boundary entry a proposal, and its rough-edges table a fifth row. The parent’s journal has a paragraph pointing here. No version: nothing in the compiler changed.


2026-09-12, later: the second decision of 0.1.0, reversed

Parasol lives in Solveig’s tree now, as parasol/, with this history intact under it. The 0.1.0 entry below records the decision this reverses: solveig-sdl had written the rule about itself, an extension is not part of Solveig, and Parasol went beside Solveig rather than inside it, argued from something stronger than tidiness. A front end with privileged access to the compiler it targets proves only that Solveig’s author can write a front end for Solveig.

What was argued, and what decided it

The argument for staying out was made again today, from this project’s own Makefile and from Solveig’s own habits, and it was recommended. Hans’s answer was the other half of the same fact: Parasol emits Solveig and nothing else, so every change to the substrate is a change Parasol has to follow, and two repositories meant discovering the breakage one suite run late. A sibling checkout gives one-way sync, this suite against ../Solveig, and nothing the other way. One tree makes the follow-up one commit and one green run. That is a judgement about the cost of keeping up, and it is his to make.

What the boundary still is

Nothing about it moved. This Makefile builds without Solveig, includes no Solveig header, links no Solveig archive, and reaches the parent only through ../bin/solas and ../bin/solvm from the targets that hand them a file. What went is the minimum-version check in make check: the parent is the version this speaks to by construction, and PARASOL_SOLVEIG_MINIMUM stays in common.h as the record of the language level, reported by --version, for anybody building against a Solveig that is not the parent. SOLVEIG defaults to ... The parent’s make builds bin/parasol, its make test runs this suite after its own, and its make install installs parasol and lib/*.psol.

The records stay here as Parasol’s own: this journal, CHANGELOG.md at 0.17.0, COMPLETED.md, ROADMAP.md and POSTMORTEM.md, which Solveig’s docs/ refuses for itself and does not refuse for this directory. The version number stays Parasol’s until there is a reason to fold it in. The standup is the parent’s. The repository at github.com/hansolovkarlsson/Proto is archived with a pointer to Solveig/parasol.


2026-09-12: an audit after eight days, and a closeout that had to be reviewed

Nothing had landed since 2026-09-04. The day opened on a standup eight days old that was still exactly true: tree clean, level with origin, 0.17.0. Two sweeps were run against it, one paragraph of the README was rewritten, the closeout was written, and a review of the closeout sent most of it back.

The setup sweep found nothing to add

/project-setup in its ensure mode matched the records by role and found all five filled, each opening with its own note, the standup in scratch/ and ignored, the licence and the three Makefile targets in place. It wrote nothing. The one divergence it named and left: this journal is a single file where the newer convention is a directory of dated files. Seven tracked files mention journal.md, six of them with a link, and this file’s own note already settles the ordering question the directory shape exists to avoid. Not worth doing unless Hans wants it.

The audit found the day’s work recorded, and one sentence left behind

The segment was the twelve commits of 2026-09-04, since the previous closeout drew that line and nothing follows it: seven pieces of work by grouping. Six have entries in CHANGELOG.md; the seventh, CLAUDE.md arriving in the repository and being corrected, has none, and the changelog’s note leaves that kind of thing to this file, where 2026-09-04’s entry has it. The one roadmap item that closed is COMPLETED.md 17, and the defect shipped as 0.17.0 is POSTMORTEM.md 24. The code diff read clean: identity and path separated everywhere they were compared, both faces of the defect tested, one exit from parasol_source_read. Suite 63, 6, 69 and 13, 151, no skips; make sanitize the same. Every open item in the standup was already on the roadmap or in solveig-notes.md.

Three claims were off. ROADMAP.md’s map row says see below about a paragraph above it, and stands. POSTMORTEM.md’s scope said four cohorts over five bullets since at least 2026-09-03; that paragraph was being rewritten for entry 28 anyway and now says five. The third is the README.

The README said two readers, and the sweep that should have caught it knew a different word

README.md’s Where to start opened: Two readers who had never seen this language were put in front of it, and neither opened this page. Written at 10:42 on 2026-09-04 and true of runs 1 and 2. At 11:33 the third reader was recorded opening the page first, reading Not here, and stopping, and the same commit did what conventions.md says to do: it grepped for the claim rather than opening documents, found two strangers in four places, including the README’s own table of documents at line 721, and corrected all four. Then d01ad10 moved them to four. Line 37 spelled the same count Two readers and neither sweep reached it, nor did the five overtaken claims corrected at 12:01.

That is POSTMORTEM 13’s class, a count that is a fact about another document, and what it adds is narrow: the sweep was run, on the right day, for the right claim, and the claim had two nouns. A grep for a phrase finds the phrase. The count was not corrected until somebody looked for the thing counted rather than for one spelling of it, which an audit did eight days later while checking the section’s file references for a different reason.

The sentence now names the first two readers as they were, the third with the date, and gives no total: a total is the count that went stale, and second-reader.md declines to put run 4 on the reader axis at all. POSTMORTEM 28.

The closeout got the geometry wrong, and a review caught it before the commit

The first draft of today’s records said the count had been bumped six lines below the sentence, in the table under Where to start, and built 28’s cause on that adjacency: the same shape as 25, a sentence outlived by something within sight of it, and therefore the second day the 2026-09-04 standup had asked for before promoting its last-paragraph rule to a standing agreement. The hunk was at line 721. The table six lines below the sentence lists three files and carries no count. Every part of the argument built on adjacency was wrong, and so was the filing: 25 defines itself against exactly this case, a claim about another document, and 13 already names the class and its defence.

A code review of the four uncommitted files found that, and eleven smaller things beside it: a claim that no reader had seen the page since, when run 4 was handed the README after the sentence went stale; went to those files about a reader the record says was sent to two of them; every one is in CHANGELOG.md about seven pieces of work of which six are; one week later in one record against eight days in two; an em dash inherited into an edited line; a defect described in the present tense against this repository’s own rule. The four files were restored from HEAD and rewritten.

What the day actually establishes is smaller than what the draft claimed. The standup’s proposed rule, read a document’s last paragraph against its middle, has one instance behind it still, and this is not a second. What this is a second instance of is 19’s observation that a defence applied is not a defence that reached, and the narrowing it offers is the one in 28: grep for the thing counted under every noun the documents give it, not for the phrase that was just changed. Whether that goes into conventions.md is Hans’s call. The other thing the day establishes is about this record: the draft stated a fact about a diff from a glance at its hunk header, which is the documents cohort’s failure one level up, and the review that caught it was the same re-derivation 13 prescribes, applied to the closeout’s own output.

2026-09-04, later — two more readers, and both of them argued with the morning

The morning’s closeout said the day was work the documents had already written down. The afternoon was the opposite: two experiments whose results were not in any document, and both of them took something back.

The third reader was aimed at a diagnostic and hit an example

0.16.0 shipped in the morning on an argument and no measurement — the hole-kind error prescribes wrap it in braces now, and prediction 9, that the message says nothing about what to do, had been untested twice because nobody reached it. Run 3 was designed to reach it by a route the dialect’s eleven-line essay does not cover: a single-statement body with no braces, which is what a C programmer types without thinking.

The reader braced it. Correct output first compile-and-run, no diagnostic, and the reason is the prediction written down as a hedge — if the reader brackets everything anyway it will be because the example braces every body. examples/clike.psol braces every body, the reader took their syntax by direct analogy from it, and the hedge is the only prediction of the six that paid.

And it falsified a sentence written the evening before

The print-is-a-repr trap fired on a reader who had read the warning about it. They quoted the comment back accurately and wrote :print anyway. Run 2 had earned this the previous evening:

A limitation explained where it is declared is not a limitation a reader pays for.

It was generalised from one reader and one limitation into four documents the same evening, in a repository whose loudest standing rule is that one customer is not enough — a bar this project applies to features and had never thought to apply to a conclusion. POSTMORTEM 26.

What survives is narrower and better. else if is a refusal: a warning read beforehand removes an option the reader was about to take. :print is a silence: it compiles, runs, and looks right.

A warning prevents a failure you would have chosen. It does not prevent one you would have walked into believing you had succeeded.

Which is does-it-pay.md’s own silence category arriving from the documentation side, and it took a third reader because the second produced a sentence good enough to stop looking.

The fourth run changed who the reader was, and refused the easy version

Three runs had failed to reach a diagnostic. The reliable way to force one is to hand a reader lib/clike.psol with its warning removed, and that was refused twice — it would measure a file written for the measurement.

So the role changed instead, to a customer 0.16.0 names in its own justification and no run had contained: an author declaring a form, rather than a user of somebody else’s dialect. Nothing in the published surface models it — examples/forms.psol declares four forms and types no hole at all.

They typed <b: block>, copying lib/control.psol’s repeat, and then braced every use anyway — the model this time being examples/dialect.psol’s repeat #3 times { "tick":display }, a one-statement body wearing braces. Run 3’s finding one level up.

Four runs, and no reader has ever seen a Parasol diagnostic

Two of the four tasks were built specifically to produce one. Prediction 21 is retired unmeasured rather than asked a fifth time, and 0.16.0 stays justified by argument — which is now a settled fact about it rather than a pending measurement.

What did happen is the failure this project says it exists to prevent. parasol exited 0, solas exited 0, and solvm reported undefined name 'total' at banner.sol:8 — a line in the generated file, after seven lines of correct output. No map had been written, the map being opt-in. With one the recovery is exact: generated 8:5 is source 10:5.

The reader got out on Solveig’s error text alone.

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 Parasol’s own would have had to work.

And the experiment was part of what it measured

POSTMORTEM 27, and it is mine. The prompt handed both readers a three-command toolchain that omits --map, where README.md’s own quickstart includes it. The write-up was one edit from reading a missing map as the reader’s choice; it was the experimenter’s.

What the experimenter hands over is part of the surface under test.

The cost of the minimal invocation is measured. A reader’s likelihood of choosing it is not, and the default should probably change is left exactly as unsettled as it was. The method cohort had one entry and now has two.

What the afternoon says about the ritual

Both experiments were designed to measure a thing and neither did. Run 3 was aimed at a diagnostic and found a rule that had been over-generalised; run 4 was aimed at the same diagnostic and found the characteristic failure arriving from the substrate with a mechanism switched off.

Both findings came from predictions written down before the run — one from the hedge, one from the confound noticed while scoring. Neither was the point of its experiment.

An experiment that answers its own question tells you what you already suspected. The ones here have paid, four times out of four, in what they found on the way.

2026-09-04 — the records were right and two of them were read wrongly

This entry was written at the closeout and ended there. It was true when it was written and describes the first half of the day. Four more commits followed, two of them reader runs, and one of those contradicted a sentence written that morning — 2026-09-04, later is above, and the numbers here are corrected in place with the earlier reading kept. The same treatment 2026-09-03 got, for the same reason, on the entry that learned it.

Nothing was designed today. Four commits, two versions, and every one of them was work some document had already written down and nobody had done. That is the day’s shape and it is worth naming, because it is what a good set of records is for and it is also how the day’s two mistakes happened.

It opened cold, and the catch-up was wrong

There was no scratch/daily-standup.md. 2026-09-03 had been closed out three times and the standup was not among what survived, so the way in was git log and docs/ — which is what CLAUDE.md says to do, and it worked for everything except one item.

The catch-up reported the substrate documentation gap as the next thing to fix. It had been fixed and measured the previous afternoon. second-reader.md’s run 2 put the one-line REFERENCE.md pointer in front of a reader and got zero does not understand probes against run 1’s six. That is in the middle of the file. Its last sentence still called the gap the thing to fix first, undated and present tense, and the last sentence is what a reader carries away.

POSTMORTEM 25, and the cohort it joins is the one about documents rather than the one about the compiler. It is 19, 20 and 21’s failure arriving from a new direction: those are a claim about another document going stale, and this is a claim about the same document, overtaken by a section appended below it. Three sweeps on 2026-09-03 grepped docs/ for stale claims and none found this, because a sweep looks for a claim it can check against something else, and this one is only wrong against a later paragraph of itself.

CLAUDE.md, and the line in it that described a document wrongly

The file had been written and left untracked. Committing it turned up its own error: it glossed POSTMORTEM.md as predictions scored, which is not what that document is — it is every defect found, what caused it, and what found it, and predictions are scored in the journal and in does-it-pay.md. The wrong gloss sat four lines above the sentence saying each document’s own italic note outranks anything said about it from outside.

The README was fixed for the readers who never opened it

Two strangers used this language on 2026-09-03 and neither opened the front page. That was recorded in second-reader.md, again in does-it-pay.md, and a third time in yesterday’s journal entry noting it was not on the roadmap and probably should be.

A roadmap entry was the wrong shape for it: the fix is twenty-five lines and there was nothing to decide. README.md opens with Where to start now, naming the dialect file, the example and REFERENCE.md, then Solveig’s reference for the library — and it says not here in its first two words, because a signpost that will not say what it is not is the thing that was already wrong.

Two claims in that new table were wrong on the first draft and were caught by checking them. examples/clike.psol was called the shortest complete example and is not — dialect.psol is 41 lines to its 45. And lib/clike.psol was credited with explaining each limitation at its declaration, which undersells one half and oversells the other: it carries a header note listing what C has that it cannot, and the eleven lines at the else form. Both were rewritten to what the files actually are.

0.16.0 — a diagnostic that prescribes, on an argument and not a measurement

Both second-reader runs named a diagnostic that points correctly and prescribes nothing as the next thing to fix. Neither reached it. Run 1’s task had no cascade; run 2’s reader was warned off by lib/clike.psol’s eleven lines and said so unprompted.

So the evidence this entry was waiting for never arrived, and the work was done anyway. That is stated in the commit and in COMPLETED.md 17 rather than dressed up, because the argument is a generalisation and not a measurement:

A limitation explained where it is declared is not a limitation a reader pays for. It is one its author paid for once — once per dialect.

lib/clike.psol’s author paid it. The next dialect with a block hole has an author who has not, and a diagnostic is the only part of this system that reaches somebody who has read nothing.

The restraint is the design. Only block gets a prescription, because braces make a block out of anything and the advice is therefore right every time; nothing makes a place out of #1, and a note that prescribed there would be advice that fails, which is how a reader learns to skip the notes. A check holds that half.

And the placement turned out to be part of the fix. Put third, the note printed the reader’s own line a third time — diag.c drops a repeated caret only for a note about the span the line above underlined. Put second, between the error and the declaration note, it prints once and the fix sits beside the problem.

The advice was run rather than assumed: else { if (…) { … } else { … } } gave low, mid and high for n of 1, 5 and 10 — checked against the { { … } } trap the dialect warns about, which fails silently and would have made the prescription worse than none.

0.17.0 — the rough edge that was not cosmetic

README.md’s known-gaps table had carried this for four days:

A @use path is not normalised. examples/../lib/control.psol is what a diagnostic shows, and two spellings of one file are two files.

The first clause is about display. The second is about identity, and nobody had asked what else compared those strings. Both @use rules did:

The limit is what stood between this and a hang, and that is why the defect was quiet. A guard that turns an infinite loop into a bad error message makes a bug survivable and therefore invisible. It held exactly as designed and it hid the thing it was protecting against.

A ParasolSource carries identity beside path now — realpath, falling back to a copy where there is nothing on disk to resolve. Display is unchanged and deliberately so: the cycle error still names ./././a.psol, because that is what the file says and where somebody can look. Only the two comparisons moved.

The fix also produced a leak in the same hour it was written — fopen’s failure path had its own copy of the cleanup and did not gain the new field when the struct did. parasol_source_read has one exit now, which is the argument against remembering.

What the two mistakes have in common

One document said a fixed thing was open; another said an open thing was cosmetic. Both were read as summaries and both were right about the facts and wrong about the state. Checking each against what it described took minutes and neither had been checked in four days of sweeps.

A sweep verifies that a document agrees with the tree. Neither of today’s two disagreed with anything — they disagreed with what the reader would do next.

Which is the one thing this day suggests changing about the ritual, and it is not written into conventions.md yet because one day is an anecdote and this project’s own rule is that one customer is not enough.

The numbers, written last

This section read four commits, and two versions when it was written, and said that the closeout’s own records were uncommitted and that a fifth commit was somebody else’s to make. That was accurate. It is ten now, and the afternoon is the entry above.

Ten commits, two versions — 0.16.0 and 0.17.0 — and four reader runs’ worth of documents. fe94694..d01ad10, all pushed. The suite went from 61, 6, 69 and 11 — 147 — to 63, 6, 69 and 13 — 151, and has not moved since 0.17.0 landed before lunch: the afternoon produced four commits and not one line of compiler code.

Four defects, and none from a test. Two before the closeout — a compiler one found by checking how a rough edge was filed, a documents one found by a reader acting on a document and being contradicted by the rest of it. Two after — a rule generalised from one reader, and a method failure that is the experiment’s own. Five days running with nothing found by the suite.

The suite went from 61, 6, 69 and 11 — 147 — to 63, 6, 69 and 13 — 151. Four new checks, two per version, and all four are negative controls that were watched failing against the unfixed compiler with make clean between the builds. 0.17.0 was additionally run clean under make sanitize.

Two defects, and neither came from a test. One in the compiler, found by checking how a rough edge had been filed; one in the documents, found by a reader acting on one and being contradicted by the rest of it. That is four days running with nothing found by the suite, which is now long enough to be the pattern rather than a run of luck — and both of today’s have new rows in the tally, which is the fourth day in a row that has been true too.

This entry was written at midday and said the day had no work in it. That was true when it was written and false four hours later. It is corrected in place rather than rewritten, because the first half is a finding and the second half is what happened next — and because a record that quietly stops saying what it said teaches nothing.

The morning: a day with no work in it, swept anyway

The shortest section here, and it was not empty, which was the finding.

The day opened with the previous one already closed and pushed. git status clean, no branches, no worktrees, no stashes, nothing touched outside git since 19:40 the night before. Asked to close out the day, the honest answer was there is nothing to close out — writing a journal entry for a day with no commits in it would be inventing one.

That answer was given, and then overruled, and the sweep found three defects.

What it found

One of them had been false since the commit that wrote it. POSTMORTEM 20’s write-up says, of the entry below it:

The entry below reports the suite as “58, 6, 60 and 11” …

and the same commit changed that entry to read 69. The sentence describing the defect was falsified by the fix it was describing, in the same diff.

Two more were in the programs/prose CHANGELOG entry, which still read lib/arith.psol has no <= and one is still not enough. Both were overtaken by programs/basic the same evening, by a commit that had no reason to open that entry and did not.

Which is a different failure from 19’s and 20’s

Those two are about a claim going stale while nobody looks. This is the opposite: the claim went stale because somebody looked, and fixed the thing it described.

Write what a document read, not what it reports. A record of a defect is history the moment the defect is fixed, and past tense survives the fix.

A present-tense sentence about another document’s current state has a lifetime of one edit, and the edit that ends it is usually in the same commit — because describing a defect and correcting it are one piece of work. Three of the day’s three are that, one of them inside the entry about documents going stale.

And the ritual is not about the day’s work

A sweep audits the documents, not the day. Nothing was written on 2026-09-03 and three things were wrong in docs/ anyway, because they had been wrong since the night before and a clean git status says nothing about that.

So there is nothing to close out was the wrong answer, and it was wrong for a reason worth writing down rather than a slip: it treated the closeout as a report on work done, when what it actually is is an audit of what the documents claim. An empty day is not a reason to skip one, and conventions.md says so now.

The grep agreement earned last night was what found two of the three, on its first use in anger. The third came from re-reading the entry that agreement was written into.

Then Solveig’s README began linking Parasol

The hold was set on 2026-08-31 and expired the same day the expander landed; conventions.md had carried it since as Hans’s decision, not one to make unasked. It was made, six programs in.

Where it went is the part worth keeping. Not into the table with solveig-gtk and solveig-sdl — those live outside Solveig so that no dependencies beyond a C11 compiler and make stays true, and Parasol lives outside so that it cannot reach in. Same shelf, opposite reasons, and folding it into a table captioned both are built against extend.h would have said the wrong thing about what it is. It gets a paragraph of its own saying it takes no header, archive or symbol, and that the coupling is a file format and a command line.

The three-line invocation in that paragraph was run before it was written.

Which made the second reader real, so the question was designed

does-it-pay.md has ended on the same sentence since it was written: what no program had tried is a dialect used by somebody who did not write it. A stranger arriving from Solveig’s front page stopped being hypothetical the moment the link landed, so the design and six predictions went in first, per the rule.

And checking a prediction instead of asserting it found a defect

The fourth prediction was going to be the else-if diagnostic will not name the fix. Checking it found that it did not name the file:

/…/lib/clike.psol:72:43: error: 'if' wants a block here, and this is a send
parasol: chain.psol -- 1 error

A hole-kind failure whose argument is itself a form use reported the position of the template it expanded into. No trail, and the reader’s own file named only in the summary count. There was no line to go back to — the one failure README.md’s first commit names as characteristic of a per-module grammar, and the thing the map and the diagnostics exist to stand in front of. 0.15.0, and POSTMORTEM 22.

Nine versions missed it because a form in a hole is the case a dialect’s users hit and its author does not. The author knows the chain wants braces and never writes the version that does not. The check that was already there could not have caught it either: asserting that something is rejected says nothing about where the caret went.

The negative control passed, and it should not have

The worst half-hour of the day. With the fix written, the new checks were run against the unfixed compiler — the discipline tests/test_expand.c states in its own opening comment. It reported 60 checks, 0 failed, and the conclusion written down was that the checks did not catch the bug.

git stash restores a file with its original timestamp, older than the object built from it, so make rebuilt nothing and the control ran the fixed compiler. The failure mode of a stale negative control is that it passes, which is the one that gets believed.

Rerun with make clean between the builds it failed at once — and watching it fail showed the first attempt tested a milder shape than the reported defect, so a third check was added for a template that builds the node. POSTMORTEM 23, and make clean between the two builds is a standing agreement now.

The measurement, and what it turned inside out

A reader with no knowledge of the language, four published files in a directory of their own, and a task with a nested loop and a counter that examples/clike.psol has neither of.

Correct output on the first compile-and-run, the syntax taken from one example and one table, the README never opened in full. And they declared an operator of their own 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 whole project exists to test, taken up correctly and unasked by the first stranger to touch it.

Every cost was on the other side of the compiler, and that is what turns the cost table inside out. Six programs found four silences and all four are things a dialect does. A reader met none of them. They met a print that is a repr and an example that taught it wrongly, and a published surface that documents how operators become sends and never says what sends exist.

A declared grammar’s cost to its author is the dialect. Its cost to a reader is the substrate. Six programs measured the first and could not have found the second, because an author already knows what sends exist.

Two predictions were wrong and stay in. The bare-integer silence never happened — # came out of the example in the first minute, so the prediction described a reader who skips an example that was provided. And the else if chain was never written, the task not needing one, which is a fault in the design rather than a result.

The caveat earned its place. The design said in advance that a no-context session still knows Smalltalk, so anything untraceable to a document is an assist. concat and asString were guessed from zero documentation. Correcting for it makes the finding worse than the run makes it look, which is what writing the caveat before the run was for.

And then the same reader again, sent at the one rough edge

Run 1 had left its fourth prediction untested — the task needed no cascade — so a second run was designed to force it: three mutually exclusive ranges, which in C is else if. Predictions 7 to 11 went in first.

The reader never wrote the chain. Nested braces on the first attempt, correct output on the first compile-and-run, no diagnostic emitted at any stage, and the reason volunteered without being asked: I would have tried else if first if the dialect file had not spent a paragraph on it.

It has. lib/clike.psol spends eleven lines at the else form’s own declaration on why a chain cannot chain — the { { … } } that answers instead of running, the #54 where #40 was right, and the spelling of the fix.

A limitation explained where it is declared is not a limitation a reader pays for. It is one its author paid for once.

So prediction 9 is untested for the second time, and now for a better reason. The first run missed it because the task had no cascade; the second missed it because the documentation prevented the failure. The diagnostic’s unhelpfulness is real and stays unmeasured, and two runs agree on why: nobody reaches it.

And the alternation entry has been answered by a customer declining to need it — the fourth time on that page, and the first by a customer being told in advance rather than working it out.

The run was also a control, and both of the morning’s fixes held

Not designed as one and it is the cleaner result. Run 1 found two things, each got one line, and neither touched the compiler:

  run 1 run 2
REFERENCE.md names the message set as Solveig’s six probes against does not understand zero, and cited as the decisive signpost
the example shows what :print really prints cost a cycle caught — the one that would have bitten me

Two sentences removed every cost the first reader met, which is the sharpest thing said all day about where a declared grammar’s cost actually lives.

Neither reader opened the README

One grepped it after the fact. One never opened it and said so unprompted: worth knowing if you were expecting the front page to be load-bearing.

Two for two, and the entry point in practice is the dialect file, the example and the reference. Six programs’ worth of argument sits on a page the only two strangers to use this language did not read — and Solveig’s front page now points at it. That is not on the roadmap and probably should be.

The numbers, written last

This section read one commit, this one, and no version at midday, and six commits at teatime. It is wrong for the second time in one day, in the entry whose own lesson is that this happens, four hours after conventions.md gained the sentence write the narrative to last and the numbers to be replaced. The agreement was vindicated faster than anything else written here.

Nine commits, this one included — a number cannot count itself and has to be told to — and one version, 0.15.0. c61680a..69a2878 went to origin/main in the morning; Solveig took bf07077, and nothing else there is this session’s. The suite went from 58, 6, 69 and 11 to 61, 6, 69 and 11 — 147 — the three new checks being the whole of the change, and it has not moved since lunch.

Five defects, three before lunch and two after, from a day that opened with nothing to do: three stale claims found by a sweep on an empty day, one misdirected diagnostic found by checking a prediction, and one negative control found by not believing it. The afternoon added none — two readers, four compile-and-run cycles between them, and nothing broken. Not one finding from a test, for the third day running.

Two experiments, four predictions wrong. Run 1 was wrong about the bare integer and never reached the chain; run 2 was wrong that the chain would be written at all, and wrong that two readers hitting one wall would prove it was the wall. Every one of the four was wrong in the same direction: they assumed a reader would meet the notation’s edges, and both readers were carried past them by a document.

The tally has two rows it did not have this morning: checking a prediction instead of asserting it, at one, and not believing a control that agreed with the code, at one.

2026-09-02, later — a sixth program, and the sweep that closed the day was wrong about the tests

The day had already been closed out. c61680a wrote the entry below, landed POSTMORTEM 19 and two standing agreements, and reported the day’s counts. This session reopened it, and one of the things it found is in that entry.

The numbers are at the foot, for the reason the entry below spent three attempts learning.

It began as a question, again

Can parasol be used to create an interpreted language like BASIC?

Two questions wearing one sentence, which is what targets.md had to untangle a day earlier for the compiler version of the same ask. So the first answer was to separate them and then check both by running them:

Both halves were tested before either was claimed, which took ten minutes and is the only reason the second half’s two limits are stated as errors the compiler actually prints rather than as things that sounded true.

The sixth program

Predictions first, in 92be288, before a line existed. Then the interpreter: line numbers, LET, PRINT, INPUT, IF/THEN, GOTO, GOSUB/RETURN, FOR/NEXT/STEP, END, REM, and a prompt with RUN, LIST and BYE.

It was picked for the thing five programs had not been. Every one of them walks its input end to end in the order it is written. An interpreter has a counter that can go backwards, an environment outliving every statement, and statements running a number of times the source does not say.

Five of six predictions were right. The two that matter were right for the wrong reason, and the wrong reason is the finding.

Prediction 2 said BASIC’s + would go undeclared because the interpreter’s own counting outnumbers BASIC’s adding. It goes undeclared, and not for that: there is nothing to declare it for. The interpreter never writes a + b on two BASIC values anywhere. It writes binop:value(op, a, b) with op a string that came from the input, and every branch is a send that already knows its operation. Two + survive in the whole file and both are pc + #1.

So prediction 1’s reason covers prediction 2 as well, and grammar’s ceiling was never about recursion:

Notation is fixed when a file is read. An interpreter’s every decision is made after that.

That dissolves the question the program was picked to ask rather than answering it. It was picked as the first program with two domains in one file — a domain of steps and a domain of values — to see whether steps want forms, values want operators could hold twice inside one dialect. The value domain never reached the dialect. It is one level down, where no header can see it. A program can contain a domain without being one, so the split five programs found is a taxonomy of domains a dialect can see.

Two things nothing had tested

A form’s word and a message selector do not collide. @syntax step sits in the same program as six sends of s:step to a scan cursor, and both compile correctly, because a selector is never in primary position and the matcher never looks there. The README’s rule that a word in a pattern is not reserved anywhere else was written about variables. Something depends on it for selectors now.

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. Without hygiene the parser loses the token it just read, four lines before it uses it. Nobody noticed while writing it — which is exactly what the argument for putting hygiene in with forms in 0.2.0 rather than after predicted, and, being a thing that does not happen, the only evidence that argument could ever have.

The bar was met, and Hans took it

lib/arith.psol shipped <, > and == alone from the first commit. The entry keeping the other three out is the one a surface does not grow without a customer was written down against. programs/prose was the first customer a day earlier and wrote around the gap; this program wanted <= and >= five times and != four.

The count did not settle it cleanly and that was reported rather than smoothed over: two customers for <=, one each for >= and !=. Hans’s answer was to take all three, and the argument now sits at the declarations:

A customer count is per surface, and a comparison set is one surface.

An arith with <= and no >= is a worse trap than an arith with neither, because the missing one is missing for no reason a reader can see. Both customers were rewritten the same day, which is the check that they were real.

And then the sweep, which found the last session’s sweep

POSTMORTEM 19 was written last night about nine stale claims and landed the agreement that everything is read once at the end of a day. Tonight’s sweep corrected fifteen, and how they divide is the whole of POSTMORTEM.md 20.

Six were stale before last night’s sweep ran, and it missed all six.

Nine more went stale during today’s own work — a sixth program and three operators in a shipped dialect ripple further than they look — and were caught the same day.

What found most of them is the thing worth keeping. Not reading everything, which 19 already prescribed and which targets.md passed: it was read, for what it says about targets, which is what it is for. What found them was grepping for the claim instead of opening the documents. One search for five programs returned every instance at once, including three in does-it-pay.md; one for declined twice returned four files. A claim repeated in three documents is one claim.

That is a standing agreement now, beside the sweep it sharpens. And the file it was added to was two of the six, which is the part to remember: a document stating a rule is the least likely of all to be opened while the rule is being applied.

What the tests did today

Nothing, and correctly so — not one line of C changed. The suite sat at 58, 6, 69 and 11 all day, which is what it should do when the work is a program written in the language rather than a change to it.

The one number that moved was in a document, and it moved backwards: the entry below had recorded 60 where the suite has printed 69 on every run since.

The numbers, written last

Four commits, this one included, and no version — nothing in parasol/ was touched. One program, one shipped dialect completed, one roadmap entry closed by a customer, and one postmortem entry with nine instances in it.

programs/basic is 251 lines over a 16-line dialect: 15 forms, 0 operators, 93 uses. It is the fourth of six programs to declare no operators, and the prediction had said seven or eight forms — wrong by half, and the only one of the six predictions that was wrong about a number rather than a reason.

Not one finding came from a test, for the second day running. One came from a question asked in the first message. Two came from writing the program. Most of the fifteen corrections came from a grep, and the oldest of them — a description wrong for eight versions — came from editing a table for an unrelated reason.

The tally has a row it did not have this morning: grepping for a claim rather than opening the documents, at one — and it is the row that found the most.

2026-09-02 — a directive removed, a survey that was partial, a fifth program, and the bar

Every number in this entry is at the foot, and this is the third attempt at saying why.

The opening first read five commits, one version. Corrected to nineteen. Corrected again with the commit count moved to the foot and the sentence this opening carries only what stopped changing — and then the version range changed, in the paragraph claiming to have fixed the problem.

So the rule is harder than it looked: nothing about a running day has stopped changing, and the only stable thing in a journal entry is its date. Not the commits, not the versions, not the range, not the heading. Everything countable goes at the bottom where it can be written last, and the top carries only what happened. POSTMORTEM.md 13 three times over, in the entry that kept quoting it.

The day opened with what’s next todo? and built none of the four things that question was answered with. It closed having closed seven of nine differences nobody knew were there at breakfast.

It began as a roadmap review and went somewhere else

The answer to what’s next ranked the open items and recommended constant folding: the only entry with a measurement behind it, and the only one where the case was already made. That item is still untouched tonight.

What happened instead is that Hans asked how the thing works, four times, and each answer turned into work. The roadmap describes what is known to be missing. It has nothing to say about what is unclear, and unclear is where the day’s whole output came from.

Two of the questions were misreadings, and both were productive

Parasol runs the rules on the code and replaces the parts that matches?

No — matching happens during parsing, inside the reader, against a table the header built before a statement was read. There is no pass over finished code.

if the body was a different programming language, then the clash might not happen with the |, say Pascal for instance?

No — | is the reader’s constraint, not the body’s and not the emitter’s. A Pascal compiler written in Parasol still cannot declare |, and a Parasol that emitted Pascal still could not either, because the ambiguity is in { a | b } and that is Parasol’s own syntax.

Neither misreading is one a document written from the inside would think to correct. GRAMMAR.md says what the syntax is; the README argues why. Nothing said matching is not a scan, because nobody on the inside would imagine it was. Both are now in what-is-parasol.md, kept as the questions they were rather than rewritten into statements, and the pipeline is drawn four times in pipeline.html — 3156d26.

@language went, and a question is what unstuck it

The roadmap had said for nine versions that it should select the reader, or the emitter, or stop existing. It had not moved because two of those three depend on a build this project has already declined — targets.md refuses a second emitter, and a second reader is larger still. The entry was not waiting on a decision. It was waiting on something that was never coming.

What settled it was Hans asking what @language Pascal would actually declare. The answer is nothing anyone would expect, and following that out gives the argument the roadmap never had:

@language solveig. at the top of examples/forms.psol says the body below is Solveig. That file declares +, <, >, unless, while and swap. Its body is not Solveig and Solveig cannot read it.

The line was false in every file that declared anything, which is every file worth writing. Removed in 0.10.0 — eb07046, COMPLETED.md 14.

The near-miss is the part worth keeping

The first recommendation was not to remove it. It was to make it assert: one reader, one emitter, so any name but solveig is an error at line 1. Fifteen lines, touching no .psol, and the exact code a selector would need later. The argument for it was reversibility — asserting is cheap and deleting is not.

That argument prices a change. It does not ask whether the thing is right, and the thing was not right. A directive whose plainest reading is false is not fixed by checking its spelling. The reversal came from taking Hans’s question seriously rather than from any new fact.

One spelling per operation

\/ had been doing two jobs — logical or in lib/arith.psol, bitwise or in examples/utf8.psol and programs/digest/sha2.psol — and ~ two as well, logical not in arith and bitwise not in sha2, which is C’s meaning. A reader had to know which file they were in before they could read a line.

Hans proposed &&, ||, ! and asked whether \ was free for the bitwise or. It is, and it is the right answer: one character, where C writes |, which is the one character a dialect can never have. The single irregularity left is forced by the design rather than chosen, which is the best kind to be left holding — it points at the constraint instead of hiding it. b57aa31.

! for bitwise or was proposed and refused on the way, for the same reason @language had gone an hour earlier: ! reads as not everywhere, and lib/clike.psol already declares it prefix-not.

POSTMORTEM.md 6 is a blind replace of these same operators that mangled a shell command in a comment — a rename applied to a file rather than to a language. This one ran only on the code portion of each line, never inside a string or after a ;, printed all 27 changed lines to be read, and then proved itself the way that entry wishes it could have: every generated .sol in the tree is byte-identical across the change. Same messages; only the spelling moved.

A segfault from 0.1.0, found by checking a sentence

programs/digest/README.md says composing sha2.psol with lib/control.psol collides on four operators. Before repeating that in another document, it was run. parasol segfaulted.

parasol_dialect_add_infix answers the entry a redeclaration displaced, so the reader can say previously declared here — and it looked that entry up before growing the array, so a realloc that relocates leaves the caller reading freed memory. add_prefix and add_macro had it too. Latent since 0.1.0. f8b219a, POSTMORTEM.md 15.

It hid because it needs three things in one call: a collision, a capacity crossing, and a realloc that moves rather than extends. Every example and program in this tree composes dialects that agree. programs/digest is the first thing here with a dialect that redefines + — and its README says composing it collides, having reasoned about it rather than run it.

And the claim was wrong anyway. Parasol reported nine redeclarations, not four: the four whose meaning differs, plus two declared identically in both and three on a different rung. It is seven now, the spelling change having removed ~ and \/ from the overlap.

make sanitize, because the tool was already there

The regression check that came with the segfault only guards under a sanitizer: whether a stale pointer lands on freed memory is the allocator’s business, and in test_use’s process it does not. It fails under make test SANITIZE="-fsanitize=address" and is clean with the fix.

Which is the finding that should sting. That invocation had been in the Makefile since the first commit and nothing had ever been run under it. Not a missing test — a tool sitting in a comment, never picked up. It is a target now and a standing agreement in conventions.md, and the suite is clean under -fsanitize=address,undefined.

The fourth program, which argued against both entries it was written for

programs/ledger was picked to answer two roadmap items at once: the domain-boundary entry wanted a second program, and the folding entry wanted a second customer. Predictions went in first, in their own commit, so the ordering is in the history rather than in a claim.

Both answers went the other way.

The domain boundary is a pattern now — digest trapped on +, ledger on / — but the second instance narrowed what the entry may promise, twice. Which operator turns traitor is not predictable from outside the domain: this program predicted * and was bitten by /. And the boundary is not only at the domain’s edge — ratio interest to subtotal answers 0.07 where the exact value is 0.074995…, because a ledger has amounts wanting two places and rates wanting five, and a dialect has one scale to give.

Folding got its second customer and the customer voted against: 4,258 instructions against 4,250 hand-folded. Eight, or 0.19%, where digest was 5.4%. Identical shape, different bill, and the reason is not the dialect — a dialect’s constants cost per use, and this dialect’s uses are outside the loop. One measurement had made folding look larger than it is.

The survey was partial, which is the thing to remember about today

ledger found that #-1225 is an error in Parasol and a valid integer in Solveig. That went into POSTMORTEM.md 16 as a missing integer literal, and it would have stayed that size if Hans had not asked whether the survey behind it had been complete.

It had not. Comparing every form in Solveig’s grammar against Parasol, one file each, gives nine divergences out of eighteen. Four versions came out of that in an afternoon: the sign on #-45, $FF08, float exponents and the escape set in 0.11.0; #[a = b] in 0.12.0; %1011 in 0.13.0.

Three of those are worth keeping past the version numbers.

"\q" was the only one pointing the other way. Every other difference was Parasol refusing something Solveig takes — a smaller language and an honest error. That one was Parasol accepting something Solveig refuses, and emitting it, so a .psol compiled clean and produced a .sol solas rejected. Parasol emitting invalid Solveig is the single failure the map and the run-every-example discipline exist to prevent, and neither caught it, because no example has a bad escape.

#[a = b] was mis-sorted as free and was not. The lexer was never the obstacle. Solveig settles what = means in a key by precedence level, which Parasol cannot copy because a dialect may declare = anywhere — lib/clike.psol puts it at 10. It needed a rule saying a context shadows a declaration, the first in this language, confined to the top level of a key so that #[(b = c) = d] still uses the declared one.

%1011 cost something, and it is the first spelling here that did. || in 0.9.0 grew the fixed vocabulary and took only { || … } out of the core, and COMPLETED.md 12 held that up as the shape any future request should take. This is the second instance with a different bill: growing the fixed vocabulary took something from what a dialect may declare. A module declaring % can no longer write a %1 without a space. Small, unused here, and loud — and the next one might be none of those.

And -3 cannot be had at all. Solveig’s scanner gives the sign to the number outside a @expr region and treats it as the operator inside one. Parasol has no regions and cannot take -3 as a literal either, because then a -3 stops being a subtraction in every dialect that declares -. So everything but operator is Solveig’s own spelling was never achievable, and 0.1.0 chose against it without recording that it had.

And a last hour of putting things back the way they should have been

Asked whether lib/arith.psol should be completed with the bitwise operators: no, and the tree makes the case rather than taste. Bitwise has one usable customer rather than two — sha2.psol cannot share a file, its << being masked and the file standalone because it redefines + — and the two existing customers chose different rungs for & and >> against their own neighbours. <=, >= and != have no customer at all: every one of arith’s five users declares no operator of its own.

The neighbourhood turned up something else, though. Seven templates were standing in for messages Solveig already had — notEquals, lessOrEqual, greaterOrEqual, and a not that arith.psol had been spelling plainly all along while clike.psol wrapped it. digest runs 920 instructions fewer, and what is left in clike.psol is three templates, each one a message cannot be.

Somebody built the thing that could not be built

A second session, running against this same working copy, made a branch and declared | as an infix operator. It worked: two hunks in reader.c, no change to the lexer, the whole suite green, every block form intact, and an example whose commented values all came out right.

Three documents said that was impossible, in nearly the same words, and I had said it twice more during the day. README.md, lex.c and COMPLETED.md 12: { a | b } would have two readings, and naming the ambiguity does not decide it.

Two questions had been run together and given one answer. May | join the operator characters? — no, and that stands, because characters in that set run together and a | there would make |= a spelling. May | be declared? — a different question, a bar being a token of its own that a parser may look up without it entering the set at all.

And the resolution was already in the language. { a | b } is a parameter and a body by rule, and { (a) | b } escapes — which is #[(b = c) = d] in a different bracket, landed four hours earlier the same day. The mechanism was in that morning’s commit message and the argument against | was not re-read in its light.

The cause was misattributed in both directions, which is the part worth keeping. Entry 12 blamed the ambiguity and stopped. The session that built it reported removing Solveig’s constraint and had removed nothing of Solveig’s — lex.c untouched, and Solveig has the identical { a | b } and settles it the identical way. What stands in the way of | is Parasol’s own block syntax.

The work was reverted, not kept: it arrived uncommitted in a shared checkout and what it costs had not been looked at. ROADMAP.md is that looking, and carries the reason not to hurry — 0.13.0 had just settled the repository on \ for bitwise or because | was unavailable, and a spelling should be changed once. POSTMORTEM.md 18.

It also came within half an hour of being swept into a commit of mine. The edit landed at 11:59 and my last git add -A was 11:30. Two sessions in one working copy is a hazard that cost nothing today by timing alone.

The question this project exists to answer, finally written down

targets.md has said since 0.1.0 that the only thing this project exists to find out is whether a grammar declared per module is a good idea, and then left it to be answered elsewhere. Four programs had answered parts of it, each in its own README, each quoting the one before — and a dialect pays per line it removes appeared in four program folders and nowhere in docs/ or the README.

does-it-pay.md is the four weighed together, with the numbers re-measured rather than carried across. Tabulating them showed something no single program could have:

  operators forms
ember, grammar — about another language 0 16, 8
digest, ledger — about a value domain 17, 10 3, 6

A domain of steps wants forms. A domain of values wants operators. Nobody chose that; four programs arrived at it independently, and ember had already found the reason without knowing it was one — a pattern reads as a step in a procedure.

Re-counting also corrected digest’s own README in both directions. It claimed Solveig writes twenty-three bitAnds by hand and Parasol writes none. Solveig’s file has 24 in code, of which 18 are bitAnd(mask); Parasol’s has 0 masks and one bitAnd, a byte extract. Eighteen hand-written masks became five declarations — the finding survives and the figures did not.

And a fifth program, for the one thing four had not tested

does-it-pay.md ended by asking for a domain that was neither arithmetic nor instructions. programs/prose is that: a document language, with the document itself written in the dialect. It is also the answerable half of a question asked the wrong way round earlier — can Parasol take prose? Not as its body; but a dialect can be shaped so the document is the program.

It found no third category. No operators, seven forms — ember’s shape exactly. A document is a domain of steps and the taxonomy absorbed it.

One prediction was wrong and it was the best finding. Nesting was predicted to be the wall. It is not: a form takes a block, a block holds statements, and statements are content forms, so indent { … } nests to any depth and Solveig’s braces carry the structure. The real ceiling is a level lower — a form can contain content; a form cannot contain half a line. Emphasis inside a sentence has nowhere to go, because a paragraph is one string.

And it split carrying a rule in two. indent { … } cannot be unbalanced, but the dialect borrowed that from Solveig rather than inventing it. sha2.psol invented its own. Only the invented kind is evidence that a declared grammar does something a fixed one cannot, and it is still the single clear instance in five programs.

It also reversed an answer given the same morning. lib/arith.psol has no <=, and the renderer wanted one. Asked at eleven whether arith should be completed, the answer was no — no customer at all, every one of arith’s five users declaring no operator of its own. This is the sixth user and the customer. One is still not enough, which is the rule that kept the bitwise operators out on the same day; the roadmap records it so the second customer settles it.

The bar, landed, and a spelling changed twice

The retraction earlier in the day said | could be declared and did not do it. Asked for an example, the honest answer was that the current build refuses one — so a scratch copy was patched to demonstrate it, and the demonstration made the case for finishing it properly.

0.14.0 is the bar, and \ retired in the same commit. The rule is the one #[k = v] already had, and the reason the two went together is the whole argument for waiting a version:

0.13.0 had settled the repository on one spelling per operation, with \ for a bitwise or because | could not be had. Landing the bar alone would have changed one spelling twice in two versions.

A spelling should be changed once, and this one was changed twice — the second time deliberately, in one commit, with the table ending as C’s exactly and nothing substituted. The two gaps the demonstration had — @prefix | accepted and inert, and a stray bar with the wrong diagnostic — were closed before it landed rather than after.

The lexer was never touched. | is still PARASOL_TOK_BAR and still not an operator character, so a tool can tokenise any .psol knowing nothing about its dialect — the property COMPLETED.md 12 was written to defend and does defend correctly, even though the conclusion drawn beside it was wrong.

A table cell that misled its first reader

The one-spelling table had an em-dash in the logical-xor cell, meaning there is not one. Its first reader read it as a proposed operator and asked whether logical xor was a minus sign.

That is the table’s fault and not the reader’s, and the answer turned out to be more interesting than the correction. There is no symbol to have — C has no ^^, Java and Python reuse ^, Pascal uses a keyword — and Solveig’s boolean understands only not, and, or, ifTrue, ifFalse and ifElse. What there is is !=: for booleans, xor and not-equals are the same operation, which is why nobody invents a symbol for it.

So lib/clike.psol has had a logical xor since it declared !=, and nobody noticed — including me, that afternoon, while replacing that very template with the direct message. And if it were ever spelled as its own operator it would be ^^, for the reason Hans gave when the answer reached him: the single character is the bitwise one and the doubled one is the logical one, as & is to &&. Recorded, and not declared, because nothing has wanted one.

What the tests did today

Nothing, and mostly that was the job. The suite held at 58, 6, 34 and 10 while the spelling change went through, which is precisely what a spelling change should do to it — a control, not a detector. It ended at 58, 6, 69 and 11: the segfault added one, and the new spellings thirty-five.

Corrected on the evening of the same day: this line read 58, 6, 60 and 11, which is 135, in an entry that reports the total as 144 six paragraphs later. Nothing had touched tests/ since. See the closeout section below.

The numbers, written last

Thirty-two commits, this one included — a number cannot count itself and has to be told to, which is why this line has been rewritten as often as it has. Five versions, 0.10.0 to 0.14.0. A fifth program. And not one finding from a test.

Two misreadings, by a person asking. One dead directive’s real argument, by a person asking what it would mean. One nine-version-old segfault, by declining to repeat a sentence without checking it. Eight of the nine syntactic differences, by a person asking whether the first survey had been complete. One impossibility, by somebody building the thing that could not be built. And one misleading table, by its first reader asking what a dash meant.

Every one by a person or a program, and the tally has two rows it did not have this morning: being asked whether a survey had been complete, at two, and somebody building the thing that could not be built, at one.

Three documents arrived that had no home before: REFERENCE.md, the page you look a spelling up in; does-it-pay.md, the answer to the only question the project exists to ask; and programs/prose/README.md, a document about a document.

Those are the day’s real output. The five versions were the easy part — four of them closed differences nobody knew were there at breakfast, and the fifth landed a thing three documents had called impossible.

And the day closed with a sweep, which found five more stale things: a version heading four releases behind, a Known gaps row for something that landed in 0.12.0, a folding claim carrying one measurement when there are two, this page’s own count in two other documents, and — while the entry recording all of that was being written — the postmortem’s claim of 109 unit tests when there are 144.

None of them was the work. All of them were a document describing yesterday’s version of itself, which is the failure this journal spent the day committing and correcting three times. POSTMORTEM.md 19 collects the nine instances and says why 13’s defence was not enough: it works when a stale claim is in front of you, and nobody re-reads a document that is not being read. The one that worked was reading everything once at the end, whether or not anything was suspected, and that is a standing agreement now.

2026-09-01 — the project changed its name, and nothing else

A rename is a strange thing to give a journal entry. This one gets one because the reason was outside the repository, and because the checking was the whole of the work.

It was renamed twice, and the first one was wrong in an interesting way

Hans had already moved folder and repository to Phoenix Proto before the session opened, to mark the tree as a prototype restart. The session’s first job was small: the local origin still pointed at the old URL, redirected by GitHub and working, and would have broken the day somebody else claimed the name.

Then the decision changed — drop the qualifier, and the name is Proto, plain. The prototype framing is the honest description of the thing rather than a modifier on a name that was leaving anyway.

The discarded step is what made the second one clear. The first pass had deliberately left phx_, Phx and .phx alone, on the reasoning that “Proto” marked a restart of the project and not a rename of what was being built — the language was still Phoenix. That distinction was written down as a standing note and had to be deleted the same day. It was a real reading of a real instruction and it was wrong, which is the argument for asking rather than inferring when a name is doing two jobs.

The name had been promised to something else, in writing, three days earlier

hansolovkarlsson/Solveig, docs/ideas.md:4451: Trigger: wanting a library that Solum consumes and that is not written in Solum. Nothing has wanted one. The name, should it happen, is Phoenix.

Dated 2026-08-28 — three days before this project had an expander. The idea it names is genuinely a different language: it earns its place by publishing a library Solum consumes, and that entry refuses in advance a nicer skin on this one. Proto emits a program’s source.

So the collision was real and had been sitting in two repositories at once, with the unbuilt idea holding the name by reservation and the shipped compiler holding it by use. The rename ends that. The reservation in ideas.md was left exactly as it stands, which is the point of having gone and read it before rewriting anything — a search-and-replace across two repositories would have taken it.

What the rename was checked against

conventions.md says every text replacement asserts its match, and three of the recorded defects are that rule being skipped. So the suite was run green first, to have a number rather than an impression to compare against. Afterwards it reported the same 58, 6, 34 and 10 checks over the same ten examples and programs, with no new warnings.

Two checks were worth more than the suite, and neither is a test:

The diff is 1,435 insertions against 1,435 deletions. A pure respelling cannot be any other shape. A replacement that swallowed a line would show up here and nowhere in the tests, because the tests only run what still compiles.

The ambiguous matches were looked for before the sed ran, not after. Every .phx in the tree turned out to be a file extension; there was no bare PHX; and the only odd identifier among the hundred and thirty-six was has_phx in default_output_path, which compares length - 4 against ".phx". .pro is also four characters, so that line survived by luck rather than by design — worth writing down, because .proto would have broken it silently and only the examples would have caught it.

One thing it cost

git log --follow no longer walks lex.h past today on its own. That header is dense enough in identifiers that respelling them all dropped it under git’s 50% similarity threshold, so it records as a delete and a create where the other thirty-five moves record as renames. The history is not gone — git log --follow --find-renames=30% walks it back through 0.4.0 to the first commit — but the default is now wrong for one file in thirty-six.

A stale number, found and then fixed

ROADMAP.md said @language was inert “eight versions in”. The changelog has nine, 0.1.0 through 0.9.0: the sentence was written at 0.8.0 and 0.9.0 landed under it without disturbing it. It is the same failure as the heading in edcf4a0 — a number true when written and not re-read when the thing beside it moved — and it is the third time this document set has been caught holding one.

It went in here as found and not fixed, on the reasoning that the day’s job was a rename and this is a correction to an argument rather than to a spelling. Hans asked for it the same hour, so it reads nine now. The identical phrase in the 2026-08-31 entry below is deliberately untouched: eight was true on the day that entry describes.

The afternoon: putting what only the session knew into the documents

conventions.md opens by saying its contents are written down so that neither depends on anybody remembering them. The rename made that concrete in a way that had already gone wrong once without being noticed.

Standing notes had been accumulating in a per-directory store keyed by the project’s path on disk. Renaming the folder moves the key. The previous rename, the day before, had already orphaned three of them — that Solveig’s README does not link here and why, that Solveig findings get written up in a shape liftable into that project, and that scratch/ is not to be read — and nothing announced it. They were live rules that had quietly stopped applying to anything, and they were only found because today’s rename was about to do it again.

All three were already in conventions.md, which is why nothing was actually lost, and which is the argument for that file existing. The one that was not written down anywhere is now the fourth standing agreement: the name Phoenix belongs to a different project, with the consequence that costs something if forgotten — Solveig’s ideas.md entry is that project’s reservation and is not to be rewritten. A search-and-replace across two repositories would have taken it, and would have looked like tidying.

Two defects went into POSTMORTEM.md, 13 and 14, and neither is a defect in Parasol. 13 is the stale version count: a number correct on the day it was written, in a document that had no way to notice when the document it described moved — a fourth kind in the In the documents cohort, where the other three are edits that ran and did nothing and this one is no edit at all. 14 is the Phoenix-Proto misreading, recorded although it cost nothing, because what it cost could have been large: an inference written into a standing note is indistinguishable from something Hans said. Both were found by a person rather than by a check, which is a first for a pair.

COMPLETED.md 13 keeps the case — the three extension schemes that were on the table, and why full-word identifiers with a short .psol extension beat being consistent.

Adding 13 and 14 immediately produced a fourth copy of 13. Three live documents said three of twelve recorded defects are the unasserted-replacement mistake, and the total went wrong the moment the count did. Bumping them to fourteen would have bought a year, maybe. They say three of the recorded defects now: the fraction is the point, the denominator was never load-bearing, and POSTMORTEM.md is the one file allowed to know it. The identical sentence in the 2026-08-31 entry below keeps its twelve, being a record of a day.

The first attempt at that removal deleted the word and left the preposition — three of recorded defects — in two files. Caught by reading the result rather than trusting the three assertions that had all correctly reported one match each. A replacement asserting its match proves it fired, not that it was right, which is defect 6 in miniature and the reason that rule has a second half.

What is still open

The folder is ~/Projects/Parasol now, moved at the end of the day rather than during it: a directory cannot be moved out from under the session working inside it.

Nothing on the roadmap moved. A rename is not progress on any of it, and the two decisions that are Hans’s — what @language should select or whether it should stop existing, and whether Solveig’s README links here now that the hold is met — are open exactly as they were yesterday.

2026-08-31, later — a question about |, and the third program

The day did not end where the entry below says it did.

It started by reading the documents rather than remembering them

The session opened with check the documents and list tasks, which is the same move that caught the four-versions-stale roadmap in 5bf83af — and it caught a smaller version of the identical thing. ROADMAP.md still had a section headed Next — optional and repeated parts over a body explaining that the item moves below whatever the next program finds. The body had been updated when programs/grammar declined the item; the heading was four words of the old plan, left standing.

A heading outliving its section is the cheap form of the failure that document set exists to prevent, and somebody skimming headings reads it as current. It is edcf4a0, and the section is called Waiting on a customer now, borrowing the vocabulary already at the foot of COMPLETED.md.

A confusion that was worth having

The question was why | could not be an operator, and the proposal was a @token directive: name a token, bind a spelling to it, and let @infix name the token instead of the characters. It was refused, and working out why took longer than the feature that came out of it.

The proposal does not reach the blocker. What stops | is not that @infix cannot spell it — \/ needs no help and declares fine — it is that { a | b } has two complete legal readings once | means something, and the declaration is not on that line to disambiguate them. A collision between core syntax and declared syntax happens at the use, and no spelling of the declaration reaches it. The split the proposal wanted already existed: PARASOL_TOK_OPERATOR is spelling only and @infix is meaning, so the real question was never how does a file name a token but which spellings are in the vocabulary.

So the vocabulary grew. || is two bars and not a bar — the lexer takes it before the bar, hands it to every dialect and lets none of them declare it, and { a || b } was already an error so nothing legal was taken. The one casualty was { || … }, an empty temporary list that emitted nothing and appeared nowhere. That is 0.9.0, and lib/clike.psol stopped apologising for \/.

What made it cheap was checking rather than reasoning. Both risky cases were compiled before the change was written. One of them turned out to be a real cost and the other turned out to be already-illegal, and neither was obvious from reading the parser.

The third program, and the first one that measured anything

programs/digest — SHA-256 — was chosen because Solveig’s own sha256sum wrote the gap down in its findings: @expr has no bit operators, so the one file here that is nothing but shifts, xors and masks is the one file that cannot use the notation at all. Parasol’s ROADMAP has claimed the answer to that since 0.1.0 with nothing to point at. Same algorithm, same substrate, one file with a fixed infix region and one that declares its own.

Five predictions, recorded first. Three right, one right and duller than hoped, one wrong — and the wrong one is the whole value of the program.

The claim was a form is a method that costs nothing at run time, since a template expands rather than calls. It is not true. Measured by binary search on --steps, the way Solveig’s own program measured its version: the template saves 2.03 instructions per rotation by not calling, and spends 2.00 recomputing a #32:sub(#17) that nothing folds. It gives back 98% of what it saves. Two predictions that were written as separate lines turned out to be one finding with the numbers meeting in the middle.

That is now a roadmap item with a measurement attached, and deliberately not a patch: folding a send at expand time means deciding which sends are safe to run, and integer:sub is a slot a Solveig program may assign. It is the guard question one size smaller, and it gets the same treatment.

What the program found that nobody predicted was three things, and the best of them is that a wrong precedence is silent. * was declared on +’s rung, at + i * #4 became (at + i) * #4, it compiled, it ran, and it failed as an array index four calls deep in generated code. A module declares its own ladder, so there is no ladder to be wrong against. It is defect 11’s shape one level over: both readings legal, nothing at the declaration able to warn.

The other two: a dialect ends at its domain and cannot say where — + masking to 32 bits is right for SHA-256 and a trap for the loop counter beside it — and 0.4.0’s collision rules got their first real customer, four collisions against lib/control.psol, reported exactly as designed, and the answer was to not compose.

What was written down rather than built

Three things went into the documents and not into the compiler. Constant folding in the expander is now a roadmap entry with a measurement instead of a patch, because folding a send means deciding which sends are safe to run. A dialect ends at its domain and cannot say where is recorded with no proposal at all, because one program is an anecdote. And the @token refusal is in ROADMAP under a dialect that changes the lexer, where the next version of that request will be read — with a note that the want was real and || is the shape an answer should take.

docs/solveig-notes.md gained a third entry: the machine counts instructions and will not say how many. --steps=N stops a run, so the count exists; a run that finishes reports nothing, and the exact figure costs 28 executions of a binary search. Two of the three suggested fixes are one fprintf.

The number, again

Twelve defects now, and still two found by tests. Four have come from writing programs in the language. The third program cost an afternoon and moved one roadmap item from a claim to a measurement, which is what the first two did and is the reason there will be a fourth.

2026-08-31 — the whole of it: nineteen commits, eight versions, and two programs that disagreed with the roadmap

The day began with a question rather than a task: a meta-language, with compiler directives designing the syntax of the language above it. It ended with 0.8.0, two real programs, three shipped dialects, and a retraction.

What shipped

Parasol 0.1.0 through 0.8.0. lib/arith.psol, lib/control.psol, lib/clike.psol. programs/ember, a compiler from a small language to ARM64 assembly. programs/grammar, a grammar toolkit. Five design notes: targets.md, rules-and-logic.md, solveig-notes.md, and this document set. 5,187 lines of C11, 103 unit checks, five examples and four programs run by make test.

The first hour was spent not writing code

The opening question was answered with prior art — Racket, Terra, Forth, Rebol, Seed7 — and one decision was pressed on before anything else: where syntax is allowed to change. Three positions were laid out; the middle one, per-module declared grammar, was chosen, and it is the reason every later decision was cheap. A file’s syntax is settled by that file’s own header, so a tool can parse it by reading it top to bottom and never has to run anything.

The second decision was where Parasol lives. Solveig existed already, and solveig-sdl’s Makefile had written the rule down about itself — this is an extension, so it is not part of Solveig — so Parasol went beside it rather than inside it. That was argued from something stronger than tidiness: a front end with privileged access to the compiler it targets proves only that Solveig’s author can write a front end for Solveig.

solas/include/solas/compiler.h decided the rest. sol_compile(source, chunk) is single-pass with no tree, so Parasol owns one, and that settles what Parasol is: a second compiler that happens to target Solveig.

Three things went into 0.1.0 that nothing used

Spans on every node, introduced_by, and scope. Two of the three were read by nothing at all. They went in because each is impossible to add later without touching every constructor — and the return came fast: 0.2.0’s expander was written in one sitting rather than three, and 0.4.0’s a span carries its file was a field rather than a rewrite.

The collision question, and the answer that was already written

From 0.2.0 onward everything queued behind one question: what happens when two dialects declare the same operator. Racket answers it with modules and scoped bindings and it was worth reading how — but Solveig’s REFERENCE.md had already answered it for two files claiming one global. The later wins, and the compiler warns rather than letting it pass. And it warns on a claim and not on an update, which is a distinction about who could have known.

Applied to syntax that gives four cases, and 0.4.0 shipped them. A language should not hold two philosophies about one question, and the hour spent reading Solveig’s reference was the cheapest hour of the day.

The roadmap had been lying for four versions

Asked what’s next?, the file was read rather than remembered, and it still called the expander next — four versions after it shipped — and still asked the collision question two versions after it was answered. Three edits between 0.2.0 and 0.5.0 had matched nothing and reported success; em dashes against two hyphens.

Fixed in 5bf83af, which says so in its own subject line. The method changed with it: every replacement asserts its match now, and it caught two more the same day.

Then the programs, which is where the day turned

Six versions in, nothing had been written in the language. lib/text.sol over in Solveig states the rule — one customer, satisfied in six lines, is not a reason to grow a surface — and its converse is what the project had been ignoring.

programs/ember was written with five predictions recorded first. Three right, one did not bite, one wrong: Solveig was predicted to bite first, most likely 3.1, and did not bite at all. Two things nobody predicted, and one of them — six hand-written (a == b):and({ … }) because && could not be declared — became 0.7.0 the same afternoon. That is the whole argument for programs/ in one example: the feature was found by a program, not by thinking about features.

programs/grammar was written second, and deliberately: it is the program most likely to want repetition, which the roadmap had wanted for four versions with nobody asking. It wants repetition, and a repeated pattern part would not have helped — a grammar cannot be written as forms at all, so the repetition wanted is one level down. The item was declined a second time, with a reason instead of a shrug. 3.1 bit this time, exactly as predicted, and cost four lines because it was predicted.

And the day ended by taking something back

Both programs had reported that a form’s trailing hole swallows what follows. Asked what’s next? again, the plan was to build the fix — and checking the claim first showed there was nothing to fix. Both had declared an application as a pattern. A call ends at its closing parenthesis; ember’s own README states the rule, and grammar was written afterwards by the author of that sentence and broke it in five forms.

The retraction is d90ea08. What is real is smaller and better: choosing the shape wrongly is silent, both readings being legal. The rule moved from a program’s note into GRAMMAR.md, where somebody choosing a shape would look — which is the most likely reason it was not followed.

What was decided and not built

@language still records a name and acts on nothing, eight versions in. It is the only inert directive and it should either select something — a reader, or an emitter, as targets.md argues — or stop existing. That is a decision, not a build.

A guard on a rule was designed and not built: solum/embed.h is the door, and the rule to fix before writing any of it is that a guard validates, it does not select — otherwise parsing depends on evaluation and no tool can read a .psol without running it. rules-and-logic.md carries the whole argument.

The number worth keeping

Eleven defects, and two were found by tests — one of those two by a test that was itself wrong. Three came from writing programs, three from reading something rather than running it, and one from re-checking a claim before acting on it. The tally is in POSTMORTEM.md, and it is the reason the next step is more likely to be a third program than a ninth feature.