Reference

This guide is the tour. The canonical contract — what the compiler actually enforces — lives in the spec/ directory at the repository root. When the guide…

This guide is the tour. The canonical contract — what the compiler actually enforces — lives in the spec/ directory at the repository root. When the guide and the spec disagree, the spec wins; when you need the exact rule, an edge case, or a diagnostic’s meaning, go there.

The spec, by topic

Section titled “The spec, by topic”
You want Read
The formal grammar spec/grammar.ebnf
Lexical structure, literals, operators spec/tokens.md
Operator precedence & associativity spec/precedence.md
Operational semantics (lifecycle, bus, recovery, fallible) spec/semantics.md
The type system spec/types.md
Memory: regions, capacity slots, projection classes spec/memory.md
The form library (vec / hashmap / ring_buffer) spec/forms.md
The always-loaded runtime spec/runtime.md
The standard library surface spec/stdlib.md
Idiomatic patterns, the seven shapes, correctness + speed rules spec/styleguide.md
The FFI contract — C (@ffi("c")) and the WASM host interface (@ffi("js") / @export) spec/ffi.md
Dependencies & vendoring spec/packages.md
Project layout & imports spec/projects.md
How tests are written and run spec/testing.md
Why the current surface is shaped this way spec/design-rationale.md
The design-decision log (the F-series: commitments, rejects, sketches) spec/decisions.md
Internal codename legend (milestone / workstream tags) spec/glossary.md

Two more anchors

Section titled “Two more anchors”

Toolchain commands

Section titled “Toolchain commands”
Command Does
hale init [dir] bootstrap a project: hale.toml, a hello-world seed, a first test, .gitignore — non-destructive, fills in only what’s missing
hale run <file/dir> compile + run (fast feedback)
hale build <file/dir> compile to a native binary
hale check parse + typecheck only
hale test run *_test.hl
hale fetch clone & pin git dependencies
hale fmt canonical formatter
hale verify check, but any advisory fails too (the CI gate)
hale bench run *_bench.hl (ns/op, allocs/op)
hale doc API reference from /// comments
hale fleet compose topology artifacts across binaries: check / dump / sign / attest / keygen
hale node <name> express a fleet plan’s instances on one machine, from the record
hale replay <rec> <file> re-run a recording; --diff reports the first divergence and per-category coverage — see Record & replay
hale iris [port] the embedded observer; hale run --observe starts a session beside a run — see Iris
hale dna <verb> attach and operate an organization: init, new, run, task create, status, ledger, body and the rest — see the DNA reference
hale inputs <seed> every file a build of the seed reads, imports followed
hale targets the targets this compiler can name, and which of them it can build
hale lsp the language server, in the compiler binary
hale mcp [--app <endpoint>] the MCP server, in the compiler binary; --app serves a running program’s exposure as tools (the members its description lists for the caller)
hale describe <endpoint | file> a served exposure’s description (members, streams, schemas, the caller and its roles), or from a program hale check --api
hale call <endpoint> <member> [json] call one member of a served exposure naming the digest read, print the response — see Build an API
hale watch <ws://hub> <topic> subscribe to a hub’s stream, one JSON line per frame
hale admin <endpoint> a local page over an exposure’s description
hale api export --surface <S> [--out <dir> | --check <dir>] a surface’s bundle from its rows: description, OpenAPI, JSON Schema, MCP and the digest; --check refuses a stale copy
hale api client --surface <S> --lang hale|ts [--out <file> | --check <file>] a typed client of a surface (the five outcomes, the digest sent, a subscription per stream), generated from its rows

Every one of them answers --help (or -h) as its first argument with its own flags, its input shape and where its output goes. hale build --help is where the built binary’s path is written down: a file target becomes ./app and a directory target becomes myapp/myapp, inside the directory — unless -o <path> (--out) says where. hale build myapp/ -o target/seeds/myapp/myapp writes exactly that path, making its directories, and nothing beside the source; a wasm build takes the same flag for its .wasm and puts the .mjs loader next to it. Build somewhere else, not in the tree, whenever the seed is not yours to litter: it is the one way to keep a checkout free of binaries.

Flags may stand on either side of the target. The first argument that is not a flag is the target, so hale build --dev app.hl and hale build app.hl --dev are the same command, as they already were for check. run follows the same rule up to the target and then stops: everything after it is the program’s own argv, which is why hale run --observe app.hl puts the flag in front.

run takes build’s options as well — --dev, --target-cpu, --link, --csrc — and builds what build would build with them, so hale run --csrc glue.c app.hl runs an @ffi program. They are part of the execution identity a recording carries, and hale replay takes the same set: a run recorded under --dev replays under hale replay --dev, and a default replay refuses it (--env likewise). A binary from hale build stamps the identity hale run would, so its recordings replay too — a directory build’s against the directory’s entry file. The flags that report on a build rather than change it (--locality-report, --target-cache, --strict, --wrap-main, -o) belong to build alone and are refused by name.

check and verify follow every import, so a parse failure anywhere in the import graph — a library you import, or one it imports — fails them both, reported at that file’s own line and column. Neither ever reports success on a tree hale build would refuse.

Every command that follows imports reports it the same way. build, run, test, bench and replay print the same path:line:col: kind: message, with the offending line and a caret under it, positioned in the file that holds the mistake — so the line number you read is the line number you open, whichever command found it.

The path is the file’s canonical one: absolute, symlinks resolved, never a ../ form, however you reached the file or spelled the target. One file has one name, so a script can compare what two commands said about it.

A refusal that comes from the backend rather than the front end reads the same way. Some things hale check accepts cannot be lowered — an array or a tuple as a generic argument, a toolchain piece the program needs and the build cannot find — and each of those is path:line:col: codegen error: message, with the line and the caret, from build, run, test, bench and replay alike. When such a refusal has no particular place in your source to point at, every one of them prints the message on its own, with no position invented for it.

With --json a parse error is reported exactly like a type error: one NDJSON record on stdout carrying the file, line, column and message, with "kind":"parse error". Whatever fails, the machine-readable stream says why — so for a gate reading it, an empty stream means the seed is clean and nothing else does.

That holds for the inputs themselves, not just for what is in them: a target that does not exist, or a file check cannot open — one of the seed’s own, or one reached through an import — is a record with "kind":"io error", the path in "file" and the operating system’s error in "message". It carries "line":0 and "col":0, since a file that never opened has no text to point into. In text mode you read the same sentence you always did, on stderr.

Some findings point into the standard library rather than into your source. When a fn that declares @effects(none: {alloc}) reaches an allocation, the allocation is often several calls deep inside a stdlib locus — and the stdlib is Hale source the compiler reads on its own terms, not a file of your project. Those locations are named, never placed:

main.hl:2:4: type error: effect assertion violated: `ship` must not reach `alloc`, but reaches ship -> std::io::tcp::Stream::recv [an allocation (struct `IoError`)]. …
fn ship(s: std::io::tcp::Stream) {
^^^^
note: the `alloc` effect happens here (in the standard library, io_tcp.hl:98:18)

The finding itself sits on the line you have to change; the note says where in the library the effect actually happens, and --json emits that note as a record of its own, the library location in its message, with an empty file and line and col 0. Your editor gets the finding and nothing else — there is no range in the file you have open for a position in another one.