Skip to content

Latest commit

 

History

History
82 lines (48 loc) · 10.6 KB

Kaleidoscope10.md

File metadata and controls

82 lines (48 loc) · 10.6 KB

Kaleidoscope:总结展望

教程结论

欢迎阅读“使用LLVM实现语言”教程的最后一章。在本教程的过程中,我们已经将我们的小Kaleidoscope语言从一个无用的玩具成长为一个半有趣(但可能仍然没用)的玩具。:)

我们构建了整个词法分析器、解析器、AST、代码生成器、交互式Run循环(使用JIT!),并在独立的可执行文件中发出调试信息。

我们的小语言支持一些有趣的特性:它支持用户定义的二元和一元运算符,它使用JIT编译进行即时计算,它支持一些带有SSA构造的控制流构造。

本教程的部分想法是向您展示定义、构建和使用语言是多么容易和有趣。构建编译器不一定是一个可怕或神秘的过程!既然您已经了解了一些基础知识,我强烈建议您拿起代码并修改它。例如,尝试添加以下内容:

  • 全局变量-虽然全局变量在现代软件工程中的价值值得怀疑,但在组合像Kaleidoscope编译器本身这样的快速小样例,它们通常很有用。幸运的是,我们当前的设置使得添加全局变量变得非常容易:在拒绝某个未解析的变量之前,只需进行值查找检查它是否在全局变量符号表中。要创建新的全局变量,请创建LLVMGlobalVariable类的实例。
  • 类型化变量-Kaleidoscope目前只支持双精度类型的变量。这使该语言非常优雅,因为只支持一种类型意味着您永远不需要指定类型。不同的语言有不同的处理方式。最简单的方法是要求用户为每个变量定义指定类型,并在符号表中记录变量的类型及其值*。
  • 数组、结构、向量等-一旦添加了类型,就可以开始以各种有趣的方式扩展类型系统。简单数组非常简单,对于许多不同的应用程序非常有用。添加它们主要是为了学习LLVMgetelementptr指令是如何工作的:它是如此巧妙/非常规,它有自己的FAQ页面
  • 运行时标准-我们当前的语言允许用户访问任意的外部函数,我们将其用于“printd”和“putchard”。当您扩展语言以添加更高级别的构造时,如果这些构造被降级为对语言提供的运行时的调用,那么这些构造通常是最有意义的。例如,如果您将哈希表添加到语言中,那么将例程添加到运行时可能会有意义,而不是完全内联它们。
  • 内存管理-目前只能在Kaleidoscope中访问堆栈。能够通过调用标准libc malloc/free接口或垃圾收集器来分配堆内存也很有用。如果您想使用垃圾回收,请注意LLVM完全支持精准垃圾回收,包括移动对象和需要扫描/更新堆栈的算法。
  • 异常处理支持-LLVM支持生成与其他语言编译的代码互操作的零成本异常。您还可以通过隐式地使每个函数返回一个错误值并检查它来生成代码。您还可以显式使用setjmp/long jmp。去这里有很多不同的方式。
  • 面向对象,泛型,数据库访问,复数,几何规划,... - 真的,有永无止境的疯狂特性可以添加到语言中。
  • 不寻常的域-我们一直在讨论将LLVM应用到一个很多人感兴趣的领域:为特定语言构建编译器。然而,还有许多其他领域可以使用编译器技术,通常不会考虑到这一点。例如,LLVM已经被用来实现OpenGL图形加速,将C++代码翻译成ActionScript,以及其他许多聪明的事情。也许你会是第一个用LLVM将正则表达式解释器编译成本机代码的人?

玩得开心--试着做一些疯狂和不同寻常的事情。像其他人一样构建一门语言,比起尝试一些疯狂的或离奇的东西,然后看看结果如何,要无趣得多。如果您遇到困难或想要讨论它,请随时发送电子邮件到llvm-dev mail list:],它有很多对语言感兴趣的人,并且通常愿意提供帮助。

在结束本教程之前,我想谈谈生成LLVM IR的一些“提示和技巧”。这些是一些更微妙的事情,可能不是很明显,但如果您想要利用LLVM的功能,它们是非常有用的。

LLVM IR的性质

关于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之后,您会了解到许多有用的提示和技巧,这些技巧乍一看并不明显。这一节不是让每个人都重新发现它们,而是讨论其中的一些问题。

实现可移植的OffsetOf/sizeof

如果您试图保持编译器“目标”生成的代码独立,那么就会出现一件有趣的事情,那就是您经常需要知道某个LLVM类型的大小或llvm结构中某个字段的偏移量。例如,您可能需要将类型的大小传递给分配内存的函数。

不幸的是,这在不同目标之间可能会有很大差异:例如,指针的宽度与目标无关。然而,有一种使用getelementptr instruction](http://nondot.org/sabre/LLVMNotes/SizeOf-OffsetOf-VariableSizedStructs.txt)的聪明方法,它允许您以可移植的方式进行计算。

垃圾收集堆栈帧

一些语言希望显式地管理它们的堆栈框架,通常是为了对它们进行垃圾回收,或者允许轻松实现闭包。通常有比显式堆栈帧更好的方式来实现这些特性,但是LLVM确实支持它们,如果您愿意,可以使用。它需要您的前端将代码转换为Continue,传递Style并使用尾部调用(LLVM也支持)。