图 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 |  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 |  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 | `图 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 |  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 |  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 |  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()图 5-1 for循环流程图
851 | ### break/continue 852 | 对于`break`和`continue`,直观理解为,break**跳出循环**,continue**跳过本次循环**。再通俗点说就是,break跳转到的是**BasicBlock**,而continue跳转到的是**ForStmt2**。这样就能达到目的了。所以,对于循环而言,跳转的位置很重要。这也是同学们在编码的时候需要着重注意的点。 853 | 854 | 同样的,针对这两条指令,对上图作出一定的修改,就是整个循环的流程图了。 855 | 856 |  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;i图 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 |  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 |图 0-1 `1` 的抽象语法树
139 | 140 | 141 | 返回上一层文法,**PrimaryExp**文法也展开完了,然后**UnaryExp**,**MulExp**,**AddExp**,**Exp**,**InitVal**文法都展开完了。这时候,当文法展开完之后,我们就可以开始自底部向上建树了。 142 | 143 | 这时候注意,我们的分析还未结束,我们此时在**VarDef**中,下一个Token是 **`;`** ,匹配成功后,VarDef也展开完成,这时候回到上一级,**VarDecl**也展开完成。由此,我们完成了对 **`int a=1;`** 这一句的语法分析,并构建了语法树。 144 | 145 |  146 | 147 | #####图 0-2 `int a=1;` 的抽象语法树
148 | 149 | 150 | 以此类推,我们可以得到上述整个程序的抽象语法树。 151 | 152 | 153 |  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 |