3.1 Attribute grammars
3.1.2 Forwarding
Forwarding [21] was introduced into attribute grammars to make it possible to com- pose language extensions, without running afoul of what is essentially the expression problem. A language extension that introduces new productions, combined with a separate extension that introduces new attributes on existing nonterminals, presents a serious problem: the new attribute may not have a defining equation on the new production. If these were introduced in independent extensions that were unaware of each other, then the composed AG would be incomplete.
Forwarding solves this problem by allowing extension productions to construct a semantically equivalent tree in the host language (that is, using only productions from the language they are extending). To accomplish this, we introduce a new kind of equation (besides defining synthesized or inherited attributes) that indicates what that production “forwards to.” We call a production that has a forwarding equation a “forwarding production.” When a synthesized attribute is requested from a forwarding production that does not have an explicit corresponding equation, the attribute can simply be evaluated on forwarded-to tree instead. For inherited attributes, we do this same automatic copy in the other direction. The forwarded-to tree is given a copy of the inherited attribute the forwarding node was given. As a result, we can generally be unconcerned about missing equations on forwarding productions, as we can obtain
3.1. Attribute grammars
a value via forwarding.
Although this behavior for forwarding appears at first to be syntactic sugar, the implicit copy equations forwarding would generate cannot be computed locally. That is, the implicit equations are a part of the final composed attribute grammar, the result of an extremely simple but whole-program analysis. This is why forwarding is able to help solve the expression problem, where is seems like syntactic sugar should not be able to: it is non-modular sugar. When one extension introduces a new (for- warding) production, and another extension introduces a new synthesized attribute, we need to generate the appropriate copy equation, but this equation cannot be placed in either extension if they are independent of each other. It must be generated (au- tomatically, as one kind of “glue code”) as a result of composition of these separate modules.
Forwarding is the primary reason we are interested in attribute grammars. Al- though it seems similar to macros at first glance, there are important differences. For one, we are able to write equations on forwarding productions (an example of which we will see shortly). This means that we could get semantics from what we forward to, but, for instance, give a value for the errors attribute specific to the forwarding production (instead of what we would have gotten from its expansion). Moreover, forwarding nodes in a tree are persistent: they are not implemented by being rewrit- ten away, as macros are. This is a critical piece of how we ensure error messages can be about the code the user wrote, and not the internal implementation details of what it expands into.
In figure 3.5, we show a simple (abstract) grammar of boolean expressions. We can construct trees using and, or, not, and literal true or false values. From these tree, we can compute two attributes: eval gives us a boolean value that is the
3.1. Attribute grammars
nonterminal Expr with eval, neg;
synthesized attribute eval :: Boolean; synthesized attribute neg :: Expr; production and
e::Expr ::= l::Expr r::Expr { e.eval = l.eval && r.eval;
e.neg = or(l.neg, r.neg); }
production or
e::Expr ::= l::Expr r::Expr { e.eval = l.eval || r.eval;
e.neg = and(l.neg, r.neg); } production not e::Expr ::= s::Expr { e.eval = !s.eval; e.neg = s; } production literal e::Expr ::= b::Boolean { e.eval = b; e.neg = literal(!b); } production implies
e::Expr ::= l::Expr r::Expr { forwards to or(not(l), r); }
production iff
e::Expr ::= l::Expr r::Expr { e.eval = l.eval == r.eval;
forwards to and(implies(l, r), implies(r, l)); }
Figure 3.5: An example grammar for boolean propositions, with two forwarding produc- tions: implies and iff.
result of evaluating the tree, and neg (negation). This latter attribute transforms the tree into one which would give an opposite value by removing nots, applying De Morgan’s laws (turning and nodes into or nodes with negated children, and vice versa), and swapping true/false literals. (These attributes are chosen semi-arbitrarily, as examples of things we might want to evaluate on a boolean expression tree.) To this little language we add two forwarding productions: implies and iff which correspond to their respective logical connectives.
There are three salient features to take note of. First, implies gives no equations for itself other than the forwarding equation. Next, iff does choose to give an equation for eval but not for neg. Finally, the forwarding equation for iff makes
3.1. Attribute grammars implies T F or not F T forwards eval: T eval: F eval: F eval: F eval: F iff T not F and implies implies T not F T not F ... ... forwards eval: T eval: F eval: F eval: F
Figure 3.6: Example of two forwarding trees and the evaluation of the eval attribute.
use of implies. While we conceptualized forwarding as constructing an equivalent tree purely the host language, as long as the expansion of forwarding terminates, that is still (eventually) the case. There are just more indirections before we get to the purely host language tree.
In figure 3.6, we show two example trees, and visually explain features of how attribute evaluation and forward expansion proceed on decorated trees. In the top example, we see the eval attribute evaluated on an implies tree. Notice how the forwarded-to tree has eval computed on it, but also take note that we do not touch the children of implies at all. In the bottom example, with the iff production that gave an explicit equation, we see that eval is evaluated on its children, and the forwarded-to tree is not touched.
3.1. Attribute grammars
ularly the not(F) tree in the bottom example. We see it drawn three times, and in fact that is only because we have not drawn the next expansion of those implies productions, which would have given us a total of five instances. This is a necessary part of forwarding, as each of those locations in the tree can be given inherited at- tributes by its parent, and so in general each may have a different set of inherited attributes and may thus compute different values for synthesized attributes.
In the worst case, this can mean forwarding results in exponential tree expansion. However, in practice we often only see a constant factor expansion. Typically, an attribute equation is supplied up front by the original forwarding productions imme- diately, or it is only evaluated on the final tree of non-forwarding productions, which allows us to “skip” most of the intermediate forwarding nodes. In the top example, is would be as though the children of implies never exist on their own at all. And in the bottom example, we might never even compute the forward tree of the iff production at all, and even if we did, we’d likely never compute anything about the children of the two implies productions. However, it is possible for forwarding per- formance to become a concern if we need to access attributes from children in order to decide what to forward to, and then see those subtrees duplicated in what we for- ward to. (Consider, for example, forwarding based on the types of subexpressions.) However, there are ways of mitigating the problem in these cases by making use of “references,” the subject of the next section.