Compiler design / 04. Project graphs and modules

Folders become a canonical source graph with providers, consumers and one compile per physical module.

Turning folders into a compiler graph

A directory is a convenience for humans. It does not tell the compiler which files form one semantic unit, which root owns them or which module must compile before another can bind a dependency.

Stage 0 answers those questions. It turns the greeting project's filesystem tree into a canonical source graph.

Nodes and edges

A graph here means nodes plus directed edges with meaning.

Nodes include modules, packages and other compilation units the build system recognises.

Edges record relationships: this module depends on that provider, this package depends on that package.

A provider supplies a public interface. A consumer binds against that interface after the provider succeeds.

In the running example, @page.moth consumes @greeting. The edge names a provider relationship. It does not copy greet's body into the page module. Think of a graft point, not a clone.

Diagram placeholder: Roots own files while dependencies create provider edges

Caption: running-example identities simplified. Question: what does a folder mean to the compiler once Stage 0 finishes?

Roots, packages and ownership

Moth marks module roots in the filesystem on purpose.

Source-backed packages compile Moth source into ordinary module artefacts. Binding-backed packages expose typed external interfaces without Moth module bodies. Both appear in the capability surface. Only source-backed modules produce HIR.

Filesystem layout carries semantic constraints. That is the point. The graph should not invent a different topology than the tree authors can see.

Owned and semantic source sets

Stage 0 distinguishes sets that newcomers often collapse.

OwnedSourceSet. Files this module root owns on disk.

SemanticSourceSet. Files that participate in semantic compilation for the command.

Check-only orphan units. Files the tool may still parse for diagnostics even when they sit outside the semantic set for a build.

Ownership does not inject disconnected declarations into a consumer. A file can be owned and still remain outside the semantic set for a given command. Dependency visibility and ownership are different axes.

Diagram placeholder: Owned files can remain outside the semantic source set

Caption: conceptual distinction. Question: why does ownership not imply binding visibility?

Provider-first scheduling

After the graph finalises, the build system schedules compile waves. Providers finish before consumers bind, root growth before branch binding.

A module waits until every required provider interface exists. Binding-backed interfaces may appear earlier than source providers because they do not wait on Moth module compilation.

One physical module compiles once per project or package compilation boundary. Entries later activate roots. They do not recompile the same module once per page.

Scheduling is where the build system stops. When a module is ready it builds one compiler input and calls one compiler module compilation service, then handles the outcome: success, diagnosed or an internal compiler error. Binding, ordering, AST, HIR and borrow validation happen inside that call. The build system never runs those stages step by step from its own code.

For the greeting project, greeting.moth's module compiles and publishes an interface. Only then does @page.moth bind @greeting greet.

Diagram placeholder: Providers finish before consumers bind

Caption: deterministic schedule. Question: when can interface binding start for the page?

Diagram placeholder: Page consumes the greeting interface

Caption: teaching graph, not a complete package graph. Question: who must compile first?

Deterministic identity

Graphs need names that do not jitter when threads finish in a different order.

Stage 0 and the compiler assign stable identities to sources, modules and later declarations. Exact Rust type names may change. The rule does not: identity must not depend on worker completion order, cosmetic filename suffixes for roots or source position alone.

Determinism lets parallel compilation merge results in canonical order. Diagnostics and artefacts stay reproducible.

What can fail here

If current code still compiles a looser closure than the accepted module model, label it transitional against the roadmap plan.

Why Moth does it this way

Alternative: global search paths, nearest-match resolution or "scan the package folder and hope."

Choice: structural roots, explicit package identity and canonical ownership.

Benefit: graph scheduling and diagnostics share the same source facts. A missing provider is a graph fact, not a surprise during type checking.

Cost: filesystem layout carries semantic constraints authors must learn.

Roadmap and current status

Exit state

You leave with a canonical source graph, source sets and ordered compile waves. Next: how the compiler prepares each selected source file once, starting from characters.

Compiler design / Previous / Next