Sisu

A tour of Sisu

The language as it works today, one small program at a time, in about ten minutes of reading.

The working programs on this page compile with sisu today. Every output and error shown is what sisu really prints. Checked against the repository at commit 3c93176.

Hello and printing

A program starts at main. print writes one value and a line break. Statements end at the end of the line; a ; puts two on one line.

hello.sisu
fn main() {
    print("Hello, sauna")
    let name = "Väinö"
    let temp = 81.456
    print("{name} sets the sauna to {temp:.1} degrees")
    print("[{name:>8}]")
}
output
Hello, sauna
Väinö sets the sauna to 81.5 degrees
[   Väinö]

Braces inside a string insert a value. After a colon comes a format spec: an alignment (<, >, ^), a width in characters and a number of decimals. Compile with target/debug/sisu build hello.sisu, then run ./hello.

let and let mut

let binds a name that cannot be reassigned. let mut binds one that can. Types are inferred.

rounds.sisu
fn main() {
    let rounds = 0
    rounds += 1
    print(rounds)
}
sisu build rounds.sisu
error[E0001]: cannot assign to `rounds`
 --> rounds.sisu:3:5
  |
2 |     let rounds = 0
  |         ------ declared with `let` here
3 |     rounds += 1
  |     ^^^^^^^^^^^ cannot assign twice
  |
help: declare it with `let mut`
  |
2 |     let mut rounds = 0
  |         +++

sisu: 1 error
sisu: for more on a code, run `sisu explain <code>`: E0001

A class object works differently. Its fields can change only if the field itself is declared mut, whatever binding holds the object. Objects are shared, so the field decides, not the binding.

player.sisu
class Player {
    name: string
    mut hp: i64
}

fn main() {
    let p = Player(name: "Aino", hp: 10)
    p.hp -= 3
    print("{p.name} has {p.hp} hp")
}
output
Aino has 7 hp

Writing p.name = "Eino" in the same program is the same error, E0001, with the help line mark the field `mut`.

Functions and if

A function names the type of each parameter and of its result. The last expression in a body is its value. if is an expression too, so it can be a function's result or the right side of a let.

functions.sisu
fn fib(n: i64) -> i64 {
    if n < 2 { n } else { fib(n - 1) + fib(n - 2) }
}

fn sign(n: i64) -> string {
    if n < 0 {
        "negative"
    } else if n == 0 {
        "zero"
    } else {
        "positive"
    }
}

fn main() {
    print(fib(30))
    let label = sign(-4)
    print(label)
}
output
832040
negative

Loops

for walks a range or an array. 0..3 leaves out its end and 0..=3 includes it. while loops on a condition, and break and continue work in both. loop runs until a break. A loop with no break never ends, so a function can return from inside it with nothing after it. while true works the same, but warns and points at loop.

loops.sisu
fn main() {
    for i in 0..3 {
        print(i)
    }
    let names = ["Aino", "Eino", "Kalle"]
    for name in names {
        print(name)
    }
    let mut n = 1
    loop {
        n *= 3
        if n == 9 { continue }
        if n > 100 { break }
        print(n)
    }
}
output
0
1
2
Aino
Eino
Kalle
3
27
81

break in a loop can carry a value, which is the value of the loop. A name before a loop is a loop label: break outer and continue outer leave or restart that loop instead of the innermost one.

search.sisu
fn main() {
    let mut n = 1
    let first = loop {
        n *= 2
        if n > 100 { break n }
    }
    print(first)
    outer: for i in 1..10 {
        for j in 1..10 {
            if i * j == 12 {
                print("{i} x {j}")
                break outer
            }
        }
    }
}
output
128
2 x 6

Optionals instead of null

Sisu has no null. A value that may be missing has an optional type, such as i64?, and is either a value or None. An i64? is not an i64: x + 1 on one does not compile, so you deal with None before you use the value.

find.sisu
fn find(names: [string], wanted: string) -> i64? {
    for i in 0..names.len() {
        if names[i] == wanted { return i }
    }
    None
}

fn main() {
    let names = ["Aino", "Eino", "Kalle"]
    print(find(names, "Eino") ?? -1)
    print(find(names, "Ilmari") ?? -1)
    if let i = find(names, "Kalle") {
        print("Kalle is at {i}")
    } else {
        print("no Kalle")
    }
    let whole = i64(41.0) ?? panic("not a whole number")
    print(whole)
    print(i64(41.5) ?? 0)
}
output
1
-1
Kalle is at 2
41
0

You unwrap an optional in one of these ways:

  • a ?? b gives the value, or b when a is None. b may be panic("why").
  • if let x = a runs its block with the value bound to x; while let loops while there is one.
  • a?.field and a?.method() reach through an optional and give an optional.
  • match a with the arms Some(x) => and None => handles both cases. i64? is short for Option<i64>, an enum that every program has.

A conversion that can fail returns an optional instead of panicking. i64(41.5) is None, because 41.5 is not a whole number.

Classes

A class declares its fields one per line and its methods after them. A method takes self and may write any mut field. Objects are freed by reference counting when the last reference goes away.

counter.sisu
class Counter {
    name: string
    mut count: i64

    fn tick(self) {
        self.count += 1
    }

    fn report(self) -> string {
        "{self.name}: {self.count}"
    }
}

fn main() {
    let a = Counter(name: "door", count: 0)
    let b = a
    b.tick()
    a.tick()
    print(a.report())
    print(a is b)
    print(a is Counter(name: "door", count: 2))
}
output
door: 2
true
false

let b = a does not copy the object: a and b hold the same counter, so both ticks count. is asks whether two references hold the same object.

planned Today == on two objects compares their fields. A recorded decision changes that: a class will have == only when it asks for one, and is stays the identity test.

Enums and match

An enum lists its variants, and each variant can carry data. match takes a value apart, one arm per line, and gives a value.

area.sisu
enum Shape {
    Circle(r: f64)
    Rect(w: f64, h: f64)
    Point
}

fn area(s: Shape) -> f64 {
    match s {
        Circle(r) => f64.pi * r * r
        Rect(w, h) => w * h
        Point => 0.0
    }
}

fn main() {
    let shapes = [Shape.Circle(r: 1.0), Shape.Rect(w: 2.0, h: 3.0), Shape.Point]
    for s in shapes {
        print("{area(s):.2}")
    }
}
output
3.14
6.00
0.00

A match must cover every value. Leave out the Point arm and the program does not compile:

sisu build area.sisu
error[E0130]: `match` does not cover `Point`
  --> area.sisu:8:5
   |
 8 |     match s {
   |     ^^^^^
   |
help: add a `_ =>` arm
   |
11 +         _ => todo
   |

sisu: 1 error
sisu: for more on a code, run `sisu explain <code>`: E0130

Patterns can also be literals, alternatives joined with |, and ranges. An arm can add a guard with if.

describe.sisu
fn describe(n: i64) -> string {
    match n {
        0 => "none"
        1 | 2 | 3 => "a few"
        4..=9 => "several"
        n if n < 0 => "below zero"
        _ => "many"
    }
}

fn main() {
    for n in [0, 2, 7, -5, 40] {
        print("{n}: {describe(n)}")
    }
}
output
0: none
2: a few
7: several
-5: below zero
40: many

Arrays are values

Assigning an array, or passing it to a function, behaves as a copy. No other holder can change your array. The copy is made only when one holder writes, so reading or passing an array never copies it.

arrays.sisu
fn zero_first(xs: [i64]) -> [i64] {
    let mut ys = xs
    ys[0] = 0
    ys
}

fn main() {
    let a = [1, 2, 3]
    let mut b = a
    b[0] = 99
    print(a[0])
    print(b[0])
    let c = zero_first(a)
    print(a[0])
    print(c[0])
    print(a == [1, 2, 3])
}
output
1
99
1
0
true

A function that changes an array returns the new one. Shared data that changes in place belongs in a class.

Integers panic on overflow

Integer arithmetic that overflows stops the program with a panic that names the line. A wrong answer is never silent. Division by zero panics the same way, and every panic exits with status 101.

factorial.sisu
fn factorial(n: i64) -> i64 {
    let mut f = 1
    for i in 1..=n {
        f *= i
    }
    f
}

fn main() {
    print(factorial(20))
    print(factorial(21))
}
terminal
$ ./factorial
2432902008176640000
sisu: panic at factorial.sisu:4:9: integer overflow
$ echo $?
101

Strings

A string is always valid UTF-8, and it is a value like an array. len() counts bytes, and there is no s[i]. To work on text, take it apart with chars(), bytes() or words(), and build it back with string(cs).

text.sisu
fn main() {
    let s = "Hyvää yötä"
    print(s.len())
    let cs = s.chars()
    print(cs.len())
    print(cs[3])
    print(s.bytes()[3])
    let mut back = [' '; cs.len()]
    for i in 0..cs.len() {
        back[cs.len() - 1 - i] = cs[i]
    }
    print(string(back))
    for w in s.words() {
        print("[{w}]")
    }
}
output
14
10
ä
195
ätöy äävyH
[Hyvää]
[yötä]

A char is one Unicode scalar value. The string has 10 of them in 14 bytes, because each ä and ö takes two bytes.

What is not here yet

Sisu is early, and much of an everyday language is still missing. Some of what is planned:

  • Interfaces planned
  • Closures and iterators planned
  • Errors with Result and ? planned
  • Modules planned

Planned work can change and has no promised date. The status page lists what works today and what is planned.