第一个 C 编译器使用了什么样的 lexer/parser?
What kind of lexer/parser was used in the very first C compiler?
在 1970 年代早期,Dennis Ritchie 编写了第一个 C 编译器。
2017年想写一个C编译器。 Deep C Secrets(Peter Van Der Linden)之类的书说,C 最重要的是被设计为易于编译。但是我一直遇到很多麻烦。
对于初学者来说,为 C 语言制定 Lex/Yacc 规范已经相对困难,而当 Ritchie his 时这些工具甚至还不存在编译器!
此外,还有许多不使用 Lex 和 Yacc 的任何帮助的令人惊讶的小 C 编译器示例。 (查看 Fabrice Bellard 的 this 微型混淆 C 编译器。请注意,他的 "production" tinycc 源代码实际上要长很多,很可能是为了适应更多的体系结构,并且更具可读性)
那么 我在这里遗漏了什么? Ritchie 在他的编译器中使用了什么样的 lexer/parser?是否有一些更简单的编写编译器的方法我只是偶然发现了?
Yacc 的名字是“yet another compiler compiler”的缩写,这强烈表明它既不是第一个也不是第二个这样的工具。
确实,History of Compiler Construction 上的维基百科文章指出
In the early 1960s, Robert McClure at Texas Instruments invented a compiler-compiler called TMG, the name taken from "transmogrification". In the following years TMG was ported to several UNIVAC and IBM mainframe computers.
…
Not long after Ken Thompson wrote the first version of Unix for the PDP-7 in 1969, Doug McIlroy created the new system's first higher-level language: an implementation of McClure's TMG. TMG was also the compiler definition tool used by Ken Thompson to write the compiler for the B language on his PDP-7 in 1970. B was the immediate ancestor of C.
这并不能完全回答您的问题,但它提供了一些可能性。
原回答:
如果 Ritchie 只是将一个手工构建的自上而下或运算符优先级解析器拼凑在一起,我一点也不会感到惊讶。这些技术众所周知,原始的 C 语言提出的挑战很少。但是解析器生成工具肯定是存在的。
后记:
Alexey Frunze points to this early version of the C compiler 对 OP 的评论。它基本上是一个递归下降的自上而下的解析器,直到需要解析表达式为止,此时它使用类似调车场的运算符优先语法。 (请参阅表达式解析器的第一个源文件中的函数 tree
。)这种从自上而下算法开始并在需要时切换到自下而上算法(例如运算符优先级)的风格有时被称为“左角”(LC)解析。
所以这基本上就是我所说的不会让我感到惊讶的架构,它确实没有:)。
值得注意的是,Alexey(以及@Torek 在对此 post 的评论中)发现的编译器无法处理我们现在通常认为的 C 语言的任何东西。特别是,它只处理声明语法的一小部分(例如,没有结构或联合),这可能是 K&R C 语法中最复杂的部分。所以它没有回答你关于如何为 C 生成“简单”解析器的问题。
C 可以(大部分)使用 LALR(1) 语法进行解析,尽管您需要实现某些版本的“lexer hack”才能正确解析强制转换表达式。解析器(翻译阶段 7)的输入将是由预处理代码(翻译阶段 4,可能包含阶段 5 和 6)产生的标记流,它本身可以利用 (f)lex 标记器(阶段 3),其输入将根据阶段 1 和阶段 2 以某种方式进行清理。(有关阶段的精确定义,请参阅第 5.1.1.2 节)。
遗憾的是,(f)lex 并未设计为管道的一部分;他们真的只想处理阅读源代码的任务。但是,可以说服 flex 允许您通过重新定义 YY_INPUT macro 来提供输入块。可以使用简单的状态机来处理三字母组(如果您选择这样做)和续行;方便的是,这些转换仅缩小输入,将最大输入长度参数的处理简化为 YY_INPUT
。 (不要像 flex 手册中的示例所建议的那样一次输入一个字符。)
由于预处理器必须产生一个标记流(此时,空格不再重要),使用bison的push-parser接口很方便。 (事实上 ,使用推送 API 通常更方便。)如果你采纳该建议,你将以第 4 阶段作为解析的顶级驱动程序结束。
您可以手动构建一个预处理器指令解析器,但是获得 #if
表达式和 pragma
s 正确建议使用单独的 bison 解析器进行预处理。
如果您只想学习如何构建编译器,您可能希望从更简单的语言开始,例如 Tiger,该语言在 Andrew Appel 的 excellent textbooks 上用作 运行 示例编译器构造。
在 1970 年代早期,Dennis Ritchie 编写了第一个 C 编译器。
2017年想写一个C编译器。 Deep C Secrets(Peter Van Der Linden)之类的书说,C 最重要的是被设计为易于编译。但是我一直遇到很多麻烦。
对于初学者来说,为 C 语言制定 Lex/Yacc 规范已经相对困难,而当 Ritchie his 时这些工具甚至还不存在编译器!
此外,还有许多不使用 Lex 和 Yacc 的任何帮助的令人惊讶的小 C 编译器示例。 (查看 Fabrice Bellard 的 this 微型混淆 C 编译器。请注意,他的 "production" tinycc 源代码实际上要长很多,很可能是为了适应更多的体系结构,并且更具可读性)
那么 我在这里遗漏了什么? Ritchie 在他的编译器中使用了什么样的 lexer/parser?是否有一些更简单的编写编译器的方法我只是偶然发现了?
Yacc 的名字是“yet another compiler compiler”的缩写,这强烈表明它既不是第一个也不是第二个这样的工具。
确实,History of Compiler Construction 上的维基百科文章指出
In the early 1960s, Robert McClure at Texas Instruments invented a compiler-compiler called TMG, the name taken from "transmogrification". In the following years TMG was ported to several UNIVAC and IBM mainframe computers.
…
Not long after Ken Thompson wrote the first version of Unix for the PDP-7 in 1969, Doug McIlroy created the new system's first higher-level language: an implementation of McClure's TMG. TMG was also the compiler definition tool used by Ken Thompson to write the compiler for the B language on his PDP-7 in 1970. B was the immediate ancestor of C.
这并不能完全回答您的问题,但它提供了一些可能性。
原回答:
如果 Ritchie 只是将一个手工构建的自上而下或运算符优先级解析器拼凑在一起,我一点也不会感到惊讶。这些技术众所周知,原始的 C 语言提出的挑战很少。但是解析器生成工具肯定是存在的。
后记:
Alexey Frunze points to this early version of the C compiler 对 OP 的评论。它基本上是一个递归下降的自上而下的解析器,直到需要解析表达式为止,此时它使用类似调车场的运算符优先语法。 (请参阅表达式解析器的第一个源文件中的函数 tree
。)这种从自上而下算法开始并在需要时切换到自下而上算法(例如运算符优先级)的风格有时被称为“左角”(LC)解析。
所以这基本上就是我所说的不会让我感到惊讶的架构,它确实没有:)。
值得注意的是,Alexey(以及@Torek 在对此 post 的评论中)发现的编译器无法处理我们现在通常认为的 C 语言的任何东西。特别是,它只处理声明语法的一小部分(例如,没有结构或联合),这可能是 K&R C 语法中最复杂的部分。所以它没有回答你关于如何为 C 生成“简单”解析器的问题。
C 可以(大部分)使用 LALR(1) 语法进行解析,尽管您需要实现某些版本的“lexer hack”才能正确解析强制转换表达式。解析器(翻译阶段 7)的输入将是由预处理代码(翻译阶段 4,可能包含阶段 5 和 6)产生的标记流,它本身可以利用 (f)lex 标记器(阶段 3),其输入将根据阶段 1 和阶段 2 以某种方式进行清理。(有关阶段的精确定义,请参阅第 5.1.1.2 节)。
遗憾的是,(f)lex 并未设计为管道的一部分;他们真的只想处理阅读源代码的任务。但是,可以说服 flex 允许您通过重新定义 YY_INPUT macro 来提供输入块。可以使用简单的状态机来处理三字母组(如果您选择这样做)和续行;方便的是,这些转换仅缩小输入,将最大输入长度参数的处理简化为 YY_INPUT
。 (不要像 flex 手册中的示例所建议的那样一次输入一个字符。)
由于预处理器必须产生一个标记流(此时,空格不再重要),使用bison的push-parser接口很方便。 (事实上 ,使用推送 API 通常更方便。)如果你采纳该建议,你将以第 4 阶段作为解析的顶级驱动程序结束。
您可以手动构建一个预处理器指令解析器,但是获得 #if
表达式和 pragma
s 正确建议使用单独的 bison 解析器进行预处理。
如果您只想学习如何构建编译器,您可能希望从更简单的语言开始,例如 Tiger,该语言在 Andrew Appel 的 excellent textbooks 上用作 运行 示例编译器构造。