Using non-terminal rules
Infix operators can also be implemented using non-terminal rules. This approach provides more flexibility and allows for better handling of operator precedence and associativity.
Let’s implement a simple expression grammar with the following operators:
| Operator | Precedence | Associativity |
|---|---|---|
+ |
1 | Left |
- |
1 | Left |
* |
2 | Left |
/ |
2 | Left |
^ |
3 | Right |
Expression:
Addition;
Addition infers Expression:
Multiplication ({infer BinaryExpression.left=current} operator=('+' | '-') right=Multiplication)*;
Multiplication infers Expression:
Exponentiation ({infer BinaryExpression.left=current} operator=('*' | '/') right=Exponentiation)*;
Exponentiation infers Expression:
Primary ({infer BinaryExpression.left=current} operator='^' right=Exponentiation)?;
Primary infers Expression:
'(' Expression ')'
| ... //prefix, literals, identifiers
;
Precedence is handled by creating a hierarchy of non-terminal rules. Each rule corresponds to a level of precedence, with higher-precedence operators being defined in rules that are called by lower-precedence rules. For example, Addition calls Multiplication, which in turn calls Exponentiation. This ensures that multiplication and division are evaluated before addition and subtraction, and exponentiation is evaluated before both.
Associativity is handled by three patterns. They make sure that operators are grouped correctly based on their associativity.
Current infers Expression:
Next ({infer BinaryExpression.left=current} operator=('op1' | 'op2' | ...) right=Next)*;
Current infers Expression:
Next ({infer BinaryExpression.left=current} operator=('op1'| 'op2' | ...) right=Current)?;
Current infers Expression:
Next ({infer BinaryExpression.left=current} operator=('op1' | 'op2' | ...) right=Next)?;