Compiler design / 17. Backend lowering

Lowerers translate validated HIR into target code without reopening source or rewriting language meaning.

Turning HIR into JavaScript and Wasm

One HIR operation may become a JavaScript expression in one partition and a Wasm instruction sequence in another. The meaning stays Moth. The spelling becomes target-native. Same source meaning, different host climates.

Lowerers translate validated inputs. They are not second language frontends.

Lowerer inputs

A lowerer receives explicit records:

A lowerer does not:

If a lowerer needs a fact, an earlier owner should have recorded it.

The ValidatedMemoryPlan is the memory half of that rule. For this physical variant, it arrives with its physical decisions already settled: selected memory layout per allocation family, region and declared-region placement, planned affine-responsibility transitions, retained-edge obligation transitions and discharge operations, cleanup and destruction plans, and REC representation decisions. The lowerer encodes those decisions. It does not infer them from borrow facts, recompute a count delta or choose a cleanup site. Borrow facts and validated lifetime topology remain validation context and assertion material only.

Because the plan is built after target partition and target validation, it is scoped to one physical variant. The same source function can be lowered GC-native in a variant targeting a host collector and collector-free with REC-capable handles in a variant targeting full memory control. Those variants share the same validated topology and the same source semantics; only the physical plan differs.

For the walkthrough, target-labelled start, greet and template work arrive ready. The JavaScript path might own page orchestration. Neutral string work may sit in either partition depending on assignment.

Diagram placeholder: Selected target sends one HIR input to two target paths

Caption: exact frontend boundary simplified. Question: what stays shared?

JavaScript lowering

The JS backend speaks host values and functions. It leans on the host GC for reachability. Runtime helpers implement language operations the surface syntax made look primitive: checked numerics, certain casts, template runtime pieces, error plumbing.

greet in JavaScript form is ordinary host code that preserves Moth contracts. A more permissive JS primitive must not silently weaken checked arithmetic, finite-Float rules or error propagation.

Wasm lowering

Accepted Wasm design prefers a structured path:

  1. optionally derive a structured HIR view (not a second semantic authority)
  2. lower into backend-owned structured LIR (a lower IR)
  3. apply ABI, handles, layouts, memory and runtime imports Ownership-aware Wasm operations encode the plan's settled transitions at their planned semantic commit points. The lowerer emits planned discharge operations and retained-edge obligation transitions as provided. It does not derive ownership or obligation changes, recompute count deltas or choose cleanup sites.

Beginner picture:

Old dispatcher-loop LIR stories, moth_start bootstrap myths, i64 Int bridges as the final story and per-module memory as the default are removed final-design paths. Teach them only as historical "do not rebuild this" notes, not as the architecture.

Diagram placeholder: A derived backend view feeds structured blocks and ABI operations

Caption: accepted Wasm design, simplified records. Question: where does target structure enter?

Shared runtime and wrappers

Accepted HTML mixed-target design wants page-local shared Wasm runtime and memory rather than a scatter of isolated memories per module as the end state. Page-local shared Wasm runtime/memory applies to Moth-linked variants only. Imported WIT components use component-private memory and value conversion.

JavaScript companion facades and wrappers call Wasm exports through explicit handles and layout rules. The bridge is data, not guesswork.

Diagram placeholder: JavaScript companion calls a Wasm export through explicit handles and layout rules

Caption: teaching boundary. Question: how do targets communicate?

External bindings and ownership facts

Binding-backed calls lower through demand-driven glue. The binder's semantic interface already existed upstream. The lowerer maps stable external ids to imports, helpers or native operations.

At a binding-backed boundary, the closed external-boundary classification and the ValidatedMemoryPlan supply the ownership and obligation behaviour for this physical variant. The lowerer encodes that settled result rather than rereading borrow facts to decide whether a call transfers or discharges responsibility. Borrow and last-use facts and validated lifetime topology remain upstream inputs and validation context, not a source for new physical decisions. Where the plan selected a GC-native representation, planned ownership-specific operations may be absent in the target encoding; where it selected affine, inferred-region, declared-region or REC handling, the lowerer emits exactly what the plan says. Every backend still requires the same validation, and optimisation must preserve observable behaviour and the same accepted program set.

What can fail here

User-facing target failures preferably die in validation, not mid-emission.

Why Moth does it this way

Alternative: give each backend a source interpreter or separate frontend.

Choice: one validated semantic HIR plus target-owned lowering.

Benefit: source behaviour remains shared across JavaScript and Wasm.

Cost: each backend needs a faithful ABI and runtime implementation.

Roadmap and current status

Exit state

You leave with JavaScript, Wasm, wrappers, runtime modules and asset records as data. Next: how the HTML builder assembles and owns those outputs on disk, ending at <p>Hello, Priya!</p>.

Compiler design / Previous / Next