Go to the first, previous, next, last section, table of contents.
So far, rules were like procedures: you invoke them and after some time they return. However, GP employs an attribute grammar, meaning that rules can be passed attributes and also return them. In this section, we'll look at an infix-expression parser, in which each rule that is responsible for parsing a subexpression will return the result of evaluating that part of the expression.
The calculator repeatedly reads an expression and prints the result of evaluating the expression. The top rule of the grammar will look like this:
%class ex5
%token NUMBER
grammar
{
int result;
}
:
[expr (result) ';'
{
[[[stdio out] print result] nl];
}
]*
;
The non-terminal expr, which has not been defined yet, is used as in
expr (result)
indicating that the expr rule employs one attribute. We need to
look at the definition of expr, to see what kind of attribute
this actually is. The expr rule starts like this:
expr (<int v) ...
This indicates that expr has one attribute, `v', of type
int. The prefix `<' means that it is a synthesized
attribute, i.e., a value for the attribute is returned to the invoking
rule.
In the output of gp, the expr rule will be implemented by
the following method:
int (v) expr;
Thus, the expr method returns an int corresponding to the
local variable `v'. Setting the variable v within the rule
will affect the value that is returned. Similarly, a rule with multiple
synthesized attributes is implemented with multiple return values.
Also in the code generated by gp, the use of the expr rule
becomes:
result = [self expr];
The full expr rule looks like this:
expr (<int v)
{
int right;
}
:
term (v)
['+' term (right)
{
v += right;
}
|'-' term (right)
{
v -= right;
}
]*
;
In words: an expr starts with a term, followed by the
addition or subtraction of zero or more terms. Similar to an
expr, a term has one synthesized attribute. The value it
returns is stored in the local variable v, which happens to be
the name of the value that the expr rule returns: if the
term is not followed by an addition or subtraction, the value
returned by the term will be returned by the expr.
When expr is to parse the expression `1 + 2', the invocation
of term will digest the `1' and return 1. This 1 will be
stored in v. Since it is followed by a `+', the first
branch of the alternative that follows will be taken. The term
will be invoked again to parse what remains, i.e. `2'. The value
that it returns is stored in the variable right, and the action
that follows will be executed, which will add right to v,
making v equal to 3. The end of the input has been reached and
the expr rule will return the value of v: it has
successfully parsed the input and correctly returned the result: 1 + 2
is 3.
The parser is still not complete, with the non-terminal term not
yet defined. What an expr does with addition and subtraction, a
term does with multiplication and division. Because the
operators are handled by two rules and because the expr depends
on term, the operator priorities are handled automatically and
`1 + 2 * 3' will produce 7 and not 9.
term (< int value)
{
int left;
}
: atom (left)
['*' atom (value)
{
left = left * value;
}
|'/' atom (value)
{
left = left / value;
}
]*
{
value = left;
}
;
Note how term uses a different approach than expr to
accumulate the result and hence must set the correct return value after
it has finished matching the input.
The definition of the atom completes the parser. An atom
is a number, a parenthesized expression, or a negated atom.
atom (< int value)
:
NUMBER
{
value = [lex number_value];
}
| '-' atom (value)
{
value = -value;
}
| '(' expr (value) ')'
;
Go to the first, previous, next, last section, table of contents.