0

I'm trying to implement try-catch-finally expression in my toy language with Bison.

One more thing is that, inspired by Scala grammar, item inside try-catch-finally is an expression, not a block statement.

Here's the grammar.y:

%code top {
#include <cstdio>
}

%union {
    int n;
    Ast *ast;
}

%code requires {
class Ast;
int yylex(void);
void yyerror(const char *msg);
}

%token<n> NUM
%token<n> PLUS '+'
%token<n> MINUS '-'
%token<n> TIMES '*'
%token<n> DIVIDE '/'
%token<n> SEMICOLON ';'
%token<n> NEWLINE '\n'
%token<n> IF "if"
%token<n> ELSE "else"
%token<n> TRY "try"
%token<n> CATCH "catch"
%token<n> FINALLY "finally"
%token<n> LPAREN '('
%token<n> RPAREN ')'

%type<ast> prog expr primaryExpr

/* grammar precedence */
%nonassoc "try_catch" /* lower than finally */
%nonassoc "try_catch_finally"

/* operator precedence is higher than grammar precedence (try-catch-finally) */
%left PLUS MINUS
%left TIMES DIVIDE


%start prog

%%

prog : expr
     ;

expr : "try" expr "catch" expr %prec "try_catch" { $$ = nullptr; }
     | "try" expr "catch" expr "finally" expr %prec "try_catch_finally" { $$ = nullptr; }
     | primaryExpr
     ;

primaryExpr : NUM { $$ = nullptr; }
            | primaryExpr '+' NUM { $$ = nullptr; }
            | primaryExpr '-' NUM { $$ = nullptr; }
            | primaryExpr '*' NUM { $$ = nullptr; }
            | primaryExpr '/' NUM { $$ = nullptr; }
            ;

%%

void yyerror(const char *msg) {
    fprintf(stderr, "%s\n", msg);
}

Generating files with: bison --debug --verbose -Wcounterexamples -o grammar.tab.cpp --defines=grammar.tab.h grammar.y, we have an grammar.output file with a shift/reduce conflict:

Terminals unused in grammar

    PLUS
    MINUS
    TIMES
    DIVIDE
    SEMICOLON
    ';'
    NEWLINE
    '\n'
    "if"
    "else"
    LPAREN
    '('
    RPAREN
    ')'


State 17 conflicts: 1 shift/reduce


Grammar

    0 $accept: prog $end

    1 prog: expr

    2 expr: "try" expr "catch" expr
    3     | "try" expr "catch" expr "finally" expr
    4     | primaryExpr

    5 primaryExpr: NUM
    6            | primaryExpr '+' NUM
    7            | primaryExpr '-' NUM
    8            | primaryExpr '*' NUM
    9            | primaryExpr '/' NUM


Terminals, with rules where they appear

    $end (0) 0
    '\n' <n> (10)
    '(' <n> (40)
    ')' <n> (41)
    '*' <n> (42) 8
    '+' <n> (43) 6
    '-' <n> (45) 7
    '/' <n> (47) 9
    ';' <n> (59)
    error (256)
    NUM <n> (258) 5 6 7 8 9
    PLUS <n> (259)
    MINUS <n> (260)
    TIMES <n> (261)
    DIVIDE <n> (262)
    SEMICOLON <n> (263)
    NEWLINE <n> (264)
    "if" <n> (265)
    "else" <n> (266)
    "try" <n> (267) 2 3
    "catch" <n> (268) 2 3
    "finally" <n> (269) 3
    LPAREN <n> (270)
    RPAREN <n> (271)
    "try_catch" (272)
    "try_catch_finally" (273)


Nonterminals, with rules where they appear

    $accept (27)
        on left: 0
    prog <ast> (28)
        on left: 1
        on right: 0
    expr <ast> (29)
        on left: 2 3 4
        on right: 1 2 3
    primaryExpr <ast> (30)
        on left: 5 6 7 8 9
        on right: 4 6 7 8 9


State 0

    0 $accept: • prog $end

    NUM    shift, and go to state 1
    "try"  shift, and go to state 2

    prog         go to state 3
    expr         go to state 4
    primaryExpr  go to state 5


State 1

    5 primaryExpr: NUM •

    $default  reduce using rule 5 (primaryExpr)


State 2

    2 expr: "try" • expr "catch" expr
    3     | "try" • expr "catch" expr "finally" expr

    NUM    shift, and go to state 1
    "try"  shift, and go to state 2

    expr         go to state 6
    primaryExpr  go to state 5


State 3

    0 $accept: prog • $end

    $end  shift, and go to state 7


State 4

    1 prog: expr •

    $default  reduce using rule 1 (prog)


State 5

    4 expr: primaryExpr •
    6 primaryExpr: primaryExpr • '+' NUM
    7            | primaryExpr • '-' NUM
    8            | primaryExpr • '*' NUM
    9            | primaryExpr • '/' NUM

    '+'  shift, and go to state 8
    '-'  shift, and go to state 9
    '*'  shift, and go to state 10
    '/'  shift, and go to state 11

    $default  reduce using rule 4 (expr)


State 6

    2 expr: "try" expr • "catch" expr
    3     | "try" expr • "catch" expr "finally" expr

    "catch"  shift, and go to state 12


State 7

    0 $accept: prog $end •

    $default  accept


State 8

    6 primaryExpr: primaryExpr '+' • NUM

    NUM  shift, and go to state 13


State 9

    7 primaryExpr: primaryExpr '-' • NUM

    NUM  shift, and go to state 14


State 10

    8 primaryExpr: primaryExpr '*' • NUM

    NUM  shift, and go to state 15


State 11

    9 primaryExpr: primaryExpr '/' • NUM

    NUM  shift, and go to state 16


State 12

    2 expr: "try" expr "catch" • expr
    3     | "try" expr "catch" • expr "finally" expr

    NUM    shift, and go to state 1
    "try"  shift, and go to state 2

    expr         go to state 17
    primaryExpr  go to state 5


State 13

    6 primaryExpr: primaryExpr '+' NUM •

    $default  reduce using rule 6 (primaryExpr)


State 14

    7 primaryExpr: primaryExpr '-' NUM •

    $default  reduce using rule 7 (primaryExpr)


State 15

    8 primaryExpr: primaryExpr '*' NUM •

    $default  reduce using rule 8 (primaryExpr)


State 16

    9 primaryExpr: primaryExpr '/' NUM •

    $default  reduce using rule 9 (primaryExpr)


State 17

    2 expr: "try" expr "catch" expr •
    3     | "try" expr "catch" expr • "finally" expr

    "finally"  shift, and go to state 18

    "finally"  [reduce using rule 2 (expr)]
    $default   reduce using rule 2 (expr)

    shift/reduce conflict on token "finally":
        2 expr: "try" expr "catch" expr •
        3 expr: "try" expr "catch" expr • "finally" expr
      Example: "try" expr "catch" "try" expr "catch" expr • "finally" expr
      Shift derivation
        expr
        ↳ "try" expr "catch" expr
                             ↳ "try" expr "catch" expr • "finally" expr
      Reduce derivation
        expr
        ↳ "try" expr "catch" expr                        "finally" expr
                             ↳ "try" expr "catch" expr •



State 18

    3 expr: "try" expr "catch" expr "finally" • expr

    NUM    shift, and go to state 1
    "try"  shift, and go to state 2

    expr         go to state 19
    primaryExpr  go to state 5


State 19

    3 expr: "try" expr "catch" expr "finally" expr •

    $default  reduce using rule 3 (expr)

Let's focus on the conflict part:

State 17

    2 expr: "try" expr "catch" expr •
    3     | "try" expr "catch" expr • "finally" expr

    "finally"  shift, and go to state 18

    "finally"  [reduce using rule 2 (expr)]
    $default   reduce using rule 2 (expr)

    shift/reduce conflict on token "finally":
        2 expr: "try" expr "catch" expr •
        3 expr: "try" expr "catch" expr • "finally" expr
      Example: "try" expr "catch" "try" expr "catch" expr • "finally" expr
      Shift derivation
        expr
        ↳ "try" expr "catch" expr
                             ↳ "try" expr "catch" expr • "finally" expr
      Reduce derivation
        expr
        ↳ "try" expr "catch" expr                        "finally" expr
                             ↳ "try" expr "catch" expr •

For "try" expr "catch" "try" expr "catch" expr "finally" expr, in the default reduce, "finally" is bind to the 1st "try" not the 2nd "try". Which I believe is not same with Java/Scala behaviour.

And I try to use %prec to adjust the precedence to solve it, but failed.

How shoud I solve this issue ?

linrongbin
  • 2,967
  • 6
  • 31
  • 59
  • The dangling finally problem is *exactly* the same as the dangling else problem. The *only* difference is that the tokens have different spellings. The solution is also the same. – rici Aug 26 '20 at 06:22
  • Also: yacc/bison's default conflict resolution algorithm is to prefer to shift. That's exactly what you want to resolve the dangling else (or finally), and it's exactly what the parser is doing here. – rici Aug 26 '20 at 06:27
  • @rici, I had update the answer with using `%prec` skill to try to solve it. but shift/reduce remains. Please check question again. – linrongbin Aug 26 '20 at 06:28
  • @rici, hi rici, I'm confused about understanding the conuterexamples that bison gives me. In the example `"try" expr "catch" "try" expr "catch" expr "finally" expr`, will it bind `finally` with the 2nd `try` ? – linrongbin Aug 26 '20 at 06:31

1 Answers1

1

As indicated in a comment, the shift-reduce conflict created by an optional finally clause in a try – catch – finally statement is exactly the same as the optional else clause in an if – then – else statement, the so-called "dangling else".

Since "dangling finally" is the same problem as "dangling else", we can expect the solution to be the same. Of the solutions, the easiest is the use of precedence declarations, of which the simplest is

%right "if" "else" "catch" "finally"

Declaring these tokens (and consequently, the productions whose last terminal is one of these tokens) as %right means that when a conflict arises involving one of these tokens, the shift action should be chosen. Since this is bison's default conflict resolution (see Note 2), the only effect of that precedence declaration is to suppress the warning message about the conflict.

The over-engineered solution in the edited question would work as well, although I'd caution against unnecessary use of %nonassoc. [Note 1] However, it is not sufficient to add a comment:

%nonassoc "try_catch" /* lower than finally */

You actually need to also add the declaration

%right finally

The precedence solution shown above has the advantage that it is self-contained. Not only does it not depend on other precedence declarations, it also does not rely on %prec declarations, which could also easily be accidentally omitted.

Although it is not particularly relevant to the question of how to solve the problem, it's worth noting that you have misinterpreted bison's report output. Bison reported the state transitions in State 17 as:

    "finally"  shift, and go to state 18

    "finally"  [reduce using rule 2 (expr)]
    $default   reduce using rule 2 (expr)

This should be read as follows:

  1. When "finally" is the lookahead, shift the lookahead token and go to state 18.

  2. There was also a conflicting action for lookahead token "finally": reduce to expr using rule 2. This action was eliminated by the conflict resolution algorithm [Note 2]. (Bison puts actions in brackets ([reduce using rule 2 (expr)] to indicate that the action was eliminated by conflict resolution.)

  3. For all other lookahead tokens ($default), reduce to expr using rule 2.

Note that Bison does not report on parse actions eliminated by precedence declarations. Those are silently dropped.


Notes

  1. If you want to declare a precedence relationship without specifying associativity, use %precedence. Unlike %nonassoc, that will not silently hide grammar bugs.

  2. The default conflict resolution algorithm is:

    • If there is a shift action, use it. (There is never more than one possible shift.)
    • If there is no shift action, use the reduce action with the smallest rule number; that is, the one which came first in the grammar file.
rici
  • 234,347
  • 28
  • 237
  • 341
  • I add `%right "catch" "finally"` after `%nonassoc "try_catch"` `%nonassoc "try_catch_finally"`, and the shift/reduce warnning disappear! Thank you a lot, rici – linrongbin Sep 04 '20 at 01:25