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 term
s. 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.