A .type attribute encodes data type. This grammar allows for both declaration and type inference.
dcl
int dlist dlist.t = int
real dlist dlist.t = real
dlist
ident ident.t := dlist.t
dlist , ident dlist1.t := dlist0.t
ident.t := dlist.t
assign
var := expr var.t = expr.t
var
ident var.t = ident.t
expr
const expr.t = const.t
sum expr.t = sum.t
sum
var sum.t = var.t
sum + var sum0.t = sum1.t = var.t
const
num const.t = int
num . num const.t = real
The relation may be calculated by a fixed-point iteration:
dcl
int dlist dlist.t := int
real dlist dlist.t := real
dlist
ident ident.t := dlist.t
dlist , ident dlist1.t := dlist0.t
ident.t := dlist.t
assign
var := expr var.t := expr.t
In a language that requires full declaration, types must
originate from declarations. Then at assignment we would merely check the relation among types,
but not propagate types:
assign
var := expr var.t == expr.t
The check might be made only
when both sides are defined. Undefined values left when the
iteration stops would be errors.
Often the direction of propagation is fully specified: every rule involves := or ==. It is usually required that the dependency graph implied by := rules be free of loops: each value gets set at most once during the fixed-point iteration.
sum
term sum.code := term.code
sum - term sum0.code := sum1.code ++ term.code ++ "-"
term
var term.code := var.code
term * var term0.code := term1.code ++ var.code ++ "*"
var
ident var.code := ident.name
Wiith a bit of cleverness we can even get a left-associative translation
out of a right-associative parse that was employed to avoid left recursion.
Code for a running total up to but not including the tail of an
expression like a-b-c-d is accumulated in an
inherited attribute .cum:
sum
term sum.code := term.code
term - tail tail.cum := term.code;
tail
term tail.code := tail.cum ++ term.code ++ "-"
term - tail tail1.cum := tail0.cum ++ term.code ++ "-"
The completed code for the sum
resides in tail.code at the end of
a chain of tail nodes. To propagate it back to sum
we add the rules shown in boldface:
sum
term sum.code := term.code
term - tail tail.cum := term.code
sum.code := tail.code;
tail
term tail.code := tail.cum ++ term.code ++ "-"
term - tail tail1.cum := tail0.cum ++ term.code ++ "-";
tail0.code := tail1.code
Attributes calculated at a child from its parent and siblings are said to be inherited. Inherited attributes with no dependency on siblings may be computed by a preorder tree walk.
Inheritance from a sibling can be reduced to inheritance from the parent by copying the sibling attribute to a dummy synthesized attribute in the parent.
In Example 2, .code is synthesized; .cum is inherited. As .code depends on .cum (by the last boldface rule), .cum must be computed first. However, the calculation may all be done in one mixed tree walk, with .cum computed on the way down and .code on the way back. In fact, the calculation can be combined with parsing.
Scheduling the calculations to respect dependencies can be tricky. The issue can be finessed, though, by entrusting the scheduling to lazy evaluation.