bison 生成的标头中最重要的是用于标识标记类型的枚举值(这些值是通过词法操作返回到解析器的值)。
标头还声明了YYSTYPE
语义类型和变量yylval
(具有该类型),用于将每个标记的语义值传达给解析器。 (至少,对于具有语义值的标记而言。)类似地,如果解析器使用位置信息,则标头定义YYLTYPE
位置类型和变量yylloc
那种类型的。
由于标头依赖关系不能是循环的,因此解析器对扫描器没有任何标头依赖关系。正是由于这个原因,您的 bison 输入文件必须包含以下声明yylex
.
这对于解析器和扫描器之间的经典接口来说很好,它们使用全局变量进行通信。它也或多或少地与可重入(“纯”)解析器一起工作,其中语义值(和位置,如果使用)通过参数传递给yylex
,尽管事实上声明yylex
不是自动的更烦人。
当扫描仪也可重入时,它就会开始崩溃。在这种情况下,解析器必须使用类型不透明的扫描器上下文对象调用扫描器yyscan_t
. yyscan_t
“属于”扫描仪,因此只能在扫描仪的标头中定义。但如上所述,这将导致循环依赖链。这揭示了传统模型的弱点:解析器是扫描器的客户端,因此扫描器依赖解析器来定义基本数据结构的事实是依赖倒置。
这是一个非常现实的问题,因为可重入扫描器的公共接口包含其原型需要解析器特定数据类型的函数(YYSTYPE
and YYLTYPE
),而解析器原型几乎肯定需要接受扫描器上下文对象作为参数,因此如果没有扫描器特定的数据类型就无法声明它yyscan_t
.
解决这个问题的通常方法是通过注意到以下内容来打破封装:yyscan_t
简单来说就是一个void*
,因此可以在解析器中这样声明,从而避免解析器需要#include
扫描仪标头(只要它不需要访问该标头中声明的任何其他公共方法)。
在我看来,一个不太难看的解决方案是完全避免这种特定的配置。 Bison 允许您请求可重入的“推送解析器”,并且可以与可重入扫描器一起使用,而不会出现上述任何复杂情况。
在推送解析器模型中,扫描器是解析文件时调用的顶级函数,扫描器每次识别令牌时都会调用解析器。标头依赖关系不再是循环的,因为解析器不需要了解有关扫描仪上下文对象的任何信息,也不需要了解有关扫描仪原型的任何信息。yylex()
。扫描器现在是解析器的客户端,因此对解析器具有自然的标头依赖性,因此解析器定义令牌枚举以及语义和位置数据类型的事实不再是例外。
除了简化两个组件之间的标头依赖性之外,推送解析器通常还简化扫描器本身内部的控制流。在许多用例中,单个扫描仪模式将导致识别多个令牌。在传统模型中,扫描器必须保留一个令牌队列,并在解析器调用时一次释放一个令牌。但在推送模型中,扫描器操作可以简单地多次调用解析器,每个识别的标记一次。该模型由 Lemon 解析器生成器(sqlite3 的一部分)普及,随后由包括 bison 在内的其他解析器生成器实现。