Albite

Values and types

Built-in types

TypeDescription
NatNatural numbers (unsigned integers)
IntSigned integers — since 2.0-alpha, see below
Booltrue and false
StringUTF-8 text
UnitThe unit type, with the single value ()

Literals

42          // Nat, unless the context asks for an Int
-7          // Int: a minus sign makes an Int
true        // Bool
false       // Bool
"hello"     // String
()          // Unit
(1, 2)      // Tuple

Operators

OperatorDescriptionExample
+Additiona + b
-Subtractiona - b
*Multiplicationa * b
/Divisiona / b
%Remaindera % b
==Equalityx == y
!=Inequalityx != y
<Less thana < b
>Greater thana > b
<=Less than or equala <= b
>=Greater than or equala >= b
&&Logical ANDp && q
||Logical ORp || q
++String concatenation"a" ++ "b"

Where precedence is not obvious, parenthesise. It costs nothing and reads better than a rule the reader has to recall.

On Nat, subtraction saturates: 3 - 5 is 0, because there is no negative natural number to be. Use Int when a difference can go below zero.

Signed integers: Int

Since 2.0-alpha — Int, signed literals, overflow traps, to_int and from_int, and match on numbers.

Int is a signed 64-bit integer, beside Nat rather than replacing it. A literal is a Nat unless its context asks for an Int — an annotation, a parameter, a return type — or it follows a minus sign. This is tests/golden/check/docs_int.tg in the compiler repository:

// `Int` is the signed integer. A literal is a `Nat` unless the context
// asks for an `Int`, or it follows a minus sign.
fn balance(deposits: Int, withdrawals: Int) -> Int {
    deposits - withdrawals
}

// Integer `match`: literal, negative-literal and or-patterns, guards, and
// exactly one unguarded catch-all.
fn sign(x: Int) -> Int {
    match x {
        0 => 0,
        n if n < 0 => -1,
        _ => 1,
    }
}

fn describe(n: Nat) -> String {
    match n {
        0 => "none",
        1 | 2 => "a few",
        _ => "many",
    }
}

fn main() -> Nat {
    let owed: Int = balance(to_int(3), to_int(5));   // -2
    let halved: Int = -7 / 2;                         // -3: toward zero
    let left: Int = -7 % 2;                           // -1: sign of the dividend
    let n: Nat = 3;
    let floor: Nat = n - 5;                           // 0: Nat subtraction saturates
    from_int(sign(owed) + halved + left + to_int(floor) + 10)
}

albite run prints 5. What to know:

  • Division truncates toward zero — -7 / 2 is -3 — and % takes the sign of the dividend, so -7 % 2 is -1.
  • Overflow aborts. +, -, * and negation trap on overflow, and / and % trap on a zero divisor, with one named line such as error: integer overflow in +. The same line, in the interpreter and in a compiled binary; there is no wrapping and no debug/release difference.
  • Converting. to_int(n) turns a Nat into an Int (and traps above the largest Int); from_int(i) turns an Int back into a Nat, clamping a negative value to 0. Both are built in.
  • Negating a Nat is refused — the compiler suggests -to_int(n).
  • match on a number — on Int or Nat — takes literal, negative-literal and | patterns, variable patterns and if guards, and needs exactly one catch-all without a guard, because literals cannot cover every integer.

The compiler’s own runtime suite for these rules is src/compiler/test_int_ops.tg in the repository.

Strings

Strings are concatenated with ++. Both operands must already be strings, so a Nat needs converting first — and there is no built-in conversion, so you write one. This is tests/golden/check/docs_nat_to_string.tg:

// There is no built-in `Nat` to `String` conversion: write one. Peeling off
// digits with `/ 10` is a measure, not a structural descent, so the helper is
// `#[partial]` — ordinary for executable code.
fn digit(d: Nat) -> String {
    match d {
        0 => "0", 1 => "1", 2 => "2", 3 => "3", 4 => "4",
        5 => "5", 6 => "6", 7 => "7", 8 => "8", _ => "9",
    }
}

#[partial]
fn digits(n: Nat, acc: String) -> String {
    if n == 0 { acc } else { digits(n / 10, digit(n % 10) ++ acc) }
}

fn nat_to_string(n: Nat) -> String {
    if n == 0 { "0" } else { digits(n, "") }
}

fn main() -> String {
    "Expected " ++ nat_to_string(42) ++ " arguments"
}

#[partial] is explained in Recursion the compiler can see ends. When a string is built from many pieces, ++ copies on every step; see StringBuilder.

There is no string interpolation. "Hello, {name}" is a string containing literal braces, not a substitution — build strings with ++ and conversion functions instead.

Tuples

Tuples group a fixed number of values of possibly different types:

fn make_pair() -> (Nat, Nat) {
    (1, 2)
}

fn make_triple() -> (Nat, Nat, Nat) {
    (10, 20, 30)
}

Take them apart by destructuring in a let, with _ for anything you do not need:

let (a, b) = make_pair();
let (x, (y, z)) = make_nested();   // nested
let (_, b) = make_pair();          // ignore the first

Bindings and the semicolon rule

let introduces a binding, and its type annotation is optional whenever the type can be inferred:

let x = 42;
let name: String = "hello";

The rule that trips people up most often:

let bindings end with a semicolon. The final expression of a block does not — that expression is the block’s value.

fn example() -> Nat {
    let x = 1;          // semicolon: this is a binding
    let y = 2;          // semicolon: this is a binding
    x + y               // no semicolon: this is the result
}

Put a semicolon after x + y and the block’s value becomes Unit, which will usually surface as a type mismatch against the declared return type.

Blocks

Curly braces introduce a block, and the same rule applies — the last expression is its value:

let result = {
    let (x, y) = make_pair();
    x + y
};

Comments

// A line comment.

/// A doc comment, attached to the declaration that follows.
/// It may span several lines.