Compiler design / 11. HIR

HIR linearises evaluation order, records link facts and becomes the first backend-facing semantic IR.

Lowering meaning into explicit control flow

Nested expressions are friendly to humans and slightly rude to machines. Evaluation order hides in tree shape. Early exits hide in nesting. A borrow checker that must rediscover all of that from sugar will rediscover it wrong.

HIR (a high-level intermediate representation in Moth's pipeline) lowers fully typed AST into the first backend-facing semantic IR. Meaning stays. Shape changes. Nested source sugar becomes an explicit branching structure analyses can walk.

Lowering without changing meaning

An intermediate representation is a program form meant for the compiler's internal work rather than the author's eyes.

Lowering transforms one representation into another while preserving the semantic contract the language already fixed.

Compare a nested call in AST spirit:


    return greet(visitor)
    

with ordered runtime work in HIR spirit:


        tmp0 = load visitor
        tmp1 = call greet(tmp0)
        return tmp1
    

The second form makes evaluation order and temporary storage obvious. Backends and analyses stop arguing about tree walk conventions.

Diagram placeholder: One nested call becomes ordered locals, operations and a terminator

Caption: teaching simplification. Question: what does lowering make explicit?

HIR building blocks

Teaching vocabulary:

Expression side effects linearise into statement preludes. The final value then feeds the next operation or terminator. Plain binary operations may remain for booleans and comparisons. Runtime template string construction lowers through explicit string append operations. Runtime scalar arithmetic lowers through explicit checked numeric statements.

Diagram placeholder: Blocks connect through branch and exit edges

Caption: conceptual CFG. Question: where do analyses walk?

Runtime template work

Neutral template handoff from AST becomes ordinary string and control-flow operations in HIR:

Compile-time page fragments and TIR itself do not enter HIR. Folded fragment metadata lives in module metadata lanes. HIR validation checks executable fragment operations only.

The greeting's runtime pieces, if any remain dynamic, look like ordinary operations over strings and calls. Static compile-time fragment text stays metadata until the HTML builder inserts it.

Call targets and link facts

Source calls use explicit target classes:

Generated concrete functions add another stable target class once materialised.

HIR stores no import aliases as semantic authority and no backend runtime names as identity. Beside HIR, the compiler records per-function link facts: call edges, helper and capability needs, reactive features, numeric and cast operations, path and asset requirements, project-context provenance and generated-function requests.

Those facts are the linking authority later stages consume.

Diagram placeholder: Local, imported, generated and binding-backed calls point to explicit identities

Caption: simplified link facts. Question: how does a backend know its dependencies?

Diagram placeholder: HIR remains unchanged while link facts sit beside it

Caption: exact side-data boundary, simplified shape. Question: what may analysis write without rewriting HIR?

Validation boundary

HIR validation completes before borrow validation or target validation. It checks definition identities, type links, CFG shape, terminators, local and place references, start-function metadata, constants, reactive metadata and related invariants.

NaN and infinity in HIR are internal invariant failures, not casual user values.

A backend-neutral structured HIR view may be derived when a structured lowerer needs it. It is not a second semantic authority. Cache it only as derived data.

Borrow validation will read validated HIR and write side tables. It will not rewrite the IR to taste.

What can fail here

Keep internal failures separate from source diagnostics. Authors did not "misuse HIR." The compiler misfired.

Why Moth does it this way

Alternative: lower AST directly into JavaScript or Wasm per backend.

Choice: backend-neutral HIR with explicit control flow and side facts.

Benefit: analyses and targets share one validated runtime representation. A borrow bug fixed once stays fixed for every backend.

Cost: lowering must materialise temporaries and preserve more explicit state.

Roadmap and current status

Exit state

You leave with validated HIR and per-function link facts. Next: what memory guarantees analysis can derive from that control flow, and why GC does not replace borrow checking.

Compiler design / Previous / Next