Semantic Analysis

Parsing proves a program is well-formed; semantic analysis proves it is meaningful — names resolve, types agree, rules hold. This pass turns Phase-1 semantics, types, and errors from a specification into a machine-checked reality.

Symbol Tables & Resolution

The analyzer walks the AST once per concern, consulting a symbol table that mirrors the language’s scoping rules.

A scope stack: global, function body, and inner block; resolution walks outward

Figure 1 — The scope stack answers “what does this name mean here?” exactly as Semantics & Scoping specified.

The Resolution Walk

For each name occurrence: search the innermost scope, then outward; annotate the AST node with the winning binding. Resolve once, store the answer, never search twice.

Duplicates and Shadowing

Report duplicate declarations in one scope; allow shadowing only where the design document did. The rule from Semantics & Scoping is now code, not prose.

Type Checking

Rules from Types & Values

For each expression node: infer its type, then check it against the operator’s domain. Dynamic modes defer this to runtime; static modes run it here, in the middle of the pipeline, and stop a bad program before it ever executes.

Inference in a Nutshell

Without writing a solver: propagate types up from literals and parameters, unify at operators, report the first node with no consistent type. For small DSLs this bottom-up synthesis is enough.

Desugaring

Remove the sugar the surface syntax promised: turn loops into the semantics’ canonical while-form, expand shorthand into explicit rules. Later passes then see exactly one canonical shape.

Sugar Discipline

Every sugar needs a differential test: the desugared form must evaluate identically to hand-written equivalents.

Semantic Errors

Report in Batches

Undefined names, duplicate declarations, type mismatches, and violated domain rules are reported here, following the Errors & Diagnostics style: location, expectation, help. Collect all findings in one pass instead of stopping at the first.

Example: A Resolution Pass

The heart of analysis in one commented function: push scopes, resolve names, and decorate the tree.

resolver.py (commented)

# resolver.py — one walk, one concern: names must resolve
class Resolver:
    def __init__(self):
        self.scopes = [{}]              # scope stack; outermost first

    def declare(self, name):
        self.scopes[-1][name] = True    # remember: declared here

    def resolve(self, name):
        # search news to old scopes; shadowing falls out of the order
        for scope in reversed(self.scopes):
            if name in scope:
                return name              # the winning binding
        raise NameError(f"undefined name {name!r}")

    def enter(self):  self.scopes.append({})
    def exit(self):   self.scopes.pop()

How to Read It

Entering a block pushes a scope; exiting pops it; resolve walks outward as Semantics & Scoping specified. Type checking and domain-rule checks are the same shape with different payloads — that uniformity is what makes the analyzer testable.

Next Steps

Continue Phase 2

A checked tree is a good IR. See how it is lowered and shaped: Intermediate Representations, then Bytecode & Codegen.

Resources