The compilation ladder: source to typed IR
Source text descends a fixed ladder, and each rung hands the next a different
artifact. core/src/lib.rs exposes the whole descent as two functions: compile
(parse → elaborate) and compile_and_typecheck (parse → elaborate → typecheck →
CompileOutcome).
- Text → tokens. The lexer reads characters into tokens with no context-dependent rules — there is one lexer, not the several a POSIX shell needs. (syntax)
- Tokens → flat surface AST. The parser builds a single flat
Astenum. The flatness is deliberate (ast-stays-flat): head classification (^name,./x,~/x, bare) happens here, but no desugaring does. - Surface AST → CBPV IR. The elaborator is the one phase that knows about
surface sugar. It enforces the command split by binding
effectful sub-expressions to fresh temporaries (a binds accumulator folded
into
Comp::Bindchains), resolves command heads against lexical scope, and runsgroup_stmtsfirst to find mutually recursive binding groups, which lower toLetRec/Rec. What it emits carries no parser syntax (ir-pure-cbpv). (elaboration) - IR → typed IR. Hindley–Milner inference annotates the
Val/Comptree (types), generalising at eachBindalong the SCC structure the elaborator already found — non-recursive groups generalise at their binding point, recursive groups stay monomorphic until their fixed point. The checker is a transformation (annotate): it returns an annotated IR carrying three kinds of verdict. Each top-level name-bind takes the generalisedSchemeit inferred, closed against the empty environment so the scheme outlives the per-turn unifier (session-scheme-continuity); everyPipelinestage andBindRHS — at any depth — takes its ground mode wire, the checker’s resolvedF[input, output]with the unification variable defaulted away, which the evaluator reads instead of re-inferring (ir-pipespec-annotation); and eachPipelinecarriesstage_types, one resolved value type per stage parallel to its wires — the data flowing between stages, retained for the structural REPL’s typed spine, never read by the evaluator. With the second mode-inference engine retired (unconditional-mode-pass), this rung is the only source of the evaluator’s modes: the pass runs on every evaluated path, so the wires are never re-derived at runtime. A node inference never visited keeps the elaborator’s placeholder —Emptyfor a wire,Unitfor a stage type. The verdict rides inside the comp;CompileOutcomeis unchanged in shape. (typecheck)
Each turn’s check is seeded from the live session — one SessionSchemes, the
scope’s name→scheme map plus the alias arms’ schemes — so a binding made in one
turn enters the next turn’s check at its inferred scheme rather than a fresh
variable. The evaluator installs each top-level bind’s scheme next to its value,
so the seed never drifts from the values it describes.
The prelude is baked once at build time as a schema-less postcard blob of this
same IR, so any field added to Comp, Val, or Pattern invalidates every
emitted blob — a hazard pinned with cargo:rerun-if-changed in one place,
bake_prelude_to_out_dir (core/src/driver.rs), since the only encode site and
the only decode site (BakedPrelude) live there together as the host-embedding
seam (host-embedding-api). The bake runs
the checker: it parses, elaborates, and hands the comp to bake_prelude
(core/src/typecheck.rs), which serialises the annotated prelude and harvests
its bind schemes from the same pass, so the baked list and a turn’s installed
schemes come from one harvest. The two blobs — annotated IR and scheme list —
land in OUT_DIR; a host embeds them through the baked_prelude! macro into a
BakedPrelude, decoded lazily on first use. The typed IR is then handed to the
evaluator, which a host reaches only through the
synchronous framed turn doors (unify-turn-evaluation).
See also cbpv, types; map hub
core. The formal account is docs/SPEC.md.