Search code examples
yaccoperator-precedence

is it possible to separate the concept of precedence and association in yacc


I would like to have a clear example of precedence and one of associativity in yacc, but I find myself yet in having troubles separating these two concepts. Perhaps this is due to the fact that I'm associating these two concepts to math and mathematical operation.. These are two old examples I built:

Associativity (*) is used to specify the kind of association to be applied (left,right, non assoc....) In fact

%left '+' '*'

instruct that plus and multiplication are left associative. So far, so good. (not exactly but it serve the purpose of the example)

Precedence (**) is used to give precedence to one operator over another.

%left '+'
%left '*'

the multiplication has higher precedence than plus operation. So we got the wanted parsing action for E+E*E

E+(E*E) in case of (**)
(E+E)*E in case of only (*) --> this is clearly wrong - but it's fine for the example

So question is, can I separate clearly associativity from precedence without using the concept of associativity? Even non-associatity implies associativity knowledge… so.. how, if possible, can I talk separately about them?


Solution

  • No. In a parser definition, associativity is just a small detail within the precedence algorithm.

    To understand that, it's important to understand what precedence actually means, in parsing terms.

    A left-to-right shift-reduce parser has a stack and an input stream. Initially, the stack is empty, and the input stream contains the input to be parsed. The SR parser repeatedly does one of the following two actions until the stack consists only of the start symbol and the input stream is empty (in which case the parse has succeeded), or neither action is possible (in which case the parse has failed):

    • reduce the production whose right-hand side is on the top of the stack by popping the right-hand side off of the stack and pushing the left-hand side non-terminal;
    • shift one input symbol from the input onto the stack.

    It's an important feature of this framework that reductions can only occur when the production's right-hand side is on the top of the stack.

    The shift action is always possible unless the input stream is exhausted, but a reduce action can only be taken if the top of the stack precisely matches the right-hand side of some production.

    Different ways of building SR parsers will involve different mechanisms for deciding which action to take in any given stack configuration. One such mechanism is the precedence algorithm. Some very simple languages can be SR parsed only with the precedence algorithm. In other cases, it can be used as an auxiliary decision algorithm in order to resolve ambiguous grammar specifications; this is the use case for precedence in yacc-derived parser generators.

    For precedence to work, it is necessary that at most one reduction action be possible in any stack configuration, which means that there cannot be two productions with the same right-hand side. [Note 1]

    Given that there is at most one possible reduction action and at most one possible shift action (since the next input symbol, if any, is given), the only issue is deciding whether to shift or reduce. The precedence algorithm involves a precedence function PREC(Aα, a) ⇒ { SHIFT, REDUCE }, whose arguments are a production A→α and a terminal symbol a, which are mapped onto either SHIFT or REDUCE.

    Although the precedence relationship is usually written as though it were a comparison, it is not a normal comparison operator because the two arguments are from different domains. It always involves a production and a terminal.

    In simple cases, however, it is possible to implement PREC using numeric comparisons. To do that, we define two functions which map productions and terminals, respectively, onto integers: f(Aα) and g(a). We use those to compute PREC:

    PREC(Aα, a) ≡
       REDUCE  if f(Aα) > g(a)
       SHIFT  if f(Aα) < g(a)   [Note 2]

    In any event, the precedence algorithm for a given stack configuration is:

    1. Identify the production P (=Aα) of the possible reduce action, if any.

    2. If only a shift or only a reduce is possible, do that. Otherwise, if both a reduce and a shift are possible, compute PREC(P, input) and reduce using P if the result is REDUCE; otherwise, shift input.

    Now that might seem confusing, since most descriptions of precedence relations describe them as though they compared terminals, rather than a production with a terminal. That's because it is normal to "name" each production using the last terminal in the production. Usually, that is unambiguous, because of the restriction on production right-hand sides: since two right-hand side must differ, it is likely that all production right-hand sides have different terminal symbols. [Note 3]

    Although that short-hand allows us to say, for example, that "* has higher precedence than +" instead of the somewhat more cumbersome "the production EE*E has precedence over the terminal +", it is important to remember that the latter statement is what we really mean.

    Precedence also applies to single operators. With most operators, we prefer to group from left to right, so that E-E-E should be parsed as though it had been written (E-E)-E. However, some operators like exponentiation group to the right, meaning that E**E**E should be parsed as E**(E**E). This is simple to define using the PREC function; for a left-grouping operator ⊕, we'll have:

    PREC(EEE, ⊕) ≡ REDUCE

    while a right-grouping operator ⊗ would have

    PREC(EEE, ⊗) ≡ SHIFT

    That's clear when we use the actual arguments to PREC, but it becomes confusing when we use the shorthand notation, which leaves us trying to say that ⊕ has higher precedence than ⊕ while ⊗ has lower precedence than ⊗. To avoid the ambiguity and still let us get away with the shorthand, we describe ⊕ as "left-associative" (%left) and ⊗ as "right-associative" (%right). But the implementation is simply an application of the normal precedence algorithm.

    As an example, consider the simple expression language:

    E → E + E
    E → E * E
    E → E ** E
    E → id
    

    Here we expect * to bind more tightly than + with ** binding tightest; the first two group to the left while exponentiation groups to the right. To achieve that, we can assign f and g functions as follows:

    Production   f(Production)     Terminal  g(Terminal)
    E → E + E         2                +          1
    E → E * E         4                *          3
    E → E ** E        5                **         6
    E → id            8                id         7
    

    Yacc-generated grammars don't use precedence to decide when to reduce the E→id production, but the above will work since the grammar can be parsed completely using only the precedence algorithm.

    Parentheses can easily be added; I'll leave that as an exercise.


    Notes

    1. There might be some other mechanism to decide between reduction actions, so the restriction is only absolute for a parser which only uses precedence. There might also be some other mechanism to restrict possible shift actions. For example, for a shift to be feasible, the tokens on the top of the stack need to eventually be reduced, which means that some suffix of the stack must be a prefix of the right-hand side of some production. Similarly, a reduction is only feasible if, post-reduce, some suffix of the stack is the prefix of the right-hand side of some production.

    2. You'll see formulations using < and ≥ (or ≤ and >), but to avoid confusion, I'm assuming that the ranges of f and g are different sets of integers. Since the functions are arbitrary, this does not restrict generality.

    3. That's not always the case. For example, languages which allow - to be either a unary or a binary operator will have productions with right-hand sides - E and E - E. Yacc-derived parser generators use the %prec TERMINAL declaration to associate a production with a terminal other than the default.