llvm是当前编译器领域非常火热的项目,其设计优雅,官方文档也很全面,可惜目前缺乏官方中文翻译。笔者在学习过程中也尝试进行一些翻译记录,希望能对自己或者他人的学习有所帮助。(PS:初步翻译文档放在github上了,需要可自取,也欢迎提PR共同完善)
欢迎阅读“使用LLVM实现语言”教程的最后一章。在本教程的过程中,我们已经将我们的小Kaleidoscope语言从一个无用的玩具成长为一个半有趣(但可能仍然没用)的玩具。:)
有趣的是,我们只用了很少的代码就已经走很远。我们构建了整个词法分析器、解析器、AST、代码生成器、交互式Run循环(使用JIT!),并在独立的可执行文件中发出调试信息-所有这些都在1000行(非注释/非空白)代码中。
我们的小语言支持一些有趣的特性:它支持用户定义的二元和一元运算符,它使用JIT编译进行即时计算,它支持一些带有SSA构造的控制流构造。
本教程的部分想法是向您展示定义、构建和使用语言是多么容易和有趣。构建编译器不一定是一个可怕或神秘的过程!既然您已经了解了一些基础知识,我强烈建议您拿起代码并修改它。例如,尝试添加以下内容:
GlobalVariable
类的实例。玩得开心--试着做一些疯狂和不同寻常的事情。像其他人一样构建一门语言,比起尝试一些疯狂的或离奇的东西,然后看看结果如何,要无趣得多。如果您遇到困难或想要讨论它,请随时发送电子邮件到llvm-dev mail list:],它有很多对语言感兴趣的人,并且通常愿意提供帮助。
在结束本教程之前,我想谈谈生成LLVM IR的一些“提示和技巧”。这些是一些更微妙的事情,可能不是很明显,但如果您想要利用LLVM的功能,它们是非常有用的。
关于LLVM IR表单中的代码,我们有几个常见的问题-让我们现在就把这些问题解决掉,好吗?
Kaleidoscope是“可移植语言”的一个例子:用Kaleidoscope编写的任何程序都可以在它运行的任何目标上以相同的方式工作。许多其他语言都有这个属性,例如LISP、Java、Haskell、javascript、Python等(请注意,虽然这些语言是可移植的,但并不是它们所有的库都是可移植的)。
LLVM的一个很好的方面是,它通常能够在IR中保持目标独立性:您可以将LLVMIR用于Kaleidoscope编译的程序,并在LLVM支持的任何目标上运行它,甚至发出C代码并在LLVM本地不支持的目标上编译。您可以很容易地看出,Kaleidoscope编译器生成与目标无关的代码,因为它在生成代码时从不查询任何特定于目标的信息。
LLVM为代码提供了一种紧凑的、与目标无关的表示形式,这一事实让很多人兴奋不已。不幸的是,这些人在询问有关语言可移植性的问题时,通常会想到C或C家族的一种语言。我说“不幸的”,因为除了随身携带源代码之外,确实没有办法使(完全通用的)C代码可移植(当然,C源代码通常也不能移植--曾经将真正的旧应用程序从32位移植到64位吗?)。
C语言的问题(再说一次,就是它的全部通用性)是它有大量的特定于目标的假设。举一个简单的例子,预处理器在处理输入文本时,通常会从代码中破坏性地删除目标独立性:
#ifdef __i386__
int X = 1;
#else
int X = 42;
#endif
虽然可以设计出越来越复杂的解决方案来解决这类问题,但它不能以比发布实际源代码更好的方式完全解决。
也就是说,C语言中有一些有趣的子集可以使其可移植。如果您愿意将原始类型固定为固定大小(例如int=32位,long=64位),不关心ABI与现有二进制文件的兼容性,并且愿意放弃其他一些次要功能,您可以拥有可移植的代码。这对于内核内语言等专门域来说是有意义的。
上面的许多语言也是“安全”语言:用Java编写的程序不可能损坏其地址空间并使进程崩溃(假设JVM没有bug)。安全性是一个有趣的属性,需要语言设计、运行时支持,通常还需要操作系统支持。
在LLVM中实现安全语言当然是可能的,但是LLVM IR本身并不保证安全。LLVM IR允许不安全的指针强制转换、在释放错误后使用、缓冲区溢出和各种其他问题。安全需要作为LLVM之上的一层来实现,为了方便起见,几个小组已经对此进行了研究。如果您对更多细节感兴趣,请访问llvm-dev邮件list]。
LLVM让许多人反感的一件事是,它不能在一个系统中解决世界上所有的问题。一个具体的抱怨是,人们认为LLVM无法执行高级语言特定优化:LLVM“丢失了太多信息”。以下是对此的一些观察结果:
首先,您说得对,LLVM确实丢失了信息。例如,在撰写本文时,无法在LLVM IR中区分SSA值是来自ILP32机器上的C“int”还是C“long”(调试信息除外)。这两个值都被编译为‘I32’值,并且关于它来自什么的信息也会丢失。这里更普遍的问题是,LLVM类型系统使用“结构等价”而不是“名称等价”。另一个让人惊讶的地方是,如果在高级语言中有两个具有相同结构的类型(例如,两个不同的结构具有单个int字段):这两个类型将编译成单个LLVM类型,并且不可能知道它来自哪里。
其次,虽然LLVM确实会丢失信息,但LLVM并不是一个固定的目标:我们在以许多不同的方式继续增强和改进它。除了添加新功能(LLVM并不总是支持异常或调试信息),我们还扩展IR以捕获用于优化的重要信息(例如,参数是符号扩展的还是零扩展的,有关指针别名的信息,等等)。许多增强都是由用户驱动的:人们希望LLVM包含一些特定的特性,所以他们继续扩展它。
第三,添加特定于语言的优化是可能而且容易,您有很多选择。作为一个简单的例子,很容易添加特定于语言的优化过程,这些优化过程“了解”为一种语言编译的代码。在C系列的情况下,有一个“知道”标准C库函数的优化过程。如果在main()中调用“exit(0)”,它知道将其优化为“return 0;”是安全的,因为C指定了“exit”函数的作用。
除了简单的图书馆知识之外,还可以将各种其他语言特定的信息嵌入到LLVM IR中。如果您有特定的需求并遇到困难,请将该主题带到llvm-dev列表中。在最坏的情况下,您可以始终将LLVM视为“哑巴代码生成器”,并在特定于语言的AST上在您的前端实现所需的高级优化。
在使用LLVM之后,您会了解到许多有用的提示和技巧,这些技巧乍一看并不明显。这一节不是让每个人都重新发现它们,而是讨论其中的一些问题。
如果您试图保持编译器“目标”生成的代码独立,那么就会出现一件有趣的事情,那就是您经常需要知道某个LLVM类型的大小或llvm结构中某个字段的偏移量。例如,您可能需要将类型的大小传递给分配内存的函数。
不幸的是,这在不同目标之间可能会有很大差异:例如,指针的宽度与目标无关。然而,有一种使用getelementptr instruction](http://nondot.org/sabre/LLVMNotes/SizeOf-OffsetOf-VariableSizedStructs.txt)的聪明方法,它允许您以可移植的方式进行计算。
一些语言希望显式地管理它们的堆栈框架,通常是为了对它们进行垃圾回收,或者允许轻松实现闭包。通常有比显式堆栈帧更好的方式来实现这些特性,但是LLVM确实支持它们,如果您愿意,可以使用。它需要您的前端将代码转换为Continue,传递Style并使用尾部调用(LLVM也支持)。