Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

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 insect name; reads
the program, dir/main.sldir/name.sl
a section file, dir/m.sldir/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.

DirectoryWhat 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:

PageWhat it answers
Lexical structureTokens, literals, keywords
Declarationsfunc, proc, data, enum, menu, form, spec, hook, hand, def, sect
ExpressionsBlocks, of, mu, do, let, chains
TypesPrimitives, arrows, connectives, rows, signs
PatternsWhat an arm or a let may match
Flow<, |, >, application, cuts
PolarityWhen a computation runs
Effects and handlershook, do, hn, hand, rows
Traitsspec, impl, bounds, associated types
Sections and filessect, cite, files
PreludeNames in every program
Standard librarySections cited by path
BuiltinsOperations the compiler provides
The slc commandrun, check, fmt
DiagnosticsPhases and runtime failures
Surface formsThe 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:

WordRole
func, proc, fnFunctions. func names one, fn is a lambda, proc is a command
data, enum, menu, formType declarations
spec, impl, forTraits
hook, do, hand, hn, resetEffects and handlers
of, mu, letMatching, consumers, binding
sect, citeSections
def, pub, dualA 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

LiteralType when unconstrainedExample
integeri6442, 1_000, -7
floatf641.5, -0.5
stringString"hello\n"
characterchar'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 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

TypeWhat it denotes
i8, i32, i64Signed integers
u8, u32, u64Unsigned integers, held in the same signed word, so a u64 reaches as far as i64
f32, f64Floating point
charA Unicode scalar
StringA string of scalars
BoolThe prelude enum False, True
FileA 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

SpellingMeaning
A -> BA function from A to B
B <- AA 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

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

PatternMatches
nameAny value, and binds it
_Any value
name @ patternThe pattern, and binds the whole value as well
literalThat integer, float, string, or character
-literalA negative numeric literal, the sign touching the digits
start..=endAn inclusive range; the endpoints are literals
pattern | patternEither pattern
(p, q)A tuple
Name { field, field: pattern }A record. A bare field name binds a variable of that name
VariantThat 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 |.

FormWhat it is
<value | functionApplication. The result is a value
<value | function | consumer>A cut. The type is (;)
function | functionComposition. 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 positionContinuation position
Positive typeData arrives: func f(x: i64)The input of a consumer transformer: func f(k: i64) <- Request
Negative typeA 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 let runs.
  • 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.

BinderWhat it holds
letThe 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}):

ParameterMeaning
AWhat the body produces
BThe handler's answer. Equal to A when there is no return clause
EConcrete effects the clauses discharge
FEffects 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

FormWhat 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 insect name; is the file
dir/main.sldir/name.sl
dir/m.sldir/m/name.sl
inline, inside sect aone 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>) }
OperationSignature
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>
nthproc (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.

OperationOn Map<K, V>On Set<K>
empty() -> Map<K, V>() -> Set<K>
lengththe number of entriesthe 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_lista list of pairs, later pairs winninga list of keys
to_listthe pairs, in orderthe keys, in order
union, intersection, differencesets only(Set<K>, Set<K>) -> Set<K>. Difference is the first set without the second
getproc (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.

OperationSignature
empty() -> Array<T>
length(Array<T>) -> i64
push(Array<T>, T) -> Array<T>
of_list, to_listbetween Array<T> and list::List<T>
getproc (Array<T>, i64) | (found: T & missing: String)
updateproc (Array<T>, i64, T) | (updated: Array<T> & missing: String)
slots(Array4<T>) -> i64
slot_getproc (Array4<T>, i64) | (found & missing)
slot_updateproc (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.

OperationSignature
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

OperationSignature
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}),
}
OperationWhat it answers
repeatthe same value at every demand
count_fromn, then n + 1, then onward
iteratex, then f(x), then onward
unfoldeach step answers an element and the next seed
mapf on each demanded element
zippairs of demanded elements
dropthe stream after n demands, performed while drop runs
takethe 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> }
OperationSignature
of_list(List<T>) -> Seq<T>
to_list(Seq<T, ..E>) -> List<T> / {..E}
of_streama by-name Stream becomes a Seq
map, filterstep the sequence, performing the function's row on demand
take(Seq<T, ..E>, i64) -> Seq<T, ..E>
take_whilea 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.

OperationOutcomes
readproc (path) | (ok: String & failed: String)
writeproc (path, contents) | (ok: unit & failed: String)
openproc (path) | (opened: File & failed: String)
read_lineproc (file) | (line: String & end: unit)
close(File) -> (,) / {Fs}
exists(String) -> Bool / {Fs}
read_dirproc (path) | (ok: List<String> & failed: String). Names only, in string order, without . or ..
removeproc (path) | (ok: unit & failed: String). A file, a symlink, or an empty directory
renameproc (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

OperationArgumentsResult
str_lenStringi64, the number of characters
str_concatString, StringString
substringString, start i64, end i64the characters from start up to end
indexString, i64char. This is the prelude function over __index
int_to_stri64String
str_eqString, StringBool
char_to_codechari64
is_digitcharBool
is_wscharBool
skip_wsString, i64the index after the whitespace
skip_digitsString, i64the 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

OperationValuesContinuations
parse_inttext Stringok of i64, invalid of String, overflow of String
char_attext String, index i64ok of char, out_of_range of String
find_chartext String, from i64, character code i64found 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.

OperationRole
__read_filefs::read, outcomes text or why
__write_filefs::write
__open_filefs::open, a File or why
__read_linefs::read_line, a line or the end
__close_filefs::close
__file_existsfs::exists
__read_dir, __dir_entryfs::read_dir. The count, then one name per index
__remove_pathfs::remove
__rename_pathfs::rename
__argument_count, __argument_atargs::arguments
__monotonic_nsclock::now
__env_get, __env_setenv::get and env::set
__sleep_nssleep::sleep
__spawn_begin, __spawn_push, __spawn_execprocess::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:

PhaseWhat it refuses
parseA token stream that is not a program
resolveA name that does not denote, or a cite that collides
traitAn impl that does not match its spec, or that overlaps another
typeA term whose type does not meet its port
polarityA value or continuation used at the wrong polarity
exhaustivenessAn of or mu that misses a shape, repeats one, or overlaps
effectAn effect the row does not allow, or a handler that does not cover it
loweringA 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 Into conversion or a sqrt that does not fit
  • a string index or substring outside the string
  • a read through a File that has been closed
  • a continuation that jumps across a reset which 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