Solveig

Parasol’s completed roadmap items

Frozen on 2026-09-15 with its nineteen entries and the numbers they had. A Parasol entry that closes after this date closes into Solveig’s COMPLETED.md under section 7, where its roadmap entries went. Nothing is appended below this note.

The case for each piece of work as it was argued before the work was done — the problem, what the options were, and why the shape chosen was the one taken. This is where a roadmap entry goes when it is finished, rather than being deleted.

What actually landed, and when, is in CHANGELOG.md, which names the commit for each. What is still outstanding is in ROADMAP.md. What went wrong on the way is in POSTMORTEM.md.

The numbers are the original ones and are never reused.


1. A tree of Parasol’s own — done, 0.1.0

The problem. Expansion and hygiene both want a tree, and Solas has none: sol_compile(source, chunk) runs the parser straight into the emitter, one pass, nothing in between.

The options. Borrow Solas’s AST — there is none. Add one to Solas — changes Solveig to suit Parasol, and single-pass is the right shape for a compiler with fixed syntax. Own one.

Why this shape. Owning a tree is what makes Parasol a compiler rather than a preprocessor, and it settles what Parasol is: not a bolt-on to Solas, but a second compiler that happens to target Solveig.

2. Spans, and the map — done, 0.1.0

The problem. A language whose syntax is declared per module has one characteristic way of failing: somebody writes one thing, is shown an error about another, and cannot get from the second back to the first. Every macro system that became unusable became unusable that way.

Why it was first and not later. A tree without spans is a tree that has to be rebuilt to get them. The map cost a column and a file; retrofitting it would have cost the tree.

What it bought later. When @use made a module several files, making a span carry its file was a field rather than a rewrite (7).

3. Operators before anything else — done, 0.1.0

The problem. Which extension point to build first.

Why operators. A precedence table composes: adding an operator cannot change what an expression that does not use it already meant. A general grammar rule can, silently. Operators are the composable, decidable fragment of the thing, which is also why the case that motivates left recursion — a + b * c — never needs left recursion here.

4. The expander — done, 0.2.0

The problem. A form is not a method: its arguments arrive unevaluated, so the template may put them somewhere the caller never wrote. That is the whole of what a form buys and a method cannot do it.

Why hygiene came in the same commit. A system that expands without hygiene grows programs that depend on the capture, and those programs are what make hygiene impossible to add. Not a schedule — a one-way door.

Why the call shape. name(args) is a shape the core grammar already had, so declaring a form added a meaning without adding a production. A production can change what a program without it already meant, and that could wait until the collision rule existed (7).

Termination, without a counter. A template is read under the header as it stood at its own line, so form N can mention only forms below N and the highest index strictly falls. A property of the header reading top to bottom.

5. Referential transparency — done, 0.3.0

The problem. Renaming a template’s binders stops a form capturing what its caller passed. The other direction was open: a template’s free total landing inside a caller whose temporary is also total.

Why it is not a resolver. Solveig’s own rule made it one pass — only parameters and | … | temporaries are locals, everything else is a flat global — so the frames are the blocks and a name that is neither needs no protecting. Reading REFERENCE.md before designing is most of why this fit in one file.

Why the caller’s local gives way. The template cannot be renamed, reaching the global being the whole of what it meant, and a local is a thing no other frame can see.

6. Diagnostics that name the form — done, 0.2.0, extended 0.6.0

The problem. The characteristic failure of a macro system is an error about code nobody wrote.

The shape. introduced_by on every node an expansion produced, and a walk of the chain. Extended in 0.6.0 so a hole can say what it accepts, which moves swap #1 and b from this cannot be assigned to after expansion to a message at the line somebody wrote.

7. Two dialects meeting — done, 0.4.0

The problem. The one everything else queued behind. A dialect was a file’s header and there was no way to import one, so nothing could collide; that stops being an answer the moment a library can publish a dialect.

The options. Racket answers it with modules and scoped bindings, and it was worth reading how.

Why Solveig’s answer. Solveig had already decided it for two files claiming one global — the later wins, and the compiler says so rather than letting it pass — and a language should not hold two philosophies about one question. The four cases differ in who could have known, which is the same distinction Solveig draws when it warns on a claim and not on an update.

What it did not cost. Hygiene. 0.3.0 predicted one scope number would stop being enough once a template could be declared outside the module using it. It does not, because globals are one flat namespace, so a template’s free total and a caller’s global total are the same variable by construction.

8. A form that reads as a statement — done, 0.5.0

The problem. unless(test, body) says what it means; unless test then body says it the way the language it is imitating does.

Why it waited. It is the first thing here that adds a production, and a production is what can change what a program without it already meant. It waited for 7 to say what happens when two files add one.

Why no backtracking. A hole is parsed once and shared by every candidate still standing, so two forms can only part company at a word — and the declaration refuses any pair that would have parted company anywhere else.

9. A hole that says what it wants — done, 0.6.0

The problem. Every hole took an expression and could not say otherwise, so a bad use expanded into Solveig that failed somewhere further down.

Why these five and not a guard. expression, name, literal, block, place are all decided by looking at what was parsed. None needs an evaluator, and Parasol has none on purpose — see rules-and-logic.md.

Why checked after expansion. A hole filled by another form is then checked against what that form became.

10. An operator that stands for a template — done, 0.7.0

The problem. @infix named a message and could not template; @syntax templates but must begin with a word, which an infix operator does not. Short-circuiting fell exactly between them, and six versions had not noticed because nothing had tried.

What tried. programs/ember, which wrote (a == b):and({ … }) by hand six times.

Why it is a form. An operator with a template is registered as one, so hygiene, provenance and the trail arrive from the expander rather than from a second implementation of each.

11. Two holes in a row when the second is a block — done, 0.8.0

The problem. if (c) { … } needs two adjacent holes.

Why the ban was wrong as stated. It said no boundary between them. A block is a primary, consumed only where an operand may start, so an expression always stops at the {. The rule is really about greed — given <a> <b> and f x + y the first hole takes the sum — and a delimited hole has no such problem.

Why it could not have been done sooner. A hole could not say it was delimited before 9 gave holes kinds.

12. || without a declarable lexer — done, 0.9.0

The problem. | is the one spelling a dialect cannot have, so lib/clike.psol declared C’s or as \/ and wrote a paragraph apologising for it beside a && that needed no apology.

The option that was proposed, and refused. A @token directive naming a token and binding a spelling to it — @token TOK_PIPE2 "||". — so that @infix could name the token rather than the characters. Refused for two reasons. It does not reach the problem: what stops | is not that @infix cannot spell it but that { a | b } would have two readings, and naming the ambiguity does not decide it. And it costs the property the whole design sits on from a new direction — not the Forth one, since a @token header is still read rather than run, but the practical one: today any tool can tokenise any .psol without knowing what a dialect is, and a declarable token stream makes every future formatter, highlighter and grep implement the directive before it can lex.

And the same proposal in quotes, refused for the same reason. The follow-up was @infix "|" 25 or. — quote the spelling in the declaration so that it cannot collide with the | in the code. It does not, and that is the point: the declaration was never ambiguous. @infix \/ 25 or. needs no quotes and compiles today; so does @infix || 25. The two readings collide at the use, where nobody writes quotes. After any spelling of that declaration the code below still says { a | b }, bare, and that text is then both a block with parameter a and body b and a block whose body is a | b — two complete legal parses of one line, with the declaration nowhere on it. Quoting a declaration cannot reach a line the declaration is not on. If the quotes are instead meant to make new spellings lexable, they are @token wearing different clothes: the lexer would have to read the header before it could lex the body.

Retracted on 2026-09-02: one sentence above claims more than it can. “Naming the ambiguity does not decide it” is true of naming and was written as though it were true of everything. A rule decides it, and Parasol had the mechanism before anybody looked: { a | b } is a parameter and a body by rule, in every module, and a declared | is an operator in every position a block is not reading a bar of its own. Escaped one bracket down, { (a) | b }, exactly as #[(b = c) = d] escapes the dictionary rule of the same shape landed in 0.12.0 — four hours before this was disproved, and by the same argument.

Demonstrated by a working change: two hunks in reader.c, no change to the lexer at all, the whole suite green and every block form intact. It was not kept — see ROADMAP.md, which records what it would cost — but the claim it disproves does not depend on keeping it.

What still stands, and it is most of the entry. @token and the quoted spelling genuinely do not reach the problem, for the reason given: the declaration was never ambiguous, and a quote cannot reach a line the declaration is not on. And | is still not an operator character and cannot become one — what changed is that it need not be one to be declarable, being a token in its own right that the parser may look up. The property the entry defends — any tool can tokenise any .psol without knowing what a dialect is — is untouched, which is why the demonstration never went near lex.c.

And the cause was misattributed all along, in both directions. This entry blamed the ambiguity and stopped; a later reading blamed Solveig and was worse. Neither is right. What stands in the way of | is Parasol’s own block syntax, which Solveig shares and settles the same way. Nothing about | was ever Solveig’s constraint to remove.

The three things being run together are worth separating, because the confusion is natural and the names do not help: the character in the source, the token the lexer makes of it, and the declaration that gives a token’s text a meaning. Quoting changes only how the third is written. The collision is between the first two, and the lexer resolves it without consulting any declaration at all — which is the property that lets a tool tokenise a file it knows nothing about.

The shape taken instead. || in the fixed lexer, taken before the bar, belonging to every dialect and declared by none. The split the proposal wanted already existed — PARASOL_TOK_OPERATOR is spelling only and @infix is meaning — so the question was never how does a file name a token but which spellings are in the vocabulary.

Why it was safe, checked rather than assumed. A block looks for a lone bar in each of the three places it allows one, so { a | b } and { x | | t | t } never see the new token; { a || b } was already an error, so nothing legal was taken. The one casualty, found by compiling it rather than by reasoning about it: { || … } parsed as an empty list of temporaries and emitted nothing. It is { | | … } now, it appeared nowhere, and it is the only thing this cost.

13. The name — done, no version, 2026-09-01

Not a roadmap item; it was never on that page. It is here because the case was argued with options before the work, which is what this file keeps.

The problem. “Phoenix” named two things. This project used it, and Solveig’s docs/ideas.md had reserved it three days earlier — a second language whose output Solum uses, closing the name, should it happen, is Phoenix — for a language that would publish a library Solum consumes. That entry explicitly refuses a nicer skin on this one, so the two are not the same idea and cannot share a name. The shipped compiler held the word by use; the unbuilt idea held it by reservation, in writing, in another repository.

The intermediate step, and why it was wrong. The first move was to Phoenix Proto, marking this tree as a prototype restart, with the language still called Phoenix. That distinction did not survive an hour — see POSTMORTEM.md 14. The prototype framing is the honest description of the thing, not a modifier on a name that was leaving anyway.

The options, on the one choice that was not mechanical. The identifier scheme is a strict three-case prefix — Phx types, phx_ functions, PHX_ macros — so respelling it is arithmetic. The extension is not:

   
Proto/proto_/PROTO_, files .proto Consistent to a fault. .proto belongs to Protocol Buffers.
Pro/pro_/PRO_, files .pro Keeps the three-letter abbreviation phx_ had. Pro reads as marketing.
Proto/proto_/PROTO_, files .pro Taken. Full word where a person reads it, short extension where a tool does.

Why .pro and not .proto. Linguist and most editors key on the extension, so every module in this tree would have been highlighted as protobuf — under a README whose opening argument is that a tool can tell what language a file is in. Losing that on the extension, of all things, would have been the wrong place to be consistent. .pro is also four characters, which is why default_output_path — it compares length - 4 against the suffix — came through untouched. That was luck, and is recorded as luck.

What it cost, which is one thing. lex.h is dense enough in identifiers that respelling them all dropped it under git’s 50% rename threshold, so it records as a delete and a create where the other thirty-five moves record as renames. git log --follow --find-renames=30% still walks it back to the first commit; the default no longer does, for one file in thirty-six.

What it did not cost. Nothing in the language changed. The suite reported the same 58, 6, 34 and 10 checks over the same ten examples and programs, and the diff was 1,435 lines out against 1,435 back in — the only shape a pure respelling can have.

14. @language, removed — done, 0.10.0

The problem. It was the only directive that did nothing. It parsed, recorded a name and a span in ParasolDialect, and nothing in the tree ever read either — not emit.c, not unit.c, not main.c. No test named it; tests/test_use.c used @language solveig. ten times purely as header filler. It enforced exactly one rule, that a module declares at most one, which by way of @use also made it an error in a dialect file — the first thing programs/digest got wrong.

Three properties that should not coexist: it was optional (module = { directive } { statement } makes every directive optional, and the name is NULL if unstated), it was ignored, and it was universally written. That is the definition of a ritual.

The options. The roadmap had said it should select the reader, or the emitter, or stop existing.

Select the emitter is blocked by targets.md, which had already refused to build a second one. Select the reader is larger still. So the item had not sat for nine versions because it was hard to decide — it had sat because two of its three options depended on a build this project has declined, and the entry could not move.

A fourth option was proposed and rejected: make it assert — one reader, one emitter, so any name but solveig is an error at line 1. Fifteen lines, touches no .psol, and it is the code a selector would need later. The argument for it was reversibility: asserting is cheap and deleting is not.

Why this shape. Because the reversibility argument prices the change and does not ask whether the thing is right, and the thing is not right.

Read @language <name>. at the top of a file and it plainly says the body below is written in <name>. That reading is false in every file with a header. examples/forms.psol said @language solveig. and then declared +, <, >, unless, while and swap; its body is not Solveig, and Solveig cannot read it. The reading under which the line was true — the substrate is Solveig — is the same for every .psol there will ever be and is already carried by the extension. A directive whose most natural reading is false is not fixed by checking its spelling.

And the thing that could one day differ between two files is the output, for which @language is the wrong word. targets.md had already reached for the right one in its own sentence — “Parasol targets ARM64” is a line in a file — so if a second emitter is ever built, what gets added is @target, naming what it selects.

What it cost. Nine .psol files, ten fixtures in tests/test_use.c, the HEADER and LANG prefixes in two more, three spots in the README, one production in GRAMMAR.md, the parse branch in reader.c, and name and declared_at in ParasolDialect.

What it did not cost. The suite reports the same 58, 6, 34 and 10 checks over the same ten examples and programs. The map test needed only its /* line N */ comments renumbered: a directive emits nothing, so removing a header line moves no generated line.

Two things were deliberately not done. No tombstone branch in reader.c saying @language was removed in 0.10.0 — the generic error already names exactly what the header takes, and carrying the name of a deleted directive for no customer is the same mistake in miniature. And the test that checked language declared twice was replaced rather than deleted, by one for the branch that now catches it: an unknown directive, which nothing had tested.

What replaced its one real rule. That a @used file is read into the header of the module using it — which programs/digest noted the old diagnostic enforced sideways without ever stating — is stated in the README under A dialect is a file, where somebody would look for it.

15. Solveig’s spellings, closed as far as they close — done, 0.11.0 to 0.13.0

The problem. GRAMMAR.md said everything but operator was Solveig’s own spelling, so that a file can be read by somebody who knows Solveig without a second set of habits, and README.md went further: a module declaring nothing reads exactly as Solveig does today. Comparing every form in Solveig’s grammar against Parasol, one file each, gives nine divergences out of eighteen. Both sentences were false, and a person who knows Solveig met the first of them at #-5.

How it was found is half the entry. programs/ledger found one of the nine — #-1225, at the second line of its data, a ledger being the first program here with an ordinary negative value. It was written up as a missing integer literal and would have stayed that size. The other eight came from being asked whether that survey had been complete. It had not been, and nothing except the question would have said so.

The options. Close what closes and restate the claim, or restate the claim and close nothing. The second was never serious once the list existed: four of the nine cost nothing at all, and one of them was a defect rather than a gap.

Why this shape. The nine sorted into five kinds, and sorting them was most of the work:

   
Free — #-45, $FF08, 1e10 Nothing in Parasol objected. Nothing else may begin with #, $ was not a token at all, and a float simply stopped at its fraction.
A defect, not a gap — "\q" The only difference pointing the other way. Parasol took any character after a backslash; Solveig has five escapes. So a .psol compiled clean and emitted a .sol solas refused — Parasol emitting invalid Solveig, which is the single failure the map and the run-every-example discipline exist to prevent, and which neither caught because no example has a bad escape.
A parse rule — #[a = b] Called free on the strength of the lexer and was not. Solveig writes pair = sum "=" expression and settles the ambiguity by level; Parasol cannot copy that, a dialect being free to declare = anywhere and lib/clike.psol putting it at 10.
A lexical split with a bill — %1011 % is an operator character here and is not one in Solveig, which has no % at all.
Already decided, or impossible — ( \| t \| … ), @expr, -3 A rough edge, a refusal, and a thing that cannot be had.

The two that were decisions, and what each conceded.

#[a = b] took the rule that a top-level = inside a dictionary is the separator, whatever the header said — the first place in this language where a context outranks a declaration. What keeps it a rule rather than = being taken away is that it stops at the first bracket: #[(b = c) = d] still uses the declared one. It is a parser rule, so nothing about tokenising a .psol without knowing its dialect changed.

%1011 took the rule that a % immediately before 0 or 1 begins a number, and everything else is the operator. No declaration is consulted, so again the tokenising line held. But this is the first spelling here that took something from what a dialect may declare: a module declaring % can no longer write a %1 without a space. Entry 12 above held || up as the shape any future request should take — grow the fixed vocabulary rather than making the vocabulary declarable — and || cost only { || … } out of the core. This is the second instance of that shape and the first to bill a dialect. Small, unused in this repository, and loud when it bites. The next one might be none of those, and 12 should be read with this attached.

And one that cannot be closed, which is the finding under all of it. -3 is a literal in Solveig, whose scanner gives the sign to the number outside a @expr region and treats it as the operator inside one. Parasol can have neither half: no regions, and taking -3 as a literal would stop a -3 being a subtraction in every dialect that declares -. So the claim was never achievable, and 0.1.0 chose against it without recording that it had. That is the extensible-operator line arriving from a direction nothing had come from — not a dialect wanting to change the lexer, but Solveig’s own number syntax being uncopyable while operators stay declarable.

What it cost. a %0… and a %1… without a space, in any module declaring %. Nothing in the tree writes that — % as mod is n % #2 throughout, because mod wants an integer and a bare digit is a float.

What it did not cost. Any part of a tool can tokenise a .psol without knowing what a dialect is. Both new rules consult the source and no declaration, and the dictionary one lives in the parser. The suite went from 34 reader checks to 60, and every example and program builds byte-identical output where the spelling did not change.

16. A declared |, and the spelling convergence it forced — done, 0.14.0

The problem. README.md, parasol/src/lex.c and entry 12 above all said a dialect cannot declare |, because { a | b } would then have two readings. Entry 12 put it as naming the ambiguity does not decide it. That was wrong, and POSTMORTEM.md 18 is how it was found: a second session built the thing and it worked.

Two questions had been run together. May | join the operator characters? — no, and that stands, because a character in that set runs together with its neighbours and a | there would make |= a spelling. May | be declared? — a different question entirely, since a bar is already a token of its own and a parser may look one up without it ever entering the set.

The options. Leave it, on the strength of the entry above; or take the rule. There was no third, once the demonstration existed.

Why this shape. A block settles its parameters and its temporaries with a bounded lookahead — looks_like_names_then_bar — that consults no dialect and never has. So the collision is exactly one production wide:

{ a | b }        a parameter and a body, in every module, declared bar or not
a | b            an or, everywhere a bar is not a block's own punctuation
{ (a) | b }      the escape: a body opening with a group is a body

That is the second place in Parasol where a context outranks a declaration, and the first — #[k = v] in 0.12.0 — is the same shape and the same escape. The mechanism was in the language four hours before the claim was disproved, which is the part of this worth remembering.

Two things the demonstration had wrong, fixed before landing. @prefix | was accepted and inert; it is refused now, because a block’s temporaries open with a bar and a prefix one would have nothing to tell them apart — and a declaration accepted and doing nothing is what @language was deleted for in 0.10.0. And a stray undeclared bar gave expected ‘.’ after this statement; it gives ’|’ has no meaning in this module now, with the note every other undeclared operator gets.

What it cost, and it is the reason this waited a version. 0.13.0 had just settled the repository on one spelling per operation, with \ for a bitwise or because | could not be had — the mnemonic written into four files. So landing the bar meant redoing that convergence, and the two went in together rather than a version apart. A spelling should be changed once, and this one was changed twice; the second time is this entry.

Nine lines of code across three files, and the prose in five more. The table is C’s exactly now:

  logical bitwise
and && &
or \|\| \|
not ! ~
xor none — and != is xor for booleans ^

What it did not cost. The lexer, which 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, which is the property entry 12 was written to defend and which it defends correctly. And |= stays undeclarable, nothing being able to run together with a bar. That is the price of not touching the lexer and it is the right price.

\ is free and unused now. Nine checks in tests/test_reader.c hold both halves of the rule and both refusals.

17. A diagnostic that prescribes — done, 0.16.0

The problem. A hole-kind failure named what it wanted and stopped. 'if' wants a block here, and this is a send is true, correctly positioned since 0.15.0, and says nothing about what to type instead. Both second-reader runs named it as the next thing to fix, and neither reached it — run 1’s task had no cascade, and run 2’s reader was warned off by the eleven lines lib/clike.psol spends at its own else declaration.

So the evidence for it never arrived, and this was done anyway. That is worth stating plainly rather than dressing up as a measurement. The argument is the generalisation of what run 2 actually found:

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

Run 3 narrowed that sentence — it holds for a refusal and not for a silence (POSTMORTEM.md 26) — and the narrowing does not touch this case. A braceless body is a refusal: the compiler stops, and the only question is whether what it says is any use.

That is now unmeasured after four runs, and the question has been retired. Runs 3 and 4 were both aimed at it — run 3 by a route the dialect’s essay does not cover, run 4 by making the reader an author rather than a user — and both were stopped by an example showing a one-statement body wearing braces. Two of four tasks were built to force a diagnostic and no reader has seen one. Prediction 21 was retired rather than asked a fifth time, so this entry’s argument is what justifies 0.16.0 permanently, not provisionally. That is a settled fact about the change and not a pending measurement.

Once per dialect. lib/clike.psol’s author paid it, and every future dialect with a block hole has an author who has not. The compiler is where that sentence can be said once for all of them, and a diagnostic is the only part of this system that reaches a reader who has read nothing.

The options.

   
Prescribe for every kind Uniform, and three of the five have no fix to name. Something assignable is not advice; it is the error restated. A note that cannot be acted on teaches a reader to skip the notes, and then the one that could be acted on is skipped too.
Prescribe for block only Braces make a block out of anything, so the advice is correct every time it appears. Not uniform, and the asymmetry is the honest shape: one kind has a universal fix and the others do not.
Say it in lib/clike.psol and leave the compiler alone Already done, and it worked — run 2’s reader never saw the error. It works for this dialect, written by somebody who had already paid. It is the thing being generalised, not an alternative to it.

The second was taken. The restraint is the design rather than a shortfall, and a check holds it: expect_rejected_without asserts that a place failure does not mention braces.

And the placement is part of it. The note goes between the error and the declaration note, not after both — the fix belongs beside the problem, and diag.c drops a repeated caret only for a note about the span the line above underlined. Putting it third printed the reader’s line three times; putting it second prints it once.

What it does not do. It does not make else if work, and it is not a step towards alternation in hole kinds — that entry stays where four customers left it. It makes the wall say how to climb it.

18. The name, a second time — done, no version, 2026-09-14

Not a roadmap item, as 13 was not. It is here because the names were put with reasons before one was taken, which is what this file keeps.

The problem. Hans was not sold on Proto, and the unease had a shape once looked for: .proto and protoc belong to Protocol Buffers, .pro to qmake and to Prolog, and in a prototype-based language the word points at the object model, which is the one thing this compiler is not about. None of it was fatal on 2026-09-01, when the name had to leave Phoenix in a morning and .pro was chosen over .proto for exactly the first collision. Thirteen days and seven programs later there was time to choose rather than to move.

The options, in the order they were put. Hans’s first was pretex, and the answer was no on three counts: it reads as pretext, an excuse; PreTeXt is an existing authoring language; and it says preprocessor, which is the design this compiler was built to avoid, there being no rewrite-and-rescan over text. Then three rounds:

     
Solfa tonic sol-fa is a notation invented so people could read music without the staff, and Sol is in it as the note not exciting
Rubric, Målform, Idiom, Fortale a red heading that says how to read what follows; Norwegian for a written standard of the language; a dialect’s turn of phrase; Norwegian for preface the grading meaning, the ring and malformed, generic, a pun in English
Kenning Old Norse for a made-up compact expression the poem defines by use, which is what a form is; ken, to know not sold
Argot, Tiro, Skald, Dialect a jargon a group makes for itself; Cicero’s scribe and the first shorthand; the poet who makes the kennings; what the documents already call a .psol file French; novice; names the maker; generic
Parasol Hans’s second suggestion. Plain English; sol in the tail as Solas and Solis have it in the head; para- is beside, and paraphrase is what the output is; the literal object is a layer held between the reader and the sun, which is what a dialect is the layer also shades what is under it, which the documents already say of a dialect
Presol, Solo before Solveig; a grammar holds for that file and no other coined; says standalone, and taken by small tools

Parasol, taken. The one word that says what the thing is and joins the family in the same breath, and free as a command name.

The extension, which was half the objection. .pro could have stayed, and would have kept the qmake and Prolog collision. .parasol is unambiguous and long. .psol reads as para-sol at a glance, is five characters, and collides with nothing; taken. It is one character longer than .pro, which is the whole of the code change: 13’s luck that .phx and .pro were the same length is spent, and default_output_path counts five.

What it cost. Nothing in the language. 2496 lines out and 2496 back in, the same shape 13 reported; the suite here and Solveig’s green before and after; the editor’s own test green over 24 .psol files. The records of 2026-09-01 keep the old name, on purpose, and the changelog entry says why.

19. --sob: solas run, not linked — done, no version, 2026-09-14

Not a roadmap item. Hans asked whether Parasol could write a .sob directly, with solas’s options, so that it can stand as a compiler on its own rather than the front half of one; the case was put and the shape chosen the same morning, and this is the case.

The problem. From a .psol to something solvm runs is two commands and two tools, and the second command is the same every time: solas on the file the first one wrote, with the same -I list. A compiler whose output needs another compiler run by hand is a compiler with a manual step in it. Hans’s framing was that Parasol should be able to act as an advanced solas, taking -o, -I and --dump, with --expr off by construction since a .psol has no use for it, and outputting .sol or .sob as asked. Not a replacement for solas; a different tool that ends where solas ends.

The options. Two, and both are small.

     
Link libsol.a and call sol_compile_options(text, path, &search, NULL, &chunk) on the emitted string, then sol_chunk_save forty lines in main.c, no fork, the chunk in hand for --dump the Makefile gains ../build/libsol.a and two solum include paths; the Makefile’s first sentence, main.c’s header, CLAUDE.md and the root Makefile’s note on how parasol/ reaches the tree all become false; the .sob format is frozen at build time; solas’s diagnostics go to stderr from inside the process, so re-saying them in .psol lines would mean changing Solas
Run solas, the one beside this binary or else PATH’s, with -o, each -I and --dump handed through the coupling stays a file format and a command line, which is the sentence the experiment stands on; the .sol still lands, which @include from a second module and the map both need; the version handshake can be a run-time question; solas speaks on a pipe Parasol holds, so its lines can one day be mapped back without Solas hearing of it a fork; a second binary to find; a .sob the driver did not make and cannot inspect

Why this shape. The second, on Hans’s decision after the two were put side by side, and for the reason the third column of the first row gives: it is not that linking is hard, it is that it would have to rewrite four statements of what this project is in order to save a fork. He also said where this is going, that Parasol should in time be a member of the toolkit with its sources laid out as the others’ are (ROADMAP.md, A member of the toolkit), and that the driver should work either way. It does: a --sob that runs solas is the same --sob whether parasol/ is beside solas/ or inside its own directory, and whether the two share a Makefile or not.

What was decided on the way. One -I list for both compilers, because in every program so far the dialect and the generated library sit in one directory (programs/bignum is the case, calc reaching bignum.sol by @include from where bignum.psol is); a second flag is a customer away. With --sob, -o names the .sob and the .sol goes beside it, not beside the source: the pair is the output, and the map points into the .sol, so the three stay together. --dump alone is a usage error rather than a silent nothing, since it is solas’s flag and there is no solas without --sob. --tree wins over everything, as before. Which solas: the one in argv[0]’s directory when there is one there, which every bin/ this repository makes; otherwise execvp’s. PARASOL_PATH is not handed on, being about @use; SOLUM_PATH is solas’s to read.

What checks it. tests/test_sob.c, the first file here that runs the binary rather than linking the library, because everything --sob does is at a process boundary: 27 checks, of which the one that matters is that the .sob the driver writes is byte for byte the one solas writes on the same .sol. Run against the driver-less compiler from a clean build, as conventions.md asks, 21 of the 27 failed and the six that passed were the ones that say a file is absent. The Makefile’s own rules still do the two steps themselves, so the suite goes on checking the pipeline a Makefile writes and not only the one the driver drives.

What it did not do. Bump the version. The roadmap’s first open entry says the next Parasol release is what forces the one-version-or-two question, and a 0.18.0 from here would have answered it by the back door; the entry is dated instead. And the diagnostics are not mapped back yet: the pipe is held, and nothing reads it. That is a customer away too, the customer being somebody who hits a Solas error in generated code and reaches for the map by hand more than once.

Settled by a customer rather than by argument

   
Optional parts Declined three times. programs/ember wanted none, if/else as two declarations being fine; programs/basic met one in BASIC’s STEP clause and it was if wordIs("STEP") in an ordinary parser, one level down.
Repeated parts Declined three times, the second and third with the same reason. programs/grammar 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 in the object language. programs/basic met it again in BASIC’s PRINT a, b, c and it was a whileTrue in a parser, for the same reason.
A trailing hole binding at unary precedence Retracted. It described a fix for a problem that was two programs choosing the wrong shape. See POSTMORTEM.md.
<=, >= and != in lib/arith.psol Taken, on 2026-09-02, and the first entry a customer settled for rather than against. programs/prose wanted <= and wrote around it; programs/basic wanted <= and >= five times and != four. Both were rewritten to use them the day they landed, which is the check that the customer was real.