Values and types
Built-in types
| Type | Description |
|---|---|
Nat | Natural numbers (unsigned integers) |
Int | Signed integers — since 2.0-alpha, see below |
Bool | true and false |
String | UTF-8 text |
Unit | The 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
| Operator | Description | Example |
|---|---|---|
+ | Addition | a + b |
- | Subtraction | a - b |
* | Multiplication | a * b |
/ | Division | a / b |
% | Remainder | a % b |
== | Equality | x == y |
!= | Inequality | x != y |
< | Less than | a < b |
> | Greater than | a > b |
<= | Less than or equal | a <= b |
>= | Greater than or equal | a >= b |
&& | Logical AND | p && q |
|| | Logical OR | p || 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_intandfrom_int, andmatchon 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 / 2is-3— and%takes the sign of the dividend, so-7 % 2is-1. - Overflow aborts.
+,-,*and negation trap on overflow, and/and%trap on a zero divisor, with one named line such aserror: 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 aNatinto anInt(and traps above the largestInt);from_int(i)turns anIntback into aNat, clamping a negative value to0. Both are built in. - Negating a
Natis refused — the compiler suggests-to_int(n). matchon a number — onIntorNat— takes literal, negative-literal and|patterns, variable patterns andifguards, 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:
letbindings 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.