如果我正确理解了这个问题,它与解析工具几乎没有关系(但请参阅下面的注释),因为它是一个更普遍的问题的实例 - 链接多对多转换器 - 这是 @ 的一个实例987654321@.
简而言之,问题在于被链接的转换器不是一对一的,因此您不能简单地为第一个转换器提供一些输入,收集其相应的输出,然后将其输入第二个转换器。相反,您必须提供某种形式的控制流反转,其中消费者和生产者都可以在另一个进程正在执行时暂停他们的执行(保留他们自己的调用堆栈)。这个模型可以通过coroutines 轻松实现,但是C++ does not (yet) have such things。在其他语言中,比如 Go,这个问题是用 channels 解决的,但是这些还不是(还)native to C++。
这正是标准外壳“管道”运算符 (|) 解决的问题,它使用pipe 系统调用和多个进程在后台实现。 (链接的手册页中有一个示例。)
我希望您能在所有这些链接之间找到满足您需求的方法。
经过反思,为了让这个答案可能对后代更有用,值得注意的是,这个问题实际上与标准解析器生成器工具的限制有关。
本质上,yacc/bison 和 (f)lex 协同工作以生成一个工具,该工具从某个源逐块读取输入,并酌情调用用户定义的操作(标记器操作和归约操作的组合)。因此,您可以将控制流视为:
--> tool -->
/\
/ \
/ \
/ \
/-->read perform<-\
| chunk action |
| | | |
|____/ \____|
工具内部的控制流是封闭的,所以我们不能简单地暂停执行以等待输入,或提供部分“输出”(即动作后处理)。
但是,为了将两个或多个这些工具结合在一起,我们需要能够做到这一点。第二个进程的输入需要来自第一个进程的输出,但是第二个进程需要调用(something)来获取它的输入,而第一个进程确定调用(something)来处理它的输出。
因此,标准解决方案如答案的第一部分所述:通过在单独的进程/线程/光纤/协程/等中运行这两个工具,将它们与其控制流分离。通过管道/队列/通道/等进行通信。中间对象,管道/队列/通道,有一个有趣的特性,两端都可以“调用”,从而允许我们反转控制流。
但是如果工具不是调用+调用,而是调用+返回(调用某些东西来获取输入,但返回每个单独的输出)或调用+调用(调用每个块输入,并调用一些东西来处理每个输出块)。在任何一种情况下,我们都可以将两个或多个工具链接在一起。例如,在第二种情况下(调用+调用),我们将有:
/-->input---->tool1
|____/ |
/-->|
/ |---->tool2
|___/ |
/-->|
/ |---->output
|___/
这通常被称为“推送”模型,因为我们将连续的输入块“推送”到系统中,最终(通过回调)提供连续的输出操作。还有一种“拉”模型,上面描述为 call+return,在这种模型中,我们每次需要一些输出时都会调用系统,而当它需要更多输入来满足请求时,它会调用输入提供者:
/-->call<----tool2
|____/ |
/-->|
/ |<----tool1
|___/ |
/-->|
/ |<----input
|___/
(注意,在push模型中,我们调用tool1,推送下一个输入,而在pull模型中,我们调用tool2来获取下一个结果。)
找到实现推送模型的解析器是微不足道的。 Lemon 解析器一直都是这样工作的,bison 已经有一个推送 API 很长一段时间了。但这还不够好,除非我们有某种机制而不是 (f)lex 构建的扫描器来提供令牌流,因为我们要推送的是“输入块”,而不是一系列令牌。
此外,找到实现拉取模型的扫描器是微不足道的。 (F)lex 一直这样做;每次有新令牌时它都会返回。
但是要将两者粘合在一起形成一个推或拉复合材料,我们需要它们同时推动或同时拉动。
这应该不难。 (f)lex 和 yacc/bison 都不产生递归函数,因此在连续调用之间保存状态应该是直截了当的。 (F)lex 扫描仪已经做到了这一点,但有一个警告(如下所述)允许它们以拉模式运行。 Bison 的推送 API 会这样做,以便在推送模式下运行。
但是……
Bison 声称有一个“拉”API,以配合“推”API。但根据上面介绍的模型,它并不是真正的“拉”API,因为它不会从每个动作中返回(就像 (f)lex 扫描仪通常那样)。生成允许从动作返回的代码意味着需要在调用动作之前保存状态,因为 C 不允许拦截 return 语句,并且(据我所知)野牛没有不要那样做。 (但添加起来可能并不难。)
另一方面,(f)lex 扫描器几乎拥有所有必要的状态基础设施,但有一个小问题:状态中最重要的部分,即被模拟的状态机的实际状态,没有被保存.结果是您只能从令牌的开头运行状态机。由于输入块不落在令牌边界上,为了处理新的输入块,扫描器必须从重新扫描当前部分扫描的令牌开始。如果输入块很小,这将变得非常低效。如果提供了推送接口,则期望可以使用连续字符调用它(以模拟交互模式),这将自动导致扫描时间与令牌长度成二次方。
事实上,每当 flex 扫描器需要重新填充其缓冲区时,这实际上都会(在内部)发生,因此较小的缓冲区大小(或较大的令牌大小)会产生非常糟糕的性能。幸运的是,在典型的语言处理器中,这些都不是真的:标记很小,缓冲区很大,并且重新扫描很少。
保存状态机的状态实际上并不难。不这样做是一个深思熟虑的选择,为了在内部扫描循环中保存一个寄存器,因为 flex 的原始硬件目标是一个寄存器匮乏的机器。对于许多现代架构,这不是必需的,实际上消除缓冲区重新填充时的重新扫描并不难。因此,制作带有推送接口的 flex 版本应该不难,但据我所知,这还没有完成。