我想解析以下内容:
name:name
其中名称以alnum开头和结尾,并且可以包含alnum和space内部的任意组合。它们也可能是空白的。我的规则是:
identifier = alnum (space* alnum)*;
name = (identifier | zlen) >sName $pName %fName;
名称可以用冒号分隔,也可以选择名称和冒号之间的空格。我的规则是:
sep = space* ":" space*;
main := name sep name;
这不起作用,因为显然space*
中的identifier
和space*
中的sep
会混淆解析器。我最终在名称的每个空格中执行了fName
操作。
如果我将sep更改为:
sep = ":";
然后一切都很好。如何修改这些规则以便解析器完成我想要的操作?
此问题的源代码:https://gist.github.com/1661150
答案 0 :(得分:10)
这类问题有两种基本解决方案。
在这种情况下,我会选择混合方法。使用操作记录name
的开始和结束位置:这些操作可以安全地执行多次,因为它们只是记录位置。一旦确定已超出名称,请执行仅执行一次的其他操作。
/* C code */
char *name_start, *name_end;
/* Ragel code */
action markNameStart { name_start = p; }
action markNameEnd { name_end = p; }
action nameAction {
/* Clumsy since name is not nul-terminated */
fputs("Name = ", stdout);
fwrite(name_start, 1, name_end - name_start, stdout);
fputc('\n', stdout);
}
name = space* %markNameStart
(alnum+ %markNameEnd <: space*)+
%nameAction ;
main := name ":" name ;
这里,name
的语法包括任意空格和至少一个字母数字字符。遇到第一个字母数字字符时,其位置将保存在name_start
中。每当字母数字字符的运行结束时,后续字符的位置将保存在name_end
中。 <:
在技术上是不必要的,但它会减少执行markNameEnd
操作的频率。
请确保不要在任何空格旁边放置这样的表达式。
我没有测试过上面的代码。在使用之前,您应该查看状态机的Graphviz可视化。
使用原始代码,我们假设输入如下:
Hello world : Goodbye world
Ragel机器从左向右扫描,找到name
的开头,并扫描字母数字字符。
Hello world : Goodbye world ↑
下一个角色是一个空格。所以我们要么在单词中遇到空格,要么在单词结尾后遇到第一个空格。 Ragel如何选择?
Ragel同时选择这两个选项。这非常重要。 Ragel试图模拟非确定性有限自动机,但由于您的计算机是确定性的,最简单的方法是将NFA转换为DFA,它可以并行模拟无限数量的NFA。由于NFA具有有限数量的状态(因此名称),因此DFA也具有有限数量的状态,因此该技术起作用。
遇到空格后,您有一个NFA处于以下状态,正在寻找name
的其余部分:
identifier = alnum (space* alnum)*; ↑ main := name sep name; ↑
第二个NFA处于以下状态,它假定name
已经结束(并且此NFA过早地执行了fName
操作“):
sep = space* ":" space*; ↑ main := name sep name; ↑
对你来说很明显,对我来说很明显只有第一个NFA是正确的。但是使用Ragel创建的机器一次只能看一个字符,他们不会向前看,看看哪个选项是正确的。第二个NFA最终将遇到一个字母数字字符,它期望看到":"
,并且由于这是不允许的,第二个NFA将会消失。
以下是%
的说明:
expr % action
离开动作操作符将一个动作排队,以嵌入到转出的过渡中 通过最终状态的机器。
对于不一定有助于成功解析的转换执行操作。有关Ragel中非确定性的更多信息,请参阅Ragel指南,第4章“控制非确定性”,尽管第4章中的技术在这种特殊情况下无法帮助您,因为您机器中的操作只能通过未绑定的前瞻来消除歧义,在有限状态机中是不允许的。