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

Types

Sage has a simple but expressive type system.

Primitive Types

TypeDescriptionExample
Int64-bit signed integer42, -17
Float64-bit floating point3.14, -0.5
BoolBooleantrue, false
StringUTF-8 string"hello"
UnitNo value (like Rust’s ())

Compound Types

List<T>

Ordered collection of elements:

let numbers: List<Int> = [1, 2, 3];
let names: List<String> = ["Alice", "Bob"];
let empty: List<Int> = [];

Map<K, V>

Key-value collections:

let ages: Map<String, Int> = {"alice": 30, "bob": 25};
let alice_age = map_get(ages, "alice");  // Option<Int>

map_set(ages, "charlie", 35);
let has_bob = map_has(ages, "bob");      // true
let keys = map_keys(ages);               // List<String>

Tuples

Fixed-size heterogeneous collections:

let pair: (Int, String) = (42, "hello");
let first = pair.0;   // 42
let second = pair.1;  // "hello"

// Tuple destructuring
let (x, y) = pair;

// Three-element tuple
let triple: (Int, String, Bool) = (1, "test", true);

Option<T>

Optional values:

let some_value: Option<Int> = Some(42);
let no_value: Option<Int> = None;

// Pattern matching on Option
match some_value {
    Some(n) => print("Got: " ++ str(n)),
    None => print("Nothing"),
}

Result<T, E>

Success or error values:

let success: Result<Int, String> = Ok(42);
let failure: Result<Int, String> = Err("not found");

match success {
    Ok(value) => print("Value: " ++ str(value)),
    Err(msg) => print("Error: " ++ msg),
}

Fn(A, B) -> C

Function types for closures and higher-order functions:

let add: Fn(Int, Int) -> Int = |x: Int, y: Int| x + y;
let double: Fn(Int) -> Int = |x: Int| x * 2;

fn apply(f: Fn(Int) -> Int, x: Int) -> Int {
    return f(x);
}

let result = apply(double, 21);  // 42

User-Defined Types

Records

Define structured data with named fields:

record Point {
    x: Int,
    y: Int,
}

record Person {
    name: String,
    age: Int,
}

Construct records and access fields:

let p = Point { x: 10, y: 20 };
let sum = p.x + p.y;

let person = Person { name: "Alice", age: 30 };
print(person.name);

Records can also be generic. See Generics for details:

record Pair<A, B> {
    first: A,
    second: B,
}

let pair = Pair { first: 42, second: "hello" };

Enums

Define types with a fixed set of variants:

enum Status {
    Active,
    Inactive,
    Pending,
}

enum Direction {
    North,
    South,
    East,
    West,
}

Use enum variants directly:

let s = Active;
let d = North;

Enum Payloads

Enums can carry data:

enum Result {
    Ok(Int),
    Err(String),
}

enum Message {
    Text(String),
    Number(Int),
    Pair(Int, String),
}

// Construct variants with payloads
let success = Result::Ok(42);
let failure = Result::Err("not found");
let msg = Message::Pair(1, "hello");

Enums can also be generic. See Generics for details:

enum Either<L, R> {
    Left(L),
    Right(R),
}

let e = Either::<String, Int>::Left("error");

Match Expressions

Pattern match on enums and other values:

fn describe(s: Status) -> String {
    return match s {
        Active => "running",
        Inactive => "stopped",
        Pending => "waiting",
    };
}

Match on integers with a wildcard:

fn classify(n: Int) -> String {
    return match n {
        0 => "zero",
        1 => "one",
        _ => "many",
    };
}

Pattern Matching with Payloads

Bind payload values in match arms:

fn unwrap_result(r: Result) -> String {
    return match r {
        Ok(value) => str(value),
        Err(msg) => msg,
    };
}

fn handle_message(m: Message) -> String {
    return match m {
        Text(s) => s,
        Number(n) => str(n),
        Pair(n, s) => str(n) ++ ": " ++ s,
    };
}

The compiler checks that all variants are covered (exhaustiveness checking).

Constants

Define compile-time constants:

const MAX_RETRIES: Int = 3;
const DEFAULT_NAME: String = "anonymous";

Agent Types

Agent<T>

A handle to a summoned agent that will yield a value of type T:

agent Worker {
    on start {
        yield(42);
    }
}

agent Main {
    on start {
        let w: Agent<Int> = summon Worker {};
        let result: Int = try await w;
        yield(result);
    }

    on error(e) {
        yield(0);
    }
}

run Main;

Oracle<T>

The result of a divine call:

let summary = try divine("Summarize: {topic}");

Oracle<T> can be used anywhere T is expected — the type coerces automatically.

Type Inference

Sage infers types when possible:

let x = 42;              // Int
let name = "Sage";       // String
let list = [1, 2, 3];    // List<Int>

Explicit annotations are required for:

  • Function parameters
  • Agent state fields
  • Closure parameters
  • Ambiguous cases

Type Annotations

Use : Type syntax:

let x: Int = 42;
let items: List<String> = [];

fn double(n: Int) -> Int {
    return n * 2;
}

agent Worker {
    count: Int

    on start {
        yield(self.count * 2);
    }
}