Kaleidoscope 系列第十章:总结和其他技巧

本贴最后更新于 1510 天前,其中的信息可能已经物是人非

原文链接:

本文是使用 LLVM 开发新语言 Kaleidoscope 教程系列第十章,对 Kaleidoscope 开发过程进行总结,分析可能使用到的其他技巧。

教程总结

欢迎来到“使用 LLVM 开发新语言 Kaleidoscope 教程”教程的最后一章。在本教程的过程中,我们已经将 Kaleidoscope 这种小语言从一种无用的玩具发展为一种半有趣(但可能仍然无用)的玩具语言。

有趣的是,我们已经走了多远,花了很少的代码。我们构建了整个词法分析器,解析器,AST,代码生成器,交互式运行循环(带有 JIT!),并在独立的可执行文件中发出了调试信息-全部都在 1000 行以下(除去注释和空行)代码中。

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

本教程的部分思想是向你展示定义,构建和使用语言的过程多么容易和有趣。构建编译器不必是一个令人恐惧或神秘的过程!既然您已经了解了一些基础知识,那么我强烈建议你使用代码并对其进行破解。例如,尝试添加:

  • 全局变量-尽管全局变量在现代软件工程中具有可疑的价值,但在将诸如 Kaleidoscope 编译器本身之类的快速小技巧汇集在一起时,它们通常很有用。幸运的是,我们当前的设置使添加全局变量变得非常容易:只需进行值查找检查,以在拒绝前在全局变量符号表中查看未解析的变量是否存在。要创建新的全局变量,请创建 LLVM GlobalVariable 类的实例。
  • 类型变量-Kaleidoscope 目前仅支持 double 类型的变量。这给该语言带来了很好的优雅,因为仅支持一种类型就意味着您不必指定类型。不同的语言有不同的处理方式。最简单的方法是要求用户为每个变量定义指定类型,并将变量的类型及其 Value * 记录在符号表中。
  • 数组,结构,向量等-添加类型后,就可以以各种有趣的方式开始扩展类型系统。简单数组非常容易,对于许多不同的应用程序非常有用。添加它们主要是学习 LLVM getelementptr 指令的工作方式的练习:它是如此的精巧/非常规,详情请见 FAQ
  • 标准运行时-我们当前的语言允许用户访问任意外部函数,并且我们将其用于“打印”和“ putchard”之类的事情。当您扩展语言以添加更高级别的结构时,如果将这些结构降低为对语言提供的运行时的调用,则通常最有意义。例如,如果将哈希表添加到该语言中,则可能需要将例程添加到运行时中,而不是始终将它们内联。
  • 内存管理-当前,我们只能在万花筒中访问堆栈。能够通过调用标准 libc malloc / free 接口或使用垃圾收集器来分配堆内存也将很有用。如果您想使用垃圾收集,请注意 LLVM 完全支持 Accurate Garbage Collection,包括移动对象并需要扫描/更新堆栈的算法。
  • 异常处理支持-LLVM 支持 零成本异常的生成,该异常可与其他语言编译的代码互操作。您还可以通过隐式使每个函数返回错误值并对其进行检查来生成代码。你还可以显式使用 setjmp / longjmp。有很多不同的方法可以去这里。
  • 面向对象,泛型,数据库访问,复数,几何编程等……-确实,您可以为该语言添加疯狂的功能。
  • 不寻常的领域-我们一直在讨论将 LLVM 应用到许多人感兴趣的领域:为特定语言构建编译器。但是,还有许多其他领域可以使用通常不考虑的编译器技术。例如,LLVM 已用于实现 OpenGL 图形加速,将 C ++ 代码转换为 ActionScript 以及许多其他可爱而聪明的事情。也许你将是第一个使用 LLVM 将正则表达式解释器编译为本机代码 JIT 的!

玩得开心-尝试做疯狂和不寻常的事情。建立一种像其他人一样的语言,要比尝试一些疯狂的事情或看看墙外的东西要有趣得多。如果你遇到困难或想要讨论它,请随时向 llvm-dev 邮件列表发送电子邮件:里面有很多对语言感兴趣并且经常乐于助人的人。

在结束本教程之前,再聊聊生成 LLVM IR 的一些“技巧”。这些是一些可能并不明显的更细微的事情,但是如果你想利用 LLVM 的功能,它们将非常有用。

LLVM IR 特性

关于 LLVM IR 格式中的代码,我们有两个常见问题-现在就让我们解决这些问题吧!

目标独立

Kaleidoscope 就是“便携式语言”的一个例子:用 Kaleidoscope 编写的任何程序都可以在其运行的任何目标上以相同的方式工作。许多其他语言都具有此属性,例如 lisp,Java,haskell,JavaScript,python 等(请注意,尽管这些语言是可移植的,但并不是所有的库都可以)。

LLVM 的一个优点是它通常能够保持 IR 中目标的独立性:你可以将 LLVM IR 用于 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 没有错误)。安全是一个有趣的属性,需要将语言设计,运行时支持以及经常的操作系统支持结合在一起。

当然可以在 LLVM 中实现安全语言,但是 LLVM IR 本身并不保证安全。LLVM IR 允许不安全的指针强制转换,释放错误后使用,缓冲区超限以及其他各种问题。安全需要作为 LLVM 之上的一层来实现,方便地,几个小组对此进行了调查。如果你对更多详细信息感兴趣,请在 llvm-dev 邮件列表中询问。

LLVM 的一件事使许多人无法接受,它不能在一个系统中解决世界上所有的问题。一个具体的困扰是人们认为 LLVM 无法执行特定于语言的高级优化:LLVM“丢失了太多信息”。以下是对此的一些观察:

首先,你是对的,LLVM 确实会丢失信息。例如,在撰写本文时,没有办法在 LLVM IR 中区分 SSA 值是来自 ILP32 机器上的 C int 还是 C long(除调试信息以外)。两者都被编译为 i32 值,并且有关其来源的信息也丢失了。这里更普遍的问题是 LLVM 类型系统使用“结构等效”而不是“名称等效”。让你惊讶的另一个地方是,如果你在高级语言中有两种类型具有相同的结构(例如,两个具有单个 int 字段的不同结构):这些类型将被编译为单个 LLVM 类型,并且这是不可能的告诉它来自哪里。

其次,虽然 LLVM 确实会丢失信息,但是 LLVM 并不是固定的目标:我们将继续以许多不同的方式来增强和改进它。除了添加新功能(LLVM 并不总是支持异常或调试信息)之外,我们还扩展了 IR 以捕获重要信息以进行优化(例如,参数是符号扩展还是零扩展,有关指针别名的信息,等等)。许多增强功能是用户驱动的:人们希望 LLVM 包含某些特定功能,因此他们继续进行扩展。

第三,添加特定于语言的优化是 可能且容易的 ,并且在执行方法方面有很多选择。作为一个简单的示例,可以很容易地添加特定于语言的优化过程,以“了解”有关为某种语言编译的代码的信息。对于 C 系列,有一个优化 pass 就可以“了解”标准 C 库函数。如果在 main()中调用 exit(0) ,它将知道将其优化为 return 0; 是安全的,因为 C 指定了 exit 函数的作用。

除了简单的库知识之外,还可以将各种其他特定于语言的信息嵌入到 LLVM IR 中。如果你有特定需求并遇到麻烦,请将该主题放在 llvm-dev 列表中。在最坏的情况下,你始终可以将 LLVM 视为“智障代码生成器”,并在特定于语言的 AST 上实现你希望在前端进行的高级优化。

其他技巧

在使用 LLVM 或使用 LLVM 后,你会发现许多有用的提示和技巧,这些乍看之下并不明显。本节将讨论其中的一些问题,而不是让所有人重新发现它们。

实现可移植的 offsetof / sizeof

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

不幸的是,这在各个目标之间变化很大:例如,指针的宽度对于目标来说是微不足道的。但是,有一种 clever way to use the getelementptr instruction,使您能够以可移植的方式进行计算。

垃圾回收堆栈框架

某些语言通常希望显式管理其堆栈框架,以便对其进行垃圾收集或轻松实现闭包。与显式堆栈框架相比,通常有更好的方法来实现这些功能,但是 LLVM 确实支持它们。它要求你的前端将代码转换为 Continuation Passing Style,并使用 tail calls (LLVM 也支持 tail calls)。


参考: Kaleidoscope: Conclusion and other useful LLVM tidbits

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...