Compiler design / 01. What a compiler does

Between source text and a finished page sits a ladder of representations. This article names the jobs on that ladder and the owners who hold them.

What a compiler does

You type source. Something emits <p>Hello, Priya!</p>. Between those two facts sits a chain of transformations. Each step is a bit less like human prose and a bit more like something a machine can use.

People often shrink that chain to a slogan: "the compiler turns code into machine instructions." That slogan fails the greeting project early. The page may never touch native machine code. It still needs tokens, types, control-flow graphs, link plans and a builder that writes HTML. That work still counts as compiling.

Meaning versus representation

Source meaning answers: what did the author intend under the language rules?

Target representation answers: what concrete form will a host execute or display?

Transformation answers: how do we move from one to the other without losing the meaning we care about?

The project tree enters as authored text:


    compiler_walkthrough/
    ├── config.moth
    └── src/
        ├── @page.moth
        └── greeting.moth
    

Those files hold characters on disk. They do not yet hold a typed call graph, a borrow fact or a Wasm export. Meaning arrives when a stage applies language rules and records the result in a structure later stages trust.

Think of a seed and a plant. The seed holds a plan. The same plan can grow under different conditions into different adult forms. Source meaning is the plan. Target code is the plant in a particular climate: browser JavaScript, Wasm, an HTML shell. Compilers live in that gap.

Diagram placeholder: One source plan becomes different target forms while keeping the same meaning

Caption: teaching analogy for source meaning and target form. Question: which parts of the greeting project still look like source plan, and which already look like a page a browser can show?

A small vocabulary

Words collide in casual talk. Pin them by job.

A compiler reads source under language rules and produces another representation a machine or host can use. That product might be native code, bytecode, JavaScript, Wasm, HTML fragments or a structured artefact a builder will assemble.

An interpreter walks a representation and performs the operations as it goes. It may parse first. Execution still happens inside the interpreter process, not as a finished artefact handed to another runtime.

A transpiler usually means a compiler whose target is another human-authored language at a similar level, such as TypeScript to JavaScript. The word names a common special case, not a third kind of computation.

A linker connects separately compiled pieces. It resolves which functions call which others, which symbols an entry needs and which objects belong in one final image or bundle.

A build system decides what to compile, in which order, with which inputs and where outputs land. It schedules work. It does not replace the language's type checker.

Moth splits those jobs on purpose. The compiler library owns semantic meaning for modules. The build system owns project graphs, entry plans and writing files. Backends lower already-validated meaning into JavaScript or Wasm. The HTML builder assembles page-shaped outputs.

Diagram placeholder: Three routes from source toward execution with overlapping responsibilities

Caption: teaching simplification, not a complete taxonomy. Question: where does execution happen for Priya's page?

Frontend, analysis and backend

Compilers often speak of a frontend, middle analysis and a backend. Treat those as responsibilities, not a sacred phase diagram every language must copy.

The frontend faces source text. It tokenises, parses shells, binds names, checks types and builds a typed structure that captures source meaning.

Analysis walks that structure to prove properties: terminal control flow, borrow safety, reachability, target capability. Some languages fold analysis into the frontend. Moth keeps several analyses after a first backend-facing IR exists.

The backend faces a concrete target. It lowers validated meaning into JavaScript, Wasm, helpers and glue. It should not reopen source files to rediscover what greet means.

Moth's compiler library holds frontend and analysis stages. The project tool and builders sit around that library. Stage 0 graph construction belongs to the build system, even when people call the whole CLI "the compiler."

Diagram placeholder: Owner map for build system, frontend, analysis and backend

Caption: ownership categories from the canonical authorities. Question: who decides which files enter the graph, and who decides whether visitor type-checks?

Why intermediate representations exist

Why not keep one giant syntax tree until the end?

Because each consumer wants a different kind of clarity. You do not jump from seed to full plant in one step. Roots, stem and leaves each solve a different problem.

Representation

Ambiguity it removes

Raw text

none yet: characters only

Tokens

word and punctuation boundaries

Prepared shells

top-level shape before bodies

Typed AST

names, types and source meaning

HIR

evaluation order and control flow

Target code

host instructions and calling conventions

Raw text still confuses a parser about where one token ends. Tokens still confuse a type checker about which greet you meant. A typed AST still nests expressions so a borrow checker must rediscover evaluation order. HIR still refuses to choose JavaScript versus Wasm spelling.

Each step deletes one class of question so the next owner can ask a harder one.

One fact, one owner. If the AST already folded greeting_prefix to "Hello", HIR should not re-parse the constant. If HIR already recorded a call edge to greet, the linker should not guess callees from string names in source. Duplication invites drift. Drift invites bugs that only appear on one backend.

Diagram placeholder: Representation path from source text to target code

Caption: simplified Moth walkthrough. Question: why keep multiple forms instead of overwriting one tree in place?

What can fail here

Failures arrive in families, not one grey cloud labelled "error."

Malformed source breaks spelling, structure or language rules: a bad escape, an unknown name, a type mismatch.

Unsupported target capability means the program makes sense as Moth yet needs something the chosen target cannot host, such as a DOM API on a pure Wasm partition.

Missing project input means the build never obtained a required config value or source file before semantic work could start.

Later articles assign exact diagnostic ownership. Here only the families matter: user source, target contract and project bootstrap fail for different reasons and travel different lanes.

Why Moth does it this way

Alternative: one target-specific compiler path that thinks in HTML and JavaScript from the first token.

Choice: backend-neutral compiler facts, with the build system orchestrating projects and builders.

Benefit: multiple builders and targets reuse the same semantic work. A check command can validate without writing a page. A future non-HTML builder can reuse module artefacts.

Cost: explicit boundaries and more intermediate data to maintain. You pay in structure so you do not pay in repeated meaning.

Roadmap and current status

Exit state

You leave with a vocabulary and a representation path. Next: why Moth places language design and build-system design in the same conversation.

Compiler design / Next