Kaleidoscope: Code generation to LLVM IR

3.1 Chapter 3 Introduction

       欢迎来到“用LLVM实现语言”教程的第3章。本章将向您展示如何将第2章中构建的抽象语法树转换为LLVM IR。这将教会您一点关于LLVM如何工作的知识,并演示如何使用它。构建lexer和解析器要比生成LLVM IR代码复杂得多。

       请注意:本章及以后的代码需要LLVM 3.7或更高版本。LLVM 3.6和before将无法使用它。还要注意,您需要使用与您的LLVM发行版相匹配的本教程版本:如果您使用的是正式的LLVM发行版,那么请使用发行版中包含的文档版本,或者在llvm.org发行版页面上使用。

3.2 Code Generation Setup

       为了生成LLVM IR,我们需要一些简单的设置。首先,我们在每个AST类中定义了虚拟代码生成(codegen)方法:

       codegen()方法表示为该AST节点以及它所依赖的所有东西发出IR,并且它们都返回一个LLVM值对象。“Value”是用来在LLVM中表示“静态单赋值(SSA)寄存器”或“SSA值”的类。SSA值最明显的方面是,它们的值是在相关指令执行时计算的,直到(和if)指令重新执行时,它才会得到一个新值。换句话说,没有办法“更改”SSA值。要了解更多信息,请仔细阅读静态单项作业——一旦你熟悉了这些概念,它们就会变得非常自然。

       注意,与其向ExprAST类层次结构添加虚拟方法,还可以使用访问者模式或其他方法对其建模。同样,本教程不会详细讨论好的软件工程实践:对于我们的目的,添加虚拟方法是最简单的。

        我们想要的第二件事是一个“LogError”方法,就像我们在解析器中使用的那样,它将用于报告在代码生成过程中发现的错误(例如,使用未声明的参数):

       静态变量将在代码生成期间使用。TheContext是一个不透明的对象,它拥有许多核心LLVM数据结构,比如type和常量值表。我们不需要详细地理解它,我们只需要一个实例来传递到需要它的api中。

       Builder对象是一个帮助对象,它使生成LLVM指令变得很容易。IRBuilder类模板的实例跟踪当前插入指令的位置,并具有创建新指令的方法。

        TheModule是一个LLVM构造,它包含函数和全局变量。在许多方面,它是LLVM IR用来包含代码的顶层结构。它将拥有我们生成的所有IR的内存,这就是为什么codegen()方法返回一个原始值*,而不是unique_ptr<Value>。

        NamedValues映射跟踪当前范围中定义的值以及它们的LLVM表示形式。(换句话说,它是代码的符号表)。在这种形式的万花筒中,唯一可以引用的是函数参数。因此,当为函数体生成代码时,函数参数将出现在这个映射中。

       有了这些基础知识,我们就可以开始讨论如何为每个表达式生成代码。注意,这假设构建器已经被设置为生成代码到某个东西。现在,我们假设这已经完成了,我们将使用它来发出代码。

3.3 Expression Code Generation

为表达式节点生成LLVM代码非常简单:所有四个表达式节点的注释代码都少于45行。首先,我们将做数字文字:

        在LLVM IR中,数值常量由ConstantFP类表示,该类在APFloat内部保存数值(APFloat能够保存任意精度的浮点常量)。这段代码基本上只创建并返回一个ConstantFP。注意,在LLVM IR中,常量都是唯一的并共享的。因此,API使用“foo::get(…)”习惯用法,而不是“new foo(..)”或“foo::Create(..)”。

        使用LLVM对变量的引用也非常简单。在简单版的Kaleidoscope中,我们假设变量已经在某个地方发出,并且它的值是可用的。实际上,NamedValues映射中唯一的值是函数参数。这段代码只是检查指定的名称是否在映射中(如果没有,则引用一个未知变量),并返回它的值。在以后的章节中,我们将在符号表中添加对循环诱导变量的支持,以及对局部变量的支持。

       二进制运算符开始变得越来越有趣。这里的基本思想是递归地发出表达式左边的代码,然后是右边的代码,然后计算二进制表达式的结果。在这段代码中,我们简单地打开操作码,创建正确的LLVM指令。

        在上面的例子中,LLVM builder类开始显示它的值。IRBuilder知道在哪里插入新创建的指令,您所要做的就是指定要创建什么指令(例如,使用CreateFAdd)、要使用哪个操作数(这里是L和R),并可选地为生成的指令提供一个名称。

        LLVM的一个优点是它的名字只是一个提示。例如,如果上面的代码发出多个“addtmp”变量,LLVM将自动为每个变量提供一个递增的惟一数字后缀。指令的本地值名称完全是可选的,但它使读取IR转储文件变得容易得多。

        LLVM指令受到严格规则的约束:例如,add指令的左右操作符必须具有相同的类型,并且add的结果类型必须与操作数类型匹配。因为万花筒中的所有值都是双精度的,这使得add、sub和mul的代码非常简单。

       另一方面,LLVM指定fcmp指令总是返回一个“i1”值(一个1位整数)。问题是Kaleidoscope希望值是0.0或1.0。为了得到这些语义,我们将fcmp指令与uitofp指令结合起来。该指令通过将输入视为无符号值,将其输入整数转换为浮点值。相反,如果使用sitofp指令,万花筒“<”操作符将返回0.0和-1.0,具体取决于输入值。

       用LLVM生成函数调用的代码非常简单。上面的代码最初在LLVM模块的符号表中执行函数名查找。回想一下,LLVM模块是保存我们正在JIT化的函数的容器。通过赋予每个函数与用户指定的名称相同的名称,我们可以使用LLVM符号表来为我们解析函数名。

        一旦我们有了要调用的函数,我们递归地对传入的每个参数进行编码,并创建一个LLVM调用指令。注意,LLVM默认使用本机C调用约定,允许这些调用也可以调用标准库函数,如“sin”和“cos”,而不需要额外的工作。

       这就结束了我们对迄今为止万花筒中已有的四个基本表达式的处理。请随意添加一些。例如,通过浏览LLVM语言参考,您会发现其他一些有趣的指令,它们非常容易插入到我们的基本框架中。

3.4 Function Code Generation

        原型和函数的代码生成必须处理许多细节,这使得它们的代码不如表达式代码生成漂亮,但是允许我们说明一些重要的要点。首先,让我们讨论原型的代码生成:它们同时用于函数体和外部函数声明。代码以:

        这段代码在几行代码中包含了很多功能。首先注意,这个函数返回一个“function *”而不是“Value*”。因为“prototype”实际上讨论的是函数的外部接口(而不是表达式计算的值),所以返回它在codegen时对应的LLVM函数是有意义的。

        对FunctionType::get的调用创建了应该用于给定原型的函数类型。因为万花筒中的所有函数参数都是double类型的,所以第一行创建了一个向量“N”LLVM double类型。然后,它使用Functiontype::get方法创建一个函数类型,该函数类型接受“N”double作为参数,返回一个double作为结果,这不是vararg (false参数指出了这一点)。请注意,LLVM中的类型与常量一样是惟一的,所以您不会“新建”一个类型,而是“获取”它。

        上面的最后一行实际上创建了与原型对应的IR函数。这表示要使用的类型、链接和名称,以及要插入哪个模块。“外部链接”意味着函数可以定义在当前模块之外,并且/或者可以由模块之外的函数调用。传入的名称是用户指定的名称:因为指定了“TheModule”,所以这个名称注册在“TheModule”的符号表中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值