Introduction
A syntax-directed definition says what each attribute equals but leaves the order of evaluation to the dependency graph. A syntax-directed translation scheme (SDT) fixes the order: it is a context-free grammar with program fragments, called semantic actions, written in braces inside production bodies. An action runs at exactly the point where it appears, so an SDT is a recipe that a parser can follow while it parses.
SDTs are how translators are actually written: postfix converters, desk calculators, type checkers and intermediate-code generators. The working tasks are tracing a short SDT with print actions to find its output on an input string, and working out what an SDT with unusual actions computes.
Prerequisites
- Syntax-Directed Definitions: attributes, synthesized and inherited attributes, S-attributed and L-attributed definitions, dependency graphs.
- Parse trees and the order in which LL and LR parsers build them (see the notes Top-Down Parsing and LL(1) and Bottom-Up Parsing and Shift-Reduce).
Core concepts
- Semantic action. A fragment such as
{ print('+') }or{ E.val = E1.val + T.val }placed anywhere in a production body. In , action runs after everything derived from has been recognised and before anything derived from . - Execution order. Treat each action as an extra leaf of the parse tree, placed where it sits in the body. Then the actions run in the order of a left-to-right depth-first (preorder) traversal of that extended tree. This rule settles every "what does it print" question.
- Postfix SDT. Every action is at the right end of its production body. Actions then run exactly when the production is reduced, so a postfix SDT suits an LR parser directly.
- SDT for an L-attributed SDD. Place the action that computes an inherited attribute of a body symbol immediately before , and the action computing the head's synthesized attribute at the end of the body.
- Marker nonterminal. A fresh nonterminal with the single production , used to give a mid-body action a reduction of its own so that a bottom-up parser can run it.
How it works
Running an SDT on an input
To find the output, build the parse tree of the input using the grammar with the actions removed, put each action back as a leaf in its position, and walk the tree left to right, depth first, executing actions as they are met.
Example 1: infix to postfix. With printing the number:
On 2 + 3 * 4 the tree has at the root with under its right child. The walk prints 2, then descends into the right , printing 3, 4, *, and finally prints + at the end of the root body. The output is 2 3 4 * +. On 2 + 3 + 4 left recursion makes the left + deeper, and the output is 2 3 + 4 +. This is the same infix-to-postfix conversion that a stack-based algorithm performs, now driven by the grammar.
Example 2: actions in front give prefix. Move each operator's action to the front of its body:
The same walk on 2 + 3 * 4 now prints + 2 * 3 4, the prefix form. The parse tree is unchanged; only the action positions differ.
Example 3: mid-body actions. Consider
The input a a c b b has one parse tree: two nested nodes around . The walk meets the first a, prints 1, descends, meets the second a, prints 1, descends to c and prints 3, then returns past each b, printing 2 twice. The output is 1 1 3 2 2. If the actions are moved to , the output on the same input becomes 3 2 1 2 1.
Example 4: unusual semantics. An SDT may swap the meanings of operators. With the usual grammar but
the input 3 + 4 * 5 is still parsed as because the grammar, not the actions, fixes the structure. The inner * node computes and the root computes , while the ordinary value is 23. On 2 * 3 + 4 the tree is , giving . Always build the tree from the grammar first and then apply the actions as written.
Postfix SDTs on an LR parser stack
In a postfix SDT every action runs at a reduction. Keep the attribute of each grammar symbol in the same stack slot as the symbol (or its state). When the parser reduces by , the three topmost slots hold , + and , so the action becomes
and the result sits where the new is pushed. This works for every S-attributed SDD on an LR-parsable grammar: turn each rule into an action at the end of its production.
Actions inside productions and markers
A mid-body action, as in , cannot run at a reduction of because the reduction happens after . The standard fix replaces the action by a marker:
The language is unchanged and, for this grammar, the parser remains LALR(1): after a, the parser reduces on lookahead a or c and so runs at the right moment.
Markers can, however, create conflicts. The grammar is LR(0). Put a different action at the start of each alternative, and the markers give with and . In the start state the parser must choose between reducing and reducing on the same lookahead a, before it has seen whether b or c follows. That reduce-reduce conflict remains even for a canonical LR(1) parser. With two symbols of lookahead the conflict disappears (reduce on ab and on ac), so the grammar is LR(2). An action can be run early only if the parser knows by then which production it is in.
Eliminating left recursion from an SDT
For a top-down parser, left recursion must go, and the actions must move with it. For a postfix SDT
introduce with an inherited attribute (the value so far) and a synthesized attribute (the final value):
Example 5: subtraction stays left-associative. For , the transformed scheme is , . On 9 - 5 - 2, takes the values 9, then 4, then 2, and the production hands 2 back up: , the same as the left-recursive version. Simply reversing the recursion without the inherited attribute would compute , the classic wrong answer.
Reading the output from the order of reductions
Example 6: actions at the ends of bodies, nested input.
On a a b c b d the root is ; its is again over a b c, and its is over b d. The walk prints 2 (inner b), 3 (c), 1 (inner complete), 2 (the b in b d), 4 ( complete), 1 (root complete): output 2 3 1 2 4 1. Every action is at the end of its body, so this is a postfix SDT, and the output order is exactly the order of reductions in an LR parse: , , , , , .
Example 7: count the prints by counting reductions.
On x x x x y z z the derivation is . Bottom-up, the reductions are , , , , , so the output is 2 3 1 3 1.
Implementing L-attributed SDTs
- Recursive descent. Each nonterminal becomes a function; its inherited attributes are parameters and its synthesized attributes are return values. An action before a symbol runs before the call; an action at the end runs before the return.
- Table-driven LL parsing. Actions and attribute records are pushed on the parse stack along with grammar symbols; inherited attributes are filled in while the record is still below the symbol, synthesized ones are copied down when the symbol is popped.
- Bottom-up parsing. Every mid-body action becomes a marker; an inherited attribute of is then held in the marker's stack slot just below , where reductions inside can find it at a known offset.
- On-the-fly code generation. When the main attribute is a code string built only by concatenation in left-to-right order, the actions can emit the pieces directly instead of building strings; the result is the same sequence of instructions.
Important properties and observations
- The grammar fixes the tree; actions only decorate it. Precedence and associativity come from the grammar (Example 4), never from the actions.
- Position matters. The same grammar with actions moved from the end to the front changes postfix output into prefix output (Examples 1 and 2).
- Postfix SDTs are exactly what an LR parser runs for free. S-attributed SDD gives a postfix SDT gives actions at reductions.
- Every L-attributed SDD on an LL grammar can be run during LL or recursive-descent parsing, and, with markers, during LR parsing of a grammar that stays LR after the markers are added.
- Markers may break determinism. Adding productions keeps the language but can introduce conflicts that one symbol of lookahead cannot resolve: the example is not LR(1), even canonical, although it is LR(2).
- Left-recursion removal needs an inherited attribute to keep left associativity.
Common misconceptions.
- "Actions run in the order they are written in the grammar." They run in the order of the depth-first walk of the parse tree of the given input.
- "A postfix SDT prints postfix notation." Postfix refers to the position of the actions in the bodies; what is printed depends on the actions.
- "Any mid-rule action can be handled by a marker without harm." The marker can introduce shift-reduce or reduce-reduce conflicts.
- "The output of a postfix SDT follows the input from left to right." It follows the order of reductions: in Example 6 the print for the first
acomes out last, when the root production is finally reduced.
GATE examination perspective
- Print-trace questions. Draw the parse tree of the given string with the actions as extra leaves and read them in preorder. Check carefully which
printsits before and which after a nonterminal. - Evaluate with swapped or unusual semantics. Build the tree from the grammar's precedence and associativity, then apply the stated actions literally (Example 4).
- Identify the translation. An SDT that prints operators at the end of bodies yields postfix; at the front, prefix; between the operands, infix.
- Choose an implementation. Postfix SDT with an LR parser; L-attributed SDT with recursive descent or LL(1); mid-rule actions in LR via markers.
- Left-recursion removal with attributes. Know the / pattern and that it preserves left associativity.
- Postfix SDT shortcut. When every action is at the end of its body, list the reductions of a bottom-up parse (the rightmost derivation read backwards) and read off one action per reduction (Examples 6 and 7).
Summary
Sources
- A. V. Aho, M. S. Lam, R. Sethi, J. D. Ullman, Compilers, Principles, Techniques, and Tools, 2nd ed., Pearson Addison-Wesley, 2007: Section 2.3.5 (translation schemes), Section 5.4 (postfix translation schemes, parser-stack implementation of postfix SDTs, SDTs with actions inside productions, eliminating left recursion from SDTs, SDTs for L-attributed definitions), Section 5.5 (translation during recursive-descent parsing, on-the-fly code generation, L-attributed SDDs and LL parsing, bottom-up parsing of L-attributed SDDs).