Solveig

Solveig cheatsheet

Everything the language answers, on one page, for when you know what you want and not what it is called. REFERENCE.md is the full account of each of these — this is the index to your own memory.

Every built-in message appears below, and a test in tests/test_compile.c fails if one is added without being listed here. The examples in the fenced blocks are run on every build and their answers checked, the same as everywhere else in this repository.


How the comments in this file are read

A ; comment after a line that prints says what it printed, and this repository runs every one of them. Two conventions are worth knowing before reading the rest, because both cost an outside reader time on 2026-09-01:

Both print and display end the line. Nothing here writes without a newline except system:write.


Syntax, all of it

a := #45.                       ; -- ':=' binds, '.' ends a statement
a:print.                        ; #45   -- ':' sends a message
a:add(#5):print.                ; #50   -- sends chain left to right

xs := [#1, #2, #3].             ; -- array literal
xs:at(#2):print.                ; #2
double := { x | x:mul(#2) }.        ; -- a block: { params | body }
double:value(#21):print.            ; #42

counted := { | n | n := #0. n:inc }.  ; -- { | temps | body }
counted:value:print.                  ; #1
point := object:new.            ; -- an object is made from another object
point:x := #3.                  ; -- a slot holds data
point:show := { self:x }.       ; -- a slot holding a block is a method
point:show:print.               ; #3
Written Is
#45 an integer
$FF08 %10101100 the same, in hexadecimal or binary — no #, no sign
45 4.5 4e2 a float — the unmarked number is the float here
"text" a string, bytes, with \n \t \" \\ escapes
'name a symbol, interned, compared by pointer
[a, b] an array — sugar for array:of(a, b)
#[k = v] a dictionary — sugar for dictionary:of(k, v); #[ is one token
{ x \| body } a block; { x, y \| \| t \| body } takes two and has a temp
; to end of line a comment
@include "file.sol". splices another file in, once, before compiling
@expr( a^2 + b/2 ) infix operators — sugar for the sends they read as
@expr( sqrt(x) ) inside a region, f(x) is x:f — one argument, any name
@expr( a < b & ~c ) = <> < > <= >=, then ~, then & and \|
@expr{ a < b } the same region over a block — a loop’s condition or body
nil true false infinity nan the value globals
object integer float string symbol array dictionary boolean block time error system the class globals

Six rules that bite

   
No implicit conversion, anywhere #1:add(1.0) is an error, not 2.0. #1:lessThan(1.0) is an error. equals is the one exception and answers false across types.
Indices are one-based xs:at(#1) is the first, and copyFrom(#a, #b) includes both ends.
ifTrue answers the block’s value Not a boolean. Never chain ifTrue(...):ifFalse(...) — use ifElse(t, f).
Integers trap, floats do not Overflow is an error; float division by zero is infinity.
A capturing block cannot outlive its frame Reading an enclosing local is fine while that frame is alive, reported after (3.1).
No ^, no early return A block answers its last expression; a loop is left by its condition or by error:raise (3.13).

Control flow is message sending

There is no if, no while, no for. These are ordinary messages taking blocks, and the compiler inlines them to jumps when written literally.

Message On Answers
ifTrue(block) ifFalse(block) boolean the block’s answer, or nil
ifElse(then, else) boolean the chosen block’s answer
and(block) or(block) boolean short-circuit; the block runs only if needed
not boolean a boolean
whileTrue(body) block nil, having run body while the receiver answers true
doUntil(condition) block nil; body first, so always at least once
repeat(#n) block nil, having run the receiver n times
#n:repeat(block) integer nil, having run the block n times
loop(block) array [#a, #b] counts up to #b inclusive, block given each
loop(block) array [#a, #b, #step] the same by #step; negative counts down
do(block) array, dictionary the receiver, block per element

Where the table says block, it means one — and it is checked when the message is sent, not when the block would have run. So false:and(#45), true:ifElse({ #1 }, #45) and []:collect(#45) are all errors, though none of them would ever have reached the argument. A block held in a name counts; only the literal form is inlined to jumps.

#3:greaterThan(#2):ifTrue({ "yes":display }).       ; yes
n := #5:greaterThan(#9):ifElse({ "big" }, { "small" }).
n:display.                                          ; small

i := #0.
{ i:lessThan(#3) }:whileTrue({ i := i:inc }).
i:print.                                            ; #3

ticks := #0.
#3:repeat({ ticks := ticks:inc }).
ticks:print.                                        ; #3

seen := "".
[#1,#7,#3]:loop({ n | seen := seen:concat(n:asString) }).
seen:display.                                       ; 147

Every type answers these

Message Answers
print the receiver, having written its literal form and a newline
display the receiver, having written it and a newline, without quotes on a string
asString its text
equals(v) notEquals(v) a boolean; value for numbers and text, identity for containers
isNil notNil a boolean
isKindOf(class) a boolean, searching the prototype chain
respondsTo('name) a boolean — whether a send would find something
perform('name, ...) sends the message that symbol names
slots an array of symbols naming the receiver’s own slots
slotAt('name) the value in that slot, searching the chain like a send
exports exports([names]) the object’s external surface, or nil; or draws one
new on a class: a new instance; on a value type: refused, there is nothing to make
#45:respondsTo('add):print.         ; true
#45:perform('add, #5):print.        ; #50
"x":isKindOf(string):print.         ; true
nil:isNil:print.                    ; true

Reflection reads and never writes. There is no slotAtPut, no way to remove a slot, and no re-parenting.

integer

Arithmetic traps on overflow rather than wrapping — which is why the textbook random generator cannot be written here at all.

Message Answers
add(n) sub(n) mul(n) an integer; traps on overflow
div(n) mod(n) floored; traps on zero
inc dec one more, one less
negated abs an integer; traps on the most negative
lessThan(n) greaterThan(n) a boolean
lessOrEqual(n) greaterOrEqual(n) a boolean
bitAnd(n) bitOr(n) bitXor(n) bitNot an integer, bit by bit
shiftLeft(#n) shiftRight(#n) an integer; #0 to #63
asFloat a float; loses precision above 2^53
asString the digits, without the #
asBase(#n) the digits in base n, 2 to 36
asCharacter the one-byte string that byte spells; #0 to #255
repeat(block) on integer, loop(block) on [#a, #b] or [#a, #b, #s] loops — see above
#7:div(#2):print.               ; #3
#-7:div(#2):print.              ; #-4   -- floored, not truncated
#7:mod(#3):print.               ; #1
#255:asBase(#16):display.       ; ff
#65:asCharacter:display.        ; A
#5:asFloat:print.               ; 5

float

The unmarked number. Everything integer has except asFloat, asBase and the overflow traps, plus:

Message Answers
floor ceiling rounded truncated an integer; errors on infinity, nan, out of range
sqrt a float; nan for a negative
sqrt a float; nan for a negative
pow(other) self raised to other
exp log e to the self; the natural logarithm
sin cos tan radians
asin acos atan radians; nan outside the domain
float:pi 3.141592653589793 — on the class
float:atan2(y, x) the angle to a point, all four quadrants — on the class

There is no asInteger: narrowing names its direction. Dividing by zero answers infinity rather than erring. The mathematics is float only and radians only; degrees are a multiplication.

2.7:floor:print.                ; #2
-2.7:truncated:print.           ; #-2   -- floor goes down, truncate to zero
9.0:sqrt:print.                 ; 3
2.0:pow(10.0):print.            ; 1024
1.0:exp:print.                  ; 2.718281828459045
float:pi:div(2.0):sin:print.    ; 1
float:atan2(1.0, 1.0):print.    ; 0.7853981633974483
1:div(3):print.                 ; 0.3333333333333333
3.14159:asString("0.2"):display.  ; 3.14
"[":concat(1234.5:asString(",10.2")):concat("]"):display.  ; [  1,234.50]

string

Bytes, not characters: "café":size is 5.

Message Answers
size an integer
at(#i) a one-character string; one-based
concat(s) a new string; strict about its argument
split(s) an array of the pieces between occurrences of s
indexOf(s) indexOf(s, #from) where s first appears, one-based, or nil — from the start or from #from
copyFrom(#a, #b) the characters #a to #b, both ends included
fill([...]) the blanks {} filled in from the array
trim the same text without the space around it
asUppercase asLowercase a new string; ASCII letters only
asInteger asFloat strict: the whole string must be a number
asInteger(#n) reads base n, 2 to 36
asByte the number of the one byte in it
asSymbol the interned symbol for these characters
asTime asTime(format) an instant; ISO-8601, or strptime format
asString(spec) padded text
lessThan(s) greaterThan(s) lessOrEqual(s) greaterOrEqual(s) a boolean, comparing bytes

There is no replace: split(a):join(b) is the same operation in two words.

"a,b,c":split(","):print.               ; ["a", "b", "c"]
"hello":copyFrom(#2, #4):display.       ; ell
"{} of {}":fill([#3, #10]):display.     ; 3 of 10
"  x  ":trim:display.                   ; x
"a-b":split("-"):join("+"):display.     ; a+b
"left":asString("<8"):concat("|"):display.  ; left    |

symbol

Interned and compared by pointer, which is what makes perform cheap.

Message Answers
size an integer
asString the name, as a string
lessThan(s) greaterThan(s) lessOrEqual(s) greaterOrEqual(s) a boolean, comparing the text
'add:asString:display.          ; add
'add:equals('add):print.        ; true

array

One-based, and add answers the array so it chains.

Message Answers
new / of(...) an empty array / one of the arguments
size an integer
at(#i) the element; out of range is an error
atPut(#i, v) the value stored
add(v) the array, so it chains
removeLast the last element, taken off; an error when empty
indexOf(v) where v first is, or nil
copyFrom(#a, #b) a new array, both ends included
first(#n) last(#n) a new array of up to n; clamps
do(block) the array, block per element
collect(block) a new array of the block’s answers
select(block) a new array of the elements the block accepted
inject(start, block) one value, folded left to right
sorted sorted(block) a new array, ascending or by the block
join(s) the strings with s between them; strict
xs := [#4, #1, #3].
xs:sorted:print.                            ; [#1, #3, #4]
xs:collect({ x | x:mul(#2) }):print.        ; [#8, #2, #6]
xs:select({ x | x:greaterThan(#2) }):print. ; [#4, #3]
xs:inject(#0, { a, b | a:add(b) }):print.   ; #8
xs:add(#9):size:print.                      ; #4   -- add answers the array

dictionary

Message Answers
new an empty dictionary
size an integer
at(key) the value; an error when the key is not there
at(key, default) the value, or default
atPut(key, value) the value stored, so it chains
includes(key) a boolean
remove(key) the value removed; an error when absent
keys values an array, in no order worth relying on
do(block) the dictionary, block once per value
keysAndValuesDo(block) the same, block taking a key and a value
dictionary:of(...) key then value, alternating; an odd count is an error
d := dictionary:new.
d:atPut("port", #8080).
d:at("port"):print.             ; #8080
d:at("host", "any"):display.    ; any
d:includes("port"):print.       ; true
d:size:print.                   ; #1

dictionary:of("port", #8080, "host", "any"):size:print.   ; #2

block

Message Answers
value(...) the block’s answer; the count must match its parameters
boundTo(receiver) a new block over the same code, with self set
whileTrue(body) doUntil(condition) repeat(#n) loops — see above
onError(handler) the block’s answer, or the handler’s if it failed
ensure(cleanUp) the block’s answer, having run cleanUp either way
timeToRun timeToRun(#n) seconds one run, or n runs, took, as a float
{ error:raise("no") }:onError({ e | e:message }):display.   ; no
{ #1:div(#0) }:onError({ e | "caught" }):display.           ; caught
cleaned := false.
r := { #2:add(#2) }:ensure({ cleaned := true }).
r:print.                                                    ; #4
cleaned:print.                                              ; true

object, and errors

Message Answers
new a fresh object delegating to the receiver
via(ancestor) a delegating view: lookup starts there, self stays
parent the prototype, or nil at the root; read-only
error:raise(text) never — it unwinds to the nearest onError
e:message the text an error was raised with

via is what super is elsewhere, except that it names what it overrides.

animal := object:new.
animal:speak := { "..." }.
dog := animal:new.
dog:speak := { self:via(animal):speak:concat("woof") }.
dog:speak:display.              ; ...woof
dog:parent:equals(animal):print.  ; true

time

An instant, held as nanoseconds since 1970. A value: two are equal when they name the same instant.

Message Answers
time:fromSeconds(f) an instant, from seconds since the epoch (on the class)
asSeconds seconds since the epoch, as a float
secondsSince(other) a float; negative when other is later
plusSeconds(f) another instant, f seconds along
year month day integers; January is #1
hour minute second integers
weekday an integer; Monday is #1, Sunday #7
asString ISO-8601 in UTC
asString(format) the format handed to strftime
lessThan(t) greaterThan(t) lessOrEqual(t) greaterOrEqual(t) a boolean
t := time:fromSeconds(946684800.0).
t:asString:display.                     ; 2000-01-01T00:00:00Z
t:year:print.                           ; #2000
t:weekday:print.                        ; #6   -- a Saturday
t:plusSeconds(86400.0):day:print.       ; #2
"2000-01-01T00:00:00Z":asTime:equals(t):print.   ; true

system

The process, rather than any value. One object with slots, not a class.

Message Answers
arguments an array of strings; empty when there were none
exit(#status) nothing — the program stops, #0 to #255
clock monotonic seconds as a float; only differences mean anything
time the current instant
write(text) writes a string to standard output, adding nothing — no newline
writeError(text) the same, to standard error — the only way to reach it
readLine readKey one line, or one byte, of standard input; nil at the end
readUpTo(#n) up to n bytes of it, as they were sent; nil at the end
isTerminal(which) whether 'input, 'output or 'error is a terminal
terminalSize a dictionary of "rows" and "columns", or nil off a terminal
keyWaiting(seconds) whether a byte is there to read, waiting up to that long
sleep(seconds) waits that long and answers nil; a float, like every duration here
load(path) true having run a compiled .sob here, false if already loaded
readFile(path) the whole file as a string; "/dev/stdin" reads a pipe too
readFile(path, #from, #count) count bytes from the one-based position from; short at the end, "" past it
writeFile(path, text) appendFile(path, text) nil, having written
fileExists(path) isDirectory(path) a boolean
fileSize(path) an integer, without reading the file; nil if nothing is there
fileId(path) which file is at that path, as a string; nil if nothing is there
filesIn(path) an array of the names in a directory
makeDirectory(path) true if it made one, false if it was there
remove(path) nil, having deleted a file or an empty directory
rename(from, to) nil, having moved it; replaces an existing to
modifiedAt(path) setModifiedAt(path, t) when a file was last written; nil if nothing is there
modeOf(path) setMode(path, #mode) the permission bits, #0 to #4095
environment(name) the variable, or nil when unset
run(argv) run(argv, streams) the exit status; argv is an array, never a command line
capture(argv) capture(argv, streams) a dictionary of "output" and "status"

run and capture take an array so that nothing in it is ever read as syntax. #127 is the status for no such command.

streams is an array of alternating name and value — "stdin", "stdout" or "stderr", then 'share, 'discard, 'merge (stderr only, and it follows stdout) or a path as a string. capture refuses "stdout", which is the one it keeps.

system:writeFile("note.txt", "hello").
system:readFile("note.txt"):display.        ; hello
system:fileExists("note.txt"):print.        ; true
system:fileSize("note.txt"):print.          ; #5
system:capture(["echo", "hi"]):at("output"):trim:display.  ; hi
system:capture(["echo", "hi"], ["stderr", 'discard]):at("status"):print.  ; #0
system:remove("note.txt").

random

A generator you make: random:new is seeded by the machine, random:new(#seed) repeats.

Message Answers
new / new(#seed) a generator — by the machine, or by you and repeatable
seed the integer it was made with; a slot, so slots shows it
upTo(#n) an integer #1 to #n, both included — the range an array is indexed by
between(#a, #b) an integer #a to #b, both included
fraction a float, at least 0.0 and less than 1.0
r := random:new(#20260824).
r:upTo(#6):print.                       ; #3
r:between(#10, #20):print.              ; #13
r:seed:print.                           ; #20260824
random:new(#7):upTo(#100):equals(random:new(#7):upTo(#100)):print.   ; true

The prototype answers none of these — random:upTo(#6) is an error, because a generator everything shares is what new exists to avoid. State on system would have given a VM a history; in an object, a program that never says random:new runs the same twice.

The library

Shipped .sol files on the search path — @include "name.sol". finds them without being told where they live.

File Binds For
control.sol integer:timesCollect(block), array:ifElseIf n results gathered; a chain of alternatives written flat
math.sol min max between on numbers; min max on arrays the comparisons written out by hand too often
text.sol integer:asUtf8 a code point as the bytes UTF-8 spells it
  string:startsWith string:endsWith a boolean; not indexOf(x):equals(#1), which searches the whole string to fail
shell.sol shell:run capture read line when the shell’s pipes and globs are the point
json.sol json:read json:write value:asJson JSON in and out
html.sol html:read, a tree with find, text, attribute HTML that recovers from bad markup
scan.sol scan:on, a cursor: peek next takeWhile since reading text a character at a time
re.sol re:on or re:ere, then find findLast matches replaceAllIn group regular expressions and substitution, POSIX basic and extended
@include "math.sol".
#3:min(#7):print.               ; #3
[4.0, 1.0, 9.0]:max:print.      ; 9
#5:between(#1, #10):print.      ; true
@include "json.sol".
v := json:read("{\"port\": 8080}").
v:at("port"):print.             ; #8080
v:asJson:display.               ; {"port":8080}

Running it

solas prog.sol              # compile to prog.sob
solas prog.sol -o out.sob   # somewhere else
solvm prog.sob              # run it
solvm prog.sob a b          # with arguments, seen as system:arguments
solvm --trace prog.sob      # write the call tree
solvm --steps N --memory N  # bound it
solis                       # the REPL
solid prog.sob              # the debugger: step, next, break, print
solid --exports lib.sob     # what it binds, and what may be sent to it
solid --exports --extension=x.so
                            # the same for a C extension

What is deliberately not in the language is in ROADMAP.md section 3. Why any of it is shaped this way is in design.md, and lineage.md places it against Smalltalk, Self, Io, Lua and Ruby.