CD Notes
Comprehensive study of attribute grammars in compiler design: attribute definitions, evaluation schemes, dependency graphs, parsing integration, practical implementations for modern compilers with industry examples.
Fundamentals
An Attribute Grammar is a context-free grammar augmented with attributes (values associated with symbols) and semantic rules (specifying how to compute them). It bridges syntax and semantics.
Components
| 1. Context-Free Grammar | Defines syntax structure |
| 2. Attributes | Values attached to each symbol occurrence |
| 3. Semantic Rules | Formulas defining attribute computation |
| 4. Conditions/Predicates | Validity constraints on values |
Attribute Definitions
Attribute Classification
For each grammar symbol X
- Synthesized attributes: X.s depends on children
- Inherited attributes: X.i depends on parent/siblings
For each symbol occurrence in parse tree
- Attribute value computed via semantic rules
- Value persists for use by other rules
Example: Type System Attribute Grammar
Grammar
Program → Decl* Stmt*
Decl → id : T
Stmt → id := Expr
T → int | real
Expr → id | Expr + Expr
Attributes
id.name ← token value (synthesized)
T.type ← "int"|"real" (synthesized)
Decl.scope ← symbol table entry (side-effect)
Expr.type ← inferred type (synthesized)
Expr.error ← type compatibility (synthesized)
Semantic Rules
T → int {T.type = "int"}
T → real {T.type = "real"}
Decl → id : T {addToSymbolTable(id.name, T.type)}
Expr → id {Expr.type = getSymbolType(id.name)}
Expr → Expr₁ + Expr₂ {
if Expr₁.type == Expr₂.type:
Expr.type = Expr₁.type
Expr.error = false
else:
Expr.type = "error"
Expr.error = true
}
Dependency Analysis
Building Dependency Graphs
For each attribute value in parse tree
1. Create node in dependency graph
2. For each rule computing attribute a:
- Add node for a
- For each attribute b used in rule:
- Add edge from b to a (b must be computed before a)
Constraints
- Acyclic: No cycles allowed (no valid evaluation order)
- Connected: Single source determines all values
Topological Sorting
Example: Dependency in Type-Checking Grammar
Evaluation Schemes
Scheme 1: Top-Down (LL-Compatible)
Evaluation
1. Process node's inherited attributes (from parent/context)
2. Process children recursively (top-down)
3. Synthesized attributes remain for siblings/parents
Example
procedure evalE(E, inherited_type):
if E has children E₁ op E₂:
evalE₁(E₁, inherited_type)
evalOp(op)
evalE₂(E₂, inherited_type)
E.synthesized := compute_from_children()
Scheme 2: Bottom-Up (LR-Compatible)
Evaluation
1. Process children recursively (bottom-up)
2. Compute node's synthesized attributes
3. Return to parent context
Example
procedure evalE(E):
if E has children E₁ op E₂:
E₁.syn := evalE(E₁)
E₂.syn := evalE(E₂)
E.syn := compute_from_children(E₁.syn, E₂.syn)
return E.syn
Grammar Properties
Well-Defined Attribute Grammars
Circularity Detection
Algorithm
1. Build dependency graph for parse tree
2. Perform DFS from each node
3. If node reachable from itself: CYCLE EXISTS
Example
A.x := A.y + 1 ✗ Direct cycle
A.x := B.y; B.y := A.x ✗ Indirect cycle
A.x := B.y; B.y := 5 ✓ No cycle
Parsing Integration
LL Parser + Attribute Grammar
During predictive parsing
1. Initialize inherited attributes from parent
2. Process right side of production
3. Synthesized attributes available for use above
Implementation
procedure A(inh):
-- compute A's synthesized from inh
for each symbol in production:
if symbol is non-terminal B:
B's inherited := compute_from_A_and_processed_symbols
recurse: B(inherited)
else terminal:
match token
A.synthesized := f(children_values)
LR Parser + Attribute Grammar
| State Stack | [s₀, s₁, ..., sₙ] |
| Value Stack | [v₀, v₁, ..., vₙ] |
| Symbol Stack | [A₀, A₁, ..., Aₙ] |
| On reduce by A | X₁X₂...Xₖ: |
| Get values | v₁, v₂, ..., vₖ |
| Compute | new_value = f(v₁, v₂, ..., vₖ) |
Practical Implementation Patterns
Pattern: Multi-Pass Compilation
| Pass 1 | Build symbol table (inherited: scope) |
| Pass 2 | Type checking (inherited: expected type) |
| Pass 3 | Code generation (synthesized: code string) |
Pattern: Context-Aware Translation
| Program | globalDecl* func* |
| globalDecl | id : T {context.global_scope.add(id, T)} |
| func | id ( param* ) T body |
Error Handling via Attributes
Quick Revision Notes
- Attribute Grammar: CFG + attributes + semantic rules
- Synthesized: Computed from children (↑ upward flow)
- Inherited: Computed from parent/siblings (↓ downward flow)
- Dependency graph: Shows evaluation order constraints
- Acyclic: Required for well-defined semantics
- Evaluation strategies: Top-down (LL), bottom-up (LR), or multi-pass
- Integration: Attributes computed during/after parsing
Interview Q&A
Q1: What is the fundamental difference between a grammar and an attribute grammar?
A: A grammar specifies syntax structure (what strings are valid). An attribute grammar adds semantics (what those strings mean). Attributes and rules specify properties of valid parse trees and how to compute translations (code, types, intermediate representations).
Q2: How do you detect if an attribute grammar is ill-formed?
A: Check for: 1) Circular dependencies in the dependency graph, 2) Attributes assigned in multiple rules (not single-assignment), 3) Use before definition. Build the dependency graph for sample parse trees and verify it's acyclic.
Q3: Can you have both inherited and synthesized attributes in the same production?
A: Yes, and this is common. Inherited attributes bring context down; synthesized flow up. The key is ensuring no circular dependencies. L-attributed grammars restrict how inherited can depend on synthesized in the same production.
Q4: What happens if your attribute grammar has circular dependencies?
A: No valid evaluation order exists. The compiler cannot decide which attribute to compute first. Parsing cannot proceed. You must restructure the grammar to break the cycle or separate into multiple passes.
Q5: How do attribute grammars relate to compilers in practice?
A: Most compilers use attribute grammar concepts: symbol tables (attributes), type checking (semantic rules), code generation (attributes computed during parsing or afterward). Parser generators like YACC embed action code equivalent to semantic rules.
Q6: Why are attribute grammars more powerful than plain context-free grammars?
A: CFGs specify syntax only. Attribute grammars specify syntax and semantic constraints (type checking, scope rules, attribute consistency). They can express properties not captured by CFG alone, making them useful for full language specification.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Attribute Grammars: Foundation of Semantic Analysis and Code Generation.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Compiler Design topic.
Search Terms
compiler-design, compiler design, compiler, design, syntax, directed, translation, attribute
Related Compiler Design Topics