Chapter 15 — Modules and the Standard Library
As a program grows past one file, you split it into modules. In Ingle, a file is a
module, and you pull one into another with import:
// geom.ig
struct Point { x: int y: int }
fn sum(p: Point) -> int { return p.x + p.y }
// main.ig
import "geom" as geom
fn main() -> int {
let p = geom.Point { x: 3, y: 4 } // construct an imported type, qualified
return geom.sum(p) // call an imported function, qualified
}
=> 7
Two things to notice. First, the import always has an alias (as geom), and you use
everything through it — geom.Point, geom.sum. There’s no implicit flat merging of names,
so you can always see at a glance where something comes from, and two modules can never collide.
Imported types work in every position you’d expect: annotations, construction literals
(including generic ones like box.Box<int> { value: 42 }), fields, and type arguments.
Second, privacy is by leading underscore. A top-level declaration whose name starts with _
is private to its own module; everything else is exported. It’s not a mere convention — it’s
enforced:
// in geom.ig: fn _secret() -> int { return 99 }
import "geom" as geom
fn main() -> int { return geom._secret() }
error: that function is private to its module (leading '_')
So: public is the default, and you opt into privacy with a _. It costs zero keywords and
self-documents at the call site.
The standard library
Some of what you’ve used — println, sqrt, the string methods — is built right into the
language. The rest of the standard library lives in real Ingle source files under the reserved
std/ prefix, and you import it exactly like your own modules. (std/ always resolves to the
toolchain’s library directory, wherever you run the compiler from.) This is the model for how
the library grows: written in Ingle, over a small native base, imported like anything else.
What’s there today:
std/string — the fuller text toolkit beyond the built-in methods: to_upper, to_lower,
trim, contains, index_of, starts_with, ends_with, repeat, substring, replace,
join. (All byte-wise ASCII, like the built-ins.)
import "std/string" as str
fn main() -> int {
let name = str.trim(" ember ")
println("Hello, " + str.to_upper(name) + "!") // Hello, EMBER!
return str.index_of("hello", "ll") // 2
}
Hello, EMBER!
=> 2
std/list — the generic functional toolkit over arrays you met in Chapter 11: map,
filter, reduce, sort, each taking a function value.
std/map — Map<K, V>, a generic hash map keyed by any type that is Hash + Eq.
The built-in scalars and strings all qualify, and a user struct that implements Hash, Eq is a
valid key too (no Copy needed — a struct key is deep-cloned on store, so the map owns its copy);
Map<string, int>, Map<int, bool>, Map<Point, V> all work from the same code. Build one, then
set, get (which returns an Option<V> — of course it does), has, size, and keys (which
hands back a [K] in bucket order):
import "std/map" as mp
fn main() -> int {
var counts = mp.Map<string, int> { buckets: [], count: 0 }
for w in "the cat the dog the bird".split(" ") {
match counts.get(w) {
case Some(n) { counts.set(w, n + 1) }
case None { counts.set(w, 1) }
}
}
match counts.get("the") { case Some(n) { println("the x{n}") } case None {} }
return counts.size() // 4 distinct words
}
the x3
=> 4
You spell out both type arguments at construction — Map<string, int>, Map<int, bool> — just
like any other generic. There’s no Copy requirement on the key, and this is newer than some of
the surrounding prose: a built-in key (a scalar or a string) copies cheaply, and a move-type
struct key is deep-cloned on store, so the map owns its copy and you keep yours — value-semantic
keys with no clone() ceremony. A key type need only be Hash + Eq. The shipped
examples/15_wordcount.ig is a real little tool built on this — tally the words in its command-line
arguments — and examples/06_calculator.ig leans on it too.
Fireside trivia.
std/mapis itself written in Ingle — a generic struct over an array ofOption<MapEntry<K, V>>buckets, boundedMap<K: Hash + Eq, V>, with open-addressing and linear probing, doubling when it gets 70% full. It dispatches each key’s ownhashandeqthrough witnesses it stores per instance — which means the map isn’t just using the language’s generics, arrays, enums, and methods, it’s using the brand-new bounds-on-generic-structs feature to do it. The standard library’s dictionary is written in the language it ships with, and it’s the proof that bounded generic structs work, because it’s made of one. (The underlying string hash is FNV-1a, a 1991 design beloved for being about five lines long and good enough for almost everything.)
std/set — a generic hash set, Set<K: Hash + Eq>. It is std/map’s twin: the same
open-addressing table with linear probing and doubling past a 0.7 load factor, but storing only
keys — no value. Membership, insertion, and iteration are amortised O(1)/O(n), and adding a key
that’s already present is a no-op. Like the map, it’s written in Ingle and is itself a proof that
bounded generic structs work.
std/slotmap — a generic generational-arena SlotMap<V>. The store owns the values; you hold
small copyable Handles instead of pointers, so identity is separated from ownership. Removing a value
bumps its slot’s generation, so a stale handle reads back as None rather than a dangling value — the
recycled-slot footgun made safe by construction. It’s the blessed tool for graph-shaped data
(Chapter 13).
std/sqlite — embedded SQL, backed by the vendored SQLite amalgamation (one public-domain C
file in-tree, so no server and no system package — it keeps the empty-dependency-tree rule). A
connection (Db) and a prepared statement (Stmt) are resource structs
(Chapter 16): each owns its SQLite handle and its drop closes it, so the
compiler guarantees every connection is closed and every statement finalized exactly once, on every
path — the classic leaked-connection bug is impossible, with no ceremony. open and prepare return
a Result (so ? handles failure), and the handle frees itself when its binding leaves scope:
import "std/sqlite" as sql
fn run() -> Result<int, string> {
let db = sql.open("notes.db")? // Db — auto-closes at scope exit (or any `?`)
let _ = sql.exec(db, "CREATE TABLE IF NOT EXISTS note(id INTEGER PRIMARY KEY, body TEXT)")?
let _ = sql.exec(db, "INSERT INTO note(body) VALUES('hello'), ('world')")?
let st = sql.prepare(db, "SELECT id, body FROM note ORDER BY id")? // Stmt — auto-finalizes
loop {
if !sql.step(st)? { break } // false = no more rows
println("{sql.column_int(st, 0)}: {sql.column_text(st, 1)}")
}
return Ok(0)
}
It links only under the database build (make db), the same way the UI modules need the graphics
build and std/http the networking one.
The next several modules are the answer to a fair question — can you build a real, networked, graphical program in this language? — and each is small, pure where it can be, and written in Ingle over a thin native base:
std/json — a real JSON parser and serializer. It dogfoods the language hard: a JSON value is
a recursive enum Json walked with match, and an object keeps its members in insertion order so
serialization round-trips. You build a tree (json.obj, json.arr, json.str, json.num) and
json.stringify it, or json.parse a string back into the enum — and escaping (quotes, newlines,
unicode) is the library’s job, not yours.
std/markdown — parses Markdown into a list of blocks — paragraphs, headings, bullets,
blockquotes, fenced code. The block model is, again, an enum, so a renderer is one exhaustive
match: exactly the language’s own machinery doing exactly what it’s for. Any Ingle GUI can reuse
it — a chat transcript, a docs viewer, a note app.
std/highlight — syntax highlighting as a lexer that emits coloured spans instead of
compiling. A code string becomes a flat list of Spans, each tagged with a Kind the renderer
maps to a colour. It’s a pragmatic C-family tokeniser (identifiers and keywords, numbers, strings,
line and block comments), and it pairs with std/markdown so a fenced code block comes out
highlighted.
std/http — HTTP/HTTPS as a thin layer over the C FFI (libcurl). Two shapes share one binding:
http.post(url, headers, body) makes a blocking request and hands back the whole response body as a
string; or you open a streaming pull — http.open(...) returns a handle, http.next(h) yields
body chunks as the network delivers them ("" once the transfer ends), http.status(h) is the HTTP
code, and http.close(h) frees it. The handle is a linear Ptr (Chapter 16),
so the compiler makes you close it exactly once, on every path. It links only under the networking
build (make net), the same way the UI modules need the graphics build.
std/sse — a Server-Sent Events decoder, the streaming layer that sits on top of std/http.
You feed it the raw response-body chunks http.next hands you; it returns the complete events
framed so far and buffers the trailing partial across feeds. That’s what turns a token-by-token API
stream into a clean sequence of events your loop can act on.
Then the graphics and UI stack, all resting on one immediate-mode idea — the UI is a pure function of state, redrawn fresh every frame, with no retained widget tree to keep in sync — which is precisely why it stays clean under the ownership model of Chapter 12:
std/draw — immediate-mode drawing over the native graphics backend: open a window, then each
frame clear it and issue primitives (rectangles, text, lines). Colours are packed 0xRRGGBB ints;
key codes follow the backend. There is no retained scene, so your app state is just your own Ingle
values.
std/ui — an immediate-mode widget toolkit over std/draw: buttons, labels, text fields,
checkboxes, sliders. A Ui value carries the small slice of cross-frame state immediate mode
needs — the layout cursor, this frame’s input snapshot, and which widget is hot (hovered) or
active (being pressed).
std/layout — a small flexbox solver. You build an ephemeral tree of boxes for one frame,
call solve() with the root rectangle, and read each box’s computed rect. It’s pure — ints,
arrays, and structs, no rendering — so the layout maths is testable on its own, without ever
opening a window.
std/flare — the declarative, component-style layer that ties the three above together:
components as functions, props, local state, declarative composition. It gets its own chapter —
Chapter 25.