Coursify

Compiler Design (CD)

Attribute Grammars and Syntax-Directed Definitions

45 mins

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 AαA \to \alpha, the SDD associates a set of semantic rules of the form b=f(c1,c2,,ck)b = f(c_1, c_2, \dots, c_k) where bb is an attribute of a grammar symbol in the production and ff is a function on attributes c1,,ckc_1, \dots, c_k 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:

ProductionSemantic Rule
EE1+TE \to E_1 + TE.val=E1.val+T.valE.val = E_1.val + T.val
ETE \to TE.val=T.valE.val = T.val
TT1FT \to T_1 * FT.val=T1.valF.valT.val = T_1.val * F.val
TFT \to FT.val=F.valT.val = F.val
FnumF \to \text{num}F.val=num.lexvalF.val = \text{num.lexval}

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:

  1. Synthesized attributes: Place the action at the end of the production (after all grammar symbols have been recognized).
  2. Inherited attributes: Place the action just before the grammar symbol that receives the inherited value (in the middle of the production).
  3. 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:

  1. Synthesized Attributes: The value of the attribute at a node is computed from the values of attributes at its children. Information flows bottom-up.
  2. 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 EE1+TE \to E_1 + T, the attribute E.val=E1.val+T.valE.val = E_1.val + T.val is synthesized — it is computed from children E1E_1 and TT.
  • Type Computation (Inherited): In DTLD \to T L, the attribute L.type=T.typeL.type = T.type is inherited — LL receives its type from its sibling/parent TT. This allows the type declared at the beginning of a declaration to propagate to each identifier in the list.

Comparison: Synthesized vs Inherited Attributes

FeatureSynthesized AttributeInherited Attribute
Value SourceChildren nodesParent and/or left siblings
Information FlowBottom-upTop-down / left-to-right
Evaluation DuringBottom-up (LR) parsingTop-down (LL) parsing
Typical UseExpression values, type of expressionsType declarations, symbol table info
ExampleE.val=E1.val+T.valE.val = E_1.val + T.valL.type=T.typeL.type = T.type
Start SymbolCan have synthesized attributes on start symbolCannot have inherited attributes on start symbol (no parent)
S-Attributed SDDAllowed (only these are used)Forbidden
L-Attributed SDDAllowedAllowed (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."


FeatureS-Attributed SDDL-Attributed SDD
Attribute Types AllowedUses ONLY Synthesized attributes.Can use Both Synthesized and Inherited attributes.
Inheritance RestrictionN/A (Inherited attributes are forbidden).Inherited attributes can only depend on parents or left siblings (never right siblings).
Evaluation OrderStrictly Bottom-Up.Left-to-Right, Depth-First (Top-Down or Top-Down with Bottom-Up evaluation).
Parsing CompatibilityEasily 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 RelationshipS-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: EE1+T{E.val=E1.val+T.val}E \to E_1 + T \quad \{ E.val = E_1.val + T.val \} (Notice E.valE.val is calculated entirely from its children E1E_1 and TT. No inherited attributes exist.)
  • L-Attributed Example: ALM{M.inherited=L.synthesized}A \to L M \quad \{ M.inherited = L.synthesized \} (Notice MM inherits a value from its left sibling LL. This is strictly allowed in L-Attributed, but forbidden in S-Attributed).
  • Neither Example: ABC{B.inherited=C.synthesized}A \to B C \quad \{ B.inherited = C.synthesized \} (Notice BB inherits from its right sibling CC. This violates the L-Attributed restriction — inherited attributes may NOT depend on right siblings.)

Constructing an Annotated Parse Tree for $3 * 5 + 4$

  1. 1
    Step 1

    Grammar:

    • EE1+TE \to E_1 + T
    • ETE \to T
    • TT1FT \to T_1 * F
    • TFT \to F
    • FnumF \to \text{num}

    SDD (all synthesized attributes):

    • EE1+TE \to E_1 + T: E.val=E1.val+T.valE.val = E_1.val + T.val
    • ETE \to T: E.val=T.valE.val = T.val
    • TT1FT \to T_1 * F: T.val=T1.valF.valT.val = T_1.val * F.val
    • TFT \to F: T.val=F.valT.val = F.val
    • FnumF \to \text{num}: F.val=num.lexvalF.val = \text{num.lexval}
  2. 2
    Step 2

    Parse tree for input 3 * 5 + 4:

  3. 3
    Step 3

    Assign F.valF.val from the lexval of each number:

    • F1.val=3F_1.val = 3 (from 3)
    • F2.val=5F_2.val = 5 (from 5)
    • F3.val=4F_3.val = 4 (from 4)
  4. 4
    Step 4

    Evaluate TT nodes from their children:

    • T1a.val=F1.val=3T_{1a}.val = F_1.val = 3 (rule TFT \to F)
    • T1.val=T1a.valF2.val=35=15T_1.val = T_{1a}.val * F_2.val = 3 * 5 = 15 (rule TTFT \to T * F)
    • T2.val=F3.val=4T_2.val = F_3.val = 4 (rule TFT \to F)
  5. 5
    Step 5

    Evaluate EE nodes from their children:

    • E1.val=T1.val=15E_1.val = T_1.val = 15 (rule ETE \to T)
    • Eroot.val=E1.val+T2.val=15+4=19E_{root}.val = E_1.val + T_2.val = 15 + 4 = 19 (rule EE+TE \to E + T)

    Final Result: Eroot.val=19E_{root}.val = 19

  6. 6
    Step 6

    The evaluation proceeds in post-order (bottom-up, left-to-right):

    1. F1F_1T1aT_{1a}F2F_2T1T_1E1E_1 (left subtree complete)
    2. F3F_3T2T_2 (right subtree base)
    3. ErootE_{root} (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 EE1+TE \to E_1 + T 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

  1. 1
    Step 1

    Consider a simplified grammar for expression evaluation with type checking:

    ProductionSemantic Rules
    EE1+TE \to E_1 + TE.val=E1.val+T.valE.val = E_1.val + T.val ; E.type=max(E1.type,T.type)E.type = \text{max}(E_1.type, T.type)
    ETE \to TE.val=T.valE.val = T.val ; E.type=T.typeE.type = T.type
    TnumT \to \text{num}T.val=num.lexvalT.val = \text{num.lexval} ; T.type=integerT.type = \text{integer}

    Input: 5 + 3

  2. 2
    Step 2
  3. 3
    Step 3

    For each grammar symbol node, create attribute nodes:

    • T1.valT_1.val, T1.typeT_1.type
    • T2.valT_2.val, T2.typeT_2.type
    • E1.valE_1.val, E1.typeE_1.type
    • Eroot.valE_{root}.val, Eroot.typeE_{root}.type
  4. 4
    Step 4

    Based on semantic rules, draw edges from source to dependent attribute:

  5. 5
    Step 5

    Topological sort yields a valid evaluation order (no attribute is evaluated before its dependencies):

    1. T1.valT_1.val, T1.typeT_1.type (base — no incoming edges)
    2. T2.valT_2.val, T2.typeT_2.type (base — no incoming edges)
    3. E1.val=T1.valE_1.val = T_1.val, E1.type=T1.typeE_1.type = T_1.type (depend only on T1T_1)
    4. Eroot.val=E1.val+T2.valE_{root}.val = E_1.val + T_2.val, Eroot.type=max(E1.type,T2.type)E_{root}.type = \text{max}(E_1.type, T_2.type) (depend on E1E_1 and T2T_2)

    This order is valid — the graph has no cycles.

  6. 6
    Step 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: AB{B.inherited=A.synthesized}A \to B \quad \{ B.inherited = A.synthesized \} AB{A.synthesized=B.inherited}A \to B \quad \{ A.synthesized = B.inherited \}

    Here A.synthesizedA.synthesized depends on B.inheritedB.inherited, and B.inheritedB.inherited depends on A.synthesizedA.synthesized — 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:

  1. Draw the parse tree.
  2. For every node, draw a point representing its attributes.
  3. If an attribute XX depends on an attribute YY to be calculated, draw a directed edge from YY to XX.
  4. 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 EE1+TE \to E_1 + T, evaluating types:

Semantic Action Tracing

  1. 1
    Step 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.

  2. 2
    Step 2

    Consider the rules:

    1. SAS{print(1)}S \to A S \{ \text{print}(1) \}
    2. SA{print(2)}S \to A \{ \text{print}(2) \}
    3. Aa{print(3)}A \to a \{ \text{print}(3) \} Input string: aa
  3. 3
    Step 3

    For input aa, the tree is:

  4. 4
    Step 4

    We evaluate the actions as the nodes are reduced (from bottom-left to top-right):

    1. Reduce the first a to AA. Action: print(3).
    2. Reduce the second a to AA. Action: print(3).
    3. Reduce the second AA to SS. Action: print(2).
    4. Reduce the first AA and the second SS to the root SS. Action: print(1). Final Printed Output: 3321

Second Tracing Example — Expression with Infix to Postfix

  1. 1
    Step 1

    Consider an SDT that converts infix expressions to postfix:

    ProductionSemantic Action
    EE1+TE \to E_1 + T{print(E1.post)}+{print(T.post)}{print("+")}\{ \text{print}(E_1.post) \} + \{ \text{print}(T.post) \} \{ \text{print}(\text{"+"}) \}
    ETE \to T{print(T.post)}\{ \text{print}(T.post) \}
    TnumT \to \text{num}{print(num.lexval)}\{ \text{print}(\text{num.lexval}) \}

    Input: 5 + 3

  2. 2
    Step 2
  3. 3
    Step 3

    Actions execute during reductions:

    1. Reduce 5 to TT: print(5)
    2. Reduce TT to EE: (no additional print, just propagation)
    3. Reduce 3 to TT: print(3)
    4. Reduce E+TE + T to root EE: **print("+")

    Final Output: 5 3 + (postfix notation)

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

Question 1 of 7
Q1Single choice

Which type of SDD allows ONLY synthesized attributes? [PYQ 2022]