Chapter 30 20 min read

System & tooling

Glide's standard library exposes a handful of modules that sit close to the runtime and the compiler itself: typed OS signal handling that plugs into select!, runtime backtrace capture, the meta AST surface for macro-authoring libraries, and the lint codes the compiler emits at build time. Each is small and focused; this page documents every public item.

Import

import stdlib::signal::*;      // Signal enum + signal_chan / raise / default / ignore
import stdlib::backtrace::*;   // stack_trace / stack_trace_skip
import stdlib::meta::*;        // Expr/Stmt/Type/... AST nodes + constructors (for @proc_macro)
import stdlib::lint::*;        // LINT_* codes + lint_code()

Each module is independent — import only the ones you use.

Module Public surface One-liner
stdlib::signal 1 enum (3 methods) + 4 free fns + 1 internal helper OS signals delivered as chan<Signal> values.
stdlib::backtrace 2 fns Walk the live call stack into a *Vector<string>.
stdlib::meta re-export of bootstrap::ast (10+ structs, 100+ consts, 50+ constructors) The compile-time AST surface for @proc_macro authors.
stdlib::lint 8 consts + 1 fn Names for the compiler's built-in warning codes + a custom-lint helper.

Signals

stdlib::signal delivers OS signals as values on a chan<Signal>, so signal handling composes with the rest of your concurrency via select!/recv() instead of running inside an async-signal-unsafe handler. On POSIX the implementation is the classic self-pipe trick: a sigaction handler writes the signum to a pipe, a reader thread reads it and pushes a Signal onto the chan. On Windows it hooks SetConsoleCtrlHandler (Ctrl+C → SIGINT, Ctrl+Break → SIGTERM, close/logoff/shutdown → SIGHUP).

Public surface

Item Signature Description
enum Signal 8 named variants + Other(i32) The signal value carried on the chan.
Signal::to_int fn to_int(self: Signal) -> i32 POSIX signum.
Signal::from_int fn from_int(n: i32) -> Signal Build from a raw signum (total).
Signal::name fn name(self: Signal) -> string POSIX-style name.
signal_chan fn signal_chan(s: Signal) -> chan<Signal> Subscribe; returns the delivery chan.
signal_default fn signal_default(s: Signal) -> ! Restore the OS default handler.
signal_ignore fn signal_ignore(s: Signal) -> ! Discard the signal entirely.
signal_raise fn signal_raise(s: Signal) -> ! Raise the signal against this process.
_signal_push fn _signal_push(c: chan<Signal>, n: i32) Internal — push from_int(n) onto a chan.

enum Signal

The eight commonly-named POSIX signals plus an Other(i32) escape hatch for any signum outside the set.

pub enum Signal {
    Hup,        // SIGHUP  = 1
    Int,        // SIGINT  = 2
    Quit,       // SIGQUIT = 3
    Kill,       // SIGKILL = 9
    Usr1,       // SIGUSR1 = 10
    Usr2,       // SIGUSR2 = 12
    Pipe,       // SIGPIPE = 13
    Term,       // SIGTERM = 15
    Other(i32), // any signum not listed above
}
Method Signature Description
to_int fn to_int(self: Signal) -> i32 POSIX signum; Other(n) returns n literally.
from_int fn from_int(n: i32) -> Signal Build from a raw signum; unknown values become Other(n). Always succeeds (total).
name fn name(self: Signal) -> string POSIX-style name ("SIGINT"); Other(n) returns "SIG#<n>".
import stdlib::signal::*;

fn main() -> i32 {
    let s: Signal = Signal::Int;
    println!(s.name(), s.to_int());        // SIGINT 2

    let t: Signal = Signal::from_int(15);
    println!(t.name());                    // SIGTERM

    let o: Signal = Signal::Other(42);
    println!(o.name(), o.to_int());        // SIG#42 42

    match s {
        Int  => { println!("interrupt"); }
        Term => { println!("terminate"); }
        _    => { println!("other"); }
    }
    return 0;
}
import stdlib::signal::*;

// match can't compare integers — convert to signum and branch with if/else.
fn classify(s: Signal) -> string {
    let n: i32 = s.to_int();
    if n == 2  { return "interrupt"; }
    if n == 15 { return "terminate"; }
    if n == 1  { return "hangup"; }
    return "other";
}

fn main() -> i32 {
    println!(classify(Signal::Int));            // interrupt
    println!(classify(Signal::Term));           // terminate
    println!(classify(Signal::from_int(99)));   // other
    return 0;
}

Subscribing — signal_chan

pub fn signal_chan(s: Signal) -> chan<Signal>

Subscribe to s. Each occurrence pushes the same Signal onto the returned channel. Idempotent — repeated calls for the same signal return the same chan (the slot is keyed by signum, clamped to 0..63). The buffer holds 64; signals delivered faster than they're consumed beyond that are dropped (fine for shutdown signals, which deliver once).

import stdlib::signal::*;

fn main() -> !i32 {
    let usr1: chan<Signal> = signal_chan(Signal::Usr1);
    signal_raise(Signal::Usr1)?;
    let s: Signal = usr1.recv();
    println!("got", s.name());     // got SIGUSR1
    return ok(0);
}

A real server drives the chan from a loop, typically alongside other arms in a select!. recv() returns Signal (not ?Signal), so a bare recv() blocks until a signal arrives:

import stdlib::signal::*;

fn run() -> !i32 {
    let intr: chan<Signal> = signal_chan(Signal::Int);
    let term: chan<Signal> = signal_chan(Signal::Term);
    signal_raise(Signal::Term)?;
    while true {
        let sig: Signal = term.recv();
        match sig {
            Int  => { println!("shutting down (int)"); break; }
            Term => { println!("shutting down (term)"); break; }
            _    => { }
        }
    }
    intr.close();        // silence chan-leak; lets receivers stop
    term.close();
    return ok(0);
}

fn main() -> !i32 {
    return run();
}

Changing disposition — signal_default, signal_ignore

pub fn signal_default(s: Signal) -> !
pub fn signal_ignore(s: Signal) -> !
Function Effect
signal_default(s) Restore the default OS handler (SIG_DFL). A chan from a prior signal_chan(s) stops receiving.
signal_ignore(s) Discard s entirely (SIG_IGN) — no chan, no default action. Common for SIGPIPE on servers that detect disconnects at the read/write level.

Both return ! (a Result carrying no value); propagate with ? or default with ??. They surface err("signal_default failed") / err("signal_ignore failed") if the underlying sigaction (POSIX) call fails — in practice only for an out-of-range signum (< 0 or >= 64).

import stdlib::signal::*;

fn main() -> !i32 {
    signal_ignore(Signal::Pipe)?;   // writes to closed sockets won't kill us
    signal_default(Signal::Int)?;   // Ctrl+C terminates again
    return ok(0);
}

Raising — signal_raise

pub fn signal_raise(s: Signal) -> !

Raise s against the current process. If a subscription is active the signal is pushed directly onto the chan (cross-platform reliable, no OS round-trip); otherwise it falls back to raise(sig) on POSIX or GenerateConsoleCtrlEvent on Windows. Because of the direct path, an in-process raise works for any signum once subscribed — even SIGUSR1/SIGUSR2 on Windows, which has no OS-level analog. This makes it the idiomatic way to drive a select! arm in tests. Returns err("signal_raise failed") on failure.

_signal_push (internal helper)

pub fn _signal_push(c: chan<Signal>, n: i32)

Pushes Signal::from_int(n) onto c. It is pub only because the C reader thread calls it Glide-side (so the runtime never needs Signal's private layout). You normally have no reason to call it; prefer signal_raise to inject a signal in tests.

import stdlib::signal::*;

fn main() -> !i32 {
    let intr: chan<Signal> = signal_chan(Signal::Int);
    _signal_push(intr, 2);
    let s: Signal = intr.recv();
    println!(s.name());            // SIGINT
    return ok(0);
}

Backtraces

stdlib::backtrace walks the live call stack via the platform unwinder (backtrace()/backtrace_symbols() on glibc/macOS/FreeBSD, CaptureStackBackTrace + DbgHelp's SymFromAddr/SymGetLineFromAddr64 on Windows). Lines are best-effort: with -O2 and frame-pointer omission you may get fewer frames or less detail, and on libcs without execinfo.h (e.g. musl) it degrades to an empty result.

Function Signature Description
stack_trace fn stack_trace() -> *Vector<string> Current call stack, one frame per element. The first entry is the caller of stack_trace; the unwinder itself is filtered out.
stack_trace_skip fn stack_trace_skip(n: i32) -> *Vector<string> Same, but skips the n frames closest to the caller — useful to hide a thin shim (e.g. a log_error! macro expansion). On Windows n is clamped to 0..32.

Both return an empty vector when the platform can't walk the stack — never an error or ?T, so always guard on .len() == 0 rather than assuming frames exist. Frame strings are whatever the unwinder produces — typically binary(function+0xoffset) [0xaddr] on POSIX and function (file:line) (when line info is present) or function+0xoffset on Windows.

import stdlib::backtrace::*;

fn buggy() {
    let frames: *Vector<string> = stack_trace();
    for f in frames {
        println!(f);
    }
    let skipped: *Vector<string> = stack_trace_skip(1);
    println!("frames", skipped.len());
}

fn main() -> i32 {
    buggy();
    return 0;
}

Defensive use — handle the degraded (empty) case explicitly and index the top frame with .get(0):

import stdlib::backtrace::*;

fn here() {
    let frames: *Vector<string> = stack_trace();
    if frames.len() == 0 {
        println!("(no frames available)");   // e.g. musl / FPO build
        return;
    }
    println!("top frame:", frames.get(0));
    println!("frame count:", frames.len());
}

fn caller() { here(); }

fn main() -> i32 {
    caller();
    return 0;
}

Declarative macros (macro name!)

A declarative macro is a macro_rules!-style template that expands at compile time, between parsing and type-checking. Matchers bind call arguments ($x:expr), and the variadic form $($x:expr),* repeats a body fragment with $( … )*:

macro bail!($cond:expr, $msg:expr) {
    if $cond { return err($msg); }
}

macro list_each!($($v:expr),*) {        // variadic
    $( println!($v); )*
}

// Type-attached: instance form (uses `self`) and the `Type::name!` form.
impl<T> Vector<T> {
    macro push_all!($($x:expr),*) { $( self.push($x); )* }
}

Macros that return a value

When a macro body ends with return <expr>;, a call in expression position expands to a block-expression that yields that value — the same rule a literal { …; return v } block follows. The very same macro splices statements at statement position and produces a value when used as one:

macro doubled!($x:expr) {
    let d: i32 = $x * 2;
    return d;
}

macro ints!($($x:expr),*) {
    let v: *Vector<i32> = Vector::new();
    $( v.push($x); )*
    return v;
}

let a = doubled!(21);                 // 42
let b = doubled!(5) + doubled!(10);   // 30 — inside a larger expression
let v = ints!(1, 2, 3);               // a real *Vector<i32>

This works for every call shape — bare name!, receiver recv.name!, and Type::name! — and in any expression slot: a let initializer, a function argument, an operand, or a return.

Hygiene

Locals a macro body introduces are renamed per expansion, so a macro can name a temporary whatever it likes without clashing with a caller variable of the same name passed in as an argument:

macro inc!($x:expr) {
    let tmp: i32 = $x + 1;
    return tmp;
}

let tmp: i32 = 5;
let y = inc!(tmp);   // y == 6 — inc's `tmp` does not capture the caller's `tmp`

A macro nested inside another macro's arguments expands correctly, and a macro defined in terms of itself stops at a recursion limit with a diagnostic instead of hanging the compiler.


Meta (AST surface for macros)

stdlib::meta is the curated, supported entry point for libraries that author @proc_macro / @proc_attr / @proc_derive macros. The module body is a single re-export:

pub import bootstrap::ast::*;

It deliberately mirrors the compiler's internal bootstrap::ast names but shields macro libraries from internal refactors — importing stdlib::meta reads as a normal dependency rather than poking at the compiler's guts. A proc macro runs in the compiler's embedded interpreter at compile time (before codegen) and is stripped from the consumer's binary, so it adds no runtime weight.

A function-style macro receives the call's *Vector<Expr> (the arguments); an attribute/derive macro receives the annotated *Stmt. Either way the output is the *Vector<Stmt> spliced into the consumer.

Node structs

The AST node types your macro builds and inspects. All fields are pub; you read e.g. expr.kind, expr.int_val, expr.line, stmt.kind.

Struct Role
Expr Expression node. Key fields: kind: i32 (an EX_*), line/column, int_val, str_val, bool_val, op_code, lhs/rhs/operand: *Expr, args: *Vector<Expr>, field: string, cast_to: *Type.
Stmt Statement node. kind: i32 (an ST_*) plus per-kind payload fields.
Type Type node. kind: i32 (a TY_*).
Param A fn parameter (name + *Type).
Field A struct field (name + *Type + is_pub).
EnumVariant One enum variant.
MatchArm One match arm.
SelectArm One select! arm.
Attr An attribute (@name(args)).
MacroParam A matcher parameter in a macro definition.
StructLitField One field of a struct literal.

Kind constants (discriminants)

Each node's kind field holds one of these i32 discriminants. The catalog is large; the table lists the prefixes and a representative slice. The full set lives in bootstrap::ast and is re-exported verbatim.

Prefix Count Examples
TY_* (type kinds) 13 TY_NAMED=0, TY_POINTER=1, TY_BORROW=2, TY_SLICE=4, TY_GENERIC=5, TY_FNPTR=6, TY_RESULT=7 (!T), TY_OPTION=9 (?T), TY_TUPLE=12
EX_* (expr kinds) 27 EX_INT=0, EX_FLOAT=1, EX_STRING=2, EX_BOOL=3, EX_IDENT=5, EX_BINARY=6, EX_UNARY=7, EX_CALL=8, EX_MEMBER=10, EX_MACRO=14, EX_IF=23, EX_MATCH=25
ST_* (stmt kinds) 27 ST_LET=0, ST_RETURN=1, ST_EXPR=2, ST_IF=3, ST_WHILE=4, ST_FN=6, ST_STRUCT=9, ST_FOR=12, ST_ENUM=15, ST_MATCH=16, ST_TRAIT=24, ST_SELECT=26
UN_* (unary ops) 7 UN_NEG=1, UN_NOT=2, UN_DEREF=3, UN_ADDR=4, UN_ADDR_MUT=5, UN_BIT_NOT=6, UN_TRY=7
OP_* (binary/assign ops) 24 OP_ADD=1, OP_SUB=2, OP_MUL=3, OP_EQ=6, OP_NE=7, OP_LT=8, OP_AND=12, OP_OR=13, OP_ASSIGN=14, OP_COALESCE=24 (??)

Constructors

Build replacement AST. There are 50+; grouped here, with verbatim signatures for the most-used.

Group Functions
Types (ty_*) ty_named, ty_pointer, ty_generic, ty_fnptr, ty_result, ty_option, ty_optres, ty_assoc, ty_tuple
Expressions (expr_*) expr_int, expr_float, expr_string, expr_bool, expr_ident, expr_char, expr_null, expr_binary, expr_unary, expr_assign, expr_call, expr_member, expr_index, expr_cast, expr_path, expr_macro, expr_macro_var, expr_if, expr_match, expr_block, expr_tuple, expr_struct_lit, expr_fnexpr, expr_postinc, expr_postdec
Statements (stmt_*) stmt_let, stmt_return, stmt_expr, stmt_fn, stmt_struct, stmt_impl, stmt_impl_trait, stmt_if, stmt_while, stmt_for, stmt_break, stmt_continue, stmt_const, stmt_import, stmt_enum, stmt_spawn, stmt_match, stmt_craw, stmt_asm
Helpers make_param, make_field, strip_ptr, ast_fill_defaults, pass_diag
pub fn expr_int(n: i64, line: i32, col: i32) -> *Expr            // integer-literal node
pub fn expr_string(s: string, line: i32, col: i32) -> *Expr      // string literal
pub fn expr_ident(name: string, line: i32, col: i32) -> *Expr    // bare identifier
pub fn expr_binary(op: i32, lhs: *Expr, rhs: *Expr) -> *Expr     // op is an OP_* code
pub fn expr_call(callee: *Expr, args: *Vector<Expr>) -> *Expr    // f(args)
pub fn stmt_let(name: string, ty: *Type, value: *Expr,
                is_mut: bool, line: i32, col: i32) -> *Stmt      // let name: ty = value;
pub fn stmt_expr(e: *Expr) -> *Stmt                              // wrap an Expr as a Stmt
pub fn ty_named(name: string) -> *Type                          // a plain type name
pub fn pass_diag(line: i32, col: i32, severity: i32,
                 code: string, msg: string)                     // emit a diagnostic

The minimal proc macro: answer!() expands to the literal 42 at every call site.

import stdlib::meta::*;

@proc_macro(answer)
fn impl_answer(args: *Vector<Expr>) -> *Vector<Stmt> {
    let out: *Vector<Stmt> = Vector::new();
    out.push(*stmt_expr(expr_int(42, 0, 0)));
    return out;
}

fn main() -> i32 {
    return 0;
}

Building a compound statement — let x: i32 = 1 + 2; — chains ty_named, expr_int, expr_binary (with the OP_ADD op code) and stmt_let:

import stdlib::meta::*;

@proc_macro(one_plus_two)
fn impl_one_plus_two(args: *Vector<Expr>) -> *Vector<Stmt> {
    let out: *Vector<Stmt> = Vector::new();
    let sum: *Expr = expr_binary(OP_ADD, expr_int(1, 0, 0), expr_int(2, 0, 0));
    let s: *Stmt = stmt_let("x", ty_named("i32"), sum, false, 0, 0);
    out.push(*s);
    return out;
}

fn main() -> i32 {
    return 0;
}

Macros can also inspect their inputs by reading the kind field against the EX_* discriminants:

import stdlib::meta::*;

// Emit a string naming the kind of the first argument expression.
@proc_macro(describe_first)
fn impl_describe_first(args: *Vector<Expr>) -> *Vector<Stmt> {
    let out: *Vector<Stmt> = Vector::new();
    let label: string = "unknown";
    if args.len() > 0 {
        let first: Expr = args.get(0);
        if first.kind == EX_INT    { label = "int"; }
        if first.kind == EX_STRING { label = "string"; }
        if first.kind == EX_IDENT  { label = "ident"; }
    }
    out.push(*stmt_expr(expr_string(label, 0, 0)));
    return out;
}

fn main() -> i32 {
    return 0;
}

pass_diag — custom compile-time diagnostics

pub fn pass_diag(line: i32, col: i32, severity: i32, code: string, msg: string)

Emit a diagnostic from a macro (or compiler pass). severity 1 = error (fails the build), 2 = warning. code is a short kebab-case id the user can silence with @allow("<code>"). Pass the .line / .column of the AST node you're flagging so the error points at the right source.

import stdlib::meta::*;

// Warn (severity 2) if the macro is called with no arguments.
@proc_macro(no_empty)
fn impl_no_empty(args: *Vector<Expr>) -> *Vector<Stmt> {
    let out: *Vector<Stmt> = Vector::new();
    if args.len() == 0 {
        pass_diag(0, 0, 2, "no-empty", "no_empty! called with no arguments");
    }
    out.push(*stmt_expr(expr_int(0, 0, 0)));
    return out;
}

fn main() -> i32 {
    return 0;
}

Lint

stdlib::lint documents the compiler's built-in warning codes and offers one runtime helper. There are no lint runners here — linting happens inside the compiler; this module is the stable surface for the code names plus a helper for building custom-lint codes. (To emit a custom diagnostic programmatically from a macro/pass, use pass_diag in stdlib::meta, above.)

Built-in warning codes

Every code below can be silenced with @allow("<code>") on the surrounding item (the fn, an inner let, a block, an if/while, or the offending stmt). Multiple codes: @allow("unfreed-alloc", "deprecated-fn"). Suppression follows the AST and does not propagate upward — a narrower @allow doesn't silence warnings emitted earlier in the same outer scope.

Constant Value Fires when
LINT_DEPRECATED_FN "deprecated-fn" Call to a fn marked @deprecated.
LINT_UNSTABLE_FN "unstable-fn" Call to a fn marked @unstable.
LINT_UNFREED_ALLOC "unfreed-alloc" malloc(...) / __glide_palloc_make() without a matching free.
LINT_ARENA_NOT_FREED "arena-not-freed" Arena::new(...) without .free() / defer.
LINT_UNUSED_VAR "unused-var" A local let is never read.
LINT_UNUSED_PARAM "unused-param" A fn parameter is never read.
LINT_UNUSED_FN "unused-fn" A fn is never called or referenced.
LINT_UNNECESSARY_MUT "unnecessary-mut" A let mut that's never reassigned.

Additional built-in codes the compiler emits but which have no named constant here: addr-of-temporary (returning &Foo { … } / dangling refs), missing-return (a non--> void fn path falls off the end), large-return (a struct >64 bytes returned by value), chan-leak (a chan left open at a return), and lint:<category> (any fn marked @lint(...)).

A second family of dead-code codes is also emitted without a named constant. Each is silenced by @allow("unused"), and each skips pub, generic, and impl'd declarations (anything that might be reached from outside the file or through a trait):

  • unused-struct — a struct is never constructed or referenced.
  • unused-enum — an enum is never used.
  • unused-const — a top-level const is never read.
  • unused-variant — an enum variant is never constructed or matched.
  • redundant-import — the same module is imported twice in one file.
import stdlib::lint::*;

fn main() -> i32 {
    println!(LINT_DEPRECATED_FN);          // deprecated-fn
    println!(LINT_UNSTABLE_FN);            // unstable-fn
    println!(LINT_UNFREED_ALLOC);          // unfreed-alloc
    println!(LINT_ARENA_NOT_FREED);        // arena-not-freed
    println!(LINT_UNUSED_VAR);             // unused-var
    println!(LINT_UNUSED_PARAM);           // unused-param
    println!(LINT_UNUSED_FN);              // unused-fn
    println!(LINT_UNNECESSARY_MUT);        // unnecessary-mut
    return 0;
}

Custom lints — lint_code

pub fn lint_code(category: string) -> string

Builds the canonical lint:<category> code from a category name (literally "lint:".concat(category)). Useful when an editor or build tool filters/formats custom-lint diagnostics programmatically.

import stdlib::lint::*;

fn main() -> i32 {
    let code: string = lint_code("blocking_io");
    println!(code);                        // lint:blocking_io
    return 0;
}

Attaching and suppressing lints

A fn annotated @lint("blocking_io", "reason") surfaces at every call site:

output
! lint:blocking_io
  function `read_sync` is flagged by `lint:blocking_io`:
    blocks the coro scheduler — use spawn_thread

Suppress it at a call site with @allow("lint:blocking_io"). Both @lint and the named @deprecated forms attach to the fn definition; the @allow goes on the call (or the enclosing item):

import stdlib::lint::*;

@deprecated("use parse_v2 instead")
fn parse_v1(s: string) -> i32 { return 0; }

@lint("blocking_io", "blocks the coro scheduler — use spawn_thread")
fn read_sync(path: string) -> i32 { return 0; }

fn main() -> i32 {
    @allow("deprecated-fn")
    let a: i32 = parse_v1("x");

    @allow("lint:blocking_io")
    let b: i32 = read_sync("/etc/hosts");

    println!(a + b);
    return 0;
}