Toolchain
Compiler & Projects
The toolchain is two binaries, not one: trident resolves a
project and writes a build plan, and leviathan compiles it.
This page covers the manifest, the dependency model, the compiler's flags,
and the native backends that turn source into an executable.
Two tools: trident & leviathan
Leviathan splits its toolchain into two separate binaries, following the cargo/rustc, MSBuild/csc, SPM/swiftc norm:
trident- the package manager / build driver — the front door
users run. It owns the project manifest (
trident.toml), resolves the dependency graph, and emits a fully-resolved build plan. leviathan- a pure compiler. It compiles the source set a plan names. It does not parse the manifest, resolve dependencies, or know a registry exists.
The boundary between them is a build plan — by default
build/plan.lvplan. trident writes it;
leviathan --plan <file> reads it. The plan is
machine-generated, never hand-authored: it lists every resolved source
(absolute path plus moduleId/origin), the
explicit entry mode, and the module adjacency that drives
phantom-dependency enforcement.
leviathan never opens a trident.toml.
trident commands
trident build [dir] # resolve trident.toml -> write plan -> leviathan --plan --build-native
trident run [dir] # ...compile and execute (tree-walk oracle)
trident check [dir] # ...parse + resolve + type-check, no execution
trident emit-llvm [dir] # ...emit LLVM IR
trident plan [dir] # resolve + write the plan only (no leviathan invocation)
trident --version # trident's version, then leviathan'sCommon flags, shared across the subcommands above:
| Flag | Meaning |
|---|---|
--out <path> | output path for the built artifact |
--target <triple> | target triple for cross-compilation |
--opt-level <0|2> | optimization level (--release is shorthand for -O2) |
--plan <path> | override the build plan location |
--leviathan <path> | override compiler discovery |
trident locates leviathan in order:
--leviathan → $LEVIATHAN → its own sibling
directory → PATH. Dependency-management subcommands —
add, remove, update,
lock, fetch, why — never invoke
the compiler at all.
The manifest: trident.toml
TOML, fixed filename trident.toml (Cargo-style — not
name-parameterized), discovered in the project root. trident
owns this format exclusively; a small hand-rolled reader keeps the
toolchain dependency-free — no external TOML library.
name = "app"
entry = "main" # a function name, or a file ("run.lev")
sources = ["*.lev"] # globs expand alphabetically; explicit lists too
assets = ["views/**", "schema.sql"] # comptime import() targets; optional
version = "0.1.0" # optional
out = "app" # optional
[[dep]] # repeated array-of-tables; omit if no deps
path = "jsonlib"
as = "Json"
version = "1.0.0"
dev = false| Field | Meaning |
|---|---|
name | project name (metadata; default output name) |
entry | a function name (gather everything, call it) or a file name (that file's top-level statements drive the program). trident classifies which and records it in the plan — the compiler never sniffs the extension. |
sources | source paths relative to the manifest, glob-expandable ("*.lev") |
assets | declared build inputs for comptime import(): literals, flat globs, or ** recursive globs (unlike sources' flat globs). Hashed by trident and carried in the plan. Absent means no assets; a glob matching zero files is a warning, a literal naming a missing file is an error. |
[[dep]] | dependency entries; omit for none |
version, out | optional metadata |
A bare file with no manifest is a legitimate project of exactly one
source. leviathan --imports/--graph/
--namespaces/--why synthesize a one-file map
so the same tooling applies uniformly whether or not a
trident.toml exists.
--namespaces and --why buy back the
discoverability the by-name, path-decoupled
namespace model otherwise costs:
because imports are by name rather than by path,
--namespaces is the symbol index (every namespace → its
opening files and members) and --why <name> [in
<file>] answers "where could this name come from, and which
candidate wins here?" For teams who want Go/Java-style folder tidiness
anyway, --lint-namespaces is an opt-in, off-by-default check
that a file opening a namespace sits in a directory whose path is a
case-insensitive suffix of that namespace's ::-segments —
it prints per-file ok/WARN lines for CI, and
never applies to dependencies.
Dependencies
Dependency resolution is entirely trident's job; leviathan only ever sees the resolved sources the plan names.
[[dep]]
path = "jsonlib"
as = "Json"
version = "1.0.0"
dev = falsepath- a local directory containing its own
trident.toml, gathered recursively into the same whole-program compilation unit — a dependency is just more source. Apaththat is not a local directory (e.g.github.com/thaden0/json) is a VCS module, which requires aversion. as- aliases the dependency's exported namespaces into a synthesized
local namespace, so
uses Json;reaches a dep declaredas = "Json"without knowing its internal namespace name. trident materializes the alias as a real source file under the build dir — the plan carries only on-disk paths. dev- a development-only dependency, excluded from the shippable-artifact
modes (
build,run,emit-llvm) but visible tocheck.
A file may only uses a namespace belonging to the
project itself or to a direct [[dep]] —
reaching a transitive dependency's namespace without declaring it
directly is a compile error (pnpm/Cargo-style strictness). trident
decides the adjacency (the plan's edge rows);
leviathan enforces it.
The leviathan compiler
Run directly, leviathan takes a file (or, via
--plan, a whole resolved project) and a flag choosing what
to do with it — parse only, execute, dump an intermediate form, or
compile to a native artifact.
leviathan --build prog file.ext # source -> native executable, one step
leviathan file.ext # parse + resolve + type-check, dump class shapes
leviathan --run file.ext # ...then execute (tree-walk oracle)Run & inspect
| Flag | Effect |
|---|---|
--run file.ext | execute on the tree-walk oracle |
--ir file.ext | lower to bytecode IR and execute that |
--ir-verify file.ext | IR execution plus ownership soundness verification |
--mem-verify file.ext | reachability oracle: cross-checks live/dead heap objects |
--ownership file.ext | dump the ownership/escape analysis report |
--resolve file.ext | stop after resolution |
--tokens file.ext | dump the token stream |
AST & rule stage
| Flag | Effect |
|---|---|
--ast file.ext | dump the AST (pre-rule-stage) |
--expand file.ext | re-emit the post-rule-stage tree as compilable source (round-trips) |
--ast-after-rules file.ext | structural AST dump after the rule stage (comptime folded) |
--no-rules file.ext | compile with the rule stage disabled |
--comptime-budget N | override the comptime step budget |
--reentrant-budget N | override the reentrant-rule round cap (default 8) |
Native output
| Flag | Effect |
|---|---|
--emit-cpp file.ext | native backend: emit a C++ translation unit (compile with g++) |
--emit-llvm file.ext | emit LLVM IR |
--native-obj out.o | direct object emission via LLVM's TargetMachine, no external llc |
--build-native out | source → linked executable in one step (LLVM path) |
--emit-elf out file.ext | frozen pure x86-64/ELF backend, zero deps — see Backends |
--build out file.ext | source → native executable in one step (emit-C++ + g++) |
Project-aware (also synthesized for a bare file — see project of one)
| Flag | Effect |
|---|---|
--plan build/plan.lvplan | compile a whole project from trident's resolved build plan |
--imports file.ext | dump the file → imports provenance map |
--graph file.ext | dump the uses include graph and build order |
--namespaces file.ext | list every namespace, the files that open it, and its members |
--why <name> [in <file>] file.ext | where a bare name resolves from; which candidate wins for a file |
--lint-namespaces file.ext | opt-in folder≈namespace convention check (off by default) |
--assets file.ext | list every comptime import()-consumed asset (path, bytes, hash, module) |
Compilation is whole-program: all source gathers into one compilation unit, though namespace boundaries persist. Diagnostics carry source spans and do not stop at the first error — a build reports everything wrong in one pass rather than dribbling errors out over repeated runs.
Backends
Four engines execute Leviathan programs today: oracle IR emit-C++ LLVM — plus a frozen fifth kept only for differential testing. Three native backends consume the same IR; only entry-reachable functions are emitted, and out-of-coverage constructs fail with a diagnostic rather than producing a silently wrong answer.
- LLVM
- the primary, portable AOT backend (built against
LLVM ≥ 18). Emits LLVM IR (
--emit-llvm) or a direct object file (--native-obj), linking a portable C runtime plus a platform floor. Covers the full implemented language: objects, collections, closures, exceptions, the event loop, natives, async/await, sockets, HTTP.--build-native outgoes straight from source to a linked executable. - emit-C++
- always available (
--emit-cpp); prints a freestanding C++ translation unit with an embedded runtime, compiled withg++ -O2. Covers the whole language except the event-loop/system layer (sockets, timers, async, file/stdin I/O) — those stay interpreter-bound for now. - Pure x86-64 / ELF
- our own machine-code emitter and ELF writer — no g++, no
assembler, no linker, no libc — talking to the kernel through
syscallonly. It once compiled the whole language, but is now frozen: kept as the differential-testing anchor and a possible zero-dependency bootstrap seed, and deliberately never extended further (e.g. it lacks the transcendentalmathmethods the other four engines all have).
The pure ELF backend is reference-only. It is not something a
trident.toml-driven project builds against — LLVM is
the AOT backend for real artifacts.
Whole-program & ownership
One semantic IR underlies both interpreted execution paths. The tree-walking evaluator is the semantics oracle; the bytecode IR (a register machine) is the lowering target the native backends share. Both run against one runtime value model, and a shared corpus of real programs with expected outputs must produce identical output on every engine — differential testing across all five holds the implementation honest as it grows.
--ownership runs an interprocedural escape analysis over
the IR, classifying every allocation site into one of three tiers:
| Class | Meaning |
|---|---|
| scope-owned | provably dies with its frame — freed at scope exit with zero runtime bookkeeping |
| ownership-transferred | transferred by return; the caller's scope inherits the free obligation |
| refcount tier | stored in objects/collections, captured, thrown, or passed to a retaining/unknown callee |
--ir-verify executes the program while checking the
analysis empirically: every scope-owned allocation is verified dead at
frame exit (weak-reference liveness), and any survivor is a soundness
violation that fails the run. Over the corpus, 85–90% of allocations
are deterministic — scope-owned or transferred — with zero violations.
This same per-frame arena tier is what the pure ELF backend and, as of
parity work on LLVM, the LLVM backend both use for scope-owned
value-struct allocations; escaping allocations fall back to
retain/release over a real free-list allocator.