注意:您的直接问题——“为什么我的语法会产生语法错误”——如果没有看到您的语法,就无法回答。所以这个答案集中在另一个你真正需要理解的问题上:“为什么我不能在if 语句的操作中使用lines 的语义值。
当产品的右侧被识别时,与产品相关的动作被执行。为了识别右手边,所有包含的非终结符必须已经被识别,因此它们的动作在产生式动作运行之前已经执行。
因此,您无法在生产中决定是否执行其子项的操作。那需要时间旅行。
一个非终结符可能有一个相关的语义值,并且这个值至少在最初是由被识别的产生式的动作计算出来的。操作函数通过将其分配给$$ 来注册此值。如果操作没有分配给$$,则非终结符没有值。如果没有动作,则使用{ $$ = $1; }。
Bison/yacc 生成的 C 程序必须符合 C 规则。在 C 中,每个值都有一个类型,编译器必须知道该类型。与 Python 等语言不同,您不能声明类型稍后确定的变量。 int i; 表示i 是int。当程序运行时,它不能将i 更改为double。作为int 它诞生了,作为int 它将会死去。
Bison/yacc 非终结符(有时称为“语法变量”)没有什么不同(或只是略有不同)。保存值的非终结符必须具有编译器(和解析器生成器)已知的类型。变量的类型不能在以后确定,它不能在解析器的两次不同执行之间变化。
Bison/yacc 实际上使用 C 联合类型来实现这一点,它有效地允许多个不同的变量使用相同的内存(但不能同时使用)。联合并不能真正避免在编译时必须知道值的类型,因为您只能使用特定联合成员引用联合的值。该联合成员有一个类型,就像任何其他变量一样。所以当你使用联合时,你实际上有两个任务:你必须给每个成员一个固定的类型,和你需要记住联合的哪些成员当前正在使用。由于编译器不知道,它不会帮助你,也不会修复你的错误。与编写 C 程序的许多方面一样,您只能靠自己。如果您不犯错误,该程序将起作用。如果你确实犯了错误,几乎任何事情都可能发生。
Bison/yacc 可以帮助解决使用联合的一些不便。它首先出于内部目的使用联合:各种活动非终结符的语义值存储在堆栈中,堆栈是给定类型的 C 值数组。通过使用联合类型,bison/yacc 可以使用堆栈上不同联合值的不同成员,只要它知道每个堆栈槽的哪个成员正在使用。而且,碰巧它确实知道这一点,因为它知道每个堆栈槽对应于哪个非终端。所有这一切意味着您可以将 bison/yacc 的语法变量视为普通 C 变量,每个变量都有一个已知类型。
Bison/yacc 还允许您拥有没有任何价值的非终端。从技术上讲,“根本没有价值”不是合法的价值。联合将具有一些价值,从上次使用该特定内存时遗留下来。但是只要您从不尝试引用非终端的值,它未初始化的事实就无关紧要。初始化变量失败不是错误。尝试使用未初始化变量的值是错误的。
因此,如果您尝试使用非终端的值并且该非终端没有声明类型(也就是说,它没有出现在任何 %type 声明中),那么 Bison 会抱怨您正在尝试使用一个没有声明类型的非终端,这实际上是说您从未为该非终端赋值。此外,如果您尝试将值分配给没有声明类型的变量(通过在该非终端的生产操作中分配给$$),那么野牛会抱怨您没有为该非终端声明类型终端。它必须这样做,因为它必须将$$ 的赋值编译成某个工会成员的赋值,而你还没有告诉它使用哪个成员。
这就是 bison/yacc 在抱怨 $6 没有声明类型时所指的问题。 $6 是 lines 非终端的实例,并且您尚未为 lines 声明 %type。而且仅仅声明一个类型是不够的:如果你要在一些生产中使用lines的值,你必须在它创建的时候设置lines的值,所以所有可以使用的生产要创建lines,必须要么分配给$$,要么必须使用默认的$$ = $1; 操作(然后$1 必须是正确类型的值)。
不幸的是,bison 不知道您是否在执行生产操作期间分配了值。甚至 C 编译器也无法始终弄清楚这一点,而且 C 编译器比 bison/yacc 更能理解 C 代码。 Bison/yacc 只是复制代码,将$x 标记更改为引用适当堆栈槽的适当联合成员的表达式。但它确实会警告您它可以计算出的东西,例如使用没有类型的非终端的值,或者在 $$ 和 $1 具有不同类型时使用默认操作。
好的,所以我们已经确定lines 的值不能用于if 语句的生产,因为lines 没有值。但是仅仅给它一个值并不能解决你的问题,因为你想要的东西不能通过给lines 一个值来解决。您想要的是 lines 根本不被评估,直到引用它的右侧决定是否应该评估它。这是不可能的,因为lines 动作总是在lines 被识别的那一刻执行。 C 没有实现延迟执行。
这并不是说你不能在 C 中实现惰性执行。你可以做任何你想做的事,取决于你的聪明才智,因为 C 是图灵完备的。您可以想出一种方法来表示您想要延迟执行的操作,并将 lines 的值作为某个操作的描述。这应该不难理解,因为这正是编译器所做的,并且根据您的问题标签,您正在尝试构建一个编译器。计算器可以只执行你扔给它的字符串,但这不是编译器所做的。编译一个程序会产生一个“可执行文件”,这正是如何执行编译后的程序的表示。
您有许多可能的方法来解决您的解析器中的这个问题。一种方法是实际将lines 编译成机器代码片段,并使该机器代码片段成为lines 的语义值。但这将是非常痛苦的,因为在您解析 lines 的那一刻,您并没有足够的信息来完全编译它。 (例如,您还不知道整个程序的存储布局。)创建某种中间表示要容易得多,以后可以将其转换为可执行程序。或者,更简单的是,创建一个包含从解析中提取的所有有用信息的解析树。 (解析树通常被称为 AST,代表“抽象语法树”——或“带注释的语法树”——如果有的话,您可以在教科书或互联网上轻松搜索该术语。)