一些经验方面的东西, 觉得有用, 粗糙翻译了一下
原文是 2012 年的, 现在编译的 JavaScript 的语言已经很多了
原文 http://programmers.stackexchange.com/a/165558/50274
介绍
一个典型的编译器做下面一些工作:
解析: 源码转化为抽象语法树(AST)
查找对其他模块的引用(C 把这个不知推迟到链接才做的)
语义检查: 清除语法正确然而无用的语句, 比如, 无法访问到的代码或者重复声明
等价替换和高级优化: AST 被转化为相同语义的更高效的计算.
包括, 提前计算常用的标法式和常量表达式, 消除过度的局部赋值, 等等生成代码: AST 转化为线性的低级代码, 包括 jump, 寄存器寻址之类
一些函数调用可以在这个环节被内联, 一些循环被展开, 等等窥孔优化: 低级代码扫描出简单的局部的低效代码进行消除
主要的现代编译器(比如, gcc 和 clang)对最后两个步骤多重复一遍
它们使用中间层低级然而平台无关的语言, 用于初次代码生成
随后这门语言被编译的平台特定的代码(x86, ARM, 等等)
针对每个平台优化的方案做一些大概类似的事情
其中包括, 比如. 在可能时使用向量指令,生成指令用于分支预测优化, 等等
之后, 对象代码可以开始链接.
大多数本地代码编译器会知道怎样调用链接器生成可执行文件,
然而这本身并不是一个编译过程.
在 Java 和 C# 之类语言中, 链接完全可以是动态的, 由 VM 在加载过程完成
牢记这些基础
使之运行
使之漂亮
使之高效
这个经典的步骤可以应用到所有的软件开发, 经得起重复
集中精力到这个步骤的第一步. 创建最简单的可以运行的程序.
读书!
阅读龙书, 作者是 Aho 和 Ullman.
这是经典, 而且今天依然行之有效
Modern Compiler Design 的声誉也不错.
如果这些资源目前对你来说太难, 可以先读一些关于语法解析的介绍.
通常语法解析的类库包含介绍还有例子
确定你能适应面对 graph 特别还有 tree 进行工作
这些是程序在逻辑层面上进行构造的工具
定义好你的语言
你想用什么记号就用什么记号, 但一定要保证语言的完整性和一致性.
包括语法也包括语义.
现在特别需要你写出新的语言的代码片段, 作为未来编译器的测试用例.
使用你最喜欢的语言
用 Python, Ruby 或其他你喜欢的语言写编译器都可以
用你能够理解的简单觉得算法去实现
第一个版本不需要快, 或者高效, 也不要功能完整
它只要求相当地正确, 而且易于后续修改
需要的话, 也可以用不同的语言写编译器不同的编译阶段
整备好写大量测试
整个语言应该被测试用例所覆盖, 实际上语言被它们所定义.
要熟悉你选择的测试框架, 从一开始就要写测试
集中精力在"正面"的测试上接受正确的代码, 而不是去检测错误代码
日常性地运行所有测试. 在继续推进前修复失败的测试
如果最终得到正确代码都无法执行的病态语言, 那真的要丢脸
写一个好的 Parser
有好多的 Parser generators. 选好你想用的.
你也可以从底层完全写你的 parser, 不过这只是在你的语言语法简单时才合适
Parser 需要能够检测和报告语法错误. 写大量的测试用例, 正面的和反面的.
在定义语言当中尽量重用自己写的代码
Parser 输出内容是抽象语法树
如果你的语言包含模块, Parser 结果应当是你生成的"物件代码"最简单的表示形式
有很多简单的办法可以把 tree 保存到文件还有快速加载回来
写一个 semantic validator
基本上你的语言会允许存在结构正确但具体场景中没意义的代码
例子比如变量重复定义或者传入错误类型的参数
validator 可以通过常看 tree 来发现这类错误
validator 也会处理你的语言中对于其他模块的引用,
加载其模块, 在检查过程当中使用
比如说, 这个步骤能保证传给另一个模块的函数的参数的数量是正确的
重申一下, 写很多测试运行很多测试
通常的测试用例对于查找问题来说是必不可少的, 跟智能和复杂一样
生成代码
使用你所知的最简单的技术, 常常就跟 HTML 模板没太大区别
可以是直接翻一个一套语言结构(比如 if
语句)到带一些参数的模板代码
重申一次, 先忽略性能, 先关心正确性.
以一个平台无关的 low-level VM 为编译目标
我假设你会忽略底层的内容, 除非你对硬件细节极其有兴趣
这些细节会非常残酷和复杂
你的选项:
LLVM: 可以做高效的机器代码生成, 通常是 x86 和 ARM
CLR: 以 .NET 为目标, 主要是基于 x86/Windows; 有一套好的 JIT
JVM: 以 Java 世界为目标, 算是多平台, 有一套好的 JIT
忽略优化
优化非常难. 基本上大部分优化都是过早的
生成不那么高效翻正确的代码. 先实现整个语言, 然后尝试优化最终代码
当然, 引入普通的优化还是 OK 的. 但是在编译器稳定前避免冒险的优化
那么?
如果这些对你来说都不怎么吓人, 那么开始做吧!
对于简单的语言, 每个步骤可能还会比你想象的要简单
看到你的编译器执行完 "Hello world" 你会努力没有白费