如果您想要一种简单的方法来编写解析器,或者您的空间有限,您应该手动编写递归下降解析器;这些本质上是LL(1) 解析器。这对于像 Basic 一样“简单”的语言尤其有效。 (我在 70 年代做过其中的几个!)。好消息是这些不包含任何库代码;就是你写的。
如果您已经掌握了语法,它们很容易编写代码。
首先,您必须摆脱左递归规则(例如 X = X Y )。
这通常很容易做到,所以我把它作为练习。
(对于列表形成规则,您不必这样做;
见下文讨论)。
那么如果你有如下形式的 BNF 规则:
X = A B C ;
为规则(X、A、B、C)中的每个项目创建一个返回布尔值的子例程
说“我看到了相应的语法结构”。对于 X,代码:
subroutine X()
if ~(A()) return false;
if ~(B()) { error(); return false; }
if ~(C()) { error(); return false; }
// insert semantic action here: generate code, do the work, ....
return true;
end X;
同样适用于 A、B、C。
如果令牌是终端,请编写检查代码
构成终端的字符串的输入流。
例如,对于数字,检查输入流是否包含数字并推进
输入流光标过去的数字。这特别容易,如果你
正在解析缓冲区(对于 BASIC,您往往一次得到一行)
通过简单地推进或不推进缓冲区扫描指针。
这段代码本质上是解析器的词法分析器部分。
如果您的 BNF 规则是递归的……别担心。只需编写递归调用。
这处理语法规则,如:
T = '(' T ')' ;
这可以编码为:
subroutine T()
if ~(left_paren()) return false;
if ~(T()) { error(); return false; }
if ~(right_paren()) { error(); return false; }
// insert semantic action here: generate code, do the work, ....
return true;
end T;
如果你有一个 BNF 规则和一个替代:
P = Q | R ;
然后用替代选择编码 P:
subroutine P()
if ~(Q())
{if ~(R()) return false;
return true;
}
return true;
end P;
有时您会遇到列表形成规则。
这些往往是递归的,这种情况很容易处理。基本思想是使用迭代而不是递归,这样可以避免以“显而易见”的方式执行此操作的无限递归。
示例:
L = A | L A ;
您可以使用迭代将其编码为:
subroutine L()
if ~(A()) then return false;
while (A()) do { /* loop */ }
return true;
end L;
通过这种方式,您可以在一两天内编写数百条语法规则。
还有更多细节需要填写,但这里的基础知识应该绰绰有余了。
如果您真的空间有限,您可以构建一个虚拟机来实现这些想法。这就是我在 70 年代所做的,那时你可以获得 8K 16 位字。
如果您不想手动编写代码,可以使用 metacompiler (Meta II) 将其自动化,它产生基本相同的内容。这些是令人兴奋的技术乐趣,并且确实可以消除所有工作,即使对于大型语法也是如此。
2014 年 8 月:
我收到很多关于“如何使用解析器构建 AST”的请求。有关这方面的详细信息,基本上详细说明了这个答案,请参阅我的另一个 SO 答案https://stackoverflow.com/a/25106688/120163
2015 年 7 月:
有很多人想要编写一个简单的表达式求值器。您可以通过执行上面“AST builder”链接所建议的相同类型的事情来做到这一点;只做算术而不是构建树节点。
这是an expression evaluator done this way。
2021 年 10 月:
值得注意的是,当您的语言没有递归下降处理不好的复杂性时,这种解析器就可以工作。我提供了两种复杂性:a) 真正模棱两可的解析(例如,解析短语的方法不止一种)和 b) 任意长的前瞻(例如,不受常数限制)。在这些情况下,递归下降变成了进入地狱的递归下降,是时候获得一个可以处理它们的解析器生成器了。请参阅我的简历,了解使用 GLR 解析器生成器处理 50 多种不同语言的系统,包括所有这些复杂性甚至到了荒谬的程度。