The SLC Programming Language
SLC is a programming language. A program is a command: it sends a value to a
continuation, and that meeting is a step of the program. Its core follows the
classical λ̄μμ̃ calculus (lambda-bar-mu-mu-tilde). Source files end in .sl.
The command-line tool is slc.
There is one grammar. A type denotes a value or a continuation, and the arrow
says which way a function faces, so the same program can be written
value-first or continuation-first. Values and
Continuations are those two halves.
Data gives them the same shapes: data and form,
enum and menu.
This book is the tutorial and the reference for the language implemented in
the SLC repository. The specification is
DESIGN.md, with the
parts under
docs/design/. Where
this book and the specification disagree, the specification and the compiler
decide.
The programs printed in the tutorial live in
book/examples/.
book/check.sh runs them and compares the output.
Two ways through one pipeline
A function written with -> is given a value and returns a value.
func area(s: Shape) -> i64 {
of s {
Circle(r) => <(3, r) | mul | x => (x, r) | mul,
Rect(w, h) => <(w, h) | mul,
}
}
of s takes the shape apart. With 3 standing in for π, the circle of radius
5 has area 75.
A function written with <- is given a continuation of the result and
returns a continuation of the input.
func area_of(out: i64) <- Shape {
mu Shape {
Circle(r) => <(3, r) | mul | x => (x, r) | mul | out>,
Rect(w, h) => <(w, h) | mul | out>,
}
}
out: i64 is the continuation of the area. mu Shape builds the
continuation of the shape, and each arm sends the area to out.
< opens a chain with a value, and | carries that value from left to
right. A chain reads each stage in the orientation that stage was written
with, so both functions sit in the same pipeline. Both
arrows runs this program.
The whole dual example in the repository is
examples/duality/two_styles.sl.
Getting started
SLC is a Cargo workspace. Building it produces the slc binary.
git clone https://github.com/KeenS/slc.git
cd slc
cargo build
The binary is target/debug/slc. A release build is cargo build --release,
and the binary is then target/release/slc. Either one is what the rest of
this book means by slc. cargo run -- from the repository root runs the
debug binary and passes the words after -- to it.
Run, compile, check, and format
slc run examples/basics/hello.sl
slc check examples/basics/hello.sl
slc fmt examples/basics/hello.sl
slc run checks the program and then runs it. By default it compiles to a
native x86-64 ELF. On any other host it uses the interpreter.
slc run --interpret evaluates the same program on the abstract machine.
Flags belong before the file name. Words after the file are the program's
own arguments.
slc compile writes that executable and does not run it. -o names the
file. With no -o, the name is the source stem in the current directory.
--fuel N is the executable's step bound. The words passed to the executable
are the program's arguments.
slc compile -o hello examples/basics/hello.sl
./hello
slc check reports what run would report before evaluating, and evaluates
nothing. A file with no main checks as a library. The command exits
non-zero when a file fails.
slc fmt rewrites a file into the one layout: four-space indentation, lines
within 100 columns, a semicolon after every statement except a block's last
expression, and a comma after every of or mu arm. --check fails when a
file would change and writes nothing. --stdout prints the result.
slc fmt --check examples/basics/*.sl
slc fmt --stdout examples/basics/hello.sl
slc run --fuel N stops after N steps. With no --fuel, a run is bounded by
memory. --fuel 0 runs no SLC code.
The version line is slc --version.
What a program is
A program is one or more .sl files. The root file declares main. The
prelude is in every program: Bool, arithmetic, println, and the IO
effect. The standard library is reached by a path, such as list::length, or
brought in with cite.
The next chapter is the smallest program that prints a line and exits.
A first program
// The smallest SLC program.
proc main | (exit: i32) / {IO} {
<"Hello, SLC!" | println;
<0 | exit>
}
Run it from the repository:
slc run book/examples/hello.sl
It prints
Hello, SLC!
and exits with status 0.
The entry point
proc main is the entry point. It takes no values and one continuation,
exit. The runtime supplies that continuation. The integer sent to exit is
the process status.
{IO} is the effect of printing. The runtime handles it. A main that only
sends a status, and prints nothing, can leave the row off. A func main, a
main with value parameters, and a main inside a section are refused. The
diagnostic names the shape proc main | (exit: -i32) / {IO}.
Every terminating path of main sends a status to exit. Falling off the
end is a type error. There is no separate exit value sitting beside main:
ending the program is the continuation main was given, and a helper receives
it only when main passes it on.
A chain
< opens a chain with a value. | carries that value to the next stage.
<"Hello, SLC!" | println sends the string to println. The semicolon ends
the statement, and the block continues. The chain has no closing >, so it
is an application: println returns the unit value (,) after writing the
line.
<0 | exit> is a cut. The > delivers 0 to the continuation exit and
does not come back. A cut is how a command ends.
A chain says what it is at both ends. < makes it start from a value, >
makes it deliver to a continuation, and the two together are a cut. "Hello, SLC!" | println is refused, because a string is a value and a chain without
< starts with a function. <0 alone is refused, because the value has
nowhere to go.
println renders its argument through Display and performs IO. print
is the same operation without a trailing newline.
Values
A value is data: a number, a string, a boolean, a tuple, a record. This program uses each of them and prints one line per result.
// Literals, tuples, records, strings, and comparisons.
data Point { x: i64, y: i64 }
proc main | (exit: i32) / {IO} {
<(2, 3) | add | println;
<("Hello, ", "SLC") | add | println;
<True | not | println;
let (a, b) = (10, 20);
<(a, b) | add | println;
let p = Point { x: 1, y: 2 };
<(p.x, p.y) | add | println;
<'A' | println;
<1.5 | println;
let text = <("Hello, ", "SLC") | add;
<(text, 7) | index | println;
<(text, 7, 10) | substring | println;
<(3, 5) | lt | println;
<0 | exit>
}
5
Hello, SLC
false
30
3
A
1.5
S
SLC
true
Literals
An integer literal takes the integer type its context requires. Sent to
exit, 0 is an i32. With nothing else to go on, it is an i64. A
floating literal takes f32 or f64 from its context and is an f64
otherwise. The widths are i8, i32, i64, u8, u32, and u64, and the
floats are f32 and f64.
A minus sign that touches its digits is part of the literal: -1, -1.5.
Negating a computed value is the function neg, as in <x | neg.
A string literal is a String. A character literal is a char. The escapes
are \n, \t, \r, \0, \\, \", and \'.
True and False are the variants of Bool, which is an ordinary enum in
the prelude. Negation is <b | not. A choice on a Bool is an of:
of ready {
True => <"yes" | println,
False => <"no" | println,
}
(,) is the unit value, and an empty block is the same value. Its type is
written (,). The name unit is that type as well.
Arithmetic and comparison
Arithmetic and comparison are functions. A pair flows into the function:
<(2, 3) | add
<(10, 4) | sub
<(6, 7) | mul
<(20, 5) | div
<(7, 3) | rem
<(3, 5) | lt
The same pattern is eq, ne, gt, le, and ge. The pair is one
argument. <1 | add does not wait for a second operand.
Division by zero and an overflowing arithmetic result are runtime errors.
Signed integers and the floats also have neg. Unsigned widths have no
neg.
String implements Add, so <("Hello, ", "SLC") | add concatenates.
Let, tuples, and records
let binds a pattern. A name is the pattern that matches any value of its
type. A tuple pattern takes a tuple apart, and a record pattern takes a
record apart. The pattern has to match every value of the type, because
there is no second arm. A pattern that can fail belongs in an of.
let (a, b) = (10, 20);
let p = Point { x: 1, y: 2 };
Point is a data declaration. Constructing it names every field.
.0 reads a tuple component, counting from zero. .x reads a record field.
(10, (20, 30)) has two components, and its .1 is the pair (20, 30).
A tuple type is (i64, i64). A longer tuple is (A, B, C). The separators
are part of the type, as the types reference
explains.
Strings
index returns the char at a zero-based position. substring returns the
characters from the start index up to, and not including, the end index.
<(text, 7) | index on "Hello, SLC" is 'S', and <(text, 7, 10) | substring is "SLC". An index or a slice outside the string is a runtime
error.
str_len is the number of characters. The builtins
page lists the rest of the string operations.
Showing a value
println calls Display. Numbers print as digits, Bool prints true and
false, a string prints its characters, and (,) prints (,). to_string
is the same rendering as a String, and fmt is the method underneath both.
Functions
A func takes data and returns data. The parameter list is one group, in
the order written, and a call supplies that whole group.
// Returning functions, a lambda, and a generic.
func double(n: i64) -> i64 {
<(n, 2) | mul
}
func greet(name: String) -> String {
<("Hello, ", name) | add
}
func id<+T>(x: T) -> T { x }
proc main | (exit: i32) / {IO} {
<21 | double | println;
<"SLC" | greet | println;
let square = fn(n: i64) -> i64 { <(n, n) | mul };
<6 | square | println;
<7 | id | println;
<"seven" | id | str_len | println;
<0 | exit>
}
42
Hello, SLC
36
7
5
Declaration and call
func double(n: i64) -> i64 {
<(n, 2) | mul
}
-> i64 is the result. <21 | double sends one integer in. A function of
several parameters takes them as one tuple, in the declared order:
func greet(name: String) -> String {
<("Hello, ", name) | add
}
<"SLC" | greet is the call. <("Hello, ", "SLC") | greet would be a type
error, because greet takes one String. Partial application is a type
error as well: the group arrives whole, or the call is refused.
The body is a block. Its value is the last expression. A semicolon separates statements before that expression.
A parenthesized call double(21) is refused for a named function. The
diagnostic tells you to flow the argument in. Two spellings do use
parentheses: a nullary call, f() or <(,) | f, and a constructor,
Shape::Circle(5). The name f alone is the function, and naming it runs
nothing.
Lambdas
fn is a function with no declaration name.
let square = fn(n: i64) -> i64 { <(n, n) | mul };
<6 | square | println;
A lambda parameter needs a polarity the checker can see. A type annotation
supplies it. Immediate application of a lambda keeps the parentheses the
declaration does not: <fn(x: i64) -> i64 { x }(42) | println.
Generics
A type parameter is declared in angles, with a sign. + means the parameter
stands for a value type.
func id<+T>(x: T) -> T { x }
Each call chooses its own T. <7 | id is an integer, and <"seven" | id
is a string. Inside the body, T is rigid: the function cannot inspect it
except through a trait bound, which the traits chapter adds.
A let generalizes a value, such as a lambda or a variant that carries no
payload. A computation stays monomorphic. The usual way to name a polymorphic
function is a func declaration.
Effects on the arrow
A function that performs an effect says so after the result:
func greet(name: String) -> (,) / {IO} {
<name | println
}
A bare -> is an empty effect row: the function performs nothing. The
effects chapter is where a row is handled.
Continuations writes the other half of a cut: the continuation a value is sent to.
Continuations
A continuation is what a value is sent to. exit in
the first program is one: the runtime supplies it, and
the integer sent to it is the process status. This program builds
continuations and sends values to them.
// A continuation receives a value. mu names one. -T is its type.
func emit(text: String, out: -String) -> (;) {
<text | out>
}
func sum_into(out: i64) <- (i64, i64) {
mu (i64, i64) {
(a, b) => <(a, b) | add | out>,
}
}
proc main | (exit: i32) / {IO} {
<mu i64 { out <= <(2, 3) | add | out> } | println;
<mu String { out <= <("Hello, ", "SLC") | add | out> } | println;
<mu String { out <= <("SLC", out) | emit> } | println;
<(6, 7) | sum_into | println;
<"shown" | fn(message: String) -> (;) {
<message | println;
<0 | exit>
}>
}
5
Hello, SLC
SLC
13
shown
Naming one
mu i64 { out <= … } names the continuation around that expression out.
The body sends an i64 to out. The value out receives is the result of
the mu.
<mu i64 { out <= <(2, 3) | add | out> } | println
<(2, 3) | add is 5. The cut <5 | out> delivers it. println receives
that 5. mu String { … } is the same shape for a string, and the second
line prints Hello, SLC.
The arm arrow is <=. A request for this surrounding continuation arrives
in the arm, and out is the continuation the request carries.
A continuation parameter
out: -String is a continuation of a String. The type under the minus is
what the continuation accepts. emit sends its text there:
func emit(text: String, out: -String) -> (;) {
<text | out>
}
The body is a cut, so it has type (;). It does not come back to emit.
The caller names where the text goes:
<mu String { out <= <("SLC", out) | emit> } | println
out is the continuation of that mu String. emit sends "SLC" to it,
the mu results in "SLC", and println prints it.
-String and String -> (;) are the same type. The positive spelling
out: String on a <- function is the same continuation again: the type
written after the name is what it accepts. sum_into is written that way.
A continuation of several values
A tuple arrives whole, so a continuation of a tuple has one arm and binds every component:
func sum_into(out: i64) <- (i64, i64) {
mu (i64, i64) {
(a, b) => <(a, b) | add | out>,
}
}
out: i64 is the continuation of the sum. <- (i64, i64) is the
continuation this function returns, a continuation of the pair. The arm
arrow is => because the pair arrives as a value. The arm sends the sum
with a cut. <(6, 7) | sum_into sends the pair in, and the sum 13 reaches
println.
Data names these shapes. A form is a continuation that wants
every field, the way sum_into wants both components. A menu answers one
item the continuation picks.
Writing one with fn
A function whose body is a cut is a continuation. -String is its type.
fn(message: String) -> (;) {
<message | println;
<0 | exit>
}
println runs, then the cut sends 0 to exit. The call closes with >,
because the function is the continuation the string is delivered to:
<"shown" | fn(message: String) -> (;) {
<message | println;
<0 | exit>
}>
That line prints shown and ends the program. println and exit perform
IO when the string is delivered. main declares that row.
Leaving early
An arm that cuts to a continuation has type (;). Another arm can return
(,), and the statements after the of still run on that path. The cut
does not come back.
// A cut leaves the function. The other arm returns, and the caller goes on.
cite list::List;
cite list::List::*;
func first_even(xs: List<i64>, found: -i64) -> (,) {
of xs {
Nil => (,),
Cons(n, rest) => of (<(n, 2) | rem) {
0 => <n | found>,
_ => <(rest, found) | first_even,
},
}
}
proc main | (exit: i32) / {IO} {
let xs = Cons(1, Cons(2, Cons(3, Nil)));
<mu i64 {
out <= {
<(xs, out) | first_even;
<0 | out>
},
} | println;
let ys = Cons(1, Cons(3, Nil));
<mu i64 {
out <= {
<(ys, out) | first_even;
<0 | out>
},
} | println;
<0 | exit>
}
2
0
first_even walks a list. The even arm sends the number to found and
leaves. The odd arm returns (,). The caller names the surrounding
continuation out. On [1, 2, 3] the cut carries 2, so the 0 after
the call is not sent. On [1, 3] the function returns and the caller sends
0.
The same early cut, with the rest of the work written after the of, is how
examples/programs/json_parser.sl
is written.
Both arrows writes one function with -> and the same function
with <-.
Data
Values and continuations meet in a cut. Four declarations name the shapes of that meeting.
data and form carry every field. A data value gives the fields. A
form is a continuation that wants the fields.
enum and menu carry one alternative. An enum value is the variant it
picked. A menu value answers the one item the continuation picks.
Each declaration has an anonymous spelling. A tuple is (A, B), a choice is
(A | B), a bundle is (A & B), and a joint is (A ; B).
// data carries every field. form wants every field.
// enum is one variant. menu answers one item.
data Pair {
left: i64,
right: i64,
}
form Total {
left: i64,
right: i64,
}
enum Colour {
Red,
Green,
}
menu Config {
retries: i64,
name: String,
}
func sum(p: Pair) -> i64 {
of p {
Pair { left, right } => <(left, right) | add,
}
}
func total(out: -i64) -> Total {
mu Total {
Total { left, right } => <(left, right) | add | out>,
}
}
func name(c: Colour) -> String {
of c {
Red => "red",
Green => "green",
}
}
func config() -> Config {
mu Config {
retries <= <3 | retries>,
name <= <"SLC" | name>,
}
}
proc main | (exit: i32) / {IO} {
let p = Pair { left: 2, right: 40 };
<p.left | println;
<p | sum | println;
<mu i64 { answer <= <Total { left: 2, right: 40 } | (<answer | total)> } | println;
<Colour::Green | name | println;
<config().retries | println;
<config().name | println;
<0 | exit>
}
2
42
42
green
3
SLC
data
A data declaration names a value that carries every field.
data Pair {
left: i64,
right: i64,
}
Pair is the name of the type. Each line inside the braces is one field: a
name, then the type of the value stored there. left stores an i64, and
so does right. The list order is the order later code writes.
Building a value
Pair { left: 2, right: 40 } is a Pair. The literal names every field,
each expression has that field's type, and the names follow the declaration.
The program binds it to p.
Leaving right out is refused:
`Pair` has fields `left`, `right`; the literal writes `left`
Writing right before left is refused:
`Pair` has fields `left`, `right`; the literal writes `right`, `left`
Reading one field
p.left is the left field, an i64. The program prints 2. p.right
is the other field. A name that the declaration does not list is refused:
`Pair` has no field `middle`
Taking the value apart
of p takes the whole value apart. A data value has one shape, so there
is one arm, and the arm binds every field:
of p {
Pair { left, right } => <(left, right) | add,
}
A bare field name binds a variable of that name. Pair { left: x, right }
binds the first field as x and the second as right. The fields in the
pattern follow the declaration. Reversing them is refused:
`Pair` has fields `left`, `right`; the pattern writes `right`, `left`
The sum of 2 and 40 is 42.
A parameter can open the value in the header. The pattern is that one parameter, and it matches every value of the type:
func skew((a, b): (i64, i64), c: i64) -> i64 {
<(a, c) | mul | x => (x, b) | sub
}
data Point { x: i64, y: i64 }
func norm(Point { x, y }: Point) -> i64 {
<(x, x) | mul | z => (z, <(y, y) | mul) | add
}
examples/basics/patterns.sl
runs these, and binds a tuple and a Point with let the same way.
The tuple
A tuple (2, 40) is this connective with the name left off. Its type is
(i64, i64). A longer tuple is (A, B, C). Nesting matters: (10, (20, 30)) has two components, and its .1 is the pair (20, 30). .0 reads a
component, counting from zero. The unit (,) is the product with no fields,
and (,) is its value. The name unit is that type as well.
enum
An enum declaration names a value that is one variant.
enum Colour {
Red,
Green,
}
enum Shape {
Circle(i64),
Rect(i64, i64),
}
Red and Green carry no payload. Colour::Green is a Colour.
Circle carries an i64, so Shape::Circle is a constructor, and
Shape::Circle(5) is a Shape. Rect carries two integers. Several
payload types are one tuple, so the constructor takes (i64, i64), and the
pattern Rect(w, h) binds both components.
One arm for each variant
of c has an arm for each variant. The arm's pattern is that variant. Each
variant appears once, and the order of the arms does not matter. Green
produces "green", which the program prints.
Leaving a variant out is refused:
non-exhaustive of: missing variant `Green` of enum `Colour`
// An enum, an exhaustive of, a wildcard, and an inclusive range.
enum Shape {
Circle(i64),
Rect(i64, i64),
}
func area(s: Shape) -> i64 {
of s {
Circle(r) => <(3, r) | mul | x => (x, r) | mul,
Rect(w, h) => <(w, h) | mul,
}
}
proc main | (exit: i32) / {IO} {
<Shape::Circle(5) | area | println;
<Shape::Rect(6, 7) | area | println;
let n = of 2 { 0 => "zero", 1 => "one", _ => "many" };
<n | println;
let band = of 4 { 1..=3 => "low", 4..=6 => "mid", _ => "high" };
<band | println;
<0 | exit>
}
75
42
many
mid
Shape::Circle(5) builds a circle. area matches it and multiplies. The
circle arm uses a chain binder, x =>, to name the product <(3, r) | mul
before multiplying by the radius again. The area of a circle of radius 5,
with 3 standing in for π, is 75. The rectangle is 6 * 7, which is 42.
A continuation of a Shape is area_of in Both arrows. A
Shape value arrives in each arm there, so the arm arrow is =>, and each
arm sends the area on with a cut. An arm that is a bare value is refused.
mu Colour { Red => 0, … } reports:
a `mu` arm is a command; this one has type +i64
Literals, wildcards, and ranges
A wildcard _ covers whatever the earlier arms left. In of 2 { 0 => "zero", 1 => "one", _ => "many" } the wildcard is the remaining integers.
An inclusive range is start..=end. It takes the numeric type of the
scrutinee. 4 falls in 4..=6 and the arm produces "mid". A literal arm
and a range belong in an of. A mu arm covers one shape of a declaration,
and a nested of is where a literal is distinguished.
Or-patterns and @
// Or-patterns and an @ binding.
enum Hue { Red, Blue, Green }
func name(h: Hue) -> String {
of h {
Red | Blue => "cool",
Green => "green",
}
}
proc main | (exit: i32) / {IO} {
<Hue::Red | name | println;
<Hue::Green | name | println;
let tagged = of (1, 2) { n @ (a, _) => <(n.0, a) | add };
<tagged | println;
<0 | exit>
}
cool
green
2
Red | Blue is one arm covering two variants. @ binds the whole value and
still takes it apart: n @ (a, _) names the pair n and its first component
a. <(n.0, a) | add is 1 + 1.
A payload that is itself a record can be opened in the same pattern,
At(Point { x, y }). A sum nested in a payload stays in that payload.
Match it with an of inside the arm.
Constructors in a pattern are paths: Hue::Red, or Red after cite Hue::*;. The type name stays a separate cite when the signature also writes
Hue. cite Hue::*; brings the variants, and cite Hue; brings the type.
The early cut cites list::List and list::List::*
that way.
The choice
A choice (i64 | String) is an enum with the name left off. ::0(v) and
::1(v) are the alternatives, counting from zero, and ::0(x) is the
pattern. A longer choice nests to the right, and ::2(v) counts along that
nesting. The unit (|) is the choice with no alternatives, and it has no
value. An of on one has no arm.
examples/basics/sums.sl
builds choices with ::0 and ::1 and takes them apart.
menu
A menu declaration names a value that answers one item. The continuation
that uses the menu picks the item.
menu Config {
retries: i64,
name: String,
}
retries: i64 is an item whose answer is an i64. name: String is an
item whose answer is a String. A Config value has an answer for each
item. Using the value runs one of them.
Building a menu
config returns a Config. mu Config builds it. Each arm is one item,
and each item appears once. A request arrives in the arm, so the arrow is
<=:
mu Config {
retries <= <3 | retries>,
name <= <"SLC" | name>,
}
retries <= <3 | retries> names the continuation carried by that request
retries, and sends 3 to it. The other spelling names the continuation
on its own:
retries: out <= <3 | out>
The type after : is what the continuation accepts. Here it accepts the
item's answer, an i64. The picked arm is the one that runs.
Leaving an item out is refused:
non-exhaustive `mu Config`: missing items name
Writing the same item twice is refused:
`mu Config` has duplicate arm for `retries`
Picking an item
config().retries picks retries. The answer is the i64 3, and the
program prints it. config().name picks name and prints SLC.
A request
.retries(a) is the request itself: the item, and the continuation a
that receives the i64. Its type is -Config.
of takes a named request apart, one arm per item:
// of takes a request apart. Each arm answers with a request.
menu Config {
retries: i64,
name: String,
}
func config() -> Config {
mu Config {
retries <= <3 | retries>,
name <= <"SLC" | name>,
}
}
func reroute(k: -Config) -> -Config {
of k {
.retries(out) <= .retries(out),
.name(out) <= .name(out),
}
}
proc main | (exit: i32) / {IO} {
<mu i64 { a <= <config() | (<.retries(a) | reroute)> } | println;
<0 | exit>
}
3
reroute has an arm for each item. .retries(out) binds the continuation
the request carries, and the arm answers with .retries(out) again. The cut
<config() | (<.retries(a) | reroute)> sends the request to the menu.
reroute answers with that same request, and a receives 3.
A field with arguments
A field may take arguments. The field is then a function of those arguments.
The type after : is the function's result, and in an arm the name after
: is still the continuation that receives that result.
// A menu field is a function. The call supplies the arguments.
menu Pair {
both(x: i64, y: i64): i64,
}
func numbers() -> Pair {
mu Pair {
both(x, y): out <= <(x, y) | add | out>,
}
}
proc main | (exit: i32) / {IO} {
<numbers().both(2, 3) | println;
<0 | exit>
}
5
both(x: i64, y: i64): i64 declares the item. A call supplies (x, y), and
the result is an i64. The arm binds the arguments and then the
continuation:
both(x, y): out <= <(x, y) | add | out>
x and y are the arguments. out receives the sum. numbers().both(2, 3)
calls the function, and the sum is 5. The same call written as a stage is
<(2, 3) | numbers().both. The name both alone is the function.
both(x, y) <= … names that continuation both.
The bundle
A bundle (k1 & k2) is a menu with the name left off. The continuation
that uses it picks one item by position. The unit (&) is the menu with no
items, and (&) is its value.
examples/basics/sums.sl
sends a choice to a bundle of two continuations. The alternative's position
picks the continuation.
form
A form declaration names a continuation that wants every field.
form Total {
left: i64,
right: i64,
}
func total(out: -i64) -> Total {
mu Total {
Total { left, right } => <(left, right) | add | out>,
}
}
left: i64 and right: i64 name what flows in. mu Total builds the
continuation. A record arrives, so there is one arm, the arrow is =>, and
the arm binds left and right together. The arm is a cut: it sends their
sum to out.
total is a -> function. Its result is the Total. The caller gives it
a continuation of the i64 and gets the form back.
The record that feeds the form is Total { left: 2, right: 40 }. The field
names follow the declaration, as they do for Pair. Reversing them is
refused:
`Total` has fields `left`, `right`; the literal writes `right`, `left`
In the program the cut is:
<mu i64 { answer <= <Total { left: 2, right: 40 } | (<answer | total)> } | println
Read it from the continuation outward. answer is the continuation of the
i64. <answer | total gives total that continuation, and the result is
a Total. The record is sent to that Total. The sum 42 reaches
answer, and println prints it.
A form takes every field at once. A parameter t: Total is the form, and
t.left is refused:
`Total` is a form, and a form takes every field at once: send it one, `<Total { … } | …>`. A single field cannot be taken out of it — the others would have to be invented
Several statements, and a second form
An arm with several statements uses braces. When those statements perform
IO, the form's declaration carries that row.
// A form wants every field. An arm that performs IO belongs to the form's row.
form Report / {IO} {
value: i64,
label: String,
}
func printer(out: -i64) -> Report {
mu Report {
Report { value, label } => {
<label | println;
<value | out>
},
}
}
func shouting(next: Report) -> Report {
mu Report {
Report { value, label } => <Report { value: value, label: <(label, "!") | add } | next>,
}
}
proc main | (exit: i32) / {IO} {
<mu i64 { a <= <Report { value: 42, label: "answer" } | (<a | printer)> } | println;
<mu i64 {
a <= <Report { value: 7, label: "relabelled" } | (<a | printer | shouting)>,
} | println;
<0 | exit>
}
answer
42
relabelled!
7
Report / {IO} allows the arm to perform IO. printer prints label
and sends value to out. The first cut feeds Report { value: 42, label: "answer" }. The label is printed, and 42 reaches println.
shouting is a Report built from another Report. Its arm rebuilds the
record with "!" added to the label and sends that record to next. In
<a | printer | shouting, next is the Report that printer returned,
so the label is printed with the extra "!" and the value 7 reaches
println.
examples/duality/form.sl
is the same pair of forms.
The joint
A joint is a form with the name left off. (-i64 ; -i64) is two
continuations of an i64, one for each component of a pair. (k1 ; k2)
builds that joint from the two continuations. Continuations
builds the same continuation in one arm, mu (i64, i64) { (a, b) => … },
where the pair arrives together. A joint has no pattern that splits it back
into k1 and k2. What is taken apart is the pair that feeds it.
// A joint sends the pair's first component to the first continuation.
proc main | (exit: i32) / {IO} {
<(6, 7) | (
fn(n: i64) -> (;) {
<n | println;
<0 | exit>
}
; fn(m: i64) -> (;) {
<m | println;
<1 | exit>
}
)>
}
6
(6, 7) is sent to the joint. Delivery starts with 6 and the first
continuation. That continuation prints 6 and cuts to exit with 0, and
the process status is 0.
The unit (;) is the joint with no fields. It is the type of a cut, and it
has no value.
Both arrows writes the <- version of total: a function
given a continuation of the sum, returning a continuation of a data value
or a tuple.
Both arrows
-> and <- are the two directions a function can face. They are different
types. A chain reads each stage in the direction that stage was written, so
both sit in the same pipeline, left to right.
// One area, written with -> and with <-.
enum Shape {
Circle(i64),
Rect(i64, i64),
}
func area(s: Shape) -> i64 {
of s {
Circle(r) => <(3, r) | mul | x => (x, r) | mul,
Rect(w, h) => <(w, h) | mul,
}
}
func area_of(out: i64) <- Shape {
mu Shape {
Circle(r) => <(3, r) | mul | x => (x, r) | mul | out>,
Rect(w, h) => <(w, h) | mul | out>,
}
}
proc main | (exit: i32) / {IO} {
<Shape::Circle(5) | area | println;
<Shape::Rect(6, 7) | area_of | println;
<0 | exit>
}
75
42
-> and <-
func area(s: Shape) -> i64 {
of s {
Circle(r) => <(3, r) | mul | x => (x, r) | mul,
Rect(w, h) => <(w, h) | mul,
}
}
func area_of(out: i64) <- Shape {
mu Shape {
Circle(r) => <(3, r) | mul | x => (x, r) | mul | out>,
Rect(w, h) => <(w, h) | mul | out>,
}
}
area is given a Shape and returns an i64. of s takes that value
apart, and the arm's value is the area.
area_of is given a continuation of an i64 and returns a continuation of
a Shape. out: i64 names the continuation it is given: the positive type
is what that continuation accepts. <- Shape is the continuation this
function returns. mu Shape builds it, one arm for each variant. A Shape
value arrives in the arm, so the arrow is =>, the same arrow mu Total
uses when Data builds a form. Each arm sends the area to out.
<Shape::Circle(5) | area | println sends the circle through area.
<Shape::Rect(6, 7) | area_of | println sends the rectangle through
area_of. In both chains the area reaches println.
The arrow on the function is the direction it faces. A function written with
-> returns a value, and that value may be a form or a menu.
Data builds both with mu: total returns a Total, and
config returns a Config. A function written with -> may also be given
a request and take it apart: reroute is given a -Config.
Continuations writes sum_into in the same shape as
area_of, for a tuple instead of an enum.
examples/duality/two_styles.sl
writes a whole program both ways.
examples/duality/polarity.sl
is each of the four places a polarity can be written.
examples/duality/connectives.sl
is data, enum, menu, and form in one program.
Commands
A proc is a computation that ends in a cut. It takes a value group, then a
menu of exits. Each exit is a continuation, and a terminating path reaches
one of them.
// A proc offers one exit per outcome. Returning functions make it yield.
proc choose(n: i64) | (nonneg: (-i64) & neg: (-i64)) {
of (<(n, 0) | lt) {
True => <n | neg>,
False => <n | nonneg>,
}
}
proc main | (exit: i32) / {IO} {
<4 | choose | (fn(n: i64) { n } & fn(n: i64) { 0 }) | println;
<-3 | choose | (fn(n: i64) { 0 } & fn(n: i64) { n }) | println;
<0 | exit>
}
4
-3
Exits
proc choose(n: i64) | (nonneg: (-i64) & neg: (-i64)) {
of (<(n, 0) | lt) {
True => <n | neg>,
False => <n | nonneg>,
}
}
The | in the header separates the values from the exits. nonneg and neg
are consumers of an i64. The body cuts the input to one of them. Writing
the positive type, (nonneg: i64 & neg: i64), names what each exit accepts
and means the same row.
A call supplies the value, then one bundle of exits:
<4 | choose | (fn(n: i64) { n } & fn(n: i64) { 0 }) | println
The two functions return. Closing the exits on returning functions makes the
command yield the chosen result, and the chain continues into println. The
non-negative arm yields 4. The negative arm of -3 yields -3.
Closing the exits on mu consumers that themselves cut makes the call a cut,
and that cut ends the block. The program above uses returning functions so
println stays outside the command.
A row is positional. The bundle is written in the order the header declared,
the width matches, and the types match. An extra exit, a missing exit, or a
swapped pair is a different row. Writing (nonneg: i64 & neg: i64) names
what each exit accepts; (-i64) names the consumer itself.
One outcome, one continuation
A result that can fail is a proc with one continuation per outcome. The
library's list::nth has found and missing. fs::read has ok and
failed. Exactly one of them runs.
<(xs, 2) | list::nth | (
mu i64 { n => <n | println; <0 | exit> }
& mu String { why => <why | println; <1 | exit> }
)>
The same shape is how main itself is typed: one exit, the status. A helper
ends the program only with a continuation it was given.
Anonymous sums carry the same choice as a value, for a program that keeps
the outcome and decides later. ::0(v) and ::1(v) are the alternatives,
counted from the left.
// An anonymous sum names each alternative by position.
func show(x: (i64 | String)) -> String {
of x {
::0(n) => <n | int_to_str,
::1(s) => s,
}
}
proc main | (exit: i32) / {IO} {
<::0(3) | show | println;
<::1("hi") | show | println;
<0 | exit>
}
3
hi
(i64 | String) is a sum with no declaration name. of matches ::0 and
::1. A longer sum is (A | B | C), and the positions count along it.
Traits
A spec names operations over an implicit Self. An impl gives them for
a type. A bound <+T: Show> lets a generic function call them. The method
is a function, and the value flows in: <x | show. Dispatch is resolved
while the program is checked.
// A spec, an impl, a bound, and a default method.
spec Show {
func show(self: Self) -> String;
}
impl Show for i64 {
func show(self: i64) -> String { <self | int_to_str }
}
func labelled<+T: Show>(label: String, x: T) -> String {
<(label, <x | show) | add
}
spec Kind {
func code(self: Self) -> i64;
func labelled_code(self: Self) -> String { <self | code | int_to_str }
}
enum Hue { Red, Blue }
impl Kind for Hue {
func code(self: Hue) -> i64 {
of self { Red => 1, Blue => 2 }
}
}
proc main | (exit: i32) / {IO} {
<("n=", 7) | labelled | println;
<Hue::Blue | code | println;
<Hue::Red | labelled_code | println;
<0 | exit>
}
n=7
2
1
A method
spec Show {
func show(self: Self) -> String;
}
impl Show for i64 {
func show(self: i64) -> String { <self | int_to_str }
}
The parameter is named self by convention. The impl repeats the signature
with Self replaced by i64. <7 | show selects that impl from the type
of 7.
A generic function states the bound and calls the method the same way:
func labelled<+T: Show>(label: String, x: T) -> String {
<(label, <x | show) | add
}
<("n=", 7) | labelled fixes T to i64. Several bounds on one parameter
are joined with +, as in <+T: Ord + Display>. Each bound is checked on
its own. A call that never fixes a trait parameter is refused.
One impl is allowed for a given trait, its arguments, and the implementing
type. A second impl Show for i64 is refused.
A default method
A method may have a body in the spec. An impl that omits it receives that
body, with Self substituted.
spec Kind {
func code(self: Self) -> i64;
func labelled_code(self: Self) -> String { <self | code | int_to_str }
}
impl Kind for Hue writes code only. <Hue::Red | labelled_code runs the
default, which calls code and prints 1. A default may call another method
of the same spec. The prelude's Eq.ne is the negation of eq, and an impl
may still override it.
Parameters and associated types
A spec may take type parameters. Self stays the value that flows in. Into
is the prelude's example: Into<+U> converts Self into U, and the type
expected of the call chooses U.
func as_i64(n: i32) -> i64 { <n | into }
<7 | as_i64 prints 7. The conversion keeps the number when it is exact
for the destination. A value that does not fit is an arithmetic overflow.
The widths and the float operations are the next program.
// Into keeps a number when it is exact for the destination.
func as_i64(n: i32) -> i64 { <n | into }
func widen(n: i64) -> f64 { <n | into }
proc main | (exit: i32) / {IO} {
<7 | as_i64 | println;
<4 | widen | println;
<9.0 | sqrt | println;
<-1.5 | abs | println;
<1.2 | floor | println;
<1.2 | ceil | println;
<0 | exit>
}
7
4
3
1.5
1
2
sqrt, abs, floor, ceil, sin, ln, and pow are methods of f32
and f64. num::abs is a separate function on i64.
An associated type is chosen by the impl:
spec Walk {
type Item;
func step(self: Self) -> Walk::Item<Self>;
}
A use writes Walk::Item<T>: the trait's arguments first, when it has any,
and the implementing type last. A bound can pin it, <+T: Walk<Item = i64>>.
The worked program is
examples/basics/associated.sl.
A supertrait is spec Rank: Eq, and an impl of Rank is accepted when Eq
is implemented for the same type. See
examples/basics/supertraits.sl.
The traits reference records the rules for dictionaries, overlapping impls, and associated types.
Effects
An effect is a set of operations a computation may perform. A hook names
them. Performing an operation suspends the computation and asks the nearest
handler that answers that operation. do installs the handler around one
expression.
// An exception that replaces the computation, and a reader that resumes.
hook Exn { func throw(message: String) -> i64; }
hook Reader { func config() -> i64; }
func checked_div(a: i64, b: i64) -> i64 / {Exn} {
of (<(b, 0) | eq) {
True => <"division by zero" | throw,
False => <(a, b) | div,
}
}
func scaled(x: i64) -> i64 / {Reader} {
<(x, config()) | mul
}
proc main | (exit: i32) / {IO} {
let safe = do (<(10, 0) | checked_div) hn { throw(m) => -1 };
<safe | println;
let ok = do (<(10, 2) | checked_div) hn { throw(m) => -1 };
<ok | println;
let reading = do (<7 | scaled) hn { config(): resume => <10 | resume };
<reading | println;
<0 | exit>
}
-1
5
70
Declaring and performing
hook Exn { func throw(message: String) -> i64; }
func checked_div(a: i64, b: i64) -> i64 / {Exn} {
of (<(b, 0) | eq) {
True => <"division by zero" | throw,
False => <(a, b) | div,
}
}
throw is an ordinary function in the scope of the hook. checked_div
names {Exn} on its arrow, so a caller can see the effect. A handler
discharges it:
let safe = do (<(10, 0) | checked_div) hn { throw(m) => -1 };
The clause binds the message and does not resume, so the handler's answer is
-1 in place of the division. The clause for a successful division resumes
nowhere, because throw never ran, and the body's 5 is the answer.
config is a nullary operation. Its clause names the resumption:
let reading = do (<7 | scaled) hn {
config(): resume => <10 | resume
};
<10 | resume supplies the operation's result. scaled multiplies 7 by
that 10, and the handler answers 70. A clause may resume once, more than
once, or not at all. The name resume is conventional; any name works in
that position.
The body's answer type is the handler's answer type when there is no
return clause. return(x) => e maps the body's value when the body
finishes through the normal result. Every operation clause produces that same
answer type, or leaves through a command.
Forwarding
A handler that names any operation of an effect covers every operation of
that effect. A handler that means to leave some of them to an outer handler
ends with _ => forward. The forwarding clause is last, and it has no
binder.
// A complete handler, then one that forwards the operation it does not answer.
hook Config {
func first() -> i64;
func second() -> i64;
}
func total() -> i64 / {Config} {
<(first(), second()) | add
}
proc main | (exit: i32) / {IO} {
let n = do total() hn {
first(): resume => <1 | resume,
second(): resume => <2 | resume,
};
<n | println;
let m = do (do total() hn {
first(): resume => <10 | resume,
_ => forward,
}) hn {
first(): resume => <0 | resume,
second(): resume => <3 | resume,
};
<m | println;
<0 | exit>
}
3
13
The first do answers both operations: 1 + 2 is 3. The inner do
answers first with 10 and forwards second. The outer handler answers
second with 3, so the sum is 13. Forwarding keeps the effect in the
outward row. The outer do is what discharges it.
Handlers as values
hn Reader { … } builds a handler and installs nothing. do expr reader
installs a handler that a let stored. The type of a stored handler is (A hn B / {E} / {F}): the body produces A, the answer is B, the handler
discharges the concrete effects E, and the clauses may still perform F.
An empty residual row is left off the type.
let reader: (i64 hn String / {Reader}) = hn Reader {
config(): resume => <10 | resume,
return(value) => <value | to_string
};
IO is the effect the runtime handles, declared in the prelude as write
and write_line. println performs it. A program may install its own IO
handler nearer the operation. main may leave {IO} undischarged; anything
else is handled before the runtime sees it.
fs::real, args::real, and clock::real are hands. A hand is inlined
at each do, as in do expr fs::real. Using the hand's name as a value is
an error that tells you to install it with do.
Bare reset expr is a delimiter. It bounds a captured continuation and
discharges no effect. It is distinct from the library hand control::reset,
which answers control::shift.
The worked effects programs start at
examples/effects/effects.sl.
The reference page is Effects and handlers.
Sections
A sect is a named scope. :: reaches into it. cite brings a name into
the current scope and does not run it. A declaration inside a section is
private unless it is marked pub. A declaration in no section is visible
everywhere, which is why a single-file program can ignore sections and why
the prelude is the prelude.
// A section keeps a helper private and publishes the rest.
sect geometry {
pub enum Shape {
Circle(i64),
Rect(i64, i64),
}
func squared(n: i64) -> i64 { <(n, n) | mul }
pub func area(s: Shape) -> i64 {
of s {
Circle(r) => <(3, <r | squared) | mul,
Rect(w, h) => <(w, h) | mul,
}
}
}
cite geometry::area;
proc main | (exit: i32) / {IO} {
<geometry::Shape::Circle(5) | area | println;
<geometry::Shape::Rect(6, 7) | area | println;
<0 | exit>
}
75
42
Inside geometry, Shape and squared are bare names. From main, the
enum is geometry::Shape and the public function can be cited:
cite geometry::area;
<geometry::Shape::Circle(5) | area | println;
squared has no pub, so geometry::squared from main is refused.
Privacy is the section and the sections nested in it.
cite geometry::Shape::*; brings the public variants in bare. cite geometry::*; brings every public member. A glob is the weakest way a name
arrives: an explicit cite and the section's own declarations win. Two globs
may offer the same name, and that is reported when the name is used.
A program's declaration of a prelude name shadows the prelude name.
A section in its own file
A sect with no body names a file:
sect shapes;
pub sect report;
The file holds the declarations themselves, with no sect shapes { … }
around them. The directory tree is the section tree.
| The declaration is in | sect name; reads |
|---|---|
the program, dir/main.sl | dir/name.sl |
a section file, dir/m.sl | dir/m/name.sl |
an inline section sect a { … } | one a/ further down |
There is no search path. Run and check the root file. The sample is
examples/programs/multi_file/.
main is declared at the root. A main inside a section is that::main,
and the entry point does not accept it.
The standard library is a set of sections, one per file: list, map,
fs, and the rest. Nothing in them is in scope until the program names it.
The next chapter does.
The library
The prelude is in every program. The standard library is one section per
file, and a name from it is a path or a cite. This program cites the list
operations it uses and reaches map, string, and option by path.
// Lists, an ordered map, a string builder, and Option.
cite list::List;
cite list::List::*;
cite list::range;
cite list::sum;
cite list::filter;
cite list::reverse;
func even(n: i64) -> Bool {
of (<(n, 2) | rem) {
0 => True,
_ => False,
}
}
proc main | (exit: i32) / {IO} {
let xs = <(1, 5) | range;
<xs | println;
<xs | sum | println;
<(even, xs) | filter | println;
<xs | reverse | println;
let m = <map::empty() | x => (x, "a", 1) | map::insert | y => (y, "b", 2) | map::insert;
<m | println;
let b = string::new();
let b = b.append("Hello");
let b = <", SLC!" | b.append;
<b.finish | println;
<option::Option::Some(3) | x => (x, 0) | option::unwrap_or | println;
<option::Option::None | x => (x, 0) | option::unwrap_or | println;
<0 | exit>
}
[1, 2, 3, 4, 5]
15
[2, 4]
[5, 4, 3, 2, 1]
{a: 1, b: 2}
Hello, SLC!
3
0
Lists
list::List<T> is the enum Nil and Cons(T, List<T>). cite list::List
brings the type in as List. cite list::List::*; brings Nil and Cons.
Citing the variants does not bring the name List, so a signature that
writes List<i64> cites the type as well, or writes list::List<i64>.
<(1, 5) | range is the inclusive list [1, 2, 3, 4, 5]. sum adds it.
filter keeps the elements for which the function returns True. reverse
reverses. length, append, map, take, drop, and fold are the same
kind of function. nth can miss, so it is a proc with found and
missing.
Display for a list is the bracket form printed above. The elements use
their own Display.
Maps and builders
map::Map<K, V> is a persistent ordered map, an AVL tree. insert and
remove return a new map and leave the old one unchanged. Keys use Ord.
Two keys are the same when neither is less. get offers found and
missing.
let m = <map::empty() | x => (x, "a", 1) | map::insert | y => (y, "b", 2) | map::insert;
The binder x => names the map so far and passes the triple insert
expects. The printed form is {a: 1, b: 2}.
string::new() is a persistent string builder, a menu. append answers the
next builder, and finish answers the String. b.append("Hello") demands
that field. <", SLC!" | b.append is the same demand written as a chain.
string::push renders any Display value and appends it.
option::Option<T> is None or Some(T). unwrap_or returns the carried
value, or the fallback when the option is None. map and and_then take
the function first. either::Either<L, R> is Left or Right, and neither
side means success.
The library reference lists every public operation: hash maps and sets, arrays, streams, sequences, files, arguments, the clock, the environment, sleep, and spawning a process.
Demand
A type is positive or negative on its own. Positive types are data: numbers, strings, tuples, records, enums. Negative types are functions, consumers, and menus. Where the computation sits decides when it runs.
- A positive computation in a
let, an argument, or a constructor runs when that constructor runs. - A negative computation stored in a binding, a field, a tuple component, or a variant payload waits until something demands it, and runs again on every demand.
- The handlers around the demand are the handlers that see its effects. Handling the binding earlier leaves the later demand untouched.
Call-by-need, memoized thunks, and an implicit cache are outside the language. A delayed computation runs afresh.
// let- keeps a computation and runs it at each demand. let+ runs it now.
hook Build { func build() -> i64; }
func make() -> i64 / {Build} { build() }
proc main | (exit: i32) / {IO} {
let- pending = fn { make() };
<"stored" | println;
let first = do (<(,) | pending) hn { build(): resume => <10 | resume };
let second = do (<(,) | pending) hn { build(): resume => <20 | resume };
<first | println;
<second | println;
let+ eager = do make() hn { build(): resume => <3 | resume };
<eager | println;
<0 | exit>
}
stored
10
20
3
let, let+, and let-
Plain let follows the polarity of the expression. When the polarity is
still unknown, the binding is refused until it is written let+ or let-.
let- holds a negative computation and reruns it on each demand. A positive
result under let- is refused. In the program, fn { make() } is a
function, so let- accepts it. The line stored prints before either call.
Each <(,) | pending runs make again, under the handler of that do. The
first demand answers 10. The second answers 20. Nothing is cached.
let+ evaluates now. let+ eager = do make() hn { … } runs make once,
under that handler, and binds the integer 3. let+ is shallow. It does
not walk into a negative value stored inside the result and force that too.
A by-name type is written (-> T / {E}). The blank domain is the unit of
the arrow. Forcing it performs E and produces T. The result may itself
be a function with a further effect row: (-> (i64 -> i64 / {Use}) / {Build}) performs Build when forced and Use when the function is
applied.
An explicit thunk
A delayed positive result has a menu in the library, lazy::Lazy<T, E>,
demanded with .force. lazy::of_delayed and lazy::to_delayed convert a
negative computation to and from (-> T / {E}) without running it, and
without caching it.
menu Lazy<*T, E> / {..E} {
force: T,
}
The * says the parameter accepts either polarity. The menu's row is the
row the force performs.
examples/laziness/delayed_and_lazy.sl
separates the Build phase from the Use phase.
examples/laziness/stream.sl
is the coinductive stream, produced only as far as a demand reads it.
Where to go next
The tutorial covered the surface a program uses every day: chains, values,
continuations, data, both arrows, commands, traits, effects, sections, the
library, and demand.
The reference half of this book is the lookup for the same language. The
specification remains
DESIGN.md.
Examples
Every file under
examples/ runs with
slc run, and the test suite checks its output.
examples/README.md
lists them.
| Directory | What it holds |
|---|---|
examples/basics/ | Literals, data, traits, the library |
examples/duality/ | Both writing styles, menus, forms, commands |
examples/effects/ | Handlers, rows, delimited control |
examples/laziness/ | Demand, streams, sequences |
examples/programs/ | A JSON parser, files, a regex menu, several files |
examples/errors/ | Programs the checker refuses, on purpose |
slc check examples/basics/*.sl is a useful sweep. Leave examples/errors/
out of that glob.
A longer program
examples/programs/json_parser.sl
parses by sending each outcome to a continuation, and it leaves a failing
check early so the rest of the function is the success path.
examples/programs/file_io.sl
reads and writes under fs::real.
examples/programs/regex_derivative.sl
is a regular expression as a menu.
Benchmarks and the editor
benches/ holds programs
with a fixed workload. Each prints one integer. benches/run.sh times a
check and a run and checks that integer.
editors/emacs/slc-mode.el
is an Emacs major mode for .sl files: highlighting, comments, and
indentation that agrees with slc fmt.
PLAN.md is what the
implementation still leaves open.
docs/MIGRATION.md
records the older spelling of each keyword. Those words are ordinary
identifiers now.
How to read this reference
The reference lists the surface the compiler accepts. Short programs in the
tutorial are included from book/examples/ and checked by book/check.sh.
A fragment in the reference shows a form and leaves the surrounding program
out; the specification's complete programs live in
docs/design/ and
are compiled by the test suite.
A signature is written the way a declaration writes it. A call supplies the value group by flowing into the function, as Flow describes.
The pages are:
| Page | What it answers |
|---|---|
| Lexical structure | Tokens, literals, keywords |
| Declarations | func, proc, data, enum, menu, form, spec, hook, hand, def, sect |
| Expressions | Blocks, of, mu, do, let, chains |
| Types | Primitives, arrows, connectives, rows, signs |
| Patterns | What an arm or a let may match |
| Flow | <, |, >, application, cuts |
| Polarity | When a computation runs |
| Effects and handlers | hook, do, hn, hand, rows |
| Traits | spec, impl, bounds, associated types |
| Sections and files | sect, cite, files |
| Prelude | Names in every program |
| Standard library | Sections cited by path |
| Builtins | Operations the compiler provides |
| The slc command | run, check, fmt |
| Diagnostics | Phases and runtime failures |
| Surface forms | The shapes, gathered in one place |
Lexical structure
A source file is Unicode text. The lexer reads one file at a time, so a string or a comment left open is reported in the file that opened it.
Comments
// to the end of the line
/* block comments nest /* like this */ */
Comments and blank lines are kept by slc fmt.
Identifiers
An identifier starts with a letter or _ and continues with letters,
digits, or _. Letters are Unicode letters.
return, if, else, true, and false are ordinary identifiers. True
and False are the variants of Bool. In a handler clause, return
followed by ( is the body's result: return(x) => e.
Keywords
These words are tokens:
| Word | Role |
|---|---|
func, proc, fn | Functions. func names one, fn is a lambda, proc is a command |
data, enum, menu, form | Type declarations |
spec, impl, for | Traits |
hook, do, hand, hn, reset | Effects and handlers |
of, mu, let | Matching, consumers, binding |
sect, cite | Sections |
def, pub, dual | A constant, visibility, the dual of a type |
let+ and let- are let with a sign touching the keyword.
The lambda stays fn. A named function is func. The migration note is
docs/MIGRATION.md.
Words it records, such as command and mod, are ordinary identifiers.
Literals
| Literal | Type when unconstrained | Example |
|---|---|---|
| integer | i64 | 42, 1_000, -7 |
| float | f64 | 1.5, -0.5 |
| string | String | "hello\n" |
| character | char | 'A', '\n' |
An integer or float literal takes a narrower type when the port it meets
requires one. A minus sign is part of the literal only when it touches the
digits. _ inside a number is ignored. A number that does not fit in 64
bits is a lex error.
String and character escapes are \n, \t, \r, \0, \\, \", and
\'. An unknown escape is a lex error.
Punctuation
Arrows: ->, <-, =>, <=. Paths: ::. Ranges in patterns: ..=.
The flow marks are <, |, and >. Type and effect rows use / and
.., as in / {IO} and ..E. +, -, and * are signs on a type, and
+ and - also mark let+ and let-. A - that touches a number is part
of that literal. @ binds a pattern: name @ pattern.
<= is the demand arrow, in a mu arm and in a chain binder k <= e.
!, %, ==, !=, >=, &&, ||, and the brackets ⟨ ⟩ are not
tokens.
Declarations
A file is a sequence of declarations. The order does not decide visibility: names in one section see each other, and a call may precede the declaration it names.
pub may precede a declaration that sits in a section. Without pub, the
declaration is visible to that section and the sections nested inside it.
func
func name(param: Type, …) -> Result / {Effects} { body }
func name<+T, …>(param: Type) -> Result { body }
func name(consumer: Type) <- Input / {Effects} { body }
The value parameters are one group. -> returns a value. <- produces a
consumer of Input; each parameter on a <- function is a consumer, and
the positive type written on it is what that consumer accepts. A bare arrow
has an empty effect row.
A type parameter carries a sign: + positive, - negative, * either.
An unsigned parameter is a row, used as ..E. Bounds are written on the
parameter, <+T: Eq + Display>.
fn
fn is an expression, listed here because it is the unnamed form of func.
fn(param: Type, …) -> Result { body }
fn { body }
A nullary lambda's name denotes the function. Calling it is f() or <(,) | f.
proc
proc name(value: Type, …) | (exit: Consumer, …) / {Effects} { body }
The body is a command. Every terminating path cuts to one of the exits. The
exit row is a bundle: positional, invariant in width and order. main is a
proc at the root with no value parameters and one exit, the status.
data and enum
data Name<+T> { field: Type, … }
enum Name<+T> { Variant, Variant(Type, …), … }
A data value carries every field. An enum value carries exactly one
variant. A variant with no payload is a value of the enum. A variant with a
payload is a constructor, and several payload types are one tuple. Type
parameters on data, enum, menu, and form take signs and do not take
trait bounds.
menu and form
menu Name<+T, E> / {..E} {
item: Type,
item(param: Type, …): Type,
}
form Name / {Effects} { field: Type, … }
A menu answers one item the consumer demands. A field with parameters is the
function of those parameters, and the answer type is that function's result.
A form wants every field at once. Both are built with mu. The effect row
on the declaration is performed when the value is demanded.
spec and impl
spec Name<+U>: Parent + Parent {
type Item;
func method(self: Self, …) -> Result / {Effects};
func method(self: Self) -> Result { body }
}
impl<+T: Bound> Name<Arg> for Type {
type Item = Concrete;
func method(self: Type, …) -> Result { body }
}
Self is the implementing type. A method body in the spec is a default,
copied into an impl that omits it. See Traits.
hook, hand, and hn
hook Name<+T, E> {
func operation(arg: Type, …) -> Result;
}
hand name / {Residual} {
operation(arg): resume => body,
_ => forward,
}
A hook declares an effect. A hand declares a handler and is not a value;
each do expr name inlines it. hn is an expression that builds a handler
value. See Effects and handlers.
def
def NAME: Type = expression;
def binds a constant. The name sits on neither side of the value and
continuation mirror, which is why the keyword is def.
sect and cite
sect name { declarations }
sect name;
cite path::name;
cite path::*;
cite Enum::{Variant, Variant};
See Sections and files.
Expressions
Blocks
A block is zero or more statements and a final expression:
{
let x = 1;
<(x, 2) | add
}
A semicolon follows every statement except the block's last expression. The
last expression is the block's value. An empty block is (,). A block whose
last expression is a cut is a command.
A chain ends at the ; or } of the block, and at the , or ) that
closes a component. A component may itself be a chain that opens with <.
let
let pattern = expression;
let+ pattern = expression;
let- pattern = expression;
The pattern is irrefutable. let+ and let- are written with the sign
touching let. Polarity is Polarity.
of
of scrutinee {
pattern => expression,
pattern => expression,
}
An arm is a pattern, then => for data or <= for a demand, then a body.
Arms cover the scrutinee exactly once. There is no guard between the pattern
and the arrow. A test is an of in the arm. The scrutinee's braces, when it
has them, are parsed as the of arms, so a record scrutinee is parenthesized
or bound with let first.
An arm that cuts has type (;) and does not fix the type of the of.
mu
mu Type { pattern => command, … }
mu Type { copattern <= command, … }
mu Type { name <= command }
mu Type {}
=> builds a consumer of Type. <= with a copattern answers a menu or a
form. <= with one name captures the surrounding continuation and produces
Type. Empty braces are the consumer of a type that has no values.
A menu copattern follows the field. item: out <= body binds the
continuation. item(param): out <= body binds the arguments and then the
continuation. item(param) <= body binds the continuation under the field's
name.
A mu arm that matches data ends in a command: a cut to a consumer in
scope.
Calls and constructors
A named function is applied by flow: <argument | function. Several
parameters are one group: <(a, b) | function. A nullary function is
function() or <(,) | function.
A constructor keeps its parentheses: Cons(head, tail), Point { x: 1, y: 2 }. A variant with no payload is the name alone. An anonymous alternative
is ::0(value), counting from zero.
A menu demand is menu.field for the function, menu.field(arguments) for
the call, or <arguments | menu.field. A record or tuple projection is
value.field and value.0.
do, hn, and reset
do expression handler
hn Effect { clauses }
hn [Effect, Effect] { clauses }
hn { clauses }
reset expression
handler is an hn expression, a name bound to one, or a hand. The body
of do is one expression, so a chain is parenthesized or written as a
block. Clause syntax is Effects and handlers.
reset expression installs a handler with no clauses. It bounds jumps and
discharges nothing.
Chains
Chain forms are the whole of Flow. Inside a chain, name => expression names the value flowing in, and name <= expression names the
consumer the rest of the chain builds. A binder's body is one stage and ends
at the next |.
Types
A type denotes a value or a continuation. The sign and the connective say which.
Primitives
| Type | What it denotes |
|---|---|
i8, i32, i64 | Signed integers |
u8, u32, u64 | Unsigned integers, held in the same signed word, so a u64 reaches as far as i64 |
f32, f64 | Floating point |
char | A Unicode scalar |
String | A string of scalars |
Bool | The prelude enum False, True |
File | A file handle from fs::open |
Every integer is one machine word. A non-literal integer meets its port
exactly: a conversion is Into, described in the prelude. An
integer literal may still take the width of the port it meets.
Signs and parameters
+T is positive, -T is negative, and *T accepts either. On a generic
parameter the sign is written in the angle list: <+T>, <-K>, <*T>. An
unsigned parameter is a row variable.
dual(T) swaps the polarity. dual of a connective is the connective
opposite it.
Arrows
| Spelling | Meaning |
|---|---|
A -> B | A function from A to B |
B <- A | A consumer transformer: give it a consumer of B, it consumes A |
A -> B / {E} | The same function, performing E |
(-> T / {E}) | A by-name computation of T, performing E when forced |
(A hn B / {E} / {F}) | A handler value. Body A, answer B, discharged effects E, residual effects F |
A -> B and B <- A are different types. An empty effect row is omitted, so
(-> T) and (-> T / {}) agree. An empty residual row on a handler is
omitted the same way.
A -> (;) is the consumer -A. (;) is the type of a command.
Connectives
| Spelling | Unit | What a value of it holds |
|---|---|---|
(A, B) | (,) | Both components, the product |
(A | B) | (|) | One alternative, named ::0 or ::1 |
(A & B) | (&) | A bundle of consumers or fields, one per position |
(A ; B) | (;) | A joint: both sides, on the negative |
Longer forms associate to the right in the way a tuple does: (A, B, C),
(A | B | C). A joint (k1 ; k2) has no destructuring pattern. The name
unit is the product unit (,).
A continuation row of a proc is a bundle, (exit: A & other: B).
Effect rows
/ {IO}
/ {Exn, Reader}
/ {..E}
/ {Exn, ..E}
A row is a set of effect applications. One effect name appears at most once.
..E is the rest of the row, and E is an unsigned generic parameter.
Reader<i64> and Reader<String> are different effects. Arguments of an
effect application are invariant.
A row on a negative type is latent: the effect happens when the value is demanded, under the handlers around that demand.
Polymorphism
func id<+T>(x: T) -> T
func map<+A, +B, E>(f: (A -> B / {..E}), xs: List<A>) -> List<B> / {..E}
A let generalizes the type and row variables of a value. A computation,
including a mu that captured its continuation and a do, stays
monomorphic.
Patterns
Patterns appear in of and mu arms, in let, and in parameter lists. An
arm covers its scrutinee by patterns. A let or a parameter has a single
pattern, so that pattern is irrefutable: it matches every value of the type.
A refutable pattern belongs in an of.
| Pattern | Matches |
|---|---|
name | Any value, and binds it |
_ | Any value |
name @ pattern | The pattern, and binds the whole value as well |
literal | That integer, float, string, or character |
-literal | A negative numeric literal, the sign touching the digits |
start..=end | An inclusive range; the endpoints are literals |
pattern | pattern | Either pattern |
(p, q) | A tuple |
Name { field, field: pattern } | A record. A bare field name binds a variable of that name |
Variant | That variant, with no payload |
Variant(p, q) | That variant's payload |
Enum::Variant(p) | A qualified variant. The path may include a section |
::0(p) | The alternative at that position of a sum |
.item(p) | A request shape in a copattern |
A payload pattern may open a tuple or a record in place, because a product
has one shape. A sum nested in a payload is not split across arms; match it
with an of inside the arm.
Literal patterns and ranges are ordered by the value, and they are part of
an of. A mu arm does not use a literal pattern. Each mu arm covers one
shape, and a nested of distinguishes literals inside the arm.
Or-patterns and ranges share an arm, so they cover several values under one body. Repeating a value across arms is an exhaustiveness error. Omitting a variant is the same error. A wildcard covers the remainder, and a wildcard that overlaps a value already covered is refused.
Qualified constructors are required when a bare variant name would be ambiguous. Inside the standard library, enum variants used as patterns are qualified for that reason.
The copatterns of mu are documented with the expression, under
Expressions.
Flow
Everything in a chain moves left to right through |.
| Form | What it is |
|---|---|
<value | function | Application. The result is a value |
<value | function | consumer> | A cut. The type is (;) |
function | function | Composition. The result is a function |
function | consumer> | Composition into a consumer |
< is required when a value starts the chain. A chain without < starts
with a function, so "hi" | println is refused. <1 with no stage is
refused. > delivers to a consumer and does not return. A consumer stands
at the right end.
A stage is read in the orientation it was declared with. area: Shape -> i64 and area_of: i64 <- Shape are different types, and a chain does not
turn one into the other. Both still read left to right:
<shape | area | println
<shape | area_of | println
The whole group
A stage receives the whole parameter group. <1 | add is a type error when
add takes a pair. <(1, 2) | add is the call. A named function is not
applied with parentheses. f() calls a nullary function, and the name f
alone is the function value.
Constructors keep parentheses: Cons(1, Nil), Point { x: 1, y: 2 }.
Commands in a chain
A proc takes the value group from the left and the exit bundle as the
closing stage:
<(xs, 2) | nth | (found & missing)>
Closing those exits on functions that return makes the command yield, and the chain continues. Closing them on consumers that cut makes the whole chain a cut.
Binders
<1 | double | x => <(x, 1) | add
<12 | halve | ok <= (ok & odd) | halve | out>
x => e names the incoming value and is the function fn(x) { e }. k <= e names the consumer the rest of the chain builds. The chain that uses <=
ends on a consumer. A binder's body is one stage.
Where a chain ends
A chain ends at ;, }, ,, or ) , whichever closes the expression that
holds it. A tuple written after | is one stage: the comma inside it does
not end the chain. A nested chain that is a component opens with its own <.
Polarity
Positive types denote values. Negative types denote continuations: functions, consumers, and menus. The four places a type is written are all used.
| Argument position | Continuation position | |
|---|---|---|
| Positive type | Data arrives: func f(x: i64) | The input of a consumer transformer: func f(k: i64) <- Request |
| Negative type | A consumer arrives as data: func f(note: -String) | The exit of a proc: proc f | (k: -i64) |
A proc splits its parameters by polarity, so a consumer parameter belongs
in the exit group. A returning func may take a negative argument, because
the function returns and the call does not promise to consume it.
A positive type in an exit position names what the continuation accepts.
exit: i32 and exit: -i32 are that spelling on main.
When a computation runs
The position decides.
- A positive computation runs when the surrounding constructor, argument, or
letruns. - A negative computation stored in a binding, a tuple component, a record field, a variant payload, or a bundle item waits. It runs when it is demanded, and it runs again on every demand.
- Effects of a demand are handled by the handlers around that demand.
let follows the polarity it can see. An unfixed let is refused until it
is annotated.
| Binder | What it holds |
|---|---|
let | The polarity of the expression |
let+ | Evaluates now. Shallow: a negative component of the result stays delayed |
let- | A negative computation, rerun on every demand. A positive result is refused |
A by-name type is (-> T / {E}). Using the name forces it: E happens
then, and the result is T. The result's own effect row is separate.
((,) -> T) is a function applied to the unit value, and (,) is the unit
value.
The explicit thunk for either polarity is lazy::Lazy<T, E>, demanded with
.force. lazy::of_delayed and lazy::to_delayed convert negative
computations without running them. Neither form caches.
Unfixed forms are rejected: a lambda parameter with no polarity, a mu with
no type where one is required, an integer literal before a port that never
settles, a plain let whose polarity is unknown.
Handlers and delay
let+ forces the expression it binds. A command left under let-, or a
function that performs Fs only when it is later called, performs that
effect at the demand. The handler has to be around the demand. Handling the
name and forcing it later places the effect outside the handler.
examples/laziness/
is the suite for these boundaries.
Effects and handlers
hook
hook Exn { func throw(message: String) -> i64; }
hook Reader<+T> { func read() -> T; }
hook Shift<+A, +R, E> {
func shift(callback: (-> ((A -> R / {..E}) -> R / {..E}) / {..E})) -> A;
}
An operation is a function. Its result type is what a resumption receives. A generic hook's operations share its parameters. A written application supplies every argument. Parameters are not repeated and do not carry trait bounds. A row holds at most one application of a given effect name.
A function that performs the effect names it:
func checked_div(a: i64, b: i64) -> i64 / {Exn}
func map<+A, +B, E>(f: (A -> B / {..E}), xs: List<A>) -> List<B> / {..E}
A call instantiates row variables from the arguments that mention them.
Inside the body the variable is rigid. A rowless arrow is a promise of
purity: passing a function that performs Exn where (i64 -> i64) is
declared is an error. A pure function may still be passed where extra
effects are allowed.
One do uses one instantiation for every operation of an effect.
{Reader<i64>} and {Reader<String>} are different capabilities.
do and clauses
do body hn {
throw(message) => -1,
config(): resume => <10 | resume,
return(value) => <value | to_string,
_ => forward,
}
An operation clause binds as many parameters as the operation declares. A
nullary operation is written config(), with no unit binder. The
continuation is bound after a colon. Omitting it means the clause never
resumes.
return(x) => e maps the body's value. Without it, the handler's answer
type is the body's type. Every operation clause produces that answer type or
ends in a command. The resumption returns the handler's answer, including a
return clause's transformation, and it keeps the residual row of that
handler.
A handler covers a whole effect. Naming any operation requires the others. _ => forward is the last clause, has no binder, and passes unmatched
operations outward. The forwarded effect stays in the outward row.
do installs a handler around one expression. A | after the whole do
belongs to the surrounding chain.
hn and hand
hn Reader { clauses }
hn [Reader, Other] { clauses }
hn { clauses }
let reader: (i64 hn String / {Reader}) = hn Reader { … };
do body reader
hn builds a value and runs nothing. The type is (A hn B / {E} / {F}):
| Parameter | Meaning |
|---|---|
A | What the body produces |
B | The handler's answer. Equal to A when there is no return clause |
E | Concrete effects the clauses discharge |
F | Effects the clauses and the residual body still perform |
All four arguments are invariant. An annotation cannot grant effects the
clauses do not answer. An open tail on E grants no unknown capability.
Installation discharges only the effects written in E. Construction does
not generalize the handler out from under the instantiation its clauses
fixed.
A hand is inlined at each do and is not a value. Its answer type is the
body's, including (;). The row written on the hand is what the clauses
perform. The diagnostic for using a hand as a value says to install it with
do expr name.
Rows and polymorphism
A handler discharges concrete effects named in the type. An effect that arrives only through a row variable, inside a closure the caller passed, passes through to the caller's handler.
A let generalizes a stored hn only under the value restriction, and only
after the clause arguments have been equated with the capability row. A do
expression is not a value, so its result is not generalized.
Effects on a menu or a form are latent. They run when the field is
demanded.
reset
reset expression is a delimiter: a handler with no clauses. It bounds a
continuation captured inside it and discharges nothing. A jump from inside
it to a continuation captured outside it is a runtime error.
control::reset is a library hand of control::Shift. It is installed
with do expr control::reset. Bare reset does not handle Shift.
IO
hook IO {
func write(text: String) -> (,);
func write_line(text: String) -> (,);
}
The runtime handles IO around main. main may leave {IO} in its row.
A nearer handler answers first.
Traits
A spec is a set of methods over Self. An impl supplies them for one
type. A method is a function selected by the type that flows in.
spec Show {
func show(self: Self) -> String;
}
impl Show for Bool {
func show(self: Bool) -> String {
of self { True => "true", False => "false" }
}
}
func labelled<+T: Show>(x: T) -> String {
<x | show
}
The call is <True | show. Several parameters are one group, <(a, b) | eq. The receiver is the Self parameter, which is the first.
Coherence
An impl is keyed by the trait, its type arguments, and the implementing
type. Into<i64> and Into<String> for the same type are two impls.
Overlapping impls are refused. There is no impl whose Self is a bare type
parameter.
A method call is accepted when a concrete type has an impl, or when a type parameter carries the bound. An unbounded type parameter does not. A trait parameter the call never fixes is refused.
Method names are unique across traits. An impl method matches the spec
signature after Self and the trait arguments are substituted. An omitted
trailing row argument still matches, so an impl for Stream<T> covers
Stream<T, row>.
Impls exist for anonymous types too: a tuple, a sum, or the unit, keyed by
the connective and the width. The prelude's Display for tuples is that
form.
Bounds and defaults
func largest<+T: Ord + Display>(a: T, b: T) -> String
impl<+T: Display> Display for list::List<T>
Bounds are joined with + inside one parameter. The next parameter's sign
comes after a comma. Bounds stay on positive type parameters. A trait's own
parameters do not carry bounds. A data, enum, menu, or form
declaration does not carry bounds.
A default body is copied into each impl that omits the method. A default may
call another method of the same spec. An impl may override the default. The
prelude writes Eq.ne as the negation of eq, and the integer impls
override it.
Supertraits
spec Rank: Eq {
func place(self: Self) -> i64;
}
Several parents are joined with +. A cycle is refused. An impl of the
child is refused until every parent is implemented for the same type. A
bound <+T: Rank> brings the parent methods as well. Ord and Eq in the
prelude are separate specs: a bound that needs both names both.
Parameters and associated types
spec Into<+U> {
func into(self: Self) -> U;
}
Self is the value that flows in. A parameter that appears in an argument
is fixed by that argument. A parameter that appears only in the result, as
U does, is fixed by the type the call is expected to have.
spec Walk {
type Item;
func step(self: Self) -> Walk::Item<Self>;
}
impl Walk for Countdown {
type Item = i64;
func step(self: Countdown) -> i64 { … }
}
Every impl binds each associated type. There is no default associated type
and no bound on an associated type. A use is Walk::Item<T>: trait
arguments first, the implementing type last. A pin is <+T: Walk<Item = i64>>. An unfixed projection is refused.
Dispatch
A call on a concrete type is a direct call to the impl. A call on a bounded type parameter takes the method from a dictionary passed with the caller. A one-method dictionary is the method. A several-method dictionary is a tuple in declaration order. There is no runtime method table.
The programs are
examples/basics/traits.sl,
examples/basics/defaults.sl,
examples/basics/supertraits.sl,
examples/basics/associated.sl,
and
examples/basics/into.sl.
Sections and files
sect geometry {
pub enum Shape { Circle(i64), Rect(i64, i64) }
func squared(n: i64) -> i64 { <(n, n) | mul }
pub func area(s: Shape) -> i64 { … }
}
cite geometry::area;
A declaration in a section is private unless it is pub. Private means the
section and the sections nested in it. A declaration in no section is
visible in the whole program. main is declared at the root.
A path resolves by its first segment. geometry::Shape::Circle is refused
when Shape is private. Inside the section, its own names are bare.
cite
| Form | What it brings |
|---|---|
cite geometry::area; | That name, bare |
cite geometry::*; | Every public member, bare |
cite list::List::*; | The public variants |
cite Colour::{Red, Blue}; | Those variants |
A cite does not run the name. Imports belong to the file that wrote them. A glob loses to an explicit cite and to a declaration in the same section. Two globs that offer one name are reported at the use. Two explicit cites of the same name are an error at the cite.
cite list; cites a section that is already reachable, which is allowed.
A program section whose name is a library section shadows that library
section.
Citing variants does not cite the type. A signature that writes List<i64>
also has cite list::List, or it uses the path list::List.
Files
sect geometry;
pub sect report;
The file contains the declarations, without a wrapping sect. The directory
tree is the section tree.
| Declared in | sect name; is the file |
|---|---|
dir/main.sl | dir/name.sl |
dir/m.sl | dir/m/name.sl |
inline, inside sect a | one a/ directory further down |
Run and check the root. There is no search path. The sample is
examples/programs/multi_file/.
Resolution flattens sections before checking. The checker sees qualified names. A library file is loaded when a path or a cite names it, and the prelude is loaded always.
Prelude
The prelude is
crates/slc-driver/src/prelude.sl.
It is ordinary SLC, appended to every program. A program's own declaration
of a prelude name shadows it. Names from the standard library are not in the
prelude; they are cited.
Bool, printing, and IO
enum Bool { False, True }
hook IO {
func write(text: String) -> (,);
func write_line(text: String) -> (,);
}
func println<+T: Display>(x: T) -> (,) / {IO}
func print<+T: Display>(x: T) -> (,) / {IO}
func to_string<+T: Display>(x: T) -> String
func not(b: Bool) -> Bool
println and print render through Display and then perform write_line
or write. not exchanges True and False.
Display
spec Display {
func fmt(self: Self) -> String;
}
Impls cover i8, i32, i64, u8, u32, u64, f32, f64, String,
Bool, char, File, the unit, and tuples and sums of up to eight
components. A string's fmt is the string. Bool renders true and
false. Sums render as ::0(…) through ::7(…).
Arithmetic and order
spec Add { func add(self: Self, other: Self) -> Self; }
spec Sub { func sub(self: Self, other: Self) -> Self; }
spec Mul { func mul(self: Self, other: Self) -> Self; }
spec Div { func div(self: Self, other: Self) -> Self; }
spec Rem { func rem(self: Self, other: Self) -> Self; }
spec Neg { func neg(self: Self) -> Self; }
spec Eq {
func eq(self: Self, other: Self) -> Bool;
func ne(self: Self, other: Self) -> Bool { <(<(self, other) | eq) | not }
}
spec Ord {
func lt(self: Self, other: Self) -> Bool;
func gt(self: Self, other: Self) -> Bool;
func le(self: Self, other: Self) -> Bool;
func ge(self: Self, other: Self) -> Bool;
}
Add, Sub, Mul, Div, Rem, Eq, and Ord are implemented for the
six integer widths and for f32 and f64. Add is also implemented for
String. Neg is implemented for the signed widths and the floats. Eq
and Ord are also implemented for char, String, and Bool. Ord and
Eq are separate specs.
Div and Rem by zero are runtime errors, as is an overflowing Add,
Sub, Mul, or Neg. The call is <(a, b) | add.
func index(s: String, i: i64) -> char
func wrapping_mul(a: i64, b: i64) -> i64
func xor(a: i64, b: i64) -> i64
func shl(a: i64, b: i64) -> i64
func shr(a: i64, b: i64) -> i64
func not(b: Bool) -> Bool
func and(a: Bool, b: Bool) -> Bool
func or(a: Bool, b: Bool) -> Bool
index is the character at a zero-based position. An index outside the
string is a runtime error. wrapping_mul is the product on the 64-bit
pattern, and xor is exclusive or on that word. shl and shr move that
pattern. The distance is the count's low six bits, so both are total, and
shr fills from the left with zeros. not, and, and or are ordinary
functions. and and or are strict: both arguments are values before the
call. There is no !, &&, or ||.
Into
spec Into<+U> {
func into(self: Self) -> U;
}
There is an impl for every pair among i8, i32, i64, u8, u32,
u64, f32, and f64, and none from a type to itself. The expected type
chooses the destination.
The number is kept when it is exact for the destination. Otherwise the
conversion is an arithmetic overflow. An integer fits in a float only when
the float is exactly that integer. A float fits in an integer width only
when it is finite, integral, and inside that width. A float fits in f32
only when it is an exact f32. Every f32 fits in f64. A negative value
does not fit an unsigned width. A u64 reaches as far as i64, because
every integer is one signed word.
Float operations
spec Sqrt { func sqrt(self: Self) -> Self; }
spec Abs { func abs(self: Self) -> Self; }
spec Floor { func floor(self: Self) -> Self; }
spec Ceil { func ceil(self: Self) -> Self; }
spec Sin { func sin(self: Self) -> Self; }
spec Ln { func ln(self: Self) -> Self; }
spec Pow { func pow(self: Self, exp: Self) -> Self; }
Each is implemented for f32 and f64. sqrt of a negative number is an
arithmetic overflow. -0.0 is in the domain. abs, floor, ceil, and
sin are the IEEE operations. ln overflows on zero, a negative, and -∞.
pow overflows when the base is negative and the exponent is finite but not
an integer, and when the base is zero and the exponent is negative. A NaN
argument of pow stays NaN. num::abs is the i64 function in the
library, and it is a different operation.
Hash
spec Hash {
func hash(self: Self) -> u64;
}
Impls cover the integer widths, char, Bool, and String. The result is
non-negative. Equal values hash equal. The mix multiplies by the bit pattern
of 0x9E3779B97F4A7C15 and clears the high bit, so a remainder of a hash is
a slot. A String is FNV-1a over char_to_code.
Standard library
The library is
crates/slc-driver/src/stdlib/.
Each file is a section named after the file. A program reaches it by path
or by cite. Only the sections a program names are loaded.
Collections are persistent. An update returns a new value and leaves the
old one unchanged. List, Seq, and Stream are three types: eager data,
a finite menu of steps, and a coinductive menu.
list
pub enum List<+T> { Nil, Cons(T, List<T>) }
| Operation | Signature |
|---|---|
length | (List<T>) -> i64 |
append | (List<T>, List<T>) -> List<T> |
map | (A -> B / {..E}, List<A>) -> List<B> / {..E} |
range | (i64, i64) -> List<i64>, inclusive, empty when from is greater than to |
filter | (T -> Bool / {..E}, List<T>) -> List<T> / {..E} |
fold | (List<A>, B, (B, A) -> B / {..E}) -> B / {..E} |
sum | (List<i64>) -> i64 |
reverse | (List<T>) -> List<T> |
take, drop | (List<T>, i64) -> List<T> |
nth | proc (List<T>, i64) | (found: T & missing: String) |
A non-positive take is the empty list, and a non-positive drop is the
whole list. A count past the end takes the whole list or drops to nothing.
range of a single i64, including the greatest one, is that one element.
Display renders [1, 2, 3].
map and set
map::Map<K, V> is an AVL tree. Keys use Ord. Two keys are the same when
neither is less, so a NaN key collides with the node the search reaches.
set::Set<K> is that tree with nothing stored beside the key. Display for
a map is {a: 1, b: 2}, and for a set {1, 2, 3}, in key order.
| Operation | On Map<K, V> | On Set<K> |
|---|---|---|
empty | () -> Map<K, V> | () -> Set<K> |
length | the number of entries | the number of keys |
insert | (map, K, V) -> Map | (set, K) -> Set |
remove | (map, K) -> Map | (set, K) -> Set |
contains | (map, K) -> Bool | (set, K) -> Bool |
of_list | a list of pairs, later pairs winning | a list of keys |
to_list | the pairs, in order | the keys, in order |
union, intersection, difference | sets only | (Set<K>, Set<K>) -> Set<K>. Difference is the first set without the second |
get | proc (map, K) | (found: V & missing: String) |
num::min and num::max use the same Ord. When the two arguments compare
equal they answer the second.
Both have a Builder menu. builder() starts empty. put on a map takes
the key and the value and answers the next builder. put on a set takes the
key. finish answers the collection. map::put is also the function a
chain flows into; builder.put is that function demanded from the menu.
hashmap and hashset
hashmap::HashMap<K, V> is a 4-way hash trie. Keys use Hash and Eq, and
equal keys hash equal. hashset::HashSet<K> stores nothing beside the key.
The operations match map and set, with Hash and Eq as the key bounds,
except a set's union, intersection, and difference.
HashMap's get is the same found / missing command. HashSet has
contains and no get. Builders match as well: a hash map's put takes
the entry, and a hash set's put takes the key.
Display uses the same braces as the ordered collections. The order is the
trie's order.
array
array::Array<T> is an immutable 4-way trie. array::Array4<T> is one
branch, One through Four, every slot occupied.
| Operation | Signature |
|---|---|
empty | () -> Array<T> |
length | (Array<T>) -> i64 |
push | (Array<T>, T) -> Array<T> |
of_list, to_list | between Array<T> and list::List<T> |
get | proc (Array<T>, i64) | (found: T & missing: String) |
update | proc (Array<T>, i64, T) | (updated: Array<T> & missing: String) |
slots | (Array4<T>) -> i64 |
slot_get | proc (Array4<T>, i64) | (found & missing) |
slot_update | proc (Array4<T>, i64, T) | (updated & missing) |
Display for an array is the bracket form used for lists. push grows a
new level when a branch is full, at 4, 16, and 64 elements.
string
menu Builder {
append(part: String): Builder,
finish: String,
}
func new() -> Builder
func push<+T: Display>(builder: Builder, value: T) -> Builder
new() is the empty builder. append and finish are fields, so
builder.append(part) and <part | builder.append are the same demand.
push renders through Display and appends. Each builder closes over one
string, so a shared prefix can diverge.
| Operation | Signature |
|---|---|
starts_with | (text, prefix) -> Bool. The empty prefix is true |
chars | (String) -> List<char>, the scalar values in order |
split | (text, sep) -> List<String>. An empty separator yields one string per scalar value |
trim | (String) -> String, dropping leading and trailing Unicode whitespace |
replace | (text, from, to) -> String, every non-overlapping occurrence. An empty from leaves the text unchanged |
lower, upper | (String) -> String, the Unicode case mapping |
option and either
enum Option<+T> { None, Some(T) }
func unwrap_or<+T>(o: Option<T>, fallback: T) -> T
func map<+A, +B, E>(f: (A -> B / {..E}), o: Option<A>) -> Option<B> / {..E}
func and_then<+A, +B, E>(f: (A -> Option<B> / {..E}), o: Option<A>) -> Option<B> / {..E}
enum Either<+L, +R> { Left(L), Right(R) }
map and and_then take the function first, as list::map does. None
stays None. Either has no methods. Neither side means success.
num
| Operation | Signature |
|---|---|
min, max | <+T: Ord>(T, T) -> T. Equal arguments answer the second |
abs, signum | (i64) -> i64 |
is_even, is_odd | (i64) -> Bool |
gcd, lcm | (i64, i64) -> i64 |
div_rem | (i64, i64) -> (i64, i64) |
abs here is the i64 function. The float method is the prelude's Abs.
stream
menu Stream<+T, E> / {..E} {
head: T,
tail: (-> Stream<T, ..E> / {..E}),
}
| Operation | What it answers |
|---|---|
repeat | the same value at every demand |
count_from | n, then n + 1, then onward |
iterate | x, then f(x), then onward |
unfold | each step answers an element and the next seed |
map | f on each demanded element |
zip | pairs of demanded elements |
drop | the stream after n demands, performed while drop runs |
take | the first n elements as a list::List |
There is no Display for Stream. Showing a prefix is take. The row is
performed when a step is demanded. The argument that passes the stream on is
a by-name computation, (-> Stream<T, ..E> / {..E}), so the stream is
rebuilt at each demand.
seq
enum Step<+T, E> { Done, Yield(T, Seq<T, ..E>) }
menu Seq<+T, E> / {..E} { next: Step<T, ..E> }
| Operation | Signature |
|---|---|
of_list | (List<T>) -> Seq<T> |
to_list | (Seq<T, ..E>) -> List<T> / {..E} |
of_stream | a by-name Stream becomes a Seq |
map, filter | step the sequence, performing the function's row on demand |
take | (Seq<T, ..E>, i64) -> Seq<T, ..E> |
take_while | a stream, cut where the predicate returns False, as a Seq |
filter over an infinite stream is a finite program when a later take
stops asking. There is no Display for Seq. Showing one is to_list.
lazy
menu Lazy<*T, E> / {..E} { force: T }
func of_delayed<-T, E>(computation: (-> T / {..E})) -> Lazy<T, ..E>
func to_delayed<-T, E>(computation: Lazy<T, ..E>) -> (-> T / {..E})
force runs the computation under the handlers around the demand.
of_delayed and to_delayed do not run it and do not cache it.
fs
fs performs the Fs effect. do expr fs::real answers it from the disk.
The hand's clauses perform IO. Closing a File is a convention: a later
read through a closed handle fails at run time.
| Operation | Outcomes |
|---|---|
read | proc (path) | (ok: String & failed: String) |
write | proc (path, contents) | (ok: unit & failed: String) |
open | proc (path) | (opened: File & failed: String) |
read_line | proc (file) | (line: String & end: unit) |
close | (File) -> (,) / {Fs} |
exists | (String) -> Bool / {Fs} |
read_dir | proc (path) | (ok: List<String> & failed: String). Names only, in string order, without . or .. |
remove | proc (path) | (ok: unit & failed: String). A file, a symlink, or an empty directory |
rename | proc (from, to) | (ok: unit & failed: String) |
// Checked, and left unrun: it would read a file from the disk.
proc main | (exit: i32) / {IO} {
do {
<"notes.txt" | fs::read | (
mu String {
text => {
<text | println;
<0 | exit>
},
}
& mu String {
why => {
<why | println;
<1 | exit>
},
}
)>
} fs::real
}
examples/programs/file_io.sl
is a complete program, including closing the handle on the way out.
args and clock
func arguments() -> List<String> / {Args}
func now() -> i64 / {Clock}
do expr args::real answers with the words after the source file, in order.
The runtime path, the file path, --fuel, and --interpret are not among
them. An empty argument list prints [].
do expr clock::real answers a monotonic nanosecond count. The origin is
arbitrary and local to the process. The difference of two readings is the
time between them, independent of the civil clock. A count that does not fit
in i64 is an arithmetic overflow.
Both hands perform IO, so main still leaves IO.
// The words after the source file. A run with none prints [].
proc main | (exit: i32) / {IO} {
let words = do (<(,) | args::arguments) args::real;
<words | println;
<0 | exit>
}
[]
// Two readings of the monotonic clock. The later one is not earlier.
proc main | (exit: i32) / {IO} {
let start = do (<(,) | clock::now) clock::real;
let later = do (<(,) | clock::now) clock::real;
<(<(later, start) | ge) | println;
<0 | exit>
}
true
slc run book/examples/args.sl one two prints [one, two].
env, sleep, and process
proc get<E>(name: String) | (found: -String & absent: -unit) / {Env, ..E}
proc set<E>(name: String, value: String) | (ok: -unit & failed: -String) / {Env, ..E}
func sleep(ns: i64) -> (,) / {Sleep}
proc spawn<E>(program: String, args: List<String>) | (exited: -i64 & failed: -String) / {Process, ..E}
do expr env::real reads and writes the process environment. Absence is its
own outcome. set refuses an empty name, a name containing =, or a null
byte in either string. do expr sleep::real waits. Zero returns at once,
and a negative count is an arithmetic overflow. do expr process::real
starts a program and waits for it. The success is the exit status, including
a non-zero one. Failure means the program could not be started. The child
inherits the environment and the standard streams. The list is the words
after the program path. Each hand performs IO.
control
hook Shift<+A, +R, E> {
func shift(callback: (-> ((A -> R / {..E}) -> R / {..E}) / {..E})) -> A;
}
hand reset { shift(callback): resume => <resume | callback }
do expr control::reset handles Shift. One installation has one answer
type R. A resumption runs the captured continuation afresh and keeps that
handler's residual row. The example is
examples/effects/composable_capture.sl.
trace
proc tap<+T: Display, E>(label: String, x: T) | (k: (-T / {..E})) / {IO, ..E}
tap prints the label and the value, then sends the value to k.
Builtins
A builtin is an operation the surface does not express: machine arithmetic
under the traits, string internals, and the runtime primitives under fs,
args, and clock. The names below are the ones a program calls. The __
names are what the prelude and the library call, and a program reaches that
behavior through the trait or the library function.
A builtin with one outcome is a function. A builtin with several outcomes is a command: the values come first, then one continuation per outcome, and exactly one continuation runs.
Strings
| Operation | Arguments | Result |
|---|---|---|
str_len | String | i64, the number of characters |
str_concat | String, String | String |
substring | String, start i64, end i64 | the characters from start up to end |
index | String, i64 | char. This is the prelude function over __index |
int_to_str | i64 | String |
str_eq | String, String | Bool |
char_to_code | char | i64 |
is_digit | char | Bool |
is_ws | char | Bool |
skip_ws | String, i64 | the index after the whitespace |
skip_digits | String, i64 | the index after the digits |
substring refuses a range outside the string. index refuses an index
outside the string. is_ws is true when the character is Unicode
whitespace. skip_ws advances over space, tab, newline, and carriage
return. Add for String is concatenation, and str_concat is that
operation under its own name.
Commands
| Operation | Values | Continuations |
|---|---|---|
parse_int | text String | ok of i64, invalid of String, overflow of String |
char_at | text String, index i64 | ok of char, out_of_range of String |
find_char | text String, from i64, character code i64 | found of i64, absent of String |
index is the function that fails as a runtime error. char_at is the
command that offers the miss to a continuation. find_char searches from
the given index for the scalar whose code is the third argument.
Under the library
These perform IO and are reached through a hand, not called at the top
of a program that wants its effects in Fs, Args, Clock, Env,
Sleep, or Process.
| Operation | Role |
|---|---|
__read_file | fs::read, outcomes text or why |
__write_file | fs::write |
__open_file | fs::open, a File or why |
__read_line | fs::read_line, a line or the end |
__close_file | fs::close |
__file_exists | fs::exists |
__read_dir, __dir_entry | fs::read_dir. The count, then one name per index |
__remove_path | fs::remove |
__rename_path | fs::rename |
__argument_count, __argument_at | args::arguments |
__monotonic_ns | clock::now |
__env_get, __env_set | env::get and env::set |
__sleep_ns | sleep::sleep |
__spawn_begin, __spawn_push, __spawn_exec | process::spawn |
The arithmetic builtins __add, __sub, __mul, __div, __rem,
__neg, the comparisons, __sqrt, __abs, __floor, __ceil, __sin,
__ln, __pow, __shl, __shr, and the __to_* conversions are the
bodies of the prelude traits. __lower and __upper are string::lower
and string::upper.
spawn takes a proc with main's signature,
proc worker | (exit: i32) / {IO}, runs it on another host thread, and
returns (,). The call performs IO. The proc's status is not a result.
process::spawn is the separate operating-system child, reached through
__spawn_begin, __spawn_push, and __spawn_exec.
The slc command
slc --version
slc run [--fuel N] [--interpret] <file.sl> [arg]…
slc compile [-o <file>] [--fuel N] <file.sl>
slc check <file.sl>…
slc fmt [--check | --stdout] <file.sl>…
slc --version prints slc and the package version.
run
slc run checks the file and then runs it. Flags are recognized only before
the file. Everything after the file is a word the program reads through
args::arguments, including a word that looks like a flag.
With no --interpret, slc compiles to a native executable, links it with
the runtime, and executes it. That executable is an x86-64 ELF. On any other
host, slc run uses the interpreter, and slc compile stops before linking.
--interpret evaluates the checked program on the abstract machine.
book/check.sh runs the tutorial programs with --interpret.
--fuel N stops after N steps. Omitting --fuel bounds a run by memory.
--fuel 0 runs no SLC code. The integer sent to exit is the process
status when it fits in a byte. A status outside 0..=255 fails the process.
A file with a main of the wrong shape is refused before it runs. The
required shape is a root proc with no value parameters and one
continuation, the status.
compile
slc compile checks the file, links the native executable, and writes that
file. It does not run the program.
-o names the executable. --output is the same flag. With neither, the
name is the source stem in the current directory, so
examples/basics/hello.sl becomes ./hello. The output path has to be a
different file from the source.
--fuel N is the executable's step bound, the same bound slc run --fuel N
uses. With no --fuel, the executable is bounded by memory. Every argument
of the executable is a program argument.
slc compile -o hello examples/basics/hello.sl
./hello
A file with no main, or a main of the wrong shape, is refused and no
executable is written.
check
slc check applies the compiler phases and stops. It evaluates nothing, so
a program that would not terminate can still be checked. Each diagnostic is
prefixed with its file. One failing file fails the command. A file with no
main is a library and checks. A glob such as examples/basics/*.sl is the
shell's, and examples/errors/ is left out of it because those files fail
on purpose.
fmt
slc fmt rewrites each file in place. The layout is four-space indentation,
lines within 100 columns, a semicolon after every statement except a block's
last expression, and a comma after every arm. A list between braces keeps
the line break its author gave it while it fits. A chain breaks before each
| when it does not fit. Comments and blank lines are kept.
The formatter changes whitespace and the separators the grammar leaves
optional. It refuses to write a file whose formatted text is not the same
program. --check writes nothing and fails when a file would change.
--stdout prints the result. --check and --stdout together are refused.
Building
From the repository:
cargo build
cargo test
cargo run -- run examples/basics/hello.sl
cargo run -- fmt --check examples/basics/*.sl
cargo test includes the example outputs, the design programs, and a check
that SLC sources are formatted. The Emacs mode is
editors/emacs/slc-mode.el.
C-c C-f formats the buffer through slc fmt.
Diagnostics
A compiler diagnostic names its phase, the file, and a line and column. It quotes the source it is about. A syntax error that runs to the end of the file, such as an unterminated string, quotes its first line. A diagnostic with no extent names no place.
The phases run in this order, and a phase that reports anything stops the later ones:
| Phase | What it refuses |
|---|---|
parse | A token stream that is not a program |
resolve | A name that does not denote, or a cite that collides |
trait | An impl that does not match its spec, or that overlaps another |
type | A term whose type does not meet its port |
polarity | A value or continuation used at the wrong polarity |
exhaustiveness | An of or mu that misses a shape, repeats one, or overlaps |
effect | An effect the row does not allow, or a handler that does not cover it |
lowering | A surface form the core cannot express |
Within one phase, diagnostics are source-ordered. slc check and slc run
report the same compiler diagnostics. run continues into evaluation only
when the phases accept the program.
Some slips name the repair directly. A handler clause with the wrong number
of parameters reports the operation's arity. A clause of the wrong answer
type reports the handler's answer type. An incomplete handler lists the
missing operations and mentions a final _ => forward. A function closed
with > where the > was meant to be left off is reported as a function
applied by flow. A clause that names no operation is refused by that name.
Runtime failures happen after evaluation starts. They are not compiler diagnostics and do not follow this order. They include:
- division by zero, and arithmetic overflow, including an
Intoconversion or asqrtthat does not fit - a string index or substring outside the string
- a read through a
Filethat has been closed - a continuation that jumps across a
resetwhich did not capture it
The programs in
examples/errors/
fail on purpose. One is a command that reaches no continuation, one is a
polarity the checker refuses, and one crosses reset at run time.
Surface forms
These are the shapes a program writes. A … stands for a form already in
the list. The parser in slc is the grammar; this page is the index.
Declarations
func name(param: Type, …) -> Type / {Row} { Expr }
func name<+T: Bound>(param: Type) -> Type { Expr }
func name(param: Type) <- Type / {Row} { Expr }
proc name(param: Type, …) | (exit: Type & …) / {Row} { Expr }
data Name<+T> { field: Type, … }
enum Name<+T> { Variant, Variant(Type, …), … }
menu Name<+T, E> / {..E} { field: Type, field(param: Type): Type, … }
form Name / {Row} { field: Type, … }
spec Name<+U>: Parent + Parent {
type Item;
func method(self: Self, …) -> Type;
func method(self: Self) -> Type { Expr }
}
impl<+T: Bound> Name<Arg> for Type {
type Item = Type;
func method(self: Type) -> Type { Expr }
}
hook Name<+T, E> { func operation(param: Type, …) -> Type; }
hand name / {Row} { operation(param): name => Expr, _ => forward, }
def NAME: Type = Expr;
pub Declaration
sect name { Declaration … }
sect name;
cite path::name;
cite path::*;
cite Enum::{Variant, …};
Expressions
name
literal
(,)
(Expr, Expr, …)
::index(Expr)
Name { field: Expr, … }
Variant(Expr, …)
name.field
name.index
{ Statement; … Expr }
let pattern = Expr
let+ pattern = Expr
let- pattern = Expr
of Expr { pattern => Expr, … }
mu Type { pattern => Expr, … }
mu Type { copattern <= Expr, … }
mu Type { name <= Expr }
mu Type {}
fn(param: Type, …) -> Type { Expr }
fn { Expr }
<(,) | name
name()
do Expr Handler
hn Effect { Clause, … }
hn [Effect, …] { Clause, … }
hn { Clause, … }
reset Expr
<Expr | Stage | …
<Expr | Stage | … | Consumer>
Stage | Stage
name => Expr
name <= Expr
Handler is an hn expression, a hand name, or a name bound to a handler
value. Stage is an expression or a binder. A cut includes the closing >.
Types
i8 i32 i64 u8 u32 u64 f32 f64 char String Bool File unit
+Type -Type *Type dual(Type)
Type -> Type
Type <- Type
(-> Type / {Row})
(Type hn Type / {Row} / {Row})
(Type, Type, …) (,)
(Type | Type | …) (|)
(Type & Type & …) (&)
(Type ; Type) (;)
Name<Type, …>
Name::Item<Type, …>
/ {Effect, …, ..Name}
The unit of a connective is the parentheses holding only its separator.
Patterns
name
_
name @ pattern
literal
start..=end
pattern | pattern
(pattern, pattern, …)
Name { field, field: pattern, … }
Variant
Variant(pattern, …)
Path::Variant(pattern, …)
::index(pattern)
Clauses
operation(param, …) => Expr
operation(param, …): name => Expr
operation(): name => Expr
return(name) => Expr
_ => forward