Compiler design / 06. Declarations, imports and ordering

Prepared shells, bound imports and local declaration order give each module its top-level silhouette before bodies type-check.

Finding the program's shape

Bodies can wait. Before the compiler checks whether greet(visitor) type-checks, it needs the module's top-level shape: which declarations exist, which imports wait on providers and which local names depend on which other local names.

Header work finds that shape once. Later stages bind and consume retained syntax. They do not reparse the file from characters whenever a provider finishes.

Prepared shells

From located tokens, syntax preparation extracts shells:

For the greeting module, shells include something like "constant greeting_prefix" and "function greet with these parameter and return type shells." For the page, shells include the import of greet, the visitor binding and page fragment metadata around the HTML template.

Preparation does not type-check executable bodies. It does not open source provider interfaces. It records what source syntax already makes knowable without them.

Three relationship classes

Header processing keeps three edge families distinct. Mixing them is a reliable way to build a confusing compiler.

Structural provider references belong to Stage 0 graph construction. They answer "which module or package must exist before this consumer can bind?"

Imported symbol bindings belong to visibility and AST. They answer "which public names from a provider become usable here, under which local spellings?"

Local declaration-ordering edges belong to Stage 3 inside one module. They answer "which top-level declarations must be processed before which others in this module?"

An imported declaration is never a node in the consumer's local declaration graph. Copying provider declarations into each consumer would destroy identity and explode work.

Diagram placeholder: Structural, imported and local-ordering edges use separate arrows

Caption: exact ownership distinction. Question: which graph is being built in each sentence of a design discussion?

Binding retained imports

PreparedHeaderSyntax exists before providers compile. Import shells wait with their source locations intact.

After @greeting compiles successfully, the build system schedules the page module. Interface binding resolves retained import shells against the immutable public interface:

Binding does not retokenize. It does not reparse declaration syntax. It fills in what only a finished provider can supply.

Before binding, greet is a name in an import shell. After binding, it is a stable origin identity with known type facts the AST can trust.

Diagram placeholder: Import shell gains stable provider identity and visible type facts

Caption: simplified retained syntax flow. Question: what waits for the provider?

Local declaration ordering

Inside one module, Stage 3 builds a declaration dependency DAG from retained local edges and sorts it.

Edges include facts AST needs before linear consumption:

Function-body references do not create ordering edges. Bodies may call functions declared later in the file. Only the top-level shape needs a stable process order.

Sorting uses stable tie-breaking so independent declarations keep source-order dignity. Same-file constants keep source-order semantics. Same-file forward references among constants are rejected. Cross-module constants arrive already folded in provider interfaces.

Dormant start appends after declarations. It never participates as a dependency node that other declarations wait on.

Diagram placeholder: Dependencies flow into a stable declaration order

Caption: conceptual topological sort. Question: why does order matter if bodies can still call later functions?

Cycles and visibility

If two local declarations require each other in a way the edge set captures, the sort stops and a cycle diagnostic fires.

Visibility rules stay strict. User-visible names do not silently shadow one another across same-file declarations, imports, aliases, prelude symbols and builtins. One collision policy covers the surface.

Imported declarations influence visibility after binding. They still do not become local graph nodes for Stage 3.

Diagram placeholder: Two declarations point at each other and a callout marks the cycle

Caption: failure-path teaching visual. Question: what stops the sort?

What can fail here

Final prose and rendering of diagnostics wait for the diagnostics article. Here the failures are structural.

Why Moth does it this way

Alternative: reparse source after providers compile, or copy declarations into each consumer.

Choice: parse and retain syntax once, then bind immutable provider interfaces.

Benefit: source locations, identities and graph scheduling stay aligned. Parallel workers can prepare shells early and bind later.

Cost: prepared syntax carries more metadata across the build boundary.

Roadmap and current status

Exit state

You leave with bound visibility, stable imported facts and ordered declarations. Next: how syntax acquires checked semantic meaning in the AST.

Compiler design / Previous / Next