因此,我们想编写自己的解析器。手写解析器的最佳方法是什么?应该使用什么类型的解析器 - 递归下降已经被推荐;这样做正确吗?我们将用C#编写它,因此任何关于在该语言中编写解析器的教程都将不胜感激。
更新:我也对涉及F#的答案感兴趣 - 我一直在寻找在项目中使用它的理由。
type expr =
| Val of string
| Int of int
| Float of float
| Decr of expr
type stmt =
| Assign of string * expr
| While of expr * stmt
| Seq of stmt list
| IfThen of expr * stmt
| IfThenElse of expr * stmt * stmt
| Print of expr
type prog = Prog of stmt list
解析器的实现(部分省略)同样简洁:
let stmt, stmtRef = createParserForwardedToRef()
let stmtList = sepBy1 stmt (ch ';')
let assign =
pipe2 id (str ":=" >>. expr) (fun id e -> Assign(id, e))
let print = str "print" >>. expr |>> Print
let pwhile =
pipe2 (str "while" >>. expr) (str "do" >>. stmt) (fun e s -> While(e, s))
let seq =
str "begin" >>. stmtList .>> str "end" |>> Seq
let ifthen =
pipe3 (str "if" >>. expr) (str "then" >>. stmt) (opt (str "else" >>. stmt))
(fun e s1 optS2 ->
match optS2 with
| None -> IfThen(e, s1)
| Some s2 -> IfThenElse(e, s1, s2))
do stmtRef:= choice [ifthen; pwhile; seq; print; assign]
let prog =
ws >>. stmtList .>> eof |>> Prog
stmt
和ch
是解析器(parsers),sepBy1
是一个单态(monadic)解析器组合器,它接受两个简单的解析器并返回一个组合解析器。在这种情况下,sepBy1 p sep
返回一个解析器,用于解析一个或多个由sep
分隔的p
的出现。因此,您可以看到如何从简单的解析器快速组合出强大的解析器。 F#对重载操作符的支持还允许使用简明的中缀符号表示,例如顺序组合器和选择组合器可以指定为>>.
和<|>
。唯一可以由理智的人手写的解析器是递归下降法。话虽如此,手写自底向上的解析器仍然可行但非常不可取。
如果您打算使用递归下降法解析SQL语法,您必须验证SQL语法是否存在左递归(如有必要,则消除该递归),然后基本上为每个语法规则编写一个函数。请参见此处以获取更多参考信息。
递归下降法会给你最简单的方法,但我必须同意mouviciel的看法,学习flex和bison绝对值得。当你发现语法错误时,在flex / bison中修复语言定义要容易得多,而不是重写递归下降代码。
顺便提一下,C#解析器是使用递归下降法编写的,它往往非常强大。
我支持使用递归下降(LL1)的方法来解析语法。它们简单快捷,而且在我看来,很容易维护。
不过,请仔细检查您的语言,确保它是LL1。如果您有像C语言那样的语法,例如((((type))foo)[ ]),需要多次遍历括号才能确定您是否正在查看类型、变量或表达式,则使用LL1会非常困难,这时候自底向上的方法效果更好。
递归下降解析器确实是最好的、也可能是唯一能够手动构建的解析器。您仍然需要了解正式的无上下文语言是什么,并将您的语言放在正常形式中。我个人建议您删除左递归并将您的语言放在 Greibach Normal Form中。这样做后,解析器几乎就能自己编写。
例如,这个产生式:
A => aC
A => bD
A => eF
变得简单,像这样:
int A() {
chr = read();
switch char
case 'a': C();
case 'b': D();
case 'e': F();
default: throw_syntax_error('A', chr);
}
这里没有更难的情况(更困难的是确保你的语法处于完全正确的形式,但这使你拥有了你提到的控制权)。
Anton's Link 似乎也很棒。
如果你想手写代码,递归下降是最明智的选择。
你也可以使用表格解析器,但这将非常难以维护。
例如:
Data = Object | Value;
Value = Ident, '=', Literal;
Object = '{', DataList, '}';
DataList = Data | DataList, Data;
ParseData {
if PeekToken = '{' then
return ParseObject;
if PeekToken = Ident then
return ParseValue;
return Error;
}
ParseValue {
ident = TokenValue;
if NextToken <> '=' then
return Error;
if NextToken <> Literal then
return Error;
return(ident, TokenValue);
}
ParseObject {
AssertToken('{');
temp = ParseDataList;
AssertToken('}');
return temp;
}
ParseDataList {
data = ParseData;
temp = []
while Ok(data) {
temp = temp + data;
data = ParseData;
}
}
没有“最好的”方法。根据您的需求,您可能需要自下而上(LALR1)或递归下降(LLk)方法。例如这篇文章给出了个人喜欢LALR(1)(自下而上)而不是LL(k)的原因。然而,每种类型的解析器都有其优点和缺点。通常情况下,由于生成查找表作为有限状态机,LALR将更快。
为了选择适合您的工具和技术,请检查您的情况;熟悉相关的工具和技术。从一些LALR和LL维基百科文章开始并不是一个坏选择。在这两种情况下,您应该始终从指定BNF或EBNF语法开始。我更喜欢 EBNF,因为它更简洁。然而,我听说LL(k)更加灵活。我从未费心去自己了解。从我的一些解析器构建经验中,不管是LALR还是LL(k),选择最适合您需求的最好方法是先编写语法。我编写了自己的C++ EBNF RD解析器生成器模板库,使用了Lex/YACC并编写了一个小型R-D解析器。这跨越了15年左右的时间,我在其中最长的项目上花费的时间不超过2个月。