Chapter 10 16 min read

Prelude & built-ins

Every Glide program starts with a handful of names already in scope. These are the prelude: the println! family of macros, the raw heap primitives (malloc/calloc/free/sizeof), channel construction, CLI-argument accessors, the cooperative scheduler hooks, the panic/assert macros, process memory introspection, and the generic `Pair<K, V>` type.

Two flavours of built-in live here, and the distinction matters:

  • Codegen built-ins — declared as extern fn / macro stubs whose bodies the compiler lowers directly to C at each call site (no Glide-level implementation). malloc, make_chan, args_at, printf, sleep_ms, etc. The stub exists only so hover/goto/completion resolve and so the typer doesn't flag the implicit runtime call.
  • Macrosprintln!, print!, format!, panic!, assert!, todo!, unimplemented!, unreachable!. These expand at compile time; the diagnostic macros lower to a single __glide_panic(msg, file, line) call carrying the source location.

A couple of items (mem_rss_mb, mem_rss_bytes, Pair::new) are ordinary pub fns with real Glide bodies — thin wrappers over the codegen-emitted runtime symbols.

Import

No import needed — these are always in scope.

Public surface at a glance

Item Kind Signature
println! macro macro println!($($arg:expr),*)
print! macro macro print!($($arg:expr),*)
format! macro macro format!($($arg:expr),*)
printf codegen extern fn extern fn printf(fmt: string, ...) -> i32
make_chan codegen extern fn extern fn make_chan(cap: i32)
malloc codegen extern fn extern fn malloc(n: usize) -> *void
calloc codegen extern fn extern fn calloc(count: usize, size: usize) -> *void
free codegen extern fn extern fn free(p: *void)
sizeof built-in operator sizeof(T) -> usize
args_count codegen extern fn extern fn args_count() -> i32
args_at codegen extern fn extern fn args_at(i: i32) -> string
yield_now codegen extern fn extern fn yield_now()
sleep_ms codegen extern fn extern fn sleep_ms(ms: i32)
panic! macro panic!(msg)
assert! macro assert!(cond)
todo! macro todo!(...)
unimplemented! macro unimplemented!()
unreachable! macro unreachable!()
mem_rss_mb pub fn pub fn mem_rss_mb() -> i32
mem_rss_bytes pub fn pub fn mem_rss_bytes() -> i64
Pair<K, V> pub struct struct Pair<K, V> { pub first: K, pub second: V }
Pair::new pub fn pub fn new(first: K, second: V) -> *Pair<K, V>
__glide_panic pub extern fn extern fn __glide_panic(msg: string, file: string, line: i32)

The __glide_palloc* / __glide_proc_rss_* symbols are also pub extern fn but are internal runtime plumbing — see Internal runtime symbols.

Printing & formatting

All four are variadic. println!/print!/format! are macros; printf is a codegen extern fn that forwards to libc.

Item Signature Description
println! macro println!($($arg:expr),*) Print every argument space-separated, then a newline. Format spec inferred per arg.
print! macro print!($($arg:expr),*) Same as println! but no trailing newline.
format! macro format!($($arg:expr),*) Interpolate {} placeholders in the literal first arg with the rest; returns a heap string.
printf extern fn printf(fmt: string, ...) -> i32 Raw libc printf — C format specifiers, no Glide-side validation. Returns the byte count written.

println! / print!

Arguments are space-separated and the format spec is inferred from each argument's type, so most values print without manual formatting. They accept zero or more args of mixed types (strings, i32/i64, floats, bools, and anything with a printable representation).

fn main() -> i32 {
    let name: string = "ada";
    let age: i32 = 36;
    println!("name:", name, "age:", age);   // name: ada age: 36

    // print! composes one logical line out of several calls
    print!("loading");
    for let i: i32 = 0; i < 3; i++ {
        print!(".");
    }
    println!(" done");                        // loading... done

    // mixed bool + float args
    println!("flag:", age > 30, "ratio:", 3.5);  // flag: true ratio: 3.5
    return 0;
}

format!

Builds a string by interpolating {} placeholders in the literal first argument with the remaining arguments. Each {} consumes one extra argument; the result is heap-allocated and owned by the caller. The format spec for each {} is inferred from its argument's type, exactly like println!.

fn main() -> i32 {
    let n: i32 = 7;
    let s: string = format!("count: {}, double: {}", n, n * 2);
    println!(s);                              // count: 7, double: 14

    // the result is a normal string — store it, pass it, nest it
    let inner: string = format!("[{}]", n);
    let outer: string = format!("wrapped {}", inner);
    println!(outer);                          // wrapped [7]
    return 0;
}

Any string literal containing ${expr} is lowered to a format! call automatically, so these two are equivalent:

let a: string = format!("count: {}, double: {}", n, n * 2);
let b: string = "count: ${n}, double: ${n * 2}";

printf

The escape hatch for genuine C printf behaviour — width, precision, hex, scientific notation. It returns the number of bytes written (the libc return value). Most code should prefer print!/println!, which pick the format spec for you.

fn main() -> i32 {
    printf("hex: %08x\n", 255);              // hex: 000000ff
    printf("pi ~= %.4f\n", 3.14159);         // pi ~= 3.1416
    printf("%-10s | %5d\n", "items", 42);    // items      |    42
    printf("%d + %d = %d\n", 2, 3, 5);       // 2 + 3 = 5

    let written: i32 = printf("%s\n", "hi"); // hi
    println!("wrote", written, "bytes");     // wrote 3 bytes
    return 0;
}

Channels

make_chan

extern fn make_chan(cap: i32);

Constructs a typed channel. The element type is inferred from the let annotation; cap is the number of pending sends the channel can buffer before send blocks. Channels are bounded MPMC queues built on a Vyukov ring with cache-padded cells, a 256-iter pause-spin slow path, and conditional cv signalling — lock-free in the fast path, no syscall when there are no waiters. Operate with c.send(x), c.recv(), c.close(), or drain with while let v = c.recv() { … }.

fn main() -> i32 {
    let c: chan<i32> = make_chan(8);
    c.send(1);
    c.send(2);
    c.send(3);
    c.close();
    while let v = c.recv() {
        println!("got", v);                  // got 1 / got 2 / got 3
    }
    return 0;
}

A producer running on another task feeds the channel; the consumer drains until close():

struct Job {
    n: i32,
}

fn produce(c: chan<*Job>) {
    for let i: i32 = 0; i < 3; i++ {
        let j: *Job = malloc(sizeof(Job)) as *Job;
        j.n = i;
        c.send(j);
    }
    c.close();
}

fn main() -> i32 {
    let c: chan<*Job> = make_chan(4);
    spawn produce(c);
    while let j = c.recv() {
        println!("job", j.n);                // job 0 / job 1 / job 2
        free(j as *void);
    }
    return 0;
}

Raw heap allocation

These are libc escape hatches lowered directly by codegen. Pair every allocation with a matching free (or wrap it in defer free(p as *void);).

Item Signature Description
malloc extern fn malloc(n: usize) -> *void Allocate n uninitialised bytes. null on OOM.
calloc extern fn calloc(count: usize, size: usize) -> *void Allocate count * size bytes, zero-initialised.
free extern fn free(p: *void) Release a malloc/calloc pointer. null is a no-op; double-free is UB.
sizeof built-in operator Byte size of a type, e.g. sizeof(Item), sizeof(Pair<i32, i32>).

The cast/free idiom

malloc/calloc return *void. The canonical pattern is: size the allocation with sizeof(T), cast the result to *T, work with it, then cast back to *void for free.

struct Item {
    id: i32,
    score: i32,
}

fn main() -> i32 {
    // single object: malloc(sizeof(T)) as *T
    let one: *Item = malloc(sizeof(Item)) as *Item;
    one.id = 5;
    one.score = 99;
    println!("item", one.id, one.score);     // item 5 99
    free(one as *void);

    // zeroed array: calloc(count, size) — fields start at 0
    let arr: *Item = calloc(10, sizeof(Item)) as *Item;
    arr.id = 1;
    println!("zeroed score:", arr.id, arr.score);  // zeroed score: 1 0
    free(arr as *void);

    // raw byte buffer + defer-free so it can't leak on early return
    let buf: *u8 = malloc(64) as *u8;
    defer free(buf as *void);

    // sizeof works on generic instantiations too
    println!("sizeof Item =", sizeof(Item));
    println!("sizeof Pair<i32,i32> =", sizeof(Pair<i32, i32>));
    return 0;
}

CLI arguments

Item Signature Description
args_count extern fn args_count() -> i32 Number of CLI arguments (mirrors C's argc).
args_at extern fn args_at(i: i32) -> string Argument at index i (mirrors argv[i]).

Index 0 is the program path; indices 1..args_count() are user arguments. A missing index returns "".

fn main() -> i32 {
    let prog: string = args_at(0);           // path to the binary
    println!("program:", prog);
    for let i: i32 = 1; i < args_count(); i++ {
        println!("arg", i, "=", args_at(i));
    }
    return 0;
}

Scheduler helpers

These cooperate with Glide's M:N scheduler.

Item Signature Description
sleep_ms extern fn sleep_ms(ms: i32) Sleep at least ms ms. Async inside a coroutine (parks on the timer thread); blocking libc sleep outside one.
yield_now extern fn yield_now() Cooperative yield — re-queue the current task and run another. No-op outside a coroutine.
fn main() -> i32 {
    for let i: i32 = 0; i < 3; i++ {
        println!("tick", i);
        yield_now();                         // give other tasks a turn
        sleep_ms(1);                          // park ~1ms
    }
    return 0;
}

sleep_ms inside a coroutine parks the coro on the runtime's timer thread, freeing the worker to run another ready task immediately. Outside a coroutine (e.g. in main before any spawn) it falls back to the blocking libc nanosleep / WinAPI Sleep.

Diagnostic macros

These macros abort the program with a message and source location. They all lower to a single __glide_panic(msg, file, line) call that the expander fills in with the call-site path and line.

Macro Use
panic!(msg) Abort immediately with msg and the call site location.
assert!(cond) Abort if cond is false.
todo!(...) Mark an unfinished branch; aborts if reached.
unimplemented!() Mark a deliberately unimplemented path; aborts if reached.
unreachable!() Assert a branch can never run; aborts if it does.
fn classify(n: i32) -> string {
    if n < 0 {
        panic!("negative input");
    }
    if n == 0 {
        return "zero";
    }
    return "positive";
}

fn main() -> i32 {
    assert!(1 + 1 == 2);
    println!(classify(5));                    // positive
    if false {
        todo!("handle the other case");
    }
    if false {
        unimplemented!();
    }
    if false {
        unreachable!();
    }
    return 0;
}

assert! pairs well with functions that have invariants. It aborts (it does not return a !T), so use it for programmer errors, not recoverable conditions:

fn checked_div(a: i32, b: i32) -> i32 {
    assert!(b != 0);                          // aborts on misuse
    return a / b;
}

fn main() -> !i32 {
    println!("div:", checked_div(10, 2));     // div: 5
    return ok(0);
}

Memory introspection

Resident-set-size of the current process. Both return -1 on platforms that don't expose RSS. These are real pub fn wrappers over the platform-specific runtime probes.

Item Signature Description
mem_rss_mb pub fn mem_rss_mb() -> i32 Process RSS in megabytes (1 MiB = 1024×1024).
mem_rss_bytes pub fn mem_rss_bytes() -> i64 Process RSS in bytes. Handy for per-task math.
fn main() -> i32 {
    let before: i64 = mem_rss_bytes();
    println!("rss MB:", mem_rss_mb());
    println!("rss bytes:", before);

    // do some work, then measure the delta
    let after: i64 = mem_rss_bytes();
    println!("delta bytes:", after - before);
    return 0;
}

mem_rss_bytes returns i64, so cast task counts to i64 for per-task math: rss / (n as i64).

These are thin Glide wrappers over the platform-specific runtime probes (GetProcessMemoryInfo on Windows, /proc/self/statm on Linux, Mach task_info on macOS). On an unsupported platform both return -1 — check for it before dividing.

Pair<K, V>

The minimum-viable tuple — Glide has no anonymous tuples yet, so APIs that return two things use this struct. Notably, HashMap::entries surfaces key/value pairs as a *Vector<Pair<string, V>> so callers can iterate without separate keys()/values() snapshots.

pub struct Pair<K, V> {
    pub first: K,
    pub second: V,
}
Field Type Description
first K First element.
second V Second element.

Pair::new

pub fn new(first: K, second: V) -> *Pair<K, V>

Builds a heap-allocated Pair. The caller owns the result and is responsible for freeing it. The two type parameters are independent, so a Pair can hold values of different types (Pair<i32, string>), and may even nest (Pair<string, *Pair<i32, i32>>).

fn divmod(a: i32, b: i32) -> *Pair<i32, i32> {
    let p: *Pair<i32, i32> = malloc(sizeof(Pair<i32, i32>)) as *Pair<i32, i32>;
    p.first = a / b;
    p.second = a % b;
    return p;
}

fn main() -> i32 {
    // Pair::new builds heap-allocated; K and V may differ
    let p: *Pair<i32, string> = Pair::new(42, "answer");
    println!(p.first, p.second);              // 42 answer
    free(p as *void);

    // fields are mutable in place
    let q: *Pair<i32, i32> = divmod(17, 5);
    println!("17 / 5 =", q.first, "rem", q.second);  // 17 / 5 = 3 rem 2
    q.first = q.first + 1;
    println!("bumped:", q.first);             // bumped: 4
    free(q as *void);

    // pairs nest
    let inner: *Pair<i32, i32> = Pair::new(1, 2);
    let outer: *Pair<string, *Pair<i32, i32>> = Pair::new("pt", inner);
    println!(outer.first, outer.second.first, outer.second.second);  // pt 1 2
    free(outer.second as *void);
    free(outer as *void);
    return 0;
}

Internal runtime symbols

The builtins module also exports a set of __glide_* extern fns backing the per-keystroke arena allocator used by the LSP/compiler pipeline:

pub extern fn __glide_palloc(size: i32) -> *void;
pub extern fn __glide_palloc_make() -> *void;
pub extern fn __glide_palloc_free(handle: *void);
pub extern fn __glide_palloc_active() -> i32;
pub extern fn __glide_palloc_get() -> *void;
pub extern fn __glide_palloc_set(a: *void);
pub extern fn __glide_palloc_owns(p: *void) -> i32;
pub extern fn __glide_pfree(p: *void);
pub extern fn __glide_palloc_total_bytes() -> i32;
pub extern fn __glide_palloc_chunks() -> i32;
pub extern fn __glide_proc_rss_mb() -> i32;
pub extern fn __glide_proc_rss_bytes() -> i64;
pub extern fn __glide_panic(msg: string, file: string, line: i32);

__glide_palloc* implement a bump (arena) allocator: every Stmt/Expr/Type/Vector/HashMap allocated while an arena is active comes from chained chunks owned by that arena, and the whole arena is dropped in bulk when the keystroke finishes. Outside the LSP path the active arena is null and __glide_palloc falls back to plain calloc, so build/run/fmt/check keep ordinary heap behaviour.