介绍
典型的编译器执行以下步骤:
- 解析:将源文本转换为抽象语法树(AST)。
- 对其他模块的引用的解析(C将此步骤推迟到链接)。
- 语义验证:清除语法上不正确的语句,例如,无法访问的代码或重复的声明。
- 等效转换和高级优化:AST被转换为表示具有相同语义的更有效的计算。例如,这包括早期计算公共子表达式和常量表达式,消除过多的局部分配(另请参见SSA)等。
- 代码生成:具有跳转,寄存器分配等功能,将AST转换为线性低级代码。在此阶段可以内联一些函数调用,展开某些循环等。
- 窥孔优化:扫描低级代码以查找简单的本地效率低下的现象,并将其消除。
大多数现代编译器(例如gcc和clang)都重复最后两个步骤。他们使用中间的低级但与平台无关的语言来生成初始代码。然后,将该语言转换为平台特定的代码(x86,ARM等),并以平台优化的方式做大致相同的事情。这包括例如在可能的情况下使用向量指令,对指令进行重新排序以提高分支预测效率等。
之后,目标代码已准备好进行链接。大多数本机代码编译器都知道如何调用链接器以生成可执行文件,但这本身不是编译步骤。在Java和C#等语言中,链接可能是完全动态的,由VM在加载时完成。
记住基础
这个经典序列适用于所有软件开发,但需要重复。
专注于序列的第一步。创建可能可行的最简单的方法。
看书!
阅读Aho和Ullman撰写的《龙书》。这是经典之举,今天仍然非常适用。
现代编译器设计也受到称赞。
如果现在这些东西对您来说太难了,请先阅读一些有关解析的介绍。通常,解析库包括简介和示例。
确保您习惯使用图,尤其是树。这些东西是程序在逻辑级别上构成的。
很好地定义您的语言
使用所需的任何符号,但请确保您对语言有完整且一致的描述。这包括语法和语义。
现在是时候用您的新语言编写代码片段,作为将来编译器的测试用例。
使用您喜欢的语言
完全可以使用Python或Ruby或任何您喜欢的语言编写编译器。使用简单的算法,您会很好理解。第一个版本不必快速,高效或功能完善。它只需要足够正确并且易于修改。
如果需要的话,也可以用不同的语言编写编译器的不同阶段。
准备编写很多测试
您的整个语言都应包含在测试用例中;有效地将由他们来定义。熟悉您首选的测试框架。从第一天开始编写测试。专注于接受正确代码的“正”测试,而不是检测错误代码。
定期运行所有测试。在继续之前,请修复损坏的测试。最终使用无法接受有效代码的定义不明确的语言而感到遗憾。
创建一个好的解析器
解析器生成器很多。选择任何你想要的。您也可以从头开始编写自己的解析器,但它只是值得的,如果你的语言的语法是死的简单。
解析器应检测并报告语法错误。编写大量的测试用例,包括正数和负数;重用您在定义语言时编写的代码。
解析器的输出是一个抽象语法树。
如果您的语言具有模块,则解析器的输出可能是您生成的“目标代码”的最简单表示。有很多简单的方法可以将树转储到文件中并快速将其重新加载。
创建一个语义验证器
您的语言很可能允许在语法上正确的构造,这些构造在某些情况下可能毫无意义。一个示例是相同变量的重复声明或传递错误类型的参数。验证器将在树上检测到此类错误。
验证程序还将解析对使用您的语言编写的其他模块的引用,加载这些其他模块并在验证过程中使用。例如,此步骤将确保从另一个模块传递给函数的参数数量正确。
同样,编写并运行许多测试用例。在故障排除中,琐碎的案例与智能和复杂一样不可或缺。
产生程式码
使用您知道的最简单的技术。通常,直接将语言结构(如if
语句)转换为轻量化的代码模板是可以的,与HTML模板不同。
同样,忽略效率,而专注于正确性。
定位与平台无关的低层虚拟机
我想除非您对特定于硬件的细节非常感兴趣,否则您将忽略低级内容。这些细节是复杂的。
您的选择:
- LLVM:通常针对x86和ARM,允许高效的机器代码生成。
- CLR:针对.NET,主要是基于x86 / Windows的;具有良好的准时性。
- JVM:面向Java世界,相当多平台,具有良好的JIT。
忽略优化
优化很难。优化几乎总是过早的。生成效率低下但正确的代码。在尝试优化结果代码之前,请实现整个语言。
当然,可以引入简单的优化。但是,在编译器稳定之前,请避免使用任何狡猾多毛的东西。
所以呢?
如果这一切对您来说都不是太吓人,请继续!对于简单的语言,每个步骤可能比您想象的要简单。
从编译器创建的程序中看到“ Hello world”可能是值得的。