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.
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}]")
}
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.
fn main() {
let rounds = 0
rounds += 1
print(rounds)
}
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.
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")
}
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.
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)
}
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.
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)
}
}
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.
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
}
}
}
}
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.
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)
}
1 -1 Kalle is at 2 41 0
You unwrap an optional in one of these ways:
a ?? bgives the value, orbwhenaisNone.bmay bepanic("why").if let x = aruns its block with the value bound tox;while letloops while there is one.a?.fieldanda?.method()reach through an optional and give an optional.match awith the armsSome(x) =>andNone =>handles both cases.i64?is short forOption<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.
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))
}
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.
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}")
}
}
3.14 6.00 0.00
A match must cover every value. Leave out the Point arm and the program does not compile:
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.
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)}")
}
}
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.
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])
}
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.
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))
}
$ ./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).
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}]")
}
}
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
Resultand?planned - Modules planned
Planned work can change and has no promised date. The status page lists what works today and what is planned.
Sisu