你要做的事情有三步:
- 语义设计
- 语法设计
- 实现一个编译器
实际上,最难的往往是第一步,也就是语义设计,因为它决定了你的语言的最终形态。「语义」的内容会很宽泛,下面这些都是语义的范畴:
- 类型系统:是强类型还是弱类型?静态类型还是动态类型?是否有类型推导?如果有,基于哪种形式系统?是否允许子类型?是否允许递归类型?类型转换的机制如何?等等。
- 编程范式:你的语言是过程式(Imperative)还是声明式(Declarative)还是两者结合?对于「声明式」,是函数式(Functional)还是逻辑式(Logical)?是否允许元编程?等等。
- 存储:你的语言是否允许用户干预存储细节?是否允许指针算数?是否允许手动内存管理?变量(符号)的作用域规则如何?是词法作用域(Lexical Scoping)还是动态作用域(Dynamic Scoping)还是两者结合?
- 子程序:你的语言是否有子程序?如果有,他们是否是第一态(First Class)的?参数传递是按值传递还是按名传递?是否允许按引用传递?求值策略是急迫求值还是懒惰求值?参数之间的求值顺序是怎样?
- 流程控制:你的语言是否允许非结构跳转?是否有内建的异常处理机制?是否有连续体(Continuation)或协程(Coroutine)机制?
在语义设计完成之后就要设计与之对应的语法(Grammar)。几乎所有的编译原理教程都是从语法开始,这在实现编译器的时候是这样,然而设计语言时,语义是比语法先完成的。一套核心语义可以有多种语法与之对应,当然这些语法里肯定有最合适的。像 Lisp 里无比强大的宏就更喜欢简单的语法。(等等……Lisp 这货有语法?)
在语法设计完成之后,你就可以着手实现编译器/解释器了。现在成熟的后端非常多,即使你想直接编译成 x86 机器代码,使用现有库的话也没有很高的难度。不过对于初学者,把编译器的目标平台定到一些虚拟机——如 JVM 或者 CLI 上会容易的多。在实现编译器的时候你要做的事情包括:
- 语法分析。这一块库相当丰富(毕竟还有一堆程序要读文本文件),从老牌的 yacc 到现在流行的 Parser Combinator 随你挑。这一步完成时你会得到一个语法树(Parse Tree)。
- 类型检查与类型推导。对于那些静态类型的语言,你要在这一步完成类型检查/类型推导。这一块没有通用的库,你得自己查阅相关算法(比如 Hindley-Milner 推导算法)。
- 语言相关优化。这一步是针对语言的优化。尤其是对那些使用大量函数式特性的语言,这一步尤其重要。优化的好,编译出的程序性能可提升数倍。当然,传统的平台无关优化,如子表达式消除、复制传播等也不能忽略。
- 中间代码生成。绝大多数后端都是线性的简单指令,此时你就要想办法把树状的语法树变成线性的指令列表。你可以用三地址(3-address)或者静态单赋值形式(SSA)。如果你使用 LLVM、JVM 这类的后端,你的编译器在这里就可以宣告完工了。
- 代码生成。如果你想编译成机器代码而不想用现成后端的话,这是最后一步。在这一步里你还要进行平台相关优化,把你的程序调教成最适合目标平台的形式。
- 标准库的编写。这个严格意义上不算编译器的内容,但是谁的语言是裸语言呢?编写一套好的标准库比你想象的困难,看看 nodejs 所用 libuv 的规模就知道了。
编译器的调试和测试是非常困难的:一方面它不允许任何错误,另一方面编译器的测试用例也非常难写。所以在优化这一步上,宁可保守一些也不要引入错误。同时在编程时,请写「明显正确」的程序:结构清楚,一看便明的程序。
——————————————————————————————————————————
补充:
- 有人提及 OOP。喂,OOP 不就是结构类型 + 子类型披了层皮吗?
- 「语法设计」我提得少,因为它相比语义设计和编译器作成来看,重要性最低。但是它的工作十分琐碎,而且对于语言用家的最终体验也有巨大影响,所以还是认真点好。真的不想设计语法,就 lisp 好了……
— 完 —
本文作者:Belleve
【知乎日报】
你都看到这啦,快来点我嘛 Σ(▼□▼メ)
此问题还有 11 个回答,查看全部。
延伸阅读:
学习编程语言最好的方法是什么呢?
一个程序只能用一种编程语言吗?