Solveig

What Parasol found in Solveig

A running log while Parasol was a repository of its own, kept there rather than raised here, so that each entry was written where it was found and could be taken into Solveig’s numbering when somebody decided it was worth taking. Since 2026-09-15 it is a page of Solveig’s documents and the log is closed: the four findings still open became 3.28 to 3.31 on the roadmap, each pointing back here for the account, and a finding after this date is Solveig’s own and goes to the roadmap directly. They were 3.23 to 3.26 for a day, until the count was found to have reused a number; the roadmap says so. 3.28 and 3.30 closed on 2026-09-16.

This is what a second customer is for. solveig-sdl’s README says the same thing about sol_symbol_intern — reachable and not promised, promised now because a second binding wanted it. Parasol is a customer of a different part: not the extension ABI but the compiler and the machine as programs, driven from a command line by something that generated their input.

Entries 1 to 4 are numbered on the roadmap, and 2 and 4 are open; 5 is an inventory and was never a request. Nothing here is a blocker.


1. Program output and a runtime error come out in the wrong order

Roadmap 3.28, done on 2026-09-16, as the one line below.

Kind: defect, small, one line.

What happens. A program that prints and then fails shows its error before its output, whenever standard output is not a terminal.

$ printf 'x := #1.\nx:print.\nx:notAMessage.\n' > rt.sol
$ solas rt.sol -o rt.sob
$ solvm rt.sob 2>&1 | cat
solvm: integer does not understand 'notAMessage'
  [rt.sol:3] in script
#1

x:print runs on line 2 and the failure is on line 3, so #1 should come first — and it does, interactively:

$ script -q /dev/null solvm rt.sob
#1
solvm: integer does not understand 'notAMessage'
  [rt.sol:3] in script

Cause. Standard output is block-buffered when it is not a terminal and standard error is not buffered at all, so the error overtakes everything the program printed and has not flushed. Interactively stdout is line-buffered and the ordering happens to be right, which is why this is invisible until something captures both.

Suggested fix. fflush(stdout) before a runtime error is written to stderr. Probably also before the machine stops for --steps or --memory, which fail the same way.

Why it matters here. make test in Parasol runs every example through solvm and captures both streams. A failing example’s last output is the first thing you want and the last thing shown, and the misordering reads as the print never happened rather than as a buffering artefact.


2. A generated file cannot say where it came from

Roadmap 3.29.

Kind: missing capability. Not urgent; the difference between a manual step and none.

What happens. Both tools report positions in the file they were handed, and there is no way to tell either one that the file is generated.

[syn.sol:2:1] solas: expected '.' between statements at 'y'
solvm: integer does not understand 'notAMessage'
  [rt.sol:3] in script

Parasol compiles vectors.psol to vectors.sol and writes vectors.sol.map beside it, so the information exists — but it exists on Parasol’s side, and solas, solvm and solid all name the generated file. Everybody who is not Parasol has to look the position up by hand.

What would close it, roughly in order of how little it asks of Solveig:

   
solas --source-name=<path> The path recorded in the chunk, rather than the one on the command line. One flag, one field. Fixes the file, not the line.
A #line-style directive in source @line 25 "vectors.psol". setting what the next statement records. Fixes both, costs a directive and a lexer case. Every generated-source language ends up with one.
The chunk carrying a map Right, and much larger, and not worth it before something asks.

Why it matters here. It is the one thing that would make Parasol’s map consumed rather than merely written. Parasol is deliberately built so that solas needs nothing from it, so this is a request rather than a dependency — and the first option alone would already turn a line in a file you did not write into a line in a file you did not write, from one you did.

Note. [rt.sol:3] at run time means the path and line are already in the .sob and already reported. The mechanism is there; what is missing is a way to set what goes into it.


3. The machine counts instructions and will not say how many

Roadmap 3.30, done on 2026-09-16, as the first row of the table below.

Kind: missing capability. Small, and the workaround works — it just costs one run per bit.

What happens. --steps=N stops a program after N instructions, so the machine is counting. Nothing reports the count. A program that finishes says nothing and exits 0; one that is stopped says the step limit of 100 was reached and exits 124, naming the limit rather than the position.

$ solvm --steps=100 m.sob
solvm: stopped: the step limit of 100 was reached
  [m.sol:2] in block
$ solvm --steps=200000 m.sob        # finishes, says nothing
$ echo $?
0

The workaround, which Solveig’s own documents describe. programs.md measures sha256sum this way: the smallest N that lets a run finish is that run’s exact count, and a binary search finds it. It is exact and it is correct. It is also 28 runs of the program to learn one number the machine had after the first one, and each run is a full execution — measuring a one-second program costs half a minute.

Suggested fix, smallest first:

   
--steps with no =N Run to completion and write the count to stderr. One flag spelling, one fprintf, no new machinery.
A count in the --steps=N stop message stopped at instruction N of a limit of N is the same number this already knows. Helps a stopped run, not a finished one.
system:steps The count from inside the program. Larger, and it changes what a program can observe about itself, which is a decision rather than a flag.

Why it matters here. programs/digest exists partly to measure what a Parasol template costs against a Solveig method, and the answer — 2.03 instructions per rotation against 2.00 — is a difference of 5% found by running two programs 56 times. Two runs would have done. The number is the whole point of that program, and it is the one thing the machine will not hand over.

Not a blocker, and not urgent. The binary search is in a nine-line shell script in Parasol’s history and can be lifted by anybody who wants it.


4. A run-time trace carries a line and no column

Roadmap 3.31.

Kind: missing functionality, small; matters more to a generated file than to a written one.

What happens. A compile error is reported with a column, [prog.sol:1:7] solas: ..., and a run-time frame is not:

solvm: integer does not understand 'asStrin'
  [bignum.sol:27] in block
  [calc.sol:16] in block
  [calc.sol:17] in script

Why it matters here. Parasol’s map is exact to the column, and a generated line is often a span: a while body or an if arm is emitted on one line, so bignum.sol:27 is bignum.psol lines 54 to 57 and the map cannot narrow it without a column to look up. Sixteen of that library’s 103 generated lines are spans. A four-line answer is a recovery; the exact one is what the map was built to give.

Expected. [bignum.sol:27:10] in block, the column of the send that failed, which the compiler had when it emitted the instruction. Observed. The line alone.

Suggested fix. Carry the column in the line table beside the line, and print it in the frame. The other half is Parasol’s, keeping a source line break inside an expanded hole, and is on its roadmap under Rough edges; either alone would do, and this one helps every generated file, not only Parasol’s.

Not a blocker. Found on 2026-09-13 by programs/bignum, which put a deliberate error in a copy of its library to test the map across two modules.

5. What a large-number library wants, and what Python’s math has that Solveig does not

Kind: an inventory, not a request. Written because Hans asked, with programs/bignum, whether a maths extension in the shape of extensions/net is wanted, and whether Python’s math module is the measure of advanced maths.

A bignum is a library. programs/bignum/bignum.sol is 126 lines of generated Solveig, correct against bc, and nothing in it needed a capability the machine lacks. What it wanted and could not have is a product wider than 64 bits, and no scripting language’s integers offer that either: CPython keeps 30-bit digits in C for the same reason this keeps nine decimal ones. The trap on overflow is what fixed the base, and it fixed it correctly. The case for a C extension is speed alone: 1000! takes 40 ms at -O2 against 0.24 ms for CPython’s int, 170×, and no program has waited for it. If one does, the shape is net’s, a global holding primitives, and the measurement comes first.

Python’s math is a different question, being float mathematics rather than large integers, and most of it sorts by 3.14’s own argument: a function a program cannot write correctly for itself is a primitive, and one it can is lib/math.sol. Against Solveig’s float today, which has floor, ceiling, rounded, truncated, sqrt, pow, exp, log, the six trigonometric messages, float:pi and float:atan2:

would be primitives, being C-library calls a program cannot reproduce log2, log10, log1p, expm1, exp2, cbrt, hypot, sinh, cosh, tanh, asinh, acosh, atanh, erf, erfc, gamma, lgamma, frexp, ldexp, nextafter, ulp
would be lib/math.sol, being arithmetic a program can write degrees, radians, fabs, copysign, fmod, remainder, modf, isclose, dist, fsum, prod, sumprod, and on integers gcd, lcm, isqrt, factorial, comb, perm
constants e and tau beside float:pi; inf and nan are already infinity and nan

fsum is the one in the second row that is not obvious: a correctly rounded sum is Shewchuk’s algorithm, which can be written in Solveig and is not short. Nothing in either row was wanted by any of the seven programs here, which is the standing rule’s answer for now: a surface does not grow without a customer, and the customer names the row.

A prediction about Solveig that was wrong

programs/ember is a lexer, a recursive-descent parser and an ARM64 code generator, written in Parasol and run on SolVM. Its README predicted that Solveig would bite before Parasol did – most likely 3.1, a block outliving the frame it was written in, since lib/scan.sol had already hit it and said so.

It did not. Recursive blocks held in globals and called from inside other blocks, an AST of object:new with slots assigned after the fact, arrays of arrays, system:arguments, system:readFile, and fill doing every line of the assembler – none of it complained. lib/scan.sol did the cursor work and lib/text.sol was included and, as it turned out, not needed.

Recorded because a prediction that was written down and then failed is worth more than one that was never made, and because it is a data point for 3.1: a program of this shape does not reach it.

Not defects, recorded so they are not re-found

Solas is single-pass and has no tree. sol_compile(source, chunk) runs the parser straight into the emitter. This is why Parasol owns a tree instead of borrowing one, and it is the right shape for Solveig — noted so that the next person to look does not read it as an omission. See PARASOL.md, What Parasol is allowed to know about Solveig.

Integer arithmetic traps rather than wrapping, and that is right. It is what made programs/digest interesting rather than what made it hard: SHA-256 is defined on mod-2^32 arithmetic, Solveig’s own programs/sha256sum pays for the difference in twenty-three hand-written bitAnds, and Parasol’s version pays for it once in a header. A language that wrapped silently would have been the convenient choice and the wrong one. Recorded so that a hash program wanted wrapping is not read as a request for it.

shiftLeft refusing to lose the number never fired. The largest shift in a 32-bit rotation moves a value under 2^32 left by thirty places, which is 2^62-ish and fits. Solveig’s own program says the same about 3.12; a second program of the same shape confirms it rather than finding an edge.

display and print both end the line. There is no write this and stay on the line among them — system:write is that, and REFERENCE.md says so. A line with two things on it is built and then written once, which is what programs/digest does. Recorded because reaching for display twice and getting two lines looks like a bug for about a minute.

:not and :not() are the same send. Parasol emits the first. Confirmed against the grammar, which makes an argument list optional; recorded because it looks like a difference and is not.