混合多个 lex/yacc 组合

mixing multiple lex/yacc combinations

本文关键字:yacc 组合 lex 混合      更新时间:2023-10-16

我有一个lex/yacc项目,它可以很好地完成一件事。我有另一个 lex yacc 它做另一件事非常好.这些 yacc 的主要部分指定了输入和输出文件,在回调中我将结果放在输出文件中。 如何将第一个 lex yacc 的输出提供给另一个 lex yacc 的输入。我不想生成中间文件并将输出文件和输入推送到第二个程序。我想改变的那部分.问题是 lex 的计数,yacc 可以完美地完成 1 件事,这是巨大的并且正在增加(目前为 100),因此我正在寻找一种可以重用的通用策略。 我正在寻找一个通用解决方案,其中所有 lex/yacc 对构成我可以使用的单个项目的一部分。有什么建议吗?

如果我正确理解这个问题,它与解析工具关系不大(但请参阅下面的注释),因为它是一个更普遍的问题的实例 - 链接多对多转换器 - 这是生产者 - 消费者问题的实例。

简而言之,问题在于被链接的变压器不是一对一的,因此您不能简单地为第一个提供一些输入,收集其相应的输出,然后将其馈送到第二个变压器中。相反,您必须提供某种形式的控制流反转,其中使用者和生产者都可以在另一个进程正在执行时暂停其执行(保留自己的调用堆栈)。这个模型可以很容易地通过协程实现,但C++还没有这样的东西。在其他语言中,如 Go,这个问题可以通过通道解决,但同样,这些通道(还)不是C++原生的。

这正是标准外壳"管道"运算符(|)解决的问题,该运算符在引擎盖下使用管道系统调用和多个进程实现。(链接的手册页中有一个示例。

我希望在所有这些链接之间,您可以找到满足您需求的方法。


经过思考,为了使这个答案可能对后代更有用,值得注意的是,这个问题实际上确实与标准解析器生成器工具的限制有关。

从本质上讲,yacc/bison和(f)lex协同工作以生成一个工具,该工具逐块读取来自某个源的输入,并根据需要调用用户定义的操作(分词器操作和缩减操作的组合)。因此,您可以将控制流查看为:

--> tool -->
/
/  
/    
/      
/-->read     perform<-
|   chunk     action  |
|     |          |    |
|____/           ____|

工具内部的控制流是密封的,因此我们不能简单地暂停执行以等待输入或提供部分"输出"(即操作后处理)。

但是,为了将两个或多个这些工具组合在一起,我们需要能够做到这一点。第二个进程的输入需要来自第一个进程的输出,但第二个进程需要调用(某物)来获取其输入,而第一个进程被确定为调用(某物)来处理其输出。

因此,标准解决方案如答案的第一部分所述:通过在单独的进程/线程/光纤/协程/等中运行它们,将它们与其控制流分离,这些进程/线程/光纤/协程/等通过管道/队列/通道/等进行通信。中间对象,管道/队列/通道,具有有趣的功能,即两端都可以"调用",从而允许我们反转控制流。

但是,如果工具不是call+call,而是call+return(调用某些东西来获取输入,但返回每个单独的输出)或called+call(用每个输入块调用,并调用一些东西来处理每个输出块),那么整个问题就可以避免。在这两种情况下,我们都可以将两个或多个工具链接在一起。例如,在第二种情况下(调用+调用),我们将有:

/-->input---->tool1
|____/          |
/-->|
/    |---->tool2
|___/        |    
/-->|
/    |---->output
|___/

这通常被称为"推送"模型,因为我们将连续的输入块"推送"到系统中,最终(通过回调)提供连续的输出操作。还有"拉"模型,上面描述为 call+return,其中我们每次需要一些输出时调用系统,每当它需要更多输入来满足请求时,它都会调用输入提供者:

/-->call<----tool2
|____/         |
/-->|
/    |<----tool1
|___/        |    
/-->|
/    |<----input
|___/

(请注意,在推送模型中,我们调用tool1,推送下一个输入,而在拉模型中,我们调用 tool2 来获取下一个结果。

找到实现推送模型的解析器是微不足道的。Lemon 解析器一直以这种方式工作,而 bison 已经有一段时间了。但这还不够好,除非我们有某种机制而不是 (f)lex 构建的扫描器来提供令牌流,因为我们想要推送的是"输入块",而不是一系列令牌。

此外,找到实现拉动模型的扫描仪是微不足道的。(F)lex一直这样做;每次有新令牌时,它都会返回。

但是要将两者粘合在一起形成一个推或拉复合物,我们需要它们同时推或同时拉动。

这应该不难。(f)lex 和 yacc/bison 都不产生递归函数,因此在连续调用之间保存状态应该是直截了当的。(F)lex扫描仪已经这样做了,但有一个警告(如下所述),允许它们在拉动模式下工作。Bison的推送API这样做是为了在推送模式下行动。

但。。。

Bison声称有一个"拉"API,与"推"API一起使用。但根据上面介绍的模型,它并不是一个真正的"拉取"API,因为它不会从每个操作返回(就像(f)lex扫描仪通常所做的那样)。生成允许从操作返回的代码意味着在调用操作之前需要保存状态,因为 C 不允许拦截return语句,而且(据我所知)bison不会这样做。(但添加起来可能并不难。

另一方面,(f)lex 扫描程序几乎拥有所有必要的状态基础结构,但有一个小问题:状态中最重要的部分,即正在模拟的状态机的实际状态,没有被保存。结果是,您只能从令牌的开头运行状态机。由于输入块不落在令牌边界上,因此为了处理新的输入块,扫描程序必须首先重新扫描当前部分扫描的令牌。如果输入块很小,这将变得非常低效。如果提供推送接口,则期望可以使用连续字符调用它(以模拟交互模式),这将自动导致令牌长度的扫描时间二次元。

事实上,每次 flex 扫描程序需要重新填充其缓冲区时,这种情况实际上都会(在内部)发生,因此较小的缓冲区大小(或较大的令牌大小)可能会产生非常糟糕的性能。幸运的是,在典型的语言处理器中,这两件事都不是真的:令牌很小,缓冲区很大,重新扫描的频率很低。

保存状态机的状态实际上并不困难。不这样做是经过深思熟虑的选择,目的是在内部扫描循环中保存一个寄存器,因为 flex 的原始硬件目标是一台寄存器匮乏的机器。对于许多现代架构,这不是必需的,实际上在缓冲区重新填充时消除重新扫描并不困难。因此,使用推送接口生成 flex 版本应该不难,但据我所知,这还没有完成。