Attribute Grammars and Syntax-Directed Definitions
Understand how compilers attach semantic meaning to grammar rules. Learn to evaluate synthesized and inherited attributes, differentiate between S-Attributed and L-Attributed SDDs, and trace dependency graphs.
Learning Goals
- Define Syntax-Directed Definitions (SDDs) as a CFG paired with semantic rules, and Syntax-Directed Translation Schemes (SDTs) as implementations with embedded actions.
- Differentiate between synthesized attributes and inherited attributes with concrete examples (type computation, expression evaluation).
- Differentiate between S-Attributed and L-Attributed definitions, prove that S-Attributed is a subset of L-Attributed, and understand parser compatibility (A highly tested exam topic).
- Construct an annotated parse tree step-by-step and evaluate attribute values at each node.
- Construct a dependency graph on a syntax tree, perform topological sort, and detect cyclic dependencies.
- Design an SDT for mathematical expressions and trace its execution to produce output.
- Trace the execution of embedded semantic action symbols (e.g., print statements) during bottom-up parsing to predict output sequences.
- Apply SDT action placement rules: synthesized attributes at end of production, inherited attributes in the middle.
Syntax-Directed Definitions (SDD) & Translation (SDT)
Syntax-Directed Translation is the process of attaching semantic rules to the grammatical productions of a Context-Free Grammar. This allows the compiler to evaluate expressions, check types, or generate intermediate code exactly at the moment a syntax rule is applied.
Formal Definition of SDD: An SDD is a Context-Free Grammar together with semantic rules associated with each production. For every production , the SDD associates a set of semantic rules of the form where is an attribute of a grammar symbol in the production and is a function on attributes of grammar symbols in the production.
SDD vs SDT — Specification vs Implementation:
- An SDD is a specification — it tells you what value each attribute should have, but not when or how to compute it during parsing.
- An SDT is an implementation — it embeds semantic actions (program fragments) within the right side of productions, telling the parser exactly when to execute each action.
Why is SDT Important? SDT bridges the gap between purely structural parsing and actual code meaning. It is crucial because it allows the compiler to perform Semantic Analysis (type checking), evaluate mathematical expressions, and generate Intermediate Code all simultaneously while the parser is building the syntax tree, making the compilation process highly efficient.
An SDD is a Context-Free Grammar paired with semantic rules. Each rule specifies an attribute's value as a function of other attributes, but does not dictate when the rule is evaluated.
Example SDD:
| Production | Semantic Rule |
|---|---|
The rules are declarative — they state relationships between attributes, not execution order.
SDT Action Placement Rules
When constructing an SDT from an SDD, follow these placement rules:
- Synthesized attributes: Place the action at the end of the production (after all grammar symbols have been recognized).
- Inherited attributes: Place the action just before the grammar symbol that receives the inherited value (in the middle of the production).
- For L-Attributed SDDs: These placement rules guarantee that all needed attributes are available when the action executes during a left-to-right parse.
Attributes
There are two fundamental types of attributes:
- Synthesized Attributes: The value of the attribute at a node is computed from the values of attributes at its children. Information flows bottom-up.
- Inherited Attributes: The value of the attribute at a node is computed from the attributes of its parent and/or its siblings. Information flows top-down or left-to-right.
Concrete Examples:
- Expression Evaluation (Synthesized): In , the attribute is synthesized — it is computed from children and .
- Type Computation (Inherited): In , the attribute is inherited — receives its type from its sibling/parent . This allows the type declared at the beginning of a declaration to propagate to each identifier in the list.
Comparison: Synthesized vs Inherited Attributes
| Feature | Synthesized Attribute | Inherited Attribute |
|---|---|---|
| Value Source | Children nodes | Parent and/or left siblings |
| Information Flow | Bottom-up | Top-down / left-to-right |
| Evaluation During | Bottom-up (LR) parsing | Top-down (LL) parsing |
| Typical Use | Expression values, type of expressions | Type declarations, symbol table info |
| Example | ||
| Start Symbol | Can have synthesized attributes on start symbol | Cannot have inherited attributes on start symbol (no parent) |
| S-Attributed SDD | Allowed (only these are used) | Forbidden |
| L-Attributed SDD | Allowed | Allowed (with left-sibling restriction) |
S-Attributed vs L-Attributed Definitions
Exam Tip — Most Tested Topic
"The distinction between S-Attributed and L-Attributed SDDs is the #1 most frequently tested question in Module 3. You will almost certainly be asked to identify whether a given SDD is S-Attributed, L-Attributed, or neither. Remember: S-Attributed ⊂ L-Attributed, so if an SDD is S-Attributed, it is automatically L-Attributed too."
| Feature | S-Attributed SDD | L-Attributed SDD |
|---|---|---|
| Attribute Types Allowed | Uses ONLY Synthesized attributes. | Can use Both Synthesized and Inherited attributes. |
| Inheritance Restriction | N/A (Inherited attributes are forbidden). | Inherited attributes can only depend on parents or left siblings (never right siblings). |
| Evaluation Order | Strictly Bottom-Up. | Left-to-Right, Depth-First (Top-Down or Top-Down with Bottom-Up evaluation). |
| Parsing Compatibility | Easily evaluated during Bottom-Up parsing (e.g., LR parsers). | Easily evaluated during Top-Down parsing (e.g., LL parsers). Also works with LR parsers if no inherited attrs. |
| Subset Relationship | S-Attributed is a strict subset of L-Attributed. | L-Attributed is a superset of S-Attributed. |
Proof that S-Attributed ⊂ L-Attributed: An S-Attributed SDD uses only synthesized attributes. Synthesized attributes depend only on children — they never depend on right siblings or parents. Therefore, they trivially satisfy the L-Attributed restriction (which only restricts inherited attributes to depend on parent/left siblings). Since S-Attributed SDDs have no inherited attributes at all, they automatically satisfy all L-Attributed constraints. Hence, every S-Attributed SDD is also L-Attributed.
Suitable Examples:
- S-Attributed Example: (Notice is calculated entirely from its children and . No inherited attributes exist.)
- L-Attributed Example: (Notice inherits a value from its left sibling . This is strictly allowed in L-Attributed, but forbidden in S-Attributed).
- Neither Example: (Notice inherits from its right sibling . This violates the L-Attributed restriction — inherited attributes may NOT depend on right siblings.)
Constructing an Annotated Parse Tree for $3 * 5 + 4$
- 1Step 1
Grammar:
SDD (all synthesized attributes):
- :
- :
- :
- :
- :
- 2Step 2
Parse tree for input
3 * 5 + 4: - 3Step 3
Assign from the lexval of each number:
- (from
3) - (from
5) - (from
4)
- (from
- 4Step 4
Evaluate nodes from their children:
- (rule )
- (rule )
- (rule )
- 5Step 5
Evaluate nodes from their children:
- (rule )
- (rule )
Final Result:
- 6Step 6
The evaluation proceeds in post-order (bottom-up, left-to-right):
- → → → → (left subtree complete)
- → (right subtree base)
- (root)
This is the natural order for bottom-up (LR) parsing and works perfectly for S-Attributed SDDs.
Annotated Parse Trees (Infix to Prefix)
An annotated parse tree is a regular parse tree where every node is annotated with the final, computed values of its attributes.
Example: Infix to Prefix Translation
Consider a grammar that translates into Prefix notation (e.g., translating 3 + 4 into + 3 4).
Semantic Rule: E.val = "+" || E1.val || T.val
If the input is A + B, the annotated parse tree evaluating bottom-up looks like this:
Constructing a Dependency Graph
- 1Step 1
Consider a simplified grammar for expression evaluation with type checking:
Production Semantic Rules ; ; ; Input:
5 + 3 - 2Step 2
- 3Step 3
For each grammar symbol node, create attribute nodes:
- ,
- ,
- ,
- ,
- 4Step 4
Based on semantic rules, draw edges from source to dependent attribute:
- 5Step 5
Topological sort yields a valid evaluation order (no attribute is evaluated before its dependencies):
- , (base — no incoming edges)
- , (base — no incoming edges)
- , (depend only on )
- , (depend on and )
This order is valid — the graph has no cycles.
- 6Step 6
If a dependency graph contains a cycle, topological sort is impossible and the SDD is invalid — no evaluation order exists.
Example of a cyclic (invalid) SDD:
Here depends on , and depends on — a cycle! This SDD cannot be evaluated.
Dependency Graphs & Evaluation Order
A Dependency Graph is a directed graph that shows the interdependencies between attributes in a parse tree. It is specifically used to determine the exact evaluation order of attributes.
How to construct and use it:
- Draw the parse tree.
- For every node, draw a point representing its attributes.
- If an attribute depends on an attribute to be calculated, draw a directed edge from to .
- Evaluation Order: The compiler performs a Topological Sort on this graph. A topological sort guarantees that an attribute is evaluated only after all the attributes it depends on have been evaluated. (Note: If the dependency graph contains a cycle, the attributes cannot be evaluated, and the SDD is invalid).
Example of a Dependency Graph (Topological Order): For the rule , evaluating types:
Semantic Action Tracing
- 1Step 1
Examiners frequently ask you to trace the output of a grammar with embedded semantic actions (like
{print()}). You must simulate a bottom-up parser (or post-order traversal) and determine the exact sequence of printed output. - 2Step 2
Consider the rules:
-
Input string:
aa
- 3Step 3
For input
aa, the tree is: - 4Step 4
We evaluate the actions as the nodes are reduced (from bottom-left to top-right):
- Reduce the first
ato . Action: print(3). - Reduce the second
ato . Action: print(3). - Reduce the second to . Action: print(2).
- Reduce the first and the second to the root . Action: print(1).
Final Printed Output:
3321
- Reduce the first
Second Tracing Example — Expression with Infix to Postfix
- 1Step 1
Consider an SDT that converts infix expressions to postfix:
Production Semantic Action Input:
5 + 3 - 2Step 2
- 3Step 3
Actions execute during reductions:
- Reduce
5to : print(5) - Reduce to : (no additional print, just propagation)
- Reduce
3to : print(3) - Reduce to root : **print("+")
Final Output:
5 3 +(postfix notation) - Reduce
Why SDT is Preferred Over SDD in Practice
In real compiler implementations, SDTs are preferred because they embed actions at specific positions in productions, allowing the parser to execute semantic actions on-the-fly during parsing. This eliminates the need to build a separate dependency graph and perform topological sort — the parser's natural traversal order guarantees correct evaluation for well-designed SDTs. SDDs, being declarative, require post-processing to determine evaluation order.
Common Questions on SDDs and SDTs
Knowledge Check
Which type of SDD allows ONLY synthesized attributes? [PYQ 2022]