Libraries (pond)

The standard library covers the substrate — I/O, time, strings, JSON, HTTP, crypto, the bus. Everything else — web stacks, databases, observability — lives…

The standard library covers the substrate — I/O, time, strings, JSON, HTTP, crypto, the bus. Everything else — web stacks, databases, observability — lives in pond, the contributed library catalog: https://github.com/hale-lang/pond.

Many lotus grow in a pond. Pond is one repository you vendor into your project; each library is a directory of .hl loci in it, and you import only the ones you use.

Using one

Section titled “Using one”

Declare it in hale.toml, fetch it, import it:

[deps]
pond = { git = "https://github.com/hale-lang/pond", rev = "<commit>" }

Pin a commit. Pond’s one published tag, v0.8.0, predates most of the catalog, and pond tracks the compiler closely, so a commit known to build with your hale is the safe pin.

Terminal window
hale fetch

The import line below is an excerpt of a program with pond vendored; on its own it resolves nothing:

import "vendor/pond/sqlite" as sqlite;

Everything the library declares is then reachable as sqlite::Name, and a qualified literal — sqlite::Db { path: "app.db" } — is typechecked against the library’s own declaration: misspell a field and hale check says so, the same as for a type you declared yourself.

So is a qualified type written as an annotation. let c: sqlite::Db = "dev"; is a type error naming sqlite::Db, and so is the same type in a parameter, a return, a struct field, a params field or a capacity slot — the library’s declaration is the type, and c.pth is a misspelled field rather than something the checker shrugs at. Import the same library under two aliases and you still have one type: what a::Row builds fits where b::Row is wanted.

The one thing to know is what you hand it: run hale check on the seed (hale check .) rather than on a single file. One file of a multi-file seed is checked the way hale build builds that file — alone — so an import line that lives in a sibling is not there, and the path is refused as unresolved, by the check and the build alike.

hale fetch clones each dependency into vendor/<name>/ and pins the resolved commit in hale.lock. Hale resolves no transitive dependencies, so every repository your program pulls in is a [deps] entry of your own and a row of your lockfile. Inside pond, when one library uses another (jobs keeps its queue in sqlite), import that one in your program too: pond asks for it, and a library’s hale.toml link settings are picked up only from your program’s own imports.

The alias is not a value

Section titled “The alias is not a value”

The name after as is a namespace, not a binding, so it can be the same as a fn you declare. With a library at ../core that declares greet, this is a file of the program:

import "../core" as core;
fn core(args: String) -> String { return "[" + args + "]"; }
fn go() -> String {
return core::greet("mid") + " " + core("x");
}

core::greet goes through the import; core("x") calls your fn. Paths take the alias, bare names take the value — and it reads the same way when someone else imports your seed.

Types are the exception: Name::Thing is also how you write an enum variant, so if you name an alias after one of your own types the type wins and the alias becomes unreachable in path position. Aliases are lower-case by convention, which keeps them out of the way of type names.

An alias also belongs to the seed that declares it: a library you vendor may call something u while your own program calls a different library u, and each u::f() means the one its own seed imported. It follows that a seed can only use the aliases it declares itself — u::f() written in a seed with no import … as u; is a check error that names the seed which does declare u, rather than quietly borrowing that seed’s import.

One library, however you spell it

Section titled “One library, however you spell it”

A library is a directory, and main.hl is that directory’s entry file rather than a library of its own — so import "../lib/main" and import "../lib" name the same library. Both spellings see every file of the seed, and both resolve to one set of symbols, so a value your app builds as lib::Config is the same type the library’s own signatures mean by it. Spell it whichever way in whichever file; there is one library either way.

Any other single file is its own small library: import "../lib/helper" brings in helper.hl and nothing else.

What you cannot do is have it both ways in one program. If some file of your app imports "../lib" — the whole directory — then "../lib/helper" is not a library of its own any more, it is a file of that one, and the second import is refused where you wrote it:

/tmp/app/other.hl:1:8: type error: `../lib/helper` is already part of the library imported as `a` at /tmp/app/main.hl:1; a single file of a directory-imported library is not a library of its own — reach its declarations as `a::<name>` and drop this import
import "../lib/helper" as h;
^^^^^^^^^^^^^^^

Those two imports really are different libraries — one file against the whole seed — and a file can only be compiled into one of them, so the compiler asks you which. The fix is in the message: drop the file import and spell the names you wanted through the alias the directory already has.

A library’s own main locus

Section titled “A library’s own main locus”

A library directory can be a program of its own: a main.hl that declares a main locus and a fn main. Importing it does not make that locus yours. The main locus that arrives through an import belongs to the library, not to your entry: it does not count against the rule that a program has at most one, its bindings { } stay inert (nothing is bound, and no socket path is touched), and its run() is never started by your program. So an app can import a seed for its functions and types and still declare its own. With a library at ../lib that declares greeting, a program can read:

import "../lib" as lib;
main locus App {
run() { println(lib::greeting()); }
}
fn main() { App { }; }

Two main locus declarations in your own files are still an error: more than one `main` locus declared.

When the path names nothing

Section titled “When the path names nothing”

A path that resolves to none of those places fails the check where you wrote it:

/tmp/app/main.hl:1:8: type error: could not resolve import `../nowhere` (tried /tmp/app/../nowhere.hl, /tmp/app/../nowhere/, and workspace-root/../nowhere/)
import "../nowhere" as nowhere;
^^^^^^^^^^^^

The three paths in the message are the three places the compiler looked, in order, so a typo and a library you have not vendored yet look different at a glance. The caret is under the string itself — the thing to change — and the file it names is the file holding the import, which for a two-hop failure is the library’s file rather than yours.

build, run and test print that same line, and hale check --json carries it as one record with the file, line and column in it, so an editor opens the import rather than the top of the program.

If the path does resolve but there is nothing to compile there — a vendored directory with no .hl files in it, a checkout that did not finish — the report names that directory instead. The import was found; reading what it named is what failed.

When the name after the alias names nothing

Section titled “When the name after the alias names nothing”

An import that resolves does not make every alias::Name you go on to write real. The compiler answers the path itself, where you wrote it:

/tmp/app/main.hl:4:13: type error: `zz::f`: `zz` is not an import or a type of this seed
/tmp/app/main.hl:6:13: type error: `b::Greeting` is not declared by the library imported as `b`; `b` provides: Config, Route, serve

The first is a head that is nothing at all — no import … as zz; in this seed, and no type of your own called zz: a typo, or an import you meant to add. The second is your own alias with a name the library does not declare; when a spelling is close the message says “did you mean”, and when none is it lists what the library provides.

Both hold in every position a path can stand in — a type annotation, a call, a Name { } literal, a const, an enum variant — and a bindings { } entry naming a topic nothing declares has always reported itself the same way. A subscribe or publish of alias::Topic gets the wording a local topic nobody declared gets, with the alias and the missing topic named:

/tmp/app/main.hl:5:19: type error: subscribe references unknown topic `b::Nope` (the library imported as `b` declares no `topic Nope`)

build, run and test print the same finding, so a gate that runs hale check and a build that runs later agree about the program.

A third shape belongs here: a stdlib call with the std:: prefix left off. env::args_count() is not an import of anything, and the message says what it is — “env::args_count is unresolved — did you mean std::env::args_count?” — at the call rather than at the end of a build. (time::sleep and time::monotonic are the exception the compiler still answers unprefixed; they are old spellings, and std::time:: is the one to write.)

The what is the one above again: hale check . and every build path see the whole seed; hale check <one file> sees that file and its own imports, as hale build <one file> does, so a path whose import line is in a sibling file is refused by both.

The catalog

Section titled “The catalog”

Persistence & data

Library Provides
db Driver-agnostic database surface: the DbDriver interface + Args bind-parameter list for parameterized ($1, $2, …) queries. Pick a backend (pq, sqlite) at the DbDriver slot.
pq PostgreSQL driver — PgConn plus PgPool, a fixed-size fd connection pool that itself satisfies db::DbDriver.
sqlite SQLite connection + fallible query surface.
migrations Schema migration runner over db::DbDriver: register the migrations, then Migrator runs up / down / steps / goto.
jobs SQLite-backed job queue (Queue) + a pinned-worker pool (Pool, Worker).

Web

Library Provides
http HTTP client (http/client) with a pool and retries. Superseded by the client in std::http; kept for older pins.
router HTTP router — method + path-param routes, middleware chain. Superseded by std::http::Router; kept for older pins.
sessions Stateless, HMAC-signed cookie sessions (session=<base64(payload)>.<base64(hmac)>).
websocket RFC 6455 WebSocket client and server-side upgrade, with ping/pong liveness deadlines (suggested alias ws).

Realtime

Library Provides
realtime/nats NATS core + JetStream client, and a std::bus adapter that carries a program’s topics over NATS with at-least-once delivery.

Observability & supervision

Library Provides
logfmt Alternative std::log sinks wearing the std::text::Sink shape. Its file and console sinks moved into std::log; OtlpSink is pond-only.
metrics Counter / gauge / histogram primitives + a Prometheus text-format renderer and /metrics endpoint. Superseded by std::metrics; kept for older pins.
tracing Span tree mirroring the locus tower — one Tracer per app; spans nest with locus instantiation.
supervisor Erlang/OTP supervision-tree strategies grafted onto Hale’s on_failure + restart / restart_in_place / bubble.

Primitives & composition

Library Provides
crypto SHA-256/512, HMAC-SHA256/512, hex encode/decode, constant-time compare, CSPRNG.
subprocess Spawn + manage child processes (suggested alias sub) — wraps std::process::run / spawn with pipes and a timeout.

Terminal & UI

Library Provides
term Tier-0 terminal infrastructure — capability/is_tty probes, SGR styling, raw-mode guard, cursor + screen control over std::term.
tui An Elm-shaped TUI runtime: write a locus with model/update/view, the runtime drives the frame loop, input, and rendering.

AI & numeric

Library Provides
agent LLM-agent toolkit — agent/{llm, tools, conversation, embeddings, sandbox}: a client surface, a tool-registry, conversation state, and a sandboxed execution path.
ml Neural-network primitives (ml/neural).
math Numeric helpers — math/{matrix, stats}.

The tree-sitter grammar that drives editor highlighting lives at tree-sitter-hale (it moved out of pond — it’s developer tooling, not a library you import). The _util directory holds small shared helper libs, mostly consumed by other pond libs.

Pond is where the ecosystem grows: if a protocol, parser, or shape is too useful to rewrite per project but doesn’t belong in the language, it lands here.