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

Values and variables

Every value in rut has a type, and every type is known at compile time. This chapter covers the primitive types, how literals infer their types, how bindings work, and the one rule that organizes everything else: primitives copy, every other value is a shared cell.

For the full grammar of literals and inference rules, see the reference on literals and inference.

The primitive types

GroupTypesNotes
unsigned integersu8 u16 u32 u64fixed width
signed integersi8 i16 i32 i64two’s complement
floatsf32 f64IEEE 754
booleanbooltrue / false
textstrimmutable UTF-8, compared by content
binarybytesimmutable octet buffer, compared by content
erasedopaquea box holding any value — see errors and optionality

There is no null, no undefined, and no character type. A str iterates as one-codepoint strs, and codepoints read as u32 (s.code(), s.code_at(i)) — see string slicing and views.

Variables: let and let mut

use ink::{ Logger };

pub fn main() {
    let log = Logger.new("vars");
    let a = 10;          // an immutable binding
    let mut b = 10;      // a mutable binding
    b = 20;              // OK — b may be reassigned
    // a = 30;           // ERROR: a is not `mut`
    log.info(f"a={a} b={b}");
}
a=10 b=20

mut is permission to write through that name: reassigning the binding, assigning to a field (p.x = 3), or assigning to an element (xs[0] = 7). Sharing is not gated by mut — two bindings can name the same value, and what mut controls is only who may write.

Every type has a zero value: 0 for numbers, false for bool, the empty string, nil for nullables. Omit a field in a struct literal and it takes the field’s initializer if there is one, else the type’s zero value (see structs, enums, and classes).

Numbers

An unsuffixed integer literal defaults to i32; an unsuffixed float defaults to f32. The default is also a ceiling: a literal adapts to the expected type only while it fits the default.

use ink::{ Logger };

pub fn main() {
    let log = Logger.new("numbers");
    let a = 10;                        // i32
    let b = 10u8;                      // u8 via suffix
    let c: u64 = 10;                   // u64 via annotation — 10 fits
    let big = 18446744073709551615u64; // past the i32 default: suffix required
    let d = 1.5;                       // f32
    let d64: f64 = 1.5;                // f64 via annotation
    let hex = 0xFF_u32;                // 0x / 0b / 0o bases, _ separators
    log.info(f"a={a} d={d} d64={d64} hex={hex} big={big}");
}
a=10 d=1.5 d64=1.5 hex=255 big=18446744073709551615

Conversions are explicit casts: expr as T. Casts truncate like C — they never trap.

use ink::{ Logger };

pub fn main() {
    let log = Logger.new("casts");
    let cast = 300 as u8;   // 44 — keeps the low 8 bits
    log.info(f"cast={cast}");
}
cast=44

Arithmetic has two sharp edges:

  • Mixed widths do not mix. Both operands must have the same width — convert one side first. let mut total = 0.0; makes an f32, and adding an f64 to it is a type error; annotate let mut total: f64 = 0.0; when you mean double precision.
  • Overflow, division by zero, and out-of-bounds indexing trap. For the explicit non-trapping ladder (wrapping_add, saturating_mul, checked_add), see the standard library.

Text: plain, raw, and format strings

Three literal forms. A plain string is always inert — no interpolation ever happens implicitly.

use ink::{ Logger };

pub fn main() {
    let log = Logger.new("text");
    let name = "rut";
    let s = "hi\tname";              // plain: escapes processed
    let raw = r"C:\temp\log.txt";    // raw: every byte is literal
    let t = f"hi {name}!";           // format: placeholders evaluated
    log.info(f"{s} | {raw} | {t}");
}
hi	name | C:\temp\log.txt | hi rut!

Plain and format strings share the same escapes (\t \n \r \\ \" and \u{...}). In an f-string, { expr } splices any expression — identifiers, calls, arithmetic — except nested string literals (bind one to a variable first). {{ and }} are literal braces:

use ink::{ Logger };

pub fn main() {
    let log = Logger.new("text");
    log.info(f"open{{close}} braces");   // prints: open{close} braces
}
open{close} braces

What an f-string can render: integers (decimal), floats (shortest round-trip decimal: 3.5, 0.1), bool (true/false), str (contents), and enum members (their name: Color.Green renders Green). Composite values — structs, classes, arrays, vecs — are a compile error inside an f-string; write a to_string()-style method and call it, or use a debug dump for development.

Sequences and buffers

The fixed array [T] is built from a literal or a repeat:

use ink::{ Logger };

pub fn main() {
    let log = Logger.new("arrays");
    let arr = [1, 2, 3];         // [i32] — fixed length
    let zero: [u8] = [0u8; 34];  // 34 slots of 0
    log.info(f"len={arr.len()} first={arr[0]} zero.len={zero.len()}");
}
len=3 first=1 zero.len=34

The growable sequence is Vec<T> (from the pouch package — use pouch::{ Vec };): Vec.new(), Vec.from([..]), Vec<i32>.filled(0, 1024), push, pop, len. Indexing and for..of work the same on both. See the standard library for the full surface.

bytes is the binary primitive. s.encode() turns text into octets, b.decode() reads it back (lossy UTF-8), bytes.zeroed(n) and bytes.from(a) build buffers directly:

use ink::{ Logger };

pub fn main() {
    let log = Logger.new("bytes");
    let b = "rut runs".encode();
    let ok = b.decode() == "rut runs";   // true — content comparison
    log.info(f"len={b.len()} roundtrip={ok}");
}
len=8 roundtrip=true

Sharing: the one rule

Primitives and fn values copy on assignment, argument passing, and return. Every other value is a shared cell: let q = p is an O(1) handle move, and a write through any alias is visible through all of them.

use ink::{ Logger };

struct Point { x: i32; y: i32 }

pub fn main() {
    let log = Logger.new("sharing");
    let mut p = Point { x: 1, y: 2 };
    let q = p;          // q and p name ONE cell
    p.x = 4;            // q.x is 4 now
    log.info(f"q.x={q.x} same cell: {q == p}");
}
q.x=4 same cell: true

Equality follows the same split:

Operands== means
numbers, boolvalue comparison
str, bytescontent comparison
everything elsecell identity — two separately-built literals are never equal

The one copy escape hatch is bytes.clone() — a fresh buffer with the same octets. There is no generic copy for any other type; if you need a divergent value, build a new one.

Absence is nil on a nullable ?T — see errors and optionality.

Tuples

Records (A, B) are first-class values with numeric fields:

use ink::{ Logger };

fn divmod(a: i32, b: i32) -> (i32, i32) {
    return (a / b, a % b);
}

pub fn main() {
    let log = Logger.new("tuples");
    let (q, r) = divmod(17, 5);   // destructuring
    let t = (1, true);
    log.info(f"q={q} r={r} t.0={t.0}");
}
q=3 r=2 t.0=1

The pair is also rut’s standard error channel — the next chapters use it constantly.

Put it together

Save this as literals.rut and run rut run literals.rut:

use pouch::{ Vec };
use ink::{ Logger };

struct Point { x: f32; y: f32 }

fn literals(name: str) {
    let log = Logger.new("literals");
    let a = 10;                      // i32 (default)
    let b = 10u8;                    // u8 via suffix
    let c: u64 = 10;                 // u64 via annotation — fits the default
    let big = 18446744073709551615u64; // past the `i32` default: suffix REQUIRED
    let cast = 300 as u8;            // 44 — `as` truncates
    let d = 1.5;                     // f32 (default)
    let e = 1.5f32;                  // f32 via suffix
    let d64: f64 = 1.5;              // f64 via annotation
    let s = "hi\tname";              // plain string: escapes processed
    let raw = r"C:\temp\log.txt";    // raw literal: NO escape processing
    let t = f"hi {name}!";           // format literal
    let ch = "h";                    // a 1-codepoint str
    let mut arr = [1, 2, 3];         // [i32] — fixed array
    let mut zero: Vec<f32> = Vec<f32>.filled(0.0, 1024);  // growable, flat
    let grow = Vec<i32>.from([1, 2, 3]);  // array -> growable
    arr[0] = 7;                      // element write needs `mut`
    zero[0] = 9.0f32;
    let bin = bytes(64);             // 64 zeroed octets
    let p = Point { x: 1, y: 2 };    // struct literal: no `new`

    log.info(f"a={a} e={e} d64={d64} ch={ch} p.x={p.x} zero[0]={zero[0]} len={grow.len()} bin={bin.len()}");
}

pub fn main() {
    literals("rut");
}
a=10 e=1.5 d64=1.5 ch=h p.x=1 zero[0]=9 len=3 bin=64

Next: control flow and when.