├── .gitignore ├── LICENSE ├── README.md ├── files ├── libsysy.c └── libsysy.h ├── image ├── 0-0-0.png ├── 0-0-1.png ├── 0-0-2.png ├── 0-0-3.png ├── 0-1.png ├── 0-2.png ├── 0-3.png ├── 1-1.png ├── 2-1.png ├── 4-1.png ├── 4-2.png ├── 5-1.png ├── 5-2.png └── 6-1.png ├── llvm.md └── 前端.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 echo子懿 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BUAA-Compiler2023-llvm-pro 2 | ## 2023年北京航空航天大学编译原理课程实验 3 | - 宇宙安全声明:此为预编写指导书,仅代表echo17666个人观点,与课程组无关,一切以最终版本为准。欸嘿~ 4 | 5 | ## 仙人指路 6 | - 前端:前端 7 | - 中端:中端 8 | 9 | - 如果你想看**官方指导书**,建议按照以下的顺序 10 | 11 | ![](./image/0-0-0.png) 12 | 13 | - 一,二,三**必看** 14 | - 如果目标码为**PCode**,看四(一),四(二),五(一),五(二)即可 15 | - 如果目标码为**LLVM**,看四(一),四(三)即可 16 | - 如果目标码为**MIPS**,且中间代码为**四元式**,看四(一),四(二),五(一),五(三),六(七),六(八),六(十)即可 17 | - 如果目标码为**MIPS**,且中间代码为**LLVM**,看四(一),四(三),五(一),五(三),六(全部)即可。 -------------------------------------------------------------------------------- /files/libsysy.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include"libsysy.h" 3 | /* Input & output functions */ 4 | int getint(){int t; scanf("%d",&t); return t; } 5 | int getch(){char c; scanf("%c",&c); return (int)c; } 6 | int getarray(int a[]){ 7 | int n; 8 | scanf("%d",&n); 9 | for(int i=0;i 5 | #include 6 | #include 7 | /* Input & output functions */ 8 | int getint(),getch(),getarray(int a[]); 9 | void putint(int a),putch(int a),putarray(int n,int a[]); 10 | #endif 11 | -------------------------------------------------------------------------------- /image/0-0-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echo17666/BUAA-Compiler2023-llvm-pro/f29706f016c9548324e1cf68924a47330dafc459/image/0-0-0.png -------------------------------------------------------------------------------- /image/0-0-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echo17666/BUAA-Compiler2023-llvm-pro/f29706f016c9548324e1cf68924a47330dafc459/image/0-0-1.png -------------------------------------------------------------------------------- /image/0-0-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echo17666/BUAA-Compiler2023-llvm-pro/f29706f016c9548324e1cf68924a47330dafc459/image/0-0-2.png -------------------------------------------------------------------------------- /image/0-0-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echo17666/BUAA-Compiler2023-llvm-pro/f29706f016c9548324e1cf68924a47330dafc459/image/0-0-3.png -------------------------------------------------------------------------------- /image/0-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echo17666/BUAA-Compiler2023-llvm-pro/f29706f016c9548324e1cf68924a47330dafc459/image/0-1.png -------------------------------------------------------------------------------- /image/0-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echo17666/BUAA-Compiler2023-llvm-pro/f29706f016c9548324e1cf68924a47330dafc459/image/0-2.png -------------------------------------------------------------------------------- /image/0-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echo17666/BUAA-Compiler2023-llvm-pro/f29706f016c9548324e1cf68924a47330dafc459/image/0-3.png -------------------------------------------------------------------------------- /image/1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echo17666/BUAA-Compiler2023-llvm-pro/f29706f016c9548324e1cf68924a47330dafc459/image/1-1.png -------------------------------------------------------------------------------- /image/2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echo17666/BUAA-Compiler2023-llvm-pro/f29706f016c9548324e1cf68924a47330dafc459/image/2-1.png -------------------------------------------------------------------------------- /image/4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echo17666/BUAA-Compiler2023-llvm-pro/f29706f016c9548324e1cf68924a47330dafc459/image/4-1.png -------------------------------------------------------------------------------- /image/4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echo17666/BUAA-Compiler2023-llvm-pro/f29706f016c9548324e1cf68924a47330dafc459/image/4-2.png -------------------------------------------------------------------------------- /image/5-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echo17666/BUAA-Compiler2023-llvm-pro/f29706f016c9548324e1cf68924a47330dafc459/image/5-1.png -------------------------------------------------------------------------------- /image/5-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echo17666/BUAA-Compiler2023-llvm-pro/f29706f016c9548324e1cf68924a47330dafc459/image/5-2.png -------------------------------------------------------------------------------- /image/6-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echo17666/BUAA-Compiler2023-llvm-pro/f29706f016c9548324e1cf68924a47330dafc459/image/6-1.png -------------------------------------------------------------------------------- /llvm.md: -------------------------------------------------------------------------------- 1 | # LLVM从入门到入土 2 | ## 写在前面 3 | 从这一部分开始,我们将正式开始进入**代码生成**阶段。课程组给出了三种目标码,分别是生成到 **`PCode`** ,**`LLVM IR`** ,以及 **`MIPS`** 。写MIPS需要额外进行**代码优化**的操作。这里建议有时间的同学们可以去提前调研一下这几种代码再进行选择。由于理论课上,以及编译原理的教材上主要介绍了**四元式**,且在课本最后也介绍了PCode,所以采用PCode作为目标码的同学可以主要参考**编译原理教材**。 4 | 5 | **`LLVM`** 可能看上去上手比较困难,毕竟我相信大部分同学是第一次接触,而在往年的编译原理课程中,LLVM的代码生成是软件学院的课程要求,指导书也是针对往届软件学院的编译原理实验。在2022年与计算机学院合并之后,课程组虽然也添加了LLVM的代码生成通道,但是由于课程合并后,例如**文法,实验过程,实现要求**等的不同,课程组同学在去年实验中收到了许许多多同届同学关于LLVM的问题,包括**看不懂指导书,无从下手**等问题,所以在今年的指导书中,我们将作出以下改进: 6 | - 根据今年的实验顺序,**重新编排**每一个小实验部分的顺序,使得同学们在实验过程中更加顺畅。 7 | - 对每一个部分进行**相关的说明**,帮助同学们更好地理解LLVM的代码生成过程。 8 | - 会在指导书的每一个章节结束给出一些相对**较强的测试样例**,方便同学们做一个部分就测试一个部分。 9 | - 由于LLVM本身就是一种很优秀的中间代码,所以对于想最终生成到MIPS的同学,今年的指导书中将新增LLVM的**中端优化部分**,帮助同学们更方便地从LLVM生成MIPS代码。 10 | 11 | ## 0. 简单介绍 12 | ### LLVM是什么 13 | 14 | **`LLVM`** 最早叫底层虚拟机 (Low Level Virtual Machine) ,最初是伊利诺伊大学的一个研究项目,目的是提供一种现代的、基于SSA的编译策略,能够支持任意编程语言的静态和动态编译。从那时起,LLVM已经发展成为一个由多个子项目组成的伞式项目,其中许多子项目被各种各样的商业和开源项目用于生产,并被广泛用于学术研究。 15 | 16 | 现在,LLVM被用作实现各种静态和运行时编译语言的通用基础设施(例如,GCC、Java、.NET、Python、Ruby、Scheme、Haskell、D以及无数鲜为人知的语言所支持的语言族)。它还取代了各种特殊用途的编译器,如苹果OpenGL堆栈中的运行时专用化引擎和Adobe After Effects产品中的图像处理库。最后,LLVM还被用于创建各种各样的新产品,其中最著名的可能是OpenCL GPU编程语言。 17 | 18 | > ~~看不懂不要紧,反正我也是官网上找的介绍~~~ 19 | > 20 | > 一些参考资料: 21 | > - https://aosabook.org/en/v1/llvm.html#footnote-1 22 | > - https://llvm.org/ 23 | 24 | ### 三端设计 25 | 传统静态编译器,例如大多数C语言的编译器,最主流的设计是**三端设计**,其主要组件是前端、优化器和后端。前端解析源代码,检查其错误,并构建特定语言的**抽象语法树 `AST`**(Abstract Syntax Tree)来表示输入代码。AST可以选择转换为新的目标码进行优化,优化器和后端在代码上运行。 26 | 27 | **`优化器`** 的作用是**增加代码的运行效率**,例如消除冗余计算。 **`后端`** ,也即代码生成器,负责将代码**映射到目标指令集**,其常见部分包括指令选择、寄存器分配和指令调度。 28 | 29 | 当编译器需要支持**多种源语言或目标体系结构**时,使用这种设计最重要的优点就是,如果编译器在其优化器中使用公共代码表示,那么可以为任何可以编译到它的语言编写前端,也可以为任何能够从它编译的目标编写后端,如图 0-1 所示。 30 | 31 | ![](image/0-1.png) 32 | 33 | #####

图 0-1 三端设计示意图

34 | 35 | 官网对这张设计图的描述只有一个单词 **Retargetablity**,译为**可重定向性**或**可移植性**。通俗理解,如果需要移植编译器以支持新的源语言,只需要实现一个新的前端,但现有的优化器和后端可以重用。如果不将这些部分分开,实现一种新的源语言将需要从头开始,因此,不难发现,支持 $N$ 个目标和 $M$ 种源语言需要 $N×M$ 个编译器,而采用三端设计后,中端优化器可以复用,所以只需要 $N+M$ 个编译器。例如我们熟悉的Java中的 **`JIT`** , **`GCC`** 都是采用这种设计。 36 | 37 | **`IR`** (Intermediate Representation) 的翻译即为中间表示,在基于LLVM的编译器中,前端负责解析、验证和诊断输入代码中的错误,然后将解析的代码转换为LLVM IR,中端(此处即LLVM优化器)对LLVM IR进行优化,后端则负责将LLVM IR转换为目标语言。 38 | 39 | ### 工具介绍 40 | > 讲道理这一节应该配合下一节一起食用,但是下一节需要用到这些工具,就先放前面写了。 41 | 42 | 我们的实验目标是将C语言的程序生成为LLVM IR的中间代码,尽管我们会给指导书,但不可避免地,同学们还会遇到很多不会的情况。所以这里给出一个能够自己进行代码生成测试的工具介绍,帮助大家更方便地测试和完成实验。 43 | 44 | 这里着重介绍 **`Ubuntu`** (20.04或更新) 的下载与操作,一是方便,二是感觉大家或多或少有该系统的Vmware或者云服务器等等。 45 | > 如果真的没有,腾讯云学生优惠有Ubuntu 20.04的云服务器,9.9RMB一年,如果实在不想花钱,也可以在Windows或MacOS上直接装。MacOS和Windows安装Clang和LLVM的方法请自行搜索。 46 | 47 | 首先安装 **`LLVM`** 和 **`Clang`** 48 | ```bash 49 | $ sudo apt-get install llvm 50 | $ sudo apt-get install clang 51 | ``` 52 | 安装完成后,输入指令查看版本。如果出现版本信息则说明安装成功。 53 | ```bash 54 | $ clang -v 55 | $ lli --version 56 | ``` 57 | > **注意:** 请务必保证llvm版本至少是**10.0.0 及以上**,否则**会影响正确性!** 58 | 59 | 如果使用apt无法安装,则将下列代码加入到 `/etc/apt/sources.list` 文件中 60 | ```bash 61 | deb http://apt.llvm.org/focal/ llvm-toolchain-focal-10 main 62 | deb-src http://apt.llvm.org/focal/ llvm-toolchain-focal-10 main 63 | ``` 64 | 然后在终端执行 65 | ```bash 66 | wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key|sudo apt-key add - 67 | apt-get install clang-10 lldb-10 lld-10 68 | ``` 69 | **`MacOS`** 上的安装也稍微提一嘴,需要安装XCode或XCode Command Line Tools, 其默认自带Clang 70 | ```bash 71 | xcode-select --install 72 | brew install llvm 73 | ``` 74 | 安装完成后,需要添加LLVM到$PATH 75 | ```bash 76 | echo 'export PATH="/usr/local/opt/llvm/bin:$PATH"' >> ~/.bash_profile 77 | ``` 78 | 这时候可以仿照之前查看版本的方法,如果显示版本号则证明安装成功。 79 | 80 | 我们安装的 **`Clang`** 是 LLVM 项目中 C/C++ 语言的前端,其用法与 GCC 基本相同。 81 | **`lli`** 会解释.bc 和 .ll 程序。 82 | 83 | 具体如何使用上述工具链,我们将在下一章介绍。 84 | ### LLVM IR示例 85 | LLVM IR 具有三种表示形式,一种是在**内存中**的数据结构格式,一种是在磁盘二进制 **位码 (bitcode)** 格式 **`.bc`** ,一种是**文本格式** **`.ll`** 。生成目标代码为LLVM IR的同学要求输出的是 **`.ll`** 形式的 LLVM IR。 86 | 87 | 作为一门全新的语言,与其讲过于理论的语法,不如直接看一个实例来得直观,也方便大家快速入门。 88 | 89 | 例如,我们的源程序 `main.c` 如下 90 | ```c 91 | int a=1; 92 | int add(int x,int y){ 93 | return x+y; 94 | } 95 | int main(){ 96 | int b=2; 97 | return add(a,b); 98 | } 99 | ``` 100 | 现在,我们想知道其对应的LLVM IR长什么样。这时候我们就可以用到Clang工具。下面是一些常用指令 101 | ```bash 102 | $ clang main.c -o main # 生成可执行文件 103 | $ clang -ccc-print-phases main.c # 查看编译的过程 104 | $ clang -E -Xclang -dump-tokens main.c # 生成 tokens 105 | $ clang -fsyntax-only -Xclang -ast-dump main.c # 生成语法树 106 | $ clang -S -emit-llvm main.c -o main.ll -O0 # 生成 llvm ir (不开优化) 107 | $ clang -S main.c -o main.s # 生成汇编 108 | $ clang -c main.c -o main.o # 生成目标文件 109 | ``` 110 | 输入 `clang -S -emit-llvm main.c -o main.ll` 后,会在同目录下生成一个 `main.ll` 的文件。在LLVM中,注释以';'打头。 111 | ```llvm 112 | ; ModuleID = 'main.c' 113 | source_filename = "main.c" 114 | target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" 115 | target triple = "x86_64-pc-linux-gnu" 116 | 117 | 118 | ; 从下一行开始,是实验需要生成的部分,注释不要求生成。 119 | @a = dso_local global i32 1, align 4 120 | 121 | ; Function Attrs: noinline nounwind optnone uwtable 122 | define dso_local i32 @add(i32 %0, i32 %1) #0 { 123 | %3 = alloca i32, align 4 124 | %4 = alloca i32, align 4 125 | store i32 %0, i32* %3, align 4 126 | store i32 %1, i32* %4, align 4 127 | %5 = load i32, i32* %3, align 4 128 | %6 = load i32, i32* %4, align 4 129 | %7 = add i32 %5, %6 130 | ret i32 %7 131 | } 132 | 133 | ; Function Attrs: noinline nounwind optnone uwtable 134 | define dso_local i32 @main() #0 { 135 | %1 = alloca i32, align 4 136 | %2 = alloca i32, align 4 137 | store i32 0, i32* %1, align 4 138 | store i32 2, i32* %2, align 4 139 | %3 = load i32, i32* @a, align 4 140 | %4 = load i32, i32* %2, align 4 141 | %5 = call i32 @add(i32 %3, i32 %4) 142 | ret i32 %5 143 | } 144 | 145 | ; 实验要求生成的代码到上一行即可 146 | 147 | attributes #0 = { noinline nounwind optnone uwtable ...} 148 | ; ...是我自己手动改的,因为后面一串太长了 149 | 150 | !llvm.module.flags = !{!0} 151 | !llvm.ident = !{!1} 152 | 153 | !0 = !{i32 1, !"wchar_size", i32 4} 154 | !1 = !{!"clang version 10.0.0-4ubuntu1 "} 155 | ``` 156 | 用`lli main.ll`解释执行生成的 .ll 文件。如果一切正常,输入`echo $?`查看上一条指令的返回值。 157 | 158 | 在本次实验中,我们会用到一些库函数。使用的时候请将 `libsysy.c``libsysy.h`放在同一目录下,对使用到了库函数的源程序进行编译时,需要用到如下指令: 159 | 160 | ```bash 161 | # 1. 分别导出 libsysy 和 main.c 对应的的 .ll 文件 162 | $ clang -emit-llvm -S libsysy.c -o lib.ll 163 | $ clang -emit-llvm -S main.c -o main.ll 164 | 165 | # 2. 使用 llvm-link 将两个文件链接,生成新的 IR 文件 166 | $ llvm-link main.ll lib.ll -S -o out.ll 167 | 168 | # 3. 用 lli 解释运行 169 | $ lli out.ll 170 | ``` 171 | 172 | 粗略一看,LLVM IR很长很麻烦,但仔细一看,在我们需要生成的代码部分,像是一种特殊的三元式。事实上,LLVM IR使用的是**三地址码**。我们对上述代码进行简要注释。 173 | 174 | - Module ID:指明 **`Module`** 的标识 175 | - source_filename:表明该Module是从什么文件编译得到的。如果是通过链接得到的,此处会显示 `llvm-link` 176 | - target datalayout 和 target triple 是程序标签属性说明,和硬件/系统有关。其本身也有一套相应的文法,各个部分说明如下图所示,感兴趣的同学可以自行查阅资料。 177 | 178 | ![](image/0-2.png) 179 | #####

图 0-2 target解释

180 | 181 | - `@a = dso_local global i32 1, align 4`:全局变量,名称是a,类型是i32,初始值是1,对齐方式是4字节。dso_local 表明该变量会在同一个链接单元内解析符号。 182 | - `define dso_local i32 @add(i32 %0, i32 %1) #0`:函数定义。其中第一个i32是返回值类型,%add是函数名;第二个和第三个i32是形参类型,%0,%1是形参名。 183 | > llvm中的标识符分为两种类型:全局的和局部的。全局的标识符包括函数名和全局变量,会加一个`@`前缀,局部的标识符会加一个`%`前缀。 184 | - #0指出了函数的`attribute group`。在文件的最后,也能找到对应的attributes #0。因为attribute group可能很包含很多attribute且复用到多个函数,所以我们IR使用attribute group ID(即#0)的形式指明函数的attribute,这样既简洁又清晰。 185 | - 而在大括号中间的函数体,是由一系列 **`BasicBlock`** 组成的。每个BasicBlock都有一个**label**,label使得该BasicBlock有一个符号表的入口点,其以terminator instruction(ret、br等)结尾的。每个BasicBlock由一系列 **`Instruction`** 组成。Instruction是LLVM IR的基本指令。 186 | - %7 = add i32 %5, %6:随便拿上面一条指令来说,%7是Instruction的实例,它的操作数里面有两个值,一个是%5,一个是%6。%5和%6也是Instruction的实例。 187 | 188 | 下面给出一个Module的主要架构,可以发现,LLVM中几乎所有的结构都可以认为是一个 **`Value`** ,结构与结构之间的Value传递可以简单理解为继承属性和综合属性。而 **`User`** 类和 **`Use`** 类则是LLVM中的重要概念,简单理解就是,User类存储使用Value的列表,而Use类存储Value和User的使用关系,这可以让User和Value快速找到对方。 189 | ![](image/0-3.png) 190 | #####

图 0-3 LLVM架构简图

191 | 这其中架构的设计,是为了方便LLVM的优化和分析,然后根据优化过后的Module生成后端代码。同学们可以根据自己的需要自行设计数据类型。但如果只是想生成到LLVM的同学,这部分内容其实没有那么重要,可以直接面向AST生成代码。 192 | 193 | 对于一些常用的Instructions,下面给出示例。对于一些没有给出的,可以参考[LLVM IR指令集](https://llvm.org/docs/LangRef.html#instruction-reference)。 194 | 195 | | llvm ir | 使用方法 | 简介 | 196 | | ---| --- | --- | 197 | | add | ` = add , ` | / | 198 | | sub | ` = sub , ` | / | 199 | | mul | ` = mul , ` | / | 200 | | sdiv | ` = sdiv , ` | 有符号除法 | 201 | | icmp | ` = icmp , ` | 比较指令 | 202 | | and | ` = and , ` | 与 | 203 | | or | ` = or , ` | 或 | 204 | | call | ` = call [ret attrs] ()` | 函数调用 | 205 | | alloca | ` = alloca ` | 分配内存 | 206 | | load | ` = load , * ` | 读取内存 | 207 | | store | `store , * ` | 写内存 | 208 | | getelementptr | ` = getelementptr , * {, [inrange] }*`
` = getelementptr inbounds , * {, [inrange] }*` | 计算目标元素的位置(这一章会单独详细说明) | 209 | | phi | ` = phi [fast-math-flags] [ , ], ...` |/| 210 | | zext..to | ` = zext to ` | 将 `ty`的`value`的type扩充为`ty2` | 211 | | trunc..to | ` = trunc to ` | 将 `ty`的`value`的type缩减为`ty2` | 212 | | br | `br i1 , label , label `
`br label ` | 改变控制流 | 213 | | ret | `ret ` ,`ret void ` | 退出当前函数,并返回值 | 214 | 215 | ### 一些说明 216 | - 这一部分的代码生成的目标,即输入 **`AST`**(或者**四元式**) ,输出一个 **`Module`**(便于继续生成到MIPS) 或直接输出 **`LLVM IR`** 代码。想通过LLVM IR生成到MIPS的同学可以根据对应的 **`Module`** 结构自行存储数据,然后根据Module生成对应的LLVM IR代码自测正确性。 217 | - 目标生成到LLVM语言的同学请注意,clang 默认生成的虚拟寄存器是**按数字顺序**命名的,LLVM 限制了所有数字命名的虚拟寄存器必须严格地**从 0 开始递增**,且每个函数**参数和基本块**都会占用一个编号。如果不能确定怎样用数字命名虚拟寄存器,请使用**字符串命名**虚拟寄存器。 218 | - 本章主要讲述通过 **`AST`** 生成对应的代码,这也是LLVM官网的默认推荐逻辑,当然同学们也可以通过自行设计的**四元式**生成LLVM。本质上LLVM其实是个**三地址码**,其指令其实就是一个**四元组**(结果,运算符,操作数1,操作数2,例如%3=add i32 %1, %2)不难发现,这其实就是**四元式**。所以从四元式生成LLVM可以直接一步到位,本章也就不作过多赘述。 219 | ## 1. 主函数与常量表达式 220 | ### 主函数 221 | 首先我们从最基本的开始,即只包含return语句的主函数(或没有参数的函数)。可能用到的文法包括 222 | ```c 223 | CompUnit → MainFuncDef 224 | MainFuncDef → 'int' 'main' '(' ')' Block 225 | Block → '{' { BlockItem } '}' 226 | BlockItem → Stmt 227 | Stmt → 'return' Exp ';' 228 | Exp → AddExp 229 | AddExp → MulExp 230 | MulExp → UnaryExp 231 | UnaryExp → PrimaryExp 232 | PrimaryExp → Number 233 | ``` 234 | 对于一个无参的函数,首先需要从AST获取函数的名称,返回值类型。然后分析函数体的Block。Block中的Stmt可以是return语句,也可以是其他语句,但是这里只考虑return语句。return语句中的Number在现在默认是**常数**。 235 | 所以对于一个代码生成器,我们需要实现的功能有: 236 | - 遍历**AST**,遍历到函数时,获取函数的**名称**、**返回值类型** 237 | - 遍历到**Block**内的**Stmt**时,如果是**return**语句,生成对应的**指令** 238 | ### 常量表达式 239 | 新增内容有 240 | ```c 241 | Stmt → 'return' [Exp] ';' 242 | Exp → AddExp 243 | AddExp → MulExp | AddExp ('+' | '−') MulExp 244 | MulExp → UnaryExp | MulExp ('*' | '/' | '%') UnaryExp 245 | UnaryExp → PrimaryExp | UnaryOp UnaryExp 246 | PrimaryExp → '(' Exp ')' | Number 247 | UnaryOp → '+' | '−' 248 | ``` 249 | 对于常量表达式,这里只包含常数的四则运算,正负号操作。这时候我们就需要用到之前的Value思想。举个例子,对于 `1+2+3*4`,我们生成的AST样式如下 250 | ![](image/1-1.png) 251 | #####

图 1-1 简单四则运算AST参考图

252 | 那么在生成的时候,我们的顺序是从左到右,从上到下。所以我们可以先生成 `1`,然后生成 `2`,然后生成 `1+2`,然后生成 `3`,然后生成 `4`,然后生成 `3*4`,最后生成 `1+2+3*4`。那对于1+2的**AddExp**,在其生成的指令中,1和2的值就类似于综合属性,即从AddExp的实例的值(3)由产生式右边的值(1和2)推导出来。而对于3\*4的**MulExp**,其生成的指令中3和4的值就类似于继承属性,即从MulExp的实例的值(12)由产生式左边的值(3和4)推导出来。最后,对于1+2+3\*4的**AddExp**,生成指令的实例的值就由产生式右边的AddExp的值(3)和MulExp的值(12)推导出来。 253 | 254 | 同理,对于数字前的正负,我们可以看做是**0和其做一次AddExp**,即+1其实就是0+1 (其实正号甚至都不用去管) ,-1其实就是0-1。所以在生成代码的时候,可以当作一个特殊的AddExp来处理。 255 | 256 | > 特别注意:`MulExp → UnaryExp | MulExp ('*' | '/' | '%') UnaryExp`运算中,`%`模运算的代码生成不是mod哦 257 | ### 测试样例 258 | 源程序 259 | ```c 260 | int main() { 261 | return --+---+1 * +2 * ---++3 + 4 + --+++5 + 6 + ---+--7 * 8 + ----++--9 * ++++++10 * -----11; 262 | } 263 | ``` 264 | 生成代码参考(因为最后的代码是扔到评测机上重新编译去跑的,所以生成的代码不一定要一样,但是要确保输出结果一致) 265 | ```llvm 266 | define dso_local i32 @main() { 267 | %1 = sub i32 0, 1 268 | %2 = sub i32 0, %1 269 | %3 = sub i32 0, %2 270 | %4 = sub i32 0, %3 271 | %5 = sub i32 0, %4 272 | %6 = mul i32 %5, 2 273 | %7 = sub i32 0, 3 274 | %8 = sub i32 0, %7 275 | %9 = sub i32 0, %8 276 | %10 = mul i32 %6, %9 277 | %11 = add i32 %10, 4 278 | %12 = sub i32 0, 5 279 | %13 = sub i32 0, %12 280 | %14 = add i32 %11, %13 281 | %15 = add i32 %14, 6 282 | %16 = sub i32 0, 7 283 | %17 = sub i32 0, %16 284 | %18 = sub i32 0, %17 285 | %19 = sub i32 0, %18 286 | %20 = sub i32 0, %19 287 | %21 = mul i32 %20, 8 288 | %22 = add i32 %15, %21 289 | %23 = sub i32 0, 9 290 | %24 = sub i32 0, %23 291 | %25 = sub i32 0, %24 292 | %26 = sub i32 0, %25 293 | %27 = sub i32 0, %26 294 | %28 = sub i32 0, %27 295 | %29 = mul i32 %28, 10 296 | %30 = sub i32 0, 11 297 | %31 = sub i32 0, %30 298 | %32 = sub i32 0, %31 299 | %33 = sub i32 0, %32 300 | %34 = sub i32 0, %33 301 | %35 = mul i32 %29, %34 302 | %36 = add i32 %22, %35 303 | ret i32 %36 304 | } 305 | ``` 306 | ## 2. 全局变量与局部变量 307 | 308 | ### 全局变量 309 | 本章实验涉及的文法包括: 310 | ```c 311 | CompUnit → {Decl} MainFuncDef 312 | Decl → ConstDecl | VarDecl 313 | ConstDecl → 'const' BType ConstDef { ',' ConstDef } ';' 314 | BType → 'int' 315 | ConstDef → Ident '=' ConstInitVal 316 | ConstInitVal → ConstExp 317 | ConstExp → AddExp 318 | VarDecl → BType VarDef { ',' VarDef } ';' 319 | VarDef → Ident | Ident '=' InitVal 320 | InitVal → Exp 321 | ``` 322 | 在llvm中,全局变量使用的是和函数一样的全局标识符 `@` ,所以全局变量的写法其实和函数的定义几乎一样。在我们的实验中,全局变/常量声明中指定的初值表达式必须是**常量表达式**。不妨举几个例子: 323 | ```c 324 | //以下都是全局变量 325 | int a=5; 326 | int b=2+3; 327 | ``` 328 | 生成的llvm如下所示 329 | ```llvm 330 | @a = dso_local global i32 5 331 | @b = dso_local global i32 5 332 | ``` 333 | 可以看到,对于全局变量中的常量表达式,在生成的llvm中我们需要算出其**具体的值**。 334 | 335 | ### 局部变量 336 | 本章内容涉及文法包括: 337 | ```c 338 | BlockItem → Decl | Stmt 339 | ``` 340 | 局部变量使用的标识符是 `%` 。与全局变量不同,局部变量在赋值前需要申请一块内存。在对局部变量操作的时候,我们也需要采用**load/store**来对内存进行操作。 341 | 同样的,我们举个例子来说明一下: 342 | ```c 343 | //以下都是局部变量 344 | int a=1+2; 345 | ``` 346 | 生成的llvm如下所示 347 | ```llvm 348 | %1 = alloca i32 349 | %2 = add i32 1, 2 350 | store i32 %2, i32* %1 351 | ``` 352 | 353 | ### 符号表设计与作用域 354 | 这一章我们将主要考虑变量,包括全局变量和局部变量以及作用域的说明。不可避免地,我们需要进行符号表的设计。 355 | 356 | 涉及到的文法如下: 357 | ```c 358 | Stmt → LVal '=' Exp ';' 359 | | [Exp] ';' 360 | | 'return' Exp ';' 361 | LVal → Ident 362 | PrimaryExp → '(' Exp ')' | LVal | Number 363 | ``` 364 | 我们举个最简单的例子: 365 | ```c 366 | int a=1; 367 | int b=2+a; 368 | int main(){ 369 | int c=b+4; 370 | return a+b+c; 371 | } 372 | ``` 373 | 374 | 如果我们需要将上述代码转换为llvm,我们应当怎么考虑呢?直观来看,a和b是**全局变量**,c是**局部变量**。我们首先将全局变量a和b进行赋值,然后到main函数内部,我们对c进行赋值。那么在 `return a+b+c;` 的时候,根据上个实验,llvm最后几行应该是 375 | ```llvm 376 | %sumab = add i32 %a, %b 377 | %sumabc = add i32 %sumab, %c 378 | ret i32 %sumabc 379 | ``` 380 | 问题就是,我们如何获取标识符 `%a,%b,%c`,这时候我们的符号表的作用就体现出来了。简单来说,符号表类似于一个索引。通过符号表,我们可以很快速的找到变量对应的标识符。 381 | 382 | 对于上面的c语言程序,llvm生成如下: 383 | ```llvm 384 | @a = dso_local global i32 1 385 | @b = dso_local global i32 3 386 | define dso_local i32 @main() { 387 | %1 = alloca i32 ;分配c内存 388 | %2 = load i32, i32* @b ;读取全局变量b 389 | %3 = add i32 %2, 4 ;计算b+4 390 | store i32 %3, i32* %1 ;把b+4的值存入c 391 | %4 = load i32, i32* @a ;读取全局变量a 392 | %5 = load i32, i32* @b ;读取全局变量b 393 | %6 = add i32 %4, %5 ;计算a+b; 394 | %7 = load i32, i32* %1 ;读取c 395 | %8 = add i32 %6, %7 ;计算(a+b)+c 396 | ret i32 %8 ;return 397 | } 398 | ``` 399 | 不难发现,对于全局变量的使用,可以直接使用全局变量的全局标识符(例如@a),而对于局部变量,我们则需要使用分配内存的标识符。由于标识符是自增的数字,所以快速找到对应变量的标识符就是符号表最重要的作用。同学们可以选择遍历一遍AST后造出一张统一的符号表,然后根据完整的符号表进行代码生成,也可以在遍历AST的同时造出一张栈式符号表,根据实时的栈式符号表生成相应代码。符号表存储的东西同学们可以自己设计,下面给出符号表的简略示例,同学们在实验中可以根据自己需要自行设计。 400 | 401 | 同时,同学们需要注意变量的作用域,即语句块内声明的变量的生命周期在该语句块内,且内层代码块覆盖外层代码块。 402 | ```c 403 | int a=1; 404 | int b=2; 405 | int c=3; 406 | int main(){ 407 | int d=4; 408 | int e=5; 409 | {//blockA 410 | int a=7; 411 | int e=8; 412 | int f=9; 413 | } 414 | int f=10; 415 | } 416 | ``` 417 | 在上面的程序中,在**blockA**中,a的值为7,覆盖了全局变量a=1,e覆盖了main中的e=5,而在main的最后一行,f并不存在覆盖,因为main外层不存在其他f的定义。 418 | 419 | 同样的,下面给出上述程序的llvm代码: 420 | ```llvm 421 | @a = dso_local global i32 1 422 | @b = dso_local global i32 2 423 | @c = dso_local global i32 3 424 | 425 | define dso_local i32 @main() { 426 | %1 = alloca i32 427 | store i32 4, i32* %1 428 | %2 = alloca i32 429 | store i32 5, i32* %2 430 | %3 = alloca i32 431 | store i32 7, i32* %3 432 | %4 = alloca i32 433 | store i32 8, i32* %4 434 | %5 = alloca i32 435 | store i32 9, i32* %5 436 | %6 = alloca i32 437 | store i32 10, i32* %6 438 | } 439 | ``` 440 | 上述程序的符号表简略示意如下: 441 | 442 | ![](image/2-1.png) 443 | #####

图 2-1 完整符号表与栈式符号表示意图

444 | 445 | ### 测试样例 446 | 源程序 447 | ```c 448 | int a=1; 449 | int b=2+a; 450 | int c=3*(b+------10); 451 | int main(){ 452 | int d=4+c; 453 | int e=5*d; 454 | { 455 | a=a+5; 456 | int b=a*2; 457 | a=b; 458 | int f=20; 459 | e=e+a*20; 460 | } 461 | int f=10; 462 | return e*f; 463 | } 464 | ``` 465 | llvm参考如下: 466 | ```llvm 467 | @a = dso_local global i32 1 468 | @b = dso_local global i32 3 469 | @c = dso_local global i32 39 470 | define dso_local i32 @main() { 471 | %1 = alloca i32 472 | %2 = load i32, i32* @c 473 | %3 = add i32 4, %2 474 | store i32 %3, i32* %1 475 | %4 = alloca i32 476 | %5 = load i32, i32* %1 477 | %6 = mul i32 5, %5 478 | store i32 %6, i32* %4 479 | %7 = load i32, i32* @a 480 | %8 = load i32, i32* @a 481 | %9 = add i32 %8, 5 482 | store i32 %9, i32* @a 483 | %10 = alloca i32 484 | %11 = load i32, i32* @a 485 | %12 = mul i32 %11, 2 486 | store i32 %12, i32* %10 487 | %13 = load i32, i32* @a 488 | %14 = load i32, i32* %10 489 | store i32 %14, i32* @a 490 | %15 = alloca i32 491 | store i32 20, i32* %15 492 | %16 = load i32, i32* %4 493 | %17 = load i32, i32* %4 494 | %18 = load i32, i32* @a 495 | %19 = mul i32 %18, 20 496 | %20 = add i32 %17, %19 497 | store i32 %20, i32* %4 498 | %21 = alloca i32 499 | store i32 10, i32* %21 500 | %22 = load i32, i32* %4 501 | %23 = load i32, i32* %21 502 | %24 = mul i32 %22, %23 503 | ret i32 %24 504 | } 505 | 506 | ``` 507 | echo $?的结果为**198** 508 | 509 | > 我相信各位如果去手动计算的话,会算出来结果是4550。然而由于echo $?的返回值只截取最后一个字节,也就是8位,所以 `4550 mod 256 = 198` 510 | 511 | ###### 废话,这种例子当然是随便编的awa 512 | ## 3. 函数的定义及调用 513 | > 本章主要涉及**不含数组**的函数的定义,调用等。 514 | ### 库函数 515 | 涉及文法有: 516 | ```c 517 | Stmt → LVal '=' 'getint''('')'';' 518 | | 'printf''('FormatString{','Exp}')'';' 519 | ``` 520 | 521 | 522 | 首先我们添加**库函数**的调用。在实验中,我们的llvm代码中,库函数的声明如下: 523 | ```llvm 524 | declare i32 @getint() 525 | declare void @putint(i32) 526 | declare void @putch(i32) 527 | declare void @putstr(i8*) 528 | ``` 529 | 只要在llvm代码开头加上这些声明,就可以在后续代码中使用这些库函数。同时对于用到库函数的llvm代码,我们在编译时也需要使用llvm-link命令将库函数链接到我们的代码中。 530 | 531 | 对于库函数的使用,在我们的文法中其实就包含两句,即`getint`和`printf`。其中,`printf`包含了有Exp和没有Exp的情况。同样的,我们给出一个简单的例子: 532 | ```c 533 | int main(){ 534 | int a; 535 | a=getint(); 536 | printf("hello:%d",a); 537 | return 0; 538 | } 539 | ``` 540 | llvm代码如下: 541 | ```llvm 542 | declare i32 @getint() 543 | declare void @putint(i32) 544 | declare void @putch(i32) 545 | declare void @putstr(i8*) 546 | 547 | define dso_local i32 @main() { 548 | %1 = alloca i32 549 | %2 = load i32, i32* %1 550 | %3 = call i32 @getint() 551 | store i32 %3, i32* %1 552 | %4 = load i32, i32* %1 553 | call void @putch(i32 104) 554 | call void @putch(i32 101) 555 | call void @putch(i32 108) 556 | call void @putch(i32 108) 557 | call void @putch(i32 111) 558 | call void @putch(i32 58) 559 | call void @putint(i32 %4) 560 | ret i32 0 561 | } 562 | ``` 563 | 不难看出,`call i32 @getint()` 即为调用getint的语句,对于其他的任何函数的调用也是像这样去写。而对于`printf`,我们需要将其转化为多条`putch`和`putint`的调用,或者使用`putstr`以字符串输出。这里需要注意的是,`putch`和`putint`的参数都是 `i32` 类型,所以我们需要将字符串中的字符转化为对应的ascii码。 564 | 565 | ### 函数定义与调用 566 | 涉及文法如下: 567 | ```c 568 | CompUnit → {Decl} {FuncDef} MainFuncDef 569 | FuncDef → FuncType Ident '(' [FuncFParams] ')' Block 570 | FuncType → 'void' | 'int' 571 | FuncFParams → FuncFParam { ',' FuncFParam } 572 | FuncFParam → BType Ident 573 | UnaryExp → PrimaryExp | Ident '(' [FuncRParams] ')' | UnaryOp UnaryExp 574 | ``` 575 | 其实之前的main函数也是一个函数,即主函数。这里我们将其拓广到一般函数。对于一个函数,其特征包括**函数名**,**函数返回类型**和**参数**。在本实验中,函数返回类型只有 **`int`** 和 **`void`** 两种。由于目前只有零维整数作为参数,所以参数的类型统一都是`i32`。FuncFParams之后的Block则与之前主函数内处理方法一样。值得一提的是,由于每个**临时寄存器**和**基本块**占用一个编号,所以没有参数的函数的第一个临时寄存器的编号应该从**1**开始,因为函数体入口占用了一个编号0。而有参数的函数,参数编号从**0**开始,进入Block后需要跳过一个基本块入口的编号(可以参考测试样例)。 576 | 577 | 当然,如果全部采用字符串编号寄存器,上述问题都不会存在。 578 | 579 | 对于函数的调用,参考之前库函数的处理,不难发现,函数的调用其实和**全局变量**的调用基本是一样的,即用`@函数名`表示。所以函数部分和**符号表**有着密切关联。同学们需要在函数定义和函数调用的时候对符号表进行操作。对于有参数的函数调用,则在调用的函数内传入参数。对于没有返回值的函数,则直接`call`即可,不用为语句赋一个实例。 580 | 581 | ### 测试样例 582 | 源代码: 583 | ```c 584 | int a=1000; 585 | int aaa(int a,int b){ 586 | return a+b; 587 | } 588 | void ab(){ 589 | a=1200; 590 | return; 591 | } 592 | int main(){ 593 | ab(); 594 | int b=a,a; 595 | a=getint(); 596 | printf("%d",aaa(a,b)); 597 | return 0; 598 | } 599 | ``` 600 | llvm输出参考: 601 | ```llvm 602 | declare i32 @getint() 603 | declare void @putint(i32) 604 | declare void @putch(i32) 605 | declare void @putstr(i8*) 606 | @a = dso_local global i32 1000 607 | define dso_local i32 @aaa(i32 %0, i32 %1){ 608 | %3 = alloca i32 609 | %4 = alloca i32 610 | store i32 %0, i32* %3 611 | store i32 %1, i32* %4 612 | %5 = load i32, i32* %3 613 | %6 = load i32, i32* %4 614 | %7 = add nsw i32 %5, %6 615 | ret i32 %7 616 | } 617 | define dso_local void @ab(){ 618 | store i32 1200, i32* @a 619 | ret void 620 | } 621 | 622 | define dso_local i32 @main(){ 623 | %1 = alloca i32 624 | %2 = alloca i32 625 | call void @ab() 626 | %3 = load i32, i32* @a 627 | store i32 %3, i32* %1 628 | %4 = call i32 @getint() 629 | store i32 %4, i32* %2 630 | %5 = load i32, i32* %2 631 | %6 = load i32, i32* %1 632 | %7 = call i32 @aaa(i32 %5, i32 %6) 633 | call void @putint(i32 %7) 634 | ret i32 0 635 | } 636 | ``` 637 | - 输入:1000 638 | - 输出:2200 639 | ## 4. 条件语句与短路求值 640 | ### 条件语句 641 | 涉及文法如下 642 | ```c 643 | Stmt → 'if' '(' Cond ')' Stmt [ 'else' Stmt ] 644 | Cond → LOrExp 645 | RelExp → AddExp | RelExp ('<' | '>' | '<=' | '>=') AddExp 646 | EqExp → RelExp | EqExp ('==' | '!=') RelExp 647 | LAndExp → EqExp | LAndExp '&&' EqExp 648 | LOrExp → LAndExp | LOrExp '||' LAndExp 649 | ``` 650 | 在条件语法中,我们需要进行条件的判断与选择。这时候就涉及到基本块的标号。在llvm中,每个**临时寄存器**和**基本块**占用一个编号。所以对于纯数字编号的llvm,这里就需要进行**回填**操作。对于在代码生成前已经完整生成符号表的同学,这里就会显得十分容易。对于在代码生成同时生成符号表的同学,也可以采用**栈**的方式去回填编号,对于采用字符串编号的则没有任何要求。 651 | 652 | 要写出条件语句,首先要理清楚逻辑。在上述文法中,最重要的莫过于下面这一条语法 653 | ```c 654 | Stmt → 'if' '(' Cond ')' Stmt1 [ 'else' Stmt2 ] (BasicBlock3) 655 | ``` 656 | 为了方便说明,对上述文法的两个Stmt编号为Stmt1和2。在这条语句之后基本块假设叫BasicBlock3。不难发现,条件判断的逻辑如左下图。 657 | ![](image/4-1.png) 658 | #####

图 4-1 条件判断流程示意图与基本块流图

659 | 660 | 首先进行Cond结果的判断,如果结果为**1**则进入**Stmt1**,如果Cond结果为**0**,若文法有else则将进入**Stmt2**,否则进入下一条文法的基本块**BasicBlock3**。在Stmt1或Stmt2执行完成后都需要跳转到BasicBlock3。对于一个llvm程序来说,对一个含else的条件分支,其基本块构造可以如右上图所示。 661 | 662 | 如果能够理清楚基本块跳转的逻辑,那么在写代码的时候就会变得十分简单。 663 | 664 | 这时候我们再回过头去看Cond里面的代码,即LOr和Land,Eq和Rel。不难发现,其处理方式和加减乘除非常像,除了运算结果都是1位(i1)而非32位(i32)。同学们可能需要用到 `trunc`或者 `zext` 指令进行类型转换。 665 | 666 | ### 短路求值 667 | 可能有的同学会认为,反正对于llvm来说,跳转与否只看Cond的值,所以我只要把Cond算完结果就行,不会影响正确性。不妨看一下下面这个例子: 668 | ```c 669 | int a=5; 670 | int change(){ 671 | a=6; 672 | return a; 673 | } 674 | int main(){ 675 | if(1||change()){ 676 | printf("%d",a); 677 | } 678 | return 0; 679 | } 680 | ``` 681 | 如果要将上面这段代码翻译为llvm,同学们会怎么做?如果按照传统方法,即先**统一计算Cond**,则一定会执行一次 **`change()`** 函数,把全局变量的值变为**6**。但事实上,由于短路求值的存在,在读完1后,整个Cond的值就**已经被确定**了,即无论`1||`后面跟的是什么,都不影响Cond的结果,那么根据短路求值,后面的东西就不应该执行。所以上述代码的输出应当为**5**而不是6,也就是说,我们的llvm不能够单纯的把Cond计算完后再进行跳转。这时候我们就需要对Cond的跳转逻辑进行改写。 682 | 683 | 改写之前我们不妨思考一个问题,即什么时候跳转。根据短路求值,只要条件判断出现“短路”,即不需要考虑后续与或参数的情况下就已经能确定值的时候,就可以进行跳转。或者更简单的来说,当**LOrExp值为1**或者**LAndExp值为0**的时候,就已经没有必要再进行计算了。 684 | ```c 685 | Cond → LOrExp 686 | LAndExp → LAndExp '&&' EqExp 687 | LOrExp → LOrExp '||' LAndExp 688 | ``` 689 | - 对于连或来说,只要其中一个LOrExp或最后一个LAndExp为1,即可直接跳转Stmt1。 690 | - 对于连与来说,只要其中一个LAndExp或最后一个EqExp为0,则直接进入下一个LOrExp。如果当前为连或的最后一项,则直接跳转Stmt2(有else)或BasicBlock3(没else) 691 | 692 | 上述两条规则即为短路求值的最核心算法,示意图如下。 693 | ![](image/4-2.png) 694 | #####

图 4-2 短路求值算法示意图

695 | 696 | ### 测试样例 697 | ```c 698 | int a=1; 699 | int func(){ 700 | a=2;return 1; 701 | } 702 | 703 | int func2(){ 704 | a=4;return 10; 705 | } 706 | int func3(){ 707 | a=3;return 0; 708 | } 709 | int main(){ 710 | if(0||func()&&func3()||func2()){printf("%d--1",a);} 711 | if(1||func3()){printf("%d--2",a);} 712 | if(0||func3()||func() 注:由于历史遗留问题,跳转参考代码较乱,参考价值不大,同学们可以自行设计,仅需要保证短路求值正确性即可。 822 | - 输出:4--14--24--3 823 | ## 5. 循环与中断 824 | ### 循环 825 | 涉及文法如下: 826 | ```c 827 | Stmt → 'for' '(' [ForStmt] ';' [Cond] ';' [ForStmt] ')' Stmt 828 | | 'break' ';' 829 | | 'continue' ';' 830 | ForStmt → LVal '=' Exp 831 | ``` 832 | 如果经过了上一章的学习,这一章其实难度就小了不少。对于这条文法,同样可以改写为 833 | ```c 834 | Stmt → 'for' '(' [ForStmt1] ';' [Cond] ';' [ForStmt2] ')' Stmt (BasicBlock) 835 | ``` 836 | 如果查询C语言的for循环,其中对for循环的描述为: 837 | ```c 838 | for(initialization;condition;incr/decr){ 839 | //code to be executed 840 | } 841 | ``` 842 | 不难发现,实验文法中的ForStmt1,Cond,ForStmt2分别表示了上述for循环中的**初始化(initialization)**,**条件(condition)**和**增量/减量(increment/decrement)**。同学们去搜索C语言的for循环逻辑的话也会发现,for循环的逻辑可以表述为 843 | - 1.执行初始化表达式ForStmt1 844 | - 2.执行条件表达式Cond,如果为1执行循环体Stmt,否则结束循环执行BasicBlock 845 | - 3.执行完循环体Stmt后执行增量/减量表达式ForStmt2 846 | - 4.重复执行步骤2和步骤3 847 | 848 | ![](image/5-1.png) 849 | 850 | #####

图 5-1 for循环流程图

851 | ### break/continue 852 | 对于`break`和`continue`,直观理解为,break**跳出循环**,continue**跳过本次循环**。再通俗点说就是,break跳转到的是**BasicBlock**,而continue跳转到的是**ForStmt2**。这样就能达到目的了。所以,对于循环而言,跳转的位置很重要。这也是同学们在编码的时候需要着重注意的点。 853 | 854 | 同样的,针对这两条指令,对上图作出一定的修改,就是整个循环的流程图了。 855 | 856 | ![](image/5-2.png) 857 | 858 | #####

图 5-2 for循环完整流程图

859 | ### 测试样例 860 | ```c 861 | int main(){ 862 | int a1=1,a2; 863 | a2=a1; 864 | int temp; 865 | int n,i; 866 | n=getint(); 867 | for(i=a1*a1;i19){ 876 | break; 877 | } 878 | } 879 | return 0; 880 | } 881 | ``` 882 | ```llvm 883 | declare i32 @getint() 884 | declare void @putint(i32) 885 | declare void @putch(i32) 886 | declare void @putstr(i8*) 887 | define dso_local i32 @main() { 888 | %1 = alloca i32 889 | store i32 1, i32* %1 890 | %2 = alloca i32 891 | %3 = load i32, i32* %1 892 | store i32 %3, i32* %2 893 | %4 = alloca i32 894 | %5 = alloca i32 895 | %6 = alloca i32 896 | %7 = call i32 @getint() 897 | store i32 %7, i32* %5 898 | %8 = load i32, i32* %1 899 | %9 = load i32, i32* %1 900 | %10 = mul i32 %8, %9 901 | store i32 %10, i32* %6 902 | br label %11 903 | 904 | 11: 905 | %12 = load i32, i32* %6 906 | %13 = load i32, i32* %5 907 | %14 = add i32 %13, 1 908 | %15 = icmp slt i32 %12, %14 909 | %16 = zext i1 %15 to i32 910 | %17 = icmp ne i32 0, %16 911 | br i1 %17, label %18, label %44 912 | 913 | 18: 914 | %19 = load i32, i32* %2 915 | store i32 %19, i32* %4 916 | %20 = load i32, i32* %2 917 | %21 = load i32, i32* %1 918 | %22 = load i32, i32* %2 919 | %23 = add i32 %21, %22 920 | store i32 %23, i32* %2 921 | %24 = load i32, i32* %4 922 | store i32 %24, i32* %1 923 | br label %25 924 | 925 | 25: 926 | %26 = load i32, i32* %6 927 | %27 = srem i32 %26, 2 928 | %28 = icmp eq i32 %27, 1 929 | %29 = zext i1 %28 to i32 930 | %30 = icmp ne i32 0, %29 931 | br i1 %30, label %31, label %32 932 | 933 | 31: 934 | br label %41 935 | 936 | 32: 937 | %33 = load i32, i32* %6 938 | %34 = load i32, i32* %1 939 | call void @putch(i32 114) 940 | call void @putch(i32 111) 941 | call void @putch(i32 117) 942 | call void @putch(i32 110) 943 | call void @putch(i32 100) 944 | call void @putch(i32 32) 945 | call void @putint(i32 %33) 946 | call void @putch(i32 58) 947 | call void @putch(i32 32) 948 | call void @putint(i32 %34) 949 | call void @putch(i32 10) 950 | br label %35 951 | 952 | 35: 953 | %36 = load i32, i32* %6 954 | %37 = icmp sgt i32 %36, 19 955 | %38 = zext i1 %37 to i32 956 | %39 = icmp ne i32 0, %38 957 | br i1 %39, label %40, label %41 958 | 959 | 40: 960 | br label %44 961 | 962 | 41: 963 | %42 = load i32, i32* %6 964 | %43 = add i32 %42, 1 965 | store i32 %43, i32* %6 966 | br label %11 967 | 968 | 44: 969 | ret i32 0 970 | } 971 | ``` 972 | - 输入:10 973 | - 输出: 974 | 975 | round 2: 2 976 | round 4: 5 977 | round 6: 13 978 | round 8: 34 979 | round 10: 89 980 | - 输入:40 981 | - 输出: 982 | - 983 | round 2: 2 984 | round 4: 5 985 | round 6: 13 986 | round 8: 34 987 | round 10: 89 988 | round 12: 233 989 | round 14: 610 990 | round 16: 1597 991 | round 18: 4181 992 | round 20: 10946 993 | > 本质为一个只输出20以内偶数项的斐波那契数列 994 | ## 6. 数组与函数 995 | ### 数组 996 | 数组涉及的文法相当多,包括以下几条: 997 | ```c 998 | ConstDef → Ident { '[' ConstExp ']' } '=' ConstInitVal 999 | ConstInitVal → ConstExp | '{' [ ConstInitVal { ',' ConstInitVal } ] '}' 1000 | VarDef → Ident { '[' ConstExp ']' } | Ident { '[' ConstExp ']' } '=' InitVal 1001 | InitVal → Exp | '{' [ InitVal { ',' InitVal } ] '}' 1002 | FuncFParam → BType Ident ['[' ']' { '[' ConstExp ']' }] 1003 | LVal → Ident {'[' Exp ']'} 1004 | ``` 1005 | 在数组的编写中,同学们会频繁用到 **`getElementPtr`** 指令,故先系统介绍一下这个指令的用法。 1006 | 1007 | getElementPtr指令的工作是计算地址。其本身不对数据做任何访问与修改。其语法如下: 1008 | ```llvm 1009 | = getelementptr , * , { }* 1010 | ``` 1011 | 现在我们来理解一下上面这一条指令。第一个 `` 表示的是第一个索引所指向的类型,有时也是**返回值的类型**。第二个 `` 表示的是后面的指针基地址 `` 的类型, ` ` 表示的是一组索引的类型和值,在本实验中索引的类型为i32。索引指向的基本类型确定的是增加索引值时指针的偏移量。 1012 | 1013 | 说完理论,我们结合一个实例来讲解。考虑数组 **`a[5][7]`**,需要获取 **`a[3][4]`** 的地址我们有如下写法: 1014 | ```llvm 1015 | %1 = getelementptr [5 x [7 x i32]], [5 x [7 x i32]]* @a, i32 0, i32 3 1016 | %2 = getelementptr [7 x i32], [7 x i32]* %1, i32 0, i32 4 1017 | 1018 | %3 = getelementptr [5 x [7 x i32]], [5 x [7 x i32]]* @a, i32 0, i32 3, i32 4 1019 | 1020 | %4 = getelementptr [5 x [7 x i32]], [5 x [7 x i32]]* @a, i32 0, i32 0 1021 | %5 = getelementptr [7 x i32], [7 x i32]* @4, i32 3, i32 0, 1022 | %6 = getelementptr i32, i32* %5, i32 4 1023 | ``` 1024 | 1025 | ![](image/6-1.png) 1026 | 1027 | #####

图 6-1 getElementPtr示意图

1028 | 1029 | 对于 **`%6`** ,其只有一组索引**i32 4**,所以索引使用基本类型为**i32**,基地址为 **`%5`** ,索引值为4,所以指针相对于%5(21号格子)前进了**4个i32**类型,即指向了25号格子,返回类型为i32*。 1030 | 1031 | 而当存在多组索引值的时候,每多一组索引值,索引使用基本类型就要去掉一层。再拿上面举个例子,对于 **`%1`** ,基地址为@a,类型为 **[5 x [7 x i32]]** 。第一组索引值为**0**,所以指针前进0个0x5x7个i32,第二组索引值为**3**,这时候就要**去掉一层**,即把[5 x [7 x i32]]中的5去掉,即索引使用的基本类型为 **[7 x i32]** ,指针向前移动3x7个i32。返回类型为 **[7 x i32]\***,而对于 **`%2`** ,第一组索引值为**0**,其首先前进了0x7个i32,第二组索引值为**4**,去掉一层,索引基本类型为i32,指针向前移动4个i32,指向25号格子 1032 | 1033 | 当然,可以一步到位,如 **`%3`** ,后面跟了三组索引值,第一组让指针前进0x5x7个i32,第二组让指针前进3x7个i32,第三组让指针前进4个i32,索引使用基本类型去掉两层,为**i32\***。 1034 | 1035 | 对于 **`%4`** ,虽然其两组索引值都是0,但是其索引使用基本类型去掉了一层,变为了 **[7 x i32]** 。在 **`%5`** 的时候,第一组索引值为3,即指针前进3x7个i32,第二组索引值为0,即指针前进0个i32,索引使用基本类型变为i32,返回指针类型为 **i32\***。 1036 | 1037 | 当然,同学们也可以直接将所有高维数组模拟为1维数组,例如对于**a[5][7]**中取**a[3][4]**,可以直接将a转换为一个**a[35]**,然后指针偏移7x3+4=25,直接取**a[25]**。 1038 | ### 数组定义与调用 1039 | 这一章我们将主要讲述数组定义和调用,包括全局数组,局部数组的定义,以及函数中的数组调用。对于全局数组定义,与全局变量一样,我们需要将所有量**全部计算到特定的值**。同时,对于全局数组,对数组中空缺的值,需要**置0**。对于全是0的地方,可以采用 **`zeroinitializer`** 来统一置0。 1040 | ```c 1041 | int a[1+2+3+4]={1,1+1,1+3-1}; 1042 | int b[10][20]; 1043 | int c[5][5]={{1,2,3},{1,2,3,4,5}}; 1044 | ``` 1045 | ```llvm 1046 | @a = dso_local global [10 x i32] [i32 1, i32 2, i32 3, i32 0, i32 0, i32 0, i32 0, i32 0, i32 0, i32 0] 1047 | @b = dso_local global [10 x [20 x i32]] zeroinitializer 1048 | @c = dso_local global [5 x [5 x i32]] [[5 x i32] [i32 1, i32 2, i32 3, i32 0, i32 0], [5 x i32] [i32 1, i32 2, i32 3, i32 4, i32 5], [5 x i32] zeroinitializer, [5 x i32] zeroinitializer, [5 x i32] zeroinitializer] 1049 | ``` 1050 | 当然,zeroinitializer不是必须的,同学们完全可以一个个**i32 0**写进去,但对于一些很阴间的样例点,不用zeroinitializer可能会导致 **`TLE`** ,例如全局数组 `int a[1000];` ,不使用该指令就需要输出**1000次i32 0**,必然导致TLE,所以还是推荐同学们使用zeroinitializer。 1051 | 1052 | 对于局部数组,在定义的时候同样需要使用`alloca`指令,其存取指令同样采用**load和store**,只是在此之前需要采用`getelementptr`获取数组内应位置的地址。 1053 | 1054 | 对于数组传参,其中涉及到维数的变化问题,例如,对于参数中**含维度的数组**,同学们可以参考上述`getelementptr`指令自行设计,因为该指令很灵活,所以下面的测试样例仅仅当一个参考。同学们可以将自己生成的llvm使用**lli**编译后自行查看输出比对。 1055 | ### 测试样例 1056 | ```c 1057 | int a[3+3]={1,2,3,4,5,6}; 1058 | int b[3][3]={{3,6+2,5},{1,2}}; 1059 | void a1(int x){ 1060 | if(x>1){ 1061 | a1(x-1); 1062 | } 1063 | return; 1064 | } 1065 | int a2(int x,int y[]){ 1066 | return x+y[2]; 1067 | } 1068 | int a3(int x,int y[],int z[][3]){ 1069 | return x*y[1]-z[2][1]; 1070 | } 1071 | int main(){ 1072 | int c[2][3]={{1,2,3}}; 1073 | a1(c[0][2]); 1074 | int x=a2(a[4],a); 1075 | int y=a3(b[0][1],b[1],b); 1076 | printf("%d",x+y); 1077 | return 0; 1078 | } 1079 | ``` 1080 | ```llvm 1081 | declare i32 @getint() 1082 | declare void @putint(i32) 1083 | declare void @putch(i32) 1084 | declare void @putstr(i8*) 1085 | @a = dso_local global [6 x i32] [i32 1, i32 2, i32 3, i32 4, i32 5, i32 6] 1086 | @b = dso_local global [3 x [3 x i32]] [[3 x i32] [i32 3, i32 8, i32 5], [3 x i32] [i32 1, i32 2, i32 0], [3 x i32] zeroinitializer] 1087 | define dso_local void @a1(i32 %0) { 1088 | %2 = alloca i32 1089 | store i32 %0, i32 * %2 1090 | br label %3 1091 | 1092 | 3: 1093 | %4 = load i32, i32* %2 1094 | %5 = icmp sgt i32 %4, 1 1095 | %6 = zext i1 %5 to i32 1096 | %7 = icmp ne i32 0, %6 1097 | br i1 %7, label %8, label %11 1098 | 1099 | 8: 1100 | %9 = load i32, i32* %2 1101 | %10 = sub i32 %9, 1 1102 | call void @a1(i32 %10) 1103 | br label %11 1104 | 1105 | 11: 1106 | ret void 1107 | } 1108 | define dso_local i32 @a2(i32 %0, i32* %1) { 1109 | %3 = alloca i32* 1110 | store i32* %1, i32* * %3 1111 | %4 = alloca i32 1112 | store i32 %0, i32 * %4 1113 | %5 = load i32, i32* %4 1114 | %6 = load i32*, i32* * %3 1115 | %7 = getelementptr i32, i32* %6, i32 2 1116 | %8 = load i32, i32* %7 1117 | %9 = add i32 %5, %8 1118 | ret i32 %9 1119 | } 1120 | define dso_local i32 @a3(i32 %0, i32* %1, [3 x i32] *%2) { 1121 | %4 = alloca [3 x i32]* 1122 | store [3 x i32]* %2, [3 x i32]* * %4 1123 | %5 = alloca i32* 1124 | store i32* %1, i32* * %5 1125 | %6 = alloca i32 1126 | store i32 %0, i32 * %6 1127 | %7 = load i32, i32* %6 1128 | %8 = load i32*, i32* * %5 1129 | %9 = getelementptr i32, i32* %8, i32 1 1130 | %10 = load i32, i32* %9 1131 | %11 = mul i32 %7, %10 1132 | %12 = load [3 x i32] *, [3 x i32]* * %4 1133 | %13 = getelementptr [3 x i32], [3 x i32]* %12, i32 2 1134 | %14 = getelementptr [3 x i32], [3 x i32]* %13, i32 0, i32 1 1135 | %15 = load i32, i32 *%14 1136 | %16 = sub i32 %11, %15 1137 | ret i32 %16 1138 | } 1139 | 1140 | define dso_local i32 @main() { 1141 | %1 = alloca [2 x [ 3 x i32]] 1142 | %2 = getelementptr [2 x [3 x i32]], [2 x [3 x i32]]*%1, i32 0, i32 0, i32 0 1143 | store i32 1, i32* %2 1144 | %3 = getelementptr [2 x [3 x i32]], [2 x [3 x i32]]*%1, i32 0, i32 0, i32 1 1145 | store i32 2, i32* %3 1146 | %4 = getelementptr [2 x [3 x i32]], [2 x [3 x i32]]*%1, i32 0, i32 0, i32 2 1147 | store i32 3, i32* %4 1148 | %5 = getelementptr [2 x [3 x i32]], [2 x [3 x i32]]*%1, i32 0, i32 0, i32 2 1149 | %6 = load i32, i32* %5 1150 | call void @a1(i32 %6) 1151 | %7 = alloca i32 1152 | %8 = getelementptr [6 x i32], [6 x i32]* @a, i32 0, i32 4 1153 | %9 = load i32, i32* %8 1154 | %10 = getelementptr [6 x i32], [6 x i32]* @a, i32 0, i32 0 1155 | %11 = call i32 @a2(i32 %9, i32* %10) 1156 | store i32 %11, i32* %7 1157 | %12 = alloca i32 1158 | %13 = getelementptr [3 x [3 x i32]], [3 x [3 x i32]]* @b, i32 0, i32 0, i32 1 1159 | %14 = load i32, i32* %13 1160 | %15 = mul i32 1, 3 1161 | %16 = getelementptr [3 x [3 x i32]], [3 x [3 x i32]]* @b, i32 0, i32 0 1162 | %17 = getelementptr [3 x i32], [3 x i32]* %16, i32 0, i32 %15 1163 | %18 = getelementptr [3 x [3 x i32]], [3 x [3 x i32]]* @b, i32 0, i32 0 1164 | %19 = call i32 @a3(i32 %14, i32* %17, [3 x i32]* %18) 1165 | store i32 %19, i32* %12 1166 | %20 = load i32, i32* %7 1167 | %21 = load i32, i32* %12 1168 | %22 = add i32 %20, %21 1169 | call void @putint(i32 %22) 1170 | ret i32 0 1171 | } 1172 | ``` 1173 | - 输出:24 -------------------------------------------------------------------------------- /前端.md: -------------------------------------------------------------------------------- 1 | # 从零开始 2 | ## 免责声明 3 | 这个是我自己根据自己的经验写的一份指导书,目前分为前端中端,前端是这份指导书,包含词法分析和语法分析,中端为llvm.md,包含代码生成。前端指导书为**个人经验指导书**,仅供参考,肯定没有课程指导书全面,中端指导书为**课程组llvm部分指导书的预编写版本**,大差不差,不代表课程组,只代表echo17666个人观点,如有错误,欢迎指正。 4 | 5 | ## 总览 6 | 前端占实验总分的40%,包含期中考试和错误处理部分,前端部分和理论部分密切相关,即只要理论认真听讲实验是没有难度的。建议先将编译理论教材的前四章看完,尤其重点关注以下内容 7 | - **第一章编译总览**:编译器的五个部分,即**词法分析,语法分析,语义分析和中间代码生成,代码优化,目标码生成**。同时**符号表**和**错误处理**贯穿整个编译器。这是编译的基本步骤,了解之后对编译器的整体设计有较大帮助。 8 | - **第二章文法解读**:看懂文法即可,尤其关注**扩充的BNF范式**,这是编译器的基础,实验给的文法也是基于扩充BNF范式给的,后面的词法分析和语法分析都是基于文法的。同时关注语法树的构建,这是语法分析的基础。 9 | - **第三章词法分析**:重点关注分词,即如何将文件里面的一串字符分为一个个词,并且分为不同类别(保留字,标识符,常数,运算符,界符等),这是词法分析的基础。 10 | - **第四章语法分析**:重点关注**递归下降子程序**。理论看懂实验就会写,亲测好用。 11 | 12 | 具体而言,前端不需要什么特殊的数据结构,具体来说,只要实现词法器+语法器就行。 13 | ## 词法分析 14 | 词法分析其实和理论没什么相关性,只需要知道,词法分析器,输入的是最原始的.txt文件,输出的是一个**Token库**就行。 15 | 16 | 那Token库是什么呢?举个栗子,比如说我们有一个.txt文件,内容如下 17 | ```c 18 | int main(){ 19 | int n; 20 | int i=0 21 | n=getint(); 22 | for(;;){ 23 | i=i+1; 24 | if(i%2==1){ 25 | continue; 26 | } 27 | printf("%d\n",i) 28 | if(i>n){ 29 | break; 30 | } 31 | } 32 | printf("hi"); 33 | return 0; 34 | } 35 | ``` 36 | 输出的是Token库,内容如下 37 | ```c 38 | INTTK int 39 | MAINTK main 40 | LPARENT ( 41 | RPARENT ) 42 | LBRACE { 43 | INTTK int 44 | IDENFR n 45 | SEMICN ; 46 | INTTK int 47 | IDENFR i 48 | SEMICN ; 49 | IDENFR n 50 | ASSIGN = 51 | GETINTTK getint 52 | LPARENT ( 53 | RPARENT ) 54 | SEMICN ; 55 | FORTK for 56 | LPARENT ( 57 | IDENFR i 58 | ASSIGN = 59 | INTCON 0 60 | SEMICN ; 61 | IDENFR i 62 | LSS < 63 | IDENFR n 64 | SEMICN ; 65 | IDENFR i 66 | ASSIGN = 67 | IDENFR i 68 | PLUS + 69 | INTCON 1 70 | RPARENT ) 71 | LBRACE { 72 | IFTK if 73 | LPARENT ( 74 | IDENFR i 75 | MOD % 76 | INTCON 2 77 | EQL == 78 | INTCON 1 79 | RPARENT ) 80 | LBRACE { 81 | CONTINUETK continue 82 | SEMICN ; 83 | RBRACE } 84 | PRINTFTK printf 85 | LPARENT ( 86 | STRCON "%d\n" 87 | COMMA , 88 | IDENFR i 89 | RPARENT ) 90 | SEMICN ; 91 | RBRACE } 92 | PRINTFTK printf 93 | LPARENT ( 94 | STRCON "hi" 95 | RPARENT ) 96 | SEMICN ; 97 | RETURNTK return 98 | INTCON 0 99 | SEMICN ; 100 | RBRACE } 101 | ``` 102 | 不难看出,词法分析中最主要的就是**分词器**,即将.txt文件中的一串字符分为一个个词,并判断类别。如何去将字符串分成一个个的词便是词法分析的核心。我个人建议将词分为两类,一类是特殊符号,即遇到特殊符号就开始分词,例如 **+ - * / , ; ( )** 等,遇到特殊符号或空格或换行符之后就开始分词,然后对于分出来的词去判断是否为保留字,例如 **main,const,int,if,else,break,continue** 等,如果为保留字按保留字处理,否则判断是否为纯数字,是的话按数字处理,否则按IDENT即一般字符处理。这样就可以将.txt文件分为一个个的词了。 103 | 104 | 值得注意的是,词法分析的难点在于注释的判断,即 **//** 后的一整行内容都不进入词法分析,而 **/\* \*/** 中间的内容也不进入词法分析,即使这之间隔了几十行。可以说,词法分析需要包含各种阴间情况,例如 **\*/** 之后刚好是EOF或者\n,或者//和/*同时出现等等,这些是需要同学自己去考虑的。词法分析作为编译器的第一步,需要保证**绝对的正确性**,即使是一个字符的错误都不能出现,否则后面的语法分析就会出现各种各样的问题,所以词法分析的正确性是编译器的基础。 105 | 106 | - p.s. 词法分析完全可以自己写,一方面是因为不难,一方面是因为怎么实现都可以,而之后的语法分析的处理方法较为同质化,所以词法分析自己写也可以降低查重率。 107 | 108 | 109 | ## 语法分析 110 | 语法分析的写法非常多,但是最简单方便的还是**递归下降子程序**,一是因为好实现,二是因为理论教过。所以简单。(以及,递归下降子程序是小测必考题目) 111 | 112 | 语法分析的作用是,输入词法分析生成的**Token库**,输出**抽象语法树AST**,当然也可以输出四元式,但我个人并不推荐这么做。 113 | 114 | 什么是AST?这里我借用中端的一张插图大概就能理解了。 115 | 116 | ![](./image/1-1.png) 117 | 118 | 如果我们仔细观察文法,会发现任何一个符合文法的程序,最后都会拆解成到Token级别,也就是说,我们可以通过输入的有顺序的**Token表**去匹配文法,然后**自底向上构建一棵树**。 119 | 120 | 举个栗子,如果源程序如下 121 | ```c 122 | int main(){ 123 | int a=1; 124 | int b=3+a*2; 125 | return 0; 126 | } 127 | ``` 128 | 拿出我们的文法表,首先我们匹配**CompUnit**,即最外面一层文法,然后判断**当前Token**为 **`int`** 。此时匹配文法,寻找文法的**First集**(不知道什么是First集合的罚你去复习理论知识),发现FuncDef和MainFuncDef的**First集**都包含int,这时候判断下一个token,发现下一位token是 **`main`** ,可以和MainFuncDef匹配上,故进入MainFuncDef文法。 129 | 130 | 在MainFuncDef中,依次匹配 **`int`** , **`main`** , **`(`** , **`)`** 。此时Token为 **`{`** ,匹配文法依次进入**Block**,(BlockItem不输出),(Decl不输出),**VarDecl**,然后读到 **`int`** ,和BType匹配(输出的时候不考虑BType),然后进入**VarDef**,读到 **`a`** ,和Ident匹配,然后匹配 **`=`** ,进入**InitVal**,**Exp**,**AddExp**,**MulExp**,**UnaryExp**,**PrimaryExp**,**Number**,然后在Number之后展开是 **`1`** (输出的时候不考虑IntConst) 131 | 132 | 如果上述过程你能完整跟下来,那这时候不难发现,Number读完了,这时候**Number**文法展开结束了,我们这时候可以将Token建立一颗树,即将文法左边作为根节点,文法右边作为叶节点,建立一颗树,如下所示。 133 | 134 |
135 | 136 |
137 | 138 | #####

图 0-1 `1` 的抽象语法树

139 | 140 | 141 | 返回上一层文法,**PrimaryExp**文法也展开完了,然后**UnaryExp**,**MulExp**,**AddExp**,**Exp**,**InitVal**文法都展开完了。这时候,当文法展开完之后,我们就可以开始自底部向上建树了。 142 | 143 | 这时候注意,我们的分析还未结束,我们此时在**VarDef**中,下一个Token是 **`;`** ,匹配成功后,VarDef也展开完成,这时候回到上一级,**VarDecl**也展开完成。由此,我们完成了对 **`int a=1;`** 这一句的语法分析,并构建了语法树。 144 | 145 | ![](./image/0-0-2.png) 146 | 147 | #####

图 0-2 `int a=1;` 的抽象语法树

148 | 149 | 150 | 以此类推,我们可以得到上述整个程序的抽象语法树。 151 | 152 | 153 | ![](./image/0-0-3.png) 154 | 155 | #####

图 0-3 `整个程序` 的抽象语法树

156 | 157 | 此时不难发现,当我们对着这棵树进行**后序遍历**的时候,我们就得到了正确的输出。 158 | 159 | ```c 160 | INTTK int 161 | MAINTK main 162 | LPARENT ( 163 | RPARENT ) 164 | LBRACE { 165 | INTTK int 166 | IDENFR a 167 | ASSIGN = 168 | INTCON 1 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | SEMICN ; 178 | 179 | INTTK int 180 | IDENFR b 181 | ASSIGN = 182 | INTCON 3 183 | 184 | 185 | 186 | 187 | 188 | PLUS + 189 | IDENFR a 190 | 191 | 192 | 193 | 194 | MULT * 195 | INTCON 2 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | SEMICN ; 205 | 206 | RETURNTK return 207 | INTCON 0 208 | 209 | 210 | 211 | 212 | 213 | 214 | SEMICN ; 215 | 216 | RBRACE } 217 | 218 | 219 | 220 | ``` 221 | --------------------------------------------------------------------------------