Skip to content

Repository files navigation

Solveig

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

The names

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 solvm had 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.

Layout

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.sob

Six 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.

Build

make          # builds bin/solas, bin/solvm, bin/solis, bin/solid
make test     # builds and runs the test suite
make clean

No 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.

Install

make install               # to /usr/local
make install PREFIX=~/opt  # or anywhere
make uninstall

The 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.

Status

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, unchangeddictionary: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 nowSolum 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 installsmake 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.0Solum 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.0sqrt 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 it1e150: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.

License

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.

About

Solveig — 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. 20k lines of C11, no dependencies.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages