问题描述
我有一个 JavaCC 语法,其中有一个麻烦的部分可以简化为:
void Start(): {}
{
A()
<EOF>
}
void A(): {}
{
( "(" A() ")" | "biz" )
( B() | C() )*
}
void B(): {}
{ "foo" A() }
void C(): {}
{ "bar" A() }
当我编译上述语法时,JavaCC 会在 ( B() | C() )*
行警告选择冲突。我想了解两件事。第一个是为什么它认为在这种情况下存在冲突。 AFAICT 在每个点都应该能够根据当前令牌确定要采取的路径。第二个是如何摆脱警告。我似乎找不到放置 LOOKAHEAD
语句的正确位置。无论我把它放在哪里,我要么得到它不在选择点的警告,要么我继续得到同样的警告。这是我认为它可能喜欢的内容:
void Start(): {}
{
A()
<EOF>
}
void A(): {}
{
( "(" A() ")" | "biz" )
( LOOKAHEAD(1) B() | LOOKAHEAD(1) C() )*
}
void B(): {}
{ "foo" A() }
void C(): {}
{ "bar" A() }
但这仍然会产生警告。我还尝试了各种语义前瞻语句,但都没有运气。
我显然错过了一些东西,但我不知道是什么。 FWIW,在 ( B() | C() )*
之后放置任何令牌也“修复”了问题,所以我猜它与不知道如何退出该循环有关,但似乎应该只是在它不退出时参见“foo”或“bar”。生成的代码似乎是正确的,但如果这里有歧义,我没有看到,那么显然这无关紧要。
编辑 1..
经过一番摸索和查看 Java 语法后,我发现这让事情变得愉快:
void Start(): {}
{
A()
<EOF>
}
void A(): {}
{
( "(" A() ")" | "biz" )
(
LOOKAHEAD(2)
(B() | C())
)*
}
void B(): {}
{ "foo" A() }
void C(): {}
{ "bar" A() }
我仍然不完全清楚为什么需要额外的令牌来决定在循环中采用哪个选项(也许它真的不需要)。
编辑 2...
好的,我现在看到问题了,歧义不是在 B 和 C 之间,而是在是否进行树的深度优先或广度优先构造之间。因此,以下内容同样含糊不清:
void Start(): {}
{
A()
<EOF>
}
void A(): {}
{
( "(" A() ")" | "biz" )
(B())*
}
void B(): {}
{ "foo" A() }
从 *
切换到 ?
按照建议解决歧义。
如果我们将 B 改为 `{ "foo" A() "end" } 也可以解决这个问题,因为 B 有一个明确的结尾。现在假设我们这样做:
void Start(): {}
{
A()
<EOF>
}
void A(): {}
{
( "(" A() ")" | "biz" )
( B() | C() )*
}
void B(): {}
{ "foo" A() "end" }
void C(): {}
{ "bar" A() }
在这里,我希望 C 会存在同样的问题,但 JavaCC 仍然报告不明确的前缀是“foo”。这只是一个报告错误吗?当然,在这种情况下使用 ?
是不可能的,因为您无法匹配连续的 B(我们想要的)。 FWIW,此处生成的代码生成深度优先树,因为这是我想要的,所以可能足以抑制警告。
解决方法
你的语法有歧义。考虑输入
biz foo biz foo biz EOF
我们有以下两个最左边的推导。
第一个是
Start
=> ^^^^^ Expand
A EOF
=> ^ Expand
( "(" A ")" | "biz") ( B | C)* EOF
=> ^^^^^ choose
"biz" ( B | C )* EOF
=> ^^^^^^^^^^ unroll and choose the B
"biz" B ( B | C )* EOF
=> ^ expand
"biz" "foo" A ( B | C )* EOF
=> ^ expand
"biz" "foo" ( "(" A ")" | "biz") ( B | C)* ( B | C )* EOF
=> ^^^^^ choose
"biz" "foo" "biz" ( B | C)* ( B | C )* EOF
=> ^^^^^^^^^^ unroll and choose the B
"biz" "foo" "biz" B ( B | C)* ( B | C )* EOF
=> ^ expand
"biz" "foo" "biz" "foo" A ( B | C)* ( B | C )* EOF
=> ^ expand
"biz" "foo" "biz" "foo" ( "(" A ")" | "biz") ( B | C)* ( B | C)* ( B | C )* EOF
=> ^^^^^ choose
"biz" "foo" "biz" "foo" "biz" ( B | C)* ( B | C)* ( B | C )* EOF
=> ^^^^^^^^^ Terminate
"biz" "foo" "biz" "foo" "biz" ( B | C)* ( B | C )* EOF
=> ^^^^^^^^^ Terminate
"biz" "foo" "biz" "foo" "biz"( B | C )* EOF
=> ^^^^^^^^^ Terminate
"biz" "foo" "biz" "foo" "biz" EOF
对于第二个推导,一切都与第一个相同,直到解析器必须决定是否进入第二个循环为止。
Start
=>* As above
"biz" "foo" "biz" ( B | C)* ( B | C )* EOF
=> ^^^^^^^^^ Terminate!! (Previously it was expand)
"biz" "foo" "biz" ( B | C )*
=> ^^^^^^^^^^ Unroll and choose B
"biz" "foo" "biz" B ( B | C )* EOF
=> ^ Expand
"biz" "foo" "biz" "foo" A ( B | C )* EOF
=> ^ Expand
"biz" "foo" "biz" "foo" ( "(" A ")" | "biz") ( B | C)* ( B | C )* EOF
=> ^^^^^ Choose
"biz" "foo" "biz" "foo" "biz" ( B | C)* ( B | C )* EOF
=> ^^^^^^^^^ Terminate
"biz" "foo" "biz" "foo" "biz"( B | C )* EOF
=> ^^^^^^^^^ Terminate
"biz" "foo" "biz" "foo" "biz" EOF
就解析树而言,有以下两种可能。左边的解析树来自第一个推导。右边的解析树来自第二个
Start --> A -+---------------------------> biz <--------------+- A <-- Start
| |
+-> B -+--------------------> foo <------+-- B <-+
| | |
+-> A -+-------------> biz <- A <-+ |
| |
+-> B -+------> foo <------+-- B <-+
| |
+-> A -> biz <- A <-+
当你有选择时
LOOKAHEAD(3) X() | Y()
这意味着最多提前 3 个标记来尝试确定 X()
是否错误。如果接下来的 3 个标记显示 X()
是错误的,解析器使用 Y()
,否则使用 X()
。当您的语法含糊不清时,再多的前瞻性也无法解决冲突。对于模棱两可的输入,向前看无助于解析器确定哪个选择是“正确的”,哪个是“错误的”,因为它们是“正确的”。
那么为什么当您输入“LOOKAHEAD”指令时,JavaCC 会停止警告。这不是因为前瞻问题已经解决。这是因为当您放入一个前瞻指令时,JavaCC 总是停止发出警告。假设您知道自己在做什么,即使您不知道。
通常处理前瞻问题的最佳方法是将语法重写为无歧义和 LL(1)。
那你该怎么办?我不确定,因为我不知道您喜欢哪种解析树。如果是左边的那个,我想将 * 更改为 ?将解决问题。
如果你喜欢右边的解析树,我觉得下面的语法就可以了
void Start(): {}
{
A()
<EOF>
}
void A(): {}
{
SimpleA()
( B() | C() )*
}
void SimpleA() : {}
{
"(" A() ")" | "biz"
}
void B(): {}
{ "foo" SimpleA() }
void C(): {}
{ "bar" SimpleA() }