编写可在 8 位嵌入式系统上使用的解析器,如 Flex/Bison

Posted

技术标签:

【中文标题】编写可在 8 位嵌入式系统上使用的解析器,如 Flex/Bison【英文标题】:Writing a parser like Flex/Bison that is usable on 8-bit embedded systems 【发布时间】:2011-01-15 19:13:34 【问题描述】:

我正在使用 avr-gcc 工具链为类似 BASIC 的简单语言编写一个小型解释器,作为在 C 语言 AVR 微控制器上的练习。

如果我写这个是为了在我的 Linux 机器上运行,我可以使用 flex/bison。既然我将自己限制在 8 位平台,我将如何编写解析器?

【问题讨论】:

有你打算使用的特定芯片吗?它有多少 ROM/RAM? 更新到 @mre 的链接。 Embedded.com 已经删除了他们的 URL。 (embedded.com/design/prototyping-and-development/4024523/…) 似乎只有 stack laguages (forth & Co) 有机会在 2KB RAM 上使用,内核闪存 【参考方案1】:

如果您想要一种简单的方法来编写解析器,或者您的空间有限,您应该手动编写递归下降解析器;这些本质上是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://***.com/a/25106688/120163

2015 年 7 月:

有很多人想要编写一个简单的表达式求值器。您可以通过执行上面“AST builder”链接所建议的相同类型的事情来做到这一点;只做算术而不是构建树节点。 这是an expression evaluator done this way。

2021 年 10 月:

值得注意的是,当您的语言没有递归下降处理不好的复杂性时,这种解析器就可以工作。我提供了两种复杂性:a) 真正模棱两可的解析(例如,解析短语的方法不止一种)和 b) 任意长的前瞻(例如,不受常数限制)。在这些情况下,递归下降变成了进入地狱的递归下降,是时候获得一个可以处理它们的解析器生成器了。请参阅我的简历,了解使用 GLR 解析器生成器处理 50 多种不同语言的系统,包括所有这些复杂性甚至到了荒谬的程度。

【讨论】:

是的,为一种简单的语言手动滚动递归下降解析器并不难。记得尽可能优化尾调用——当你只有几千字节的 RAM 时,堆栈空间很重要。 All:是的,您可以进行尾调用优化。这无关紧要,除非您希望在解析的代码中嵌套非常深;对于 BASIC 代码行,很难找到深度超过 10 个括号的表达式,并且您始终可以输入深度限制计数来启动。诚然,嵌入式系统往往堆栈空间较小,所以至少要注意这里的选择。 @Mark:可能是 2012 年,但我参考的 1965 年技术论文现在和当时一样好,而且非常好,尤其是在你不知道的情况下。 @IraBaxter:我并不是在暗示你的答案已经过时,我是在指出你打错了。你写了“编辑 2011 年 3 月 16 日”。 通过空字符串,我想你是说你有一个语法规则,比如 X -> Y | ε。在这种情况下,您为 X 编写一个调用 Y 的子例程;如果找到 Y,则返回成功。如果它没有找到 Y,它仍然返回 true。.【参考方案2】:

我已经实现了一个针对ATmega328p 的简单命令语言的解析器。该芯片有 32k ROM,只有 2k RAM。 RAM 绝对是更重要的限制——如果您还没有绑定到特定芯片,请选择具有尽可能多 RAM 的芯片。这将使您的生活更轻松。

起初我考虑使用 flex/bison。我决定反对这个选项有两个主要原因:

默认情况下,Flex 和 Bison 依赖于一些标准库函数(尤其是 I/O),这些函数在 avr-libc 中不可用或工作方式不同。我很确定有支持的解决方法,但这是您需要考虑的一些额外工作。 AVR 有一个Harvard Architecture。 C 的设计并未考虑到这一点,因此即使是常量变量也会默认加载到 RAM 中。您必须使用特殊的宏/函数来存储和访问flash 和EEPROM 中的数据。 Flex & Bison 创建了一些相对大的查找表,它们会很快耗尽你的 RAM。除非我弄错了(这很有可能),否则您必须编辑输出源才能利用特殊的 Flash 和 EEPROM 接口。

在拒绝了 Flex & Bison 之后,我去寻找其他的生成器工具。以下是我考虑过的一些:

LEMON Ragel re2c

您可能还想看看Wikipedia's comparison。

最终,我最终手动编写了词法分析器和解析器。

对于解析,我使用了递归下降解析器。我认为Ira Baxter 已经完成了足够的工作来涵盖这个主题,并且网上有很多教程。

对于我的词法分析器,我为我的所有终端编写了正则表达式,绘制了等效的状态机图,并将其实现为一个巨大的函数,使用goto's 在状态之间跳转。这很乏味,但结果很好。顺便说一句,goto 是实现状态机的好工具——所有状态都可以在相关代码旁边有清晰的标签,没有函数调用或状态变量开销,而且速度差不多得到。 C 确实没有更好的构造来构建静态机器。

需要考虑的一点:词法分析器实际上只是解析器的一种特殊化。最大的区别是常规语法通常足以进行词法分析,而大多数编程语言(大部分)具有上下文无关语法。因此,实际上没有什么能阻止您将词法分析器实现为递归下降解析器或使用解析器生成器来编写词法分析器。它通常不如使用更专业的工具方便。

【讨论】:

轻微的挑剔,但 C 语言可以很好地处理 AVR 和哈佛架构。相反,gcc 编译器 不是为处理哈佛架构而设计的。创建 AVR 指令集时,硬件设计人员咨询了知名编译器供应商:web.archive.org/web/20060529115932/https://… 老实说,我没有跟上最新 C 标准的细节,但我的理解是 C99 为数据指定了单个地址空间,因此在哈佛架构上将常量放入程序内存中需要一些非标准的东西。该标准的“嵌入式 C”扩展确实提供了一种机制来处理多个不同地址空间中的数据。 open-std.org/JTC1/SC22/WG14/www/docs/n1169.pdf(第 37 页)【参考方案3】:

您可以在 Linux 上使用 flex/bison 及其原生 gcc 来生成代码,然后您将使用 AVR gcc 为嵌入式目标进行交叉编译。

【讨论】:

【参考方案4】:

GCC 可以交叉编译到各种平台,但您在运行编译器的平台上运行 flex 和 bison。他们只是吐出编译器随后构建的 C 代码。测试它以查看生成的可执行文件到底有多大。请注意,它们具有运行时库(libfl.a 等),您还必须交叉编译到您的目标。

【讨论】:

我仍然需要调查这些库的大小,这就是我首先提出这个问题的原因。我想要一些专门针对小型 MCU 的东西。

以上是关于编写可在 8 位嵌入式系统上使用的解析器,如 Flex/Bison的主要内容,如果未能解决你的问题,请参考以下文章

用纯 JavaScript 编写的用于嵌入式环境的 XML 解析器 [关闭]

使用位域解析网络数据包

支持 16 位地址的 I2c

第一章嵌入式系统基础1.4

如何用 C# 编写解析器? [关闭]

如何为 GraphQL Mutation 字段编写解析器