S₀ is the lowest level “machine language” that Swanson hosts execute. It is primarily intended as a translation target, and not meant to be written directly. It doesn’t even have a textual language for writing it! It only has a binary format, SBL.
S₁, on the other hand, is Swanson’s “assembly language”. It’s still quite low-level, mapping very closely to S₀ and the Swanson execution model. It adds several convenience instructions, most of which desugar directly into one or more S₀ instructions.
Other than that, S₁ purposefully does not provide very many other higher level affordances, so that it is simple to write a bootstrap translator from S₁ to S₀.
Whitespace separates tokens but is otherwise ignored. A comment starts with
# and continues through the end of the line.
Swanson names contain arbitrary binary data. A bare S₁ name can contain ASCII
letters, digits, and any of _.@$!. Other names are written in double quotes.
Quoted names support escapes, including \\, \", \n, and \xNN.
An S₁ source file defines one module. It consists of the module keyword, the
module’s name, and a single branch of S₁ code:
module example {
>:drop₁ ~
:₃
}
(This example contains some other S₁ constructs that we haven’t described yet; it will make more sense after we get through the following sections.)
An S₁ module is a valid Swanson unit. When loaded, the module’s branch will be executed with a loader on stack 1, and a continuation on stack 3. The module can use the loader to load other Swanson units that it depends on. It should leave a single value on stack 0, and return control to the continuation. That value is what represents the loaded module to other Swanson units.
The four stacks are written with Unicode subscripts: ₀, ₁, ₂, and
₃. Most S₁ instructions end with the stack that they operate on. The
translator emits the necessary S₀ instructions to move values to/from the stack
that you wish to operate on.
An explicit move is written as a source stack, followed by to, followed by a
target stack. This instruction moves the top value from stack 0 to stack 2:
₀to₂
Square brackets create a literal from a list of hexadecimal octets, separated by whitespace. Parentheses create a literal whose binary content comes from an S₁ name. (As with all names, use double-quotes if the name contains a few non-ASCII characters, and you don’t want to use the more opaque hex-octet form.) Both forms end with the target stack:
[00 ff]₂
(hello)₀
("hello world")₁
The following instructions apply the corresponding S₀ operation to the indicated stack:
newtag₀
droptag₀
lock₀
unlock₀
nil₀
dropnil₀
cons₀
uncons₀
A quotation instruction constructs an invokable whose branches are written in S₁. The simplest form has one branch with the empty name:
{
:₃
}
By default, the new quotation is pushed onto stack 3. Use quoteₙ to select a
different target stack:
quote₂ {
:₃
}
Prefix a branch with & to give it a name. Place named branches next to each
other to construct an invokable with more than one branch:
quote₂
&first {
:₃
}
&second {
:₃
};
The semicolon marks the end of the quotation’s branches. A quotation can also
have a name, written with % before its branches:
quote₂ %example {
:₃
}
Every branch must end with an invocation. :nameₙ invokes the name branch
of the invokable on top of stack n. Leave out name to invoke the empty
branch, as in :₃.
Prefix a quotation with encloseₙ to construct a closure. The instruction
pops the enclosed value from stack n and pairs it with the new quotation:
enclose₁ quote₂ {
:₃
}
The closure itself is pushed onto the quotation’s target stack, which is stack 2 in this example.
An upquote refers to a lexically enclosing quotation instead of defining new branches. The number after the target stack is the lexical depth; 0 selects the immediately enclosing quotation:
upquote₂ 0
An upquote can also be enclosed, producing a closure around the selected quotation:
enclose₁ upquote₂ 0
buryₙ depth moves the top value of stack n below the next depth
values: (note that the top of stack is always on the right)
# ⁰a b c
bury₀ 2
# ⁰c a b
Similarly, digₙ depth moves the value depth places below the top of
stack n to the top:
# ⁰a b c
dig₀ 2
# ⁰b c a
A depth of 0 makes either instruction a no-op. Both instructions expand into S₀ move instructions.
packₙ size pops size values from stack n and pushes one list
containing those values in the same order:
# ⁰a b c
pack₀ 3
# ⁰[a b c]
unpackₙ size performs the inverse operation. It pops a list containing
exactly size values and pushes those values back onto stack n:
# ⁰[a b c]
unpack₀ 3
# ⁰a b c
unpack halts and catches fire if the list does not contain
exactly the requested number of values. Packing or unpacking zero values
creates or consumes an empty list, respectively.
The Swanson execution model is inherently based on continuations: to invoke an invokable and then do something else after it’s done, you have to wrap the “after” logic in a continuation, which you pass in as an input. That can cause S₁ to look “inside out”. For instance, with what we’ve learned so far, the S₁ equivalent of the following pseudocode:
if 10 < 3
print "Math is broken"
else
print "Math is good"
would be:
# ¹uint_constants ³print
{ # ⁰ten ¹uint_constants ³print
{ # ⁰ten three ¹uint_constants ³print
{ # ⁰is_lt ¹uint_constants ³print
₀to₁ # ¹uint_constants is_lt ³print
&true { # ¹uint_constants ³print
{ # ³print
("Math is broken")₁ # ¹message ³print
:print₃
} # ¹uint_constants ³print κ
:drop₁
}
&false { # ¹uint_constants ³print
{ # ³print
("Math is good")₁ # ¹message ³print
:print₃
} # ¹uint_constants ³print κ
:drop₁
} # ¹uint_constants is_lt ³print κ
:evaluate₁
} # ⁰ten three ¹uint_constants ³print κ
:lt₀
} # ⁰ten ¹uint_constants ³print κ
:three₁
} # ¹uint_constants ³print κ
:ten₁
This makes it very hard to follow the flow of logic in the code. To make this less painful, S₁ provides two pseudo-instructions, named “slip” and “slurp”.
Every instruction can be “slipped” by preceding it with one or more >
characters. The slipped instruction is moved past one following instruction for
each >. For example:
>:print₃ (message)₁
is equivalent to:
(message)₁
:print₃
The literal is one S₁ instruction, even though it expands into more than one S₀ instruction. Slip operates on S₁ instructions before they are expanded into S₀.
A slurp is written as ~. It starts a quotation branch without introducing
another level of braces. The branch continues through the remainder of the
enclosing branch, so there is no closing brace for the slurped branch. For
example:
>:drop₁ {
:₃
}
can be written as:
>:drop₁ ~
:₃
Slurp is most useful with slip, when an invocation receives its continuation as its last input.
Together, slip and slurp let us transform the earlier example into:
# ¹uint_constants ³print
>:ten₁ ~ # ⁰ten ¹uint_constants ³print
>:three₁ ~ # ⁰ten three ¹uint_constants ³print
>:lt₀ ~ # ⁰is_lt ¹uint_constants ³print
>>:evaluate₁
₀to₁ # ¹uint_constants is_lt ³print
&true { # ¹uint_constants ³print
>:drop₁ ~ # ³print
>:print₃ ("Math is broken")₁
}
&false { # ¹uint_constants ³print
>:drop₁ ~ # ³print
>:print₃ ("Math is good")₁
}
This is entirely equivalent in terms of what S₀ it gets translated into, but the control flow is more obvious.