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

core and the swappable packages

The standard library splits in two:

  • core — the only standard. The prelude surface, uniformly engine-implemented: it registers no host bodies and needs no mount beyond the base one.
  • the swappable packages — in-tree rut/host packages that a module declares in its manifest [deps] (project structure). The community may replace any of them wholesale; nothing in the engine knows their names.

Anything else — gfx, imaging, your own my_map — is an embedder package: a declaration file plus Rust bodies registered the same way (embedding and native modules).

core — the ambient prelude

Every builtin name is in scope in every compilation unit; no use is needed. A use core::{ … }; statement stays legal but is redundant. The one exception is the const NAN: use core::{NAN} is explicit.

Functions

signaturemeaning
assert(cond: bool, msg: str) -> niltrap Assert when false; msg may be omitted at the call site
panic(msg: str) -> nilabort with the message
on_drop<T>(p: ?T, cleanup: fn(?T)) -> nilrun cleanup(p) when p’s cell refcount reaches zero; one callback per pointer
string_join(parts: [str]) -> strjoin in one pass
capture_stacktrace() -> StackTraceopt-in stack snapshot: raw frames only, symbols resolved lazily per access
str.from_code(n: u32) -> strthe 1-codepoint string

Primitives and their members

builtin primitive str {
    fn len(self) -> i32;                     // codepoints
    fn code(self) -> u32;                    // the FIRST codepoint (traps on empty)
    fn code_at(self, i: i32) -> u32;         // traps out of bounds
    fn encode(self) -> bytes;                // the UTF-8 octets
    fn slice(self, from: i32, to: i32) -> str; // O(1) view, codepoint bounds
    fn scan(self, from: i32, set: [u8]) -> i64;  // fused classify; (stop << 8) | class
    fn starts_with(self, from: i32, head: str) -> bool;
}

builtin primitive bytes {
    fn len(self) -> i32;                     // octets
    fn decode(self) -> str;                  // UTF-8, lossy
    fn clone(self) -> bytes;                 // the one copy escape hatch
}
// type-methods: bytes.zeroed(n) -> bytes, bytes.from(a: [u8]) -> bytes

builtin primitive opaque {
    fn downcast<T>(o: Self) -> ?T;           // nil on mismatch; `x is T` probes
}

Construction keeps its builtin forms: opaque(v) seals, Weak.new(v) wraps (the class-method construction), StrBuf(cap) pre-sizes (opaque, weak references).

Builtin classes

classmembers
StackTracelen() -> i32, name(i) -> str, line(i) -> i32, col(i) -> i32, render() -> str
StrBufStrBuf(cap), push(str), push_code(u32) (invalid scalars mint U+FFFD), len() -> i32, finish() -> str — the ONE materialization; the builder keeps its buffer
Weak<T>Weak.new(v) (traps on nil; reference types only), upgrade() -> ?T — nil once the referent died

Engine-woven traits

traitmembernotes
Iterator<E>fn __iterate(self, emit: fn(E) -> bool)for (x of it) desugars to it; emit returning false stops
Future<T>fn yield(cx: RunContext)every async fn’s hidden frame implements it; await consumes it
RunContextcheckpoint() -> u32, next_checkpoint(mut self, v: u32) -> nil, cancelled() -> boolthe async protocol’s cx record (async and await)

Users implement these with ordinary impl blocks; the engine has compiler-backed impls for its own types.

Numeric methods (per integer width i8–u64)

wrapping_add/sub/mul/shl   — modular, no trap
saturating_add/sub/mul     — clamped at the width's ends
checked_add/sub/mul        — the tuple (value, ok); ok is false exactly
                             when the mathematical result escaped the width

Ambient like the primitives themselves: x.wrapping_add(y) needs no use. The trap-on-overflow +/-/*/<< stay the default operators.

Constants

NAN (f64) — core’s one const, behind use core::{NAN}. All other float constants are calc’s.

What core does not have

removedreplacement
Option<T> / Result<T, E>?T with nil as absence; (T, err) tuples (by-reference and nullable)
own / make_ptr?T bindings are the cell reference
charstr of one codepoint; s.code()/str.from_code(n)
free downcast<T>(o)opaque.downcast<T>(o)
Array as a namethe [T] grammar; [v; n] repeat construction
output builtins (print, console)a logger package (ink)

No removed surface keeps compatibility routing: a removed head in an unresolvable position is an ordinary unknown-name error.

The swappable set

packagekindsurface
rthost pkgcreate_logger(name: str) -> opaque, logger_log(log: opaque, level: i32, msg: str)
inkinline rut pkgthe Logger class over rt
pouchinline rut pkgthe growable sequence Vec<T>
nmap_host / nmapsethost pkg + inline rut pkgthe native key table; HashMap/HashSet
jsoninline rut pkg, zero host fnsencodeJson / decodeJson / decodeJsonBytes + traits
strbuildinline rut pkg, zero depsthe StringBuilder class
calchost pkgthe Math namespace
async_engine / async_hosthost pkg + inline rut pkgthe launcher rows; launch_future / sleep
http_host / httphost pkg + rut pkgthe std HTTP lanes
bench-crosshost pkgthe crossing-tax benchmark rows

rt and ink — logging

There is no print, no global output builtin. All logging goes through a used logger; the host owns the sink, and an uninstalled sink is a silent no-op — a script cannot accidentally spam an embedded host’s stdout.

use ink::{ Logger };

pub fn main() {
    let log = Logger.new("app");
    log.info(f"started");
}
started
methodlevel passed to rt
debug(msg)0
info(msg) / log(msg)1
warn(msg)2
error(msg)3

Embedder side: rut_std::logger::install_std_log(&mut hosts, |s| println!("{s}")). Mounting ink pulls rt along ([deps]).

pouch — Vec<T>

The growable sequence, written in rut over the fixed [T] array:

membermeaning
new() / with_capacity(cap) / filled(v, n)construction ([v; n] under the hood)
push(v) / pop() -> Tpop returns the removed element; traps on empty — guard with len() > 0
get/set via v[i], for (x of v)compiler-lowered; element access aliases the stored cell
from([T]) / as_array() -> [T]bridges to the fixed array
freeze() -> bytesthe Vec<u8> → immutable bytes copy (exactly the live length)
slice(from, to) -> ?Vec<T>compiler-lowered O(1) fixed-length window; detaches on grow

nmapset — HashMap/HashSet

Thin wrappers over the native key table (nmap_host rows): the whole state is one opaque handle, every method one host call.

pub class HashMap<K requires i8 | i16 | i32 | i64 | u8 | u16 | u32 | u64 | bool | str | bytes, V> {
    pub fn new() -> Self;
    pub fn with_capacity(n: i32) -> Self;
    pub fn put(mut self, k: K, v: V) -> bool;      // true = newly inserted
    pub fn get(self, k: K) -> ?V;                  // the STORED cell, not a copy
    pub fn has(self, k: K) -> bool;
    pub fn remove(mut self, k: K) -> bool;
    pub fn len(self) -> i32;
}
pub class HashSet<T requires ..same key set..> { new, with_capacity, put, has, remove, len }

Laws: the key set is closed (no floats — no stable equality; encode a custom key canonically to bytes); get answers the stored cell, so two gets of one key name one value until a replace; values release host-side. str keys hash by content; str keys also carry *_range(parent, off, len) members keyed by a byte range of the parent string — a range key is the same key as its content, with no key cell minted on a probe (string views).

calc — the Math namespace

use calc::{ Math };
use ink::{ Logger };

pub fn main() {
    let log = Logger.new("t");
    let x = 3.0f64;
    let y = 4.0f64;
    let d = Math.sqrt(x * x + y * y);
    let a = Math.abs(x);
    let hf = Math.sqrt_f(2.0);            // f32 twin — the width is in the name
    log.info(f"{d} {a} {hf}");
}
5 3 1.4142135

Functions (each with an _f f32 twin): sqrt, floor, ceil, round, trunc, exp, ln, log2, log10, sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, pow, atan2, hypot, copysign, fma, plus the float helpers abs, min, max, signum. Constants: Math.PI, TAU, E, SQRT_2, LN_2, LN_10, LOG2_E, LOG10_E, INFINITY, NEG_INFINITY, EPSILON, MAX, MIN, MIN_POSITIVE. Integer numeric methods are core’s, not calc’s.

json — the serde package

Pure rut, inline = true, zero host fns — a json mount adds no host bindings.

fn encodeJson<T requires JsonSerialize>(v: T) -> (?str, ?EncodeJsonError);
fn decodeJson<T requires JsonDeserialize>(s: str) -> (?T, ?DecodeJsonError);
fn decodeJsonBytes<T requires JsonDeserialize>(b: bytes) -> (?T, ?DecodeJsonError);

trait JsonSerialize   { fn encode(self, mut w: JsonWriter) -> ?EncodeJsonError; }
trait JsonDeserialize { fn decode(mut r: JsonReader) -> (?Self, ?DecodeJsonError); }
  • The pair law: (?T, ?E) with exactly one nil — success = (value, nil), failure = (nil, err). Encode returns ?EncodeJsonError because a cyclic structure is expected, recoverable data, not a trap.
  • Direct decode: no intermediate document; a type’s decode reads its expectations straight off the cursor. A Vec<Row> decode mints exactly the program’s values, once.
  • Numbers: i64 accepts integer lexemes only (overflow/decimal lexeme = WrongType, never a silent wrap); f64 parses IEEE-exact in the common range, ±1 ulp beyond (disclosed); encode renders the shortest round-trip decimal.
  • Depth: capped at 128 both directions, recoverable (DecodeErrorKind::Depth / EncodeErrorKind::Depth).
  • Errors: payloadless kind enums + fixed-field structs — DecodeErrorKind { Unexpected, Truncated, InvalidUtf8, WrongType, Depth, Trailing }, EncodeErrorKind { Depth, NotFinite, KeyUnsupported }; decode details carry at/got/expected, encode details carry at and the lazily built $.rows[3].name path.
  • decodeJsonBytes is strict UTF-8 (InvalidUtf8), never lossy.
  • Ownership: json owns the traits; all container impls live in json, gated by peer groups — Vec rows activate when pouch is anywhere in the consumer’s closure, the map/set rows when nmapset is (dependency kinds). A consumer without the peers mounts json light.
  • The writer accumulates through strbuild’s StringBuilder; the reader rides the core str.scan/starts_with primitives and O(1) string views.

strbuild — the builder

use ink::{ Logger };
use strbuild::{ StringBuilder };

pub fn main() {
    let log = Logger.new("t");
    let k = "name";
    let mut b = StringBuilder.with_cap(1024);   // octet hint
    b.append(f"{k}=");
    b.append_code(0x21);
    let s = b.build();                          // the ONE materialization
    log.info(s);
}
name=!
membermeaning
new()grow from small
with_cap(cap: i32)pre-size to an octet hint (negative traps)
append(mut self, value: str)amortized O(|s|), in place
append_code(mut self, cp: u32)one codepoint; invalid scalars mint U+FFFD
len() -> i32codepoints so far, O(1)
build() -> strfresh immutable str; the builder keeps its buffer

Sharing is the default: appends through an alias (or a mut parameter) land in the caller’s document; copies happen at exactly two engineered points — build’s materialization and growth’s prefix move.

calc’s company: async and http

  • async_engine declares the engine rows (__launch, __abort, __sleep, __sleep_yield); async_host restores the typed surface: launch_future(f: Future<T>) -> LaunchedFutureHandle<T>, LaunchedFutureHandle.abort() -> bool, sleep(ms: u32) -> Future<nil>. Each embedder mounts the pair and installs rut_std::async_host::install_std_async; a session that mounts neither has no launcher (tasks, host futures).

  • http_host declares the transport rows (three async, five sync readbacks); http wraps them in HttpClient / RequestBuilder / Request / Response / ByteStream — async only at the points that really wait:

    use http::{ HttpClient, ClientQueryMethod };
    
    let resp = await HttpClient.new().request()
        .method(ClientQueryMethod.Post)
        .url("https://example.com/api")
        .header("Accept", "application/json")
        .body(payload)
        .build()
        .send(cx);
    

    send resolves at headers; body(cx) drains the wire; byte_stream()/next(cx) walk it in chunks. Status 0 is reserved for transport failure. The reqwest lane is native-only.

Mounting

  • mount_std mounts core + calc; mount_std_async adds the async pair. Swappable packages mount by directory or bundle (module bundles); the rut CLI mounts the tree packages a loose file names by use (the rut CLI).
  • inline = true packages (ink, pouch, nmapset, json, strbuild, async_host) are source-inlined into each consumer — required for class-method and generic surfaces, which cannot cross a module link boundary (the frontend).
  • Generic functions cannot cross a module link boundary; exported linked surfaces carry only monomorphized fns.