【发布时间】:2020-02-21 18:38:24
【问题描述】:
如何在 yacc/bison 中实现#define?
例如:
#define f(x) x*x
如果 f(x) 出现在任何函数中,则将其替换为 宏替换参数“x”。
例如,f(3) 将替换为 3*3。该宏也可以调用另一个宏。
【问题讨论】:
标签: c compiler-construction bison flex-lexer yacc
如何在 yacc/bison 中实现#define?
例如:
#define f(x) x*x
如果 f(x) 出现在任何函数中,则将其替换为 宏替换参数“x”。
例如,f(3) 将替换为 3*3。该宏也可以调用另一个宏。
【问题讨论】:
标签: c compiler-construction bison flex-lexer yacc
通常不可能在解析器中进行宏扩展,至少 C 风格的宏不能,因为 C 风格的宏扩展不尊重语法。例如
#define IF if(
#define THEN )
是合法的(尽管恕我直言,风格很糟糕)。但是为了在语法内部处理,有必要允许宏标识符出现在输入中的任何位置,而不仅仅是可能需要标识符的位置。对语法的必要修改将使其可读性大大降低,并且很可能会引入解析器操作冲突。 [注1]
或者,您可以在词法分析器中进行宏扩展。词法分析器不是解析器,但是解析 C 风格的宏调用不需要太多复杂性,如果不允许使用宏参数,那就更简单了。这就是 Flex 在其正则表达式中处理宏替换的方式。 (例如{identifier}。[注 2] 由于 Flex 宏只是原始字符序列,而不是像 C 样式宏那样的标记列表,因此可以通过将替换文本推回输入流来处理它们。(F)lex为此提供了unput 特殊操作。unput 将一个字符推回输入流中,因此如果要推整个宏替换,则必须 unput 一次一个字符,从后向前这样最后一个字符 unput 是之后要读取的第一个字符。
这是可行的,但很难看。即使是 C 预处理器提供的小功能列表,它也不能真正扩展。而且它违反了软件设计的基本原则,即每个组件只做一件事(以便它能够做好)。
所以剩下最常见的方法是添加一个单独的宏处理器组件,这样解析就不是词法扫描/语法分析,而是词法扫描/宏扩展/语法分析。 [注3]
在词法分析器和句法分析器之间工作的 C 风格的宏处理器本身可以用 Bison 编写。正如我上面提到的,解析要求通常很少,但仍有解析工作要做,Bison 大概已经是项目的一部分。虽然我不知道有任何宏处理器(除了我自己编写的概念验证程序)可以做到这一点,但我认为这是一个非常灵活的解决方案。特别是,Bison 句法分析阶段可以使用 push-parser 来实现,这避免了生成整个宏扩展令牌流以使其可供传统 pull-parser 使用的需要。
不过,这并不是设计宏的唯一方法。确实,它有很多缺点,因为宏扩展并不卫生,既不尊重语法也不尊重范围。可能任何使用过 C 宏的人都曾被这些问题所困扰。最简单的表现形式是定义一个宏,例如:
#define NEXT(a) a + 1
然后写
int x = NEXT(a) * 3;
这不会产生预期的结果(除非预期的结果违反了最后一条语句的句法形式)。此外,任何需要使用局部变量的宏扩展迟早会因为意外的名称冲突而产生不正确的扩展。卫生宏扩展旨在通过将宏扩展视为对语法树而不是令牌流的操作来解决这些问题,从而使解析范式成为词法扫描/语法分析/宏扩展(解析树的)。对于该操作,合适的工具可能是某种树解析器。
此外,您还想从解析树中删除标记 Yacc/bison 确实有一个记录不充分的特性 YYBACKUP,它可能有助于实现此目的。我不知道这是否是它的预期用例之一;事实上,我不清楚它的预期用例是什么。
(f)lex 文档调用这些定义,但它们确实是宏,并且它们会遇到宏带来的所有常见问题,例如与周围语法的神秘交互。
另一种可能性是宏扩展/词法扫描/语法分析,可以使用像 M4 这样的宏处理器来实现。但这完全将宏与语言的其他部分分开。
【讨论】:
yacc 和 lex 最后生成 c 源代码。因此,您可以在解析器和词法分析器操作中使用宏。
实际的 #define 预处理器指令可以放在词法分析器和解析器文件的第一部分
%{
// Somewhere here
#define f(x) x*x
%}
这些部分将被逐字复制到生成的 c 源代码中。
【讨论】: