7.2 Syntax Trees, Directed Acyclic Graphs & Value Numbering
💡 Core Intuition: The 3-Second Mental Model
🍳 The Everyday Analogy: The Shared Blueprint Deduplication Station
Imagine an architect drawing building plans for a 50-story commercial skyscraper:
- Parse Tree: A massive, legalistic municipal contract documenting every single grammatical rule used to describe the building (including commas, punctuation, and structural boilerplate).
- Abstract Syntax Tree (AST): A clean architectural blueprint showing only the actual physical walls, beams, and windows. However, if all 50 floors use identical bathroom plumbing, the AST draws 50 separate, duplicate copies of that plumbing schematic.
- Directed Acyclic Graph (DAG): The master engineer notices the redundancy and draws the bathroom plumbing schematic exactly once in a shared blueprint box. All 50 floors simply draw an arrow pointing to that single, shared plumbing box.
- Value Numbering: The inventory manager stamps that plumbing assembly with ID
#42. Whenever any worker asks to construct that exact plumbing assembly, the manager says: "That is already component #42. Do not build it again; reuse the existing unit!"
💻 Bridging to Computer Science: Value Numbering & Common Subexpression Elimination
In intermediate code optimization, compilers convert expression trees into Directed Acyclic Graphs (DAGs):
- In an AST, every subexpression forms an independent tree node, causing identical subexpressions (e.g. ) to be duplicated in memory.
- In a DAG, nodes representing identical computations are deduplicated and shared: internal nodes can have multiple parent edges (in-degree ).
- Value Numbering is the linear-time algorithm that maintains a hash table of
(op, left_child, right_child)tuples, mapping identical computations to a single canonical node ID.
🏗️ Architectural Foundations: Parse Tree vs AST vs DAG
Parse Tree vs Syntax Tree (AST) vs Directed Acyclic Graph (DAG)
The progressive compression of syntactic representation into canonical computation graph
Abstract Syntax Tree (AST)
- •Interior nodes are operators; leaves are operands.
- •Stripped of grammatical non-terminals and syntactic punctuation.
- •Every subexpression appears as an independent, isolated subtree.
- •Duplicate computations like (a + b) appear multiple times.
Directed Acyclic Graph (DAG)
- •Identical common subexpressions are shared with multiple parent edges.
- •Interior nodes have in-degree >= 1 (multiple parents).
- •Automatically exposes dead code and redundant memory loads.
- •Forms the theoretical foundation for local code generation.
📐 Formal Foundations: DAG Construction & Node Invariants
A Directed Acyclic Graph (DAG) for an expression is a directed graph with no directed cycles such that:
- Leaves: Represent unique variable names or literal constants.
- Interior Nodes: Represent operator applications.
- Sharing: A node has multiple parent edges if the subexpression rooted at is used as an operand in multiple operations.
🏗️ The DAG Construction Algorithm (Hash Table Value Numbering)
To construct a DAG in linear time , the compiler maintains a hash table of existing nodes:
The Value-Numbered DAG Construction Pipeline
Checking existing computation nodes before allocating new records
Query Node Hash Table
Hash signature: key = (op, left_child_id, right_child_id).
Deduplication Branch
If found: return existing node ID. If absent: allocate new node record.
Update Variable Association
Attach destination variable label to the resulting node ID.
Formal Algorithm: MakeNode(op, left, right)
- Compute hash key .
- If an entry exists in the node table:
- Return pointer to existing node .
- Else:
- Create a new node with operator , left pointer , and right pointer .
- Insert into the hash table under key .
- Return .
🧱 DAG Construction for a Basic Block
In a basic block, variables can be assigned multiple times. When variable is updated ():
- The compiler evaluates the right-hand side using current node pointers for and .
- It obtains node .
- It attaches label to node .
- Crucial Rule: The label is removed from any previous node it was attached to, because that old value is now killed!
Basic Block DAG Construction Trace: Handling Variable Reassignment
Tracing node creation, operator evaluation, and label killing when variable 'a' is reassigned
Initial Leaf Registration
Statement 1: a = b + c
Statement 2: a = a * d (Kills Old 'a')
Label Migration & Historical Preservation
🎯 What a Basic Block DAG Reveals:
- Common Subexpressions: Any internal node with in-degree represents a subexpression computed once and reused multiple times.
- Dead Code Elimination: Any internal node that has no parents and whose attached variable labels are not live at the exit of the basic block can be safely eliminated.
- Variables Used Before Definition: Any leaf node labeled with an initial variable name represents an input variable whose value originated outside the basic block.
🏭 Real-World Production Context: How Industry Builds This
In industrial production compilers (such as LLVM and GCC):
Production compilers expand basic block DAG construction into function-wide optimization:
- Global Value Numbering (GVN) operates directly over SSA form and dominator trees.
- Because each SSA variable is assigned exactly once, value numbers are globally valid across all basic blocks dominated by the definition.
- LLVM's
GVNPassdetects common subexpressions across control branches, eliminates redundant heap loads, and hoists loop-invariant expressions.
Compilers for functional languages (OCaml, Haskell) use hash-consing to guarantee that structurally identical syntax subtrees share the exact same physical heap address, slashing compiler memory footprints from gigabytes to megabytes.
📐 Numerical Applications & Worked Examples
Example 1: Step-by-Step Basic Block DAG Construction
Construct the minimal DAG for the following sequence of three-address statements in a basic block:
Determine:
- Total number of nodes (leaves + internal operators) in the final DAG.
- Total number of directed edges.
- The optimized three-address code reconstructed from the DAG.
Step-by-Step Construction:
- Statement 1:
- Allocate leaf nodes: , .
- Internal operator: . Attach label: .
- Statement 2:
- Internal operator: . Attach label: .
- Statement 3:
- Internal operator: . Attach label: .
- Statement 4:
- Query table for . Found existing node !
- Attach label: to node . (Common Subexpression eliminated!)
- Statement 5:
- Operands: () and ().
- Internal operator: . Attach label: .
- Statement 6:
- Attach label to node .
- ⚠️ Killed Variable: Variable now points to node . Leaf remains as (initial value of ).
- Statement 7:
- Operands: Current node for is . Current node for is .
- Signature: .
- This is a new distinct operation ( is not the old ).
- Allocate internal operator: . Attach label: .
Final DAG Node & Edge Counts:
- Leaf Nodes (2):
- (initial value of )
- Internal Operator Nodes (5): 3. with labels 4. with label 5. with label 6. with labels 7. with label
- Total Nodes: .
- Directed Edges (10):
- (2 edges)
- (2 edges)
- (2 edges)
- (2 edges)
- (2 edges)
- Total = .
Reconstructed Optimized Three-Address Code:
Original instructions: 7. Optimized instructions: 5. Redundant instructions eradicated: 2.
Example 2: Minimal DAG for a Compound Arithmetic Expression
Find the minimum number of nodes and edges in the DAG for the arithmetic expression:
Step-by-Step Analytical Derivation:
- Identify Subexpressions:
- Graph Construction:
- Leaf nodes: (3 leaf nodes).
- Node 4: [Computes ]
- Node 5: [Computes ]
- Node 6: [Computes , both children point to Node 5!]
- Node 7: [Computes , reuses Node 4!]
- Counts:
- Total Nodes: .
- Total Edges:
- Node 4: 2 edges (to )
- Node 5: 2 edges (to , Node 4)
- Node 6: 2 edges (both to Node 5)
- Node 7: 2 edges (to Node 6, Node 4)
- Total edges = .
🎯 Exam & Interview Pitfall Check
Question 1: "How does a compiler know if an expression in a DAG is a common subexpression?"
Key Focus Points:
- An internal node in a DAG represents a common subexpression if and only if its in-degree is greater than 1 (it has two or more incoming parent edges).
- This indicates that the value computed by this node is consumed as an input by multiple downstream operations.
Question 2: "What happens when a variable on the left-hand side of an assignment is reassigned in the same basic block?"
Key Focus Points:
- Its variable label is migrated to the new computation node.
- It is removed from the previous node. Any subsequent operations referencing that variable will link to the new node, while existing parent edges continue to point to the historical node.
Trap 1: Assuming commutative operations match automatically in a value numbering table.
Answer: Implementation pitfall. In a naive hash table, key does not equal key . Production compilers canonicalize commutative operator operands (e.g. enforcing ) before querying the value table.
Trap 2: Ignoring memory aliasing when optimizing array loads in a DAG.
Answer: Silent corruption danger. If an array store occurs between two loads and , the compiler CANNOT eliminate the second load unless alias analysis proves . If , the store mutates the value, invalidating the previous load node.