Type Checking, Error Reporting, and Expression Graphs in Compilers
In a typical compiler pipeline, type checking happens during semantic analysis after parsing (and often after name resolution). The compiler uses (1) the AST (syntax tree) to understand the structure of a program and (2) semantic/type rules to assign types to expressions and verify that operations are applied to compatible types. For error reporting, compilers track source locations (line/column spans) for tokens and AST nodes, then emit diagnostics with helpful context, often attempting recovery to continue compiling and report multiple errors in one pass.
This section focuses on two connected views of an expression statement:
- The syntax tree (AST) derived from the grammar, which preserves the original operator nesting and parentheses.
- The expression DAG (Directed Acyclic Graph), which can share common subexpressions to reduce recomputation, similar to what many optimizing compilers do (e.g., common subexpression elimination).
We will illustrate both for the statement:
where denotes an infix binary operator (e.g., some language-defined operator).
Key terms you’ll use repeatedly:
- type checking
- AST
- diagnostic
- semantic analysis
Type Checking and Semantic Analysis in Compilers (Educational Overview)
1) Where type checking fits in the compiler
A common high-level organization:
- Lexing: turns source text into tokens, each with location spans.
- Parsing: builds an AST (or parse tree) according to grammar.
- Semantic analysis:
- Name/scope resolution
- Type checking
- Additional legality checks (e.g., “operator applied to wrong kinds of operands”).
- Intermediate representation (IR) generation / optimization:
- Expression DAG building / common-subexpression sharing may occur here or earlier/later depending on compiler design.
- Code generation
Type checking is typically syntax-directed: each AST node corresponds to type rules, and the compiler computes types bottom-up (postorder) or via constraints.
2) How type checking is performed (core mechanisms)
Most compilers implement type checking with one of these closely related approaches:
-
Attribute evaluation (syntax-directed translation)
Each AST node synthesizes attributes like:type(node)isLValue(node)constValue(node)(optional)
Rules are driven by grammar productions; e.g., for a binary operator node
E1 op E2, the checker:- checks
type(E1)andtype(E2) - applies operator typing rules to compute
type(E1 op E2) - reports diagnostics if rules don’t match.
-
Constraint-based typing (unification / inference)
For languages with type inference, nodes generate constraints like:type(E1) = inttype(E2) = type(E1)type(E1) -> type(E2)for function application, etc.
A solver then finds consistent assignments. Errors can be reported when constraints become unsatisfiable.
-
Environment-driven checking
For variables and function calls:- the checker queries a symbol table (environment) for declared types
- then validates usage against expected signatures.
We’ll use the bottom-up, rule-based view since it naturally maps to building an AST and, optionally, to an expression DAG.
Key terms
- symbol table
- synthesized attribute
- constraint solver
- operator typing rule
Bottom-up type checking for an expression AST
- 1Step 1
Parsing already built an AST. Each node retains token spans for later diagnostics.
- 2Step 2
For a node with children and , compute and (postorder traversal).
- 3Step 3
For a binary op node , consult the typing rule for to compute the result type from . If multiple overloads exist, choose the best match or report ambiguity.
- 4Step 4
If the language distinguishes l-values/r-values or requires numeric types for arithmetic, validate those requirements and emit errors on mismatch.
- 5Step 5
Store for later stages (IR generation, codegen).
- 6Step 6
If typing fails, create a diagnostic containing: operator name, expected vs actual types, and the source span from or its children.
- 7Step 7
To avoid cascading failures, insert an error type for and let parent checks proceed conservatively.
- 8Step 8
For an assignment node , compute and and enforce compatibility (exact match or allowed coercions).
Pro Tip: Prevent diagnostic cascades
When a subexpression fails to type-check, assign it a special error type so parent nodes can keep going without inventing dozens of follow-up errors.
Location matters for actionable error reports
Attach diagnostic spans to the smallest responsible AST node (e.g., the operator token or specific operand) so users can fix the exact source fragment.
3) Error reporting: what compilers typically include
A “good” compiler diagnostic often includes:
- Severity: error vs warning vs note
- Location: file, line, column, and sometimes a highlighted snippet
- Message: what went wrong (e.g., “operator
*cannot be applied toboolandint”) - Expected vs actual types (or expected operand categories)
- Related information:
- notes pointing to the declaration of an identifier
- additional context for overload resolution failures
- Recovery strategy:
- local recovery (skip tokens or insert placeholder nodes)
- panic-mode recovery (synchronize at statement boundaries)
- continue to find more errors
These details require the AST (or parse tree) to preserve token spans and—during type checking—to keep both inferred and expected types available for messages.
Syntax tree (AST) for the statement
We assume standard precedence/associativity for binary operators * and +, and that binds less tightly than but more tightly than a following in this expression as written. Interpreting:
Inside the right-hand side:
- Left operand of :
- Right operand of :
- Then the whole thing is added with (because there is a trailing after the expression)
So the RHS structure is:
AST (textual)
Let the node labels be:
AssignPlusfor+Timesfor*UpArrowfor↑
RHS AST:
Plus- left:
UpArrow- left:
Plus- left:
Timesab
- right:
c
- left:
- right:
Times- left:
Plusbc
- right:
b
- left:
- left:
- right:
c
- left:
Mermaid diagram: AST shape
Expression DAG (common subexpression sharing)
In the AST, the subexpressions and each occur in multiple places (notably, appears inside , and appears repeatedly as a leaf). A DAG can share structurally identical subexpressions so that the compiler computes them once.
For this specific expression, the obvious repeated subexpression is :
- It appears as the left child of the
Timesnode in((b+c)*b)only once as written, but it may still be shared with other identical occurrences if the overall program repeats it (or if the AST is built from a larger expression where(b+c)occurs multiple times).
To make the DAG meaningful for this single statement, we share at least:
- the leaf occurrences of identical variables (
a,b,c) are conceptually shared, - and we share the computed node for
(b+c)if there were multiple uses—here, it has one use within((b+c)*b). Still, we can express the DAG structure that would allow future reuse.
A more illustrative DAG would show sharing if the expression were, for example, (b+c) * b + (b+c) * b. Since it’s not, the DAG is mostly the same shape as the AST, but it still encodes identity of computed nodes.
DAG diagram
AST vs DAG: where computation can be reused
Qualitative comparison for this single statement (fewer repeats => smaller gains).
Type-checking and error-reporting edge cases
Knowledge Check
In a typical compiler, when is type checking performed?