语法匹配的正则表达式字符类,后接破折号

时间:2019-07-19 22:22:39

标签: parsing grammar bnf lalr

我正在编写自己的LALR(1)解析器生成器,所以不确定我的解析器生成器或语法是否有问题。

我正在尝试为正则表达式生成解析器。 对于字符类,我有以下规则(略有简化):

LBRACKET: "[" // the [ character
RBRACKET: "]"
DASH: "-"
CHAR: [^[\]] // everything except square brackets

class ::= LBRACKET class_contents RBRACKET

class_contents ::= class_element | class_element class_contents

class_element ::= literal | literal DASH literal

literal ::= DASH | CHAR

我可以匹配诸如[a-bc-d]之类的正则表达式,但不能匹配应与匹配字符[a-bc-de-]的规则相对应的a, b, c, d, e, -

似乎在看到令牌e(类型literal)和-(类型DASH)时,解析器尝试匹配规则literal DASH literal 。 看到](类型为RBRACKET)后,它需要意识到它开始了错误的生产。

这种情况是解析器需要2个超前标记,因此LALR(1)不足吗? 在这种情况下,是否有办法重写语法以使其起作用? 还是该语法对匹配[a-bc-de-]有效,我应该在解析器生成器中查找错误吗?

3 个答案:

答案 0 :(得分:1)

是的,LALR(1)不足。 LALR(1)解析器生成器应该抱怨生产中的移位减少冲突:

class_element ::= literal | literal DASH literal

在移动literal之后,您将进入一个内核为以下两项的状态:

class_element ::= literal .
class_element ::= literal . DASH literal

分别要求执行减少动作和移位动作,并且不能通过1个超前符号来解决,因为减少动作的跟随集包括DASH。

2个先行标记也不起作用。实际上,此语法对于任何k而言都不是LALR(k),因为它是模棱两可的:class_contents可以以两种方式(以三种literal DASH literal或一种方式)派生出class_elements。 / p>


  

在这种情况下,有没有办法重写语法以使其起作用?

(抱歉,错过了原始问题的一部分。)

有可能对此语言做出明确的语法。几乎可以肯定,您必须放弃literal ::= DASH生产。而且您可能需要/想要将“文字DASH”限制在课程末尾。例如,我认为这可以做到:

class_contents ::= DASH | class_element | class_element class_contents

class_element ::= literal | literal DASH literal

literal ::= CHAR

(您可以等效地将“文字DASH”限制为该类的 start 。允许两者都可以,但可能不那么简单。)

尽管没有歧义,但该语法仍然不是LALR(1):它具有与原始语法相同的移位减少问题。但是,我认为这是LALR(2)。

如果您真的想要LALR(1),则有一个理论结果表明,任何LALR(k)语法都可以转换为等效的LALR(1)语法。但是我不确定结果会是什么样。

答案 1 :(得分:1)

LALR(1)应该可以。您只需要将class_element重写为左递归,这在LALR(1)中通常更可取

class: LBRACKET class_contents RBRACKET
class_contents: class_element | class_element class_contents
class_element: literal | class_element DASH literal
literal: DASH | CHAR

我在以下输入中测试了该语法,它似乎运行良好:

[a-bc-de-]
[a-bc-de]
[-a-bc-de]
[-]

答案 2 :(得分:0)

正如已经指出的那样,您的语法是模棱两可的。虽然有时可以使用标准试探法来解决歧义(显示为移位/减少冲突),例如“偏爱减少移位”,但这种技术并不完全通用,而且并不总是容易理解决议的后果。

实用的LALR生成器确实具有解析算法,通常基于运算符优先级声明,并且具有默认算法的后备(优先选择移位,如果没有移位,则首选语法的第一个归约法)。这些技术可以简化语法编写,有时可以使语法更具可读性和解析速度。但是,一旦您走出舒适区进行自动解析器冲突解决,光泽就会逐渐消失。

创建明确的语法并不难,特别是如果您首先精确定义了允许的句子是什么。在这里,我将对正则表达式字符类使用Posix标准的简化版本,以便将重点放在允许尾随破折号作为字符的精确问题上。从标准中删除:

  • 排序规则元素([.ä.]

  • 等价类([=œ=]

  • 标准命名字符类([:alpha:]

  • 否定类([^…]

根据Posix,将字符类中的-视为普通字符,“如果它首先出现(在初始^之后,如果出现)或最后出现在列表中,或作为范围表达式的结束范围点。” (此外,不允许使用空字符类;如果类中的第一个字符是],它也将被视为普通字符。)下面,我(尚未)实现这三个字符中的第一个标准(列表中的第一个),重点关注其他两个。这允许非常简单的语法,类似于Michael Dyck提供的语法:

/* I use bison syntax in order to be able to easily test the grammars */
class   : '[' contents ']'   /* See Note 1 */
contents: '-'
        | unit
        | unit contents
unit    : CHAR
        | CHAR '-' CHAR      /* See Note 2 */
        | CHAR '-' '-'

与迈克尔的语法一样,该语法是明确的,但是LALR(2),这使它在理论上很有趣,但由于几乎没有可用的LALR(2)解析器生成器,因此几乎几乎没有用。您可以使用GLR或Early parser对其进行解析,但也可以将(LA)LR(k)语法机械地转换为(LA)LR(1)语法[注3]。 (迈克尔也暗示了这种结构。)

我已经在许多SO答案中提到了这一事实,但是这种语法实际上足够简单,可以手动进行转换,这可能会使它更容易理解。

转换非常简单。为了将LR(k)减少为LR(1),我们只需将每个减少量向右移动k-1个标记即可。为此,对于每个语法符号V(包括终端和非终端),我们创建所有可能的“延迟”语法符号。每个此类符号的格式为firstVfollow,其中firstfollowk-1FIRSTk−1组{{ 1}} [注3]。新符号FOLLOWk−1代表V的一个实例,该实例在输入流中较早地出现了k-1个令牌,但是由于现在有足够的信息可以确定,因此可以在此时减少它。

很明显,这代表语法的巨大爆炸,尽管对于使用firstVfollow的简单语法而言,它或多或少是可以管理的。在实践中,构造(LA)LR(k)解析器同样容易管理。而且,转换后的语法远非易读,这是基于语法的解析器生成的关键功能。 (当然,如果转换是在计算机程序的后台完成的,那也没关系。)但是这种构造确实可以证明每个(LA)LR(k)语法都具有等效的(LA)LR(1) )语法。

完整的构造还显示了在解析树的构造过程中如何撤消转换。我还没有看到关于如何转换语义动作的描述,但是使用yacc / bison并不是很难。所需要的是为每个(变换的)符号赋予两个属性(或者,按照双音符号的说法,V由两个值组成):一个代表被减少的(延迟的)符号的语义值,另一个代表刚刚转移的令牌的语义值。

k = 2形式的符号中,reduce值是struct的语义值,而延迟的令牌值是firstVfollow中最后一个令牌的语义值。 Yacc / bison实现了对V语法的很少使用的扩展,该语法允许语义操作通过使用小于1的follow的值来引用出现在值堆栈中较早的语义值。 ,对应符号$i的令牌值将在i处找到。 (由于$i必须为常量,因此在编写规则时必须自己进行减法运算。)

在下面的示例中,我完全不使用归约值;相反,减少量只是打印减少量的值。诸如$(i − (k−1))之类的语义值引用是应用上述公式的结果。 (在这种情况下,i为1,因此$0指向右侧位置1处的符号的令牌值。)

有了它,这是一个完整的程序,可用于测试语法:

k-1

这里是一个简短的示例运行:

$0

研究野牛踪迹可能有帮助,也可能没有帮助。 FWIW,这是一个示例:

$ cat charclass.y
%token CHAR
%code {
#include <ctype.h>
#include <stdio.h>
#include <string.h>

int yylex(void) {
  int c;
  do c = getchar(); while (c != '\n' && isspace(c));
  yylval = c;
  switch (c) {
    case EOF: return 0;
    case '\n': case '-': case '[': case ']': return c;
    default: return CHAR;
  }
}
void yyerror(const char* msg) {
  fprintf(stderr, "%s\n", msg);
}
}

%%
input: %empty
     | input class '\n'
     | input '\n'
     | error '\n' { yyerrok; }

/* Original untransformed grammar, for reference */
class: '[' contents ']'
contents: '-' | unit | unit contents
unit    : CHAR | CHAR '-' CHAR | CHAR '-' '-'
*/

class              : '[' OPEN-class-epsi                          { fputc('\n', stderr); }
OPEN-class-epsi    : OPEN-OPEN-DASH DASH-contents-CLOS CLOS-CLOS-epsi
                   | OPEN-OPEN-CHAR CHAR-contents-CLOS CLOS-CLOS-epsi
DASH-contents-CLOS : DASH-DASH-CLOS                               { fprintf(stderr, "CHR(%c) ", $0); }
CHAR-contents-CLOS : CHAR-unit-CLOS
                   | CHAR-unit-DASH DASH-contents-CLOS
                   | CHAR-unit-CHAR CHAR-contents-CLOS
CHAR-unit-CLOS     : CHAR-CHAR-CLOS                               { fprintf(stderr, "CHR(%c) ", $0); }
                   | CHAR-CHAR-DASH DASH-DASH-CHAR CHAR-CHAR-CLOS { fprintf(stderr, "RNG(%c-%c) ", $0, $2); }
                   | CHAR-CHAR-DASH DASH-DASH-DASH DASH-DASH-CLOS { fprintf(stderr, "RNG(%c-%c) ", $0, $2); }
CHAR-unit-DASH     : CHAR-CHAR-DASH                               { $$ = $1; fprintf(stderr, "CHR(%c) ", $0); }
                   | CHAR-CHAR-DASH DASH-DASH-CHAR CHAR-CHAR-DASH { $$ = $3; fprintf(stderr, "RNG(%c-%c) ", $0, $2); }
                   | CHAR-CHAR-DASH DASH-DASH-DASH DASH-DASH-DASH { $$ = $3; fprintf(stderr, "RNG(%c-%c) ", $0, $2); }
CHAR-unit-CHAR     : CHAR-CHAR-CHAR                               { $$ = $1; fprintf(stderr, "CHR(%c) ", $0); }
                   | CHAR-CHAR-DASH DASH-DASH-CHAR CHAR-CHAR-CHAR { $$ = $3; fprintf(stderr, "RNG(%c-%c) ", $0, $2); }
                   | CHAR-CHAR-DASH DASH-DASH-DASH DASH-DASH-CHAR { $$ = $3; fprintf(stderr, "RNG(%c-%c) ", $0, $2); }
CLOS-CLOS-epsi     : %empty
CHAR-CHAR-CHAR     : CHAR
CHAR-CHAR-CLOS     : ']'
CHAR-CHAR-DASH     : '-'
DASH-DASH-CHAR     : CHAR
DASH-DASH-CLOS     : ']'
DASH-DASH-DASH     : '-'
OPEN-OPEN-DASH     : '-'
OPEN-OPEN-CHAR     : CHAR

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

我依靠S. Sippu和E:Soisalon-Soininen(Springer-Verlag,1988)在解析理论的6.7节中对算法的描述,应该在任何优秀的学术机构中都可以找到。库。

注意:

  1. 野牛像许多解析器生成器一样,使您可以编写带有单引号的单字符标记,这使语法更具可读性,恕我直言。

  2. 为了简化以下步骤,我避免定义

    任意:CHAR | '-'

    可以用来允许$ bison -t -o charclass.c charclass.y && gcc -Wall -std=c11 -o charclass -ggdb charclass.c $ ./charclass [abc] CHR(a) CHR(b) CHR(c) [a-bc] RNG(a-b) CHR(c) [ab-c] CHR(a) RNG(b-c) [ab-] CHR(a) CHR(b) CHR(-) [a-b-] RNG(a-b) CHR(-) [a--] RNG(a--) [a---] RNG(a--) CHR(-) [a-b-c] RNG(a-b) syntax error 作为“结束范围点”(如$ ./charclass -d <<< '[a-b-]' Starting parse Entering state 0 Reading a token: Next token is token '[' () Reducing stack by rule 1 (line 23): -> $$ = nterm input () Stack now 0 Entering state 2 Next token is token '[' () Shifting token '[' () Entering state 6 Reading a token: Next token is token CHAR () Shifting token CHAR () Entering state 8 Reducing stack by rule 29 (line 58): $1 = token CHAR () -> $$ = nterm OPEN-OPEN-CHAR () Stack now 0 2 6 Entering state 12 Reading a token: Next token is token '-' () Shifting token '-' () Entering state 19 Reducing stack by rule 24 (line 53): $1 = token '-' () -> $$ = nterm CHAR-CHAR-DASH () Stack now 0 2 6 12 Entering state 26 Reading a token: Next token is token CHAR () Shifting token CHAR () Entering state 31 Reducing stack by rule 25 (line 54): $1 = token CHAR () -> $$ = nterm DASH-DASH-CHAR () Stack now 0 2 6 12 26 Entering state 33 Reading a token: Next token is token '-' () Shifting token '-' () Entering state 19 Reducing stack by rule 24 (line 53): $1 = token '-' () -> $$ = nterm CHAR-CHAR-DASH () Stack now 0 2 6 12 26 33 Entering state 37 Reducing stack by rule 16 (line 45): $1 = nterm CHAR-CHAR-DASH () $2 = nterm DASH-DASH-CHAR () $3 = nterm CHAR-CHAR-DASH () RNG(a-b) -> $$ = nterm CHAR-unit-DASH () Stack now 0 2 6 12 Entering state 22 Reading a token: Next token is token ']' () Shifting token ']' () Entering state 14 Reducing stack by rule 26 (line 55): $1 = token ']' () -> $$ = nterm DASH-DASH-CLOS () Stack now 0 2 6 12 22 Entering state 16 Reducing stack by rule 8 (line 37): $1 = nterm DASH-DASH-CLOS () CHR(-) -> $$ = nterm DASH-contents-CLOS () Stack now 0 2 6 12 22 Entering state 29 Reducing stack by rule 10 (line 39): $1 = nterm CHAR-unit-DASH () $2 = nterm DASH-contents-CLOS () -> $$ = nterm CHAR-contents-CLOS () Stack now 0 2 6 12 Entering state 20 Reducing stack by rule 21 (line 50): -> $$ = nterm CLOS-CLOS-epsi () Stack now 0 2 6 12 20 Entering state 28 Reducing stack by rule 7 (line 36): $1 = nterm OPEN-OPEN-CHAR () $2 = nterm CHAR-contents-CLOS () $3 = nterm CLOS-CLOS-epsi () -> $$ = nterm OPEN-class-epsi () Stack now 0 2 6 Entering state 10 Reducing stack by rule 5 (line 34): $1 = token '[' () $2 = nterm OPEN-class-epsi () -> $$ = nterm class () Stack now 0 2 Entering state 7 Reading a token: Next token is token '\n' () Shifting token '\n' () Entering state 13 Reducing stack by rule 2 (line 24): $1 = nterm input () $2 = nterm class () $3 = token '\n' () -> $$ = nterm input () Stack now 0 Entering state 2 Reading a token: Now at end of input. Shifting token $end () Entering state 4 Stack now 0 2 4 Cleanup: popping token $end () Cleanup: popping nterm input () )。 (-)。相反,我编写了两个%--规则,有效地扩展了上面的unit: CHAR '-' any生产。

  3. 下面描述的转换将LR(k)语法转换为LR(1)语法,或将LALR(k)语法转换为LALR(1)语法。我用unit作为这两种情况的简写。这是不精确的,因为该转换还将SLR(k)语法转换为SLR(1)语法。

  4. 在这里的示例中,any为1,并且没有ε产生,因此我们可以简单地将(LA)LR(k)设置为语法符号。但是在一般情况下,给定符号k-1的派生可能短于FIRST。更为精确的表述是,Vk-1中的元素,而followFOLLOWk−1(V)中的元素,使用first作为并置运算符。