第一个 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 表达式和 pragmas 正确建议使用单独的 bison 解析器进行预处理。

如果您只想学习如何构建编译器,您可能希望从更简单的语言开始,例如 Tiger,该语言在 Andrew Appel 的 excellent textbooks 上用作 运行 示例编译器构造。