├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── README.md ├── REFERENCE.md ├── SUMMARY.md ├── book.json ├── deploy.sh ├── docs ├── contest │ ├── backend.md │ ├── frontend.md │ ├── intro.md │ └── midend │ │ ├── cp.md │ │ ├── dce.md │ │ ├── ir.md │ │ ├── irgen.md │ │ ├── midend.md │ │ ├── short_circuit.jpg │ │ ├── short_circuit.md │ │ └── ssa.md ├── misc │ └── schedule.md ├── ref │ └── riscv.md ├── step-parser │ ├── example.md │ ├── intro.md │ └── spec.md ├── step0 │ ├── env.md │ ├── errate.md │ ├── intro.md │ ├── pics │ │ └── README.md │ ├── riscv.md │ ├── riscv_env.md │ ├── testing.md │ └── todo.md ├── step1 │ ├── arch.md │ ├── clean │ ├── example.md │ ├── intro.md │ ├── pics │ │ ├── antlr.png │ │ ├── antlr2.png │ │ ├── example.dot │ │ ├── example.png │ │ ├── grun.png │ │ ├── main.dot │ │ ├── main.png │ │ ├── parsetree.dot │ │ ├── parsetree.svg │ │ ├── riscv_reg.png │ │ ├── with-ir.svg │ │ └── without-ir.svg │ ├── provided.md │ ├── spec.md │ └── visitor.md ├── step10 │ ├── example.md │ ├── intro.md │ ├── pics │ │ └── program_memory_layout.png │ └── spec.md ├── step11 │ ├── example.md │ ├── guide.md │ ├── intro.md │ ├── pics │ │ └── README.md │ └── spec.md ├── step12 │ ├── example.md │ ├── intro.md │ ├── pics │ │ └── README.md │ ├── spec.md │ └── typesystem.md ├── step13 │ ├── example.md │ ├── intro.md │ ├── pics │ │ ├── 1.png │ │ └── 2.png │ └── readme.md ├── step14 │ ├── example.md │ └── intro.md ├── step2 │ ├── example.md │ ├── intro.md │ └── spec.md ├── step3 │ ├── example.md │ ├── intro.md │ ├── pics │ │ ├── exp1.svg │ │ ├── exp2.svg │ │ ├── exp_fixed.svg │ │ ├── ops │ │ ├── ops.svg │ │ └── push_val.svg │ ├── precedence.md │ └── spec.md ├── step4 │ ├── example.md │ ├── intro.md │ ├── pics │ │ └── README.md │ └── spec.md ├── step5 │ ├── example.md │ ├── intro.md │ ├── pics │ │ ├── call_stack.svg │ │ ├── sf.drawio │ │ ├── sf.svg │ │ └── stack.png │ └── spec.md ├── step6 │ ├── dataflow.md │ ├── example.md │ ├── intro.md │ ├── pics │ │ ├── bad_stack_pointer.svg │ │ ├── bad_stack_pointer_2.svg │ │ ├── bad_stack_pointer_3.svg │ │ ├── bad_stack_pointer_4.svg │ │ ├── dataflow.png │ │ ├── flowgraph.png │ │ ├── formula.png │ │ ├── namer.drawio │ │ └── namer.svg │ └── spec.md ├── step7 │ ├── example.md │ ├── intro.md │ ├── manual-parser.md │ ├── pics │ │ └── README.md │ └── spec.md ├── step8 │ ├── example.md │ ├── intro.md │ ├── pics │ │ ├── README.md │ │ ├── dataflow.png │ │ ├── flowgraph.png │ │ ├── formula.png │ │ └── namer.svg │ └── spec.md └── step9 │ ├── example.md │ ├── intro.md │ ├── pics │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── after_call.svg │ ├── after_function_prologue.svg │ ├── after_ret.svg │ ├── before_function_call.svg │ ├── before_function_call_args_pushed.svg │ ├── param.svg │ ├── reg.png │ ├── riscv-regs.png │ └── stack_frame.svg │ └── spec.md ├── howto-gitbook.md ├── readme_for_ta.md └── styles ├── pdf.css └── website.css /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Website 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: '10' 15 | 16 | - name: Install dependencies and Build 17 | run: | 18 | sudo npm install gitbook-cli -g 19 | gitbook install 20 | gitbook build 21 | 22 | - name: Deploy 23 | uses: JamesIves/github-pages-deploy-action@releases/v3 24 | with: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | BRANCH: gh-pages 27 | FOLDER: _book 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _book 2 | node_modules 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MiniDecaf 编译实验 2 | 3 | > 实验手册指北:实验文档看起来会有一些长,是因为编译器本身就是一个庞大的系统,我们希望提供尽可能全面的内容来帮助大家理解框架的构成。请大家认真阅读文档,并且尽可能按照文档去动手试一试,而不是直接开始动手写作业。 4 | 5 | ## 实验概述 6 | MiniDecaf [^1] 是一个 C 的子集,去掉了`include/define`等预处理指令,多文件编译支持,以及结构体/指针等语言特性。 本学期的编译实验要求同学们通过多次“思考-实现-重新设计”的过程,一步步实现从简单到复杂的 MiniDecaf 语言的完整编译器,能够把 MiniDecaf 代码编译到 RISC-V 汇编代码。进而深入理解编译原理和相关概念,同时具备基本的编译技术开发能力,能够解决编译技术问题。MiniDecaf 编译实验分为多个 stage,每个 stage 包含多个 step。**每个 step 大家都会完成一个可以运行的编译器**,把不同的 MiniDecaf 程序代码编译成 RISC-V 汇编代码,可以在 QEMU/SPIKE 硬件模拟器上执行。随着实验内容一步步推进,MiniDecaf 语言将从简单变得复杂。每个步骤都会增加部分语言特性,以及支持相关语言特性的编译器结构或程序(如符号表、数据流分析方法、寄存器分配方法等)。下面是采用 MiniDecaf 语言实现的快速排序程序,与 C 语言相同。 7 | 8 | ```c 9 | int qsort(int a[], int l, int r) { 10 | int i = l; int j = r; int p = a[(l+r)/2]; 11 | while (i <= j) { 12 | while (a[i] < p) i = i + 1; 13 | while (a[j] > p) j = j - 1; 14 | if (i > j) break; 15 | int u = a[i]; a[i] = a[j]; a[j] = u; 16 | i = i + 1; 17 | j = j - 1; 18 | } 19 | if (i < r) qsort(a, i, r); 20 | if (j > l) qsort(a, l, j); 21 | return 0; 22 | } 23 | ``` 24 | 25 | 2024 年秋季学期基本沿用了 2023 年秋季学期《编译原理》课程的语法规范。为了贴合课程教学内容,提升训练效果,课程组设计了比较完善的编译器框架,包括词法分析、语法分析、语义分析、中间代码生成、数据流分析、寄存器分配、目标平台汇编代码生成等步骤。每个 step 同学们都会面对一个完整的编译器流程,但不必担心,实验开始的几个 step 涉及的编译器框架知识都比较初级,随着课程实验的深入,将会循序渐进地引入各个编译器功能模块,并通过文档对相关技术进行分析介绍,便于同学们实现相关编译功能模块。 26 | 27 | 从2023年起,课程组增加了大实验环节,大实验是一个**可选**环节。可以参考[大实验参考文档](docs/contest/intro.md)获取更多信息。 28 | 29 | ## 实验起点和基本要求 30 | 31 | 本次实验一共设置 13 个步骤(其中 step 0 和 step 1 为实验框架熟悉,不需要修改框架代码)。后续的 step 2-13 我们将由易到难完成 MiniDecaf 语言的所有特性,由于编译器的边界情况很多,你**只需通过我们提供的正例与负例即可**。 32 | 33 | 我们以 stage 组织实验,各个 stage 组织如下: 34 | 35 |
    36 |
  1. 37 | 第一个编译器(step0-step1)。我们给的实验框架可以通过所有测试用例,你需要做的事情为跟着文档阅读学习实验框架代码。请各位同学注意,stage0 尤为重要,掌握好实验框架是高质量和高效率完成后续实验的保证。 38 |
  2. 39 |
  3. 40 | 常量表达式(step2-step4)。在这个 stage 中你将实现常量操作(加减乘除模等)。 41 |
  4. 42 |
  5. 43 | 变量和赋值(step5)。在这个 stage 中你将第一次支持变量声明与赋值。 44 |
  6. 45 |
  7. 46 | 作用域和块语句(step6)。在这个 stage 中你的编译器将支持作用域,以便支持后续的条件和循环。 47 |
  8. 48 |
  9. 49 | 条件和循环(step7-step8)。在这个 stage 中你将支持条件判断和循环语句,此时,你的编译器可以编译的程序就从线性结构程序到了有分支结构的程序。 50 |
  10. 51 |
  11. 52 | 函数(step9)。在这个 stage 中你将支持函数的声明和调用,这样你就可以写很多有意思的代码了。 53 |
  12. 54 |
  13. 55 | 全局变量和数组(step10-step12)。在这个 stage 中,你将支持全局变量和数组,数组中包括全局数组和局部数组。 56 |
  14. 57 |
  15. 58 | 寄存器分配算法(step13)。在这个 stage 中,你将实现基于图染色的寄存器分配算法,替代当前框架中简单的启发式算法。 59 |
  16. 60 |
61 | 62 | 63 | 其中,stage0 为环境配置和框架学习,无需进行编程,不计入成绩。 64 | stage1 - stage5 为 5 个基础关卡,你需要通过它们以拿到一定的分数(35%)。 65 | stage6 为升级关卡,如果你学有余力,完成它们可以减少期末考试在总评中所占的比重(完整完成可以获得占总评 7% 的成绩并替代期末考试对应权重)。 66 | stage7 为进阶关卡,如果你依然学有余力,你可以在这里实现一些编译优化(完整完成可以获得占总评 8% 的成绩并替代期末考试对应权重)。注意,你需要在完成 stage6 后才能尝试 stage7,否则无法获得对应分数。 67 | 68 | 我们以 step 组织文档,每个 step 的文档都将以如下形式组织:首先我们会介绍当前 step 需要用到的知识点,其次我们会以一个当前 step 具有代表性的例子介绍它的整个编译流程。在之前 step 中已经介绍的知识点,我们会略过,新的知识点和技术会被详细介绍。 69 | 70 | 我们通过[问答墙](https://docs.qq.com/doc/DY1hZWFV0T0N0VWph)来集中解决大家在环境配置及完成实验中遇到的问题。如果你遇到了任何问题,都可以在[问答墙](https://docs.qq.com/doc/DY1hZWFV0T0N0VWph)中检索;如果你的问题尚未有其他人提问过,欢迎向助教提问,助教会尽快回复的。 71 | 72 | ### **诚信守则** 73 | 74 | **请注意,诚信守则是参加本课程的学生应遵守的道德行为规范。实验指导中给出的生成结果(抽象语法树、三地址码、汇编)只是一种参考的实现,同学们可以按照自己的方式实现,只要能够通过测试用例即可。但是,严格杜绝抄袭现象,如果代码查重过程中发现有抄袭现象,抄袭者与主动提供抄袭信息的被抄袭者将被记为0分。** 75 | 76 | ## 实验提交 77 | 78 | 大家在网络学堂提交 **git.tsinghua.edu.cn** 的帐号名后,助教会给每个人建立一个私有的仓库,URL 为 https://git.tsinghua.edu.cn/compiler24/stu24/minidecaf-你的学号 ,将作业提交到那个仓库即可。 79 | 每个 stage 会对应于一个 branch,当切换到一个新的 branch 上实现时,你可以用 `git checkout -b` 来创建一个新的分支。 80 | 81 | 本学期我们使用清华大学代码托管服务(git.tsinghua)的 CI(持续集成)来**测试**大家的代码实现及**提交实验报告**。 82 | `.gitlab-ci.yml` 中描述了如何运行 CI,你**不允许**修改此文件; 83 | `prepare.sh` 是在测试前会运行的准备脚本,包括安装所需的依赖(python),如果你想添加新的依赖或者修改编译流程,请修改此文件。 84 | 在 CI 中会检查是否通过所有测例及是否有提交报告,只有通过所有测例且提交报告,才会被视为通过 CI。 85 | 86 | 我们只接受 pdf 格式的实验报告。你需要将报告放在仓库的 `./reports/.pdf` 路径,比如 stage 1 的实验报告需要放在 `stage-1` 这个 branch 下的 `./reports/stage-1.pdf`。 87 | 实验报告中需要包括: 88 | * 你的学号姓名 89 | * 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 90 | * 指导书上的思考题 91 | * 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 92 | * 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 93 | 94 | ## 评分标准 95 | 96 | 对于每个阶段(stage): 97 | * 80% 的成绩是自动化测试的结果,你可以直接在 **git.tsinghua** 的 CI 测试中看到。 98 | * 20% 的成绩是实验报告,其中对实验内容的描述占 10%,对思考题的回答占 10%。 99 | 100 | 评分会以每个 stage 的 branch 最后一次触发的 CI 及触发此次 CI 的 commit 里的实验报告为准,详见[补交政策](docs/misc/schedule.md#补交政策)。 101 | 102 | 如果你认为成绩有问题,请及时与助教联系。 103 | 104 | 时间安排及补交政策请看[实验进度安排](docs/misc/schedule.md)。 105 | 106 | ## 学术规范 107 | 108 | 由于实验有一定难度,同学之间相互学习和指导是提倡的。 109 | 对于其他同学的代码(包括实验报告中思考题的回答),可以参考,但禁止直接拷贝。 110 | 如有代码交给其他同学参考,必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 111 | 请所有同学不要将自己的代码托管至任何公开的仓库上(如 GitHub),托管至私有仓库的请不要给其他同学任何访问权限。 112 | 我们将会对所有同学的代码作相似度检查,如发现有代码雷同的情形,拷贝者和被拷贝者将会得到同样的处罚,除非被拷贝的同学提交时已做过声明。 113 | 114 | 代码雷同情节严重的,课程组有权上报至院系和学校,并按照相关规定严肃处理。 115 | 116 | ## 相关资源 117 | 118 | - [实验指导书(首页有实验报告提交要求)](https://decaf-lang.github.io/minidecaf-tutorial/) 119 | - [实验指导书勘误表](https://decaf-lang.github.io/minidecaf-tutorial/docs/step0/errate.html) 120 | - [课程问答墙](https://docs.qq.com/doc/DY1hZWFV0T0N0VWph) 121 | - [实验思路指导与问答墙](https://docs.qq.com/doc/DY05QVmJFcGNWcllo) 122 | 123 | ## 参考资料 124 | - [Writing a C Compiler: by Nora Sandler](https://norasandler.com/2017/11/29/Write-a-Compiler.html) 125 | - [nqcc](https://github.com/nlsandler/nqcc) 126 | - [http://scheme2006.cs.uchicago.edu/11-ghuloum.pdf](http://scheme2006.cs.uchicago.edu/11-ghuloum.pdf) 127 | 128 | ## 备注 129 | [^1]: 关于名字由来,由于往年的实验叫 Decaf,我们在新的且更简单的语言规范下复用了 Decaf 的编译器框架,所以今年的实验就叫 MiniDecaf 了。 130 | -------------------------------------------------------------------------------- /REFERENCE.md: -------------------------------------------------------------------------------- 1 | # 参考资料 2 | 3 | * [Writing a C Compiler: by Nora Sandler](https://norasandler.com/2017/11/29/Write-a-Compiler.html) 4 | 5 | * [An Incremental Approach to Compiler Construction : by Abdulaziz Ghuloum](http://scheme2006.cs.uchicago.edu/11-ghuloum.pdf) 6 | 7 | * [Monkey: The programming language that lives in books](https://monkeylang.org/) 8 | 9 | * [C17 标准草案 N2176](https://web.archive.org/web/20181230041359if_/http://www.open-std.org/jtc1/sc22/wg14/www/abq/c17_updated_proposed_fdis.pdf)(N2176 是 C17 标准正式发布前的最后一版草案,根据 C17 标准的编者之一 Jens Gustedt 的[博文](https://gustedt.wordpress.com/2018/04/17/c17/),其与 C17 标准相比只有表述上的差异) 10 | 11 | * [RISC-V 手册](https://riscv.org/technical/specifications/) 12 | 13 | * [RISC-V(非官方)汇编指令用法](https://github.com/TheThirdOne/rars/wiki/Supported-Instructions) 14 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | * [实验简介](README.md) 2 | * [实验进度安排](docs/misc/schedule.md) 3 | * [勘误表](docs/step0/errate.md) 4 | * [RISC-V 参考资料](docs/ref/riscv.md) 5 | 6 | ## 前置准备 7 | 8 | * 配环境、跑测试 9 | * [实验环境简介](docs/step0/intro.md) 10 | * [RISC-V 环境配置](docs/step0/riscv_env.md) 11 | * [RISC-V 的工具链使用](docs/step0/riscv.md) 12 | * [实验框架环境配置](docs/step0/env.md) 13 | * [运行实验框架](docs/step0/testing.md) 14 | 15 | ## Stage0:第一个编译器 16 | 17 | * [MiniDecaf 编译器结构](docs/step1/arch.md) 18 | * [已提供的语法特性](docs/step1/provided.md) 19 | 20 | * step1:仅一个 return 的 main 函数 21 | * [实验要求](docs/step1/intro.md) 22 | * [通过例子学习](docs/step1/example.md) 23 | * [Visitor 模式速成](docs/step1/visitor.md) 24 | * [规范](docs/step1/spec.md) 25 | 26 | ## Stage1:常量表达式 27 | 28 | * step2:一元操作 29 | * [实验要求](docs/step2/intro.md) 30 | * [通过例子学习](docs/step2/example.md) 31 | * [规范](docs/step2/spec.md) 32 | 33 | * step3:加减乘除模 34 | * [实验要求](docs/step3/intro.md) 35 | * [通过例子学习](docs/step3/example.md) 36 | * [优先级和结合性](docs/step3/precedence.md) 37 | * [规范](docs/step3/spec.md) 38 | 39 | * step4:比较和逻辑表达式 40 | * [实验要求](docs/step4/intro.md) 41 | * [通过例子学习](docs/step4/example.md) 42 | * [规范](docs/step4/spec.md) 43 | 44 | ## Stage2:变量 45 | 46 | * step5:局部变量和赋值 47 | * [实验要求](docs/step5/intro.md) 48 | * [通过例子学习](docs/step5/example.md) 49 | * [规范](docs/step5/spec.md) 50 | 51 | ## Stage3:作用域 52 | 53 | * step6:作用域和块语句 54 | * [实验要求](docs/step6/intro.md) 55 | * [通过例子学习](docs/step6/example.md) 56 | * [数据流分析](docs/step6/dataflow.md) 57 | * [规范](docs/step6/spec.md) 58 | 59 | ## Stage4:条件和循环 60 | 61 | * step7:条件语句 62 | * [实验要求](docs/step7/intro.md) 63 | * [通过例子学习](docs/step7/example.md) 64 | * [规范](docs/step7/spec.md) 65 | 66 | * step8:循环语句 67 | * [实验要求](docs/step8/intro.md) 68 | * [通过例子学习](docs/step8/example.md) 69 | * [规范](docs/step8/spec.md) 70 | 71 | ## Stage5:函数 72 | 73 | * step9:函数 74 | * [实验要求](docs/step9/intro.md) 75 | * [通过例子学习](docs/step9/example.md) 76 | * [规范](docs/step9/spec.md) 77 | 78 | ## Stage6(升级):全局变量和数组 79 | 80 | * step10:全局变量 81 | * [实验要求](docs/step10/intro.md) 82 | * [通过例子学习](docs/step10/example.md) 83 | * [规范](docs/step10/spec.md) 84 | 85 | * step11:数组 86 | * [实验要求](docs/step11/intro.md) 87 | * [通过例子学习](docs/step11/example.md) 88 | * [规范](docs/step11/spec.md) 89 | 90 | * step12:为数组添加更多支持 91 | * [实验要求](docs/step12/intro.md) 92 | * [通过例子学习](docs/step12/example.md) 93 | * [规范](docs/step12/spec.md) 94 | 95 | ## Stage7(升级):寄存器分配与代码优化 96 | 97 | * [选做二说明](docs/step13/readme.md) 98 | 99 | * step13:寄存器分配算法改进 100 | * [实验要求](docs/step13/intro.md) 101 | * [实验指导](docs/step13/example.md) 102 | 103 | ## 大实验参考文档 104 | 105 | * [大实验简介](docs/contest/intro.md) 106 | * [前端设计](docs/contest/frontend.md) 107 | * [中端设计](docs/contest/midend/midend.md) 108 | * [中间表示设计](docs/contest/midend/ir.md) 109 | * [中间代码生成](docs/contest/midend/irgen.md) 110 | * [静态单赋值](docs/contest/midend/ssa.md) 111 | * [常量传播](docs/contest/midend/cp.md) 112 | * [死代码消除](docs/contest/midend/dce.md) 113 | * [后端设计](docs/contest/backend.md) 114 | 115 | ## 参考资料 116 | 117 | * [参考资料](REFERENCE.md) 118 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "hide-element", 4 | "chapter-fold", 5 | "katex", 6 | "alerts", 7 | "emphasize", 8 | "mermaid-gb3", 9 | "codeblock-label", 10 | "code", 11 | "search-pro", 12 | "click-reveal", 13 | "expandable-chapters-interactive", 14 | "localized-footer", 15 | "intopic-toc" 16 | ], 17 | "pluginsConfig": { 18 | "fontsettings": { 19 | "theme": "white", 20 | "family": "sans", 21 | "size": 1 22 | }, 23 | "localized-footer": { 24 | "filename": "gitalk.html" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 下面的 DEPLOY_DIR 目录将关联到 https://github.com/decaf-lang/minidecaf-tutorial-deploy 远程仓库 3 | # 部署后可以通过 https://rcore-os.github.io/minidecaf-tutorial-deploy 来访问 4 | DEPLOY_DIR=../minidecaf-tutorial-deploy/ 5 | CURRENT_DIR=$(pwd) 6 | DEPLOY_DIR=$CURRENT_DIR/$DEPLOY_DIR 7 | 8 | # 判断是否需要重新 clone 仓库 9 | if [ -d "$DEPLOY_DIR" ]; then 10 | echo "$DEPLOY_DIR exists, resetting to remote master ..." 11 | cd $DEPLOY_DIR 12 | else 13 | echo "$DEPLOY_DIR doesn't exist, cloning from remote ..." 14 | mkdir -p $DEPLOY_DIR 15 | cd $DEPLOY_DIR 16 | git init 17 | # 该脚本将强制通过 SSH 协议和 GitHub 通信 18 | git remote add origin git@github.com:decaf-lang/minidecaf-tutorial-deploy.git 19 | fi 20 | 21 | # 重置到 remote master 22 | git fetch origin 23 | git reset --hard origin/master 24 | 25 | # 构建 GitBook 并复制 26 | cd $CURRENT_DIR 27 | gitbook build 28 | cp -r _book/* $DEPLOY_DIR 29 | cd $DEPLOY_DIR 30 | 31 | # Commit 并 push 32 | CURRENT_TIME=$(date +"%Y-%m-%d %H:%m:%S") 33 | git add * 34 | git commit -m "[Auto-deploy] Build $CURRENT_TIME" 35 | #git push origin master 36 | 37 | # 返回当前目录 38 | cd $CURRENT_DIR 39 | -------------------------------------------------------------------------------- /docs/contest/backend.md: -------------------------------------------------------------------------------- 1 | # 后端设计 2 | 3 | 编译器后端的主要功能是将中间表示(IR)转换为目标架构的汇编代码,在我们的课程实验中即将TAC翻译为RISC-V汇编代码。与目标机器架构紧密相关的优化也会在这一阶段进行。 4 | 5 | ## 代码生成 6 | 7 | 目标代码的生成是后端的核心部分。通常中间表示不会与目标架构的汇编代码非常相似,一方面它们所用的指令不一样,另外中间表示也会省略掉与物理寄存器和函数调用的若干细节。这要求我们在将IR翻译为汇编指令时处理好这些缺失的部分,生成合法的汇编程序。 8 | 9 | ### 指令选择 10 | 11 | 对于一条IR指令,选择合适的汇编指令对应物。对于大部分算术指令,一对一翻译即可,这没有什么难度;而某些IR指令没有直接的相应汇编指令,需要被翻译为多条汇编指令。你可能需要选择相对更优的候选指令序列。一对多指令翻译包含一些微妙之处,比如可能引入额外的寄存器、有潜在的副作用、干扰数据流分析,有时将它们视为一个指令整体是更合理的选择。你可能需要恰当地选择将一条“指令”彻底地转化为汇编指令的时机。 12 | 13 | 这里举几个例子: 14 | 15 | - 逻辑与和逻辑或。可详见step4。 16 | - 函数调用。函数调用通常不止一条call指令,在它之前需要生成传参的指令(参数少时用mv,多的时候压栈),在它之后可能要修改栈指针。 17 | - SSA IR中的Phi指令。通常Phi指令会被翻译为mv指令,但留意多条Phi指令同时存在的情况,它们在语义上“同时发生”,而实际指令序列具有顺序,这可能导致寄存器中的值被错误覆盖。 18 | 19 | ### 寄存器分配 20 | 21 | IR里通常会假设数量无限的虚拟寄存器(或称作变量),但目标ISA(Instruction Set Architecture)通常只允许有限数量的物理寄存器,我们必须将虚拟寄存器映射到物理寄存器上。如果物理寄存器无法容纳所有的活跃变量,它们就需要溢出(spill)到栈上。大多数架构上寄存器访问开销显著低于内存访问开销,因此我们应尽量避免发生spill。 22 | 23 | 课程实验使用的寄存器分配算法非常简单,它以基本块为单位,在基本块结束处活跃的变量会全部被spill到栈上。你会发现这个算法显得比较愚蠢,产生了大量实际无用的load和store指令。因此,你需要实现一个“全局”的寄存器分配算法,它应当能够跨基本块进行分析。(这里的“全局”通常以函数为粒度) 24 | 25 | 常见的全局寄存器分配算法包括图染色和线性扫描。由于我们并没有较为严格的编译时间要求,大家可以使用**step13**中提到的图染色算法。该算法的一个优势在于能够顺带处理mv指令,可以消除掉无用复制,这使得你前面做代码生成时可以轻松一点(能够较为无顾虑地生成mv指令)。 26 | 27 | 寄存器分配算法中存在一个比较微妙的地方:当我们不得不选择一个变量spill时,优先选择哪个变量。通常这里是启发式的,我们需要对每个变量设置一个优先级或溢出权重(spill weight)。假设我们已知一个变量中存放的是常数,那么它的保存和恢复开销都会比其它变量更低:无须保存,恢复时只需一条li指令而不必生成load。这种低spill开销的变量可以优先成为被踢出内存的倒霉蛋候选。(思考:我们是否应该优先spill循环体中的变量?)为了给变量设定合理的溢出权重,你可能需要依赖一些分析pass的结果。 28 | 29 | ### 栈帧确定和最终代码生成 30 | 31 | 在代码生成的早期阶段我们无法确定最终栈帧的大小。比如在寄存器分配阶段产生的溢出变量会使得栈帧大小增加,我们需要追踪栈上变量的偏移量和大小。留意load和store指令中允许的立即数偏移范围,当一个函数具有巨大的栈帧时,你可能需要插入一些额外的代码来计算栈上的地址或访问栈上的变量,甚至需要重新进行寄存器分配。 32 | 33 | 在这里我们介绍一种可能的实现方式。我们暂不考虑VLA(variable-length array),即认为栈上的所有对象都可以在编译期确定大小。首先我们将栈上的对象统一抽象为`StackObject`,包括栈上的数组、溢出的临时变量、用栈传入的函数参数。然后所有对栈的操作均使用单独的“指令”,例如 34 | 35 | - `LoadFromStack t0, obj, offset`: 将栈上对象`obj`偏移`offset`(立即数)处的内容加载到 `t0` 36 | - `StoreToStack t0, obj, offset`:将`t0`中的内容写入到栈上对象`obj`偏移`offset`处 37 | - `LoadStackAddr t0, obj, offset`:计算栈上对象`obj`偏移`offset`处的地址,将结果存放在`t0` 38 | 39 | 代码生成的大部分阶段均保持以上指令形式。最终确定栈帧时,统计所有栈上对象并为它们赋予一个相对栈帧的偏移。如果你打算在生成的代码中使用栈帧指针`fp`(frame pointer),展开的指令中可以直接使用这个偏移;如果你打算用栈指针`sp`进行寻址,你最好维护指令序列中`sp`发生的变化并计算栈上对象相对于`sp`的偏移(主要为了应对涉及栈传参的函数调用)。 40 | 41 | 最终我们将以上的这些“指令”展开。例如`LoadFromStack`可以保守地展开为以下RISC-V指令序列: 42 | ```assembly 43 | li t0, (some immediate offset) 44 | add t0, sp, t0 45 | ld t0, 0(t0) 46 | ``` 47 | 48 | 但大多数时候`ld t0, offset(sp)`就足够了。需要注意的是`StoreToStack`可能无法展开,也许要在更早的阶段引入额外的临时变量并将其变换为`LoadStackAddr`和一条store指令。 49 | 50 | 确定栈帧后生成函数的prologue和epilogue,其中主要包括callee-saved寄存器的保存与恢复、对栈指针的调整。注意有些架构可能对栈指针有对齐要求(e.g. 必须是8的整数倍)。 51 | 52 | ### 附:函数调用相关 53 | 54 | 处理函数调用通常需要插入额外的指令用于传参,而寄存器传参的调用约定又和寄存器分配有一定关系。在Iterated Register Coalescing的论文中并没有提及函数调用约定的处理方式,在这里以RISC-V为例进行一些说明。一种直观的想法是将函数参数对应的临时变量直接预着色为对应的参数寄存器,但这样的方案存在较明显的问题。下面展示两个C语言片段: 55 | 56 | ```C 57 | int f(int x) { 58 | // lots of stuff... 59 | return x; 60 | } 61 | ``` 62 | 63 | 在这个例子中,如果我们将`x`对应的临时变量直接绑定到参数寄存器`a0`上,那么`a0`即`x`具有超长的生命周期,可能与大量的临时变量节点相干涉。如果中间的代码含有其它函数调用,对`a0`的使用存在冲突,有可能需要生成大量load/store。 64 | 65 | ```C 66 | int swap(int x, int y) { 67 | // ... 68 | swap(y, x); 69 | // ... 70 | } 71 | ``` 72 | 73 | 对于外层`swap`,直观上`x`和`y`会被分别绑定到`a0`和`a1`;而中间再次调用`swap`时却又要求`y`在`a0`且`x`在`a1`中,这种冲突免不了一番折腾。 74 | 75 | 可以发现问题在于我们强行把参数变量和参数寄存器的生命周期绑定在了一起,而事实上调用约定只要求在传参时参数变量位于指定寄存器中。在函数体其它部分的代码中,调用约定不关心也管不着参数变量到底在哪个寄存器里。你可能会反驳:我们其实也关心,尽量让参数变量分配到对应的参数寄存器中有助于减少无意义的move指令。没错,但这个步骤可以交给寄存器分配算法和后续优化处理,在生成代码时我们更关注代码逻辑,应当将参数变量和传参时的寄存器解耦。 76 | 77 | 具体而言,这种解耦可以通过插入新的临时变量和move指令实现。(在下面的描述中只考虑寄存器传参) 78 | 79 | - 调用其它函数前:假设函数调用的实参位于临时变量`x1`至`xn`中。那么我们引入新临时变量`T1`到`Tn`,然后按照`mv Ti, xi`的方式将**全部**`xi`移入`Ti`中,接下来再生成`mv aj, Ti`复制到目标参数寄存器。注意这里的2n条mv指令形成了两阶段,每个阶段内部的move指令顺序不重要,但**不要跨阶段移动指令**。 80 | - 处理在寄存器中的传入参数:假设函数的形参对应临时变量`x1`到`xn`。直接在函数开头生成`mv xi, ai`即可。 81 | 82 | 以上面的`swap`函数为例子,插入上述辅助指令后的汇编伪代码如下: 83 | ```assembly 84 | swap: 85 | mv x, a0 # 1 86 | mv y, a1 # 2 87 | 88 | # first move phase 89 | mv _T0, x # 3 90 | mv _T1, y # 4 91 | 92 | # second move phase 93 | mv a0, _T1 # 5 94 | mv a1, _T0 # 6 95 | call swap 96 | ``` 97 | 98 | 在经过带move合并的寄存器分配后,大概率会得到这样的汇编代码: 99 | ```assembly 100 | swap: 101 | mv t0, a0 102 | mv a0, a1 103 | mv a1, t0 104 | call swap 105 | ``` 106 | 107 | 这里引入了最少数量的额外寄存器,正是我们所期望的变量交换代码。首先前两条mv指令提示寄存器分配算法合并`x`和`a0`、`y`和`a1`,这一分配方案是可行的,因此前两条无用mv被消去。接下来我们注意到`_T0`与`a1`相干涉(指令4的Use集合、指令3的LiveOut集合包含`a1`,`_T0`在指令3的Def集合中),因此`_T0`不能被分配到`a1`;同时`_T0`也与`a0`相干涉(指令6的Use集合,指令5的LiveOut集合包含`_T0`,`a0`在指令5的Def集合中),最终`_T0`被分配到一个新的寄存器`t0`。而`_T1`可以安全地被分配到`a1`,故指令4被视作无用指令消除。 108 | 109 | 在生成函数调用的代码时,除传参外,还需要考虑caller-saved寄存器的处理。在我们的基本实验框架中,你可以在call指令前后保存并恢复活跃且在caller-saved寄存器中的变量,这样在其它指令看来是无事发生。不过在这里有一种更简便的实现方式:将所有caller-saved寄存器加入到call指令的Def集合中,剩下的事情交给寄存器分配算法处理。考虑以下C语言片段: 110 | 111 | ```C 112 | int getint(); 113 | void putint(int); 114 | 115 | int main() { 116 | int x = getint(); 117 | putint(x); 118 | return x; 119 | } 120 | ``` 121 | 122 | 在寄存器分配前可能对应如下代码: 123 | ```assembly 124 | main: 125 | # prologue 126 | 127 | call getint 128 | mv x, a0 129 | 130 | mv a0, x # ... omitted 131 | call putint 132 | 133 | mv a0, x 134 | # epilogue 135 | ret 136 | ``` 137 | 138 | 采用上述方式,`x`处于`call putint`的LiveOut集合中,会与全部的caller-saved寄存器相干涉,这样`x`就会自动被分配到callee-saved寄存器上。经过后续优化可能的最终汇编代码如下: 139 | ```assembly 140 | main: 141 | # prologue 142 | call getint 143 | mv s0, a0 144 | call putint 145 | mv a0, s0 146 | # epilogue 147 | ret 148 | ``` 149 | 150 | ## 目标架构相关优化 151 | 152 | 这里简单地举几个例子。 153 | 154 | 1. 指令选择相关的窥孔优化 155 | 156 | 此类优化指的是将局部的几条指令替换为更优的指令序列的一类优化,并非特指。需要注意的是此类优化较为琐碎,建议按需实现。 157 | 158 | 例如以下的RISC-V指令序列 159 | ```assembly 160 | li t0, 0 161 | bne a0, t0, label1 162 | ``` 163 | 164 | 可以被替换为`bne a0, zero, label1`,后续再通过无用指令消除去掉`li t0, 0`(假设该值不再使用)。总的来说,一类优化机会包括识别出指令序列中的常量,尝试将它们嵌入至指令中(RISC-V的I型指令),并进行无效果指令消除(mv到自身、加0、乘1)、强度削减(乘除2的幂转移位,除法转乘法)等优化。 165 | 166 | 再举一个ARM的例子。ARM的访存指令支持基址+索引*4的寻址模式(类似x86),以下汇编指令序列 167 | ```assembly 168 | mov r1, r1, LSL #2 169 | add r0, r0, r1 170 | ldr r0, [r0] 171 | ``` 172 | 173 | 可以被合并为一条指令: 174 | ```assembly 175 | ldr r0, [r0, r1, LSL #2] 176 | ``` 177 | 178 | 这种汇编代码模式在数组访问中较为常见。 179 | 180 | 2. 指令调度 181 | 182 | 指令调度指的是在不影响指令逻辑的前提下调整指令的顺序,目的之一是利用现代处理器的特性提升指令级并行度。基本块内的指令调度首先会利用指令间的依赖关系构造DAG,然后利用关键路径长度、寄存器压力、处理器发射宽度等因素结合处理器功能单元的执行模型依次决定指令的执行顺序。感兴趣的同学可以自行查看相关资料。 183 | -------------------------------------------------------------------------------- /docs/contest/intro.md: -------------------------------------------------------------------------------- 1 | # 大实验参考文档 2 | 3 | 注:大实验文档目前还在完善中,会不断迭代更新。如果对于评分部分有更新,会通知所有选择大实验的同学。 4 | 5 | ## 介绍 6 | 7 | 大实验编译器目标:完成一个具有编译优化功能的高性能编译器。部分达到[系统能力设计大赛——编译系统设计赛](https://compiler.educg.net/#/index?TYPE=COM)的要求。 8 | 9 | 参加大实验的同学应该需要自己从头设计一个符合 [minidecaf 规范](../step12/spec.md) 的编译器,包括前端、中端和后端。参加大实验可以替代期末考试,详见[评分方法](#评分方法)一节。 10 | 11 | 有两个原因我们要求同学们从头设计一个编译器: 12 | 1. 为了简化课程实验,我们的基础实验框架在设计时并未考虑大实验的需求(例如:IR 的类型系统简易、没有区分基本块),在现有框架的基础上重构实现编译优化反而在一定程度上限制了编译器的优化能力。 13 | 2. 大实验设计的其中一个目标是鼓励同学们参加系统能力设计大赛,比赛有查重要求,如果同学们使用相同的框架开始参加大实验并参与后续比赛,可能存在代码被判定为重复的问题。 14 | 15 | 大实验在 2024 年相对于 2023 年有一些变化,主要体现在: 16 | - 增加了实验文档 17 | - 语法要求从 Sysy 语法改为了 MiniDecaf,主要差别在于`const`标志符号、数组初始化等语法上的区别,难度有所降低 18 | - 不再要求完成基础实验以后再进行大实验 19 | 20 | 大实验的语法规范与 step12 的[规范](../step12/spec.md)是一致的。不过有一点需要注意: 21 | - 我们要求实现函数声明,即一个函数可以只有声明没有定义,主要是用于评测性能,比如读入数据和打印结果,我们将会把你的代码和一个外部库进行链接编译。**这意味着,你需要实现标准的 RiscV 调用约定。** 22 | 23 | 你可以选择 C++,Rust 实现你的编译器,你的编译器生成的目标代码可以是 RISC-V 或者 ARM 架构的,这与比赛要求一致。如果你想用其他语言实现,请告知助教。 24 | 25 | 大实验为组队实验,4人一组(可以更少,但是评分标准保持不变)。没有特殊情况时,同组同分。 26 | 27 | **注意:大实验工作量较大,并不推荐所有同学都参加。** 28 | 29 | ## 编译器的构成 30 | 31 | 一个编译器主要由以下几个部分构成: 32 | - 前端:负责词法分析、语法分析、语义分析,生成抽象语法树(AST)。 33 | - 词法分析器(Lexer):将输入的源代码转换为一个个的标记(Token)。 34 | - 语法分析器(Parser):将标记(Token)转换为抽象语法树(AST)。 35 | - 语义分析器(Semantic Analyzer):检查AST是否符合语法规则和语义规则。 36 | - 中端:负责中间代码生成、优化。 37 | - 中间代码生成器(Intermediate Representation Generator):将 AST 转换为中间代码。 38 | - 优化器(Optimizer):对中间代码进行优化。 39 | - 后端:负责目标代码生成。 40 | - 目标代码生成器(Target Code Generator):将优化后的中间代码转换为目标机器代码。 41 | - 寄存器分配:将中间代码中的变量分配到实际的物理寄存器中。 42 | 43 | 可以通过后续的文档了解每个部分的更多细节。 44 | 45 | ## 参考实现进度及顺序 46 | 47 | 1. 编写前端、设计 IR、完成中间代码生成 (两周) 48 | - 前端:你可以使用现有的框架完成前端(如:Antlr、Flex & Bison)辅助你生成 AST,完成词法分析、语法分析、语义分析以及中间代码生成。如果你想在这个过程中锻炼你对分析方法的理解,你可以自己实现 LR(1)、LL(1) 等分析器。 49 | - 设计 IR 也是需要进行代码编写的,可以参考基础实验框架的IR在代码层面是如何实现的(`utils/tac`)。 50 | - 中间代码生成:将 AST 转换为 IR,你可以参考基础实验框架的中间代码生成部分(`frontend/tacgen`)。 51 | 52 | 此阶段分工建议:两位同学负责前端,两位同学负责中间表示设计和中间代码生成。 53 | 54 | 2. 完成后端(两周) 55 | - 实现后端代码生成、栈帧管理 56 | - 实现一个简单的寄存器分配方案,保证编译器能够完成全流程的运行,然后再考虑优化。 57 | 58 | 3. 增加中端优化和后端优化(剩下的时间) 59 | - 中端优化:死代码消除、常量传播、复写传播、循环不变量外提等等 60 | - 后端优化:图染色寄存器分配、线性扫描法、指令折叠等等 61 | 62 | 分工建议:两位同学负责中端优化,两位同学负责后端优化。 63 | 64 | ### 进度检查 65 | - 第一次进度检查:第六周周六(10.19) 66 | - 你的编译器应该能完成将简单的程序转换为 RISC-V 汇编代码,可以选择在这次检查时退出大实验。如果退出大实验,你需要在第八周周日(11.3)Stage 3 截止之前完成 Stage 1-3 的实验,不会有额外扣分。 67 | 68 | - 第二次进度检查(中期检查):第八周周六(11.2) 69 | - 这时候你的编译器应该能通过基础实验的所有测试样例(Stage 1-5)。如果不能完成,可能会被取消大实验的资格,同时你需要重新完成基础实验你需要在第十周周日(11.17)Stage 4 截止前完成 Stage 1-4 ,不额外扣分。也可以继续大实验不做基础实验,但是至少要在 Stage-5 让你的编译器能够通过 Stage 1-5 的测试样例。 70 | - 你们需要提交一个简单的报告,说明每个同学在实验过程中的分工以及完成的功能。(如果缺少这部分实验报告,那么报告成绩将会被扣除5分(总评 5%)) 71 | 72 | - 第三次进度检查:第十二周周六(11.30) 73 | - 你们需要提交一个简单的报告,说明每个同学在上次检查后的分工以及完成的功能。(如果缺少这部分实验报告,那么报告成绩将会被扣除 5 分(总评 5%)) 74 | 75 | - 第四次进度检查(期末检查):第十六周周末(12.29) 76 | - 你的编译器应该能通过所有的测试样例(Stage 1-6),包括附加测试样例。 77 | - 你应该提交一个完整的实验报告,包括实验的设计、实现、优化以及遇到的问题和解决方法。不需要卷页数,但应该说明了你们实现的功能。(如果缺少这部分实验报告,你将不会得到任何报告成绩) 78 | 79 | ## 评分方法 80 | 81 | 因为大实验实现难度较高且工作量较大,优化目标可能相对难以完成,因此我们给出两种评分方案: 82 | 83 | - 选项一 完成竞赛第二阶段的优化编译器,替代期末考试 84 | 85 | 成绩占比 90%,剩余 10% 为书面作业和日常成绩。 86 | 87 | 其中这90%构成为: 88 | - 50% 正确性测试:你需要通过 Stage 1-6 的所有测试样例以及附加测试的测试样例,这样你可以获得 50% 的正确性得分。 89 | - 10% 报告,介绍你的编译器的设计、你们进行的优化以及每个人完成的功能。 90 | - 30% 性能测试,将根据你的编译器的性能进行评分。 91 | 92 | 性能评分方案: 93 | 附加测试中`performance`部分测试样例,以 gcc 打开`-O2`优化的性能的 60% 为满分,按照比例折算。如果一个程序 gcc 编译后运行时间为 12s ,如果你的程序执行时间为 20s 即为满分。 94 | 95 | 你的单个测试点的得分为: 96 | ``` 97 | min{100, 100 * GCC编译程序运行时间 * 1.67 / 你的程序运行时间} 98 | ``` 99 | 所有测试点取**算数平均值**,最后结果 * 30% 作为你的最终性能测试成绩。 100 | 101 | 评测将会在我们提供的服务器上进行,通过 QEMU 模拟 RISC-V 或者 ARM 架构的 CPU 运行你的程序。经过测试 QEMU与 真实硬件的性能相对差值是比较恒定的(如比较 gcc `-O1`与`-O2`)。 102 | 103 | 实验评测仓库在[这里](https://github.com/decaf-lang/minidecaf-additional-test)。 104 | 105 | 你也可以选择参加期末考,那么你的成绩将会是评分方案一、二取最高的一个。 106 | 107 | - 选项二 仅完成竞赛第一阶段(达到课程基础实验的要求) 108 | 109 | 实验部分占比与基础实验一致,你不需要完成思考题,但是需要简单介绍你的编译器是怎么完成每一个 step 的。根据通过测试样例情况评分。 110 | 111 | - 完成 Stage 1 - 5 实验成绩 35% ,书面作业和日常成绩 10% ,期末成绩 55%。 112 | - 完成 Stage 1 - 6 实验成绩 42% ,书面作业和日常成绩 10% ,期末成绩 48%。 113 | - 完成 Stage 1 - 7 实验成绩 50% ,书面作业和日常成绩 10% ,期末成绩 40%。 114 | -------------------------------------------------------------------------------- /docs/contest/midend/cp.md: -------------------------------------------------------------------------------- 1 | # 常量传播/常量折叠 2 | 3 | 常量传播/常量折叠的目的在于发掘代码中可能存在的常量,尽量用对常量的引用替代对虚拟寄存器的引用(虚拟寄存器和变量是同一个概念,以下都使用变量),并尽量计算出可以计算的常量表达式。 4 | 5 | 常量传播通常依赖Use-Def和Def-Use数据流分析([这里](https://people.cs.vt.edu/ryder/415/lectures/machIndepOpts.pdf)有一个参考资料),这个数据流分析可以帮我们找到每个指令用到的变量是在哪里定义的。 6 | 7 | 例如,对于如下代码: 8 | 9 | ``` 10 | _main: 11 | _T0 = 2 12 | _T1 = 0 13 | _T2 = _T0 + 3 14 | _T3 = _T1 + 5 15 | _T4 = _T2 * 2 16 | _T5 = _T3 - _T1 17 | _T6 = _T4 + _T5 18 | ret _T6 19 | ``` 20 | 21 | 经过常量传播/常量折叠优化后,代码变为: 22 | 23 | ``` 24 | _main: 25 | _T0 = 2 26 | _T1 = 0 27 | _T2 = 5 28 | _T3 = 5 29 | _T4 = 10 30 | _T5 = 5 31 | _T6 = 15 32 | ret _T6 33 | ``` 34 | 35 | ## 常量传播/常量折叠的实现 36 | 37 | 常量传播/常量折叠的实现依赖于数据流分析,一种可能的实现方法如下: 38 | 1. 遍历所有语句,找出常量定义,将其全部加入常量表。例如: 39 | ``` 40 | _T0 = 2 41 | _T1 = 0 42 | _T2 = _T0 + 3 43 | ``` 44 | _T0和_T1的值是常量,将_T0和_T1的值分别存入常量表。 45 | 46 | 2. 依据Def-Use关系,找出所有用到常量_T0和_T1的地方,如果这些地方计算的结果也是常量,则将计算结果也加入常量表。上述代码中,_T2的值为5,也是一个常量,将_T2的值加入常量表。 47 | 48 | 3. 重复上述过程,直到常量表不再增加为止。 49 | -------------------------------------------------------------------------------- /docs/contest/midend/dce.md: -------------------------------------------------------------------------------- 1 | # 死代码消除 2 | 3 | 死代码消除(Dead code elimination, DCE)即无用代码消除,死代码和不可达代码是两个概念。前者指的是执行之后没有任何作用的代码(例如:多余的计算),后者指的是永远无法被执行到的代码。 4 | 5 | 死代码消除通常依赖于Use-Def和Def-Use数据流分析,这个数据流分析可以帮我们找到每个指令用到的变量是在哪里定义的。 6 | 7 | 这里介绍一种 DCE 的方法(来源于《高级编译器设计与实现》(鲸书)): 8 | 9 | - 首先,标识所有计算**必要值**的指令。比如在函数中要返回(`return`)或输出(`print`)的值,或者它可能会对从函数外访问的存储单元有影响(全局内存访问,对函数外定义的数组访问)。 10 | - 然后,以迭代的方式逐步标记对这种对计算**必要值**有贡献的指令。假如一个指令的结果是另一个**必要值**计算指令的输入,那么这个指令也是必要的。 11 | - 当以上迭代函数稳定不变时,所有未标记的指令都可以认为是Dead Code,可以删除。 12 | 13 | 具体实现上,可以借助du/ud链来实现: 14 | 15 | - 维护一个set,存储所有必要值的定义指令。 16 | - 找出函数所有的**必要值**,标记这些值的定义指令。 17 | - 对于set中的每个指令,顺着ud链找到所有使用这个指令的指令,将这些指令加入set。 18 | - 对于上一步中新加入的指令,继续顺着ud链找到所有使用这个指令的指令,将这些指令加入set。 19 | - 重复上一步,直到set不再变化。 20 | - 函数中的指令,如果不在set中,就可以认为是Dead Code。 21 | 22 | 此处举个例子: 23 | ```asm 24 | _main: 25 | _T0 = 1 26 | _T1 = 2 27 | _T2 = _T1 + 5 28 | _T3 = _T0 + 2 29 | _T4 = _T3 * 5 30 | return _T4 # _T4 是必要值 31 | ``` 32 | 33 | 顺着ud链,可以找到 `_T4 = _T3 * 5`,因此 `_T3` 也是必要值。继续找到 `_T3 = _T0 + 2`,因此 `_T0` 也是必要值。最终 `_T0`、`_T3`、`_T4` 都是必要值,而 `_T1`、`_T2` 的定义指令都可以认为是Dead Code。 34 | 35 | 因此可以优化为: 36 | ```asm 37 | _main: 38 | _T0 = 1 39 | _T3 = _T0 + 2 40 | _T4 = _T3 * 5 41 | return _T4 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/contest/midend/ir.md: -------------------------------------------------------------------------------- 1 | # 中间表示设计 2 | 3 | 这里我们以一种经典 IR —— 三地址码为例,介绍中间表示的设计。 4 | 5 | ## 三地址码 6 | 7 | **三地址码**(Three Address Code, TAC)是一种经典的 IR 设计,TAC 看起来很像汇编,与汇编最大的区别在于 —— 汇编里面使用的是目标平台(如 risc-v, x86, mips)规定的物理寄存器,其数目有限;而 TAC 使用的是 **“虚拟寄存器”** (也可以称作临时变量),其数目不受限制,可以任意使用(这意味着直接将临时变量转化为物理寄存器可能会出现寄存器不够用的情况)。**在后端生成汇编代码时,我们再考虑如何为临时变量分配物理寄存器的问题。** 8 | 9 | ```asm 10 | main: # main 函数入口标签 11 | _T0 = 1 # 加载立即数 12 | _T1 = _T0 # 临时变量赋值操作 13 | _T2 = ADD _T0, _T1 # 加法操作 _T2 = _T0 + _T1 14 | _T3 = NEG _T0 # 取负操作 _T3 = -_T0 15 | return _T2 # 函数返回 16 | ``` 17 | 18 | 以上给出了一份 TAC 示例程序。请注意 TAC 代码只是一种中间表示,并不需要像汇编语言那样有严格的语法。因此,可以自由选择输出 TAC 代码的格式,只要方便自己调试即可。例如,你也可以将 _T2 = ADD _T0, _T1 输出成 _T2 = _T0 + _T1。下面是另一个IR输出格式的例子: 19 | ```asm 20 | i32 main() { 21 | _B0: 22 | i32 _T0 = 1 23 | i32 _T1 = _T0 24 | i32 _T2 = _T0 + _T1 25 | i32 _T3 = -_T0 26 | return i32 _T2 27 | } 28 | ``` 29 | 你会发现,这种IR输出格式包含了一些类型信息,也更加易读。 30 | 31 | TAC 指令与汇编指令比较类似,每条 TAC 指令由操作码和操作数(最多3个,函数调用除外,由于函数参数可能有多个,使用严格的三个操作数反而会使得函数实现更为复杂)构成。操作数可能会有:临时变量、常量、标签(可理解为常量地址)和全局变量。 32 | 33 | 我们来思考一下,如果需要完整描述源程序的语义,需要哪些语句? 34 | 35 | - 算术语句:这是计算机最基础的语义。 36 | - 二元运算(如加、减、乘、除) 37 | - 形式:dst = op src1, src2 38 | - 示例:_T2 = ADD _T0, _T1 39 | - 一元运算(如取负、取位反) 40 | - 形式:dst = op src 41 | - 示例:_T3 = NEG _T0 42 | - 跳转语句:用于实现程序的控制流,如循环和条件跳转,通常结合标签使用。 43 | - 条件跳转语句 44 | - 形式:branch cond, label1, label2 45 | - 示例:branch _T0, _L1, _L2 46 | - 无条件跳转语句 47 | - 形式:jump label 48 | - 示例:jump _L0 49 | - 函数调用语句 50 | - 形式:dst = call (func_name, arg1, arg2, ...) 51 | - 示例:_T2 = call (foo, _T0, _T1) 52 | - 赋值语句 53 | - 形式:dst = src 54 | - 示例:_T2 = _T0 55 | - 访存语句 56 | - 加载操作:dst = load addr, offset 57 | - 示例:_T2 = load _T0, 0 58 | - 存储操作:store src, addr, offset 59 | - 示例:store _T0, _T1, 0 60 | - 内存申请语句(注意区分,这里指编译器静态分配,与运行时动态分配的 malloc 不同,主要用于在栈上分配内存) 61 | - 形式:dst = alloc size 62 | - 示例:_T2 = alloc 40 63 | - 返回语句 64 | - 形式:return src 65 | - 示例:return _T0 66 | 67 | 有了这些语句以后,我们的IR就可以描述源程序的语义了。 68 | 69 | ## 内存数据结构 70 | 71 | 中间表示是一种内存数据结构,不仅需要方便**阅读**,还需要方便**进行后续操作**(如优化、翻译)。 72 | 73 | 我们可以为所有指令定义一个基类 `Instruction`,然后根据不同的指令类型定义不同的子类。 74 | 75 | ```c++ 76 | struct Instruction { 77 | Type type; 78 | }; 79 | ``` 80 | 81 | 下面以二元运算指令为例,展示如何定义一个具体的指令类。 82 | 83 | 二元运算需要两个操作数,并且会产生一个计算结果。 84 | 85 | 而操作数可能是一个立即数,也可能是一个变量。例如以下的情况: 86 | 87 | ``` 88 | _T2 = ADD _T0, _T1 89 | _T3 = ADD _T0, 2 90 | ``` 91 | 92 | 因此为了指令实现方便,我们可以将操作数定义为一个如下的结构体: 93 | 94 | ```c++ 95 | struct Operand { 96 | union{ 97 | int value; 98 | int reg_id; 99 | }; 100 | bool is_reg; 101 | ... ... 102 | }; 103 | ``` 104 | 105 | 我们使用一个 `union` 来存储操作数的值或者寄存器编号,使用一个 `bool` 来标记操作数是否是一个寄存器。当然,你可以要求操作数必须是寄存器,这样就不需要 `is_reg` 这个标记了。这样你需要增加一条指令,将立即数分配到一个寄存器中。 106 | 107 | 有了操作数,我们就可以定义指令了,我们将二元运算指令定义为如下的结构体,其中Opcode是操作码,用来标记不同的二元运算类型,src1和src2是两个操作数,dst是运算结果存放的寄存器: 108 | 109 | ```c++ 110 | enum Opcode { 111 | ADD, SUB, MUL, DIV 112 | }; 113 | 114 | struct Binary : public Instruction { 115 | Opcode opcode; // 操作码 116 | Operand src1; // 操作数一 117 | Operand src2; // 操作数二 118 | Operand dst; // 目标寄存器 119 | }; 120 | ``` 121 | 122 | 一元运算指令的定义与二元运算指令的定义类似,这里不再赘述。 123 | 124 | 跳转语句应该怎么定义?这里我们需要引入基本块的概念。 125 | 126 | 在中端进行优化时,我们需要进行[数据流分析和控制流分析](../../step6/dataflow.md),控制流分析过程中我们会将程序分解为多个基本块,基本块是一系列连续的指令序列,基本块内部指令序列的执行顺序是固定的,且不会被其他指令打断。我们可以将基本块定义为如下的结构体: 127 | 128 | ```c++ 129 | struct BasicBlock { 130 | std::vector instructions; 131 | std::string label; 132 | }; 133 | ``` 134 | 135 | 基本块的引入可以让我们便捷地进行各种编译优化,同时也简化了跳转语句的设计,只需要一个目标基本块即可: 136 | 137 | ```c++ 138 | struct Jump : public Instruction { 139 | BasicBlock *target; // 跳转目标 140 | }; 141 | ``` 142 | 143 | 我们是以函数为单位来组织基本块的,函数定义为如下的结构体: 144 | 145 | ```c++ 146 | struct Function { 147 | std::string name; 148 | std::vector blocks; 149 | }; 150 | ``` 151 | 152 | 整个程序又是由多个函数和全局变量组成的,因此我们可以将程序定义为如下的结构体: 153 | 154 | ```c++ 155 | struct Program { 156 | std::vector functions; 157 | std::vector globals; 158 | }; 159 | ``` 160 | 161 | 你会发现,我们的程序组织成了一个树状结构,即 `Program` 包含多个 `Function`,每个 `Function` 包含多个 `BasicBlock`,每个 `BasicBlock` 包含多条 `Instruction`。 162 | 163 | 一些tips: 164 | - 你可以在`Instruction`的层次上再次进行抽象,将运算指令和跳转分开,设计专门的运算指令类和跳转指令类,这样可以让程序的结构更加清晰,比如你可以将 `Binary` 和 `Unary` 都继承自 `Arithmetic`,将 `Jump` 继承自 `ControlFlow`, `Arithmetic` 和 `ControlFlow` 都继承自 `Instruction`。 165 | - 你可以在`Instruction`中添加一些成员变量,如`use`和`def`,用于在数据流分析后记录一些中间结果用于优化。 166 | - 你完全可以不按照我们给出的这些结构来设计你的 IR,这里有一些参考: 167 | - [北大编译实验Koopa IR](https://pku-minic.github.io/online-doc/#/lv0-env-config/koopa) 168 | - [LLVM IR](https://llvm.org/docs/LangRef.html) 169 | 170 | ### 静态单赋值(SSA) 171 | 172 | 进一步地,你可以实现符合[静态单赋值](./ssa.md)要求的 IR ,静态单赋值的 IR 在编译器中有着广泛的应用,比如 LLVM 的 IR 就是一种静态单赋值的 IR。在静态单赋值的 IR 中,每个变量只被赋值一次,这使得编译器可以更容易地进行优化。 -------------------------------------------------------------------------------- /docs/contest/midend/midend.md: -------------------------------------------------------------------------------- 1 | # 中端介绍 2 | 3 | 中端的设计包括:中间表示的设计、中端代码生成和中端优化。 4 | 5 | ## 中间表示 6 | 7 | 前端的解析和中端设计密不可分,通常,我们需要设计一个中间表示(Intermediate Representation, IR)来连接前端和后端。也只有我们定义好了中间表示,才能将来自于前端的AST转换为中端代码。 8 | 9 | ### 什么是中间表示? 10 | 11 | 中间表示(也称中间代码,intermediate representation / IR)是介于语法树和汇编代码之间的一种程序表示。 它不像语法树一样保留了那么多源程序的结构,也不至于像汇编一样底层。 12 | 13 | 由于源语言(MiniDecaf)和目标语言(RISC-V 汇编)一般存在较大的差别,因此直接把源语言翻译为目标语言中的合法程序通常是比较困难的。大多数编译器实现中所采取的做法,是首先把源语言的程序翻译成一种相对接近目标语言的中间表示形式,然后再从这种中间表示翻译成目标代码。中间表示(IR)的所带来的优势如下: 14 | 15 | - 缩小调试范围,通过把 AST 到汇编的步骤一分为二。如果目标代码有误,通过检查 IR 是否正确就可以知道:是AST 到 IR 翻译有误,还是 IR 到汇编翻译有误。 将 AST 转换到汇编的过程分成两个步骤,每个步骤代码更精简,更易于调试。 16 | - 适配不同指令集(RISC-V, x86, MIPS, ARM...)和源语言(MiniDecaf, C, Java...)。由于不同源语言的 AST 不同,直接从 AST 生成汇编的话,为了支持 N 个源语言和 M 个目标指令集,需要写 N * M 个目标代码生成模块。如果有了 IR,只需要写 N 个 IR 生成器和 M 个汇编生成器,只有 N + M 个模块。 17 | 18 | - 便于优化,中间表示可以附带一些额外信息,比如类型信息、控制流信息等,这些信息辅助编译器进行优化。 19 | 20 | 例如以下是一个IR代码的例子: 21 | 22 | ```assembly 23 | _main: 24 | _T1 = 0 25 | _T2 = 100 26 | _T3 = 0 27 | _L0: 28 | _T4 = _T1 < _T2 29 | beqz _T4, _L1, _L2 30 | _L1: 31 | _T3 = _T1 + _T3 32 | _T1 = _T3 + 1 33 | jump _L0 34 | _L2: 35 | _T5 = 2 36 | ret _T5 37 | ``` 38 | 39 | 从这个IR例子中,我们可以看到,相对于c语言,IR中没有了while、for这样的循环语句,而是通过标签和jump、branch指令来实现循环。高级语言的许多特性在IR中都被抹去了,让代码更加简洁,便于优化。而相对于汇编代码,IR中无需关注寄存器、函数调用的上下文切换等信息,与具体的硬件架构解耦。 40 | 41 | 我们将在[中间表示设计](./ir.md)中介绍IR设计时候需要考虑的地方和并列举一些实例。 42 | 43 | ## 中间代码生成 44 | 45 | 前端解析后,我们会得到一棵抽象语法树,接下来我们需要将这棵抽象语法树转换为中间代码。依据你设计的IR,你需要在保证语义的情况下,将AST用你的IR表示出来。可以参考基础实验框架中`frontend/tacgen/`的代码。 46 | 47 | 如以下是一个简单的例子: 48 | 49 | ```C 50 | int main(){ 51 | int a = 2; 52 | int b = 0; 53 | if(a) 54 | b = 1; 55 | else 56 | b = -1; 57 | return b; 58 | } 59 | ``` 60 | 61 | 生成的AST可能如下: 62 | ``` 63 | Program 64 | |- (children[0]) Function 65 | |- (ret_t) TInt 66 | |- (ident) Identifier("main") 67 | |- (body) Block 68 | |- (children[0]) VarDecl 69 | |- (type) TInt 70 | |- (ident) Identifier("a") 71 | |- (init) IntLiteral(2) 72 | |- (children[1]) VarDecl 73 | |- (type) TInt 74 | |- (ident) Identifier("b") 75 | |- (init) IntLiteral(0) 76 | |- (children[2]) If 77 | |- (cond) Identifier("a") 78 | |- (children[0]) Assign 79 | |- (lhs) Identifier("b") 80 | |- (rhs) IntLiteral(1) 81 | |- (children[1]) Assign 82 | |- (lhs) Identifier("b") 83 | |- (rhs) UnaryOp(NEG) 84 | |- (expr) IntLiteral(1) 85 | |- (children[3]) Return 86 | |- (expr) Identifier("b") 87 | ``` 88 | 89 | 你需要通过遍历AST的节点来将其转换为IR。例如,当你遇到一个`if`节点时,你可以先生成三个标签,一个用于表示`if`语句的开始,一个用于表示`else`语句的开始,一个用于表示整个`if`语句的结束。先生成一个判断语句,在生成if条件满足对应的标签以及代码,最后生成一个跳转语句,跳过else块。然后再生成else块的标签和代码。 90 | 91 | 例如上述代码转化为IR后可能如下: 92 | 93 | ```asm 94 | _main: 95 | _T0 = 2 # 代表a = 2 96 | _T1 = 0 # 代表b = 0 97 | bnez _T0, _L0, _L1 # 如果a != 0,跳转到_L0,否则跳转到_L1 98 | _L0: 99 | _T2 = 1 # 代表b = 1 100 | jump _L2 # 跳转到_L2,跳过else块 101 | _L1: 102 | _T2 = -1 # 代表b = -1 103 | jump _L2 # 跳转到_L2 104 | _L2: 105 | ret _T2 106 | ``` 107 | 我们将在[中间代码生成](./irgen.md)中介绍生成中间代码时需要考虑的地方和并列举一些实例。 108 | 109 | ## 中端优化 110 | 111 | 中端的优化是编译器的一个重要组成部分,它可以在保持程序功能不变的前提下,提高程序的性能。中端优化的目标是提高程序的性能,减少程序的运行时间和资源消耗。中端优化的方法有很多,比如常量传播、死代码消除、循环不变量外提、循环展开、函数内联等。 112 | 113 | 一个经典的例子是常量传播。常量传播是指将一个常量值替换为它的值,以便于在中端直接完成一些计算以降低运行时开销。比如,对于下面的 IR 代码: 114 | 115 | ```asm 116 | _T1 = 5 117 | _T2 = _T1 + 6 118 | _T3 = _T2 + 7 119 | _T4 = _T3 + 8 120 | _T5 = _T4 + 9 121 | ret _T5 122 | ``` 123 | 124 | 经过常量传播优化后,可以得到: 125 | 126 | ```asm 127 | _T1 = 5 128 | _T2 = 11 129 | _T3 = 18 130 | _T4 = 26 131 | _T5 = 35 132 | ret _T5 133 | ``` 134 | 135 | 进一步如果我们进行死代码消除,可以得到: 136 | > 死代码消除是指删除程序中没有用到的代码,以减少程序的运行时间和资源消耗。 137 | 138 | ```asm 139 | _T5 = 35 140 | ret _T5 141 | ``` 142 | 143 | 中端优化依赖与数据流、控制流分析,你需要先了解一些数据流分析的基础知识才能进行一些中端优化。 144 | 145 | 我们的文档里在[数据流分析](../../step6/dataflow.md)中对数据流分析进行了简单介绍,你可以在这里了解一些数据流分析的基础知识。除了这个文档中介绍的数据流分析,还有很多其他的数据流分析方法,比如Use-Def链、Def-Use链、可达定义分析等。 146 | 147 | 我们在文档中对两个优化进行简单介绍,详见[常量传播](./cp.md)和[死代码消除](./dce.md)。 148 | 149 | ## 中端参考资料 150 | 151 | 本章中我们以几个简单的例子介绍了什么是中间表示、中端优化以及如何做中端优化。此外我们也将会在这里给出一些中端优化的参考资料,供大家学习。 152 | 153 | - [GCM & GVM](https://courses.cs.washington.edu/courses/cse501/06wi/reading/click-pldi95.pdf) 154 | 155 | - [Engineering A Compiler](https://github.com/lighthousand/books/blob/master/Engineering%20A%20Compiler%202nd%20Edition%20by%20Cooper%20and%20Torczon.pdf) 156 | 157 | - [LLVM IR](https://llvm.org/docs/LangRef.html) 158 | 159 | - [SSA book](https://pfalcon.github.io/ssabook/latest/book-full.pdf) 160 | 161 | 162 | ## 预期目标 163 | 164 | 完成这部分内容后,你的编译器应该能将 MiniDecaf 程序翻译成 IR,并能够输出 IR。进一步地,如果你希望参加性能评测,你还需要实现一些中端优化。 165 | 166 | -------------------------------------------------------------------------------- /docs/contest/midend/short_circuit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/contest/midend/short_circuit.jpg -------------------------------------------------------------------------------- /docs/contest/midend/short_circuit.md: -------------------------------------------------------------------------------- 1 | # 短路求值 2 | 3 | ## 介绍 4 | 逻辑与和逻辑或操作符总是先计算其左操作数,然后再计算其右操作数。只有在仅靠左操作数的值无法确定该逻辑表达式的结果时,才会求解其右操作数。我们常常称这种求值策略为“短路求值(short-circuit evaluation)”。 5 | 6 | ## 我们为什么要这样做呢? 7 | 最直观的好处就是:能够通过左操作数就确定逻辑表达式的结果,降低了程序计算量,提高程序效率。 8 | 9 | 除此之外,在某些情况下,避免对表达式的第二部分求值可以防止潜在的错误或异常,比如在C++中,对于如下代码: 10 | ```cpp 11 | int *p = nullptr; 12 | /*... */ 13 | if (p && *p) { /*... */ } 14 | /*... */ 15 | ``` 16 | 在这个例子中,如果`p`为`nullptr`,那么`*p`将会导致错误。而如果我们使用短路求值,那么在`p`为`nullptr`时,`*p`不会被求值,直接跳过`*p`的计算,从而避免了错误。 17 | 18 | 当然,在我们的 MiniDecaf 中,并不支持指针,但是逻辑表达式是否为短路求值仍然有可能对程序的含义产生影响,例如: 19 | ```c 20 | int main() { 21 | int a = 0; 22 | if (a && (a = 1)) { 23 | return a; 24 | } 25 | return a; 26 | } 27 | ``` 28 | 在这个例子中,如果没有短路求值,最终`a`应该为`1`;如果使用短路求值,就不会到达`a = 1`,此时`a`的值为`0`。 29 | 30 | ## 我们如何实现短路求值呢? 31 | 首先我们要知道,只有在出现了`&&`或`||`时,才会使用短路求值。当 `&&` 的第一个运算数的值为 false 时,其结果必定为 false;当 `||` 的第一个运算数为 true 时,最后结果必定为 true,在这种情况下,就不需要知道第二个运算数的具体值。 32 | 33 | 具体的实现方法有`跳转法`和`拉链与代码回填(backpatching)`,这里我们只讲解`跳转法`。同时,要注意逻辑表达式不仅出现在条件判断中,因此我们分以下两种情况考虑: 34 | 35 | ### 用于条件判断 36 | 对于下面这个例子: 37 | ```c 38 | int main() { 39 | int a = 1, b = 2, c = 3, d = 4; 40 | if (a && b || c) { 41 | d = 1; 42 | } 43 | else { 44 | d = 0; 45 | } 46 | return d; 47 | } 48 | ``` 49 | `if`部分的流程图应该如下所示:(别忘了`&&`和`||`的优先级) 50 | ![短路求值流程图](short_circuit.jpg) 51 | 52 | 上述代码转化为IR后可能如下: 53 | ``` 54 | i32 main() { 55 | _B0: 56 | alloca i32* _T0 = 4 # a 57 | i32 _T1 = 1 58 | store *(i32* _T0 + 0) = i32 _T1 59 | alloca i32* _T2 = 4 # b 60 | i32 _T3 = 2 61 | store *(i32* _T2 + 0) = i32 _T3 62 | alloca i32* _T4 = 4 # c 63 | i32 _T5 = 3 64 | store *(i32* _T4 + 0) = i32 _T5 65 | alloca i32* _T6 = 4 66 | i32 _T7 = 4 67 | store *(i32* _T6 + 0) = i32 _T7 68 | load i32 _T8 = *(i32* _T0 + 0) 69 | if i32 _T8 == 0 jump _B4 else jump _B5 # 判断a 70 | _B1: 71 | i32 _T11 = 1 72 | store *(i32* _T6 + 0) = i32 _T11 # d = 1 73 | jump _B3 74 | _B2: 75 | i32 _T12 = 0 76 | store *(i32* _T6 + 0) = i32 _T12 # d = 0 77 | jump _B3 78 | _B3: 79 | load i32 _T13 = *(i32* _T6 + 0) 80 | return i32 _T13 # return d 81 | _B4: 82 | load i32 _T10 = *(i32* _T4 + 0) 83 | if i32 _T10 == 0 jump _B2 else jump _B1 # 判断c 84 | _B5: 85 | load i32 _T9 = *(i32* _T2 + 0) 86 | if i32 _T9 == 0 jump _B4 else jump _B1 # 判断b 87 | } 88 | ``` 89 | 你可以参考上述 IR 中的分支跳转指令,思考如何使用跳转指令来实现短路求值。 90 | 91 | ### 非条件判断 92 | 上一部分是逻辑表达式为条件语句的情况,那如果逻辑表达式不用于条件判断呢?例如: 93 | ```c 94 | int main() { 95 | int a = 1, b = 2, c = 3, d = 2; 96 | d = a && b || c; 97 | return d; 98 | } 99 | ``` 100 | 一种思路是把它当成`if`语句来处理: 101 | ```c 102 | int main() { 103 | int a = 1, b = 2, c = 3, d = 4; 104 | if (a && b || c) { 105 | d = 1; 106 | } 107 | else { 108 | d = 0; 109 | } 110 | return d; 111 | } 112 | ``` 113 | 最终可以得到与前一个例子同样的IR。 -------------------------------------------------------------------------------- /docs/contest/midend/ssa.md: -------------------------------------------------------------------------------- 1 | 2 | # 静态单赋值 3 | 4 | > 静态单赋值这一小节参考并改编自北航的编译课程实验文档: 5 | > https://buaa-se-compiling.github.io/miniSysY-tutorial/challenge/mem2reg/help.html 6 | > 在此表示感谢! 7 | 8 | 静态单赋值(Static Single Assignment, SSA)是编译器中间表示(IR)阶段的一个重要概念,它要求程序中每个变量在使用之前只被赋值一次。 9 | 10 | 例如,考虑使用 IR 编写程序计算 1 + 2 + 3 的值,一种可能的写法为: 11 | 12 | ```assembly 13 | _T0 = 1 14 | _T1 = 2 15 | _T2 = 3 16 | _T3 = _T0 + _T1 17 | _T3 = _T3 + _T2 18 | ret _T3 19 | ``` 20 | 很遗憾,上述程序并不符合 SSA 的要求,因为其中变量 _T3 被赋值了两次。正确的写法应该为: 21 | ```assembly 22 | _T0 = 1 23 | _T1 = 2 24 | _T2 = 3 25 | _T3 = _T0 + _T1 26 | _T4 = _T3 + _T2 27 | ret _T4 28 | ``` 29 | #### 我们为什么要这样做呢? 30 | 31 | 因为 SSA 可以简化每个变量的属性,进而简化编译器的优化过程。 32 | 33 | 例如,考虑下面这段伪代码: 34 | 35 | ```assembly 36 | y = 1 37 | y = 2 38 | x = y 39 | ``` 40 | 很显然,其中变量 y 的第一次赋值是不必须的,因为变量 y 被使用前,经历了第二次赋值。对于编译器而言,确定这一关系并不容易,需要经过定义分析(Reaching Definition Analysis)的过程。在很多控制流复杂的情况下,上述过程将变得更加困难。 41 | 42 | 但如果将上述代码变为 SSA 形式: 43 | 44 | ```assembly 45 | y1 = 1 46 | y2 = 2 47 | x1 = y2 48 | ``` 49 | 上述关系变得更加显而易见,由于每一个变量只被赋值一次,编译器可以轻松地得到 x1 的值来自于 y2 这一信息。 50 | 51 | 正因如此,许多编译器优化算法都建立在 SSA 的基础之上,例如:死代码消除(dead code elimination)、常量传播(constant propagation)、值域传播(value range propagation)等。 52 | 53 | #### 我们如何实现 SSA 呢? 54 | 55 | 例如,考虑使用 IR 编写程序使用循环计算 5 的阶乘。 56 | 57 | 按照 C 语言的思路,我们可能给出如下写法: 58 | 59 | ```assembly 60 | _L0: 61 | _T0 = 0 62 | _T1 = 1 63 | _T2 = 2 64 | _T3 = _T0 + _T1 # int temp = 1 65 | _T4 = _T0 + _T2 # int i = 2 66 | _T5 = 5 67 | _L1: 68 | _T6 = _T4 < _T5 # i < 5 69 | beqz _T6, _L3 70 | _L2: # loop label 71 | _T3 = _T3 * _T4 # temp = temp * i 72 | _T4 = _T4 + _T1 # i = i + 1 73 | jump _L1 74 | _L3: # break label 75 | ret _T3 76 | ``` 77 | 我们注意到,变量 _T3 和 _T4 由于循环体的存在可能被赋值多次,因此上述写法并不符合 SSA 的要求。 78 | 79 | 一种可能的方案是使用 Phi 指令。Phi 指令的语法是 ` = PHI [, ], [, ] ...` 。它使得我们可以根据进入当前基本块之前执行的是哪一个基本块的代码来选择一个变量的值。 80 | 81 | 由此,我们的程序可以改写为: 82 | 83 | ```assembly 84 | _L0: 85 | _T0 = 2 86 | _T1 = 1 87 | _L1: 88 | _T2 = PHI [_T0, _L0], [_T6, _L2] # int i = 2 89 | _T3 = PHI [_T1, _L0], [_T7, _L2] # int temp = 1 90 | _T4 = 5 91 | _T5 = _T2 < _T4 # i < 5 92 | beqz _T5, _L3 93 | _L2: # loop label 94 | _T7 = _T3 * _T2 # temp = temp * i 95 | _T6 = _T2 + _T1 # i = i + 1 96 | jump _L1 97 | _L3: # break label 98 | ret _T3 99 | ``` 100 | 由此,上述程序中每一个变量只被赋值了一次,满足了 SSA 的要求。(注意,SSA 仅要求变量在静态阶段被单一赋值,而不是在运行时仅被赋值一次) 101 | 102 | 另一种可能的方案是使用 Alloca、Load 和 Store 的组合。SSA 要求中间表示阶段虚拟寄存器满足单一赋值要求,但并不要求内存地址如此。因此,我们可以在前端生成中间代码时,将每一个变量都按照栈的方式使用 Alloca 指令分配到内存中,之后每次访问变量都通过 Load 或 Store 指令显式地读写内存。使用上述方案编写的程序满足 SSA 的要求,且避免了繁琐地构造 Phi 指令,但频繁地访问内存将导致严重的性能问题。 103 | 104 | #### 有没有更好的解决方案呢? 105 | 106 | 有,我们可以将两种方案结合起来。 107 | 108 | 在前端生成中间代码时,首先使用第二种方案利用 Alloca、Load、Store 指令快速地构建满足 SSA 要求的代码。 109 | 随后,在上述代码的基础上, 将其中分配的内存变量转化为虚拟寄存器,并在合适的地方插入 Phi 指令。 110 | 这一解决方案也被称为 mem2reg 技术。 111 | 112 | mem2reg 使得我们可以在生成中间代码时,使用 Alloc、Load 和 Store 的组合针对局部变量生成符合 SSA 要求的代码。 113 | 114 | 举个例子,一种可能的中间代码表示为: 115 | 116 | ```assembly 117 | main: 118 | _T0 = alloc 4 119 | _T1 = alloc 4 120 | store _T0, 1 121 | load _T2, _T0 122 | _T4 = _T2 > 0 123 | beqz _T4, _L2 124 | store _T2, 1 125 | _L1: 126 | load _T5, _T2 127 | ret _T5 128 | _L2: 129 | _T6 = 0 - 1 130 | store _T2, _T6 131 | jump _L1 132 | ``` 133 | 134 | 在此基础上,进行 mem2reg 转化: 135 | 136 | ```assembly 137 | main: 138 | _T0 = 1 > 0 139 | beqz _T0, _L2 140 | _L1: 141 | _T2 = phi [1, main], [_T3, _L2] 142 | ret _T2 143 | _L2: 144 | _T3 = 0 - 1 145 | jump _L1 146 | ``` 147 | 148 | 需要注意的是,所有的 Phi 指令应当在基本块的开头同时支持并行执行(即在同一个基本块内的 Phi 指令的顺序对结果没有影响)。 149 | 150 | 在实现 mem2reg 时,我们需要首先对代码进行数据流分析,计算控制流图中的支配关系和每个基本块的支配边界。 151 | 152 | > 相关的解释和详细说明可以参考: 153 | > 如何构建 SSA 形式的 CFG:https://szp15.com/post/how-to-construct-ssa/ 154 | 155 | 随后,我们需要实现 SSA 构造算法。一种常用的算法是将整个过程分为:插入 phi 函数和变量重命名,两个阶段。 156 | 157 | 在第一阶段,记录每个局部变量相关的 Alloc 和 Store 指令,并由此在基本块的开头插入 Phi 指令。 158 | 159 | 在第二阶段,遍历所有基本块,对其中局部变量相关的 Alloc,Load 和 Store 指令进行改写,以保证程序语义的正确性。在遍历一个基本块的所有指令后,维护该基本块的所有后继基本块中的 Phi 指令。 160 | 161 | > 相关的解释和详细说明可以参考: 162 | > 163 | > Static Single Assignment Book 的 Chapter3:https://pfalcon.github.io/ssabook/latest/ 164 | -------------------------------------------------------------------------------- /docs/misc/schedule.md: -------------------------------------------------------------------------------- 1 | # 实验进度安排 2 | 3 | 所有截止时间均为所标日期的23:59:59(UTC+8),即第二天0点之前,如有特殊情况将会在网络学堂通知。 4 | 5 | ## 必做部分: 6 | - 第三周周日(9.29):Stage 0 截止 熟悉框架和基础知识 (占比:0%) 7 | - stage 0 不需要你编写任何代码,stage 0的思考题请与stage 1一起提交。 8 | 9 | 10 | - 第四周周日(10.6):Stage 1 截止 常量表达式(占比:7%) 11 | 12 | - 第六周周日(10.20):Stage 2 截止 变量(占比:7%) 13 | 14 | - 第八周周日(11.3):Stage 3 截止 作用域(占比:7%) 15 | 16 | - 第十周周日(11.17):Stage 4 截止 控制语句(占比:7%) 17 | 18 | - 第十四周周日(12.15):Stage 5 截止 函数(占比:7%) 19 | - 函数部分由于难度较大,给大家预留了四周时间,请大家不要等到最后一周再开始。 20 | 21 | ## 选做部分: 22 | 选做部分难度较大,且时间较紧,同学可能需要提前一些开始才能保证完成。 23 | 24 | - 第十五周周日(12.22):Stage 6 截止 全局变量和数组(占比:7%) 25 | 26 | - 第十六周周日(12.29):Stage 7 截止 寄存器分配与代码优化(占比:8%) 27 | 28 | 29 | ## 补交政策 30 | 31 | * 假设 a 日 24:00 是某个 stage 的截止时间; 32 | * a + k 日 24:00 前补交,此 stage 得分乘以 max(1 - (k / 20), 0.5); 33 | * 提交/补交时间是该 stage 截止后这个 stage 的 branch 最后一次触发 CI 的时间; 34 | * 更具体来讲是创建 pipeline 的时间,而不是 pipeline 更新的时间,这几乎等于你 push 到远端仓库的时间。并且,你也可以随意地 retry 反复运行 CI,这不会对你的提交/补交产生影响。 35 | * 如果在多次 retry 中你的代码会得到不一致的结果,请联系助教。 36 | * 选做实验不接受补交。 37 | -------------------------------------------------------------------------------- /docs/ref/riscv.md: -------------------------------------------------------------------------------- 1 | # RISC-V相关内容补充 2 | 3 | ## RISC-V官方资料 4 | 5 | 不建议阅读,太过冗长,这对于编译知识提升非常有限。 6 | 7 | [RISC-V 官方](https://riscv.org/technical/specifications/) 8 | 请下载ISA Specifications (Ratified)中的Volume 1, Unprivileged Specification。 9 | 10 | 如果你时间充足,你可以阅读: 11 | 12 | Chapter 24 RV32/64G Instruction Set Listings 13 | 14 | Chapter 25 RISC-V Assembly Programmer’s Handbook 15 | 16 | ## 如何快速查询RISC-V指令 17 | 18 | ### 在线编译器 19 | 20 | 你可以使用[Compiler Explorer (godbolt.org)](https://godbolt.org/)来快速获得一个riscv指令的实现 21 | 22 | 在左边输入以下例子 23 | 24 | ```c++ 25 | int mod(int x, int y) { 26 | // 注意:此处不要直接写一个可以计算得到结果的式子 27 | // 比如5 % 8会被编译器优化为5 28 | return x % y; 29 | } 30 | ``` 31 | 32 | 把右边的编译器选为RISC-V(32-bits)中的任何一个,在编译选项中写上-O2(减少不必要的指令生成),翻译一条指令看看效果。 33 | 34 | 35 | ### 本地编译器 36 | 37 | 你可以通过 gcc 编译如下程序来了解如何翻译逻辑非运算符到 RISC-V 汇编 riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 foo.c -S -O3 -o foo.s(**记得加 -O3 或者 -O2 选项**): 38 | ```c++ 39 | int foo(int x) { 40 | return !x; 41 | } 42 | ``` 43 | 44 | 不出意外你会获得如下结果: 45 | ``` 46 | foo: 47 | seqz a0,a0 48 | ret 49 | ``` -------------------------------------------------------------------------------- /docs/step-parser/example.md: -------------------------------------------------------------------------------- 1 | # parser-stage 框架介绍 2 | 3 | 下面我们介绍 parser-stage 实验框架中的一些函数,来帮助大家更好地理解实验框架。 4 | 5 | 注意**我们的框架并不完全是课堂讲授的基于 LL(1) 文法的递归下降分析方法**,在有些地方会通过 while 循环来解析多个连续的、左结合的表达式,对应于等价的拓展巴克斯范式(EBNF)文法。详见下文关于 `p_multiplicative` 函数的介绍。 6 | 7 | ## 框架接口 8 | 9 | ### lookahead 函数 10 | 11 | `def lookahead(self, type: Optional[str] = None) -> Any` 12 | 13 | 词法分析器 lex 将程序字符串转换为一串 token, 用 `next(lexer)` 获取词法分析器提供的下一个 token。 14 | 15 | 为了实现递归下降语法分析中的 "lookahead" 机制,我们需要**看下一个 token 是什么,但却不能消耗这个 token**,为此我们用一个变量 `next_token` (Parser.next_token)暂存将要被解析的下一个 token。通过判断 `next_token` 的类型,来判断将要使用哪一条产生式。 16 | 17 | 而 `lookahead()` 函数有两个重载的版本。一个版本不带参数,直接读取一个 token; 18 | 另一个版本传入了一个 token 类型做参数,表示希望读取一个特定类型的 token,如果类型不符则报错。每次执行 `lookahead()` 函数,都会**消耗**当前的 `next_token` 并从词法分析器获得新的 token 赋值给 `next_token` 变量。 19 | 20 | 注意直接访问 `next_token` 变量和执行 `lookahead()` 函数的区别在于:是否消耗一个 token 并向词法分析器请求下一个 token。`next_token` 变量不会消耗,而 `lookahead()` 函数则会消耗一个 token。 21 | 22 | ### First/Follow 23 | 24 | 某个产生式的 First 集合,包括可能在该产生式右端第一个出现的所有 token。如果该产生式可以产生空串,则该 First 集合也包含空串。 25 | 26 | 某个非终结符的 Follow 集合,包括可能紧跟出现在该非终结符之后 token。 27 | 28 | 在我们的框架里,因为语法非常简单,所以没有进一步计算 PS 预测集合,而是直接用 if 语句结合 First/Follow 集合直接进行判断(用 if 语句枚举判断输入的 token 是否属于集合中的元素)。 29 | 30 | 代码框架中通过**装饰器模式**(decorator pattern)定义了每个产生式左端非终结符的 First 集合,例如 `p_declaration` 函数开头的 `@first("Int")` 表示 `declaration` 的 First 集只包含 token `'Int'`。代码框架里没有显式定义 Follow 集合。事实上,需要同学们完善的部分里并不需要用到 First/Follow 集合,直接使用 if 语句判断即可。 31 | 32 | ### p_Multiplicative 33 | 34 | 我们使用代码框架中的 `p_multiplicative()` 函数介绍框架里是如何使用与语法规范等价的 EBNF 文法及其解析方法。这两个函数都希望从当前的 token 流中,解析出一个 `multiplicative` 表达式,并返回其语法树结点。 35 | 36 | `multiplicative` 对应的语法为: 37 | 38 | ``` 39 | multiplicative : multiplicative '*' unary 40 | | multiplicative '/' unary 41 | | multiplicative '%' unary 42 | | unary 43 | ``` 44 | 45 | 容易发现,这个产生式是左递归的,不适合基于 LL(1) 的递归下降 分析器直接处理。我们将其转换为 EBNF 的形式进行程序解析:`multiplicative : unary { '*' unary | '/' unary | '%' unary }` 其中,EBNF 中的大括号表示重复零次或任意多次。 46 | 47 | 注意到产生式的开头总有一个 `Unary` 非终结符,所以我们递归调用 `p_Unary()` 函数解析对应的 `Unary` 非终结符,如果通过 `next_token` 检查到后续符号不属于 `*`、`/` 或 `%`,就可以直接返回创建并返回 `Unary` AST结点。否则,通过 `lookahead()` 读取掉运算符 `(* / %)`,并按照左结合的方法,循环解析更多的 `Unary` 非终结符。最终完成 Multiplicative 对应 AST 结点的构建。 48 | 49 | 例如,让我们考虑这个函数如何处理连乘积 `1*2*3*(4+5)*x`: 50 | 51 | 递归解析出 `1` 对应的 `Unary` AST 结点,然后进入 `while` 循环: 52 | 53 | while 循环第一轮: `lookahead` 消耗掉 `*`,递归解析出 `2` 对应的 AST 结点,然后构建 `1*2` 这个乘法表达式对应的 AST 结点; 54 | 55 | while 循环第二轮: `lookahead` 消耗掉 `*`,递归解析出 `3` 对应的 AST 结点,然后构建 `1*2*3` 这个乘法表达式对应的 AST 结点; 56 | 57 | ……(以此类推) 58 | 59 | 直到处理完 `x` 后,发现下一个 token 不是 `*`,那么当前 `multiplicative` 非终结符对应的文法 parse 结束,并返回 AST 结点。 60 | 61 | 最后得到的 AST 为: 62 | ``` 63 | binary(*) [ 64 | binary(*) [ 65 | binary(*) [ 66 | binary(*) [ 67 | int(1) 68 | int(2) 69 | ] 70 | int(3) 71 | ] 72 | binary(+) [ 73 | int(4) 74 | int(5) 75 | ] 76 | ] 77 | identifier(x) 78 | ] 79 | ``` 80 | 81 | 82 | ### 需要填写的函数 83 | 84 | 实验框架中标记有 `TODO` 的函数需要我们填写。填写正确后,合并 stage2 的中端、后端,要求**通过 step1-6 的测例**。 85 | 86 | 需要完成的函数: 87 | ``` 88 | p_relational p_logical_and p_assignment p_expression p_statement 89 | p_declaration p_block p_if p_return p_type 90 | ``` 91 | 92 | 93 | # 思考题 94 | 95 | 1. 在框架里我们使用 EBNF 处理了 `additive` 的产生式。请使用课上学习的消除左递归、消除左公因子的方法,将其转换为不含左递归的 LL(1) 文法。(不考虑后续 multiplicative 的产生式) 96 | ``` 97 | additive : additive '+' multiplicative 98 | | additive '-' multiplicative 99 | | multiplicative 100 | ``` 101 | 102 | 2. 对于我们的程序框架,在自顶向下语法分析的过程中,如果出现一个语法错误,可以进行**错误恢复**以继续解析,从而继续解析程序中后续的语法单元。 103 | 请尝试举出一个出错程序的例子,结合我们的程序框架,描述你心目中的错误恢复机制对这个例子,怎样越过出错的位置继续解析。(注意目前框架里是没有错误恢复机制的。) 104 | 105 | 3. (选做,不计分)指出你认为的本阶段的实验框架/实验设计的可取之处、不足之处、或可改进的地方。 106 | -------------------------------------------------------------------------------- /docs/step-parser/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 parser-stage:自顶向下语法分析器 2 | 3 | 在 Stage1-2 中,实验框架使用了 ply 作为语法分析器,解析 MiniDecaf 程序并生成 AST。 4 | 5 | 在 parser-stage 中,我们将结合课堂上学习的 LL(1) 分析方法,完成一个**手工实现的递归下降**语法分析器。为了降低难度和工作量,将提供分析器的基本框架和部分实现,同学们只需要补全代码片段即可。所实现的手工语法分析器,只需要支持 [**Step1-6 的语法**](spec.md)。 6 | 7 | ## 准备工作 8 | 9 | 10 | parser-stage 不涉及中端、后端部分,所以请同学们将 stage2 中完成的中后端代码合并到 parser-stage 的实验框架上。具体的操作可以参考如下步骤: 11 | 12 | ```bash 13 | $ git switch parser-stage 14 | $ git merge stage-2 15 | ``` 16 | 17 | (本步骤所需要的额外文件请在[此处](https://cloud.tsinghua.edu.cn/d/9b34fdf53a3c48b8bc52/)获取,在 python/ 下) 18 | 19 | 在切换到 `parser-stage` 分支之后,从[链接](https://cloud.tsinghua.edu.cn/d/9b34fdf53a3c48b8bc52/)下载 python 目录下的文件,并使用 `frontend/parser/` 目录整个替换你 stage2 代码的对应目录,然后在整体框架上完成实验。 20 | 21 | **需要注意的是**,parser-stage 的实验相对于其他 stage 是独立的。在后续进行 stage3 的实验时,应从 stage2 所完成的代码开始,而不需要用 parser-stage 的代码。未来在进行 stage3 实验时,建议进行如下操作: 22 | 23 | ```bash 24 | $ git switch stage-3 25 | # 注意不要从 parser-stage merge 26 | $ git merge stage-2 27 | ``` 28 | 29 | ## 背景知识 30 | 31 | 如果你已经很熟悉自顶向下语法分析、自底向上语法分析的原理,可以跳过这部分。这里我们只对两种语法分析方法进行简单介绍,**详细原理请参考课件**。 32 | 33 | bison/ply 自动生成的语法分析器,属于 LALR(1) 语法分析,是**自底向上**的语法分析方法。 34 | 35 | 具体来说,维护一个栈(保存状态和符号),每一步操作如果是移进(shift)操作,则将新的 token 加入栈顶;如果是归约(reduce)操作,则依据归约对应的产生式的右端,将栈顶的状态和符号依次弹出,然后将产生式左端的非终结符(以及对应转移到的状态)入栈。根据归约的结果,从语法树的最底层开始,自底向上构建 AST 结点,最终得到整个 AST。 36 | 37 | 38 | 而**递归下降**语法分析的过程是: 39 | 40 | 从文法开始符号(对应 AST 的根结点)起,通过预测集合 PS(实际实现中,为了简便,直接采用了 First 集和 Follow 集)以及输入符号,选择对应的产生式。对于产生式右侧的非终结符和终结符分别进行不同的操作,对于非终结符通过调用递归函数进行处理,对于终结符通过 matchToken(实际实现中,用 lookahead 函数实现) 进行处理。由于在递归下降分析的过程中,只有分析完叶子结点后,才会返回,所以实际的 AST 构造过程也是自底向上构建 AST 结点,最终得到整个 AST。 41 | 42 | 43 | ## 任务描述 44 | 45 | 要求: 46 | 1. 使用所提供的 parser-stage 框架替换你的编译器中的 parser 部分,完善框架中的实现,**通过 Step1-6 的测试**。 47 | 2. 本步骤需要修改的代码均有 `TODO` 标识,并有相关的引导注释。其中需要修改的文件为 `frontend/parser/my_parser.py`。 48 | 3. 完成实验报告(具体要求请看实验指导书的首页)。实验报告中需要包括: 49 | * 你的学号姓名 50 | * 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 51 | * 指导书上的思考题 52 | * 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 53 | * 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 -------------------------------------------------------------------------------- /docs/step-parser/spec.md: -------------------------------------------------------------------------------- 1 | # parser-stage 语法规范(同 step6) 2 | 3 | 4 |
 5 | 
 6 | program
 7 |     : function
 8 | function
 9 |     : type Identifier '(' ')' '{' block_item* '}'
10 | type
11 |     : 'int'
12 | block_item
13 |     : statement
14 |     | declaration
15 | statement
16 |     : 'return' expression ';'
17 |     | expression? ';'
18 |     | 'if' '(' expression ')' statement ('else' statement)?
19 | declaration
20 |     : type Identifier ('=' expression)? ';'
21 | expression
22 |     : assignment
23 | assignment
24 |     : conditional
25 |     | Identifier '=' expression
26 | conditional
27 |     : logical_or
28 |     | logical_or '?' expression ':' conditional
29 | logical_or
30 |     : logical_and
31 |     | logical_or '||' logical_and
32 | logical_and
33 |     : equality
34 |     | logical_and '&&' equality
35 | equality
36 |     : relational
37 |     | equality ('=='|'!=') relational
38 | relational
39 |     : additive
40 |     | relational ('<'|'>'|'<='|'>=') additive
41 | additive
42 |     : multiplicative
43 |     | additive ('+'|'-') multiplicative
44 | multiplicative
45 |     : unary
46 |     | multiplicative ('*'|'/'|'%') unary
47 | unary
48 |     : primary
49 |     | ('-'|'~'|'!') unary
50 | primary
51 |     : Integer
52 |     | '(' expression ')'
53 |     | Identifier
54 | 
55 | 56 | -------------------------------------------------------------------------------- /docs/step0/env.md: -------------------------------------------------------------------------------- 1 | # 实验框架环境配置 2 | 3 | ## Python 实验框架环境配置 4 | 5 | 关于操作系统,助教推荐使用 Linux 环境(如 Ubuntu,Debain 或 Windows 下的 WSL 等),当然你也可以在类 Unix 系统环境(Mac OS)中进行开发。助教不推荐直接在 Window 中搭建开发环境。你需要安装或保证如下软件满足我们的要求: 6 | 7 | 1. python >= 3.9 8 | 9 | 助教**强烈建议**使用类似 [Miniconda](https://docs.conda.io/en/latest/miniconda.html) 或venv的系统管理不同的Python环境。你可以方便地使用miniconda安装最新的Python版本,安装好之后使用pip安装依赖即可。 10 | 11 | 框架本身在 python 3.9 下进行开发,使用了 python 3.9 的新特性,请保证你所使用的 python 版本高于此版本。 12 | 13 | 如果你**没有**使用虚拟环境,可以参考下面的指导。Linux 环境下安装 Python 3 可以尝试如下命令: 14 | ```bash 15 | > sudo add-apt-repository ppa:deadsnakes/ppa 16 | > sudo apt update 17 | > sudo apt install python3 18 | ``` 19 | 20 | 此外,如果安装了多个版本的 python,可以通过 `update-alternatives` 命令修改 python 版本使用的优先级,对所有服务器用户都有效,具体用法可参见[这里]( https://medium.com/analytics-vidhya/how-to-install-and-switch-between-different-python-versions-in-ubuntu-16-04-dc1726796b9b)。你可以通过此命令来检查当前优先的 Python3 版本: 21 | ``` 22 | > python3 --version 23 | ``` 24 | 25 | 框架里已经提供了需要的 python 包列表文件 requirements.txt,你可以通过 pip 命令安装下文提到的 python 依赖包 ply 和 argparse: 26 | 27 | ```bash 28 | $ python3 -m pip install -r ./requirements.txt 29 | ``` 30 | 31 | 2. argparse 32 | 33 | 框架使用了 [argparse](https://docs.python.org/zh-cn/3/library/argparse.html) 以处理命令行参数。官方文档中提供了它的[教程](https://docs.python.org/zh-cn/3/howto/argparse.html)。 34 | 35 | 3. ply 36 | 37 | ply是一个自动生成词法分析器和语法分析器的工具,其中ply.lex为词法分析相关的模块而ply.yacc为语法分析相关。可以参考 ply 的[文档](https://www.dabeaz.com/ply/ply.html)。 38 | 39 | 助教在项目中使用 [type hints](https://www.python.org/dev/peps/pep-0483/),如果你习惯在 vscode 中进行开发的话同时推荐使用 [pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) 这一插件。 40 | 41 | 由于 python 的跨平台性,理论上也可以在 Windows 下进行开发。但**不保证Windows和在线测试环境下程序行为的一致性**。 42 | -------------------------------------------------------------------------------- /docs/step0/errate.md: -------------------------------------------------------------------------------- 1 | # 勘误表 2 | 3 | 在这里我们会列出与实验相关的**勘误**,它会和[问答墙](https://docs.qq.com/doc/DY1hZWFV0T0N0VWph)上的勘误部分保持一致。同学们遇到问题时,请先在勘误表中查找查看是否已有解答。 4 | 5 |   6 | 7 | Q:使用 `pip install -r ./requirements.txt` 命令无法正确安装依赖? 8 | 9 | A:如果你安装了多版本的 python,使用 pip 命令未必会对应 3.9 版本的包管理器。请尝试使用 `python3.9 -m pip install -r ./requirements.txt` 安装依赖。 10 | 11 |   12 | 13 | Q: 代码框架 step7 中,由 multi_nesting.c 生成的以下中间代码无法成功生成目标代码。 14 | 15 | 经过使用 print 法调试,发现是 `_T1` 所对应的寄存器在 `return _T1` 前就被释放了,后端会尝试到栈中寻找 `_T1` 并且不会找到,出现报错: 16 | `utils.error.IllegalArgumentException: error: encounter a non-returned basic block` 17 | 18 | 请问是后端实现上有问题,还是这一部分本来就需要我们自己修改呢? 19 | 20 | A:代码框架的后端除了要修改指令选择部分之外,还需要修改基本块 CFG,可以参见 BruteRegAlloc 的注释里给出的提示。 21 | 22 |   23 | 24 | Q:我怎样才能知道我的提交通过了所有测试用例? 25 | 26 | A:可以通过本地测试或者通过 CI 结果可以判断是否通过了本阶段测例(不过你需要确保你的提交在对应的 branch 上,如 stage1 对应 stage-1 分支)。 27 | 28 |   29 | 30 | Q:如何提交课程报告? 31 | 32 | A: 33 | 34 | 1. 请将实验报告以 pdf 格式提交到 git.tsinghua 自己的仓库中,放在仓库根目录下的 `reports/.pdf`,比如 stage 1 的实验报告需要放在 stage-1 这个 branch 下的 `reports/stage-1.pdf`。 35 | 36 | 2. 最新的 CI 会检查是否通过所有测例及是否有提交报告,只有通过所有测例且正确地提交报告,才会算作 pass。 37 | 38 | 3. 如果关于报告提交有任何问题,请及时联系助教。 39 | -------------------------------------------------------------------------------- /docs/step0/intro.md: -------------------------------------------------------------------------------- 1 | # 实验环境简介 2 | 3 | 2024年秋季学期,助教给大家提供了服务器。同学们可以选择使用我们已经配置好的机器,节省一定的时间花费。 4 | 5 | 服务器环境如下: 6 | - Ubuntu 23.04 7 | - python 3.11.4 8 | 9 | 注意: 10 | 1. 禁止大家在服务器上安装软件、运行与课程实验无关的程序或者破环系统环境。否则将有可能受到惩罚。 11 | 2. 为了安全起见,服务器仅可通过校内网络访问。 12 | 3. 服务器的地址、账号和密码会通过网络学堂发给大家,请登录网络学堂查收。 13 | 14 | 提示: 15 | 1. 如何使用 ssh 登陆服务器? 16 | ```bash 17 | ssh username@ip -p port 18 | # 假如你的账号为 2024000001,服务器 ip 地址为 192.168.1.1,端口为 223,则命令为: 19 | ssh 2024000001@192.168.1.1 -p 223 20 | ``` 21 | 2. 建议配置 ssh 免密登录,方便大家使用服务器,然后在服务器上运行。你可以参考[这里](https://blog.csdn.net/qq_51447496/article/details/132089964)。 22 | 23 | 3. vscode 也是可以使用ssh远程写代码的,参考[这里](https://code.visualstudio.com/docs/remote/ssh)。 24 | 25 | **如果你使用我们提供的服务器,你可以直接来看[RISC-V 的工具链使用](riscv.md)和[运行实验框架](testing.md)。** -------------------------------------------------------------------------------- /docs/step0/pics/README.md: -------------------------------------------------------------------------------- 1 | some pics 2 | -------------------------------------------------------------------------------- /docs/step0/riscv.md: -------------------------------------------------------------------------------- 1 | # RISC-V 相关信息 2 | RISC-V 是一个很像 MIPS 的 RISC 指令集架构,编译实验要求你的编译器把 MiniDecaf 程序编译到 RISC-V 汇编。 3 | 4 | 指令集文档在[这里](https://riscv.org/technical/specifications/),我们只需要其中的 "Unprivileged Spec"。 5 | > 另外[这里](https://github.com/TheThirdOne/rars/wiki/Supported-Instructions)也有(非官方的)指令用法说明。 6 | > 不过事实上,很多时候看 gcc 输出的汇编比看什么文档都有用。 7 | 8 | ## RISC-V 工具使用 9 | 我们提供预先编译好的 RISC-V 工具,在[环境配置](./env.md)中已经叙述了安装和使用方法。 10 | 下面汇总一下。 11 | 12 | > 注意,我们虽然是用的工具前缀是 `riscv64`, 13 | > 但我们加上参数 `-march=rv32im -mabi=ilp32` 以后就能编译到 32 位汇编 [^1]。 14 | > 使用时记得加这个参数,否则默认编译到 64 位汇编。 15 | 16 | 我们假设你已经正确设置好了环境变量,否则运行 `riscv64-unknown-elf-gcc` 或 `qemu-riscv32` 或 `spike` 时请用完整路径。 17 | 18 | * gcc 编译 `input.c` 到汇编 `input.s`,最高优化等级(否则输出的汇编会很冗长) 19 | 20 | ```bash 21 | # input.c 的内容 22 | $ cat input.c 23 | int main(){return 233;} 24 | 25 | # 编译到 input.s 26 | $ riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 -O3 -S input.c 27 | 28 | # gcc 的编译结果 29 | $ cat input.s 30 | .file "input.c" 31 | .option nopic 32 | .attribute arch, "rv32i2p0_m2p0" 33 | .attribute unaligned_access, 0 34 | .attribute stack_align, 16 35 | .text 36 | .section .text.startup,"ax",@progbits 37 | .align 2 38 | .globl main 39 | .type main, @function 40 | main: 41 | li a0,233 42 | ret 43 | .size main, .-main 44 | .ident "GCC: (SiFive GCC 8.3.0-2020.04.0) 8.3.0" 45 | ``` 46 | 47 | * gcc 编译 `input.s` 到可执行文件 `a.out` 48 | 49 | ```bash 50 | # input.s 的内容,就是上面汇编输出的简化版本 51 | $ cat input.s 52 | .text 53 | .globl main 54 | main: 55 | li a0,233 56 | ret 57 | 58 | # 编译到 a.out 59 | $ riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 input.s 60 | 61 | # 输出结果,能看到是 32 位的 RISC-V 可执行文件 62 | $ file a.out 63 | a.out: ELF 32-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, not stripped 64 | ``` 65 | 66 | * 【Linux 用户】qemu 运行 `a.out`,获取返回码 67 | 68 | ```bash 69 | # 运行 a.out 70 | $ qemu-riscv32 a.out 71 | 72 | # $? 是 qemu 的返回码,也就是我们 main 所 return 的那个值 73 | $ echo $? 74 | 233 75 | ``` 76 | 77 | * 【macOS 用户】Spike 模拟器运行 `a.out`,获取返回码 78 | 79 | ```bash 80 | # 运行 a.out 81 | # /usr/local/bin/pk 替换为你自己的 pk 路径 82 | $ spike --isa=RV32G /usr/local/bin/pk a.out 83 | bbl loader 84 | 85 | # $? 是 spike 的返回码,也就是我们 main 所 return 的那个值 86 | $ echo $? 87 | 233 88 | ``` 89 | 90 | --- 91 | 92 | [^1]: 这里的 `rv32im` 表示使用 RV32I 基本指令集,并包含 M 扩展(乘除法)。本实验中我们不需要其他扩展。 93 | -------------------------------------------------------------------------------- /docs/step0/riscv_env.md: -------------------------------------------------------------------------------- 1 | # RISC-V 环境配置 2 | 3 | ## 必做:RISC-V 的 gcc 和 qemu 4 | 5 | 我们的编译器只生成 RISC-V 汇编,然后再使用 gcc 把 RISC-V 汇编变成 RISC-V 可执行文件,最后用 qemu/spike 等模拟器来运行 RISC-V 可执行文件。 6 | > 注意这里的 gcc 和常说的 gcc 不一样。 7 | > 常说的 gcc 运行在我们的 x86 机器上、把 C 编译到 x86 可执行文件; 8 | > 而这里的 gcc 虽然也运行在我们的 x86 机器上,却要编译到 RISC-V 可执行文件。 9 | > 这种“gcc 跑在 x86 却编译出 RISC-V 代码”的操作被称为交叉编译(cross compilation)。 10 | > 11 | > 因此我们不能直接使用有些系统自带的 gcc,这种 gcc 生成的可执行程序只能在你本机(x86)上运行。 12 | > 我们需要下载安装 riscv64-unknown-elf-gcc,用来生成 RISC-V 可执行程序。 13 | 14 | > 建议各位同学使用我们提供的 RISC-V 工具链,由 SiFive 预编译的较新版本的工具链对 32 位的支持存在问题。 15 | 16 | 我们提供了预编译的 riscv64-unknown-elf-gcc 和 qemu/spike 模拟器,不过只能在 Linux/Mac 下运行(qemu 对应 Linux,spike 对应 Mac),Windows 的同学可以使用 WSL,或者运行一个虚拟机。 17 | 命令行基础操作我们就不赘述了,大家可以自己在网上查找资料。 18 | 下面是环境配置指南,请阅读自己的系统的那一小节。 19 | 20 | ``` 21 | 你的编译器 gcc qemu/spike 22 | MiniDecaf 源文件 ------------> RISC-V 汇编 -----> 可执行文件 --------> 输出 23 | ``` 24 | 25 | ### Windows 用户环境配置指南 26 | 下面描述了 WSL 的一种参考方法。 27 | 你还可以开一个 Linux 虚拟机,使用 Virtualbox 或 VMWare 等,然后参考下面 Linux 配置。 28 | 29 | Win10 设置 30 | 1. 参考 https://blog.csdn.net/daybreak222/article/details/87968078 ,设置“开发者模式”以及“启用子系统功能”。 31 | 32 | 2. 打开Microsoft Store,搜索Ubuntu,选择ubuntu20.04. 33 | 34 | 3. 按照下面的 Linux 用户环境配置指南安装 riscv 工具链。 35 | 36 | ### Ubuntu 用户环境配置指南 37 | 38 | 1. 建议使用 Ubuntu 20.04 及更高的版本,你可以直接使用 apt 来安装用户态的 qemu,即 `apt install qemu-user`。 39 | 40 | 如果使用的是低版本的 WSL,通过 Windows 应用商店可以很容易地安装 Ubuntu 20.04 LTS; 41 | 如果在机器上直接安装了较低版本的 Ubuntu, 可以参考[这个教程](https://www.cyberciti.biz/faq/upgrade-ubuntu-18-04-to-20-04-lts-using-command-line/)进行升级,升级时**注意备份**。 42 | 43 | 如果出于某些原因必须使用低版本的 Ubuntu,你需要自己编译出可用的用户态 QEMU。 44 | ```bash 45 | git clone https://mirrors.tuna.tsinghua.edu.cn/git/qemu.git 46 | cd qemu && ./configure --prefix=/usr/local --target-list=riscv32-linux-user 47 | make 48 | make install 49 | qemu-riscv32 --version # 检查是否安装成功 50 | ``` 51 | 52 | 2. 从[这里](https://static.dev.sifive.com/dev-tools/freedom-tools/v2020.08/riscv64-unknown-elf-gcc-10.1.0-2020.08.2-x86_64-linux-ubuntu14.tar.gz)下载预编译好的 RISC-V 工具链并解压。 53 | 54 | 3. 安装工具链 `cp riscv64-unknown-elf-gcc-10.1.0-2020.08.2-x86_64-linux-ubuntu14/* /usr/ -r` 55 | > 在第 2. 步,你可以选择不安装到系统目录下。相应的,你需要设置环境变量: 56 | > 57 | > 首先把文件夹`riscv64-unknown-elf-gcc-10.1.0-2020.08.2-x86_64-linux-ubuntu14/`改名为 riscv-prebuilt(这一步实际不是必须的,主要为缩短文件夹名字的长度),然后修改`~/.bashrc` 文件, 把`export PATH=$PATH:/path/to/riscv-prebuilt/bin`加入到.bashrc文件的末尾。注意,此处的`/path/to` 需要替换解压文件夹所在目录。每次修改`.bashrc`文件后,都需要执行命令`source ~/.bashrc`使修改生效。 58 | > (如果你不用系统自带的 bash 而是用 zsh 之类的 shell,那加到 `~/.zshrc` 等 shell 配置文件里) 59 | 60 | ### macOS 用户环境配置指南 61 | 62 | 1. 从[这里](https://static.dev.sifive.com/dev-tools/riscv64-unknown-elf-gcc-8.3.0-2020.04.0-x86_64-apple-darwin.tar.gz)下载预编译好的 RISC-V 工具链并解压到你喜欢的目录。 63 | 2. 由于 macOS 不支持 QEMU 的用户态模式,我们使用 [Spike](https://github.com/riscv/riscv-isa-sim) 模拟器和一个简易内核 [riscv-pk](https://github.com/riscv/riscv-pk) 提供用户态程序的运行环境。你可以选择下面两种安装方法中的任意一种: 64 | 65 | 1. 从[这里](https://cloud.tsinghua.edu.cn/f/6246e90c407b4a508816/)下载我们预编译的二进制程序包 spike-pk-prebuilt-x86_64-apple-darwin.tar.gz,不过还需要通过 [Homebrew](https://brew.sh/) 安装依赖 device tree compiler: 66 | 67 | ```bash 68 | $ brew install dtc 69 | ``` 70 | 71 | 2. 通过 [Homebrew](https://brew.sh/) 安装 Spike(会自动安装 dtc): 72 | 73 | ```bash 74 | $ brew tap riscv/riscv 75 | $ brew install riscv-isa-sim 76 | ``` 77 | 78 | 然后从[这里](https://cloud.tsinghua.edu.cn/f/6246e90c407b4a508816/)上下载我们预编译的二进制程序包 spike-pk-prebuilt-x86_64-apple-darwin.tar.gz,只用里面的 `pk`。 79 | 80 | > Homebrew 也提供了 riscv-pk,不过那是 64 位的,而我们需要 32 位的,请使用我们预编译的 riscv-pk 或自行编译。 81 | > 82 | > 请注意我们提供的预编译 pk 是 x86 版本,如果你是其他平台(如M1 Mac),可以尝试自行根据 pk 的源码进行编译,附 [Github 仓库链接](https://github.com/riscv/riscv-pk)。 83 | 84 | 3. (可选)设置环境变量,方法与 Linux 一样,见上一节。如果不设置每次使用 gcc 和 spike 时都要输入完整路径。不过对于 `pk` 设置环境变量不管用,要么把它放到系统目录 `/usr/local/bin/pk`,要么每次都用完整路径。 85 | 86 | 4. 测试你 GCC 和 Spike 是否成功安装,详见[RISC-V 的工具链使用](./riscv.md)。 87 | 88 | 89 | ## 必做:测试你是否正确配置好了环境 90 | 1. 创建 `test.c` 文件,其中写入如下内容 91 | ```c 92 | #include 93 | int main() { printf("Hello world!\n"); } 94 | ``` 95 | 96 | 2. 编译 `test.c` 文件,`gcc` 应该输出一个可执行文件 `a.out`。但 `a.out` 是 RISC-V 可执行文件,所以我们的 X86 计算机无法运行。 97 | ```bash 98 | $ riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 -O3 test.c 99 | $ ls a.out 100 | a.out 101 | $ ./a.out 102 | # 如果没有安装qemu模拟器,则会出现如下错误:"bash: ./a.out: cannot execute binary file: Exec format error" 103 | ``` 104 | 后面[RISC-V 的工具链使用](./riscv.md)总结了 gcc 和 qemu 在编译实验中可能需要的用法。 105 | 106 | 3. 使用 qemu 执行 `a.out`,具体操作如下 107 | 108 | ### Linux用户 109 | ```bash 110 | $ qemu-riscv32 a.out 111 | Hello world! 112 | 注意:安装了qemu之后,直接运行 ./a.out 往往也可以调用qemu环境正确执行,并得到"Hello world!"输出。 113 | ``` 114 | 115 | ### Mac OS用户,假设你已经将spike加入环境变量,将pk加入系统目录 116 | ```bash 117 | $ spike --isa=RV32G pk a.out 118 | bbl loader 119 | Hello world! 120 | ``` 121 | 122 | # 备注 123 | [^1]: 开头的 `$ ` 表示接下来是一条命令,记得运行的时候去掉 `$ `。例如,让你运行 `$ echo x`,那你最终敲到终端里的是 `echo x`(然后回车)。如果开头没有 `$ `,那么这一行是上一条命令的输出(除非我们特别说明,这一行是你要输入的内容)。 124 | -------------------------------------------------------------------------------- /docs/step0/testing.md: -------------------------------------------------------------------------------- 1 | ## 运行实验框架 2 | 配好环境以后,我们强烈推荐你构建运行我们提供的实验框架初始代码。 3 | > 接下来我们会用到 git。 4 | > git 的安装和使用会在软件工程课上讲述,同学们也自行查阅相关资料,也可以参考[这里](https://www.liaoxuefeng.com/wiki/896043488029600) 。 5 | 6 | 1. 按照本文档的前几节([RISCV 环境配置](./riscv_env.md)和[实验框架环境配置](./env.md))配置好实验环境。 7 | 8 | 2. 助教已经为每位同学在 git.tsinghua.edu.cn 创建了一个仓库,其中 minidecaf 的[测例仓库](https://git.tsinghua.edu.cn/compiler/2024/minidecaf-tests)为其中的一个子模块,你可以通过以下指令来在克隆主仓库的同时克隆子模块 `git clone --recursive `。 9 | 由于测例仓库会有所更新,在克隆之后你需要在主仓库目录下使用 `git submodule update --remote --merge` 来手动更新。 10 | 11 | > 注意:由于子模块使用 ssh 链接,你需要将你的 ssh 公钥添加到你的 git.tsinghua 账号上,才能将其克隆下来。 12 | 13 | 3. 按照[测例](https://git.tsinghua.edu.cn/compiler/2024/minidecaf-tests)的 README 运行测试 step1,实验框架给出的初始代码可以通过 step1 的所有测例。 14 | 15 | 测试运行的 **输出结果** 大致如下。 16 | 17 | ```bash 18 | $ STEP_UNTIL=1 ./check.sh 19 | gcc found 20 | qemu found 21 | parallel found 22 | OK testcases/step1/multi_digit.c 23 | OK testcases/step1/newlines.c 24 | ...... 其他测试点,太长省略 25 | ``` 26 | 27 | -------------------------------------------------------------------------------- /docs/step0/todo.md: -------------------------------------------------------------------------------- 1 | # 还需要完善的地方 2 | 3 | - [ ] 更新 2021 年实验提交安排,为同学们创建清华 git 仓库(README.md,step0/testing.md)@杨耀良 4 | - [ ] 将实验框架放到 github 上(step0/testing.md)@陈之杨@曾军 5 | - [ ] 加入代码框架后端相关代码位置(step1/arch.md)@杨耀良 6 | - [ ] step1 词法分析语法分析需完善,需要排考虑如何排版和呈现(step1/example.md)@刘润达@周智 7 | - [ ] 快速上手 flex 和 bison 的教程(step1/...)@刘润达 8 | 9 | -------------------------------------------------------------------------------- /docs/step1/arch.md: -------------------------------------------------------------------------------- 1 | # MiniDecaf 编译器结构 2 | MiniDecaf 编译器大致划分为三个部分:前端、中端、后端。通过编译器前端,可以读入 MiniDecaf 源程序,然后通过**词法分析**和**语法分析**将源程序转化为一个**抽象语法树**(Abstract Syntax Tree, AST),接下来通过扫描 AST 进行语义分析,检查是否存在语义错误;在编译器中端,通过扫描 AST 生成中间代码 —— 三地址码;在编译器后端中,将三地址码转换为 **RISC-V 汇编代码**。下面依次介绍上述编译步骤,以及对应框架代码的位置。 3 | 4 | > 我们在这里针对每个步骤只是简要介绍,目的是给同学们一个大致的印象:编译器到底是由哪些部分组成的,这些部分又有什么作用。具体的技术点,我们将在用到的 step 作详细介绍。 5 | 6 | ## 词法分析和语法分析 7 | 8 | > 此部分对应框架源码位置: 9 | > 10 | > 词法分析程序位于 `frontend/lexer/`;语法分析程序位于 `frontend/parser/`;语法树位于 `frontend/ast/`。 11 | 12 | 编译器前端分为两个子任务,一是**词法分析**,二是**语法分析**。词法分析的功能是从左到右扫描 MiniDecaf 源程序,识别出程序源代码中的标识符、保留字、整数常量、算符、分界符等单词符号(即终结符),并把识别结果返回给语法分析器,以供语法分析器使用。语法分析的功能是在词法分析的基础上针对所输入的终结符串建立语法树,并对不符合语法规则的 MiniDecaf 程序进行报错处理。一般而言,这一步所生成的语法树并非表示了所有语法细节的语法分析树,而是只表示其树形结构的抽象语法树([Abstract Syntax Tree, AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree))。比如,对于下面这一段简单的MiniDecaf 代码: 13 | 14 | ```C 15 | if (i) i = 1; 16 | ``` 17 | 18 | 它对应的完整语法分析树可能长这样: 19 | 20 | ``` 21 | if_stmt 22 | |- "if" 23 | |- "(" 24 | |- Identifier("i") 25 | |- ")" 26 | |- assign_stmt 27 | |- Identifier("i") 28 | |- "=" 29 | |- Int(1) 30 | |- ";" 31 | ``` 32 | 33 | 其中双引号下的和大写字母开头的都为词法分析器产出的终结符。而对应的抽象语法树可能长这样: 34 | 35 | ``` 36 | if_stmt 37 | |- (condition) Identifier("i") 38 | |- (body) assign_stmt 39 | |- (lh) Identifier("i") 40 | |- (rh) Int(1) 41 | ``` 42 | 43 | AST省略掉了完整的语法分析树中不必要的细节(例如条件表达式旁边的括号),有利于简化树的结构与后续对树的处理。 44 | 45 | 词法分析和语法分析的最终结果是一棵跟所输入的 MiniDecaf 源程序相对应的语法树。本阶段的实验重点是掌握 LEX 和 YACC 的用法,了解编译器自动构造工具的特点,并且结合实验内容理解正规表达式、自动机、LALR(1) 分析等理论知识在实践中的应用。 46 | 47 | ## 语义分析 48 | 49 | > 此部分对应框架源码位置: 50 | > 51 | > 符号表构建位于 `frontend/typecheck/namer.py`;类型检查位于 `frontend/typecheck/typer.py`;符号表相关的数据结构位于`frontend/symbol/`;作用域相关数据结构位于 `frontend/scope/`。 52 | 53 | 语法分析树的建立可以说明所输入的 MiniDecaf 源程序在语法规范上是合法的,但是要进行有效的翻译,编译器还需要理解每个程序语句的含义。了解程序含义的过程称为**语义分析**。 54 | 55 | 可以把语义分析过程分为两个部分:分析符号含义和检查语义正确性。分析符号含义是指对于表达式中所出现的符号,找出该符号所代表的内容,这个工作主要通过检索符号表来实现。检查语义正确性指的是检查每条语句是否合法,比如检查每个表达式的操作数是否符合要求,每个表达式是否为语言规范中所规定的合法的表达式,使用的变量是否都经过声明等。程序代码通过了词法和语法分析,其语义未必正确,因此未必是合法的语句。不合法的语句的具体含义在语言规范中没有规定,从而使得编译器无法确定这些语句的确切含义,所以检查语义的正确性是很有必要的。如果一个程序成功通过语义分析,则说明这个程序的含义对于编译器来说是明确的,翻译工作可以继续进行。 56 | 57 | 具体来说,在这一阶段中,我们需要对 AST 进行两遍扫描,分别完成以下的检查: 58 | 59 | * **符号表构建**:声明了哪些标识符,待编译程序使用的标识符对应于哪个位置的声明。 60 | * **类型检查**:各语句和表达式是否类型正确。 61 | 62 | 如果在语义分析阶段发现错误,那么整个编译过程在这一阶段结束后将终止,并报告编译错误。所有的语义错误都应该在这一阶段,且只应该在这一阶段报告。下面分别介绍符号表构建和类型检查的内容。 63 | 64 | ### 符号表构建 65 | 66 | 针对 MiniDecaf 程序中所有定义的标识符,包括函数名和变量名,我们统一用一种具有层次结构的符号表来维护。使用符号表的好处包括:(1) 在分析各语句和表达式时,若它们引用了某些标识符,我们可以在符号表中查询这些标识符是否有定义以及相关信息(如类型);(2) 符号表的层次结构与作用域是一一对应的,便于检查出符号定义是否有冲突,以及确定不同作用域引用的标识符。 67 | 68 | > step1-4 中只需要考虑常量的计算,直到 step5 才需要考虑符号表构建。 69 | 70 | ### 类型检查 71 | 72 | 完成符号表构建后,我们就可以自顶向下地遍历 AST,对每个语句和表达式逐一进行类型检查,并在 AST 上进行类型标注。对于静态类型(statically-typed)语言,在语言设计之初,设计者都会考虑该语言支持表达哪些类型,并给出定型规则(typing rules)。 在已知定型规则的情况下编码实现类型检查算法并不困难——往往只要逐条将其翻译为代码即可。 73 | 74 | > 事实上,由于 MiniDecaf 代码的基本类型只有整数类型(int),因此我们在类型检查时只需要考虑 int 和 int 数组两种类型。在支持数组(step11)之前,都基本不需要考虑类型检查。 75 | 76 | ## 中间代码生成 77 | 78 | > 三地址码定义位于 `utils/tac/`;中间代码生成位于 `frontend/tacgen/tacgen.py`。 79 | 80 | 在对 AST 进行语义分析后,我们将在这一阶段把带有类型标注的 AST 翻译成适合后端处理的一种**中间表示**。**中间表示**(也称中间代码,intermediate representation / IR)是介于语法树和汇编代码之间的一种程序表示。 它不像语法树一样保留了那么多源程序的结构,也不至于像汇编一样底层。 81 | 82 | 由于源语言(MiniDecaf)和目标语言(RISC-V 汇编)一般存在较大的差别,因此直接把源语言翻译为目标语言中的合法程序通常是比较困难的。大多数编译器实现中所采取的做法,是首先把源语言的程序翻译成一种相对接近目标语言的中间表示形式,然后再从这种中间表示翻译成目标代码。中间表示(IR)的所带来的优势如下: 83 | 84 | 1. 通过把 AST 到汇编的步骤一分为二,缩小调试范围。如果目标代码有误,通过检查 IR 是否正确就可以知道:是 AST 到 IR 翻译有误,还是 IR 到汇编翻译有误。 将 AST 转换到汇编的过程分成两个步骤,每个步骤代码更精简,更易于调试。 85 | 2. 通过 IR 可以适配不同指令集(RISC-V, x86, MIPS, ARM...)和源语言(MiniDecaf, C, Java...)。由于不同源语言的 AST 不同,直接从 AST 生成汇编的话,为了支持 N 个源语言和 M 个目标指令集,需要写 N * M 个目标代码生成模块。如果有了 IR,只需要写 N 个 IR 生成器和 M 个汇编生成器,只有 N + M 个模块。 86 | 87 | 接下来,将对我们所使用的中间代码 —— **三地址码 (TAC)** 做简要介绍,后续的实验步骤中需要同学们添加恰当的三地址码指令来完成特定的功能。为了降低实验难度,给出部分参考实现,各位同学可以依据参考实现完成设计,也可以自行设计三地址码。需要指出的是,我们使用的 TAC 并非严格按照课本上的定义,并且也没有严格要求只能使用三个寄存器地址,不过绝大多数指令均可以仅使用三个寄存器地址实现(函数调用除外)。 88 | 89 | ### 三地址码 90 | 91 | **三地址码**(Three Address Code, TAC)看起来很像汇编,与汇编最大的区别在于 —— 汇编里面使用的是目标平台(如 risc-v, x86, mips)规定的物理寄存器,其数目有限;而 TAC 使用的是“虚拟寄存器”,我们称为**临时变量**,其数目不受限制,可以任意使用(这意味着直接将临时变量转化为寄存器可能会出现寄存器不够用的情况)。**在后端生成汇编代码时,我们再考虑如何为临时变量分配物理寄存器的问题。** 92 | 93 | ```asm 94 | main: # main 函数入口标签 95 | _T0 = 1 # 加载立即数 96 | _T1 = _T0 # 临时变量赋值操作 97 | _T2 = ADD _T0, _T1 # 加法操作 _T2 = _T0 + _T1 98 | _T3 = NEG _T0 # 取负操作 _T3 = -_T0 99 | return _T2 # 函数返回 100 | ``` 101 | 102 | > 以上给出了一份 TAC 示例程序。请注意 TAC 代码只是一种中间表示,并不需要像汇编语言那样有严格的语法。因此,同学们可以自由选择输出 TAC 代码的格式,只要方便自己调试即可。例如,你也可以将 _T2 = ADD _T0, _T1 输出成 _T2 = _T0 + _T1。 103 | 104 | TAC 程序由**标签**和**指令**构成: 105 | 106 | 标签用来标记一段指令序列的起始位置。从底层实现的角度来看,每个标签本质上就是一个地址,且往往是某一段连续内存的起始地址。在我们的实验框架中,标签有两个作用:作为**函数入口地址**(如上例中的 main 函数入口),以及作为**分支语句的跳转目标**(TAC 指令不支持 MiniDecaf 语言中条件和循环控制流语句,而是将它们都翻译成更加底层的跳转语句)。 107 | 108 | TAC 指令与汇编指令类似,每条 TAC 指令由操作码和操作数(最多3个,函数调用除外,由于函数参数可能有多个,使用严格的三个操作数反而会使得函数一节中实现更为复杂)构成。 操作数可能会有:临时变量、常量、标签(可理解为常量地址)和全局变量(全局变量的处理比较特殊,由于 step10 才需要考虑,届时再介绍其处理方法)。如上例所示,TAC 中的临时变量均用 "_Tk" 的形式表示(k表示变量的编号)。 109 | 110 | TAC 程序是**无类型**的,或者说它仅支持一种类型:32位(4字节)整数。为了简化实验内容,MiniDecaf 只支持 int 类型和 int 数组类型,其值和地址都可以用一个32位整数存储,故 MiniDecaf 程序中的变/常量和 TAC 中的变/常量可以直接对应。 111 | 112 | 数组类型无法用临时变量直接表示,可以用**一段连续内存的起始地址**表示。其实现细节将在 step11 详细讨论。 113 | 114 | ## 控制流、数据流分析和寄存器分配 115 | 116 | > 数据流图定义及优化在 `backend/dataflow/` 中;寄存器分配在 `backend/reg/` 中 117 | 118 | ### 控制流和数据流分析 119 | 120 | 一般来说,三地址码是可以直接翻译为目标代码的,但是这样的直接翻译会导致所产生的代码的效率比较差,所以多数编译器都会进行一定的优化工作。为了进行更深入的优化,编译器需要了解程序语义的更多内容,例如一个变量的某个赋值在当前指令中是否有效、一个变量在当前指令以后是否还会被使用、当前运算指令的两个操作数是否都能够在编译的时候计算出来、循环体中某些代码是否能够提出到循环外面、循环次数是不是编译的时候已知的常数等等,这些语义分析和代码优化离不开控制流分析和数据流分析。 121 | 122 | 所谓**控制流分析**,是指分析程序的执行路径满足什么性质,包括基本块划分、流图构造、以及分析循环或其他控制区域(region)。而所谓**数据流分析**,是指分析各种数据对象在程序的执行路径中的状态关系,例如一个变量在某个语句以后是否还被用到等。依据数据流分析的结果,可以进行后续的中间代码优化以及寄存器分配等相关步骤。 关于数据流分析的细节,我们将在 step7 做详细介绍。 123 | 124 | ### 寄存器分配 125 | 126 | 所谓**寄存器分配**,是指为中间代码中的虚拟寄存器分配实际的物理寄存器。对中间代码来说,通常假设虚拟寄存器的数量是无限的,这导致我们在分配物理寄存器时无法简单的对虚拟寄存器做一一映射,需要有一个调度与分配算法来合理使用有限的物理寄存器。本实验框架中使用了一种暴力寄存器分配算法,具体细节将在 step5 中详细说明,当然如果你感兴趣,你也可以基于我们的框架实现更高级的干涉图分配算法,具体不作要求。 127 | 128 | ## 目标平台汇编代码生成 129 | 130 | > 目标平台汇编代码生成在 `backend/asm.py | subroutineinfo.py` 以及 `backend/riscv/` 中。 131 | 132 | 通常我们认为的目标代码生成步骤包含寄存器分配、指令选择。**寄存器分配**是指为中间代码中的虚拟寄存器分配实际的物理寄存器,涉及物理寄存器的调度分配。**指令选择**是指选用合适的汇编指令来翻译中间代码指令,如中间代码生成章节提供的例子中,使用 addi 汇编指令来翻译 ADD 中间代码指令。需要特别提出的是,RISC-V 指令集的设计思路是尽可能简洁,因此有些指令并没有直接提供,需要用多条简单指令代替,如相等、大于等于、逻辑与、逻辑或等等。同学们实现时需要特别注意。 133 | 134 | 课程实验的目标平台为 RISC-V,RISC-V 是一个与 MIPS 类似的 RISC 指令集架构,编译实验要求所实现的编译器把 MiniDecaf 程序编译到 RISC-V 汇编代码。指令集文档在[这里](https://riscv.org/technical/specifications/),我们只需要其中的 "Unprivileged Spec",另外[这里](https://github.com/TheThirdOne/rars/wiki/Supported-Instructions)也有(非官方的)指令用法说明。下图给出了 RISC-V 的32个整数寄存器的相关说明,其中需要特别注意的寄存器有 ra(存放函数返回地址)、sp(存放当前栈顶地址)、fp(存放当前栈底地址)、a0&a1(存放函数返回值)。为了简单起见,我们简化了 RISC-V 的调用约定,由调用者(caller)负责保存寄存器内容,因此,无需关心某个寄存器是 caller-saved 还是 callee-saved。 135 | 136 | ![](./pics/riscv_reg.png) 137 | -------------------------------------------------------------------------------- /docs/step1/clean: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm *.{tokens,interp,class} 3 | rm *{Lexer,Parser,Listener,Visitor}* 4 | rm input 5 | rm __pycache__ -rf 6 | -------------------------------------------------------------------------------- /docs/step1/example.md: -------------------------------------------------------------------------------- 1 | # 通过例子学习,一个仅有 return 的主函数编译全流程: 2 | 3 | 本步骤主要涉及的语法为主函数和 return 语句,完成本步骤之后,你的编译器将支持将一个仅有 return 的主函数编译为 32 位 RISC-V 汇编代码,并通过 RISC-V 工具链生成可以在硬件模拟器上正确运行的程序。因为这是大家首次接触 MiniDecaf 编译实验框架,我们给大家的代码框架中已经包含所有 step1 的实现,大家可以直接运行通过 step1 的测试用例。并且,我们在每个步骤的文档中会详细梳理介绍在当前步骤中需要用到的知识点以及对应的代码片段和注释,如果我们认为当前步骤并不需要了解某部分知识点(如数据流分析、寄存器分配),我们会在后续的步骤中进行知识点的讲解。 4 | 5 | 下面我们将通过一个简单的 step1 测试用例,一起走过它的编译全流程: 6 | 7 | ```C 8 | int main() { 9 | return 2024; 10 | } 11 | ``` 12 | 13 | ## 词法分析 & 语法分析 14 | 15 | 在词法分析 & 语法分析这一步中,我们需要将输入的程序字符流按照[语法规范](./spec.md)转化为后续步骤所需要的 AST,我们使用了 lex/yacc 库来实现这一点。[yacc](https://en.wikipedia.org/wiki/Yacc) 是一个根据 EBNF 形式的语法规范生成相应 LALR parser 的工具,支持基于属性文法的语法制导的语义计算过程。**你可以根据我们的框架中对 lex/yacc 的使用,结合我们的文档,来快速上手 lex/yacc,完成作业;也可以选择阅读一些较为详细的文档,来系统地进行 lex/yacc 的入门,但这不是必须的。** 16 | 17 | 在实验框架中,我们使用的是 lex/yacc 的一个纯 python 实现,称为 python-lex-yacc(简称 ply),其使用方法与 lex/yacc 有一些差异。 18 | 19 | [Python-lex-yacc 快速入门](https://www.dabeaz.com/ply/ply.html) 20 | 21 | 程序的入口点在 `main.py`,它通过调用 `frontend.parser.parser`(位于 `frontend/parser/ply_parser.py`)来完成语法分析的工作,而这一语法分析器会自动调用位于 `frontend/lexer/ply_lexer.py` 的词法分析器进行词法分析。语法的定义和语法分析器都位于 `frontend/parser/ply_parser.py`,而词法的定义位于 `frontend/lexer/lex.py`。AST 节点的定义位于 `frontend/ast/tree.py` 中。以下表示中的符号都出自于这几个文件。 22 | 23 | 这部分的工作流程如下: 24 | 25 | ``` 26 | 读内容 词法分析 & 语法分析 语义分析 27 | readCode parser.parse Namer.transform & Typer.transform 28 | MiniDecaf 源文件 --------> 字节流 -----------> AST -------------------------------> ... 29 | ``` 30 | 31 | 当程序读入程序的字符流之后,它首先会被 lexer 处理,并被转化为如下形式的一个 Token 流: 32 | 33 | `Int Identifier("main") LParen RParen LBrace Return Integer(2024) Semi RBrace` 34 | 35 | 在`frontend/lexer/lex.py`文件中你可以看到每个 Token 是如何定义的,每个`token`都会以`t_`开头。如`t_Semi = ";"`代表分号被解析以后会转化为 `Semi` 这个Token。而对于一些复杂的 Token,我们需要在`lexer`中定义一个正则表达式来匹配它,lex中通过定义一个函数来实现正则匹配。以匹配整数为例,函数的第一行`r"[0-9]+" `代表匹配用到的正则表达式,而函数的参数`t`则是被匹配得到的字符串,我们通过python中的类型转换将其变为一个整数,你可以在文件中看到以下代码: 36 | 37 | ```python 38 | def t_Integer(t): 39 | r"[0-9]+" # can be accessed from `t_Interger.__doc__` 40 | t.value = int(t.value) 41 | return t 42 | ``` 43 | 44 | 之后,这些 token 会被 yacc 生成的 LALR(1) parser 转化为如下形式的 AST: 45 | 46 | ``` 47 | Program 48 | |- (children[0]) Function 49 | |- (ret_t) TInt 50 | |- (ident) Identifier("main") 51 | |- (body) Block 52 | |- (children[0]) Return 53 | |- (expr) IntLiteral(2024) 54 | ``` 55 | 56 | 得到的 AST 也就是 `main.py` 中 `step_parse` 这一函数里 `parser.parse(...)` 的输出。 57 | 58 | 在`frontend/parser/ply_parser.py`文件中,你可以看到我们是如何定义语法规则的,文件的最末尾有`parser = yacc.yacc(start="program")`代表了parser的入口点是`program`,而`program`的定义在`p_program`函数中,你可以看到这个函数的docstring中定义了`program`的语法规则。**注意docstring(即三个引号之间的内容)在这里并非注释,而是用于定义语法规则。** 59 | 60 | ``` 61 | def p_program(p): 62 | """ 63 | program : function 64 | """ 65 | p[0] = Program(p[1]) 66 | 67 | def p_function_def(p): 68 | """ 69 | function : type Identifier LParen RParen LBrace block RBrace 70 | """ 71 | p[0] = Function(p[1], p[2], p[6]) 72 | ``` 73 | 74 | 我们先看`p_program`函数,我们定义的语法规则是`program`由一个`function`组成,对应的上下文无关表达式就是`program -> function`,同时代码中的`p[0] = Program(p[1])`代表了构建AST的计算过程,这里的`p[0]`代表的是当前语法规则的左部,`p[1]`代表的是当前语法规则的右部第一个符号(即`function`),`p[2]`代表的是当前语法规则的右部第二个符号(这里没有),以此类推。这样递归下去,就能解析完整个程序。`p[0] = Program(p[1])`最后就会变为`p[0] = Program(Function(...))`,这里`Program`、`Function`类的定义在`frontend/ast/tree.py`文件中,你可以看到`Function`这个类的构造函数接受了三个参数,分别是返回值类型、函数名和函数体。 75 | 76 | 尝试运行 `python main.py --input example.c --parse` 你应该就能看到类似的输出。(记得自己写一个`example.c`) 77 | 78 | ## 语义分析 79 | 80 | 在 step1 语义分析步骤中,我们要遍历 AST,检验是否存在如下的语义错误: 81 | 82 | * main 函数是否存在。(`frontend/typecheck/namer.py:37`) 83 | 84 | 在实际操作中,我们遍历 AST 所用的方法就是的 [Visitor 模式](./visitor.md),通过 Visitor 模式,我们可以从抽象语法树的根结点开始,遍历整颗树的所有语法结点,并针对特定的语法结点作出相应的操作,如名称检查和类型检查等。在编译器中,这种基于 Visitor 的对语法树进行一次遍历,完成某种检查或优化的过程,称为遍(pass)。不难想到,一个现代编译器是由很多遍扫描组成的,如 gcc 根据优化等级不同会有数百个不等的 pass。下面,我们将指出,step1 中我们是如何实现符号表构建 pass 和类型检查 pass 的,同学们可以选择去看相应的代码注释与实现细节。 85 | 86 | `frontend/typecheck/namer.py` 和 `typer.py` 分别对应了符号表构建和类型检查这两次遍历。在框架中,`Namer` 和 `Typer` 都是继承 `frontend/ast/visitor.py` 中的 `Visitor` 类来通过 Visitor 模式遍历 AST 。 87 | 88 | ## 中间代码生成 89 | 90 | 在通过语义检查之后,编译器已经掌握了翻译源程序所需的信息(符号表、类型等),下一步要做的则是将抽象语法树翻译为便于移植和优化的中间代码,在本实验框架中就是三地址码。如何翻译抽象语法树?当然还是无所不能的 Visitor 模式,我们在中间代码生成步骤中再遍历一次语法树,对每个结点做对应的翻译处理。具体来说,在 step1 当中,我们只需要提取 return 语句返回的常量,为之分配一个临时变量,再生成相应的 TAC 返回指令即可。不难看出,本例对应的三地址码为: 91 | 92 | ```asm 93 | main: # main 函数入口标签 94 | _T0 = 2024 # 为立即数 2024 分配一个临时变量 95 | return _T0 # 返回 96 | ``` 97 | 98 | > 下面,我们同样也指出了在代码中我们是怎样实现这个中间代码生成 pass 的,大家可以参考注释和代码了解实现细节。 99 | 100 | `utils/tac` 目录下实现了生成 TAC 所需的底层类。其中 `tacinstr.py` 下实现了各种 TAC 指令,同学们可以在必要时修改或增加 TAC 指令。 101 | 102 | `frontend/tacgen/tacgen.py` 中通过一遍 AST 扫描完成 TAC 生成。和语义分析一样,这部分也使用了 Visitor 模式。这个文件里除了类型`TACGen`之外还有一个辅助类`TACFuncEmitter`,它用于处理产生TAC代码过程中一些相对底层的细节。在本框架中,TAC 程序的生成以函数为单位,对每个函数(step1-8 中只有 main 函数)分别使用一个 `TACFuncEmitter` 来生成对应的 TAC 函数代码。如果你增加了 TAC 指令,则可能需要在 `TACFuncEmitter` 类中增加生成相应指令的代码。 103 | 104 | ## 目标代码生成 105 | 106 | 目标代码生成步骤是对中间代码的再一次翻译,在本例中,你需要了解并掌握的知识点有: 107 | 108 | 1. 如何将一个立即数装载到指定寄存器中? 109 | 110 | RISC-V 提供了 li 指令来支持加载一个 32 位立即数到指定寄存器中,其中 表示寄存器名, 表示立即数值,如:`li t0, 2024`,就是将立即数 2024 加载到寄存器 t0 中。 111 | 112 | 2. 如何设置返回值? 113 | 114 | 在 RISC-V 中,a0 和 a1 是 gcc 调用约定上的存储返回值的寄存器,返回值会按照其大小和顺序存储在 a0 和 a1 中。也就是说,如果你有一个 32 位的返回值,你可以放在 a0 中返回,如果你有两个 32 位的返回值,你就需要把它们分别放在 a0 和 a1 中返回。更多的返回值会全部放入内存返回,如约定好的栈的某个位置,这取决于函数调用约定。 115 | 116 | 在我们的实验要求中,返回值均是单个 32 位的值。因此在当前步骤中你只需要了解,将需要返回的值放入 a0 寄存器中,然后在后面加上一条 ret 指令即可完成函数返回的工作。 117 | 118 | 综上所述,我们上述中间代码翻译成如下 RISC-V 汇编代码: 119 | 120 | ```asm 121 | .text # 代码段 122 | .global main # 声明全局符号 main 123 | main: # 主函数入口符号 124 | li t0, 2024 # 加载立即数 2024 到 t0 寄存器中 125 | mv a0, t0 # 将返回值放到 a0 寄存器中 126 | ret # 返回 127 | ``` 128 | 129 | 实验框架中关于目标代码生成的文件主要集中 `backend` 文件夹下,step1 中你只需要关注 `backend/riscv` 文件夹中的 `riscvasmemitter.py` 以及 `utils/riscv.py` 即可。具体来说 `backend/asm.py` 中会先调用 `riscvasmemitter.py` 中的 `selectInstr` 方法对每个函数内的 TAC 指令选择相应的 RISC-V 指令,然后会进行数据流分析、寄存器分配等流程,在寄存器分配结束后生成真正的汇编指令(即所有操作数都已经分配好寄存器的指令),最后通过 `RiscvSubroutineEmitter` 的 `emitFunc` 方法生成每个函数的 RISC-V 汇编。 130 | 131 | ## 思考题 132 | 133 | 1. 在我们的框架中,从 AST 向 TAC 的转换经过了 `namer.transform`, `typer.transform` 两个步骤,如果没有这两个步骤,以下代码能正常编译吗,为什么? 134 | 135 | ```c 136 | int main(){ 137 | return 10; 138 | } 139 | ``` 140 | 141 | 2. 我们的框架现在对于 `return` 语句没有返回值的情况是在哪一步处理的?报的是什么错? 142 | 143 | 3. 为什么框架定义了 `frontend/ast/tree.py:Unary`、`utils/tac/tacop.py:TacUnaryOp`、`utils/riscv.py:RvUnaryOp` 三种不同的一元运算符类型? 144 | -------------------------------------------------------------------------------- /docs/step1/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step1:一个仅有 return 的 main 函数 2 | 3 | 实验框架已经完成并通过了本节的测例,因此你不需要在这个阶段修改代码,但需要在 stage 1 的报告中(注意不是 stage 0)完成[通过例子学习](./example.md)一节末尾的思考题。 4 | 5 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/.pdf`,比如 stage 1 的实验报告需要放在 `stage-1` 这个 branch 下的 `./reports/stage-1.pdf`。整个 stage 1 只需要提交一份报告,你不需要单独为 step 1 准备报告。 -------------------------------------------------------------------------------- /docs/step1/pics/antlr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step1/pics/antlr.png -------------------------------------------------------------------------------- /docs/step1/pics/antlr2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step1/pics/antlr2.png -------------------------------------------------------------------------------- /docs/step1/pics/example.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | node [shape=plain] 3 | rankdir=LR 4 | 5 | c [label=< 6 | 7 | 8 |
50
1226
>] 9 | 10 | b [label=< 11 | 12 | 13 | 14 |
1226
50
1176
>] 15 | 16 | a [label=< 17 | 18 | 19 | 20 | 21 |
50
1176
50
1176
>] 22 | 23 | subgraph cluster_0 { 24 | label = "before ADD" 25 | a 26 | 运算栈1 -> a:0 27 | 运算栈1 -> a:1 28 | i1 -> a:2 29 | sum1 -> a:3 30 | 运算栈1[label = "运算栈"] 31 | i1[label = "i"] 32 | sum1[label = "sum"] 33 | } 34 | 35 | subgraph cluster_1 { 36 | label = "after ADD" 37 | b 38 | 运算栈2 -> b:0 39 | i2 -> b:2 40 | sum2-> b:3 41 | 运算栈2[label = "运算栈"] 42 | i2[label = "i"] 43 | sum2[label = "sum"] 44 | } 45 | 46 | subgraph cluster_2 { 47 | label = "after STORE" 48 | c 49 | 运算栈3 50 | i3 -> c:2 51 | sum3-> c:3 52 | 运算栈3[label = "运算栈(空)"] 53 | i3[label = "i"] 54 | sum3[label = "sum"] 55 | } 56 | } -------------------------------------------------------------------------------- /docs/step1/pics/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step1/pics/example.png -------------------------------------------------------------------------------- /docs/step1/pics/grun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step1/pics/grun.png -------------------------------------------------------------------------------- /docs/step1/pics/main.dot: -------------------------------------------------------------------------------- 1 | graph { 2 | prog -- func 3 | 4 | func -- {ty, Ident, Lparen, Rparen, Lbrace, stmt, Rbrace} 5 | ty[label="Keyword(int)"] 6 | Ident[label="Ident(\"main\")"] 7 | stmt -- {Return, expr, Semicolon} 8 | Return[label="keyword(\"return\")"] 9 | expr -- Integer 10 | Integer[label="Integer(0)"] 11 | } 12 | -------------------------------------------------------------------------------- /docs/step1/pics/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step1/pics/main.png -------------------------------------------------------------------------------- /docs/step1/pics/parsetree.dot: -------------------------------------------------------------------------------- 1 | graph { 2 | e1 -- b1 3 | b1 -- {b1e1, "*", b1e2} 4 | b1e1 -- b2 5 | b1e2 -- i3 6 | b2 -- {b2e1, "-", b2e2} 7 | b2e1 -- i1 8 | b2e2 -- i2 9 | i1 -- ii1 10 | i2 -- ii2 11 | i3 -- ii3 12 | 13 | e1[label=expr] 14 | b1e1[label=expr] 15 | b1e2[label=expr] 16 | b2e1[label=expr] 17 | b2e2[label=expr] 18 | b1[label=binary] 19 | b2[label=binary] 20 | i1[label=int] 21 | i2[label=int] 22 | i3[label=int] 23 | ii1[label=20] 24 | ii2[label=13] 25 | ii3[label=3] 26 | } 27 | -------------------------------------------------------------------------------- /docs/step1/pics/riscv_reg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step1/pics/riscv_reg.png -------------------------------------------------------------------------------- /docs/step1/provided.md: -------------------------------------------------------------------------------- 1 | # 已经提供的语法特性 2 | 3 | 为了方便同学们完成实验,我们提供的实验框架中已经完成了部分语言特性的实现。对于某些步骤,同学们可以参照已实现的特性完成剩余的部分。例如,根据提供的 while 循环实现,同学们可以参考着完成 for 循环的实现。 4 | 5 | 下面我们列出了所有框架中已经完成的特性: 6 | 7 | 1. step1 中我们提供了所有实现。 8 | 2. step2 中我们提供了取负运算的实现。 9 | 3. step3 中我们提供了加法和逻辑或操作运算的实现。 10 | 4. step5 中我们提供了基础数据结构——符号表的实现。 11 | 6. step6 中我们提供了基础数据结构——单层作用域的实现。 12 | 5. step7 中我们提供了 if 语句的实现。 13 | 7. step8 中我们提供了 while 循环的实现。 14 | 8. 此外,我们提供了 step1-6 需要的语法树节点和中间代码指令,以及后端中的寄存器分配算法。 15 | -------------------------------------------------------------------------------- /docs/step1/spec.md: -------------------------------------------------------------------------------- 1 | # 规范 2 | 每个步骤结尾的 **规范** 一节都会对这个步骤中的新特性给出规范,方便大家查阅。 3 | 4 | # step1 语法规范 5 | 6 | 我们采用 **EBNF (extended Barkus-Naur form)** 记号书写语法规范,采用类似 ANTLR 的记号: 7 | * 小写字母打头的是非终结符(如 `program`),大写字母打头的是终结符(如 `Identifier`),可以用字符串字面量表示终结符(如 `'int'`) 8 | * 后面会用到:`(` 和 `)` 表示分组,`|` 表示选择,`*` 零或多次,`+` 一或多次,`?` 零或一次。 9 | - 很容易通过增加新的非终结符,去掉这些符号。例如 `x+` 就可以被替换成新的非终结符 `y`,并且 `y : x | x y`。 10 | 11 | > EBNF 也有很多写法,另一种是用尖括号表示非终结符 ` ::= ` 等。 12 | 13 |
14 | 
15 | program
16 |     : function
17 | 
18 | function
19 |     : type Identifier '(' ')' '{' statement '}'
20 | 
21 | type
22 |     : 'int'
23 | 
24 | statement
25 |     : 'return' expression ';'
26 | 
27 | expression
28 |     : Integer
29 | 
30 | 31 | # step1 语义规范 32 | **1.1** MiniDecaf 的 int 类型具体指 32 位有符号整数类型,范围 [-2^31, 2^31-1],补码表示。 33 | 34 | **1.2** 编译器应当只接受 [0, 2^31-1] 范围内的整数常量, 不支持负整数常量,如果整数不在此范围内,编译器应当报错。引入负号`-`后,可以用负号配合正整数常量来间接表示负整数常量。 35 | 36 | **1.3** 如果输入程序没有 `main` 函数,编译器应当报错。 37 | -------------------------------------------------------------------------------- /docs/step1/visitor.md: -------------------------------------------------------------------------------- 1 | # Visitor 模式速成 2 | 编译器的构造中会使用到很多设计模式,Visitor 模式就是常见的一种。 基础的设计模式都在 OOP 课程中覆盖,这里重提一下 Visitor 模式,并以框架中的代码为示例进行介绍。 3 | 4 | 我们知道,编译器里有很多的树状结构。最典型的就是,源程序通过上下文无关文法解析后,得到的抽象语法树。在语义分析和中间表示生成两个步骤中,我们都需要遍历整个抽象语法树。Visitor 模式的目的,就是对遍历树状结构的过程进行封装,本质就是一个 DFS 遍历。 5 | 6 | 让我们考虑 step1 的文法: 7 | 8 | ``` 9 | program : function 10 | function : type Identifier '(' ')' '{' statement '}' 11 | type : 'int' 12 | statement : 'return' expression ';' 13 | expression : Integer 14 | ``` 15 | 16 | 以这个文法对应的一段 MiniDecaf 代码为示例: 17 | 18 | ```C 19 | int main() { 20 | return 2; 21 | } 22 | ``` 23 | 24 | 它会对应如下的 AST 结构: 25 | 26 | ``` 27 | program 28 | function 29 | type(int) 30 | identifier(main) 31 | param_list 32 | return 33 | int(2) 34 | ``` 35 | 36 | > 我们用缩进表示树结构,其中 program, function, type, identifier, param_list, block, return, int 等均为 AST 上的结点类型。 37 | 38 | 39 | 在框架中,我们有以下的 AST 结点类实现(进行了适当的简略): 40 | 41 | ```python 42 | ''' 43 | frontend/ast/node.py 44 | ''' 45 | class Node: # 所有 AST 结点的基类 46 | # ... 47 | ''' 48 | frontend/ast/tree.py 49 | ''' 50 | class Program(ListNode[Union["Function", "Declaration"]]): # 程序,AST 的根结点类型 51 | # ... 52 | class Function(Node): # 函数 53 | # ... 54 | class Statement(Node): # 语句基类 55 | # ... 56 | class Return(Statement): # return 语句 57 | # ... 58 | class TypeLiteral(Node): # 类型基类 59 | # ... 60 | class TInt(TypeLiteral): # 整型 61 | # ... 62 | ``` 63 | 64 | 假设在经过了词法分析和语法分析后,我们已经成功将 MiniDecaf 代码转化为了 AST 结构。现在,我们想要编写代码对 AST 进行扫描。很容易写出递归的 DFS 遍历: 65 | 66 | ```python 67 | def dfs(node: Node): 68 | if isinstance(node, Program): 69 | for func in node.functions: 70 | dfs(func) 71 | elif isinstance(node, Function): 72 | # do something for scanning a function node 73 | elif isinstance(node, Return): 74 | # ... 75 | ``` 76 | 77 | dfs 函数接收一个结点,根据这个结点的类型进行深度优先遍历。容易看出,dfs 函数根据被遍历的结点类型不同,执行不同的遍历逻辑。 那么我们把这些遍历逻辑封装到一个类里面,就得到了一个最简单的 Visitor。此外,为了便于实现,我们不使用 isinstance 来判断结点类型,而是调用结点自身的一个 accept 函数,并把不同的 visitXXX 函数抽象到一个接口里,各种具体的 Visitor 来实现这个接口。 78 | 79 | ```python 80 | ''' frontend/ast/node.py ''' 81 | class Node: # 所有 AST 结点的基类 82 | def accept(self, v: Visitor[T, U], ctx: T) -> Optional[U]: 83 | raise NotImplementedError 84 | ''' frontend/ast/tree.py ''' 85 | class Program(ListNode[Union["Function", "Declaration"]]): 86 | # ... 87 | def accept(self, v: Visitor[T, U], ctx: T): 88 | return v.visitProgram(self, ctx) 89 | class Function(Node): 90 | # ... 91 | def accept(self, v: Visitor[T, U], ctx: T): 92 | return v.visitFunction(self, ctx) 93 | # ... 94 | ''' frontend/ast/visitor.py ''' 95 | class Visitor(Protocol[T, U]): 96 | def visitOther(self, node: Node, ctx: T) -> None: 97 | return None 98 | def visitProgram(self, that: Program, ctx: T) -> Optional[U]: 99 | return self.visitOther(that, ctx) 100 | def visitFunction(self, that: Function, ctx: T) -> Optional[U]: 101 | return self.visitOther(that, ctx) 102 | # ... 103 | ``` 104 | 105 | 之后,如果我们想要编写一种遍历 AST 的方法,可以直接继承 Visitor 类,并在对应结点的 visit 成员方法下实现对应的逻辑。例如,框架中用如下的方法进行符号表构建: 106 | 107 | ```python 108 | class Namer(Visitor[ScopeStack, None]): 109 | def visitProgram(self, program: Program, ctx: ScopeStack) -> None: 110 | # ... 111 | for child in program: 112 | if isinstance(child, Function): 113 | child.accept(self, ctx) 114 | def visitFunction(self, func: Function, ctx: ScopeStack) -> None: 115 | # ... 116 | # ... 117 | ``` 118 | 119 | 如果想要访问某个子结点 child,直接调用 child.accept(self, ctx) 即可。 120 | -------------------------------------------------------------------------------- /docs/step10/example.md: -------------------------------------------------------------------------------- 1 | # step10 实验指导 2 | 3 | 本实验指导使用的例子为: 4 | 5 | ```C 6 | int x = 2024; 7 | int main() { return x; } 8 | ``` 9 | 10 | ## 词法语法分析 11 | 12 | 针对全局变量,我们需要新设计 AST 节点来表示它,只需修改根节点的孩子类型即可:原先表示整个 MiniDecaf 程序的根节点只能有函数类型的子节点,现在还可以允许变量声明作为子节点。 13 | 14 | ## 语义分析 15 | 16 | 本步骤引入全局变量,在引入全局变量之后,AST 根结点的直接子结点不只包括函数,还包括全局变量定义。全局变量符号存放在栈底的全局作用域符号表中。在遍历 AST 构建符号表的过程中,栈底的全局作用域符号表一直都存在,不会被弹出。 17 | 18 | ## 中间代码生成 19 | 20 | 经过 Step5 的学习,我们知道局部变量是存储在寄存器或栈中的,可以直接访问。然而,全局变量存储在特别的内存段中,不能直接访问。课程实验建议的加载全局变量方式为:首先加载全局变量符号的地址,然后根据地址来加载数据。因此,需要定义两个中间代码指令,完成全局变量值的加载: 21 | 22 | > 请注意,TAC 指令的名称只要在你的实现中是一致的即可,并不一定要和文档一致。 23 | 24 | | 指令 | 参数 | 含义 | 25 | | --- | --- | --- | 26 | | `LOAD` | `T1, offset` | 临时变量 T1 中存储地址,加载与该地址相差 offset 个偏移的内存地址中的数据 | 27 | | `LOAD_SYMBOL` | `symbol` | symbol 为字符串,加载 symbol 符号所代表的地址 | 28 | 29 | 有了上述两条指令,可以将测试用例翻译如下: 30 | 31 | ``` 32 | main: 33 | _T0 = LOAD_SYMBOL x 34 | _T1 = LOAD _T0, 0 35 | return T1 36 | ``` 37 | 38 | > 需要说明的是,你也可以把两条指令合并为一条指令,直接加载全局变量的值,但分为两条指令的方式可扩展性更好些。 39 | 40 | 请注意,翻译所得的 TAC 代码中没有为全局变量赋予初始值(2024)。可以将变量的初始值存放在变量符号对应的符号表里,在后端代码生成时**通过读取符号表得到初值**。此处给出的只是一种参考实现,大家也可以将全局变量的定义显式翻译为 TAC 代码,这样可以使中端与后端完全解耦。 41 | 42 | ## 目标代码生成 43 | 44 | Step10 中目标代码生成的主要任务有:翻译中间代码,将全局变量放到特定的数据段中。 45 | 46 | 1. 翻译中间代码 47 | 48 | 实际上,我们提供的中间代码设计和 RISC-V 汇编的思想是一致的,RISC-V 汇编中有对应 LOAD 和 LOAD_SYMBOL 的指令,我们直接给出翻译结果: 49 | 50 | ```assembly 51 | main: 52 | la t0, x # _T0 = LOAD_SYMBOL x 53 | lw t1, 0(t0) # _T1 = LOAD _T0, 0 54 | mv a0, t1 55 | ret 56 | ``` 57 | 58 | 2. 将全局变量放到特定的数据段中 59 | 60 | 到目前为止,翻译中间代码的方式是有问题的,问题在于,需要加载的 x 变量符号究竟存在哪里,如果所生成的汇编程序不给出 x 的定义,程序是有bug的。实际上,RISC-V 提供了一系列的[汇编指令](https://github.com/TheThirdOne/rars/wiki/Assembler-Directives),用以声明全局变量 x 所对应的数据段。 61 | 62 | 下面给出 RISC-V 用以全局变量声明的汇编指令,其他全局变量的声明只需修改变量名称和初始值即可: 63 | 64 | ```assembly 65 | .data 66 | .globl x 67 | x: 68 | .word 2024 69 | ``` 70 | 71 | 上例中,.data 表示输出到 data 数据段;.globl x 声明 x 为全局符号;.word 后是一个 4 字节整数,是 x 符号对应的初始值。 72 | 73 | 按照汇编约定,data 段中存放已初始化的全局变量,未初始化的全局变量则存放在 bss 段中。举例而言,下面的示例将未初始化的全局变量 x 存放到 bss 段中。其中,.space 表示预留一块连续的内存,4 表示存储空间大小为 4 字节。 74 | 75 | ```assembly 76 | .bss 77 | .globl x 78 | x: 79 | .space 4 80 | ``` 81 | 82 | # 思考题 83 | 1. 写出 `la v0, a` 这一 RiscV 伪指令可能会被转换成哪些 RiscV 指令的组合(说出两种可能即可)。 84 | 85 | 参考的 RiscV 指令链接:https://github.com/TheThirdOne/rars/wiki/Supported-Instructions 86 | -------------------------------------------------------------------------------- /docs/step10/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step10:全局变量 2 | step10 我们要支持的是全局变量,语法改动非常简单: 3 | 4 |

 5 | 
program 6 | : (function | declaration)* 7 |
8 | 9 | 全局变量和局部变量不同,它不是分配在栈上,而是放在某个固定地址,写在汇编的 .bss 段或 .data 段里。 10 | 访问它也不能通过 `fp` 加偏移量,而是需要通过它的符号加载它的地址,通过它的地址访问它。 11 | > 汇编课上应该讲过,实际中(包括 gcc 和 qemu)使用的可执行文件的格式是 ELF(Executable and Linking Format)。 12 | > .text 是其中存放代码的段(section),.bss 和 .data 都是其中存放数据的段,前者零初始化后者须指定初始值。 13 | > 14 | > 对有兴趣的同学: 15 | > 全局变量地址不是被狭义上的编译器(compiler)确定的,也不是被汇编器(assembler)确定的,而是被链接器(linker)或加载器(loader)确定的。 16 | > 简单的说,狭义上的编译器把源代码变成文本汇编,汇编器把文本汇编给编码到二进制代码,然后通过链接器变成可执行文件,运行时由加载器加载到内存中运行。 17 | > 当然,广义上的编译器就囊括了这所有阶段。 18 | 19 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/.pdf`,比如 stage 6 的实验报告需要放在 `stage-6` 这个 branch 下的 `./reports/stage-6.pdf`。整个 stage 6 只需要提交一份报告,你不需要单独为 step 10 准备报告。 20 | 21 | 你需要: 22 | 1. 改进你的编译器,支持本节引入的新特性,通过相关测试。 23 | 2. 完成实验报告(具体要求请看实验指导书的首页)。实验报告中需要包括: 24 | * 你的学号姓名 25 | * 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 26 | * 指导书上的思考题 27 | * 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 28 | * 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 -------------------------------------------------------------------------------- /docs/step10/pics/program_memory_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step10/pics/program_memory_layout.png -------------------------------------------------------------------------------- /docs/step11/example.md: -------------------------------------------------------------------------------- 1 | # step11 实验指导 2 | 3 | 本实验指导使用的例子为: 4 | 5 | ```C 6 | int x[10]; 7 | int main() { int y[10]; return 0; } 8 | 9 | ``` 10 | 11 | ## 词法语法分析 12 | 13 | 针对数组,我们需要设计 AST 节点来表示它,给出的参考定义如下: 14 | 15 | | 节点 | 成员 | 含义 | 16 | | --- | --- | --- | 17 | | `IndexExpr` | 索引基底 `base`,索引下标 `index` | 索引运算 | 18 | 19 | ## 语义分析 20 | 21 | 由于 step 11 里引入了数组,现在我们的变量类型不只是 int 型了,还包括 int 型数组。因此,为了保证所有表达式中变量的类型均合法,需要进行类型检查。 22 | 23 | **注意**:引入数组后,左值不再一定是 identifier 了,还有可能是如 `a[0][1]` 这样的索引运算表达式,因此同学们可能需要仔细考虑一下如何处理赋值时对值类别的检查。 24 | 25 | `frontend/type/array.py` 里实现了数组类型,同学们可以使用它完成实验,也可以自行对其进行修改。 26 | 27 | 有能力的同学可以考虑将原先 `Namer` 中类型检查的部分,以及 stage 5 需要增加的类型检查重构进 `Typer` 中,使实现更加模块化。 28 | 29 | ## 中间代码生成 30 | 31 | 数组和普通变量类似,可以分为局部数组和全局数组。 32 | 33 | 全局数组的处理与全局变量类似,由于是升级关卡,我们留给同学**自行思考**(和全局变量究竟有什么不同,是不是需要的内存空间更大?提示:1. 需要申请更大的 bss 段内存)。 34 | 35 | 针对局部数组,给出一种参考实现,实际上不只存在一种实现方法。实验文档给出一种参考实现方法,定义了一条中间代码指令 ALLOC 用于分配内存空间: 36 | 37 | > 请注意,TAC 指令的名称只要在你的实现中是一致的即可,并不一定要和文档一致。 38 | 39 | | 指令 | 参数 | 含义 | 40 | | --- | --- | --- | 41 | | `ALLOC` | `size` | 分配 size 字节的内存,并返回内存首地址 | 42 | 43 | 采用 ALLOC 指令,测试样例中的局部数组部分代码可以翻译为如下中间代码(忽略全局数组部分): 44 | 45 | ``` 46 | main: 47 | T0 = ALLOC 40 # 一个 int 类型为 4 个字节 48 | T1 = 0 49 | return T1 50 | ``` 51 | 52 | > 通过这种方式,我们实际上是把内存分配的锅甩给了目标代码生成,这大大提升了目标代码生成的自由度,属于合理分锅。 53 | 54 | 除了分配数组,我们还需要考虑如何访问数组元素。通过 ALLOC 指令我们得到了数组的首地址,那么任何一个数组元素的地址可以通过在首地址的基础上加上偏移量得到。于是,读取数组元素可以使用 Step10 中引入的 LOAD 指令来实现,我们还需要引入一条类似的 STORE 指令将值写入数组元素。 55 | 56 | 那么,如何将数组下标对应到偏移地址?对一维数组,下标的常数倍(int 型的大小为 4 个字节,倍数为4)即为偏移量。而对于高维数组,我们可以将其视为一个展开成一维的大数组。对于数组 a[d1][d2]...[dn],访问元素 a[i1][i2]...[in] 可以等价于访问 a[i1*d2*d3*...*dn + i2*d3*...*dn + ... + in]。在将数组索引翻译成 TAC 时,同学们需要自行将数组下标转换成地址计算指令。这个步骤并不困难,但可能比较繁琐,同学们在实现时要注意细节,避免错误。 57 | 58 | ## 目标代码生成 59 | 60 | 同中间代码生成,全局数组**自行思考实现**。 61 | 62 | 对于局部数组的内存分配,推荐在栈上为局部数组分配所需的空间,实际上,Step5 栈帧中的**局部变量区域**,可以用于存储局部数组。因此,大家需要模仿新建栈帧的操作,对栈顶指针 sp 进行修改,在栈上开辟出一块连续内存,并将这块内存的首地址返回即可。后续如有对数组中元素的访问,基于首地址进行偏移操作即可。 63 | 64 | # 思考题 65 | 66 | 1. C 语言规范规定,允许局部变量是可变长度的数组([Variable Length Array](https://en.wikipedia.org/wiki/Variable-length_array),VLA),在我们的实验中为了简化,选择不支持它。请你简要回答,如果我们决定支持一维的可变长度的数组(即允许类似 `int n = 5; int a[n];` 这种,但仍然不允许类似 `int n = ...; int m = ...; int a[n][m];` 这种),而且要求数组仍然保存在栈上(即不允许用堆上的动态内存申请,如`malloc`等来实现它),应该在现有的实现基础上做出那些改动? 67 | 68 | > 提示:不能再像现在这样,在进入函数时统一给局部变量分配内存,在离开函数时统一释放内存。 69 | > 70 | > 你可以认为可变长度的数组的长度不大于0是未定义行为,不需要处理。 71 | -------------------------------------------------------------------------------- /docs/step11/guide.md: -------------------------------------------------------------------------------- 1 | # step11 实验指导 2 | 3 | ## 词法语法分析 4 | 如果你使用工具完成词法语法分析,修改你的语法规范以满足要求,自行修改词法规范,剩下的交给工具即可。 5 | 6 | 你可能注意到,虽然数组是一种类型,但我们没有把数组放到 `type` 中,而是只放在 `declaration` 里。 7 | 这一方面是因为我们并不完全支持 C 的数组(例如我们没有指向数组的指针),另一方面 C 语言本身设计就如此。 8 | 9 | > 对有兴趣的同学:C 的这个设计很麻烦…… 你能区分 `int*[]` 和 `int(*)[]` 哪个是指针的数组、哪个是数组的指针吗? 10 | > 加上函数指针就更麻烦了,例如声明 `int (*(*vtable)[])(void*);` 中变量 `vtable` 的类型是 `int (*(*)[])(void*)`,含义是 11 | > 12 | > > “是一个指针,指向一个数组,数组每个元素是函数指针,函数接受一个 `void*` 参数,函数返回 `int`”。 13 | > 14 | > 当然,实际中我们一般不会写出这样的代码,更好的方法是用 typedef 包装一下,例如上面的会写成 `typedef int (*funcptr_t)(void*) ; typedef funcptr_t vtable_t[] ; vtable_t *vtable`。 15 | > 16 | > 至于为什么 C 声明被设计成这样,有一个说法是设计者希望声明能够体现变量的用法。例如上面 `vtable` 的用法是 `int v= (*(*vtable)[0])(voidptr_expr)`,非常类似其声明。 17 | > 18 | > 当然这些都和我们 **课程无关** ,我们更不用实现它们。 19 | 20 | 如果你是手写分析,参见[这里](./manual-parser.md)。 21 | 22 | ## 名称解析 23 | 引入数组后,变量的大小不一定是 4 了,例如 `int a[5][4]` 大小是 80。 24 | 因此变量的数据结构还需要增加一个 `size` 属性,并且变量的 `frameaddr` 不一定连续了(但每个变量所占的一片内存空间一定连续)。 25 | > 例如,某种实现中 `int main(){ int a[2][2]; int b[2]; int c; }` 中, 26 | > `a` 的 frameaddr 是 0,`b` 的是 4,`c` 的是 6。 27 | 28 | 另外,我们修改了左值的定义 12.9: 29 | > **12.9** (更新 11.1)表达式是左值的必要条件是它能被下面几条规则构造出来 30 | > * 被声明过的变量,如果声明类型不是数组,那么它是左值; 31 | > * 如果 `e` 是左值,那么括号括起的 `(e)` 也是左值; 32 | > * 如果 `e` 是类型为 `T*` 的表达式,那么 `*e` 是类型为 `T` 的左值; 33 | > * 下标运算的结果,如果其类型不是数组类型,那么它是左值。 34 | 35 | 因为本质上,`int a;` 的 `a` 的值被存放在内存中,需要一次访存 `load 它的frameaddr` 才能取得。 36 | 而局部变量 `int a[2];` 中的 `a` 是一个编译期就确定的偏移量常数,它的值就是 `fp` 加上这个偏移量常数,无须访存。 37 | > 全局变量 `int a[2];` 也是类似的。 38 | 39 | 另外,数组各维长度必须是正整数,别忘实现对应的语义检查。 40 | 41 | ## 类型检查 42 | 除了 step12 的 `IntegerType` 和 `PointerType`, 43 | 我们还需要增加数组类型 `ArrayType(baseType, length)`。 44 | > 例如 `int *a[10][20]` 就是 `ArrayType(ArrayType(PointerType(IntegerType()), 20), 10)`,特别注意 20 和 10 的位置。 45 | > 46 | > 当然,就 MiniDecaf 而言,实现中你可以一口气把所有维长度都存起来,变成 `ArrayType(baseType, lengthList)`,如上就是 `ArrayType(PointerType(IntegerType()), [10, 20])` 47 | > 48 | > 如[step12](../lab11/typeck.md)中所说,你也可以用不那么通用的方法来表示类型。 49 | > 因为我们不允许指向数组的指针,所以可以用一个`(int, 整数列表)`的二元组表示step11中任何表达式的类型。 50 | > 其中`int`部分表示数组的元素类型,它只可能是`int`的若干重指针,比如用 0 表示 `int`,3 表示 `int***`。 51 | > `整数列表`部分表示数组维度,如果为空,就是一个普通变量,否则就和上面的`lengthList`的含义一致。 52 | > 53 | > 不管你怎么表示类型,类型检查的规则是不会变的,`int *a[10][20]`可以表示成`ArrayType(ArrayType(PointerType(IntegerType()), 20), 10)`或者`(1, [10, 20])`,但是这只是同一个类型的两种的记录方式而已,不会影响到上层的逻辑。 54 | 55 | 并且相关类型规则是(语义规范 12.12, 12.13) 56 | 1. 对于下标操作 `e1[e2]`,要求 `e1` 是指针类型或者数组类型,`e2` 是整数类型;结果类型是指针/数组的基类型。 57 | > 注意,这里判断不要写 `e1.type == PointerType`, 而要写 `e1.type instanceof PointerType`(或者类似的手段)。 58 | > 可以写 `e2.type == IntegerType()` 或者 `e2.type instanceof IntegerType`。 59 | 2. 对于加法操作,除了最基础的 `int` 加法还要支持指针加法:两个操作数中一个是指针、另一个是 `int`;结果类型和指针操作数的类型一致。 60 | 3. 对于减法操作,除了 `int` 减法还可能有两种情况 61 | 1. 指针减整数:左操作数是指针类型、右操作数是 `int`;结果类型和第一个操作数的类型相同。(当然,MiniDecaf 禁止 `int` 减指针) 62 | 2. 指针减指针:左右操作数是相同的指针类型,结果类型是 `int` 63 | 64 | ## IR 生成 65 | 无须新增 IR 指令。 66 | 67 | **数组声明** 无需 IR 上特别处理,只要注意变量大小不一定是 4 即可。 68 | 并且,数组中数据的内存空间是连续的,因此无论数组的原型是几维的,都可以看做是一个一维的大数组。 69 | 对于一个数组 $$\mathtt{int}~a[d_1][d_2]\cdots[d_n]$$,可看做是 $$\mathtt{int}~a'[d_1d_2\cdots d_n]$$。访问 $$a[i_1][i_2]\cdots[i_n]$$,就是访问 $$a'[i_1d_2d_3\cdots d_n + i_2d_3d_4\cdots d_n + \cdots + i_n]$$。 70 | > 例如,对于数组 `int a[3][4][5]`,有: 71 | > * `a[i]` 的地址是 `a + (i * 4 * 5) * sizeof(int)`; 72 | > * `a[i][j]` 的地址是 `a + [(i * 4 * 5) + (j * 5)] * sizeof(int)`; 73 | > * `a[i][j][k]` 的地址是 `a + [(i * 4 * 5) + (j * 5) + k] * sizeof(int)`。 74 | 75 | **下标操作** `e1[e2]` 需要分数组和指针来说,并且需要类型检查阶段所计算出的表达式类型信息。 76 | 1. 如果 `e1` 是数组: 77 | 显然,`e1[e2]` 的地址是 `e1` 起始地址加上 `e2` 的值**乘以 S**,其中 S 为 `e1` 基类型的大小。 78 | > 我们约定,任何数组类型类型表达式的 IR 执行后,栈顶正好多出一个元素,其为该数组的起始地址。 79 | 因此,为了生成 `e1[e2]` 的 IR,先生成 `e1` 的 IR,再生成 `e2` 的 IR,再生成三条指令:`push S ; mul ; add`; 80 | 这一步生成的是 `e1[e2]` 的地址,如果 `e1[e2]` 不是左值也不是数组,还需要一个 `load`。 81 | > 例如 `int a[10][20];`,设 `a` 的 frameaddr 为 20,则 `a` 的 IR 如 `frameaddr 20`。 82 | > 而 `a[2+3]` 的 IR 如下(其中 `80 == 20 * sizeof(int)`) 83 | ``` 84 | frameaddr 20 85 | push 2 ; push 3 ; add 86 | push 80 87 | mul 88 | add 89 | ``` 90 | > 而 `a[2+3][17]` 作为非左值的 IR 如(如果是左值,去掉最后 `load` 即可) 91 | ``` 92 | ...(和上面一样) 93 | push 17 94 | push 4 95 | mul 96 | add 97 | load 98 | ``` 99 | 100 | 2. 如果 `e1` 是指针: 101 | 类似上面,`e1[e2]` 的地址是 `e1` 的值加上 `e2` 的值**乘以 S**,其中 S 为 `e1` 的基类型的大小。 102 | 因此,为了生成 `e1[e2]` 的 IR,先生成 `e1` 的 IR(这里 `e1` 不是左值),然后生成 `e2` 的 IR,然后还是 `push S ; mul ; add ; load`。 103 | 不过 `e1[e2]` 可能作为左值,如果作为左值,那么生成地址的 IR 和上面一样,但去掉最后的 `load`。 104 | 105 | **指针算术** 也分两类 106 | 1. 指针加整数:`e1 + e2`,其中 `e1` 是指针、`e2` 是整数。 107 | 注意指针加整数的值是:指针的值,加上整数**乘以 S**,其中 S 为指针基类型的大小 [^1]。 108 | IR 生成类似上面,请自行设计。 109 | > 例如 `int *p;`,设 `p` 的 frameaddr 是 20,那么 `p+61` 的 IR 如下(注意其中 `push 4 ; mul`) 110 | > `frameaddr 20 ; load ; push 61 ; push 4 ; mul ; add`。 111 | > 整数加指针、指针减整数类似。 112 | 2. 指针减指针:同上,指针数值相减后,要除以基类型的大小。 113 | 114 | > 因此 `int *p` 那么 `(p+10) - (&p[3])` 等于 7。 115 | 116 | ## 汇编生成 117 | 无需特别修改。 118 | 119 | # 思考题 120 | 1. 设有以下几个函数,其中局部变量 `a` 的起始地址都是 `0x1000`(4096),请分别给出每个函数的返回值(用一个常量 minidecaf 表达式表示,例如函数 `A` 的返回值是 `*(int*)(4096 + 23 * 4)`)。 121 | ```c 122 | int A() { 123 | int a[100]; 124 | return a[23]; 125 | } 126 | 127 | int B() { 128 | int *p = (int*) 4096; 129 | return p[23]; 130 | } 131 | 132 | int C() { 133 | int a[10][10]; 134 | return a[2][3]; 135 | } 136 | 137 | int D() { 138 | int *a[10]; 139 | return a[2][3]; 140 | } 141 | 142 | int E() { 143 | int **p = (int**) 4096; 144 | return p[2][3]; 145 | } 146 | ``` 147 | 148 | 2. C 语言规范规定,允许局部变量是可变长度的数组([Variable Length Array](https://en.wikipedia.org/wiki/Variable-length_array),VLA),在我们的实验中为了简化,选择不支持它。请你简要回答,如果我们决定支持一维的可变长度的数组(即允许类似 `int n = 5; int a[n];` 这种,但仍然不允许类似 `int n = ...; int m = ...; int a[n][m];` 这种),而且要求数组仍然保存在栈上(即不允许用堆上的动态内存申请,如`malloc`等来实现它),应该在现有的实现基础上做出那些改动? 149 | > 提示:不能再像现在这样,在进入函数时统一给局部变量分配内存,在离开函数时统一释放内存。 150 | > 151 | > 当同时存在>= 2个可变长度的数组时,至少有一个数组的起始地址不能在编译时决定。 152 | > 153 | > 你可以认为可变长度的数组的长度不大于0是未定义行为,不需要处理。 154 | 155 | # 总结 156 | 本节内容本身难度不大,但细节很多(尤其注意指针加整数时,整数要乘一个数),也有相当代码量。 157 | 158 | # 备注 159 | [^1]: MiniDecaf 中指针基类型只能是 `int`、`int*`、`int**`……,所以这里 S 只可能等于 4。 160 | -------------------------------------------------------------------------------- /docs/step11/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step11:数组 2 | step11 的目标是支持数组: 3 | 4 | 语法上没有太大改动, 5 | 1. 数组的初始化: 6 |

 7 | declaration
 8 | 
: type Identifier ('[' Integer ']')* ('=' expression)? ';' 9 |
10 | 11 | 2. 数组的下标操作 12 |

13 | postfix
14 |     : primary
15 |     | Identifier '(' expression_list ')'
16 | 
| postfix '[' expression ']' 17 |
18 | 19 | step11 难度不大,但有了数组让我们能够写很多有意思的程序了,step11 之前甚至 MiniDecaf 连快速排序都写不了。 20 | 21 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/.pdf`,比如 stage 6 的实验报告需要放在 `stage-6` 这个 branch 下的 `./reports/stage-6.pdf`。整个 stage 6 只需要提交一份报告,你不需要单独为 step 11 准备报告。 22 | 23 | 你需要: 24 | 1. 改进你的编译器,支持本节引入的新特性,通过相关测试。 25 | 2. 完成实验报告(具体要求请看实验指导书的首页)。实验报告中需要包括: 26 | * 你的学号姓名 27 | * 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 28 | * 指导书上的思考题 29 | * 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 30 | * 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 -------------------------------------------------------------------------------- /docs/step11/pics/README.md: -------------------------------------------------------------------------------- 1 | some pics 2 | -------------------------------------------------------------------------------- /docs/step12/example.md: -------------------------------------------------------------------------------- 1 | # step12 实验指导 2 | 3 | 本实验指导使用的例子为: 4 | 5 | ```c 6 | int func(int param[]){ 7 | param[0] = 1; 8 | return 0; 9 | } 10 | 11 | int main() { 12 | int arr[4] = {1,2}; 13 | func(arr); 14 | return arr[0] + arr[1] + arr[2]; 15 | } 16 | ``` 17 | 18 | ## 词法语法分析 19 | 20 | 我们需要增加一个数组的初始化列表,可以直接修改上一节数组的AST结点增加一个数组用于记录初始化元素。 21 | 22 | 函数的参数列表需要加上数组类型。 23 | 24 | ## 语义分析 25 | 26 | 由于 step 12 里额外引入了数组传参和数组初始化,所以你需要修改语义分析,以支持数组传参。传参出现了一种特殊情况,即:函数参数数组的第一维可以为空。 27 | 28 | ```c 29 | int fun(int a[][12]){ 30 | a[0][1] = 1; 31 | return 0; 32 | } 33 | ``` 34 | 35 | ## 中间代码生成 36 | 37 | 在C语言中,对于全局数组,如果没有初始化,那么其值全为0,而对于局部数组来说,如果没有初始化,其值是未定义的。 38 | 39 | 而初始化后数组的元素值是确定的,如果初始化时指定的的元素个数比数组大小少,剩下的元素都回被初始化为 0。例如: 40 | 41 | ```c 42 | int arr[3]={1,2}; 43 | // 等价于 44 | int arr[3]={1,2,0}; 45 | ``` 46 | 47 | 当数组长度较长时,如果对每个位置产生一条赋值语句可能会让生成的汇编代码非常冗长。因此你可能需要内置一个 `memset` 这样的函数来实现数组的清零。由于gcc的汇编器通常自带一个`memset`函数,我们这里采用`fill_n`命名。 48 | 49 | ```c 50 | // fill_n 函数原型,三个参数分别是目标内存地址,设置的内容,长度(以数组元素个数为单位) 51 | int fill_n(int *dst, int res, int cnt); 52 | ``` 53 | 54 | 因此,上述初始化可以等价地转化为: 55 | 56 | ```c 57 | int arr[3]; 58 | fill_n(arr, 0, 3); 59 | a[0] = 1; 60 | a[1] = 2; 61 | ``` 62 | 63 | # 目标代码生成 64 | 65 | 数组传参相对于初始化是简单的,回想函数一节的传参方式,**自行实现**。 66 | 67 | # 思考题 68 | 69 | 1. 作为函数参数的数组类型第一维可以为空。事实上,在 C/C++ 中即使标明了第一维的大小,类型检查依然会当作第一维是空的情况处理。如何理解这一设计? 70 | 71 | # 总结 72 | 恭喜你实现了 MiniDecaf 语言的所有特性。回过头看,我们从常量表达式开始,逐步为编译器增加变量、作用域等特性,又引入控制逻辑,最后实现全局变量和数组,编译器逐渐变得功能齐全。编译器每一个新的特性都带来了新的挑战,而你通过自己的智慧,逐步解决了这些挑战。顺利完成实验后,相信你对编译器也有了自己独特的理解。 -------------------------------------------------------------------------------- /docs/step12/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step12:为数组添加更多支持 2 | step12 的目标是支持数组的初始化和传参: 3 | 4 | 语法上没有太大改动, 5 | 6 | 数组的初始化: 7 | 8 |

 9 | declaration
10 | 
: type Identifier ('[' Integer ']')+ ('=' '{' (Integer (',' Integer)*)? '}')? ';' 11 |
12 | 13 | 14 | 15 | 数组的传参: 16 | 17 |
18 | function 19 | : type Identifier '(' parameter_list ')' (compound_statement | ';') 20 | parameter_list 21 | : (type Identifier ('[' ']')?(('['Integer']')*)? (',' type Identifier ('[' ']')?(('['Integer']')*)?)*)? 22 |
23 | 24 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/.pdf`,比如 stage 6 的实验报告需要放在 `stage-6` 这个 branch 下的 `./reports/stage-6.pdf`。整个 stage 6 只需要提交一份报告,你不需要单独为 step 12 准备报告。 25 | 26 | 你需要: 27 | 1. 改进你的编译器,支持本节引入的新特性,通过相关测试。 28 | 2. 完成实验报告(具体要求请看实验指导书的首页)。实验报告中需要包括: 29 | * 你的学号姓名 30 | * 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 31 | * 指导书上的思考题 32 | * 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 33 | * 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 34 | -------------------------------------------------------------------------------- /docs/step12/pics/README.md: -------------------------------------------------------------------------------- 1 | some pics 2 | -------------------------------------------------------------------------------- /docs/step12/typesystem.md: -------------------------------------------------------------------------------- 1 | # 类型系统 2 | 3 | 相信大家都了解过编程语言中**类型**的概念,数据的类型描述了数据的“含义”,描述了编译器和解释器会如何使用这份数据,比如在 C 语言中,下标运算只能应用于数组类型或指针类型。 4 | C 编译器会检查下标运算所应用的表达式的类型,如果它不是数组类型或者指针类型,比如 `int a; a[3];`,C 编译器就会认为其无法被应用于下标运算,继而报出一个编译错误。 5 | 除了数据类型之外,人们同样也会讨论函数类型,但由于 MiniDecaf 几乎不支持任何函数式特性,简单起见,这里我们所讨论的类型仅包含数据类型。 6 | 7 | 将一种语言中的所有类型及对各种类型的所有使用规则形式化,严格地描述语言中如何为每个表达式指定类型,这就构成了**类型系统**。 8 | 类型系统包含若干条类型规则,每条规则描述了如何为一种表达式指定类型。 9 | 一条类型规则形如 10 | 11 | $$ 12 | \frac{p_1 \qquad \cdots \qquad p_m}{e : t} 13 | $$ 14 | 15 | ,其中 $$e : t$$ 指表达式 $$e$$ 的类型为 $$t$$,这条规则的含义(读法)为当横线上方的前提条件 $$p_1 \cdots p_m$$ 都被满足时横线下方的结论成立。 16 | MiniDecaf 的类型系统将会在之后详尽列出。 17 | 18 | 基于类型系统,我们便可以通过自底向上地遍历 AST 来做**类型检查**,即对于一个表达式,尝试找到符合子表达式类型及运算的类型规则,通过找到的类型规则来推导出这个表达式的类型;如果没有符合的类型规则,则报出一个类型错误。 19 | 通过类型检查,编译器可以很好地减少程序中可能的 bug。 20 | 类型检查可以放在编译时(静态)或者运行时(动态)执行,或者在编译时和运行时都执行。 21 | 22 | ## MiniDecaf 的类型系统 23 | 24 | MiniDecaf 包含两种类型: 25 | - $$\mathrm{int}$$:一个 32 位整数; 26 | - $$[\mathrm{int}]^n$$:一个 $$n~(n \ge 1)$$ 维数组,其元素类型为 $$\mathrm{int}$$。 27 | 28 | MiniDecaf 包含五条类型规则: 29 | 30 | $$ 31 | \frac 32 | {e : \mathrm{int} \qquad \bullet \in UN} 33 | {\bullet (e) : \mathrm{int}} 34 | \text{(T-un)} 35 | $$ 36 | 37 | 第一条规则是对一元运算的结果的指定规则,其中 $$UN$$ 是 MiniDecaf 中所有的一元运算集合,$$\bullet(e)$$ 表示将 $$\bullet$$ 这个运算应用于 $$e$$ 上所得的结果。 38 | 直观来讲,即是说当一元运算的操作数是一个 $$\mathrm{int}$$ 时,该操作的结果也是一个 $$\mathrm{int}$$。 39 | 40 | $$ 41 | \frac 42 | {e_1 : \mathrm{int} \qquad e_2 : \mathrm{int} \qquad \bullet \in BIN} 43 | {\bullet (e_1, e_2) : \mathrm{int}} 44 | \text{(T-bin)} 45 | $$ 46 | 47 | 第二条规则是对二元运算的结果的指定规则,其中 $$BIN$$ 是 MiniDecaf 中除了下标运算以外的所有的二元运算集合,$$\bullet(e_1, e_2)$$ 表示将 $$\bullet$$ 这个运算应用于 $$e_1$$ 和 $$e_2$$ 上所得的结果。 48 | 直观来讲,即是说当一个不是下标运算的二元运算的操作数都是一个 $$\mathrm{int}$$ 时,该操作的结果也是一个 $$\mathrm{int}$$。 49 | 50 | $$ 51 | \frac 52 | {e_1 : \mathrm{int} \qquad e_2 : \mathrm{int} \qquad e_3 : \mathrm{int} \qquad \bullet \in TERN} 53 | {\bullet(e_1, e_2, e_3) : \mathrm{int}} 54 | \text{(T-tern)} 55 | $$ 56 | 57 | 第三条规则是对三元运算的结果的指定规则,其中 $$BIN$$ 是 MiniDecaf 中所有的三元运算集合,$$\bullet(e_1, e_2, e_3)$$ 表示将 $$\bullet$$ 这个运算应用于 $$e_1$$、$$e_2$$ 和 $$e_3$$ 上所得的结果。 58 | 直观来讲,即是说当一个三元运算的操作数都是一个 $$\mathrm{int}$$ 时,该操作的结果也是一个 $$\mathrm{int}$$。 59 | 60 | $$ 61 | \frac 62 | {a : [\mathrm{int}]^n \qquad i : \mathrm{int} \qquad n > 1} 63 | {a[i] : [\mathrm{int}]^{n-1}} 64 | \text{(T-hda)} 65 | $$ 66 | 67 | 第四条规则是对高维数组(维数大于 1)的下标运算的结果的指定规则。 68 | 直观来讲,即是说当下标运算被应用于一个高维数组且下标为 $$\mathrm{int}$$ 时,其运算结果是一个比原数组低一维的数组。 69 | 70 | $$ 71 | \frac 72 | {a : [\mathrm{int}]^1 \qquad i : \mathrm{int}} 73 | {a[i]: \mathrm{int}} 74 | \text{(T-oda)} 75 | $$ 76 | 77 | 第五条规则是对一维数组的下标运算的结果的指定规则,其运算结果是一个整数。 78 | 直观来讲,即是说当下标运算被应用于一个一维数组且下标为 $$\mathrm{int}$$ 时,其运算结果是一个 $$\mathrm{int}$$。 79 | 80 | 另外,对于仅包含单个变量或者函数调用的表达式 $$e$$,我们有 $$e : \mathrm{int}$$。 81 | -------------------------------------------------------------------------------- /docs/step13/example.md: -------------------------------------------------------------------------------- 1 | # step13 实验指导 2 | 3 | 本节实验要求实现论文 [TOPLAS'1996: *Iterated Register Coalescing*](https://dl.acm.org/doi/pdf/10.1145/229542.229546) 提出的寄存器分配算法。推荐大家完整读一遍论文,并通过[论文作者的讲解课件](https://people.cs.rutgers.edu/~zz124/cs516_spring2015/lectures/IteratedRegisterCoalescing.pdf),(如果前面的链接失效了,可以打开[这个链接](https://pdfs.semanticscholar.org/1a58/9e3ff9e594597b4373d93ec030c09c880377.pdf))辅助理解。论文文末的附录有完整的伪代码,你可以在它的基础上完成本次实验。 4 | 5 | 下面简要介绍一些你可能需要的预备知识。 6 | 7 | ## 回顾:启发式寄存器分配算法 8 | 9 | 在[step 6 的数据流分析一节](../step6/dataflow.md)中,提到了活跃变量的概念。即对于一个临时变量来说,如果它在某个执行点处具有的值会在这个执行点以后被用到,那么它在这个执行点处是活跃的。 10 | 11 | 而在[step5 中提到了一个简单的启发式寄存器分配算法](../step5/example.html#简单的启发式寄存器分配算法)。在给一个变量分配寄存器时,它的大致思路如下: 12 | 13 | - 首先检查是否存在空闲的寄存器,有则直接分配给当前变量。 14 | - 否则,检查是否存在寄存器,使得它关联的临时变量在当前位置已经不是活跃变量了,如是则把它关联到当前变量。 15 | - 否则,说明所有寄存器所关联的变量都是活跃的。此时随机选择某个寄存器,把它关联的临时变量存到栈帧上(这叫做溢出(`spill`)到内存),然后把它关联到当前变量。 16 | 17 | ## 基于图染色的寄存器分配算法 18 | 19 | 我们可以换一种角度去思考寄存器分配问题:两个变量在什么情况下不能被分配到同一个寄存器?当且仅当两个变量同时活跃时,它们不能被分到同一个寄存器。可以把这样的一对变量定义为相干的(interference),或者说相互冲突的。 20 | 21 | 重用一下 step 6 中活跃变量的例子: 22 | 23 | | TAC 代码 | 活跃变量集合 | 相干寄存器 | 24 | | ----------------- | --------------- | ------------------------ | 25 | | `_T0 = 4` | {_T0} | | 26 | | `_T1 = 3` | {_T0, _T1} | (_T0,_T1) | 27 | | `_T2 = _T0 * _T1` | {_T0} | | 28 | | `_T3 = _T0 * _T0` | {_T0, _T3} | (_T0,_T3) | 29 | | `_T2 = _T3 * _T3` | {_T0, _T2, _T3} | (_T0,_T2),(_T0, _T3),(_T2, _T3) | 30 | | `_T2 = _T0 * _T2` | {_T2, _T3} | (_T2,_T3) | 31 | | `_T1 = _T2 * _T3` | {_T1} | | 32 | | `return _T1` | {} | | 33 | 34 | 这时我们再提出一个问题:最少可以用多少个寄存器完成上面代码的寄存器分配? 35 | 36 | 容易发现,至少需要3个寄存器。因为 `_T0,_T2,_T3` 相互冲突,需要各一个寄存器,而 `_T1` 可以跟 `_T2` 或者 `_T3` 共用寄存器。 37 | 38 | 这个思路相比代码框架中的启发式寄存器分配算法有以下好处: 39 | 40 | - 尽量减少使用的寄存器个数,在函数调用或返回时需要保存或恢复尽量少的 `callee save/caller save` 寄存器,减少变量溢出到内存的次数。这一部分对运行效率的影响很大,因为访存通常比访问寄存器慢很多。 41 | - 方便全局优化寄存器分配,减少基本块之间的 `move` 指令。 42 | 43 | 事实上,我们可以用图染色问题去描述“相互冲突的变量”: 44 | 45 | - 图染色问题:有 `n` 个结点,`m` 条边,你需要给每个结点指定一个颜色,使得任意两个有边直接相连的结点的颜色不同。 46 | 47 | - 寄存器分配问题:有 `n` 个变量,`m` 组冲突的变量。你需要给每个变量指定一个寄存器,使得任意两个冲突的变量的寄存器不同。 48 | 49 | ![](./pics/1.png) 50 | 51 | 上面这两个问题描述是一一对应的。如图所示(暂时先忽略图中的虚线边),如果把每个字母看成一个变量,每种颜色看成一个寄存器,那么图中的染色方案就对应了一个寄存器分配方案。 52 | 53 | ##### 如何找到所有冲突的变量 54 | 55 | 这里只提一个最简单的思路:看上面我们分析时列出的表格,先列举出每一步的活跃变量集合,然后两两连边。 56 | 57 | ##### 如何解决寄存器分配对应的图染色问题 58 | 59 | 假定我们有 `k` 种颜色可用于染色(对应 `k` 个寄存器可用于存放变量),那么可以依照下面的顺序执行 60 | 61 | 1. 寻找图中是否有连接了少于 `k` 条边的结点,如果有,把它记录下来然后从图中删除。重复这个步骤直到不存在少于 `k` 条边的结点。 62 | 2. 如果图中已经没有结点,则进入步骤3;否则,此时图中所有点都连接了至少 `k` 条边。这时,选择一个点(可以随机选,但可以通过其他信息来优化你的选择),把它记录下来然后从图中删除。然后重复步骤1。 63 | 3. 按删除的逆序恢复所有结点。 64 | 1. 当恢复一个从步骤1删除的结点时,因为当前它连接了少于 `k` 条边,所以我们总能为它指定一个颜色,使之不和相邻的最多 `k-1` 个点的颜色冲突。 65 | 2. 当恢复一个从步骤2删除的结点时,检查它连接的所有边。如果我们足够幸运,与它相邻的所有点没有用完所有 `k` 种颜色,那么我们可以为这个点指定一个不冲突的颜色。否则,为它选择一个颜色,这意味着它和另一个变量被分配到同一个寄存器里。别担心,这不会导致算法失败,只是会使得这个变量在使用时需要从栈帧保存与恢复,对应启发式寄存器分配算法中溢出(`spill`)到内存的情况。 66 | 4. 将每种颜色对应到寄存器上,生成后端代码。 67 | 68 | ## 基于复制指令的寄存器合并 69 | 70 | 在图染色的基础上有一种合并寄存器的进阶方法:合并通过复制指令(`copy instructions`)(其实就是赋值)传值的寄存器。 71 | 72 | 例如下面的代码 73 | 74 | ```c 75 | int f() { 76 | int a = 1; 77 | int b = a; 78 | int c = a + 2; 79 | int d = b + 3; 80 | return a + b + c + d; 81 | } 82 | ``` 83 | 84 | 用上面提过的活跃变量分析可以算出,在 `c = a + 2` 执行时 `a` 和 `b` 都是活跃变量。但观察代码可以发现 `a` `b` 事实上存的是同样的值,只需要用同一个寄存器存就行。这篇论文使用了这个优化,并改进了前人的类似优化方案。 85 | 86 | 这样我们可以在图染色问题中把仅因复制(其实就是赋值)指令相互冲突的一对点之间的连边标记成虚线,表示如果它们最终染同一种颜色,就可以删去这条边然后合并这两个点。这有助于把上述图染色算法中从步骤2删去的点挪到步骤1删去,避免溢出到内存的情况。 87 | 88 | ##### 可以直接合并这两个点吗? 89 | 90 | 可以,但可能会导致产生出连接许多边的结点,反而使得后续染色困难,不得不溢出到内存。这实际上是更早的 `Chaitin` 的解决方案。 91 | 92 | ##### 可以在保证合并后边数 `.pdf`,比如 stage 7 的实验报告需要放在 `stage-7` 这个 branch 下的 `./reports/stage-7.pdf`。整个 stage 7 只需要提交一份报告。 8 | 9 | 你需要: 10 | 11 | 1. 改进你的编译器,支持上面提到的寄存器分配算法。 12 | * 除了替换位于 `backend/reg/bruteregalloc.py` 的分配算法外,你可能还需要修改其他文件以适配算法的需求。如果你忘了这部分内容,可以看看 [step 6 的数据流分析一节](../step6/dataflow.md)。 13 | * 你需要**为代码添加合理的注释**以便批阅。 14 | * 你需要设计新测例(见本节实验指导),用以检测新分配算法的优化效果。 15 | 2. 完成实验报告(具体要求请看实验指导书的首页)。实验报告中需要包括: 16 | * 你的学号姓名 17 | * 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 18 | * 详细说明你的代码的运行逻辑 19 | * **举例说明**:对于某几个测例,你在本节实验之前的代码编译出什么?本节实验之后编译出什么?它是如何被优化的? 20 | * 对于本节实验的新测例,分别使用本节实验之前之后的代码进行编译,**测量并统计运行时间**。改进后的编译器编译出的代码的运行效率需要有显著提升,但效率提升的高低不会作为评分的考察点。 21 | * 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 22 | * 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 23 | -------------------------------------------------------------------------------- /docs/step13/pics/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step13/pics/1.png -------------------------------------------------------------------------------- /docs/step13/pics/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step13/pics/2.png -------------------------------------------------------------------------------- /docs/step13/readme.md: -------------------------------------------------------------------------------- 1 | # 选做实验二说明 2 | 3 | 选做实验二是给希望继续深入了解编译器知识的同学设计的实验部分,这部分内容具有**较大难度**,请同学们依据自己的时间安排情况决定是否选做。由于我们的实验框架并非为实现这个Step设计,改动部分可能较大,请确保你对框架有完整的了解再开始实验。**这部分以报告评分,没有额外的测试样例。** 4 | -------------------------------------------------------------------------------- /docs/step14/example.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step14:静态单赋值 2 | 3 | 本节实验指导使用的例子为: 4 | 5 | ```C 6 | int main() { 7 | int x = 1; 8 | int cond = 1; 9 | if (cond > 0) { 10 | x = 1; 11 | } else { 12 | x = -1; 13 | } 14 | return x; 15 | } 16 | ``` 17 | 18 | ## 词法语法分析 19 | 20 | mem2reg 属于在中间代码基础上的优化,因此词法语法分析部分没有额外增加的内容。 21 | 22 | ## 语义分析 23 | 24 | mem2reg 属于在中间代码基础上的优化,因此语义分析部分没有额外增加的内容。 25 | 26 | ## 中间代码生成 27 | 28 | mem2reg 使得我们可以在生成中间代码时,使用 Alloc、Load 和 Store 的组合针对局部变量生成符合 SSA 要求的代码。 29 | 30 | 对于本节实验用例,一种可能的中间代码表示为: 31 | 32 | ```assembly 33 | main: 34 | _T0 = ALLOC 4 35 | _T1 = ALLOC 4 36 | STORE _T0, 1 37 | LOAD _T2, _T0 38 | _T4 = GT _T2, 0 39 | BEQZ _T4, _L2 40 | STORE _T2, 1 41 | _L1: 42 | LOAD _T5, _T2 43 | return _T5 44 | _L2: 45 | _T6 = SUB 0, 1 46 | STORE _T2, _T6 47 | JUMP _L1 48 | ``` 49 | 50 | 在此基础上,进行 mem2reg 转化: 51 | 52 | ```assembly 53 | main: 54 | _T0 = GT 1, 0 55 | BEGZ _T0, _L2 56 | _L1: 57 | _T2 = phi [1, main], [_T3, _L2] 58 | return _T2 59 | _L2: 60 | _T3 = SUB 0, 1 61 | JUMP _L1 62 | ``` 63 | 64 | 需要注意的是,所有的 Phi 指令应当在基本块的开头同时支持并行执行(即 Phi 指令的执行顺序对结果没有影响)。 65 | 66 | 在实现 mem2reg 时,我们需要首先对代码进行数据流分析,计算控制流图中的支配关系和每个基本块的支配边界。 67 | 68 | > 相关的解释和详细说明可以参考: 69 | > 如何构建 SSA 形式的 CFG:https://szp15.com/post/how-to-construct-ssa/ 70 | 71 | 随后,我们需要实现 SSA 构造算法。一种常用的算法是将整个过程分为:插入 phi 函数和变量重命名,两个阶段。 72 | 73 | 在第一阶段,记录每个局部变量相关的 Alloc 和 Store 指令,并由此在基本块的开头插入 Phi 指令。 74 | 75 | 在第二阶段,遍历所有基本块,对其中局部变量相关的 Alloc,Load 和 Store 指令进行改写,以保证程序语义的正确性。在遍历一个基本块的所有指令后,维护该基本块的所有后继基本块中的 Phi 指令。 76 | 77 | > 相关的解释和详细说明可以参考: 78 | > 79 | > Static Single Assignment Book 的 Chapter3:https://pfalcon.github.io/ssabook/latest/ 80 | 81 | ## 目标代码生成 82 | 83 | 将 Phi 指令翻译为目标代码的过程相对复杂,本节实验不对这部分做要求。 -------------------------------------------------------------------------------- /docs/step14/intro.md: -------------------------------------------------------------------------------- 1 | ### 实验指导 step14:静态单赋值 2 | 3 | > 本节选做实验改编自: 4 | > https://buaa-se-compiling.github.io/miniSysY-tutorial/challenge/mem2reg/help.html 5 | > 在此表示感谢! 6 | 7 | 静态单赋值(Static Single Assignment, SSA)是编译器中间表示(IR)阶段的一个重要概念,它要求程序中每个变量在使用之前只被赋值一次。 8 | 9 | 例如,考虑使用 IR 编写程序计算 1 + 2 + 3 的值,一种可能的写法为: 10 | 11 | ```assembly 12 | _T0 = 1 13 | _T1 = 2 14 | _T2 = 3 15 | _T3 = ADD _T0, _T1 16 | _T3 = ADD _T3, _T2 17 | return _T3 18 | ``` 19 | 很遗憾,上述程序并不符合 SSA 的要求,因为其中变量 _T3 被赋值了两次。正确的写法应该为: 20 | ```assembly 21 | _T0 = 1 22 | _T1 = 2 23 | _T2 = 3 24 | _T3 = ADD _T0, _T1 25 | _T4 = ADD _T3, _T2 26 | return _T4 27 | ``` 28 | #### 我们为什么要这样做呢? 29 | 30 | 因为 SSA 可以简化每个变量的属性,进而简化编译器的优化过程。 31 | 32 | 例如,考虑下面这段伪代码: 33 | 34 | ```assembly 35 | y := 1 36 | y := 2 37 | x := y 38 | ``` 39 | 很显然,其中变量 y 的第一次赋值是不必须的,因为变量 y 被使用前,经历了第二次赋值。对于编译器而言,确定这一关系并不容易,需要经过定义分析(Reaching Definition Analysis)的过程。在很多控制流复杂的情况下,上述过程将变得更加困难。 40 | 41 | 但如果将上述代码变为 SSA 形式: 42 | 43 | ```assembly 44 | y1 := 1 45 | y2 := 2 46 | x1 := y2 47 | ``` 48 | 上述关系变得更加显而易见,由于每一个变量只被赋值一次,编译器可以轻松地得到 x1 的值来自于 y2 这一信息。 49 | 50 | 正因如此,许多编译器优化算法都建立在 SSA 的基础之上,例如:死代码消除(dead code elimination)、常量传播(constant propagation)、值域传播(value range propagation)等。 51 | 52 | #### 我们如何实现 SSA 呢? 53 | 54 | 例如,考虑使用 IR 编写程序使用循环计算 5 的阶乘。 55 | 56 | 按照 C 语言的思路,我们可能给出如下写法: 57 | 58 | ```assembly 59 | _L0: 60 | _T0 = 0 61 | _T1 = 1 62 | _T2 = 2 63 | _T3 = ADD _T0, _T1 # int temp = 1 64 | _T4 = ADD _T0, _T2 # int i = 2 65 | _T5 = 5 66 | _L1: 67 | _T6 = LT _T4, _T5 # i < 5 68 | BEQZ _T6, _L3 69 | _L2: # loop label 70 | _T3 = MUL _T3, _T4 # temp = temp * i 71 | _T4 = ADD _T4, _T1 # i = i + 1 72 | JUMP _L1 73 | _L3: # break label 74 | return _T3 75 | ``` 76 | 我们注意到,变量 _T3 和 _T4 由于循环体的存在可能被赋值多次,因此上述写法并不符合 SSA 的要求。 77 | 78 | 一种可能的方案是使用 Phi 指令。Phi 指令的语法是 ` = PHI [, ], [, ] ...` 。它使得我们可以根据进入当前基本块之前执行的是哪一个基本块的代码来选择一个变量的值。 79 | 80 | 由此,我们的程序可以改写为: 81 | 82 | ```assembly 83 | _L0: 84 | _T0 = 2 85 | _T1 = 1 86 | _L1: 87 | _T2 = PHI [_T0, _L0], [_T6, _L2] # int i = 2 88 | _T3 = PHI [_T1, _L0], [_T7, _L2] # int temp = 1 89 | _T4 = 5 90 | _T5 = LT T2, _T4 # i < 5 91 | BEQZ _T5, _L3 92 | _L2: # loop label 93 | _T7 = MUL _T3, _T2 # temp = temp * i 94 | _T6 = ADD _T2, _T1 # i = i + 1 95 | JUMP _L1 96 | _L3: # break label 97 | return _T3 98 | ``` 99 | 由此,上述程序中每一个变量只被赋值了一次,满足了 SSA 的要求。(注意,SSA 仅要求变量在静态阶段被单一赋值,而不是在运行时仅被赋值一次) 100 | 101 | 另一种可能的方案是使用 Alloca、Load 和 Store 的组合。SSA 要求中间表示阶段虚拟寄存器满足单一赋值要求,但并不要求内存地址如此。因此,我们可以在前端生成中间代码时,将每一个变量都按照栈的方式使用 Alloca 指令分配到内存中,之后每次访问变量都通过 Load 或 Store 指令显式地读写内存。使用上述方案编写的程序满足 SSA 的要求,且避免了繁琐地构造 Phi 指令,但频繁地访问内存将导致严重的性能问题。 102 | 103 | #### 有没有更好的解决方案呢? 104 | 105 | 有,我们可以将两种方案结合起来。 106 | 107 | 在前端生成中间代码时,首先使用第二种方案利用 Alloca、Load、Store 指令快速地构建满足 SSA 要求的代码。 108 | 随后,在上述代码的基础上, 将其中分配的内存变量转化为虚拟寄存器,并在合适的地方插入 Phi 指令。 109 | 这一解决方案也被称为 mem2reg 技术。 110 | 111 | 在本次选做实验中,我们希望同学实现这一技术,针对局部的 int 类型变量生成符合 SSA 要求的中间表示代码。 112 | 113 | 你需要: 114 | 115 | 1. 改进你的编译器,支持上面提到的 mem2reg 技术。除完成代码实现外,还需要: 116 | - 为代码添加合理的注释以便批阅。 117 | - 设计合适的测例并展示生成的中间表示,以证明你的实现是正确的。 118 | 2. 完成实验报告(具体要求请看实验指导书的首页)。实验报告中需要包括: 119 | - 你的学号姓名 120 | - 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 121 | - 详细说明你的代码的运行逻辑 122 | - 你设计的测例和对应生成的中间代码,并进行适当的分析。 123 | - 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 124 | - 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 125 | -------------------------------------------------------------------------------- /docs/step2/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step2:一元运算符 2 | step2 中,我们要给整数常量增加一元运算:取负 `-`、按位取反 `~` 以及逻辑非 `!`。 3 | 4 | 语法上,我们需要修改 `expression` 的定义,从 `expression : Integer` 变成: 5 | 6 |
7 | expression 8 | : unary 9 | 10 | unary 11 | : Integer 12 | | ('-'|'!'|'~') unary 13 |
14 | 15 | 三个操作的语义和 C 以及常识相同,例如 `~0 == -1`,`!!2 == 1`。 16 | 稍微一提,关于按位取反,我们使用补码存储 int;关于逻辑非,只有 0 表示逻辑假,其他的 int 都是逻辑真。 17 | 18 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/.pdf`,比如 stage 1 的实验报告需要放在 `stage-1` 这个 branch 下的 `./reports/stage-1.pdf`。整个 stage 1 只需要提交一份报告,你不需要单独为 step 2 准备报告。**stage 1 的报告还需要额外包含 step 1 的思考题**。 19 | 20 | 你需要: 21 | 1. 改进你的编译器,支持本节引入的新特性,通过相关测试。 22 | 2. 完成实验报告(具体要求请看实验指导书的首页)。实验报告中需要包括: 23 | * 你的学号姓名 24 | * 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 25 | * 指导书上的思考题 26 | * 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 27 | * 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 28 | 29 | ## 如何检查我是否通过自动测试(CI) 30 | 31 | 在 `git.tsinghua` 上打开你的项目,在界面的右侧,Clone 按钮的下方,`commit id` 的左侧,可以看到一个画圈的 `×` 或者 `√` 的图标,代表当前 commit 是否通过 CI 测试。 32 | 33 | 如果你希望获取详细测试输出,可以点击这个画圈的 `×` 或者 `√` 的图标,或者在网页左侧选择 `CI/CD` 一栏的 `Jobs`,然后选择希望查看的评测结果即可。如果测试输出无法显示,可以点击输出框右上角四个按钮中最左边的一个,或者在当前地址(如`.../jobs/123456`)的后面加上`/raw`(如`.../jobs/123456/raw`),即可获取测试输出。 -------------------------------------------------------------------------------- /docs/step2/spec.md: -------------------------------------------------------------------------------- 1 | # 规范 2 | 每个步骤结尾的 **规范** 一节都会对这个步骤中的新特性给出规范,方便大家查阅。 3 | 4 | # step2 语法规范 5 | 灰色部分表示相对上一节的修改。 6 | 7 |
 8 | 
 9 | program
10 |     : function
11 | 
12 | function
13 |     : type Identifier '(' ')' '{' statement '}'
14 | 
15 | type
16 |     : 'int'
17 | 
18 | statement
19 |     : 'return' expression ';'
20 | 
21 | 
expression 22 | : unary 23 | 24 | unary 25 | : Integer 26 | | ('-'|'!'|'~') unary 27 |
28 | 29 | # step2 语义规范 30 | **2.1** 运算符 `-` 的结果是其操作数的相反数。 31 | 32 | **2.2** 运算符 `~` 的结果是其操作数的二进制反码(也就是说,结果中的每一个二进制位是 1 当且仅当其对应的二进制位是 0)。 33 | 34 | **2.3** 当操作数不等于 0 时,逻辑非运算符 `!` 的结果为 0;当操作数等于 0 时,其结果为 1。 35 | 36 | **2.4** MiniDecaf 中,负数字面量不被整体作为一个 token。它被看成是一个取负符号、后面是它的绝对值。 37 | 所以我们无法用字面量表示 `-2147483648`,但可以写成 `-2147483647-1`(待我们加上四则运算后)。 38 | 39 | **2.5** 整数运算越界是**未定义行为**(undefined behavior),即对程序的行为无任何限制。 40 | > 例如 `-(-2147483647-1)` 是未定义行为。这一条规则对于后续 step 引入的运算符也都适用。 41 | > 42 | > 对于含有未定义行为的 C/C++ 程序,在启用优化选项编译时,编译器可能产生意料之外的结果。 43 | -------------------------------------------------------------------------------- /docs/step3/example.md: -------------------------------------------------------------------------------- 1 | # step3 实验指导 2 | 3 | 本实验指导使用的例子为: 4 | 5 | ```C 6 | 1+3 7 | ``` 8 | 9 | ## 词法语法分析 10 | 在 step3 中,我们引入了算术运算,因此需要引入新的抽象语法树节点: 11 | 12 | | 节点 | 成员 | 含义 | 13 | | --- | --- | --- | 14 | | `Binary` | 左操作数 `lhs`,右操作数 `rhs`,运算类型 `op` | 二元运算 | 15 | 16 | > 对有兴趣的同学:虽然 `-2` 和 `2-3` 里面的 `-` 意义不同,但 lexer 不知道这点(parser 才知道),所以它们都会用同样的 token kind `-` 表示。 17 | > 但有时,可能需要后续阶段告诉 lexer(或 parser)一些信息,最经典的例子是 [“typedef-name identifier problem”](https://en.wikipedia.org/wiki/Lexer_hack)。 18 | 19 | ## 语义分析 20 | 21 | 同 Step2。 22 | 23 | ## 中间代码生成 24 | 与一元操作类似,针对加法,我们需要设计一条中间代码指令来表示它,给出的参考定义如下: 25 | 26 | > 请注意,TAC 指令的名称只要在你的实现中是一致的即可,并不一定要和文档一致。 27 | 28 | | 指令 | 参数 | 作用 | 29 | | ----- | ------- | -------------- | 30 | | `ADD` | `T0,T1` | 将两个参数相加 | 31 | 32 | 因此,测例可以翻译成如下的中间代码: 33 | 34 | ```assembly 35 | _T0 = 1 36 | _T1 = 3 37 | _T2 = ADD _T0, _T1 38 | ``` 39 | 40 | ## 目标代码生成 41 | 42 | step3 目标代码生成步骤的关键点与 step2 相同,针对中间代码指令,选择合适的 RISC-V 指令来完成翻译工作。 43 | 44 | ```assembly 45 | li t0, 1 46 | li t1, 3 47 | add t2, t0, t1 48 | ``` 49 | 50 | # 思考题 51 | 52 | 1. 我们知道“除数为零的除法是未定义行为”,但是即使除法的右操作数不是 0,仍然可能存在未定义行为。请问这时除法的左操作数和右操作数分别是什么?请将这时除法的左操作数和右操作数填入下面的代码中,分别在你的电脑(请标明你的电脑的架构,比如 x86-64 或 ARM)中和 RISCV-32 的 qemu 模拟器中编译运行下面的代码,并给出运行结果。(编译时请不要开启任何编译优化) 53 | 54 | ```c 55 | #include 56 | 57 | int main() { 58 | int a = 左操作数; 59 | int b = 右操作数; 60 | printf("%d\n", a / b); 61 | return 0; 62 | } 63 | ``` 64 | 65 | # 总结 66 | 本步骤中其他运算符的实现逻辑和方法与加法类似,可以参考二元加法的实现方法设计实现其他二元运算符。 67 | -------------------------------------------------------------------------------- /docs/step3/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step3:加减乘除模 2 | step3 我们要增加的是:加 `+`、减 `-`、乘 `*`、整除 `/`、模 `%` 以及括号 `(` `)`。 3 | 4 | 语法上我们继续修改 `expression`,变成 5 | 6 |
7 | expression 8 | : additive 9 | 10 | additive 11 | : multiplicative 12 | | additive ('+'|'-') multiplicative 13 | 14 | multiplicative 15 | : unary 16 | | multiplicative ('*'|'/'|'%') unary 17 | 18 | unary 19 | : primary 20 | | ('-'|'~'|'!') unary 21 | 22 | primary 23 | : Integer 24 | | '(' expression ')' 25 |
26 | 27 | 新特性的语义、优先级、结合性和 C 以及常识相同,例如 `1+2*(4/2+1) == 7`。 28 | 29 | 我们这种表达式语法写法可能比较繁琐,但它有几个好处: 30 | 1. 和 [C17 标准草案](../../REFERENCE.md)保持一致 31 | 2. 把优先级和结合性信息直接编码入语法里,见[优先级和结合性](./precedence.md)一节。 32 | 33 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/.pdf`,比如 stage 1 的实验报告需要放在 `stage-1` 这个 branch 下的 `./reports/stage-1.pdf`。整个 stage 1 只需要提交一份报告,你不需要单独为 step 3 准备报告。**stage 1 的报告还需要额外包含 step 1 的思考题**。 34 | 35 | 你需要: 36 | 1. 改进你的编译器,支持本节引入的新特性,通过相关测试。 37 | 2. 完成实验报告(具体要求请看实验指导书的首页)。实验报告中需要包括: 38 | * 你的学号姓名 39 | * 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 40 | * 指导书上的思考题 41 | * 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 42 | * 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 43 | 44 | ## 如何检查我是否通过自动测试(CI) 45 | 46 | 在 `git.tsinghua` 上打开你的项目,在界面的右侧,Clone 按钮的下方,`commit id` 的左侧,可以看到一个画圈的 `×` 或者 `√` 的图标,代表当前 commit 是否通过 CI 测试。 47 | 48 | 如果你希望获取详细测试输出,可以点击这个画圈的 `×` 或者 `√` 的图标,或者在网页左侧选择 `CI/CD` 一栏的 `Jobs`,然后选择希望查看的评测结果即可。如果测试输出无法显示,可以点击输出框右上角四个按钮中最左边的一个,或者在当前地址(如`.../jobs/123456`)的后面加上`/raw`(如`.../jobs/123456/raw`),即可获取测试输出。 -------------------------------------------------------------------------------- /docs/step3/pics/ops: -------------------------------------------------------------------------------- 1 | 7Vtbk5owGP01eWwHiCA+quvuzvQ20+1M26dOhAh0I3FivPXXN2i4GdalTt0g+GRyknA5Jyf5wjcCOJ5vHxhahJ+ojwmwDH8L4B2wLNM0LPGTILsDYtuDAxCwyJedcuAp+oMlaEh0Ffl4WerIKSU8WpRBj8Yx9ngJQ4zRTbnbjJLyXRcowArw5CGiot8jn4cH1LWNHH/EURDy7IVlyxylnSWwDJFPNwUITgAcM0r5oTTfjjFJyEt5OYy7f6E1ezCGY15nwL3L198+05k7fNz8+tB7iMmXr++gvMwakZV8Y2A5RFxwNGWiFCSlxWoZij6pJEu+S9nBviBLVmMai58Ro6vYx8k9DVGjjIc0oDEiHyldCNAU4G/M+U5KjVacCijkcyJb8TbiPwrln4Xy3VZed1/ZpZWYs92PYiUbk1TyQftaOoqgKSYj5D0H+0ceU0JZ/hqHV03e70W6JbSkK+bhExxLB3DEAsxP9DPNbFYIO2E6x+JxxUCGCeLRuvwgSM7rIOuXSy8KUv1/mAmWOhEmFnANMHTywrH+ZbE3YcTx0wLt2diI9aAs7CwiJGUZWHA2m1meJ/AlZ/QZF1p8Z+rYTibCGjOOt6dlUFlLB8hJK5cfy5X1TW5mR0JhwcfQuBDNENY3HLz57Ry/mek+8arhoE7DmRVLbyscB5tmuV4Nyy1X05vbznIbrOs2W6vbKtbdNrjNspvmNrv+BmffLHeW5ey6lutrtVzFTGiD5WC/aZZzaliO4fnNbWe5rV/XbQOtbuu30229QcPcZg7aSbTTtKNy+uGsQHTFJ6jmE6sciGzdxKonz6skVol9tROrfkRTv+FcIa+Wbl7VSLJ/hbwqgaP2+aoGjtfIqxIiaOdVjcXUo+4V8qp9HXAVXq0r5FWJtHTPV6jyCiYOGBrAdcFkAIYmGKkhrSCAl9kssyaPYkWKJYRIFMSi6gnCsMBHCZ2Rh8hQNswj309uUylfWeD/sd8dy2ErcvQq5LAuJkfVCSOTwwajIRio60lb5YAVq86bypHerKNymL2m6VGVOOuOHtnW0Bg91FNPl/SAbtP0qEp1dUcP22iaHhX55w7p4TRuP6/KS3Un2j3ez7OzqTY9KtJXHdLjeD83B7r9oX4DumSi0HhvgzxVmGUHK1OFb5b0S+fkq0m/Q0ddSb9etw/ux6GX9qXMPnlUbL0ex6GX9qXMPnlUbL0ex6HXBf0hqvlfGfZthT+EwMlf -------------------------------------------------------------------------------- /docs/step3/precedence.md: -------------------------------------------------------------------------------- 1 | # 优先级和结合性 2 | 3 | 操作符有优先级和结合性的概念,在之前的编程经历中大家应该已经对这两个概念已经有了直观的理解,这里用例子进一步解释一下: 4 | 5 | 1. 优先级是两个操作符之间的关系,例如`*`的优先级比`+`高,所以表达式`1 + 2 * 3`应该解析成语法树`add (1 mul (2 3))`(前序表示),不能解析成`mul (add (1 2) 3)` 6 | 2. 结合性是一个操作符的性质,例如`-`是左结合的,所以表达式`1 - 2 - 3`应该解析成`sub (sub (1 2) 3)`,不能解析成`sub (1 sub (2 3))` 7 | 8 | 我们给出的语法规范已经表示了这样的性质,因此理论上我们不需要再额外定义操作符的优先级和结合性了。你可以自己试试,按照本步给出的语法规则,上面的两个表达式确实只能解析成我们期望的结果。 9 | 10 | 但是有一个问题:这样的语法规范虽然是正确的,也确实可以直接用来实现语法分析器了,但并不符合直观:我们一开始学习C或者别的编程语言的时候,讲的就是一个二元表达式由两个子表达式和中间的操作符组成,并且操作符有优先级和结合性。也就是这样的: 11 | 12 |
13 | expression 14 | : expression ('+'|'-') expression 15 | | expression ('*'|'/'|'%') expression 16 | | ('-'|'~'|'!') expression 17 | | Integer 18 | | '(' expression ')' 19 |
20 | 21 | 当然,它是有歧义的,你也可以自己试试,如果只有这些产生式的话,上面的两个表达式都可以解析成正确或者错误的结果。所以如果想基于这个规范来实现语法分析器,就必须告诉语法分析工具这些操作符的优先级和结合性是什么。 22 | 23 | 之后每一步给出的语法都是没有歧义,本身就能体现优先级和结合性的。如果你确实想借助优先级和结合性来实现,需要两个步骤: 24 | 25 | 1. 把我们给出的语法规范转化成类似上面这样“更模糊”,有歧义的语法规范。我们相信这个方向的转化应该是容易的。 26 | 2. 指定每个操作符的优先级和结合性。可以参考[https://en.cppreference.com/w/c/language/operator_precedence](https://en.cppreference.com/w/c/language/operator_precedence),它给出了C语言操作符的优先级和结合性,因为我们的MiniDecaf语言是C语言的一个子集,所以这张表格也足够我们的语言使用了。 27 | -------------------------------------------------------------------------------- /docs/step3/spec.md: -------------------------------------------------------------------------------- 1 | # 规范 2 | 每个步骤结尾的 **规范** 一节都会对这个步骤中的新特性给出规范,方便大家查阅。 3 | 4 | # step3 语法规范 5 | 灰色部分表示相对上一节的修改。 6 | 7 |
 8 | 
 9 | program
10 |     : function
11 | 
12 | function
13 |     : type Identifier '(' ')' '{' statement '}'
14 | 
15 | type
16 |     : 'int'
17 | 
18 | statement
19 |     : 'return' expression ';'
20 | 
21 | 
expression 22 | : additive 23 | 24 | additive 25 | : multiplicative 26 | | additive ('+'|'-') multiplicative 27 | 28 | multiplicative 29 | : unary 30 | | multiplicative ('*'|'/'|'%') unary 31 | 32 | unary 33 | : primary 34 | | ('-'|'~'|'!') unary 35 | 36 | primary 37 | : Integer 38 | | '(' expression ')' 39 |
40 | 41 | # step3 语义规范 42 | 43 | **3.1** 二元操作符 `*` 的结果是操作数的乘积。 44 | 45 | **3.2** 二元操作符 `/` 的结果是第一个操作数除以第二个操作数所得的商的整数部分(即所谓“向零取整”),二元操作符 `%` 的结果是第一个操作数除以第二个操作数所得的余数。在两种操作中,如果第二个操作数为 0,则其行为都是未定义的。当 `b` 不为 0 时,表达式 `(a/b)*b + a%b` 应该等于 `a`。 46 | 47 | **3.3** 二元操作符 `+` 的结果是操作数的和。 48 | 49 | **3.4** 二元操作符 `-` 的结果是第一个操作数减去第二个操作数所得的差。 50 | 51 | **3.5** 除非特别声明,子表达式求值顺序是**未规定行为**(unspecified behavior),即其行为可以是多种合法的可能性之一。也就是说,以任意顺序对子表达式求值都是合法的。 52 | 例如:执行 `int a=0; (a=1)+(a=a+1);` 之后 a 的值是未规定的(待我们加上变量和赋值运算符后,这个问题才会产生真正切实的影响)。 53 | -------------------------------------------------------------------------------- /docs/step4/example.md: -------------------------------------------------------------------------------- 1 | # step4 实验指导 2 | 3 | 本实验指导使用的例子为: 4 | 5 | ```C 6 | 1<2 7 | ``` 8 | 9 | ## 词法语法分析 10 | 11 | 本 step 中引入的运算均为二元运算,在 step3 中引入的二元运算节点中进行修改即可。 12 | 13 | ## 语义分析 14 | 15 | 同 Step2。 16 | 17 | ## 中间代码生成 18 | 针对小于符号,我们显然需要设计一条中间代码指令来表示它,给出的参考定义如下: 19 | 20 | > 请注意,TAC 指令的名称只要在你的实现中是一致的即可,并不一定要和文档一致。 21 | 22 | | 指令 | 参数 | 含义 | 23 | | --- | --- | --- | 24 | | `LT` | `T0,T1` | 给出 `T0 需要特别注意的是,在 C 语言中,逻辑运算符 || 和 && 有短路现象,我们的实现中不要求大家考虑它们的短路性质。 27 | 28 | 因此,测例可以翻译成如下的中间代码: 29 | 30 | ```assembly 31 | _T0 = 1 32 | _T1 = 2 33 | _T2 = LT _T0, _T1 34 | ``` 35 | 36 | ## 目标代码生成 37 | 38 | step4 目标代码生成步骤的关键点与 step3 相同,针对中间代码指令,选择合适的 RISC-V 指令来完成翻译工作。 39 | 40 | ```assembly 41 | li t0, 1 42 | li t1, 2 43 | slt t2, t0, t1 44 | ``` 45 | 46 | 逻辑表达式会麻烦一点,因为 gcc 可能会用跳转来实现`&&`和`||`,比较难以理解,所以下面直接给出 `land` 和 `lor` 对应的不使用跳转的汇编。 47 | 48 | | IR | 汇编 | 49 | | --- | --- | 50 | | `lor` | `or t3,t1,t2 ; snez t3,t3` | 51 | | `land` | `snez d, s1; sub d, zero, d; and d, d, s2; snez d, d;` | 52 | 53 | > 注意 RISC-V 汇编中的 `and` 和 `or` 指令都是位运算指令,不是逻辑运算指令。 54 | 55 | # 思考题 56 | 57 | 1. 在 MiniDecaf 中,我们对于短路求值未做要求,但在包括 C 语言的大多数流行的语言中,短路求值都是被支持的。为何这一特性广受欢迎?你认为短路求值这一特性会给程序员带来怎样的好处? 58 | 59 | # 总结 60 | 本步骤中其他运算符的实现逻辑和方法与小于符号类似,可以参考小于符号的实现方法设计实现其他逻辑运算符。 61 | 62 | 恭喜你!到目前为止,你已经成功实现了一个基于 MiniDecaf 语言的计算器,可以完成基本的数学运算和逻辑比较运算了,成就感满满!然而,目前你的计算器还只能支持常量计算,这大大降低了计算器的使用体验,因此,在下一个 Stage,我们将一起实现对变量的支持。无论如何,当前的任务已经完成,好好休息一下吧☕️ -------------------------------------------------------------------------------- /docs/step4/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step4:比较和逻辑表达式 2 | step4 我们要增加的是: 3 | 4 | 1. 比较大小和相等的二元操作:`<`、`<=`、`>=`, `>`, `==`, `!=` 5 |
6 | equality 7 | : relational 8 | | equality ('=='|'!=') relational 9 | 10 | relational 11 | : additive 12 | | relational ('<'|'>'|'<='|'>=') additive
13 | 14 | 2. 逻辑与 `&&`、逻辑或 `||` 15 |
16 | expression 17 | : logical_or 18 | 19 | logical_or 20 | : logical_and 21 | | logical_or '||' logical_and 22 | 23 | logical_and 24 | : equality 25 | | logical_and '&&' equality
26 | 27 | 新特性的语义、优先级、结合性和 C 以及常识相同,例如 `1<3 == 2<3 && 5>=2` 是逻辑真(int 为 `1`)。 28 | 但特别注意,C 中逻辑运算符 `||` 和 `&&` 有短路现象,我们不要求。 29 | 30 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/.pdf`,比如 stage 1 的实验报告需要放在 `stage-1` 这个 branch 下的 `./reports/stage-1.pdf`。整个 stage 1 只需要提交一份报告,你不需要单独为 step 4 准备报告。**stage 1 的报告还需要额外包含 step 1 的思考题**。 31 | 32 | 你需要: 33 | 1. 改进你的编译器,支持本节引入的新特性,通过相关测试。 34 | 2. 完成实验报告(具体要求请看实验指导书的首页)。实验报告中需要包括: 35 | * 你的学号姓名 36 | * 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 37 | * 指导书上的思考题 38 | * 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 39 | * 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 40 | 41 | ## 如何检查我是否通过自动测试(CI) 42 | 43 | 在 `git.tsinghua` 上打开你的项目,在界面的右侧,Clone 按钮的下方,`commit id` 的左侧,可以看到一个画圈的 `×` 或者 `√` 的图标,代表当前 commit 是否通过 CI 测试。 44 | 45 | 如果你希望获取详细测试输出,可以点击这个画圈的 `×` 或者 `√` 的图标,或者在网页左侧选择 `CI/CD` 一栏的 `Jobs`,然后选择希望查看的评测结果即可。如果测试输出无法显示,可以点击输出框右上角四个按钮中最左边的一个,或者在当前地址(如`.../jobs/123456`)的后面加上`/raw`(如`.../jobs/123456/raw`),即可获取测试输出。 -------------------------------------------------------------------------------- /docs/step4/pics/README.md: -------------------------------------------------------------------------------- 1 | some pics 2 | -------------------------------------------------------------------------------- /docs/step4/spec.md: -------------------------------------------------------------------------------- 1 | # 规范 2 | 每个步骤结尾的 **规范** 一节都会对这个步骤中的新特性给出规范,方便大家查阅。 3 | 4 | # step4 语法规范 5 | 灰色部分表示相对上一节的修改。 6 | 7 |
 8 | 
 9 | program
10 |     : function
11 | 
12 | function
13 |     : type Identifier '(' ')' '{' statement '}'
14 | 
15 | type
16 |     : 'int'
17 | 
18 | statement
19 |     : 'return' expression ';'
20 | 
21 | 
expression 22 | : logical_or 23 | 24 | logical_or 25 | : logical_and 26 | | logical_or '||' logical_and 27 | 28 | logical_and 29 | : equality 30 | | logical_and '&&' equality 31 | 32 | equality 33 | : relational 34 | | equality ('=='|'!=') relational 35 | 36 | relational 37 | : additive 38 | | relational ('<'|'>'|'<='|'>=') additive 39 |
40 | additive 41 | : multiplicative 42 | | additive ('+'|'-') multiplicative 43 | 44 | multiplicative 45 | : unary 46 | | multiplicative ('*'|'/'|'%') unary 47 | 48 | unary 49 | : primary 50 | | ('-'|'~'|'!') unary 51 | 52 | primary 53 | : Integer 54 | | '(' expression ')' 55 |
56 | 57 | 58 | # step4 语义规范 59 | 60 | **4.1** 关系操作符 `<`(小于)、`>`(大于)、`<=`(小于等于)和`>=`(大于等于)的结果取决于两个操作数是否满足它们所指定的关系,当满足时结果为 1,当不满足时结果为 0。 61 | > 关系操作符可能导致表达式的含义与数学文献中常见的含义不同,例如 `0<1<2` 的含义与 `(0<1)<2` 相同,即“如果 0 小于 1,那么判断是否有 1 小于 2,否则判断是否有 0 小于 2”。 62 | 63 | **4.2** 判等操作符 `==`(等于)和 `!=`(不等于)类似于关系操作符,结果取决于两个操作数是否满足它们所指定的关系,当满足时结果为 1,当不满足时结果为 0。但判等操作符的优先级比关系操作符更低。对于任意一对操作数,这两个操作符中有且仅有一个结果为 1。 64 | > 其优先级的设定会导致其含义在某些时候可能会反直观,例如,`0 < 1 == 2 < 3` 的运算结果为 1。 65 | 66 | **4.3** 当操作数都非 0 时,逻辑与操作符 `&&` 的结果为 1;否则其结果为 0。 67 | 68 | **4.4** 当操作数有一个非 0 时,逻辑或操作符 `||` 的结果为 1;否则其结果为 0。 69 | 70 | **4.5** 逻辑操作符 `||` 和 `&&` 依然遵循语义规范 3.5,即其操作数的求值顺序是未指定行为。 71 | 换言之,我们不对逻辑表达式的短路求值做要求,可以将操作数两个以任意顺序计算出,再计算逻辑操作的结果。 72 | 73 | -------------------------------------------------------------------------------- /docs/step5/example.md: -------------------------------------------------------------------------------- 1 | # step5 实验指导 2 | 3 | 本实验指导使用的例子为: 4 | 5 | ```C 6 | int main() { 7 | int x = 2024; 8 | return x; 9 | } 10 | ``` 11 | 12 | ## 词法语法分析 13 | 针对局部变量定义和赋值操作,我们需要设计 AST 节点来表示它,给出的参考定义如下(框架中已经提供): 14 | 15 | | 节点 | 成员 | 含义 | 16 | | --- | --- | --- | 17 | | `TInt` | 无 | 整型 | 18 | | `Identifier` | 名称 `value` | 标识符(用于表示变量名) | 19 | | `Assignment` | 同 `Binary` | 赋值运算 | 20 | | `Declaration` | 类型 `var_t`,标识符 `ident`,初始表达式 `init_expr` | 变量声明 | 21 | 22 | 请注意,赋值操作是一种特殊的**二元运算**,因此可以将它合并到 `Binary` 节点里,也可以单独设置一类节点继承 `Binary` 类来处理它。 23 | 24 | ## 语义分析 25 | 26 | 从本节开始,我们需要在语义分析阶段对局部变量的规范进行检查。具体来说,我们需要名为符号表的数据结构。符号表的实现已经在框架中给出。因此,你只需要修改语义分析部分的代码,在必要时调用符号表的接口即可。 27 | 28 | 在符号表构建过程中,我们要按照语句顺序,逐一访问所有变量定义声明。在访问变量声明时,我们需要为该变量赋予一个变量符号,并将它存入符号表中。由于变量不能重复声明,在定义变量符号前需要在符号表中检查是否有同名符号。 29 | 30 | 类似地,在访问表达式时,如果遇到变量的使用,我们也需要在符号表中检查,避免使用未声明的变量。例如,如果我们将测例修改为: 31 | 32 | ```C 33 | int main() { 34 | int x = 2024; 35 | return x + y; 36 | } 37 | ``` 38 | 39 | 那么在扫描到加法操作的 AST 结点时,会依次检查该操作的两个操作数 x 和 y。这两个操作数均为变量标识符,因此我们需要到符号表中搜索 x 和 y 对应的符号。符号 x 可以在符号表中找到(我们在扫描 `int x = 2024;` 这条语句后已经为其定义),而 y 无法找到,因此编译器需要在扫描到 y 对应的结点时报错。 40 | 41 | 符号表总是和作用域相关的。例如,在 C 语言中,我们可以在全局作用域中定义名为 "a" 的全局变量,同时在 main 函数中定义名为 "a" 的局部变量,这并不产生冲突。不过由于本节还无需支持全局变量和块语句,同学们不用考虑这一点,只考虑 main 函数作用域对应的单张符号表即可。 42 | 43 | 此外,在本节中,我们引入了赋值操作。赋值可以看作一种特殊的二元运算,但需要注意,赋值号左侧必须为一个**左值**。具体来说,同学们需要检查赋值号左侧只能是变量名标识符。在 step11 中,我们会将左值的范围进一步包括数组元素。 44 | 45 | 对应到框架代码上: 46 | 47 | `frontend/symbol` 目录下为符号的实现。其中 `symbol.py` 为符号类的基类,`varsymbol.py` 为变量符号。在本节中,同学们只需要考虑变量符号即可。 48 | 49 | `frontend/scope` 目录下为符号表的实现。其中 `scope.py` 为作用域类,在本节中由于只有一个局部作用域,因此无需考虑作用域栈。同学们只需要新建一个 Scope 对象,用以维护 main 函数中所有出现过的变量符号即可。 50 | 51 | ## 中间代码生成 52 | 53 | 我们首先来看本节指导用例所对应的中间代码: 54 | 55 | ```assembly 56 | main: 57 | _T1 = 2024 58 | _T0 = _T1 59 | return _T0 60 | ``` 61 | 62 | 针对赋值操作,我们显然需要设计一条中间代码指令来表示它,给出的参考定义如下: 63 | 64 | > 请注意,TAC 指令的名称只要在你的实现中是一致的即可,并不一定要和文档一致。 65 | 66 | | 指令 | 参数 | 含义 | 67 | | --- | --- | --- | 68 | | `ASSIGN` | `T0,T1` | 临时变量的赋值 | 69 | 70 | 从中间代码可以看出,尽管我们引入了变量的概念,但是在比较低级的中间代码上,数据的存储和传递仍然是基于虚拟寄存器进行的。由于 MiniDecaf 语言中的基本类型只有 int 型,而 TAC 里的临时变量也是 32 位整数,因此,我们可以把 MiniDecaf 局部变量和 TAC 临时变量对应起来。 71 | 72 | 在扫描到 `int x = 2024;` 这条语句时,中间代码先把立即数 2024 加载到临时变量 _T1 中,然后再把 _T1 的值赋给临时变量 _T0,此时 _T0 已经成为了变量 x 的“替身”。每次需要用到变量 x 的值时,我们都会去访问 _T0。例如,测例中直接用返回 _T0 代替了返回变量 x 的值。因此,为了在后续使用变量 x 时能快速找到 _T0 这个临时变量,在符号表中存储 x 这个符号时,应当为该符号设置一个成员,存储 x 对应的临时变量。每当在 AST 上扫描到一个变量标识符结点时,我们都直接调用该变量对应的临时变量作为结点的返回值。 73 | 74 | 请注意 `frontend/symbol/varsymbol.py` 中,变量符号的定义里有该变量对应的 TAC 临时变量成员。 75 | 76 | ## 目标代码生成 77 | 78 | 本节指导用例对应如下 RISC-V 汇编代码: 79 | 80 | ```assmbly 81 | .text 82 | .global main 83 | main: 84 | li t1, 2024 85 | mv t0, t1 # 我们使用 mv 指令来翻译中间表示里的 ASSIGN 指令 86 | mv a0, t0 87 | ret 88 | ``` 89 | 90 | ### 简单的启发式寄存器分配算法 91 | 92 | 在中间代码中,我们使用了虚拟寄存器来存储变量的值。如果所使用的虚拟寄存器的个数,超过了目标机器实际拥有的物理寄存器数目,将无法生成正确的目标代码。此时,需要采用**寄存器分配算法**,调度和分配数目有限的物理寄存器资源,保证所有**临时变量(虚拟寄存器或伪寄存器)**都有合适的物理寄存器与之对应。在程序执行的任何时刻,都需要保证不会出现寄存器分配冲突,即两个同时有效且将被引用的临时变量(虚拟寄存器)被分配到同一个物理寄存器中,寄存器分配冲突将造成程序运行结果的错误。然而,寄存器分配问题是**NP 完备问题**(可以从 3-SAT 问题归约),这意味着对于一个含有大量临时变量的程序,为了获得最优寄存器分配方案,编译器将耗费可观的计算时间用于寄存器分配。因此,考虑到执行效率问题,实际的编译器实现中一般采用启发式算法。 93 | 94 | 实验框架中所采用的启发式寄存器分配算法基于**活跃性分析**。为避免一次性介绍过多的知识,将在 Step6 详细介绍活跃性分析的相关理论。大家目前只需要了解,活跃性分析是为了求解每个临时变量是否会在程序某点之后被引用,如果被引用,这个临时变量就是活跃的。 95 | 基于活跃性分析的启发式寄存器分配算法的基本思路:针对每一条 TAC 指令(例如 _T2 = ADD _T1, _T0),对于每个源操作数对应的临时变量(本例中 _T1 和 _T0),我们检查该临时变量是否已经存放在物理寄存器中,如果不是,则分配一个物理寄存器,并从**栈帧**中把该临时变量加载到寄存器中;对于目标操作数对应的临时变量(本例中的 _T2),如果该临时变量没有对应的物理寄存器,则为其分配一个新的物理寄存器。寄存器分配过程中,将为临时变量和为该变量分配的物理寄存器之间建立一种关联关系。 96 | 在分配寄存器时,首先检查是否存在空闲的寄存器(即尚未跟任何临时变量建立关联关系的寄存器),有则选择该寄存器作为分配结果。否则,检查有没有这样的寄存器,其所关联的临时变量在当前位置已经不是活跃变量了,这说明该寄存器所保存的数据未来不会被用到,可以回收使用这个寄存器而不用担心引起数据错误。一种可能的情况是,所有寄存器所关联的变量都是活跃的,即不存在空闲的寄存器。此时,将把某个寄存器所关联的暂时不用的变量存到栈帧(内存的一部分)中,腾出这个寄存器,这也称为溢出(spill)到内存。所腾空的寄存器是随机选取的,因此,所采用的寄存器分配算法有些暴力,存在进一步优化空间。 97 | 98 | 在实验框架中已经给出寄存器分配算法的代码,集中在 `backend/reg/bruteregalloc.py` 中,主要有以下几个函数: 99 | 100 | 1. `accept`:根据每个函数的 DFG(数据流图)进行寄存器分配,寄存器分配结束后生成相应汇编代码。 101 | 2. `bind`:将一个 Temp(临时变量)与寄存器绑定。 102 | 3. `unbind`:将一个 Temp(临时变量)与相应寄存器解绑定。 103 | 4. `localAlloc`:根据活跃变量信息对一个 BasicBlock(基本块)内的指令进行寄存器分配。 104 | 5. `allocForLoc`:每一条指令进行寄存器分配。 105 | 6. `allocRegFor`:根据活跃变量信息决定为当前 Temp(临时变量)分配哪一个寄存器。 106 | 107 | ### 栈帧 108 | 109 | 上面的描述中提到,在分配寄存器的时候**从栈帧中加载数据**,以及**将暂时不用的变量存储到栈帧**中,接下来介绍栈帧的概念。 110 | 111 | 1. 栈帧的概念 112 | 113 | 在汇编语言课程学习中,大家应该已经接触到**栈帧**的概念,下面简单回顾一下。在程序执行过程中,每次调用和执行一个函数,都会在栈上开辟一块新的存储空间,这块存储空间就叫做“栈帧”。栈帧中存放了函数执行所需的各种数据,包括需要临时保存的局部变量、在栈上临时申请的存储空间(如数组,在 Step11 中介绍)、被调用者负责保存的寄存器等。栈帧是函数正确调用和执行的保证。 114 | 115 | > 需要注意的是,由于我们目前只支持一个 main 函数,直到 Step9 才会有多函数支持。所以现在关于栈帧的讨论,就只针对 main 函数的栈帧,并且集中于临时变量的存储和加载。 116 | 117 | 假设当前函数被某个函数调用,下图给出当前函数的栈帧。如图所示,当前函数的栈帧由被调用者负责保存的寄存器、保存的临时变量以及局部变量三个部分组成,fp 指向当前栈帧的栈底,sp 指向当前栈帧的栈顶,fp 和 sp 之间的部分就是当前函数的栈帧。当前实验步骤中,需要关注的是**临时变量保存区域**,正是在这个区域中,保存了为腾空物理寄存器而取出的临时变量(仍然活跃的临时变量)。值得一提的是,临时变量保存区域中还保存了基本块出口处仍活跃的临时变量(关于基本块的概念,将在 Step6 介绍,在当前的步骤不需要考虑)。 118 | 119 | ![ ](./pics/stack.png) 120 | 121 | 2. 栈帧的建立与销毁 122 | 123 | 栈帧是函数运行所需要的上下文的一部分,在进入函数的时候需要建立对应的栈帧,在退出函数的时候需要销毁其对应的栈帧。栈帧对于函数的运行非常重要。那么程序在运行的过程中如何建立和销毁栈帧呢?实际上,建立栈帧的操作是由编译器生成代码完成的。在每个函数的起始位置,由编译器生成的用于建立栈帧的那段汇编代码称为函数的 **prologue**。prologue 所做的事情包括:分配栈帧空间和保存相应寄存器的值。相应的,在每个函数的末尾,用于销毁栈帧的那段汇编代码称为函数的 **epilogue**。epilogue 所做的事情包括:设置返回地址,回收栈帧空间,以及从当前被调用函数过程返回 124 | 125 | 貌似创建和销毁栈帧是一个大工程?实际不然,确定栈帧只需要维护好两个寄存器,sp 和 fp,它们分别保存当前栈帧的栈顶地址和栈底地址。当新的函数被调用时,需要把旧栈帧的栈底地址(fp)保存起来,用旧栈帧的栈顶地址(sp)表示新栈帧的栈底地址(新fp)。不难看出,新老栈帧在栈内存中是连续的存储空间。此外,每个函数体中需要分配的局部变量以及需要保存的临时变量在编译过程中是可知的。因此,栈帧的大小在编译期可以计算得出,即存储寄存器的空间,临时变量存储空间与局部变量空间三者之和。在求得栈帧大小之后,可以通过修改栈顶指针(sp)的值来分配恰当的栈帧空间。 126 | 127 | 3. 一个例子 128 | 129 | ```C 130 | #include 131 | 132 | int calculate() { 133 | int a = 1; 134 | int b = 2; 135 | int c = 3; 136 | int d = 4; 137 | int e = 5; 138 | int result = a + b + c + d + e; 139 | return result; 140 | } 141 | 142 | int main() { 143 | int result = calculate(); 144 | printf("%d\n", result); 145 | return 0; 146 | } 147 | ``` 148 | 149 | 在这个示例中,我们在 calculate 函数内部声明了 5 个局部整数变量(a 到 e)。假设我们的处理器只有 4 个通用寄存器,在这种情况下,我们无法将 5 个局部变量都保存在寄存器中。因此,编译器需要在栈上分配空间来存储这些变量。以下是栈空间的变化过程: 150 | 151 | 1. main 函数调用 calculate 函数,将返回地址压入栈中。 152 | 2. calculate 函数执行 prologue,将 fp 的值保存到栈中,然后将 sp 的值赋给 fp,此时 fp 和 sp 的值相同,都指向栈顶。 153 | 3. calculate 函数分配栈帧空间,在这个例子中,假设 a 到 d 保存在寄存器中,e 保存在栈帧中,因此需要分配 4 字节的栈帧空间。sp 指向栈顶,因此 sp 的值减去 4,即可得到 e 的地址。 154 | 4. 计算完成后,calculate 函数执行 epilogue,将 fp 的值赋给 sp,并恢复 fp 的值,然后将返回地址弹出栈中,跳转到返回地址。 155 | 156 | # 思考题 157 | 158 | **请将你的整个stage-2作业放置在分支`stage-2`下,你可以通过`git checkout -b stage-2`创建一个新的分支并继承当前分支的修改。** 159 | 160 | 1. 我们假定当前栈帧的栈顶地址存储在 sp 寄存器中,请写出一段 **risc-v 汇编代码**,将栈帧空间扩大 16 字节。(提示1:栈帧由高地址向低地址延伸;提示2:risc-v 汇编中 addi reg0, reg1, <立即数> 表示将 reg1 的值加上立即数存储到 reg0 中。) 161 | 2. 有些语言允许在同一个作用域中多次定义同名的变量,例如这是一段合法的 Rust 代码(你不需要精确了解它的含义,大致理解即可): 162 | 163 | ```Rust 164 | fn main() { 165 | let a = 0; 166 | let a = f(a); 167 | let a = g(a); 168 | } 169 | ``` 170 | 171 | 其中`f(a)`中的`a`是上一行的`let a = 0;`定义的,`g(a)`中的`a`是上一行的`let a = f(a);`。 172 | 173 | 如果 MiniDecaf 也允许多次定义同名变量,并规定新的定义会覆盖之前的同名定义,请问在你的实现中,需要对定义变量和查找变量的逻辑做怎样的修改?(提示:如何区分一个作用域中**不同位置**的变量定义?) 174 | 175 | # 总结 176 | 177 | Step5 主要涉及的知识为符号表、寄存器分配和栈帧,对于大家来说有一定的跳跃性和挑战性,希望大家能够尽早开始。 178 | -------------------------------------------------------------------------------- /docs/step5/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step5:局部变量和赋值 2 | 这一步我们终于要增加变量了,包括: 3 | * 变量的声明 4 | * 变量的使用(读取/赋值) 5 | 6 | 此外,我们的 main 函数内部可以包含多条语句和声明了。 7 | 8 | 为了增加变量,我们需要确定:变量存放在哪里、如何访问变量。我们将借此引入 **栈帧** 的概念,并介绍它的布局。 9 | 10 | 语法上,step5 的改动如下: 11 |
12 | 
13 | 
function 14 | : type Identifier '(' ')' '{' statement* '}' 15 |
16 | statement 17 | : 'return' expression ';' 18 |
| expression? ';' 19 | | declaration 20 | 21 | declaration 22 | : type Identifier ('=' expression)? ';' 23 | 24 | expression 25 | : assignment 26 | 27 | assignment 28 | : logical_or 29 | | Identifier '=' expression 30 |
31 | 32 | primary 33 | : Integer 34 | | '(' expression ')' 35 |
| Identifier 36 |
37 | 38 | 我们要增加和变量相关的语义检查:变量不能重复声明,不能使用未声明的变量。 39 | 40 | **请将你的作业放置在分支`stage-2`下,你可以通过`git checkout -b stage-2`创建一个新的分支并继承当前分支的修改。** 41 | 42 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/.pdf`,比如 stage 2 的实验报告需要放在 `stage-2` 这个 branch 下的 `./reports/stage-2.pdf`。注意报告的标题是 `stage-2` 而不是 `step-5`。 43 | 44 | 你需要: 45 | 1. 改进你的编译器,支持本节引入的新特性,通过相关测试。 46 | 2. 完成实验报告(具体要求请看实验指导书的首页)。实验报告中需要包括: 47 | * 你的学号姓名 48 | * 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 49 | * 指导书上的思考题 50 | * 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 51 | * 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 52 | -------------------------------------------------------------------------------- /docs/step5/pics/sf.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/step5/pics/stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step5/pics/stack.png -------------------------------------------------------------------------------- /docs/step5/spec.md: -------------------------------------------------------------------------------- 1 | # 规范 2 | 每个步骤结尾的 **规范** 一节都会对这个步骤中的新特性给出规范,方便大家查阅。 3 | 4 | # step5 语法规范 5 | 灰色部分表示相对上一节的修改。 6 |
 7 | 
 8 | program
 9 |     : function
10 | 
11 | 
function 12 | : type Identifier '(' ')' '{' statement* '}' 13 |
14 | type 15 | : 'int' 16 | 17 | statement 18 | : 'return' expression ';' 19 |
| expression? ';' 20 | | declaration 21 | 22 | declaration 23 | : type Identifier ('=' expression)? ';' 24 | 25 | expression 26 | : assignment 27 | 28 | assignment 29 | : logical_or 30 | | Identifier '=' expression 31 |
32 | logical_or 33 | : logical_and 34 | | logical_or '||' logical_and 35 | 36 | logical_and 37 | : equality 38 | | logical_and '&&' equality 39 | 40 | equality 41 | : relational 42 | | equality ('=='|'!=') relational 43 | 44 | relational 45 | : additive 46 | | relational ('<'|'>'|'<='|'>=') additive 47 | 48 | additive 49 | : multiplicative 50 | | additive ('+'|'-') multiplicative 51 | 52 | multiplicative 53 | : unary 54 | | multiplicative ('*'|'/'|'%') unary 55 | 56 | unary 57 | : primary 58 | | ('-'|'~'|'!') unary 59 | 60 | primary 61 | : Integer 62 | | '(' expression ')' 63 |
| Identifier 64 |
65 | 66 | # step5 语义规范 67 | 68 | **5.1** 每一条变量声明(定义)指定了对标识符的解释和属性。当变量被定义时,应当有一块存储空间为这个变量所保留。当变量声明之后,若与这个变量的名称相同的标识符作为操作数(operand)出现在一个表达式中时,其就应被指派(designate)为这个变量。 69 | 70 | **5.2** 变量的初始化表达式指定了变量的初始值。 71 | 72 | **5.3** 同一个标识符应只能作为至多一个变量的名字,即是说,不允许声明重名变量。 73 | 74 | **5.4** 对未声明的变量的使用是错误。 75 | 76 | **5.5** 没有被初始化的(局部)变量的值是不确定的。 77 | > 在初始化表达式中,正在被初始化的变量已被声明,但其值尚未被初始化。 78 | > 例如,`int a = a + 1;`,这样一条声明在语义上等价于 `int a; a = a + 1;` 79 | 80 | **5.6** 局部变量的名字可以为 `main`。 81 | 82 | **5.7** 赋值运算 `=` 的左操作数必须是一个**可修改的左值**(modifiable lvalue)。**左值**(lvalue)即一个会被指派为某个变量的表达式,如在 `int a; a = 1;` 中,`a` 即是一个会被指派为变量的表达式。左值**可修改**是指被指派的变量不能是一个左值数组。 83 | > 就 step5 来说,这一点其实几乎已经被语法保证,因为其 `=` 的左边只能是一个标识符,只需再要求其是一个已经声明过的变量的名字即可。 84 | 85 | **5.8** 在赋值运算(`=`)中,右操作数的值会被存在左操作数所指派的变量中。 86 | 87 | **5.9** 赋值表达式的结果,为赋值运算完成后左操作数所指派的变量的值,但这个结果本身并非左值。 88 | 89 | **5.10** 一个函数中可以有任意多条 `return` 语句。 90 | 91 | **5.11** 当 `main` 函数执行至 `}` 时,应终止执行并返回 0。 92 | -------------------------------------------------------------------------------- /docs/step6/dataflow.md: -------------------------------------------------------------------------------- 1 | # 数据流分析 2 | 3 | 编译优化的基础是**数据流分析**。 4 | 5 | 基本块(basic block)和控制流图(control-flow graph)是用于进行上述分析的数据结构。 6 | 7 | 以下讲述数据流分析的内容中,所有的 CondBranch 指令为条件跳转指令,Branch 指令为跳转指令。 8 | 9 | ## 基本块 10 | 11 | 基本块是指一段这样的代码序列: 12 | 13 | 1. 除出口语句外基本块中不含任何的 Branch、Beqz(条件为假时跳转)、Bnez(条件为真时跳转)或者 Return 等跳转语句(但可以包含 Call 语句)。 14 | 15 | 2. 除入口语句外基本块中不含任何的 Label 标记,即不能跳转到基本块中间。 16 | 17 | 3. 在满足前两条的前提下含有最多的连续语句,即基本块的头尾再纳入一条语句将会违反上面两条规则。 18 | 19 | 下面的例子中,代码中不同的基本块被标以不同的颜色: 20 | 21 | ![](./pics/dataflow.png) 22 | 23 | 也就是说,基本块内的代码执行过程总是从基本块入口开始,到基本块出口结束的,中间不会跳到别的地方或者从别的地方跳进来。 24 | 25 | ## 控制流图 26 | 27 | 控制流图是一个有向图:它以基本块作为结点,如果一个基本块 A 执行完之后,有可能跳转到另一个基本块 B,则图中包含从 A 对应结点到 B 对应结点的有向边。对于以 Branch 语句或者任何非跳转语句结尾的基本块,其后继只有一个结点;对于以 CondBranch 语句结尾的基本块,其后继含有两个结点,分别对应跳转条件为真和假的情况。不难想像,控制流图的有向边组成的每一个环路都对应着程序中的一个循环结构。由于该图给出程序控制流的各种可能执行路径,因此也称为控制流图。 28 | 29 | 为进行编译优化,建立控制流图是必不可少的一步。已知一个操作序列,如何根据这个操作序列建立对应的控制流图呢?通常分为两步进行: 30 | 31 | 1. 划分基本块。 32 | 33 | 2. 建立基本块之间的连接关系。 34 | 35 | 基本块的划分算法比较简单:从头到尾扫描操作序列,当遇到以下情况时结束当前基本块,并开始一个新的基本块建立过程: 36 | 37 | 1. 当遇到一个 Label 标记而且存在跳转语句跳转到这个行号时。 38 | 39 | 2. 当遇到 Branch、CondBranch 或者 Return 等跳转语句时。 40 | 41 | 整个操作序列扫描完毕后,我们就成功建立了所有基本块。 42 | 43 | 在划分好基本块之后,需要从头到尾依次扫描所有的基本块建立控制流图: 44 | 45 | 1. 如果当前基本块以 Branch 结尾,则在当前基本块与所跳转到的目标基本块之间加入一条有向边。 46 | 47 | 2. 如果当前基本块以 CondBranch 结尾,则在当前基本块和跳转条件成立与不成立的目标基本块之间分别加入一条有向边(共 2 条边)。 48 | 49 | 3. 如果当前基本块以 Return 结尾,则不需要加入新的边。 50 | 51 | 在所有的基本块都扫描完毕后,即建立了控制流图。基于控制流图,可以进行控制流分析。 52 | 53 | 上面例子对应的控制流图如下: 54 | 55 | ![](./pics/flowgraph.png) 56 | 57 | ## 活跃变量和活跃变量方程 58 | 59 | 从编译器中端出来的中间代码中,我们对 TAC 中使用的临时变量的个数并没有做任何限制。但是在实际机器中,物理寄存器的数量是有限的。 60 | 61 | 因此我们需要想办法把这些无限多的临时变量“塞”到有限个物理寄存器里面:如果两个临时变量不会在同一条指令中被用到,那么我们可以让这两个临时变量使用同一个物理寄存器(把一部分当前指令用不到的临时变量保存到栈上)。 62 | 63 | 根据这样的原则,大多数的临时变量都可以用有限的几个物理寄存器对应起来,而“塞不下”的那些临时变量,则可以暂时保存到内存里面(因为访问内存的时间比访问寄存器的时间多得多,因此临时变量应尽可能一直存放在物理寄存器中,尽量不要 spill 到栈上)。 64 | 65 | 由于一个物理寄存器在确定的时刻只能容纳一个临时变量,因此为了把若干个变量塞到同一个物理寄存器里面,我们需要知道各个临时变量分别在哪条指令以后不会再被用到(以便腾出当前临时变量占用的物理寄存器给别的临时变量)。此时我们需要用到活性分析(liveness analysis),或者称为“活跃变量分析”。 66 | 67 | 一个临时变量在某个执行点是活的(也叫“活跃”、live),是指该临时变量在该执行点处具有的值会在这个执行点以后被用到,换句话说,就是在该执行点到给这个临时变量重新赋值的执行点之间存在着使用到这个临时变量的语句。活性分析是指分析每一个临时变量在程序的每一个执行点处的活跃情况,通常是通过计算出每个执行点处的活跃变量集合来完成。 68 | 69 | 下面代码中每行语句右边都给出了执行完该语句后的活跃变量集合: 70 | 71 | | TAC 代码 | 活跃变量集合 | 72 | | ----------------- | --------------- | 73 | | `_T0 = 4` | {_T0} | 74 | | `_T1 = 3` | {_T0, _T1} | 75 | | `_T2 = _T0 * _T1` | {_T0} | 76 | | `_T3 = _T0 * _T0` | {_T0, _T3} | 77 | | `_T2 = _T3 * _T3` | {_T0, _T2, _T3} | 78 | | `_T2 = _T0 * _T2` | {_T2, _T3} | 79 | | `_T1 = _T2 * _T3` | {_T1} | 80 | | `return _T1` | 空集 | 81 | 82 | 一般来说,活性分析是通过求解活跃变量方程来完成的。为了介绍活跃变量方程的概念, 我们需要先引入下面四种针对基本块的集合: 83 | 84 | 1. Def 集合:一个基本块的 Def 集合是在这个基本块内被定值的所有变量。所谓的定值 (definition),可以理解为给变量赋值,例如加法语句给目标变量定值等(注意:Store 语句不给任何变量定值,Load 语句则会给对应变量定值)。 85 | 86 | 2. LiveUse 集合:一个基本块的 LiveUse 集合是在这个基本块中所有在定值前就被引用过的变量,包括了在这个基本块中被引用到但是没有被定值的那些变量。 87 | 88 | 3. LiveIn 集合:在进入基本块入口之前必须是活跃的那些变量。 89 | 90 | 4. LiveOut 集合:在离开基本块出口的时候是活跃的那些变量。 91 | 92 | 其中 Def 和 LiveUse 是基本块本身的属性,对每个基本块从后往前遍历基本块内的指令便可以求出。 93 | 94 | 有了基本块的这四个集合的概念,我们给出控制流图中每个基本块满足的活跃变量方程: 95 | 96 | ![](./pics/formula.png) 97 | 98 | 该方程说的是一个基本块的 LiveOut 集合是其所有后继基本块的 LiveIn 集合的并集,而且 LiveIn 集合是 LiveUse 集合的变量加上 LiveOut 集合中去掉 Def 集合以后的部分。 99 | 100 | 这个方程的直观意义是: 101 | 102 | 1. 一个基本块的任何一个后继基本块入口处活跃的变量在这个基本块的出口必须也是活跃的。 103 | 104 | 2. 在一个基本块入口处需要活跃的变量是在该基本块中没有定值就被使用的变量,以及在基本块出口处活跃但是基本块中没有定值过的变量(因为它们的初值必定是在进入基本 块之前就要具有的了)。 105 | 106 | 根据这个方程,我们可以通过迭代更新的办法求出每个基本块的 LiveIn、LiveOut 集合,以下是求解的伪代码: 107 | 108 | ``` 109 | for i <- 1 to N do compute Def[B_i] and LiveUse[B_i]; 110 | for i <- 1 to N do LiveIn[B_i] <- phi ; 111 | changed <- true; 112 | while (changed) do { 113 | changed <- false; 114 | for i <- N downto 1 do { 115 | LiveOut[B_i] <- Union (LiveIn[s]) where s belongs to succ(B_i) ; 116 | NewLiveIn <- Union (LiveUse[B_i], (LiveOut[B_i] – Def[B_i])); 117 | if (LiveIn[B_i] != NewLiveIn) then { 118 | changed <- true; 119 | LiveIn[B_i] <- NewLiveIn; 120 | } 121 | } 122 | } 123 | ``` 124 | 125 | 获得了每个基本块的 LiveIn 和 LiveOut 集合以后,我们需要进一步地计算基本块内每个 TAC 语句的 LiveIn 和 LiveOut 集合。如果我们把基本块内所有 TAC 语句分别看成是一个独立的基本块,则不难想像,前面提到的活跃变量方程仍然有效,不同之处在于,一个基本块对应的 “控制流图” 有以下三种特点: 126 | 127 | 1. 每个节点的出度都是 1,也就是说 LiveOut(B) = LiveIn(Succ(B))。 128 | 129 | 2. 由于每个结点只含有一个语句,因此其 Def 集要么是空集,要么只含有一个元素。 130 | 131 | 3. 由于每个结点对应的语句里所引用的所有变量在使用的时候都未在该基本块中经过定值,其 LiveUse 集合就是源操作数对应的所有变量。 132 | 133 | 基于上面三个特点,已经求出基本块的 LiveOut 集合的前提下我们只需要在每个基本块内从后往前遍历基本块内的指令就可以对每条基本块内指令求出 LiveIn、LiveOut。 134 | -------------------------------------------------------------------------------- /docs/step6/example.md: -------------------------------------------------------------------------------- 1 | # step6 实验指导 2 | 3 | 本实验指导使用的例子为: 4 | 5 | ```C 6 | int main() { 7 | int x = 1; 8 | { 9 | x = 2; 10 | int x = 3; 11 | } 12 | x = 4; 13 | return x; 14 | } 15 | ``` 16 | 17 | ## 词法语法分析 18 | 针对块语句,我们需要设计 AST 节点来表示它,给出的参考定义如下: 19 | 20 | | 节点 | 成员 | 含义 | 21 | | --- | --- | --- | 22 | | `Block` | 子语句列表 `children` | 语句块 | 23 | 24 | ## 语义分析 25 | 26 | 从 Step6 开始,我们需要考虑作用域和代码块。简而言之,一份代码中可能有多个代码块的嵌套,因此作用域开始出现了层次结构。例如,在示例中,尽管 main 函数里定义了变量 x,但随后我们开启了一个新的代码块。在这个代码块中,赋值语句 `x = 2;` 中的 x 就是指 main 作用域中定义的 x,而随后通过 `int x = 3;` 我们定义了另一个变量 x,这个 x 只在内部大括号括起的作用域内生效。 27 | 28 | 在 Step5 中,我们只维护了 main 的作用域,所有符号都在这个作用域的符号表中维护。现在,为了维护层次嵌套的作用域,我们引入了**作用域栈**(Scope Stack)这个数据结构。在进行符号表构建的扫描过程中,我们需要动态维护作用域栈,保存当前扫描结点所在的从内到外所有作用域。每次我们开启一个代码块时,要新建一个作用域并压栈;而当退出代码块时,要弹栈关闭此作用域。 29 | 30 | 接下来针对上述代码示例,讲述作用域栈的维护方式。首先,栈底有一个全局作用域,其符号表里只有 main 函数。由于目前不需要考虑函数和全局变量,可以暂时忽略全局作用域。进入 main 函数时,开启一个局部作用域,在扫描 `int x = 1;` 时定义变量符号 x,并将其加入栈顶作用域对应的符号表中。如下所示: 31 | 32 | | 作用域栈 | 符号表 | 33 | | ------------------ | ------------------- | 34 | | 全局作用域(栈底) | 函数 main(可忽略) | 35 | | 局部作用域(栈顶) | 变量 x | 36 | 37 | 接下来,扫描到一个局部代码块,由此建立一个局部作用域并压栈。在扫描 `x = 2;` 时,我们需要分析 x 这个变量对应着哪个作用域里的符号。此时的作用域栈是这样的: 38 | 39 | | 作用域栈 | 符号表 | 40 | | ------------------ | ------------------- | 41 | | 全局作用域(栈底) | 函数 main(可忽略) | 42 | | 局部作用域 | 变量 x | 43 | | 局部作用域(栈顶) | 空 | 44 | 45 | 对变量x的查找从栈顶开始,由上向下依次查找对应的符号表,直至找到变量 x 为止。由于在栈顶作用域对应的符号表中不存在变量符号 x,于是向下继续查找。在 main 函数对应的作用域中,可以找到变量符号 x。因此,语句 `x = 2;` 中的 x 对应 main 函数作用域里定义的变量 x。 46 | 47 | 接下来,当扫描到语句 `int x = 3;` 时,定义了另一个变量 x。此时,只需要在栈顶作用域中查找该变量是否存在。若不存在,即在符号表中加入对应符号。此时的作用域栈如下: 48 | 49 | | 作用域栈 | 符号表 | 50 | | ------------------ | ------------------- | 51 | | 全局作用域(栈底) | 函数 main(可忽略) | 52 | | 局部作用域 | 变量 x | 53 | | 局部作用域(栈顶) | 变量 x | 54 | 55 | 请务必注意上表中的两个变量 x 是**不同的变量**。 56 | 57 | 接下来,退出代码块,将其对应的作用域弹出栈,此时的作用域栈如下: 58 | 59 | | 作用域栈 | 符号表 | 60 | | ------------------ | ------------------- | 61 | | 全局作用域(栈底) | 函数 main(可忽略) | 62 | | 局部作用域(栈顶) | 变量 x | 63 | 64 | 最后,扫描语句 `x = 4;` 时,从栈顶作用域符号表查找 x,所找到的变量 x 为 main 作用域定义的 x 变量。 65 | 66 | ## 中间代码生成 67 | 68 | 本步骤中无须新增新的 TAC 指令。 69 | 70 | 让我们来看看示例所对应的 TAC 代码: 71 | 72 | ```asm 73 | main: 74 | _T1 = 1 75 | _T0 = _T1 # int x = 1; 76 | _T2 = 2 77 | _T0 = _T2 # x = 2 78 | _T4 = 3 79 | _T3 = _T4 # int x = 3; 80 | _T5 = 4 81 | _T0 = _T5 # x = 4; 82 | return _T0 83 | ``` 84 | 85 | 显然,两个代码块里的变量 x 是不同的变量,因此它们分别对应着不同的临时变量。其中,_T0 对应着 main 作用域里的 x,而 _T3 则对应着内层代码块定义的变量 x。只要同学们在符号表构建阶段把每个变量和正确作用域的变量符号关联起来,这一步就非常简单了:找到对应变量符号,使用该符号对应的临时变量即可。 86 | 87 | ## 目标代码生成 88 | 89 | 不需要新增新的中间代码指令。 90 | 91 | 代码框架需要同学们对寄存器分配相关的 CFG 的内容进行细微修改。具体来说,需要在 `backend/dataflow/cfg.py` 中添加基本块是否可达的判断。在寄存器分配算法 `backend/reg/bruteregalloc.py` 的注释中,我们给出了提示,如果一个基本块不可达,那么无须为它分配寄存器。 92 | 93 | 94 | # 实现提示 95 | 96 | 1. 在 step5 中,namer/typer 遍历时的上下文信息(参数 ctx)是单一的作用域。到了 step 6,你需要按照实验指导书中描述,**把上下文信息改成“作用域栈”**。也即定义 `class Namer(Visitor[Scope, None])` 应改为 `class Namer(Visitor[YourType, None])`,其中 `YourType` 是你的作用域栈类型,你可以任意命名它。我们推荐把这个类的定义放在 `frontend/scope/` 下。class Typer 也需要如上改动。 97 | 98 | 2. 之前 step5 的全局唯一的作用域可以被当作“函数作用域使用”,在 visitFunction 入栈。然后在新的 visitBlock 中,再进一步将局部作用域压栈。最后,在所有这些方法的末尾,不要忘了把对应作用域退栈。 99 | 100 | 3. 当只有一个作用域时,“不可以定义新变量a”就意味着当前“可以获取变量a的值”,反之亦然,所以“定义变量”和“获取变量”的检查都可以用 `Scope.lookup` 实现。但有了多个作用域之后,就出现了“既可以拿到a的值,也可以重新定义一个a”的情况。这需要重新考虑 Typer / Namer 中的每一个 `Scope.lookup` ,看她们是否需要换成新函数。 101 | 102 | 4. 后续 stage-4 时,你需要一个机制来检查 break/continue 语句是否在一个循环内。这可以通过修改 namer/typer 中的对应结点来实现。另外,别忘了循环本身也是一个作用域! 103 | 104 | 5. 后续如果你选做“全局变量”部分,可以在 Namer 和 Typer 的 transform 方法中先将全局作用域加入栈底,再往上才是 visitFunction 的函数作用域。 105 | 106 | # 思考题 107 | 1. 请画出下面 MiniDecaf 代码的控制流图。 108 | ```c 109 | int main(){ 110 | int a = 2; 111 | if (a < 3) { 112 | { 113 | int a = 3; 114 | return a; 115 | } 116 | return a; 117 | } 118 | } 119 | ``` 120 | -------------------------------------------------------------------------------- /docs/step6/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step6:作用域和块语句 2 | step6 我们要增加块语句的支持。 3 | 4 | 虽然块语句语义不难,就是把多个语句组成一个块,每个块都是一个作用域。 5 | 随之而来一个问题是:不同变量可以重名了。 6 | 重名的情况包括作用域内部声明覆盖(shadowing)外部声明,以及不相交的作用域之间的重名变量。 7 | 因此,变量名不能唯一标识变量了,同一个变量名 `a` 出现在代码不同地方可能标识完全不同的变量。 8 | 我们需要在符号表构建的过程中,确定 AST 中出现的每个变量名分别对应那个变量。 9 | 10 | 语法上改动不大 11 | 12 |

13 | 
function 14 | : type Identifier '(' ')' compound_statement 15 |
16 |
compound_statement 17 | : '{' block_item* '}' 18 |
19 | statement 20 | : 'return' expression ';' 21 |
| compound_statement 22 | block_item 23 | : statement 24 | | declaration
25 |
26 | 27 | 语义检查我们也要修改了,只有在同一个作用域里,变量才不能重复声明。 28 | 当然,如果变量在使用前还是必须先被声明。 29 | 30 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/.pdf`,比如 stage 3 的实验报告需要放在 `stage-3` 这个 branch 下的 `./reports/stage-3.pdf`。注意报告的标题是 `stage-3` 而不是 `step-6`。 31 | 32 | 你需要: 33 | 1. 改进你的编译器,支持本节引入的新特性,通过相关测试。 34 | 2. 完成实验报告(具体要求请看实验指导书的首页)。实验报告中需要包括: 35 | * 你的学号姓名 36 | * 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 37 | * 指导书上的思考题 38 | * 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 39 | * 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 -------------------------------------------------------------------------------- /docs/step6/pics/dataflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step6/pics/dataflow.png -------------------------------------------------------------------------------- /docs/step6/pics/flowgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step6/pics/flowgraph.png -------------------------------------------------------------------------------- /docs/step6/pics/formula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step6/pics/formula.png -------------------------------------------------------------------------------- /docs/step6/pics/namer.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/step6/spec.md: -------------------------------------------------------------------------------- 1 | # 规范 2 | 每个步骤结尾的 **规范** 一节都会对这个步骤中的新特性给出规范,方便大家查阅。 3 | 4 | # step6 语法规范 5 | 灰色部分表示相对上一节的修改。 6 | 7 |
 8 | 
 9 | program
10 |     : function
11 | 
12 | 
function 13 | : type Identifier '(' ')' compound_statement 14 |
15 | type 16 | : 'int' 17 | 18 |
compound_statement 19 | : '{' block_item* '}' 20 | 21 | block_item 22 | : statement 23 | | declaration 24 |
25 | 26 | statement 27 | : 'return' expression ';' 28 |
| compound_statement 29 |
30 | declaration 31 | : type Identifier ('=' expression)? ';' 32 | 33 | expression 34 | : assignment 35 | 36 | assignment 37 | : conditional 38 | | Identifier '=' expression 39 | 40 | conditional 41 | : logical_or 42 | | logical_or '?' expression ':' conditional 43 | 44 | logical_or 45 | : logical_and 46 | | logical_or '||' logical_and 47 | 48 | logical_and 49 | : equality 50 | | logical_and '&&' equality 51 | 52 | equality 53 | : relational 54 | | equality ('=='|'!=') relational 55 | 56 | relational 57 | : additive 58 | | relational ('<'|'>'|'<='|'>=') additive 59 | 60 | additive 61 | : multiplicative 62 | | additive ('+'|'-') multiplicative 63 | 64 | multiplicative 65 | : unary 66 | | multiplicative ('*'|'/'|'%') unary 67 | 68 | unary 69 | : primary 70 | | ('-'|'~'|'!') unary 71 | 72 | primary 73 | : Integer 74 | | '(' expression ')' 75 | | Identifier 76 |
77 | 78 | 79 | # step6 语义规范 80 | 81 | **6.1** 根据其声明的位置,每一个标识符都属于一个作用域。目前我们有两种作用域:文件级和块级。如果是在块中声明,则标识符其声明所属的块的作用域中,例如局部变量;否则标识符在文件级(全局)作用域中,例如全局变量。 82 | 83 | **6.2** (更新 5.6)如果一个标识符在两个作用域里面,这两个作用域必然是嵌套的,即一个内层作用域完全被另一个外层作用域所覆盖。且在内层作用域中,外层作用域里该标识符所指派(designate)的变量或函数是不可见的。 84 | > 在初始化表达式中,其正在初始化的变量已被声明,会隐藏(shadow)外层作用域的同名变量,但其值不确定。例如在下面的代码片段中,`a + 1` 的值是不确定的。 85 | > ``` 86 | > int a = 1; 87 | > { 88 | > int a = a + 1; 89 | > } 90 | > ``` 91 | 92 | **6.3** (更新 5.3)对于同一个标识符,在同一个作用域中至多有一个声明。 93 | 94 | **6.4** (更新 5.4)使用不在当前开作用域中的变量名是不合法的。 95 | 96 | -------------------------------------------------------------------------------- /docs/step7/example.md: -------------------------------------------------------------------------------- 1 | # step7 实验指导 2 | 3 | 本实验指导使用的例子为: 4 | 5 | ```C 6 | int main() { 7 | int x = 1; 8 | if (x) x = 2; else x = 3; 9 | return x; 10 | } 11 | ``` 12 | 13 | ## 词法语法分析 14 | 针对 if 语句,我们需要设计 AST 节点来表示它,给出的参考定义如下(框架中已经提供): 15 | 16 | | 节点 | 成员 | 含义 | 17 | | --- | --- | --- | 18 | | `If` | 分支条件 `cond`,真分支 `then`,假分支 `otherwise` | if 分支语句 | 19 | 20 | 仿照 if 节点,还需要类似地实现条件表达式节点。 21 | 22 | ### 悬吊 else 问题 23 | 24 | 这一节引入的 if 语句既可以带 else 子句也可以不带,但这会导致语法二义性:`else` 到底和哪一个 `if` 结合? 25 | 例如 `if(a) if(b) c=0; else d=0;`,到底是 `if(a) {if(b) c=0; else d=0;}` 还是 `if(a) {if(b) c=0;} else d=0;`? 26 | 这个问题被称为 **悬吊 else(dangling else)** 问题。 27 | 28 | 如果程序员没有加大括号,那么我们需要通过一个规定来解决歧义。 29 | 我们人为规定:`else` 和最近的 `if` 结合,也就是说上面两种理解中只有前者合法。 30 | 为了让 parser 能遵守这个规定,一种方法是设置产生式的优先级,优先选择没有 else 的 if。 31 | 按照这个规定,parser 看到 `if(a) if(b) c=0; else d=0;` 中第一个 if 时,选择没有 else 的 if; 32 | 而看到第二个时只能选择有 else 的 if ,也就使得 `else d=0;` 被绑定到 `if(b)` 而不是 `if(a)` 了。 33 | 34 | > 需要说明的是 bison 默认在 shift-reduce conflict 的时候选择shift,从而对悬挂else进行就近匹配。 35 | 36 | ## 语义分析 37 | 38 | 本步骤中语义分析没有特别需要增加的内容,只需要在扫描到 if 语句和条件表达式时递归地访问其子结点即可。请注意 if 语句**不总是**有 else 分支,所以在递归到子结点时,请先判断子结点是否存在。 39 | 40 | ## 中间代码生成 41 | 从本步骤开始,由于 MiniDecaf 程序出现了分支结构,我们需要开始考虑跳转语句了。在 Step1-4 中,TAC 代码中的标签只有标志 main 函数入口这一个功能。而现在,我们需要使用标签来指示跳转指令的目标位置。我们用 _Lk 来表示跳转用标签,以此和函数入口标签区分开来。 42 | 43 | 为了实现 if 语句,我们需要设计两条中间代码指令,分别表示条件跳转和无条件跳转,给出的参考定义如下: 44 | 45 | > 请注意,TAC 指令的名称只要在你的实现中是一致的即可,并不一定要和文档一致。 46 | 47 | | 指令 | 参数 | 作用 | 48 | | --- | --- | --- | 49 | | `BEQZ` | `T0, Label` | 若 T0 的值为0,则跳转到 LABEL 标签处 | 50 | | `JUMP` | `Label` | 跳转到 LABEL 标签处 | 51 | 52 | 现在让我们来看看示例所对应的 TAC 代码: 53 | 54 | ```assembly 55 | main: 56 | _T1 = 1 57 | _T0 = _T1 58 | BEQZ _T0, _L1 59 | _T2 = 2 60 | _T0 = _T2 61 | JUMP _L2 62 | _L1: 63 | _T3 = 3 64 | _T0 = _T3 65 | _L2: 66 | return _T0 67 | ``` 68 | 69 | 在这段 TAC 代码中,x 对应的临时变量为 _T0。如果 x 的值为真(不等于0),那么应当执行 then 分支 `x = 2;`,否则执行 else 分支 `x = 3;`。因此,我们设置了两个跳转标签 _L1 和 _L2,分别表示 else 分支开始位置和整个 if 语句的结束位置。如果 x 为假,那么应当跳转到 _L1 处,我们使用一条 BEQ 指令来执行。如果 x 为真,那么按顺序执行 then 分支的代码,并在该分支结束时,用一条 JMP 指令跳转到 if 语句的结束位置,从而跳过 else 分支。在 TAC 生成过程中,每当扫描到 if 语句时,都需要调用 TAC 的底层接口,新建两个跳转标签,并按照这种方式生成中间代码。 70 | 71 | 当然,如果一条 if 语句没有 else 分支,那么只需要一个跳转标签即可。例如我们将例子中的 if 语句修改为 `if (x) x = 2;`,则对应的 TAC 代码可简化为: 72 | 73 | ```assembly 74 | main: 75 | _T1 = 1 76 | _T0 = _T1 77 | BEQ _T0, _L1 78 | _T2 = 2 79 | _T0 = _T2 80 | _L1: 81 | return _T0 82 | ``` 83 | 84 | 同样地,条件表达式也可以使用类似的方法完成中间代码生成。要注意的是,条件表达式是一种特殊的**表达式**,因此有返回值。同学们在实现的时候不要忘记为其分配临时变量。 85 | 86 | ## 目标代码生成 87 | Step7 中目标代码生成主要是指令的选择以及 label 的声明,RISC-V 提供了与中间代码中 BEQZ 和 JUMP 类似的指令: 88 | 89 | ```assembly 90 | step7: # RISC-V 汇编标签 91 | beqz t1, step7 # 如果 t1 为 0,跳转到 step7 标签处 92 | j step7 # 无条件跳转到 step6 标签处 93 | ``` 94 | 95 | # 思考题 96 | 97 | 1. 我们的实验框架里是如何处理悬吊 else 问题的?请简要描述。 98 | 99 | 2. 在实验要求的语义规范中,条件表达式存在短路现象。即: 100 | 101 | ```c 102 | int main() { 103 | int a = 0; 104 | int b = 1 ? 1 : (a = 2); 105 | return a; 106 | } 107 | ``` 108 | 109 | 会返回 0 而不是 2。如果要求条件表达式不短路,在你的实现中该做何种修改?简述你的思路。 110 | 111 | # 总结 112 | 本节主要就是引入了跳转,后面 Step8 循环语句还会使用。 113 | 114 | -------------------------------------------------------------------------------- /docs/step7/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step7: 2 | step7 我们要支持条件语句,包括 if 语句和条件表达式(又称三元/三目表达式,ternary expression)。 3 | 4 | 语法上的改动是: 5 | 6 | 1. if 表达式 7 |

 8 | statement
 9 |     : 'return' expression ';'
10 |     | expression? ';'
11 | 
| 'if' '(' expression ')' statement ('else' statement)? 12 |
13 | 14 | 2. 条件表达式 15 |
16 | assignment 17 | : conditional 18 | | Identifier '=' expression
19 | conditional 20 | : logical_or 21 | | logical_or '?' expression ':' conditional 22 |
23 | 24 | if 语句的语义和 C 语言相同,注意条件表达式优先级只比赋值高。 25 | 26 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/.pdf`,比如 stage 4 的实验报告需要放在 `stage-4` 这个 branch 下的 `./reports/stage-4.pdf`。整个 stage 4 只需要提交一份报告,你不需要单独为 step 7 准备报告。 27 | 28 | 29 | 你需要: 30 | 1. 改进你的编译器,支持本节引入的新特性,通过相关测试。 31 | 2. 完成实验报告(具体要求请看实验指导书的首页)。实验报告中需要包括: 32 | * 你的学号姓名 33 | * 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 34 | * 指导书上的思考题 35 | * 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 36 | * 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 -------------------------------------------------------------------------------- /docs/step7/manual-parser.md: -------------------------------------------------------------------------------- 1 | ## 手写 parser 简单实例 2 | 3 | ### 定义变化 4 | 5 | 增加两种新节点的同时,需要增加 Node 的内容。 6 | 7 | ``` 8 | struct NodeKind { 9 | + ND_IF, 10 | + ND_TERN, // :? 运算 11 | } 12 | 13 | struct Node { 14 | + Node* cond; // 储存条件表达式 15 | + Node* then; // 储存条件判断成功时执行的语句(返回的表达式) 16 | + Node* else; // 储存条件判断失败时执行的语句(返回的表达式) 17 | } 18 | ``` 19 | 20 | 注意,对于 `:?`运算符,`then` 和 `else` 是两个表达式节点, 对于 if 语句,这两个变量是两个语句节点。 21 | 22 | ### 解析变化 23 | 24 | 按照生成式变化改变即可。if 语句示例如下: 25 | 26 | ```c++ 27 | Node* stmt() { 28 | // ... 29 | // IF statement 30 | if (parse_reserved("if")) { 31 | assert(parse_reserved("(")); 32 | node = new_node(ND_IF); 33 | node->cond = expr(); 34 | assert(parse_reserved(")")); 35 | node->then = stmt(); 36 | if(parse_reserved("else")) 37 | node->els = stmt(); 38 | return node; 39 | } 40 | // ... 41 | } 42 | ``` 43 | 44 | 以后同质化的内容不再展示。 -------------------------------------------------------------------------------- /docs/step7/pics/README.md: -------------------------------------------------------------------------------- 1 | some pics 2 | -------------------------------------------------------------------------------- /docs/step7/spec.md: -------------------------------------------------------------------------------- 1 | # 规范 2 | 每个步骤结尾的 **规范** 一节都会对这个步骤中的新特性给出规范,方便大家查阅。 3 | 4 | # step7 语法规范 5 | 灰色部分表示相对上一节的修改。 6 | 7 |
 8 | 
 9 | program
10 |     : function
11 | 
12 | function
13 |     : type Identifier '(' ')' '{' block_item* '}'
14 | 
15 | type
16 |     : 'int'
17 | 
18 | compound_statement
19 |     : '{' block_item* '}'
20 | 
21 | block_item
22 |     : statement
23 |     | declaration
24 | 
25 | statement
26 |     : 'return' expression ';'
27 |     | compound_statement
28 | 
| expression? ';' 29 | | 'if' '(' expression ')' statement ('else' statement)? 30 |
31 | declaration 32 | : type Identifier ('=' expression)? ';' 33 | 34 | expression 35 | : assignment 36 | 37 |
assignment 38 | : conditional 39 | | Identifier '=' expression 40 | 41 | conditional 42 | : logical_or 43 | | logical_or '?' expression ':' conditional 44 |
45 | logical_or 46 | : logical_and 47 | | logical_or '||' logical_and 48 | 49 | logical_and 50 | : equality 51 | | logical_and '&&' equality 52 | 53 | equality 54 | : relational 55 | | equality ('=='|'!=') relational 56 | 57 | relational 58 | : additive 59 | | relational ('<'|'>'|'<='|'>=') additive 60 | 61 | additive 62 | : multiplicative 63 | | additive ('+'|'-') multiplicative 64 | 65 | multiplicative 66 | : unary 67 | | multiplicative ('*'|'/'|'%') unary 68 | 69 | unary 70 | : primary 71 | | ('-'|'~'|'!') unary 72 | 73 | primary 74 | : Integer 75 | | '(' expression ')' 76 | | Identifier 77 |
78 | 79 | > 注意:`if` 的 `then` 分支和 `else` 分支需要是一个语句(statement)而非声明(declaration)。 80 | > 例如 `if (1) int a;` 不是合法的 MiniDecaf 程序。 81 | 82 | # step7 语义规范 83 | 84 | **7.1** 条件表达式会先对第一个操作数求值,再根据其值选择计算第二个或第三个操作数。当且仅当第一个操作数的值不等于 0,我们会对第二个操作数求值。当且仅当第一个操作数的值等于 0,我们会对第三个操作数求值。当第一个操作数的值为 0 时,条件表达式的求值结果为第二个操作数所求得的值;当第一个操作数的值非 0 时,条件表达式的求值结果为第三个操作数所求得的值。 85 | > 不论选择第二个操作数或者是第三个操作数去求值,都必须首先计算完第一个操作数,之后才能开始第二个或第三个操作数的求值计算。 86 | 87 | **7.2** 对于 if 语句而言,当控制条件不等于 0 时,会执行第一个子句;当控制条件等于 0 时,如果有 else 分支,就会执行第二个语句,否则整个 if 语句的执行便已经完成。 88 | 89 | **7.3** 如果出现悬吊 `else`(dangling else),要求 `else` 优先和最接近的没有匹配 `else` 的 `if` 匹配。 90 | > 例如 `if (0) if (0) ; else ;` 等价于 `if (0) { if (0) ; else; }` 而非 `if (0) { if (0) ; } else ;`。 91 | -------------------------------------------------------------------------------- /docs/step8/example.md: -------------------------------------------------------------------------------- 1 | # step8 实验指导 2 | 3 | 本实验指导使用的例子为: 4 | 5 | ```C 6 | for (int i = 0; i < 5; i = i + 1) 7 | break; 8 | // 后续语句 ... 9 | ``` 10 | 11 | ## 词法语法分析 12 | 13 | 针对循环语句和 break/continue 语句,我们需要设计 AST 节点来表示它,给出的参考定义如下: 14 | 15 | | 节点 | 成员 | 含义 | 16 | | --- | --- | --- | 17 | | `While` | 循环条件 `cond`,循环体 `body` | while 循环语句 | 18 | | `For` | 初始语句 `init`,循环条件 `cond`,更新语句 `update`,循环体 `body` | for 循环语句 | 19 | | `Break` | 无 | break 语句 | 20 | | `Continue` | 无 | continue 语句 | 21 | 22 | 其中,while 和 break 语句的实现已经在框架中给出,同学们可以参考并实现 for 和 continue 语句。 23 | 24 | ## 语义分析 25 | 26 | 本步骤语义分析阶段的处理方式和 Step7 中的 if 语句相类似,但是请额外注意以下两点: 27 | 28 | 1. for 循环要自带一个作用域。在示例里,`for (int i = 0; i < 5; i = i + 1)` 语句里定义的循环变量处于一个独自的作用域里。这也就是说,我们可以在循环体内部定义同名变量。如果我们把示例修改为:`for (int i = 0; i < 5; i = i + 1) { int i = 0; }` 这也是合法的 MiniDecaf 程序。因此,在符号表构建阶段,扫描到 for 结点时,不要忘记开启一个局部作用域。 29 | 30 | 2. break 和 continue 语句必须位于循环体内部才合法。因此,在扫描过程中,需要记录当前结点位于多少重循环内。扫描到 break 和 continue 结点时,若当前不处于任何循环内,则报错。 31 | 32 | ## 中间代码生成 33 | 34 | 本步骤中没有需要新增的 TAC 指令。不过为了实现循环语句,需要仔细地考虑如何将 MiniDecaf 循环语句翻译成 TAC 的分支跳转指令。由于 while 循环可以看作 for 循环的特例,我们选择了 for 循环作为示例。 35 | 让我们先来看看示例对应的 TAC 代码: 36 | 37 | ```assembly 38 | _T1 = 0 39 | _T0 = _T1 # int i = 0; 40 | _L1: # begin label 41 | _T2 = 5 42 | _T3 = LT _T0, _T2 43 | BEQZ _T3, _L3 # i < 5; 44 | _L2: # loop label 45 | _T4 = 1 46 | _T5 = ADD _T0, _T4 47 | _T0 = _T5 # i = i + 1; 48 | JUMP _L1 49 | _L3: # break label 50 | # 后续指令 ... 51 | ``` 52 | 53 | 为了实现所有可能的跳转,对每个 for 循环我们都需要定义三个跳转标签:begin, loop 和 break。它们的作用如下: 54 | 55 | 1. begin 标签(示例中的 _L1)是循环体的开始位置。初次进入循环时,从这个标签的位置开始执行,并判断循环条件是否满足,若不满足,则跳转到 break 标签(示例中的 _L3)处。 56 | 57 | 2. loop 标签(示例中的 _L2)是执行 continue 语句时应当跳转到的位置。 58 | 59 | 3. break 标签是整个循环结束后的位置。如果循环条件不满足,或者执行了 break 语句,那么应当跳转到此处,执行循环之后的指令。 60 | 61 | 请注意,示例给出的只是一种循环语句**参考实现**,同学们也可以设计自己的实现方法。 62 | 63 | 由于循环语句可以嵌套,所以 TAC 语句生成过程中需要动态维护 loop 标签和 break 标签,这样才能确定每一条 break 和 continue 语句跳转到何处。因此,在 TAC 生成时,需要使用栈结构维护从内到外所有的 loop 标签和 break 标签。 64 | 65 | `utils/tacgen/tacgen.py` 里的 `TACFuncEmitter` 类里实现了维护 TAC 生成时需要的上下文信息的功能。同学们可以在这个类中增加对循环所需的 break/continue 标签的维护。 66 | 67 | ## 目标代码生成 68 | 69 | 由于不需要增加新的中间代码指令,本步骤中目标代码生成模块没有新的内容。除非之前步骤的实现有误,否则这个步骤应该不会出现错误。 70 | 71 | # 思考题 72 | 73 | 1. 将循环语句翻译成 IR 有许多可行的翻译方法,例如 while 循环可以有以下两种翻译方式: 74 | 75 | 第一种(即实验指导中的翻译方式): 76 | 77 | + `label BEGINLOOP_LABEL`:开始下一轮迭代 78 | + `cond 的 IR` 79 | + `beqz BREAK_LABEL`:条件不满足就终止循环 80 | + `body 的 IR` 81 | + `label CONTINUE_LABEL`:continue 跳到这 82 | + `br BEGINLOOP_LABEL`:本轮迭代完成 83 | + `label BREAK_LABEL`:条件不满足,或者 break 语句都会跳到这儿 84 | 85 | 第二种: 86 | 87 | + `cond 的 IR` 88 | + `beqz BREAK_LABEL`:条件不满足就终止循环 89 | + `label BEGINLOOP_LABEL`:开始下一轮迭代 90 | + `body 的 IR` 91 | + `label CONTINUE_LABEL`:continue 跳到这 92 | + `cond 的 IR` 93 | + `bnez BEGINLOOP_LABEL`:本轮迭代完成,条件满足时进行下一次迭代 94 | + `label BREAK_LABEL`:条件不满足,或者 break 语句都会跳到这儿 95 | 96 | 从执行的指令的条数这个角度(`label` 不算做指令,假设循环体至少执行了一次),请评价这两种翻译方式哪一种更好? 97 | 98 | 2. 我们目前的 TAC IR 中条件分支指令采用了单分支目标(标签)的设计,即该指令的操作数中只有一个是标签;如果相应的分支条件不满足,则执行流会继续向下执行。在其它 IR 中存在双目标分支(标签)的条件分支指令,其形式如下: 99 | 100 | ```assembly 101 | br cond, false_target, true_target 102 | ``` 103 | 104 | 其中`cond`是一个临时变量,`false_target`和`true_target`是标签。其语义为:如果`cond`的值为0(假),则跳转到`false_target`处;若`cond`非0(真),则跳转到`true_target`处。它与我们的条件分支指令的区别在于执行流总是会跳转到两个标签中的一个。 105 | 106 | 你认为中间表示的哪种条件分支指令设计(单目标 vs 双目标)更合理?为什么?(言之有理即可) 107 | -------------------------------------------------------------------------------- /docs/step8/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step8:循环语句 2 | step8 我们要增加对循环语句,以及 break/continue 的支持: 3 | 4 |

 5 | statement
 6 |     : 'return' expression ';'
 7 |     | expression? ';'
 8 |     | 'if' '(' expression ')' statement ('else' statement)?
 9 |     | compound_statement
10 | 
| 'for' '(' expression? ';' expression? ';' expression? ')' statement 11 | | 'for' '(' declaration expression? ';' expression? ')' statement 12 | | 'while' '(' expression ')' statement 13 | | 'break' ';' 14 | | 'continue' ';' 15 |
16 |
17 | 18 | 循环语句的语义和 C 语言相同,注意检查 break/continue 不能出现在循环外。 19 | 20 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/.pdf`,比如 stage 4 的实验报告需要放在 `stage-4` 这个 branch 下的 `./reports/stage-4.pdf`。整个 stage 4 只需要提交一份报告,你不需要单独为 step 8 准备报告。 21 | 22 | 你需要: 23 | 1. 改进你的编译器,支持本节引入的新特性,通过相关测试。 24 | 2. 完成实验报告(具体要求请看实验指导书的首页)。实验报告中需要包括: 25 | * 你的学号姓名 26 | * 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 27 | * 指导书上的思考题 28 | * 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 29 | * 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 -------------------------------------------------------------------------------- /docs/step8/pics/README.md: -------------------------------------------------------------------------------- 1 | some pics 2 | -------------------------------------------------------------------------------- /docs/step8/pics/dataflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step8/pics/dataflow.png -------------------------------------------------------------------------------- /docs/step8/pics/flowgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step8/pics/flowgraph.png -------------------------------------------------------------------------------- /docs/step8/pics/formula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step8/pics/formula.png -------------------------------------------------------------------------------- /docs/step9/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step9:函数 2 | step9 开始,我们要支持多函数了。 3 | 4 | 1. 我们需要支持函数的声明和定义: 5 |
6 |
program 7 | : function* 8 | 9 | function 10 | : type Identifier '(' parameter_list ')' (compound_statement | ';') 11 |
12 |
parameter_list 13 | : (type Identifier (',' type Identifier)*)? 14 |
15 |
16 | 17 | 2. 我们还需要支持函数调用: 18 |
19 |
expression_list 20 | : (expression (',' expression)*)? 21 |
22 |
unary 23 | : postfix 24 | | ('-'|'~'|'!') unary 25 | 26 | postfix 27 | : primary 28 | | Identifier '(' expression_list ')' 29 |
30 |
31 | 32 | 语义检查部分,我们需要检查函数的重复定义、检查调用函数的实参(argment)和形参(parameter)的个数类型一致。我们不支持 void 返回类型,这可以通过忽略函数的 int 返回值实现。 33 | 34 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/.pdf`,比如 stage 5 的实验报告需要放在 `stage-5` 这个 branch 下的 `./reports/stage-5.pdf`。注意报告的标题是 `stage-5` 而不是 `step-9`。 35 | 36 | 你需要: 37 | 1. 改进你的编译器,支持本节引入的新特性,通过相关测试。 38 | 2. 完成实验报告(具体要求请看实验指导书的首页)。实验报告中需要包括: 39 | * 你的学号姓名 40 | * 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 41 | * 指导书上的思考题 42 | * 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 43 | * 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 44 | -------------------------------------------------------------------------------- /docs/step9/pics/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step9/pics/1.png -------------------------------------------------------------------------------- /docs/step9/pics/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step9/pics/2.png -------------------------------------------------------------------------------- /docs/step9/pics/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step9/pics/3.png -------------------------------------------------------------------------------- /docs/step9/pics/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step9/pics/4.png -------------------------------------------------------------------------------- /docs/step9/pics/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step9/pics/5.png -------------------------------------------------------------------------------- /docs/step9/pics/reg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step9/pics/reg.png -------------------------------------------------------------------------------- /docs/step9/pics/riscv-regs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step9/pics/riscv-regs.png -------------------------------------------------------------------------------- /howto-gitbook.md: -------------------------------------------------------------------------------- 1 | ## 运行步骤 2 | 3 | 1. 安装 gitbook: 4 | ```bash 5 | npm install gitbook-cli -g 6 | ``` 7 | 8 | 如果环境里没有 `npm` 则需要先安装 `npm` 9 | 10 | 2. 进入本项目根目录 11 | 12 | 3. 安装 gitbook: 13 | 14 | ```bash 15 | gitbook install 16 | ``` 17 | 18 | 这一步通常会出现类似如下报错: 19 | ```bash 20 | Installing GitBook 3.2.3 21 | /usr/local/lib/node_modules/gitbook-cli/node_modules/npm/node_modules/graceful-fs/polyfills.js:287 22 | if (cb) cb.apply(this, arguments) 23 | ^ 24 | 25 | TypeError: cb.apply is not a function 26 | at /usr/local/lib/node_modules/gitbook-cli/node_modules/npm/node_modules/graceful-fs/polyfills.js:287:18 27 | at FSReqCallback.oncomplete (fs.js:169:5) 28 | ``` 29 | 30 | 此时进入该代码文件,将62至64行的如下代码删去,然后重新运行 `gitbook install` 即可: 31 | ```js 32 | fs.stat = statFix(fs.stat) 33 | fs.fstat = statFix(fs.fstat) 34 | fs.lstat = statFix(fs.lstat) 35 | ``` 36 | 37 | 4. 在本地构建文档: 38 | 39 | ```bash 40 | gitbook build 41 | ``` 42 | 43 | 完成后可以在 `_book/index` 访问该文档,**但无法点击文档内项目进行跳转**。 44 | 45 | 此时可运行如下命令: 46 | ```bash 47 | sed -i "s/if(m)for(n/if(false)for(n/g" _book/gitbook/theme.js 48 | ``` 49 | 50 | 再次打开文档后就可以跳转了。 51 | 52 | ## 常见问题 53 | 54 | 1. Q: `gitbook build`生成的_book下html在左侧栏无法跳转菜单 55 | 56 | A: 57 | 58 | 在导出的文件夹目录下找到gitbook->theme.js文件.找到下面的代码搜索 if(m)for(n.handler&& 59 | 60 | 将if(m)改成if(false) 61 | 62 | 再次打开_book下的index.html页面,确认能够跳转页面。 63 | 64 | ### 如果有其他奇奇怪怪的问题 65 | 66 | 由于 [gitbook-cli](https://github.com/GitbookIO/gitbook) 的开发及维护已经废止,如果有其他奇奇怪怪的问题的话,可以尝试使用修复了 gitbook-cli 的最后版本中的一些 bug 的 [gitbook-ng](https://www.npmjs.com/package/@gitbook-ng/gitbook/v/3.3.6)。 67 | -------------------------------------------------------------------------------- /readme_for_ta.md: -------------------------------------------------------------------------------- 1 | # 2023 对文档的改动 2 | 3 | 将往年文档中的手写 parser 等文档删除,整理了文档结构,可以看一下对应commit的记录。 4 | 5 | # 2024 对文档的改动 6 | 7 | - 删除了 step14 SSA,现在选做二只包括 step13 寄存器分配算法这一个选择。 8 | - 添加了大实验的文档。 -------------------------------------------------------------------------------- /styles/pdf.css: -------------------------------------------------------------------------------- 1 | website.css -------------------------------------------------------------------------------- /styles/website.css: -------------------------------------------------------------------------------- 1 | .SpecRuleStart { color: #af0087; } 2 | .SpecRuleIndicator { color: #af0087; } 3 | .SpecOperator { color: red; } 4 | .Normal { background-color: #ffffff; padding-bottom: 1px; } 5 | .SpecToken { color: #af5f00; } 6 | .SpecRule { color: #8080ff; } 7 | .changed { background: lightgrey; } 8 | --------------------------------------------------------------------------------