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/macrostubs 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. - Macros —
println!,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.