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— astructis never constructed or referenced.unused-enum— anenumis never used.unused-const— a top-levelconstis never read.unused-variant— anenumvariant 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:
! 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;
}