2

我正在研究 GNU bison 中的 GLR 解析器,但遇到以下问题:

我试图解析的语言允许布尔表达式,包括关系(<、>、<=、...)和布尔组合(和、或、非)。现在的问题是该语言还允许在关系的右侧有多个算术表达式......并且它们使用用于布尔组合的相同 AND 标记组合!这是一个非常愚蠢的语言设计,但我无法改变它。

所以你可以拥有a > b and c应该等同于的(a > b) and (a > c),你也可以拥有a > b and c > d应该等同于的(a > b) and (c > d)

这导致的 S/R 冲突在此示例中已经很明显:在a > b使用前瞻读取之后,and您可以将 减少a > b为布尔表达式并等待另一个布尔表达式,或者您可以转移and并等待另一个算术表达式。

我的语法目前看起来像这样:

booleanexpression
    : relation
    | booleanexpression TOK_AND booleanexpression
    ...
;
relation
    : arithmeticexpression TOK_GT maxtree
    ...
;
maxtree
    : arithmeticexpression
    | maxtree TOK_AND maxtree
    ...
;

对于任何k ,该语言显然不是LR(k) ,因为使用任何常量k -lookahead都无法解决 S/R 冲突,因为两者之间的算术表达式可以有任意多个标记。因此,我打开了 GLR 解析。

但是,当我尝试a > b and c对此进行解析时,我可以在调试输出中看到解析器的行为如下:

  • 它读取并在a前瞻>中将aarithmeticexpression
  • 它读取b并向前看and它减少b到一个arithmeticexpression然后已经减少到一个maxtree
  • 它减少a > b到一个relation
  • 它读取并将其简化carithmeticexpression

然后什么也没有发生!and c显然被丢弃了 - 调试输出不显示这些令牌的任何操作。甚至没有错误消息。我的 AST 中不存在相应的 if 语句(我仍然得到一个 AST,因为我有错误恢复)。

我认为,在阅读完之后b,应该有 2 个堆栈。但是b不应该减少。或者至少它应该给我一些错误信息(“语言是模棱两可的”会好的,我以前看过这条信息 - 我不明白为什么它不适用于这里)。任何人都可以理解这一点吗?

看了一会儿语法,你可以看出这里的主要问题是在下一个算术表达式之后是否出现

  • 另一个关系标记(那么你应该减少)
  • 另一个布尔组合(那么你应该转移)
  • 布尔/算术表达式语法(如 THEN)之外的标记,它将终止表达式,您还应该转换

你能想出一种不同的语法以更好/更确定的方式捕捉情况吗?你会如何处理这个问题?我目前正在考虑使语法更从右到左,比如

booleanexpression : relation AND booleanexpression
maxtree : arithmeticexpression AND maxtree
etc.

我认为这会使野牛更喜欢变速,并且只会首先减少右侧。也许通过使用不同的非终结符,它可以允许算术表达式后面的准“前瞻”......

旁注:GnuCOBOL 通过收集所有标记、将它们推送到中间堆栈并从那里手动构建表达式来处理这个问题。这让我灰心丧气,但我坚持希望他们这样做是因为野牛在开始时不支持 GLR 解析......

编辑:一个可重复的小例子

%{
#include <stdio.h>
int yylex ();
void yyerror(const char* msg);
%}

%glr-parser
%left '&'
%left '>'

%%
input: %empty | input bool '\n' {printf("\n");};

arith : 'a' | 'b' | 'c';
maxtree : arith { printf("[maxtree : arith]  "); }
        | maxtree '&' maxtree { printf("[maxtree : maxtree & maxtree]  "); } ;
rel : arith '>' maxtree { printf("[rel : arith > maxtree]  "); } ;
bool : rel { printf("[bool : rel]  "); }
     | bool '&' bool { printf("[bool : bool & bool]  "); } ;
%%

void yyerror(const char* msg) { printf("%s\n", msg); }
int yylex () {
    int c;
    while ((c = getchar ()) == ' ' || c == '\t');
    return c == EOF ? 0 : c;
}
int main (int argc, char** argv) {
    return yyparse();
}

这个奇怪地确实在 input 上打印了错误消息“语法错误” a>b&c

4

3 回答 3

2

能够通过使用优先声明来简化语法确实很方便(有时)[注 1],但它不能很好地使用 GLR 解析器,因为它可能导致早期拒绝明确的解析。

优先级声明背后的想法是,它们使用简单的单标记前瞻和在可能的减少和可能的移位之间配置的优先级来解决歧义(或者更准确地说,移位/减少冲突)。如果语法没有移位/归约冲突,则不会使用优先声明,但如果使用它们,它们将用于抑制移位或归约,具体取决于(静态)优先关系。

Bison 生成的 GLR 解析器实际上并不能解决歧义,但它允许继续开发可能不正确的解析,直到语法解决歧义。与使用优先级不同,这是一个延迟的解决方案;有点慢,但更强大。(GLR 解析器可以生成一个包含所有可能解析的“解析森林”。但 Bison 没有实现此功能,因为它希望解析编程语言,并且与人类语言不同,编程语言不能模棱两可。)

正如您在问题中指出的那样,用您的语言,不可能静态地解决移位/减少冲突的不确定性。您的语法根本不是 LR(1),更不用说运算符优先级,因此 GLR 解析是一个实用的解决方案。但是您必须允许 GLR 完成它的工作。用优先级比较过早地消除一个似是而非的解析将阻止 GLR 算法稍后考虑它。如果您设法消除唯一可能正确的解析,这将特别严重。

在您的语法中,不可能在rel产生式和&符号之间定义优先关系,因为不存在优先关系。在某些句子中,rel减少需要获胜;换句话说,转变应该获胜。由于语法没有歧义,只要允许 shift 和 reduce 继续进行,GLR 最终会找出哪个是哪个。

在您的完整语言中,布尔表达式和算术表达式都有类似于运算符优先级的东西,但仅限于它们各自的域内。运算符优先级解析器(以及等效的 yacc/bison 的优先级声明)通过擦除不同非终结符之间的差异来工作;它无法处理像您这样的语法,其中某些运算符在不同域(或不同域之间)具有不同的优先级。

幸运的是,优先声明的这种特殊使用只是一种捷径;它不会为语法提供任何额外的功能,并且可以通过创建新的非终结符来轻松机械地实现,每个优先级都有一个。替代语法不会有歧义。expr/term/factor 语法是经典的例子,你可以在任何包含解析算术表达式的教科书或教程中找到它。这里我还提供了优先语法进行比较:

                              %left '+' '-'
                              %left '*' '/'
%%                            %%
expr  : term
      | expr '+' term         expr: expr '+' expr
      | expr '-' term             | expr '-' expr
term  : factor
      | term '*' factor           | expr '*' expr
      | term '/' factor           | expr '/' expr
factor: ID                        | ID
      | '(' expr ')'              | '(' expr ')'

在您的最小示例中,已经有足够的非终端,不需要发明新的,所以我只是根据上述模型重写了它。

我已经在编写动作时留下了它们,以防样式对您有用。请注意,这种风格会像筛子一样泄漏内存,但这对于快速测试来说是可以的:

%code top {
#define _GNU_SOURCE 1
}

%{
#include <ctype.h>
#include <stdio.h>
#include <string.h>

int yylex(void);
void yyerror(const char* msg);
%}

%define api.value.type { char* }
%glr-parser
%token ID

%%
input   : %empty
        | input bool '\n'   { puts($2); }

arith   : ID
maxtree : arith 
        | maxtree '&' arith { asprintf(&$$, "[maxtree& %s %s]", $1, $3); }
rel     : arith '>' maxtree { asprintf(&$$, "[COMP %s %s]", $1, $3); }
bool    : rel
        | bool '&' rel      { asprintf(&$$, "[AND %s %s]", $1, $3); }
%%

void yyerror(const char* msg) { printf("%s\n", msg); }
int yylex(void) {
    int c;
    while ((c = getchar ()) == ' ' || c == '\t');
    if (isalpha(c)) {
      *(yylval = strdup(" ")) = c;
      return ID;
    }
    else return c == EOF ? 0 : c;
}

int main (int argc, char** argv) {
#if YYDEBUG
    if (argc > 1 && strncmp(argv[1], "-d", 2) == 0) yydebug = 1;
#endif
    return yyparse();
}

这是一个示例运行。请注意野牛关于移位/减少冲突的警告。如果没有这样的警告,GLR 解析器可能就没有必要了,因为没有冲突的文法是确定性的。(另一方面,由于 bison 的 GLR 实现针对确定性进行了优化,因此在确定性语言上使用 GLR 解析器并没有太多成本。)

$ bison -t -o glr_prec.c glr_prec.y
glr_prec.y: warning: 1 shift/reduce conflict [-Wconflicts-sr]
$ gcc -Wall -o glr_prec glr_prec.c
$ ./glr_prec
a>b
[COMP a b]
a>b & c
[COMP a [maxtree& b c]]
a>b & c>d
[AND [COMP a b] [COMP c d]]
a>b & c & c>d
[AND [COMP a [maxtree& b c]] [COMP c d]]
a>b & c>d & e
[AND [COMP a b] [COMP c [maxtree& d e]]]
$

笔记

  1. 尽管当您了解实际发生的情况时,优先声明很方便,但人们有一种巨大的趋势,即从他们在互联网上找到的其他语法中进行货物崇拜,而不是经常从其他地方也被货物崇拜的语法. 当优先声明没有按预期工作时,下一步是随机修改它们以希望找到一个有效的配置。有时这会成功,通常会留下不必要的碎屑,这些碎屑会再次被货物崇拜。

    因此,尽管在某些情况下优先声明确实简化了语法并且明确的实现会复杂得多(例如具有许多不同复合语句类型的语言中的悬空-else 解析),但我仍然发现自己不推荐他们的用途。

    最近对另一个问题的回答中,我写了我希望是对优先算法的一个很好的解释(如果不是,请告诉我它是如何不足的)。

于 2018-06-19T22:18:01.713 回答
2

欢迎来到 COBOL 的精彩世界。我可能是错的,但您可能会在这里遇到一些额外的问题。A > B AND C在您知道 C 是如何声明的之前,诸如 COBOL 中的表达式是模棱两可的。考虑以下程序:

   IDENTIFICATION DIVISION.
   PROGRAM-ID EXAMPLE.
   DATA DIVISION.
   WORKING-STORAGE SECTION.
   01     A     PIC 9 VALUE 2.
   01     B     PIC 9 VALUE 1.
   01     W     PIC 9 VALUE 3.
       88 C           VALUE 3.
   PROCEDURE DIVISION.
       IF A > B AND C
          DISPLAY 'A > B AND 88 LEVEL C is TRUE because W = ' W
       ELSE
          DISPLAY 'A not > B or 88 LEVEL C is not TRUE'
       END-IF
       DISPLAY 'A: ' A ' B: ' B ' W:' W  
       GOBACK
       .

该程序的输出是:

A > B AND 88 LEVEL C is TRUE because W = 3
A: 2 B: 1 W: 3

本质上,表达式:A > B AND C等价于: A > B AND W = 3。如果 C 以类似于 A 和 B 的方式定义,则语义将是:A > B AND A > C,在这种情况下为 FALSE。

于 2018-06-20T14:20:26.173 回答
0

上面提到的代码运行良好,但我从来没有让它在我的真实项目中工作,即使我看不出我的真实项目和这段代码之间有什么区别。

这让我发疯了,但我刚刚在我的代码中发现了另一个问题,它阻止了这种方法的工作:我的%skeleton "lalr1.cc"序言中有一个(诚然是货物崇拜),它再次禁用了 GLR 解析!我需要用

%skeleton "glr.cc"
于 2018-08-06T20:43:26.447 回答