我正在编写自己的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-]
有效,我应该在解析器生成器中查找错误吗?
答案 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
,其中first
和follow
是k-1
和FIRSTk−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节中对算法的描述,应该在任何优秀的学术机构中都可以找到。库。
野牛像许多解析器生成器一样,使您可以编写带有单引号的单字符标记,这使语法更具可读性,恕我直言。
为了简化以下步骤,我避免定义
任意: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
生产。
下面描述的转换将LR(k)语法转换为LR(1)语法,或将LALR(k)语法转换为LALR(1)语法。我用unit
作为这两种情况的简写。这是不精确的,因为该转换还将SLR(k)语法转换为SLR(1)语法。
在这里的示例中,any
为1,并且没有ε产生,因此我们可以简单地将(LA)LR(k)
设置为语法符号。但是在一般情况下,给定符号k-1
的派生可能短于FIRST
。更为精确的表述是,V
是k-1
中的元素,而follow
是FOLLOWk−1(V)
中的元素,使用first
作为并置运算符。