Section 7 · 8 min read
Syntax-directed translation
Synthesized and inherited attributes, S-attributed and L-attributed definitions, dependency graphs, and how to evaluate translation schemes the way GATE asks.
On this page
The idea
Syntax-directed translation attaches meaning to a grammar. Each grammar symbol gets attributes (a value, a type, a piece of code, a pointer to a tree node), and each production gets rules that compute those attributes. The parser drives the computation: whenever it uses a production, the attached rules run.
Two forms are used:
- A syntax-directed definition (SDD) gives semantic rules with no stated order of evaluation. Example: .
- A syntax-directed translation scheme (SDT) embeds program fragments, called semantic actions, at specific positions inside the right side. Example: . The position of the action fixes when it runs.
Synthesized and inherited attributes
At a parse tree node labelled with nonterminal :
- A synthesized attribute of is computed from attributes of 's children and of itself. Information flows upward. Terminals have only synthesized attributes, supplied by the scanner (for example
num.lexval). - An inherited attribute of is computed from attributes of 's parent, of 's siblings, and of itself. Information flows downward or sideways.
Typical uses:
| Purpose | Attribute kind |
|---|---|
| Value of an expression | synthesized |
| Building a syntax tree | synthesized |
| Type passed down a declaration list | inherited |
| Left operand passed into a tail after left-recursion removal | inherited |
Dependency graphs and evaluation order
Draw one node per attribute instance in the parse tree. Put an edge from attribute to attribute when the rule for uses . Any topological order of this graph is a valid evaluation order. If the graph has a cycle, no order exists and the SDD cannot be evaluated for that tree. Testing whether an arbitrary SDD can ever produce a cycle is computationally expensive (exponential in general), which is why compilers restrict themselves to the two classes below.
S-attributed definitions
An SDD is S-attributed if it uses only synthesized attributes.
- Evaluate it with any bottom-up order, such as a postorder traversal of the tree.
- It fits LR parsing perfectly: when the parser reduces by , all attributes are on the stack, so 's attributes can be computed during the reduction.
- In an SDT form, every action sits at the right end of its production. Such an SDT is called postfix.
L-attributed definitions
An SDD is L-attributed if every attribute is either synthesized, or inherited with this restriction: in a production , an inherited attribute of may depend only on
- inherited attributes of the head , and
- attributes (inherited or synthesized) of , the symbols to the left of , and
- attributes of itself, as long as no cycle is formed.
It may not use synthesized attributes of or attributes of .
Facts:
- Every S-attributed SDD is L-attributed. The converse is false.
- An L-attributed SDD can be evaluated in one depth-first, left-to-right traversal of the parse tree.
- L-attributed SDDs on an LL(1) grammar can be evaluated during top-down parsing. For a bottom-up parser, inherited attributes are handled with marker nonterminals that derive .
Where actions run in a translation scheme
For an SDT, imagine each action as an extra leaf in the parse tree at the position where it appears. Then do a preorder walk from left to right: every action runs when the walk reaches its leaf.
During a bottom-up parse, an action at the end of a production runs at the moment of the reduction. This gives a simple procedure for "what is printed" questions:
- Build the parse tree for the input (respecting precedence and associativity from the grammar).
- List the reductions in the order an LR parser makes them, which is a postorder of the tree.
- Read off the action of each reduction.
An action placed in the middle of a production, like , runs after everything under is done and before anything under starts.
Grammar shapes that decide precedence
When the SDT rules give operators unusual meanings, the grammar alone decides grouping:
- An operator introduced at a lower level of the grammar (closer to the terminals) has higher precedence.
- A left-recursive rule like makes left associative. A right-recursive rule like makes it right associative.
Never apply ordinary arithmetic precedence. Draw the tree from the grammar.
Worked examples
Worked example 1: Value with unusual operators
E -> E # T { E.v = E1.v * T.v }
E -> T { E.v = T.v }
T -> T & F { T.v = T1.v - F.v }
T -> F { T.v = F.v }
F -> num { F.v = num.lexval }
Evaluate 6 & 2 # 3 # 5 & 4.
Since is generated lower in the grammar, it binds tighter than . Both are left associative.
- -groups:
6 & 2gives ;3gives ;5 & 4gives . - is left associative over : .
Answer: 12. A left-to-right evaluation ignoring the grammar would give , a wrong distractor.
Worked example 2: What gets printed
S -> X Y { print("s") }
X -> p X { print("1") }
X -> q { print("2") }
Y -> Y r { print("3") }
Y -> t { print("4") }
Input: p q t r r.
Bottom-up reductions in order:
qreduced to : prints 2.p Xreduced to : prints 1. The whole subtree is now done.treduced to : prints 4.Y rreduced to : prints 3.Y rreduced to : prints 3.X Yreduced to : prints s.
Output: 21433s. Note that is right recursive, so the innermost (built from q) is reduced first, while is left recursive, so the leftmost part (t) is reduced first.
Worked example 3: Checking L-attributed
For the production , which rules keep the SDD L-attributed?
| Rule | L-attributed? | Why |
|---|---|---|
| Yes | Uses head inherited and left sibling | |
| Yes | Both are left siblings | |
| Yes | Synthesized rules are always allowed | |
| No | is to the right of | |
| No | Synthesized attribute of the head |
Worked example 4: Inherited type in a declaration
D -> T L { L.in = T.type }
T -> int { T.type = integer }
T -> float { T.type = float }
L -> L1 , id { L1.in = L.in; addtype(id.entry, L.in) }
L -> id { addtype(id.entry, L.in) }
For float a, b, c the type is computed once at , passed into as an inherited attribute, and then passed down the left spine of the chain. addtype runs three times, once for each identifier. The definition is L-attributed: depends on its left sibling , and depends on its parent's inherited attribute. It is not S-attributed because is inherited.
Worked example 5: Counting with attributes
N -> N D { N.v = 3 * N1.v + D.v }
N -> D { N.v = D.v }
D -> 0 | 1 | 2 { D.v = digit value }
This reads a base 3 numeral. For 1202: . Answer 47. If the rule were written with right recursion and the same formula, the meaning would change, because the first digit would then be combined last.
Common traps
- Using normal arithmetic precedence instead of the grammar's structure.
- Thinking an action at the right end of a right-recursive rule runs from left to right in the input. For the innermost (rightmost) reduces first.
- Calling an SDD L-attributed when an inherited attribute uses a sibling to the right.
- Believing every L-attributed SDD is S-attributed. It is the other way round.
- Forgetting that terminals cannot have inherited attributes; their values come from the scanner.
- Assuming the dependency graph is always acyclic. If a cycle exists, there is no valid order.
Quick revision
- Synthesized: from children. Inherited: from parent and siblings.
- S-attributed: only synthesized; bottom-up evaluation; natural for LR parsers; actions at the right end.
- L-attributed: inherited attributes depend only on the head's inherited attributes and left siblings; one left-to-right depth-first pass.
- S-attributed L-attributed.
- Any topological order of the dependency graph is a valid evaluation order; a cycle means no order.
- Print questions: build the tree from the grammar, list reductions in postorder, read off actions.
- Lower in the grammar means higher precedence; left recursion means left associativity.