Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Frontend and checking

frontend.rs handles name resolution, local inference, mutability, and lowering to typed operations. Tokens and the lexer (frontend/lexer.rs), the syntax tree (frontend/syntax.rs) and the core parser (frontend/parser.rs) read no lowering state; feature modules add their own parsing beside their lowering. Type references retain source positions. Aliases resolve through an iterative, cached dependency walk before signatures are collected; alias-only cycles are rejected without recursive traversal of long chains.

Nominal definitions reserve identities before resolving fields. Weak stored edges and table-owning handles represent recursive type graphs without compiler memory cycles. Concrete type facts and native metadata use finite graph walks. Cloning a structural type shares its already-owned child storage. When a field escapes a nominal table, cloning promotes its weak edges and copies only the structural paths that contain them. This is a single pass through the structural type, without following nominal table links; stored definitions still keep weak edges so sharing cannot create a table ownership cycle. Callable and boxed callable signatures follow the same rule: a signature is shared unless an input or the output holds a weak edge. A complete class or enum caches its facts; a collection or box combines its contents’ facts instead of walking again. Aliases normalize to Ty values and generate no runtime operations. Generic instance arguments distinguish Type(Ty) from typed constant values; values never become storage types. The constant evaluator also serves value arguments during type resolution, before ordinary module-constant folding. Quantity constant output types resolve lazily with cycle detection. The Bounded checker validates each concrete interval and lowers admitted literals and checked conversions to private single-field class construction.

The compiler’s own hash tables (src/hash.rs) use FxHash rather than std’s SipHash. Their keys are names and types from the program being compiled, so flood resistance buys nothing, and SipHash was about 11% of checking instructions (issue #302). Iteration order stays unspecified.

Module loading canonicalizes qualified names and checks imported modules together before producing one application object. Import traversal is bounded to depth 128 and 4,096 modules. This is not separate module compilation.

A module is lexed one top-level item at a time. The parser holds the tokens of the item it is parsing and of the next one, so a module’s tokens never all exist beside its syntax tree (issue #54). A lexical error anywhere in a module is still reported before any syntax error in it.

The whole syntax tree is live when lowering starts, so its layout sets much of checking’s peak memory. Blocks, call arguments and parameters are collected on parser scratch stacks and stored at their exact length. Every node holds a 40-byte token that names its module by an interned file number, not a reference-counted path (issue #422).

op.rs defines backend-neutral structured operations and an independent checker. Its compile-time operand stack is private compiler machinery. CompiledFn carries a signature, documentation, body, and typed local slots. Unit produces zero stack values. Loads and stores address slots; exhaustive branches produce conditional values.

The checker collects every signature on one thread, then checks function bodies on all available cores: a body reads only the signature table. It reports the first error in program order whichever body failed first, so diagnostics do not depend on the core count. Operations, types, and signatures are shared between threads without copying.

Name resolution of a module’s functions, and of its classes’ fields and methods, runs on all cores when the module has at least 256 functions or 64 classes: each reads only the module’s scope. Frame layout and generator operation validation after lowering also run on all cores. Both report the first error in source or program order, as a serial pass would. Threads share generator, closure, and nominal layout caches. A cached layout records how deep its frames and environments nest, and is reused only where it cannot hide the nesting limit of 64; otherwise the type is measured again. Each function’s layout error is therefore the same whichever thread measured a type first (issue #126).

Parsing, the rest of name resolution, type resolution, class expansion, generic preparation, signatures, and lowering bodies to operations run on one thread (issue #59). Type resolution can create generic data instances in a table shared by every module, and lowering interns strings, records generic instances, and infers returned fields in tables shared by every body. Generic preparation resolves each concrete function’s parameter and return types once; registering the function’s signature reuses them.

Three ways to run these phases on several threads were measured and not adopted. Each gained little and raised peak memory, because each thread’s allocator keeps its own free memory:

  • Parsing one module in pieces: for 5,000 units, parsing took 0.09 s instead of 0.21 s, lowering took 0.06 s longer, and peak memory rose from 417 MB to 538 MB.
  • Parsing imported modules in parallel, a level of imports at a time: for a program whose imports load 21 standard modules (12,549 lines), parsing took 9 ms instead of 23 ms and lowering 5 ms longer; the check was no faster, and peak memory rose from 28 MB to 35 MB.
  • Lowering bodies speculatively on all cores and keeping them in queue order: checking 5,000 units was about 9% faster and object emission no faster, and peak memory rose 12–14%.

Measuring compilation gives the time each phase takes.

Control flow and access

Return, TailCall, Break, and Continue terminate paths. The checker joins only continuing branches, validates transfers against signatures or loop entry stacks, and rejects operations after guaranteed exits. Loops lower to headers, bodies, and exits; the for increment precedes each continue.

An if statement lowers to one operation that holds its elif arms as a list, each with its own condition, so no pass recurses once per arm and a chain costs linear time. Every pass does recurse once per level of block or expression nesting, as do the clone and drop of the syntax tree. The parser therefore bounds that depth to 64 and later passes rely on it instead of checking again.

Ownership checking tracks definite slot availability through branches and loop backedges. Borrow checking flattens typed access facts into a separate source-level CFG with branch successors, early exits, loop edges, and stable binding-place IDs. Each CFG node stores up to two successors inline; only matches with more than two arms allocate a separate successor slice. Edges retain source branch order, including duplicate successors when empty arms join the same continuation. Loan liveness reaches a fixed point before native lowering. Functions without loan facts skip this analysis. It never runs on Cranelift IR and needs no whole-program alias analysis or per-call inspection of function bodies.

Native lowering still consumes structured operations. Cranelift SSA variables represent slots; stores define variables and sealed joins reconcile paths. Only slots whose address is taken (&x, &mut x) or whose values have inline storage live in the function’s stack frame; the frame holds those slots alone. Augmented assignment of a local (x += 1) takes no address, and starting a thread adds no slot: task storage has its own frame space and a worker reaches parent slots only through borrowed arguments. Generators keep every slot in memory. Source spans and marker operations carry diagnostic provenance without changing move or loan rules. A generic instance also records the use that first required it, so that lowering and ownership errors in its body are reported at that use.

Std ceiling checking

frontend/ceilings.rs checks fixed upper bounds without introducing value generics or another integer type. Embedded std source can append __max(1024) to a parameter or field declared Positive[i64] or NonNegative[i64]. The decimal non-negative bound is metadata on its TypeRef; resolved types, layout, equality and callable signatures are unchanged. The marker is rejected in user declarations and is hidden in public editor signatures and the generated std signature index.

Direct function, method and generated class-constructor calls check their supplied arguments before named/default wrappers replace expressions with local names. Defaults are checked at their declaration. Direct field assignments and replace(owner.field, value) use the same check, including through borrowed or boxed owners. The module-constant evaluator handles literals, module constants (including integer-domain and Quantity constants), and supported constant operators/casts. Constant folding runs after type resolution so quantities retain normalized dimensions and storage types during evaluation. Ordinary local bindings, arbitrary function calls and explicit fallible domain conversions are not constant evaluation. Indirect callable invocations and writes through a separately borrowed field do not retain this metadata. Existing std runtime guards cover values that reach operations through those paths; this mechanism does not claim a proof-carrying bounded integer type.

The annotations cover the fixed ceilings enforced by the std limit-domain migration: format/JSON/TOML/XML/template/regex nesting, fs/tempfile cleanup depth and attempts, sketch construction limits, unicode scalar limits, JWT token size, HKDF output size, image dimensions, actor heaps and procfs read sizes. Decoded sketch max_capacity has no ceiling. General bounded integers and lower bounds above one remain B96.

Embedding APIs

Source-string checking and compilation accept Plenty source and reject imports. File APIs (check_file, check_module_file, and file compilation) resolve imports from an explicit optional root. Checking performs no execution or native emission.

Editor queries

ide::analyze, behind plenty lsp, runs the same load, resolution, and lowering with a per-thread recorder switched on (src/frontend/ide.rs). Module loading records modules and imports; name resolution records declarations, module-level names, and locals with their binding positions; lowering records what needs types: methods resolved through the receiver, fields, enum variants, and the types of locals. An ordinary compilation records nothing. Expressions keep only the position of their first token, so member expressions also carry the position of the member name. In this mode an error in one function’s resolution or lowering is recorded and checking moves to the next function. See the language server.