A small object-oriented language and its toolkit — bytecode compiler (Solas),
virtual machine (SolVM), REPL (Solis) and debugger (Solid). Prototype-based,
everything is a message send, 145 messages, no control-flow
syntax and no operators outside @expr(a^2 + b/2), which is notation for the
same sends. 20k lines of C11, no dependencies.
Documentation: https://hansolovkarlsson.github.io/Solveig/ — a tutorial that builds one program from nothing, a guide to every concept, and the reference. Measured against CPython 3.14 on nine matched programs, it comes out a little ahead — and the comparison is what found the three defects that got it there.
Everything is an object, including classes, and work happens by sending messages to them:
a := #45. ; -- ':=' binds a name; '#' tags an integer ( bare 45 is a float )
a:print. ; -- ':' sends a message, '.' ends the statement
xs := [#1, #2, #3]. ; -- sugar for array:of(#1, #2, #3)
xs:do({ x | x:print }). ; -- { } is a block: code as a value
Solveig is the language, and the toolkit around it: compiled by Solas,
run by SolVM, explored through Solis, taken apart with Solid. The
ground those four stand on is Solum -- the machine's own layer, its
instruction set, what a program is finally laid down on. bin/solvm is that
machine and its sources are under solum/: the same word in two hands, as
below.
The language was called Solum until 0.36, and much of what is written here still calls it that. The name was not replaced; it moved down a layer. Solum is the ground, and on reflection the ground is the machine -- which is what
solvmhad been saying all along. Documents that record history -- the changelog, the journal, the completed work -- are left as they were written.
Solveig is Old Norse -- Sólveig, from sól, "sun", joined to veig, which is usually read as "strength", though the second element is not settled and has also been glossed as "draught" or "power". It is still an ordinary given name across Norway, Sweden, and Denmark. Most people who recognise it will recognise it from Ibsen's Peer Gynt, where Solveig is the one who waits, and from Grieg's setting of her song.
SolVM reads two ways, and both are meant. It is the Sol virtual machine -- and it is SOLVM, which is how solum was written before the alphabet split V into two letters. Classical Latin had a single V standing for both the vowel and the consonant; U is a much later invention, which is why Roman inscriptions give us SOLVM and not SOLUM. So the machine is not named after anything. It is the word for what it is, cut in stone.
Solum was picked for both of its Latin senses, and each of them says something about the layer it names.
As a noun it is the ground -- soil, floor, the base a thing stands on. That
is what the machine is to everything written above it: the last thing still
there once the language has been compiled away, and what a .sob file is a
description of.
As an adverb, sōlum, it means "only", and that is the design principle rather than a decoration. There is only one kind of thing here: everything is an object, classes included. There is only one thing that happens to it: a message is sent. No operators, no control-flow syntax, no second mechanism behind the first. And the machine is where that is true rather than merely intended -- the dispatch loop has one way to call something, so a language with a second mechanism could not be run by it without being given one. Only is the whole idea, and the ground is where it is kept.
Those are two different words, and the sun is a third. Latin tells them apart by vowel length -- solum the ground has a short o, sōlum "only" has a long ō from sōlus, "alone", and sōl, sōlis, the sun, is a root of its own. They are not one word wearing three hats. They are three words that happen to look alike, chosen for what each of them says.
Solidus is a fourth, and the debugger's. It is Latin for firm, whole, sound, usually taken back to a root meaning "whole" -- the one behind salvus, "safe" -- and unrelated to either the ground or the sun however alike they look. A debugger is the tool for finding out whether a program is sound, standing on the ground called solum. It is also sol-interactive-debugger, which is the sort of coincidence a name should be chosen for.
The mark is those two words in one shape: a disc parted, sól above the line and solum below it. It is also only one shape, which is the other sense of solum and the argument the language makes -- see assets/README.md.
The sun is what ties them together. It is the single body everything else in the system turns around, and the one thing this planet's life has always depended on -- alone in the sky, central, and the reason anything else works. A language in which there is only one kind of thing, and only one thing you can do to it, is named for that on purpose. Solis is "of the sun", solum is the ground it lights, and Solveig carries the same star into Norse: sól joined to veig, the strength of the sun.
| Path | What |
|---|---|
solas/ |
Solas -- the compiler: source text to bytecode |
solum/ |
SolVM -- the virtual machine, built as bin/solvm |
solis/ |
Solis -- the REPL: reads until the input could compile |
solid/ |
Solid -- the debugger: breakpoints, stepping, and locals |
docs/ |
GUIDE.md, TUTORIAL.md, REFERENCE.md, CHEATSHEET.md, GRAMMAR.md, SOLABASIC-CHEATSHEET.md, PASCAL.md, design.md, BYTECODE.md, ROADMAP.md, COMPLETED.md, CHANGELOG.md, journal.md, programs.md, embedding.md, extensions.md, NET.md, lineage.md, method.md |
tests/ |
Test suite |
examples/ |
One .sol demonstration per concept the guide names |
programs/ |
Whole .sol programs, each written to do a job -- what each does |
experiment/ |
Solum's compiler written in Solum: proved, then parked -- what it was |
embed/ |
A C program that holds a SolVM -- see embedding.md |
lib/ |
The library that ships with the language, found on the search path |
extensions/ |
Bundles built by make and loaded only when a host names one -- net, UDP sockets, documented in NET.md |
Two more live outside this repository, and outside it on purpose — the front
page says no dependencies beyond a C11 compiler and make, and it stays true
because they are not here. extensions/net may be here for the same reason read
the other way: sockets need POSIX, which every dlopen and fork in this tree
already assumes, so nothing has to be installed for make to build it.
| Repository | What |
|---|---|
| solveig-gtk | A GTK4 window, loaded at run time. 17 messages, and the editor above ported to it |
| solveig-sdl | An SDL2 surface, shaped nothing like the GTK one because SDL is not |
Both are built against extend.h and loaded with
--extension=; see docs/extensions.md.
A third is outside for a different reason.
| Repository | What |
|---|---|
| Proto | A compiler whose syntax arrives with the file it is compiling. A module declares its own grammar in its header, and what comes out is Solveig source, which solas turns into bytecode like any other |
The two above are outside so that no dependencies beyond a C11 compiler and
make stays true. Proto is outside so that it cannot reach in. Its whole
claim is that a language is something you write on top of a substrate you do not
get to change, and a front end living two directories from the compiler it
targets would prove only that Solveig's author can write a front end for
Solveig. So it takes nothing from this repository at all — no header, no
archive, no symbol. It emits text, solas reads text, and the coupling is a
file format and a command line, which is the same surface anybody else would
have.
proto vectors.pro -o vectors.sol && solas vectors.sol && solvm vectors.sobSix programs are written in it — an assembler reaching ARM64, a PEG toolkit, SHA-256, a ledger, a document, and a BASIC interpreter — and what they say about whether a grammar declared per module is worth having is in that repository's does-it-pay.md.
Each component keeps its public headers in <component>/include/<component>/
and its implementation in <component>/src/. solum/include/solum/bytecode.h
is the contract between the compiler and the VM -- it is the one file both
sides include, so the instruction set is defined exactly once.
make # builds bin/solas, bin/solvm, bin/solis, bin/solid
make test # builds and runs the test suite
make cleanNo dependencies beyond a C11 compiler and make. Every push is built and the
suite run three ways — gcc and clang on Linux, clang on macOS — and again under
ASan and UBSan, so that sentence is checked
rather than asserted.
make install # to /usr/local
make install PREFIX=~/opt # or anywhere
make uninstallThe binaries go to $PREFIX/bin and the library to $PREFIX/lib/solum, which
is where an installed solas looks for @include "text.sol". It has to be
told: argv[0] says where the binary is only when it was named with a path,
and a program found on PATH has nothing to work from — so the Makefile writes
the location into the build, and changing PREFIX rebuilds what depends on it.
Four places are searched, in this order: -I arguments, then the
colon-separated SOLUM_PATH, then the library beside the binary
(bin/../lib, which is what a checkout has), then where the install put it. A
checkout keeps winning over anything installed on the machine, which is what
makes testing a change mean anything.
make dist writes dist/solveig-<version>.tar.gz from HEAD, named for the
version the binaries inside it report.
0.43.0 — a decompressor, and the read it turned out to want.
gzip is the twenty-second program and inflates a gzip
stream: a bit reader, canonical Huffman a bit at a time, a 32 KB window that
back-references copy out of, and the checksum held against the trailer. Its
oracle produced every input it is checked against — /usr/bin/gzip
compresses and this decompresses, 66 round trips, so a disagreement cannot be a
difference of opinion about what the input meant. It was written to measure what
a 32 KB window costs as boxed values, and the window is 4.8% of the program
where the bit-by-bit decode is 70.7%, though 93% of the output comes out of that
window. The expensive thing is the one that happens most often, not the one that
looks heaviest. It also found what no specification could: gzip -l's ratio
column is integer arithmetic with a floor at -99.9%, not the obvious formula,
and is in the tool rather than in RFC 1952. system:readUpTo(#n) closes
6.45 — up to n bytes of standard input exactly as sent, read(2)'s contract
rather than fread's, so memory stops depending on the size of the stream:
41,983 bytes held whatever the input, against a whole read that tracks it. 145
messages, up from 144, and .sob files are still format version 14.
0.42.0 — a corpus another implementation can score itself against.
conformance/ is 90 cases in three kinds — one that
runs and prints, one that runs and then stops, one the compiler must reject —
each scored on its exact bytes, with both tools taken from SOL_COMPILE and
SOL_RUN so a stranger swaps one and keeps the other. Every answer in it was
written from the documentation before it was run, which is the only
arrangement under which it can find anything; 40 of the first 42 held, and both
misses were arithmetic rather than the machine. What it found was in the
documents: REFERENCE.md was wrong about onError in both halves of one
paragraph, and PRODUCING.md filed a self-including file as a refusal when it
is a warning. diff and sort are the
twentieth and twenty-first programs, the first held against a tool byte for byte
over a corpus with deliberate divergences in it, and between them they raised
both of the roadmap's open entries. readFile answered "" for every pipe
and reads them whole now — the size came from a seek, a seek fails on a stream,
and nought is indistinguishable from an empty file. 144 messages, unchanged,
and .sob files are still format version 14: this release adds a way of
checking the language rather than anything to check.
0.41.0 — held against implementations this repository did not write, and
that is where every defect came from. 144 messages, up from 141, and .sob
files are format version 14. lib/re.sol carries both POSIX
dialects in one engine — groups, alternation, back-references — and it exists
because a shipped program was misreading valid input: sed.sol read \( as a
literal parenthesis, so a script using a group came out inverted, with no
error and exit 0, past sixty corpus cases of which not one had used one.
awk is the nineteenth program and the engine's second
customer. system:sleep, system:isTerminal and system:fileId are new, and
the idiom the second replaces had been throwing away the input of every
pipeline slow to produce its first byte. A file can be read in part,
closing 3.22 — a range rather than a handle, so there is nothing to open, close
or leak. sha256sum produced the first absolute figure
this project has for what a bytecode instruction costs: 234 million a second,
4.3 nanoseconds each. And the first outside user read the documents for an
afternoon and moved eight things, one of them a compiler emitting bytecode its
own verifier refused.
0.40.0 — a dictionary literal, and the documents held to the standard the
code is held to. #["key" = value] is new syntax and real desugaring: it
compiles to a global load of dictionary and a send of of, held byte-for-byte
against the written-out form by a test, the way [...] is held against
array:of. 141 messages, unchanged — dictionary:of
was added and of was already a name — and .sob files are format version 14.
string:startsWith and string:endsWith arrived once three programs had
written them, two of them independently. The editor stopped corrupting files
that are not ASCII: $x on café had been writing half a character to disk.
And five claims standing in the documentation were tested, of which four were
wrong — none found by a program failing.
0.39.0 — measured against another implementation for the first time, and
three things that found. 141 messages, unchanged, and
.sob files are format version 14. Nine matched programs against CPython 3.14
put Solveig level at first — geometric mean 1.09 — and every defect the
comparison turned up had been invisible from the inside: a heap object allocated
for every character read, a hash lookup for every global variable, and a
three-branch function that could not be inlined for sitting in the wrong file.
It is 0.885 now, ahead on five of the nine, with none of it a rewrite.
docs/performance.md is the account and
comparisons/python holds the programs.
The extension ABI is declared rather than inferred, which is the one thing
here to read before upgrading. bin/solvm exported 146 sol_* functions where
extend.h named 23 — the surplus being the
parser, the lexer, the REPL's line editor and the bytecode reader, none of them
promised to anybody. It exports 29 now: the 23, plus six promoted on review,
three of which close a gap extensions.md had recorded and left open. A bundle
that used the documented surface is unaffected, checked by loading 0.38.0's
net.so on this build; one that reached past it will not load, and that is the
point.
0.38.0 — two things that add no messages: a notation and a bundle.
141 messages, unchanged, and .sob files are format
version 14. @expr{...} is the infix region over a block rather than a group,
emitting exactly what { @expr(...) } emits, jumps included — so a program
using it compiles here and its bytecode runs on 0.37.0 unchanged, which is the
difference between a notation and a message. And
extensions/net is UDP sockets as a loadable bundle, the first to
ship inside this repository: five messages, a client and a server, and no
networking at all until a host says --extension=.
0.37.0 — one message, asked for by a program. 141 messages, up from 140,
and .sob files are format version 14. replace
answers a new string with every occurrence replaced, because the idiom it
replaces — split then join — replaces all of them, and a first-only version
would have meant something different from what programs were already writing.
Bytecode is compatible with 0.36.0 both ways round; a program that uses the
new message of course needs a machine that has it, and says so. It came from
porting the editor to solveig-gtk,
which wanted it three times in one line and is the only thing that port asked
the language for.
0.36.0 — a notation that adds no messages, and an interface that adds no
dependencies. 140 messages, unchanged, and .sob files
are format version 14 — bytecode from 0.35.0 still runs and 0.35.0 still runs
this release's, checked both ways round by building the old release from its tag
rather than asserted.
@expr(a^2 + b/2) is infix arithmetic, comparison and logic inside a marked
region, and every operator lowers to the send it already read as — the bytes are
the chain's, so the notation costs nothing and adds nothing to the language.
Extensions give the machine a capability it could not grow: a C file
compiled on its own, named with --extension= when a program is started and
never from inside one, since native code runs past every limit.
extend.h is the contract,
docs/extensions.md the prose, and a resource an extension
owns is a value the collector gives back — on the sweep and at teardown, so a
program stopped by a limit still has its sockets closed. Two bundles live out of
tree and prove the mechanism carries a real toolkit:
solveig-gtk and
solveig-sdl.
And the language is called Solveig now — Solum moved down a layer to name
the machine, its bytecode, and the ground a program is finally laid on, which is
what solvm had been saying all along.
0.35.0 — two languages and a module system, and the machine needed one new
message for all of it. .sob files are format version 14, unchanged, and
bytecode from 0.34.0 still runs — checked both ways round.
programs/pascal.sol compiles ISO 7185 Pascal to
bytecode in eight stages against fpc -Miso, and the machine needed nothing
added: no instruction, no message, no roadmap entry, with a recursive Pascal
function reaching exactly the 254 levels the machine itself allows.
programs/check_syntax.sol is told a grammar in
Wirth EBNF and checks a file against it, as a stack machine with no depth limit;
Solum's own grammar is now written down and held against the reference.
system:load lets one .sob load another — @include's run-time twin,
once-only, needing no new memory model because the separation was already there.
And exports lets an object say what it publishes: from outside it is
that list, opt-in and inherited, with five libraries using it and one
deliberately not.
0.34.0 — two integer literals and an interpreter checked against a suite it
did not write. .sob files are format version 14, unchanged, and bytecode from
0.33.0 still runs. $FF08 and %10101100 write the same integer in
hexadecimal or binary, which is where a colour, a file mode or a set of flags is
readable and #493 is not — sugar in the scanner, no opcode and no message. And
programs/basic.sol was run against the NBS Minimal BASIC Test Programs,
208 programs written at the National Bureau of Standards in 1980: it found
seven defects, none of which the eighty-three claims in that file had caught.
Disagreements went 16 to 5, and those five want a person at a keyboard.
0.33.0 — one message, and the program that asked for it got an interface.
.sob files are format version 14, unchanged, and bytecode from 0.32.0 still
runs. system:writeError closes the last thing on the roadmap (3.19):
display, print and system:write all went to standard output and nothing
went to standard error, so a program could not separate what it produced from
what went wrong producing it. And BASIC gained a prompt — a line with a
number goes into the program, a line without one happens now, plus LIST,
RUN, NEW, LOAD, SAVE and BYE. 136 messages, up from 135.
0.32.0 — the language gained twelve messages, the most at once in a long
while, and every one because a single program asked. .sob files are format
version 14, unchanged, and bytecode from 0.31.0 still runs.
programs/basic.sol is an interpreter for ECMA-55 Minimal
BASIC — all twenty statements and all eleven supplied functions, about 420,000
BASIC statements a second. It put three entries on the roadmap in a day and
closed two: pow, exp, log and the trigonometry (3.14, which had been
waiting since it was written for a program that wanted an angle), and
system:write (3.18, no way to write without ending the line, found by
INPUT needing a prompt beside its answer). The third is open — no way to write
to standard error, so a failing listing puts its diagnostic in the output
file. 830 documented claims, up from 808.
0.31.0 — nothing in the language changed; .sob files are format version
14, unchanged, and bytecode from 0.30.0 still runs. What changed is that two
libraries and every compiler warning are now checked. lib/scan.sol shipped
in 0.30.0 verified by a harness that was written once and deleted, and
lib/shell.sol had never had a test at all — they have an example each now,
808 documented claims in total, up from 764. And 45 shipped files must
compile without solas saying anything, where before they only had to
compile: both of the compiler's warnings exist because the failure they describe
surfaces far from its cause, and nothing in the build failed on either.
0.30.0 — one new library file and no change to the language; .sob files
are format version 14, unchanged, and bytecode from 0.29.0 still runs.
lib/scan.sol is a cursor over text — five files here had
each written that object for themselves, two of them disagreeing about whether
the method that moves forward is step or advance, and all five are converted
onto it. It is not a pattern language: what repeated across those files was
never a pattern, it was a position. The honest cost is 46 lines recovered
against 48 spent, so it pays for itself and no more. And json:read could
not read a string containing a newline — for four days and four releases,
found by recording a baseline before a refactor rather than by looking for it.
0.29.0 — three names go and nothing is lost with them; .sob files are
format version 14, unchanged, though bytecode compiled before this release
will not run, because the names inside it moved. array:at_put is
array:atPut — it was the only message of 125 with an underscore, and
dictionary had answered atPut all along. The counted loop takes its
numbers together: [#1,#10,#3]:loop(block) replaces toDo and toByDo,
which read as todo and hid the start value in the receiver. The language
answers 123 messages across 219 registrations, down from 125 across 220,
and nothing became unaskable. The roadmap has one entry on it again —
a cursor five programs each wrote for themselves.
0.28.0 — nothing in the language changed and everything about how it is
checked did; .sob files are format version 14, unchanged. The suite runs
where it was not written: gcc and clang on Linux, clang on macOS, and all
762 documentation claims hold there too. The front-page claim of no
dependencies beyond a C11 compiler and make turned out to be false when
first tested, and GCC found undefined behaviour in the interpreter's jump
that clang had been quietly getting right. ASan and UBSan now run the whole
suite on every push, with leak detection, which had never been checked. And
it installs — make install, make uninstall, make dist, with an
installed binary now able to find its own library.
0.27.0 — one new debugger command and no change to the language; .sob
files are format version 14, unchanged. solid gains globals, which lists
what a program bound in the order it bound them — the one question a program
cannot ask about itself, since the globals are slots on an object with no name
in the language. Why := is syntax and not a message is written down where
a reader asks it. Regular expressions were argued three ways and the finding
is that what repeats across the scanning here is the cursor, not the pattern.
And the documentation checker stopped confusing a suffix with a substring,
at six sites where hello.sol.bak passed as a Solum file. Documented claims go
from 756 to 762.
0.26.0 — two known limitations close and .sob files are format version 14,
unchanged. A child's streams go where they are told: system:run and
system:capture take an optional second argument, so a command that complains
on stderr can be quietened without a shell. random:new is a generator you
make — seeded by the machine, or by a number you name and then repeatable —
which is where the state had to live for a chunk to keep running the same twice.
And a block argument is checked when the message is sent, closing fourteen
places where false:and(#45) was accepted because a false receiver never
reaches its argument. Documented claims go from 729 to 756, the difference
being claims that were written down and never read.
0.25.0 — a journal release; no code changed and .sob files are format
version 14, unchanged. docs/journal.md gains a day that was
three releases long, with a six-item postmortem whose items share one shape:
each is a claim made from reasoning where a short measurement would have refuted
it. Alongside it, the observation that
3.16 now has more
instances behind it than any other open entry — the checker cannot catch a
claim that stops being checked, and three arrived in one day.
0.24.0 — a library and documentation release; no language change and .sob
files are format version 14, unchanged. array:ifElseIf writes a chain of
alternatives flat instead of as nested ifElse, measured at 5.8× a chain and
three frames a level through a recursion, so it is for a flat dispatch and not a
recursive descent. 3.17
is new: global lookup walks a list, linearly, and the order is recency — so the
name a library bound first is the slowest to read. Three design questions were
explored and recorded rather than built — default parameter values, constants,
and forever/break/continue — each redirected by a measurement rather than
an opinion.
0.23.0 — Solum compiles Solum. The compiler is written in the language
it compiles, it compiles its own source, and the result compiles its own source
again to the same bytes; all 47 .sol files here compile to bytes identical to
what solas produces. The one language change is a number: SOL_FRAMES_MAX is
256 rather than 64, so recursion reaches 254 levels rather than 62 — which
cost 4% more memory once the value stack stopped being sized from the frame
count, and which was the last thing standing between the compiler and its own
source. .sob files are format version 14, unchanged. Nothing was added to the
language to make any of it possible, and the code is now parked in
experiment/.
0.22.0 — sqrt is a message a float understands, and it is a primitive
rather than a library method because it was written in Solum twice and both
versions were wrong and silent — the second, written to fix the first, was
wrong by nineteen orders of magnitude above 1e21. min, max and between are
in the new lib/math.sol, being the half nothing can get wrong.
docs/CHEATSHEET.md is new: the whole language on one page,
a line each, with a test that fails if a message is missing from it. And
design.md now states what had never
been written down — Solum is meant to be a general-purpose language, and what
the twenty-two programs happen to do is not a boundary. .sob files are format version
14, unchanged.
0.21.0 — a fix release, and the fix is a memory-safety one: a large float
asked for decimals could print 93 bytes of the stack behind it —
1e150:asString("0.6") answered 157 characters out of a 64-byte buffer — and a
script could read them. .sob files are format version 14, unchanged. It was
found by the tenth program: bench.sol, which times a
command and says whether two commands really differ, needed a square root the
language does not have, wrote one, and tested it at 1e300. The square root
converged; the formatter did not. Three roadmap entries came with it — no
sqrt, min or randomness anywhere in the language, a child's stderr that
cannot be discarded, and what the new checker does not check.
0.20.0 — a testing release; no code changed and .sob files are format
version 14, unchanged. Everything this repository writes down about what it
prints is now checked on every build — 589 claims across 40 files, where
before the suite compiled every example and ran none of them. It found a stale
stack trace in the guide, a slot count nine releases out of date, an opening
snippet on this page that did not compile — the four lines that introduce the
language were missing a . — and, in the checker itself, an answer that
depended on how many times it had been run.
0.19.0 — a documentation release; no code changed and .sob files are
format version 14, unchanged. The roadmap now has nothing left to decide:
6.32, whether a script should run with less than the whole machine, was deferred
rather than taken and moved to ideas.md, being the only entry
that ever came from a concern rather than from a program.
docs/lineage.md is new and places the language among
Smalltalk, Self, Io, Lua and Ruby — read it first if you already write another
language. Surveying what those have that this might want produced exactly one
roadmap entry, and it came from this repository's own programs:
3.13, a
loop is left by its condition or by failing, with nine of 69 loops here carrying
a boolean whose only job is to stop one.
0.18.0 — .sob format 14, so recompile: a file built by 0.17.0 or
earlier is refused with unsupported bytecode version, and the remedy is
solas again, which costs nothing since a .sob is derived from a .sol that
is still there. Nothing about the language changes. A .sob had been a
little-endian container holding a big-endian instruction stream — two
conventions arrived at separately and never compared until
disasm.sol decoded both in one program and got the
operands backwards. The order now lives in two constants rather than thirteen
places, which made the flip a two-character edit; and the thing that caught the
one reader make test cannot check was comparing that program's output against
solvm --dump over 5,737 instructions.
0.17.0 — two programs that read this project's own work, and the four faults
they found in it. disasm.sol disassembles a .sob, which
is a second implementation of a format that had one — and that is how you find
out whether a specification is true. BYTECODE.md had never said what byte an
opcode is, design.md said both "big-endian" and "little-endian throughout"
about the same bytes, and the .sob format table had been missing three sections
since version 12. All fixed, and the opcode numbers now have a test.
expect.sol runs every example and checks the comments
saying what each line prints — nothing ever had, and all 398 hold; it is in
make test now. No language change and no API change: .sob files are format
version 13, unchanged since 0.11.0.
0.16.0 — a data race fixed, and threads settled by measuring. The serial a
machine is stamped with in sol_vm_init was incremented non-atomically, so two
threads building one at once could be handed the same number — 16 threads
building 480,000 machines collided 10,319 times, one in fifty. It matters to
anybody embedding on more than one thread and to nobody else; solvm, solas,
solis and solid each build one machine on one thread and could not reach it.
0.14.1 and 0.15.0 both carry the bug. What is now tested and promised is one
VM and one chunk per thread — a chunk cannot be shared, since running one mutates
it. Also sol_vm_set_error_reporting, so a host can keep failures out of stderr
and in its own log, and two things measured rather than guessed: a fresh VM per
request is a third of a request, and compiling a script is 279µs paid once. No
language change; .sob files are format version 13, unchanged since 0.11.0.
0.15.0 — embedding is a documented interface.
solum/embed.h is the whole supported surface a C
program holds a SolVM through, docs/embedding.md is the
contract, and tests/test_embed.c holds every promise it
makes. Six functions, none of them new capability — each names two or three
calls a host could already have made, which is the point, since three internal
calls in the right order is not something anybody can rely on. What is
deliberately not promised is stated as plainly as what is. Written before
deciding what permissions a host may impose, because a permission is a promise
about what a host may rely on and there was no list of that. No language change;
.sob files are format version 13, unchanged since 0.11.0.
0.14.1 — a use-after-free, found by the first program to embed the machine.
A chunk recorded which VM had interned its names by pointer, so a host that
builds a VM per request — every one at the same stack address — had its second
request reading the freed first machine's name table. SolChunk.interned_for is
a serial now, which is a type change in a public header and nothing else.
solvm, solas, solis and solid each build one VM and could not reach it;
.sob files are unchanged. Also embed/host.c and
docs/embedding.md, which are what found it.
0.14.0 — the shipped files divide in two: examples/ holds one
demonstration per concept the guide names, programs/ holds seven whole
programs each written to do a job. The split was already drawn in the programs'
own headers; docs/programs.md says what each does and how to
run it. The seventh, serve.sol, is a CGI-shaped request
handler and the first program here whose input does not come from whoever ran
it — which found that a limit bounds dispatch, not work: readFile of 256MB
and a scan of all of it is eight instructions, the same eight as for 64MB, so
0.13.0's limits stop a program that loops and do not bound the cost of one
message. That is 3.7, and
two documents that claimed otherwise now do not. No language change: .sob files
are format version 13, unchanged from 0.11.0.
0.13.0 — a program can be given a limit. solvm --steps=N bounds how many
instructions it may execute and --memory=N how much it may hold at once, with
sol_vm_set_step_limit and sol_vm_set_memory_limit for a program embedding
the machine — which is the case that wanted them, a webserver having no ctrl-c
to fall back on. A stop cannot be caught: onError does not see it and
ensure does not run its cleanup, since both are ways of running more code and
the allowance for running code is what ran out. A stopped program exits 124. What
they bound is a program that loops, which 0.14.0 had to go on to say. The
matching question — whether a script should also be able to run with less than
the whole machine — is recorded on the roadmap and not answered. .sob files
are format version 13, unchanged from 0.11.0.
0.12.0 — a debugger, and a program can run another program. bin/solid
stops a program where it is running, or where it broke, and shows the frames and
their locals by name. system:run and system:capture invoke other programs,
taking an array of arguments rather than a command line so that a filename
is never read as syntax; lib/shell.sol is there when pipes and globs are the
point. Also string:trim, because every tool pads its output. .sob files are
format version 13, unchanged from 0.11.0.
0.11.0 — a frame slot knows what it was called, so solvm --trace names its
arguments and anything looking at a frame can say average rather than slot 3. The compiler always knew and threw it away once the index was emitted.
Another .sob format change, 12 to 13 — recompile the .sol — costing
+0.2% to +3.4%. One entry left on the roadmap, and it is the debugger.
0.10.0 — a stack trace says which file, and the .sob format changes for
the first time since 0.1.0 to carry that. It was misleading rather than merely
thin: a chunk holds every included file's code, so a bare line number named a
line in a file nobody had recorded and read as a line of the one you were
looking at. A .sob from an earlier release is refused rather than misread —
recompile the .sol. solvm --version says which format a build speaks.
0.9.0 — bits, and the first tools for looking at a program rather than
writing one. solvm --trace writes the call tree, and because conditionals and
loops compile to jumps a three-hundred-thousand-turn loop adds nothing to it.
solis --interactive runs a file and stays at the prompt with what it left —
after a failure too, which works because a script's own names are globals and
survive the unwind. Bit operations and shifts, which lib/text.sol had been
writing as div and mod for want of them, and inc and dec, which are
three in ten of all the arithmetic here.
0.8.0 — a program can deal with a filesystem it has to change, and with a
keyboard. modeOf and setMode so a copy keeps the executable bit,
setModifiedAt so it keeps its time, makeDirectory answering whether it made
one, and readKey for a program that wants a keypress rather than a line. Every
one of them was asked for by a program rather than planned —
mirror.sol copies a directory tree and found a defect in
modifiedAt on the way. The reference has a contents and an index of all 110
messages. The roadmap is empty.
0.7.0 — the prompt became a place you can work: ↑ and ↓ through what you
have typed, ← and → within the line, the readline bindings, and history kept in
$HOME/.solis_history so it is still there tomorrow. It reads a line exactly as
it used to through a pipe or a file, and needs nothing installed either way. The
language itself is unchanged, and with this the roadmap has nothing left on it
to build — only the restrictions it keeps on purpose.
0.6.0 — no design questions are left open. The last one, whether the class
side and the instance side should be separate objects, is closed by drawing the
line between them with the receiver each message requires rather than by
splitting anything: respondsTo now agrees with sending everywhere, and an
instance can no longer answer for its class. The compiler also warns when two
files claim one global name, there being no module system to stop them. The one
behaviour change is that [#1]:new and [#1]:of(...) are refused rather than
answering.
0.5.0 — the language reads formats it was not built for. lib/json.sol and
lib/html.sol are a JSON reader and an HTML reader written in Solum, on the
search path. The HTML one does not fail on bad input, because bad input is
what HTML is: it recovers and reports what it recovered from. Building it turned
up where the recursion limit really lives — not in the data, but in how you walk
it — and array:removeLast, array:indexOf and an ordering on symbols, each
from a workaround that was already shipped. .sob files were format version 11,
unchanged since 0.1.0, so one built then runs here; everything added since is a
primitive or a library rather than an opcode. solvm --version says which
format a build speaks.
The language is settled enough to write programs in.
programs/log.sol is one written to do a job rather than to
show a feature — it reads an access log, tallies it, ranks it and reports — and
lib/json.sol and lib/html.sol are a JSON reader
and an HTML reader written in Solum, on the search path so a program says
@include "html.sol". and nothing about where it lives.
It is 0.1 rather than 1.0 because the restrictions in the roadmap are real and deliberate: no non-local return, a capturing block tied to the frame it was written in, recursion to about 254 levels, and text is bytes rather than characters. Each is documented where a program would meet it.
Recovering from a failure was on that list until 0.2.0.
Source text goes through the scanner, compiler, and dispatch loop:
$ ./bin/solis
> a := #45.
> a:add(#5):print.
#50
> a:print.
#45
> #45:add(1.5).
solvm: 'add' expects integer, got float (no implicit coercion)Arithmetic is strict: integers and floats never coerce, and integer overflow traps rather than wrapping.
Built in: integer, float, string, symbol, boolean, nil, block,
array, dictionary and object, all delegating to object so that
everything is an object in the type graph as well as in the slogan. Strings
split and join, arrays fold and slice, and a dictionary keeps values under keys.
A program reads and writes files, reads its input, times itself, stops with a
status, and is split across files with @include — or, once they are compiled,
with system:load, which is the same thing done while running.
An object can say which of its slots are anybody else's business.
json:exports(['read, 'write, 'quote, 'keyText]) draws a line, and from outside
that object is those four names — a name off the list can be neither sent nor
bound, while inside nothing changes. It is opt-in, inherited by whatever an
object makes, and an object that never draws one behaves exactly as it always
did.
Compiling to a file and running it separately works too:
$ ./bin/solas examples/hello.sol # writes examples/hello.sob
$ ./bin/solvm examples/hello.sob
#45
.sob files are little-endian and independent of the host, and are verified
before they run -- every instruction has to fit, every operand index something
that exists, every jump land on the start of an instruction, and the last
instruction stop the machine. See docs/design.md for the
layout, docs/BYTECODE.md for the instruction set.
They are not portable across releases: the format carries a version, a build
reads only its own, and an older file is refused with unsupported bytecode version rather than misread.
A method is a name bound on a class, just as a variable is a name bound in the
globals -- so it uses the same :=. The right-hand side is evaluated, and a
slot holding a block is what makes a method:
> integer:double := { self:mul(#2) }.
> #21:double:print.
#42
> integer:poly := { a, b | self:mul(a):add(b) }.
> #10:poly(#3, #7):print.
#37
A slot holding anything else is data, evaluated once when bound. And because
:= evaluates, a method can be computed rather than written out:
> maker := { { self:mul(#2) } }.
> integer:double := maker:value().
> #21:double:print.
#42
See examples/methods.sol.
Braces make a block -- code as a value. Control flow is then ordinary message sending, with no control-flow syntax in the language at all:
> #5:lessThan(#10):ifElse({ #100:print }, { #200:print }).
#100
> i := #0.
> { i:lessThan(#5) }:whileTrue({ i := i:add(#1) }).
> i:print.
#5
Which is enough to be Turing-complete:
integer:factorial := {
self:lessThan(#2):ifElse({ #1 }, { self:mul( self:sub(#1):factorial ) })
}.
#20:factorial:print. ; #2432902008176640000
and and or take a block for the same reason, so the answer can be settled
without running it:
> x := #3.
> x:greaterThan(#0):and({ x:lessThan(#10) }):print.
true
Written literally, all of those compile to jumps -- no block allocated, no frame entered -- while staying ordinary messages you can send any other way. See examples/blocks.sol.
MIT -- see LICENSE.
docs/GUIDE.md is the tour: every concept in the language, in an order that builds, each pointing at a runnable example. Start there.
docs/REFERENCE.md is the language reference: syntax, semantics, and every built-in message.
docs/CHEATSHEET.md is the same surface on one page, a line each -- for when you know what you want and not what it is called. Every built-in message is on it and a test says so.
docs/lineage.md places it: what it borrowed from Smalltalk and Self, what Io and Lua are near it, and what to expect if you arrive knowing one of those. Worth a read before the tour if you already write another language.
The full list of what is left -- open design questions, known limitations, and unbuilt work -- is in docs/ROADMAP.md.
See docs/design.md for the object model, docs/BYTECODE.md for the instruction set, and docs/CHANGELOG.md for what has changed. docs/journal.md is the day-by-day account: what was picked up and why, including the hours that produced no code.