int x, y, z; must travel
down and across to reach each identifier; a symbol's scope flows in from its
surroundings; a label's address must be known before the jump that targets it is generated. An
attribute grammar is the formal framework for all of this: a context-free grammar in
which every symbol carries attributes, and every production carries equations (semantic rules) defining
how those attributes relate. It is the theory that tells you which translations can be done in one
pass, which need several, and which cannot be done at all.
The whole subject turns on one distinction: the direction an attribute flows.
Each attribute of a grammar symbol is one of two kinds, decided by which production computes it.
A slogan captures it: values flow up, types flow down. Synthesized attributes summarise a subtree for its parent; inherited attributes carry context from the surroundings into a subtree. Terminals have only synthesized attributes (their lexical value), supplied by the scanner. The start symbol has no inherited attributes worth speaking of — there is nothing above it to supply them.
Consider the declaration real p, q with the SDD
The upward arrow (synthesized) and the downward-and-leftward arrows (inherited) are the entire story. This
mixture — a synthesized attribute feeding an inherited one, which feeds a computation deeper in the tree —
is exactly why some SDDs cannot be evaluated bottom-up: you cannot compute
There is no "left to right" law of attribute evaluation — only the dependency graph. Draw
a node for every attribute instance in the decorated parse tree, and an edge
Most real definitions fall into two well-behaved classes, and their whole appeal is that you can find a valid order without building the dependency graph at parse time:
The elegance of an L-attributed SDD is that its evaluation order is exactly a depth-first
traversal: at each node, compute inherited attributes on the way down (before recursing into a
child) and synthesized ones on the way up (after the children return). The recursion order and the
legal attribute order coincide, so a single traversal suffices. Here we stamp a declared type onto a list
of names — inh down and reading nothing that
is not already to the left.
Notice you could not reorder this into a pure bottom-up pass: evalL needs
Ttype from its parent before it can touch a single name. That downward dependency is the
signature of an inherited attribute, and it is precisely what a one-pass bottom-up (S-attributed) evaluator
cannot express — you would have to buffer the names and patch them afterwards, i.e. take a second pass.
An LR/yacc value stack naturally supports only synthesized attributes — everything flows up on reduce. Yet
yacc programmers routinely pass a declared type into a list of names. The trick is that the needed value is
already sitting lower on the parse stack (it was reduced earlier, to the left), and yacc lets an
action reach down the stack to read it via the $0, $-1 pseudo-variables. This
works only for L-attributed definitions, because only a strictly-left dependency is guaranteed to
be present-and-below on the stack at the moment you need it. Reach for a value that is to the
right, and it simply has not been parsed yet — there is nothing on the stack to read. The stack
discipline enforces L-attributedness whether you meant it to or not.
Two failures haunt attribute grammars. The first: a circular dependency. If attribute
The second, subtler trap: assuming every attribute can be computed in a single bottom-up pass. Synthesized attributes can. Inherited attributes cannot — an inherited attribute of a node depends on its parent and left siblings, which a post-order (children-before-parents) walk has not fully processed in the way you need. If your translation needs information to flow downward, either use an L-attributed evaluator (a depth-first walk that sets inherited attributes on the way down), or make two passes. Trying to force it bottom-up produces attributes read before they are written.