Solveig

SolaBasic — a compiled BASIC for SolVM

The language definition. Written before the compiler, and frozen when it starts, because there is no standard for this dialect and somebody has to hold the line that a standard would have held.

SolaBasic is BASIC in the shape QuickBASIC gave it — labels rather than line numbers, SUB and FUNCTION, block IF and SELECT CASE — compiled to a .sob file and run by bin/solvm. It is not QBasic and does not try to be. It is a subset, and this document is the whole of it.

For writing SolaBasic rather than reading about it, see the reference manual, which describes what the compiler accepts today — statement by statement, with what it says when it refuses — or the cheatsheet, which is the same ground on one page for when you know what you want and not what it is called. This page is the definition and the reasoning; that one is the desk copy.

The name

Sola is Norwegian for the sun — the definite form of sol, which is how it comes to stand beside Solveig. In Latin sōla is the feminine of sōlus, the same root as the sōlum the README takes the whole design principle from. Basic is on the end because the dialect is BASIC, and because it belongs to this project rather than to Microsoft.

The obvious name was taken, and taken from close by. S-BASIC — Structured Basic, Topaz Programming, 1981, distributed with Kaypro’s CP/M machines — had optional line numbers, labels that were not numbers, and a two-pass compiler. It is CB80’s contemporary and competitor, which is to say it sits inside the same paragraph this language borrows its boundary from. SmallBASIC is in use as well. So the short name is spoken for twice over, and this is the longer one.

Why this document exists at all

basic.sol could point at ECMA-55 and let a published standard decide when it was finished. SolaBasic has no such standard, and the reason is worth writing down, because it is not for want of looking.

There is exactly one standardised structured BASIC: Full BASIC, ratified as ECMA-116 (1986), ANSI X3.113-1987 and ISO/IEC 10279:1991. It has SUB, FUNCTION, SELECT CASE, DO...LOOP, EXIT, and structured exception handling, and ECMA publishes it for nothing exactly as it publishes ECMA-55. Three things rule it out:

   
Line numbers are still mandatory Full BASIC adds structure without removing the line editor it was designed around. It fails the first requirement.
176 keywords, 161 concepts plus 38 mathematical and 14 string functions, plus five optional modules, one of them a graphics system. Against Minimal BASIC’s twenty statements, that is a different category of job rather than a larger one.
Nobody appears to have built a conforming implementation and there is no Full BASIC counterpart to the NBS test programs. A standard nobody met and nobody tests is not an external authority; it is a long document.

So the choice was a vendor dialect, and the trouble with a vendor dialect is that the subset boundary is drawn by whoever is writing the compiler, on the day they are writing it, which is the failure ECMA-55 protected the interpreter from.

The line is therefore borrowed rather than invented. It is CB80’s.

Where the line is drawn

CB80 — the CBASIC Compiler, Digital Research, 1982 — is the closest thing this project has to an ancestor. CBASIC compiled to an intermediate .INT file executed by a separate runtime called CRUN, which is this design in 1977. CB80 added alphanumeric labels to it, along with nested IF, variable type declarations, CALL with parameters, and multiple-line functions with local variables.

That list is a boundary drawn by people building a compiled, structured, label-based BASIC with no machine underneath it — no screen, no memory map, no interrupt table. It is the boundary SolaBasic uses:

Everything QBasic has that CB80 also had is language. Everything QBasic adds beyond it is either the PC or convenience, and neither is here.

Where SolaBasic departs from that rule it says so, in Where this is not QBasic and What is not here.

Why compile it at all

Because GOTO is not expressible in Solum.

Solum has no control-flow syntax; a loop is a message send. A transpiler would have to compile every statement into a block, keep them in an array, and dispatch on a label variable — a full send per BASIC statement, which is what basic.sol already pays as a tree-walker. It would be a slower interpreter wearing a compiler’s name.

Emitting bytecode, GOTO is OP_JUMP and OP_LOOP. This is the rare case where dropping a level buys a construct rather than a constant factor, and it is the reason this program exists.

The verifier cooperates. Its rule is the JVM’s — the paths into a point must agree on stack height (design.md, verify_stack_heights). SolaBasic statements compile at depth 0 with a POP at each boundary, so every label is a depth-0 merge point by construction and an arbitrary jump between statements verifies without any analysis at all.


Program structure

A SolaBasic program is a sequence of lines. A line holds one statement, or several separated by :, or nothing.

There are no line numbers. A line may carry a label:

Again:  PRINT "round and round"
        GOTO Again

A label is an identifier followed by :, at the start of a line. A number is also a label, and this is CB80’s rule taken verbatim: the compiler treats a label as a string of characters rather than a numeric quantity, so labels need not be ordered, need not be present, need not be unique in any numeric sense, and mean nothing except as the target of a jump. 100 and 100.0 are two different labels. Old listings therefore pass through unaltered, and nothing in the compiler ever sorts them.

QuickBASIC agrees, which was not known when this was written and is now measured: 4.5 compiles a listing whose numeric labels descend, and prints what SolaBasic prints. So the rule borrowed from CB80 is not a divergence from QBasic, and the list below needs no entry for it.

A program is one file. There is no CHAIN, no COMMON, and no linker.

Module level and procedure level

Statements outside any SUB or FUNCTION are module level and run in order, top to bottom, from the first such statement. SUB and FUNCTION definitions are not executed where they stand.

GOTO may not cross between module level and a procedure, or between two procedures. Each is a separate chunk and a jump is an offset within one.


Lexical structure

   
Case Keywords and identifiers are case-insensitive. PRINT, Print and print are one word; Total and TOTAL are one variable.
Identifiers A letter, then letters, digits and ., up to 40 characters, optionally ending in a type suffix.
Comments REM to end of line, or ' to end of line. ' may follow a statement; REM may follow one after a :.
Continuation None. A statement ends at the end of its line.
Whitespace Required between a keyword and what follows it. See Where this is not QBasic.
Statement separator : joins statements on one line.

Literals

   
Integer 42, -7, &HFF (hex), &O17 (octal)
Double 3.14, 1.5E-3, 2D6
String "between double quotes". There is no escape; a string may not contain ". Use CHR$(34).

Types

Three, and no more.

Type Suffix AS name Backed by
Integer % INTEGER, LONG SolVM’s tagged 64-bit integer
Double # DOUBLE SolVM’s f64
String $ STRING SolString, dynamic length

LONG and & are accepted as synonyms for INTEGER. SINGLE is not in the language and ! is not a suffix — see the divergences.

A name’s type is fixed by, in order: its suffix; an AS clause on its DIM; the DEFtype range covering its first letter; otherwise DOUBLE.

DEFINT A-N
DIM Count AS INTEGER
Total# = 0
Name$ = "Solveig"

Conversion and arithmetic


Declarations and scope

   
DIM name AS type declares a scalar
DIM name(bounds) AS type declares an array
DIM SHARED ... at module level, makes the name visible inside every procedure
CONST name = expression a named constant, folded at compile time
SHARED name inside a procedure, names a module-level variable to use
STATIC name inside a procedure, a local that survives between calls
DEFINT/DEFLNG/DEFDBL/DEFSTR letter-letter sets the default type for a range of initial letters
OPTION BASE 0 | 1 the default lower bound for arrays. Once, before any DIM.

A variable not declared anywhere springs into being on first use, with the type its suffix or the DEFtype ranges give it, and the value 0 or "".

A procedure’s variables are local to it unless named by SHARED. Module level and procedure level do not otherwise see each other.

Arrays

DIM Grid(1 TO 8, 1 TO 8) AS INTEGER
DIM Names$(100)

Bounds are constant expressions. DIM a(10) runs from OPTION BASE to 10 inclusive. Up to eight dimensions. Arrays are not resizable; there is no REDIM.


Expressions

Highest binding first. Every level is left-associative except ^, which is right-associative.

   
^ exponentiation, always Double
- unary minus
* / multiply, divide
\ integer division
MOD integer remainder
+ - add and subtract; + also concatenates Strings
= <> < > <= >= comparison, answering -1 for true and 0 for false
NOT bitwise complement
AND bitwise and
OR XOR bitwise or, exclusive or

There is no boolean type. A condition is true when it is non-zero, which is why the comparisons answer -1: NOT (a = b) then works out.

EQV and IMP are not here.


Statements

Assignment

LET x = 1
x = 1
SWAP a, b

LET is optional and means nothing.

Conditionals

IF x > 0 THEN PRINT "positive"
IF x > 0 THEN PRINT "positive" ELSE PRINT "not"

IF x > 0 THEN
    PRINT "positive"
ELSEIF x = 0 THEN
    PRINT "zero"
ELSE
    PRINT "negative"
END IF
not
zero
SELECT CASE grade$
    CASE "A", "B"
        PRINT "pass"
    CASE IS >= "C"
        PRINT "marginal"
    CASE ELSE
        PRINT "fail"
END SELECT
fail

CASE takes a list of values, ranges written low TO high, or IS followed by a comparison operator.

Loops

FOR i = 1 TO 10 STEP 2
    PRINT i
NEXT i

DO WHILE more
    ...
LOOP

DO
    ...
LOOP UNTIL done

WHILE more
    ...
WEND

NEXT may name its variable or not, and may close several at once (NEXT j, i). DO/LOOP takes WHILE or UNTIL at either end, or neither, in which case only EXIT DO leaves it.

EXIT FOR and EXIT DO leave the innermost enclosing loop of that kind.

Jumps

GOTO Cleanup

Within one procedure, or within module level. That is the whole of it: there is no GOSUB, no ON n GOTO, and no RETURN. See What is not here.

Procedures

SUB Greet (name$)
    PRINT "Hello, "; name$
END SUB

FUNCTION Area# (r#)
    Area# = 3.14159265358979 * r# ^ 2
END FUNCTION

CALL Greet("world")
Greet "world"
PRINT Area#(2)
Hello, world
Hello, world
 12.56637061435916

A FUNCTION answers by assigning to its own name. EXIT SUB and EXIT FUNCTION leave early.

Parameters are passed by reference, as in QBasic: assigning to a parameter assigns to the caller’s variable. Wrapping an argument in parentheses passes it by value instead — CALL Greet((name$)) — which is QBasic’s own idiom for it.

An array is passed by writing a() at the call site and a() in the parameter list. Arrays are always by reference.

SUB and FUNCTION may recurse, subject to the frame limit — see What this costs.

DECLARE is accepted and ignored. SolaBasic resolves every procedure in a pass over the whole file before it compiles anything, so nothing needs declaring ahead of its use.

Input and output

PRINT "answer:", x; y
PRINT TAB(20); "indented"
PRINT USING "###.##"; total
INPUT "Name"; name$
LINE INPUT line$

PRINT separates with , to the next 14-column zone and with ; not at all. A trailing ; or , suppresses the newline. TAB(n) and SPC(n) are allowed in the list.

A number prints with a leading space when positive — the place where a minus sign would go — and a trailing space always. This is QBasic’s rule and Minimal BASIC’s before it, and it is why BASIC output looks airy.

PRINT USING supports #, ., ,, +, -, $$, **, ^^^^ for numbers and &, !, \ \ for strings.

Files

OPEN "data.txt" FOR INPUT AS #1
DO UNTIL EOF(1)
    LINE INPUT #1, line$
LOOP
CLOSE #1

Sequential only: FOR INPUT, FOR OUTPUT, FOR APPEND. PRINT #, INPUT #, LINE INPUT #, WRITE #, EOF, CLOSE. There is no random access, no FIELD, no GET/PUT.

Ending

END stops the program. STOP does the same and says where.


Built-in functions

Mathematical. ABS ATN COS EXP FIX INT LOG RND SGN SIN SQR TAN, and RANDOMIZE as a statement.

String. ASC CHR$ INSTR LCASE$ LEFT$ LEN LTRIM$ MID$ RIGHT$ RTRIM$ SPACE$ STR$ STRING$ UCASE$ VAL.

MID$ is a function only. It is not an assignment target.

Twenty-seven in all, against Minimal BASIC’s eleven and Full BASIC’s fifty-two.


What is not here

Never — the PC

QBasic’s largest surface is not language, it is a machine. None of it is here and none of it is planned:

SCREEN, PSET, LINE, CIRCLE, PAINT, DRAW, PALETTE, GET/PUT for graphics, PLAY, SOUND, BEEP, PEEK, POKE, DEF SEG, VARPTR, CALL ABSOLUTE, CALL INTERRUPT, INP, OUT, WAIT, SHELL, ON KEY, ON TIMER, KEY, LOCATE, COLOR, CLS, INKEY$, CHAIN, COMMON, RUN.

Never — the vestiges

  why
GOSUB / RETURN / ON n GOSUB SUB is the mechanism. RETURN also needs a computed jump, which SolVM does not have: the return address is dynamic and OP_JUMP takes a literal offset, so each RETURN would compile to a chain of comparisons over a return-id stack. Cutting it saves that entirely.
ON n GOTO the same computed-jump problem, in the form the tree-walker solved with an array. SELECT CASE says it.
DATA / READ / RESTORE a listing carrying its own input is a line-numbered idea. Files are here instead.
line numbers labels are here instead, and a leading number is one.
DEF FN FUNCTION says it.
SINGLE see the divergences.

Not yet

Named so that adding one is a decision rather than a drift. Each says what would make it worth doing:

  when
ON ERROR GOTO / RESUME / ERR CB80 had it, so the cut line says it belongs. It waits because Solum already has the unwinding half — onError and ensure — and the design should be settled against those rather than guessed at. The first SolaBasic program that needs to survive a bad file.
TYPEEND TYPE records. The first program wanting more than parallel arrays.
REDIM and dynamic arrays the first program that cannot size an array at compile time.
OPTION EXPLICIT when a misspelled variable has cost somebody an afternoon.
MID$ as a statement when something needs to patch a string in place and LEFT$ + RIGHT$ is the workaround being written twice.
Random-access files the first program with a record format.

Where this is not QBasic

The list basic.sol keeps, for the same reason: a gap written down is a gap, and a gap discovered is a bug.

1. There is no SINGLE. QBasic’s default numeric type is a 32-bit float printed to seven significant digits. SolVM has i64 and f64 and nothing between, so emulating it means rounding on every store and would still print differently. SolaBasic’s default numeric type is DOUBLE, ! is not a suffix, and AS SINGLE is refused rather than silently widened. A ported program will print more digits than it used to. This is the largest single divergence and the one most likely to surprise.

2. INTEGER is 64 bits. QBasic’s is 16 and LONG is 32. Measured: a QuickBASIC 4.5 program compiled with BC.EXE wraps at 32767 rather than stopping — BC compiles without overflow checking unless /D asks for it, where the QB.EXE environment would raise Overflow. Either way, SolaBasic has one integer type, and LONG is a synonym for it. A program relying on 32767 + 1 failing will not fail here — it will be right, which is worse.

3. Spaces between tokens are required. FORI=1TO10 is not FOR I = 1 TO 10. This is inherited deliberately from basic.sol, which explains at length why: a tokeniser that ignores spaces cannot work left to right on characters alone, and has to know where it is in the grammar. It is a different scanner, not a missing branch. Nobody writes it; it is still a gap.

4. A string may not contain a double quote. QBasic has the same restriction, and CHR$(34) is the same answer. Recorded because it looks like an oversight.

5. A Double prints to the shortest text that reads back as the same number, where QuickBASIC prints sixteen significant digits. Measured, against QuickBASIC 4.5: 1# / 3# is .3333333333333334 there and .3333333333333333 here, and 1# / 7# is .1428571428571429 against .14285714285714285. So they agree whenever the shortest round-trip is sixteen digits or fewer and rounds the same way, and part company on a seventeenth digit or on the last one. Exponential form agrees exactly. This is the entry that said it was not settled; it is.

6. VAL is strict. BASIC’s reads a number off the front of a string and answers nought for junk; this one wants the whole string to be a number. Reading a number out of the front of text wants a scanner, and there is no library in the file the compiler writes to hold one.

7. An array name means one array in the whole listing. QBasic lets two procedures each DIM a Temp of their own; here the second is dimensioned twice. And an array parameter is one-dimensional — a bigger one’s strides would have to travel with it.

8. A file is written with line feeds, where QBasic writes a carriage return and a line feed. Reading takes a carriage return off, so a file written by either is readable here; one written here is not in DOS’s convention.

9. Procedures are resolved before compilation. QBasic requires DECLARE for a procedure used before it is defined, and QB’s editor writes them for you. SolaBasic takes a pass first, so DECLARE is accepted and does nothing.


What this costs

Recursion depth is SolVM’s, not BASIC’s. A SUB compiles to a Solum block and a SolaBasic call is a Solum frame, so a SolaBasic program’s call depth is bounded by 3.5 — about 254 levels. ideas.md predicted exactly this when it argued for a Pascal interpreter: “a lexically nested language spends frames in proportion to the interpreted program’s call depth, so 3.5 would be met head on rather than dodged.” SolaBasic meets it head on. A line-numbered BASIC never could, which is what made basic.sol fit.

By-reference parameters need boxing. Solum sends values, so a variable passed by reference to a SUB that assigns to it must live in a cell rather than a slot. Which variables those are is decided statically, in the same pass that resolves procedures. This is the most expensive item in the language and the one most likely to be underestimated. It is here anyway, because passing by value instead would leave SWAP-shaped programs running and answering differently — and a wrong answer is the one outcome this project does not accept.

A jump reaches 64KB. Every offset is a u16, so no jump can span more than 65,535 bytes of code within one chunk. That is a few thousand statements per procedure — generous, and a hard wall for one long module-level program.

Arithmetic stays a message send. SolVM has no arithmetic instruction; a + b compiles to one OP_SEND, not an add. It is “a much faster interpreter” rather than “compiled”, and the document should say so before the benchmark does — but the benchmark has now spoken and it is 45 times the tree-walker, not the order of magnitude first written here. The same counting loop, 200,000 iterations of two statements, is 1.54s under basic.sol and 0.034s compiled, both including VM start.


How done is decided

There is no standard and no conformance suite, so the authority has to be manufactured. Three mechanisms, in descending order of how much they are worth:

1. A real QBasic is the oracle, and programs/sola/oracle.sh is how it is asked. The corpus is in two halves, and the split is the whole design:

   
oracle/agree/ must produce the same bytes under both. A difference is news — a defect, or a divergence nobody wrote down.
oracle/differ/ must not. Each exercises a divergence recorded above and says at its head what each language should do. One that suddenly agrees is also news: the divergence has gone and the list still claims it.

So the divergence list stops being prose and becomes something that can fail. This is the nearest thing to somebody else’s test, and the only mechanism here that can find something nobody thought of.

Every program in agree/ says its types outright — DEFINT, AS INTEGER, a suffix — because QBasic’s default numeric type is SINGLE and SolaBasic’s is a Double. A bare name is not the same variable in the two languages, and a file testing PRINT must not be testing that instead.

The harness needs an oracle it does not carry. It takes any command that runs a .bas through SOLA_ORACLE, and finds qb64 or fbc if either is installed. For the article:

SOLA_QB_DIR=/path/to/qb45 ./programs/sola/oracle.sh

That directory wants BC.EXE, LINK.EXE and BCOM45.LIB from QuickBASIC 4.5. DOSBox is found on the PATH or inside /Applications/dosbox.app, which is where Homebrew’s cask puts it and is why it is not on the PATH at all.

BC.EXE and not QB.EXE: the QuickBASIC environment writes to the screen, which a script cannot read, where a compiled .EXE redirects into a file the host picks up off the mounted drive. That is why this wants QuickBASIC 4.5 and not the QBasic 1.1 that came with MS-DOS, which has no compiler in it. DOS ends its lines with CR LF and the harness strips the CR before comparing, so a difference it reports is a difference in what was printed.

2. A recorded transcript per feature, compared byte for byte on every build, in the manner of programs/basic/. Every statement in this document and every function in it has one, and the check is that the document and the transcripts cover the same list.

3. This document is frozen when the compiler starts. It may still change — a specification written before an implementation is always partly wrong — but every change is recorded below, with its date and its reason. The goal may move; it may not move silently. That is the whole of what ECMA-55 gave the interpreter, and it is the part that can be had without a standard.

Changes to this document

2026-08-26 — stage 3 was built first, and the claim it tests holds. programs/sola.sol compiles labels and GOTO to OP_JUMP and OP_LOOP. Before it was written, a chunk was hand-assembled with a backward jump to an arbitrary earlier offset, a forward jump over dead code, and a conditional between them: it verified and ran. Both ways of getting it wrong were checked too, because a test that cannot fail proves nothing — a jump into the middle of an instruction and a jump to a point at a different stack depth are each refused at load, exit 65, as a message rather than a crash. The depth-0 discipline is load-bearing rather than tidy, and nothing in the design section above needed changing.

2026-08-26 — a third program, and it found nothing. A word-frequency count: read a text, split it on anything that is not a letter, tally the words in parallel arrays, sort by count and then alphabetically, and lay out the table. It was written to work the string functions hard, those being where the hand-emitted clamping lives and the likeliest place for an edge to be wrong — MID$ a character at a time down a line, UCASE$, string comparison, concatenation in a loop, and two arrays of different types handed to one procedure.

It matched QuickBASIC byte for byte on the first run, and asked for nothing. That is the first real program here to find neither a defect nor a missing feature, and it is worth recording as a result rather than a quiet success: the string half of the language is answering the way it should, and is now held to that by somebody else. Twenty agree/ programs.

2026-08-26 — a second program, and it asked for more. Conway’s Life on a grid, which is the canonical BASIC program and the only thing here that works a two-dimensional array hard. It wanted two things and found a third.

An array parameter may now have any number of dimensions. This was on the not yet list with the reason its strides would have to travel with it — and they need not. The bounds are read off the call sites, every array handed to one parameter having to be the same shape, and a listing that hands it two being refused rather than answering the wrong element. A descriptor travelling with the array would cost every subscript in the language a lookup to buy a case this refuses out loud.

An array element could not be assigned on a one-line IF. IF n = 3 THEN nxt%(r, c) = 1 is as ordinary as IF n = 3 THEN x = 1, and the list of what may go there had simply never had 'arrayset added to it. Nor the file statements. It has now.

And QuickBASIC wants the type spelled on an array parameterg%() and not g() — where SolaBasic will take it from a DEF. That is SolaBasic being the more permissive of the two, so a listing written here may not compile there; it is in the reference manual where somebody writing one will look.

Nineteen agree/ programs.

2026-08-26 — a real program, and what it asked for. Not a feature but a program: a sales report that reads records out of a file into parallel arrays, totals them, and lays out a table. Written to find out what a real one would want, which has been the most productive thing on this list — four of the defects found so far came out of writing the runtime rather than out of testing the compiler.

It wanted INPUT into an array element, which was on the not yet list with the trigger the first program that asks. INPUT #1, nm$(n), qty(n), price#(n) is how records go into parallel arrays, and the trigger fired the moment a real listing was written. It is in.

And it found a defect on the way. Those subscripts were never being typed, so the compiler had nothing to compare against and coerced an integer subscript as though it were a Double — integer does not understand 'rounded', from a statement that looked perfectly ordinary. Every walker that visits expressions now visits those too.

The report matches QuickBASIC byte for byte, and is in the corpus. Eighteen agree.

What it did not want was ON ERROR. That entry’s trigger is the first program that needs to survive a bad file, and this one writes the file it reads, so it never does. The trigger stays unfired and the entry stays where it is, which is the point of writing them down.

2026-08-26 — the : the definition had promised, and two bad errors. Three things a reader hits at once, none of them a new feature.

: between statements was in this document from the start and was never built. Lexical structure says it joins statements on one line; the compiler refused it. That is the one thing the frozen-document discipline exists to catch, and it sat there through eight stages. It works now, and a one-line IF takes everything after THEN to the end of the line — measured against QuickBASIC rather than guessed, because both readings are plausible.

A missing file said the wrong thing in the wrong place. OPEN on something that is not there gave the machine’s own cannot read against a line number inside the runtime, reported as though it were a line of the user’s listing — which that listing did not have. It says File not found, which is QuickBASIC’s message, and a chunk now carries the file it was compiled from, so a failure inside the runtime says the SolaBasic runtime and the trace goes on to the user’s own line.

2026-08-26 — files, and the list is finished. OPEN, CLOSE, PRINT #, WRITE #, INPUT #, LINE INPUT # and EOF, sequential only — and they matched QuickBASIC byte for byte on the first comparison, which no other stage managed. Sixteen agree/ programs now.

There is no streaming underneath, the machine reading and writing whole files, so a channel open for reading holds the file and a position in it, and one open for writing holds what has been written until it is closed. Stopping the program closes what is still open, because otherwise nothing would have been written at all.

Two things came out of writing it. INPUT # was not taking the quotes off a field that WRITE # had put them on, so a round trip through a file gave back "Hans" rather than Hans — and fixing it properly meant making the field splitter quote-aware, so that a comma inside quotes stops separating and text with one in it survives.

And one divergence is new: a file is written with line feeds where QBasic writes a carriage return and a line feed. A carriage return is taken off what is read, so a file written by either is readable here; one written here is not in DOS’s convention. It is in the list.

2026-08-26 — PRINT USING, measured before it was written. The fiddliest formatting in BASIC, and the first feature here built the other way round: twenty-one formats were run through QuickBASIC 4.5 first, and the formatter was written to reproduce what came back. Every one matched on the first comparison but one — the exponent letter, where PRINT USING said E and plain PRINT already said D, so the oracle caught this compiler disagreeing with itself.

The formatter is written in SolaBasic, beside PRINT’s and INPUT’s, and building it there is what turned up three defects in what stages 4 and 5 had already shipped — DIM SHARED on a plain variable doing nothing, a procedure zero-initialising the module’s shared variables, and a FUNCTION of no arguments called without brackets being read as a variable. Writing a real program in the language keeps being the thing that finds them.

2026-08-26 — the corpus grew to cover what it had been missing, and found nothing. GOTO was not tested against QuickBASIC at all, which was a hole in the middle of the design: interleaved jumps, a jump out of a block, a label before NEXT, out-of-order numeric labels, the by-reference chain, and the numeric functions all went in. All fourteen agree/ programs match, and no new defect turned up.

That is worth recording as a result rather than a non-event. Three defects came out of the first fourteen programs, and the five written to close the biggest gap in the coverage came out clean — so the areas the design rests on are answering the way they should, and are now held to it by somebody else.

One thing was settled on the way. Program structure says a number at the start of a line is a label and not a line number, taken from CB80, and that it lets old listings through unaltered. QuickBASIC does the same: it compiles a listing whose numeric labels descend. The rule is not a divergence and the list needs no entry for it.

2026-08-26 — INPUT, and the oracle earned its keep twice more. INPUT and LINE INPUT are in, and match QuickBASIC byte for byte. Two of the three things worth recording came out of the comparison rather than out of writing the feature.

A function of no arguments was being read as a variable. RND on its own is a call; the parser made it a name, so r = RND quietly read an uninitialised variable and answered nought — and the test written for it passed, because it checked that the answer was at least nought and less than one. Found when SOLAREAD$ did the same thing and made the runtime loop for ever.

QuickBASIC echoes an answer it read from a file, so that a redirected session reads the way the interactive one looked. SolaBasic did not, and basic.sol had recorded that as a limitation of its own. It does now, when the output is not a terminal — there is no way to ask whether input is one, and output is the same question every time it matters.

And the end of input has to be told from a blank line. Both are the empty string to a program; the machine answers nil for one and not the other, so the runtime is handed a NUL, which no typed line can contain, and stops with Input past end of file rather than asking again for ever.

2026-08-26 — the oracle ran, and it was worth building. QuickBASIC 4.5 under DOSBox, through oracle.sh. All eight agree/ programs match byte for byte and all five divergences are still there. Three things came out of the first run, and not one of them was reachable from anything already here.

A real defect. PRINT (1 < 2) printed truD. A comparison used as a number is -1, and this document has said so since it was written — but PRINT was emitting the value without saying what type it wanted, so the machine’s boolean went out as text, and the runtime’s exponent swap turned true into truD. Eleven recorded transcripts were green, and one of them recorded truD as correct, because a transcript records what a program does rather than what it should do and re-recording after a change bakes the change in. That is the whole argument for this stage, demonstrated on the first attempt.

Two entries in the divergence list were wrong. INTEGER overflow was predicted to stop the program and does not — BC.EXE compiles without overflow checking, so QuickBASIC wraps to -32768. And the digit count, the one thing this document said was not settled, is settled: QuickBASIC prints sixteen significant digits and SolaBasic prints the shortest that reads back the same.

And two literal forms were missing. 1# and 1D20 are how QBasic writes a Double, and were needed to ask the digits question properly at all — found by a corpus file refusing to compile rather than by anything looking for them.

2026-08-26 — the oracle harness is built, and its verdict is not in. programs/sola/oracle.sh compares SolaBasic against a real QuickBASIC over a corpus in two halves: agree/, which must match, and differ/, which must not and says why at the head of each file. That turns this document’s divergence list from prose into something that can fail — a program in differ/ that starts agreeing means the divergence has gone and the list is now wrong.

No verdict yet. The machine this was written on has no QuickBASIC, no DOSBox, no qb64 and no fbc, and installing one is not this script’s business — the repository claims no dependencies beyond a C11 compiler and make, and the harness keeps that claim by saying what it needs instead of fetching it. So what is recorded here is the mechanism and the corpus; the answers arrive when somebody runs it.

Both paths are exercised, so the harness itself is not taken on trust: run with SolaBasic standing in as its own oracle, every agree/ matched and every differ/ was reported as having lost its divergence, which is precisely what that arrangement should produce.

2026-08-26 — stage 5, and an array is already a reference. DIM with constant bounds and up to eight dimensions, OPTION BASE, CONST, DIM SHARED, and arrays passed to procedures.

By-reference for an array is free, which is the finding — and it is the opposite of what a scalar cost. A Solum array is a reference, so Sort(n(), 6) hands the array over and the callee’s atPut writes the caller’s storage because it is the same array. No box, no analysis, nothing to keep alive.

Every subscript of a multi-dimensional array is checked, and a one-dimensional one is not. One out of range would otherwise land on a different element rather than off the end — a(1, 9) in an eight-by-eight is index 9, which is a(2, 1) — and answering the wrong element quietly is the one thing this must not do. A one-dimensional array needs no check, because there is nowhere for a bad subscript to go except outside the array and the machine refuses that itself.

Two things are narrower than QBasic and are recorded as divergences. An array name means one array in the whole listing, where QBasic lets two procedures each DIM a Temp of their own — bounds are settled while compiling and looked up by name, so the name has to be the whole of the question. And an array parameter is one-dimensional, because the strides of a bigger one would have to travel with it and there is no descriptor to carry them.

2026-08-26 — stage 1: types first, then everything else falls out. The three types, the whole operator table and all twenty-seven supplied functions are in programs/sola.sol. Only PRINT’s formatting is left of this stage, and that is stage 6’s business.

Types have to be settled before a byte is emitted, which is the finding. A conversion is an instruction acting on the top of the stack, so widening an Integer must happen after it is pushed and before the value beside it — by which time it is far too late to work out that it was needed. So the tree is typed in a pass of its own and emitting is a second walk that already knows where the conversions go. Everything else in the stage is a table.

There is no boolean type, and this document was right to say so. A comparison answers -1 or 0 used as a number, which is why NOT, AND and OR are bit operations and still read correctly. Internally a comparison answers the machine’s boolean, because that is what a conditional jump wants — turning one into -1 costs a jump, so the jump is emitted only where the value really is used as a number.

Two operators follow QBasic against the machine. SolVM’s integer divide and remainder are floored, so -7 \ 2 would be -4 and -7 MOD 2 would be 1. QBasic says -3 and -1, and this says QBasic — exactly, in integers, by adding one to the floored quotient when there is a remainder and the signs differ. An earlier version went through the float divide and was wrong above 2^53; a truncating divide on integer is what would make it one send instead of twelve instructions.

And VAL is strict, which is a new divergence rather than one this document predicted. BASIC’s VAL reads a number off the front of a string and answers nought for junk; that wants a scanner, and the file this compiler writes has no library in it to hold one. It is in Where this is not QBasic now.

2026-08-26 — stage 4, the expensive one, and by reference is a box. SUB, FUNCTION, locals, SHARED, STATIC and by-reference parameters are in programs/sola.sol. A procedure is a block bound to a global and a call is value, which fits closely: a block has its own frame, takes arguments in slots 1..n, and answers its last expression.

It never captures its home frame, so 3.1 does not bite — every name a procedure uses is its own slot or a global, so there is nothing to reach out for. 3.5 does, exactly as What this costs predicted before any of this was written: recursion stops around 254 levels, and the trace names the BASIC procedure and the BASIC line.

By reference cost far less than billed. The document called it the most expensive item in the language, and the reason it was not is the representation: a variable ever passed by reference is kept in a one-element array, always, so the call hands the array itself over and the callee’s atPut reaches the caller’s storage. No wrapping at the call site, no copying back, no temporary to keep alive across the call, and nothing to get wrong when the call is recursive. What it costs is one send on each read and write of such a variable, paid only by variables that are actually passed that way.

The analysis is a fixed point and that part was as billed: a parameter is by reference when its procedure assigns to it or hands it on to something that does, and the second half chains through as many procedures as the listing has.

And two things BASIC requires turned out to need doing rather than assuming. A variable this compiler never stores into is an undefined name to the machine rather than a nought, so every name a scope mentions is now given its nought before the scope’s first line — PRINT Z prints 0, as it must. And a STATIC cannot be a frame slot, because a frame is new every call; it is a private global instead, initialised once at module level.

2026-08-26 — stage 2 needed nothing the back end did not already have. IF, SELECT CASE, FOR, DO, WHILE and EXIT are in programs/sola.sol, and the whole of them is one stack of open blocks over the hole-and-fill the GOTO work had already built: a forward jump emitted before its target exists, patched when the closing line turns up instead of when a label does. Nothing was added to the emitter, which is the finding — the structured half of the language is the unstructured half with a stack on top, and doing stage 3 first is what made that visible rather than lucky.

Two things this settled that the document had left open. The blocks are a stack and the statements stay flat, rather than a parser that builds a tree: BASIC’s blocks are an opening line and a closing line, half the errors worth reporting are the two not matching, and a stack has the mismatch in its hand where a tree would refuse to parse and have less to say. And EXIT FOR leaves the innermost FOR rather than the innermost block, so it searches down that stack — four lines, against a tree walk.

Where FOR is not exact is now written down, because it was decided here. A step written as a literal fixes the loop’s direction at compile time, which is nearly every loop. A step that is an expression does not, so the test becomes (limit - counter) * step >= 0 — right for either sign, and forever on a step of nought, as BASIC is. The one case it gets wrong is a product that underflows to -0.0, which compares as >= 0 and buys one extra iteration. Unreachable from a literal step.

2026-08-26 — the speed estimate was too modest, and is now measured. What this costs said compiling should be worth “roughly an order of magnitude” over the tree-walker. It is 45 times: the same 200,000- iteration loop is 1.54s under basic.sol and 0.034s compiled. The sentence has been replaced with the measurement. The claim it qualifies — that this is a much faster interpreter rather than compiled code, because arithmetic is still a send — is unchanged and still the honest description.


Stages

The language above is the finish line. It is reached: every stage below is done, and each is held against a real QuickBASIC 4.5 rather than only against transcripts this compiler recorded of itself. What is left over is the list of things this document marked not yet from the start.

The order it was reached in:

   
0 Doneprograms/sola.sol. A .sob out of a SolaBasic program, running, with nothing of the compiler present.
1 Done. The three types, the whole operator table, all twenty-seven supplied functions — and PRINT’s rules, brought forward out of stage 6 because stage 7 cannot compare anything until output matches.
2 Done. IF in both shapes, SELECT CASE, FOR/NEXT, DO/LOOP, WHILE/WEND, EXIT FOR and EXIT DO, all compiled to jumps.
3 Done, and first, as this table said it should be. GOTO and labels, forwards and backwards, to any label in the program. What it found is below.
4 Done. SUB, FUNCTION, CALL, locals, SHARED, STATIC, EXIT SUB/EXIT FUNCTION, and by-reference parameters.
5 Done. DIM with constant bounds and up to eight dimensions, OPTION BASE, CONST, DIM SHARED, and arrays passed to procedures.
6 Done. PRINT’s rules, PRINT USING, INPUT, LINE INPUT, and sequential files — OPEN, CLOSE, PRINT #, WRITE #, INPUT #, LINE INPUT #, EOF.
7 Done, and the verdict is in. oracle.sh against QuickBASIC 4.5 under DOSBox: all fourteen agree/ programs match byte for byte, and all five divergences are still there. It has found three real defects and corrected two entries in the list below.

Stage 3 is the one to reach early even though the ordering does not demand it, because it is the claim the whole design rests on. If arbitrary GOTO between statements does not verify as predicted, that is worth knowing in week one rather than week six.