-
-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
2026-09 note: the tree-walking interpreter (
interpreter.rs) and the live HTTP server (serve.rs) described in older versions of this page were removed entirely in a compiler-hardening pass — neither file exists in this tree anymore, and there is no interpreted fallback for anythingcodegen.rsdoesn't yet reach. This page now describes the compiled-only pipeline as it actually is; see Honest Scope & Roadmap for what that removal traded away and what's landed since.
This page describes the real compiler pipeline as implemented in
crates/compiler/src/, not an idealized diagram — every stage below is a
named file you can open, and crates/compiler/src/INDEX.md
is the maintained structural index into the largest of them (line numbers
for every major function) — though check it against the real file if the
two ever drift; this wiki page and that index are both prose, not the
compiler itself.
The parser (crates/compiler/src/parser.rs) is hand-written recursive descent
with strictly one token of lookahead and no backtracking, anywhere — the
operational definition of LL(1). Binary-operator precedence is handled
by a precedence-climbing chain (parse_logic_or → parse_logic_and → parse_equality → parse_comparison → parse_additive → parse_multiplicative → parse_unary), not left-recursive grammar rules — that's what keeps the
expression grammar LL(1)-parseable without a transformation step.
LL(1) parseability is a decidable, checkable property of the grammar itself — not a claim about code style. It's the property that makes grammar-constrained decoding possible: a sampler can walk the same grammar alongside the model's token stream and mask out every token that would violate it, guaranteeing the sampler, not the model's judgment, keeps generation syntactically valid. See LLM Integration for how this becomes a shipped GBNF artifact.
Nirdosha's grammar claims are verified by external tools, not just asserted by re-reading the hand-written parser:
-
grammar_check/feeds the grammar tolalrpop, an independent LALR(1) generator. This cross-check found something real: because the language has no statement separator (no semicolons, no significant newlines), the grammar as an abstract CFG is ambiguous wherever a token could either extend an expression or start a new statement. The parser resolves every such case with one deterministic rule — always extend the current expression over ending the statement (shift over reduce, no exception) — soreturn xfollowed on the next line by-yparses asreturn (x - y), not as two statements. The hand-written parser is unambiguous; the bare CFG is not, without this rule stated explicitly — a distinction only visible by running a second tool against it. -
grammar_export/hand-translates the grammar to GBNF (crates/compiler/nirdosha.gbnf), validated two ways: byllama-cpp-gbnf(the real llama.cpp parser, which caught a real bug during writing), and by running a corpus — every shipped.nirexample plus rejection cases — through the GBNF and confirming it accepts/rejects exactly what the real compiler does.
Top-level: item ::= fn_decl | struct_decl | enum_decl | screen_decl | dashboard_decl. The full EBNF lives in
GRAMMAR.md.
Notable choices: no statement separators (see above); keyword-heavy over
symbol-heavy syntax (aids LLM reliability); construction is an ordinary
call (a struct's name is its own positional constructor via Expr::Call,
not a separate literal form); contextual keywords (field, action,
tile, chart, role, claim) match only in their leading slot, staying
ordinary identifiers everywhere else.
Every subcommand but emit-ast (main.rs::typecheck_and_own/
typecheck_and_own_optional_main_with_ui_components) runs the same
shared pipeline before doing anything backend-specific — a single
compiled path, no interpreter branch:
source.nir
│
▼
parser.rs hand-written LL(1) recursive descent → AST (ast.rs)
│
▼
typeck.rs whole-program type checking (~5,100 lines)
│ • two namespaces: type names vs. callable names
│ • struct/enum exhaustiveness in `match`
│ • effect(...) inference and mismatch detection
│ • validate_fragment(): typecheck one expression
│ fragment in a given variable-type context —
│ the agent-facing incremental-validation primitive
▼
ownership.rs affine/linear ownership checking (~855 lines)
│ • move-tracking scope stack, branch-uniform cleanup
│ (an affine value moved on only *some* branches is
│ rejected everywhere after that point)
│ • compute_free_map(): the deliverable codegen consumes
│ to know exactly which nir_free to emit, where
▼
refine.rs + smt.rs bounds proving, tiered (~676 + ~699 lines)
│ • Tier 1 (refine.rs): interval/range analysis —
│ always available, no external solver dependency
│ • Tier 2 (smt.rs): real Z3-backed proof for facts
│ Tier 1 can't decide
│ • codegen elides a runtime guard if *either* tier
│ already proved it safe; otherwise a guard is
│ inserted — never a silent gap
▼
codegen.rs (AOT, ~6,900 lines)
• check_supported(): walks the whole program and rejects — by name,
with a stated reason — anything not yet compiled, rather than
mis-compiling it or silently falling back to something interpreted
(there is nothing left to fall back to). See "Why 'reject, don't
mis-compile'" below.
• emits real LLVM IR via a hand-rolled Codegen driver, then invokes
clang/the linker for a native binary
• fully unrolls Vector/Matrix ops at compile time (the linalg speed
story, see Benchmarks)
One more pass sits alongside this core pipeline:
-
ui_gen.rs(~2,300 lines) — reads the typed program (it needs resolved struct fields and function signatures) and derives the CRUD + dashboard manifestnirdosha emit-uirenders into a static HTML/JS file. See The UI Engine — including why "static" is a real, current limitation, not a stylistic choice: the live, server-enforced half this diagram's older version credited to aserve.rsdoesn't exist right now (that file, and thenirdosha servesubcommand, were removed with the interpreter; see Honest Scope & Roadmap).
codegen.rs::check_supported is the single gate every not-yet-compiled
construct passes through, and its purpose is structural: grep this
function for the ground-truth "what compiles today" list — don't trust
prose docs, including this wiki, to be perfectly in sync with it. A
program using a not-yet-compiled builtin gets a named compile error from
nirdosha build/emit-llvm, not a silently wrong binary, and — since
the interpreter's removal — not a silent fallback to running interpreted
either: it's a hard compile-time rejection with a stated reason, full
stop. For an agent, this converts "did my program actually compile the
way I intended" from a trust question into a checkable one: the failure
mode for an unsupported construct is a structured diagnostic naming the
construct, identical in shape to any other compile error (see
LLM Integration).
runtime_kernels.rs (~825 lines) compiles as an isolated
rustc --crate-type staticlib (no --extern), statically embedded into
codegen.rs via include_bytes! and linked into every compiled binary —
so a compiled Nirdosha program has no runtime dependency on the compiler
installation that produced it. It carries the C-ABI surface codegen.rs
calls by name (nir_det, nir_tcp_connect, nir_sha256_hex,
nir_rand_f64, nir_alloc/nir_free, ...) plus a from-scratch SHA-256
implementation bit-verified against the standard's own test vectors — the
same "native call costs what inlined IR costs" reasoning that justifies
calling libm/printf directly rather than reimplementing them in IR.
-
crates/compiler/src/INDEX.md— line-numbered index of every major struct/function in the largest source files; the durable ground truth when this page's summary drifts. -
GRAMMAR.md— the full EBNF. -
LANGUAGE.md— the authoritative feature reference (see also Language Features).
Why
How
For LLM agents
Using it