Compiler design / 07. AST semantics and types

Name resolution, type identity and receiving-boundary rules turn ordered shells into typed semantic structure.

Giving syntax checked meaning

Parsed text can look right and still mean nothing the machine should trust. greet(visitor) on the page is a shape. Until name resolution and type checking finish, it is not yet a call with a known callee, argument contract and return type.

The AST stage turns ordered shells and bound visibility into a typed semantic structure. That structure owns source meaning for the module.

Syntax tree versus semantic tree

A syntax tree records how tokens grouped: call expression, argument list, identifier nodes.

A semantic tree (still often stored as AST nodes in Moth) adds resolved facts:

Name resolution connects spellings to declarations. Type checking checks that operations and values agree under language rules. Semantic identity names the underlying declaration or type in a way that survives aliases and module boundaries.

Before checking, the page call is unresolved shape. After checking, it is a fact later stages may lower without reopening the import list.

Diagram placeholder: Unresolved names gain symbol, argument and return facts

Caption: teaching simplification. Question: what does type checking add that parsing refused to invent?

Environments and visibility

AST consumes bound file visibility. It may validate semantic use of visible symbols. It does not rebuild imports or rediscover top-level visibility. If a top-level name cannot be found by walking sorted declarations and bound visibility, the missing fact belongs earlier in the pipeline.

Scopes nest inside bodies. The collision policy does not. All user-visible names share one rule: same-file declarations, source imports, binding imports, aliases, prelude symbols and builtins cannot silently shadow one another.

Local declarations, imports and reserved interfaces remain distinct origins even when their spellings sit in one lookup table.

Diagram placeholder: Two visible declarations trigger one collision result

Caption: accepted visibility policy. Question: who owns a name when two imports want the same spelling?

Type identity

Each compiled module owns one local TypeEnvironment. Inside that module, TypeId equality is the only valid comparison for local semantic decisions.

Cross-module interfaces use canonical type identities, not donor-local indexes. A consumer may intern compact local handles for imported types. Equality across modules still compares canonical identity, never rendered names alone.

Why refuse donor-local indexes at the boundary? Because module-local numbering is an implementation accident. Stable meaning must survive recompilation order, re-exports and parallel workers.

DataType spellings after resolution are parse or diagnostic helpers. They must not drive executable AST, HIR or backend decisions.

Access classification stays separate from type identity. Mutability and exclusive access do not manufacture fake type shapes.

Diagram placeholder: Local handles point to stable semantic identities

Caption: exact identity principle, simplified storage. Question: why not share donor indexes into the consumer?

Receiving boundaries and coercion

Expression evaluation finds an expression's natural type and stays strict. Contextual coercion applies only at explicit receiving boundaries owned by the frontend:

Natural typing asks "what is this?" Contextual coercion asks "may this value enter that slot?" Explicit casts ask "please convert under compiler-owned rules." Those paths stay separate on purpose. Broad implicit conversion would hide bugs. Forbidding every conversion would drown source in noise.

On the page, visitor as a String meets greet's name parameter at a receiving boundary. The constant "Priya" met visitor's declaration boundary earlier.

Diagram placeholder: A contextual boundary permits a documented coercion or requires a cast

Caption: simplified typing flow. Question: where does conversion happen, and who requested it?

Bodies, blocks and terminality

Functions contain statements and expressions. Some constructs produce values: value-producing if, match and block-form catch act as closed receiving constructs rather than free-floating general expressions.

They are valid only where a receiver is explicit. Every producing path must satisfy the receiving arity. Non-unit success returns must be terminal before HIR lowering: control must not fall off the end without producing the promised values.

AST owns those diagnostics. If HIR later sees a non-unit function that can fall through, AST broke its contract and HIR reports an internal transformation error rather than a casual source shrug.

HIR will make runtime control flow explicit. AST still owes a body that already makes sense.

What can fail here

Diagnostic transport and rendering wait for the diagnostics article. The semantic failures belong here.

Why Moth does it this way

Alternative: infer meaning from rendered type names, allow broad implicit conversion and permit shadowing as a convenience.

Choice: stable identities, strict names and narrow receiving-boundary rules.

Benefit: later stages consume facts rather than rediscovering source meaning from pretty strings.

Cost: source needs explicit disambiguation and casts in more cases.

Roadmap and current status

Exit state

You leave with a typed AST, semantic identities and checked executable bodies. Next: which static computations disappear before HIR, and which become explicit data.

Compiler design / Previous / Next