From 9dc3376fecf66971ddf1dc04eccf5db358bd8cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BB=81=E5=93=B2?= Date: Sun, 15 Mar 2026 13:08:36 +0800 Subject: [PATCH] =?UTF-8?q?docs(Lab2.md):=20=E4=B8=8A=E4=BC=A0Lab2?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- doc/Lab1语法树构建.md | 154 ++++++++++++++++++++++++++++++++++ doc/Lab2中间表示生成.md | 122 +++++++++++++++++++++++++++ 3 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 doc/Lab1语法树构建.md create mode 100644 doc/Lab2中间表示生成.md diff --git a/README.md b/README.md index 798b7e6..f51d825 100644 --- a/README.md +++ b/README.md @@ -165,12 +165,12 @@ compiler [选项] source_file 1. **生成中间文件**(用于调试) ```bash - compiler -ast ast.txt -ir ir.txt -asm output.s test.sy + compiler -ast test.ast -ir test.ll -asm test.s test.sy ``` 2. **生成优化后的中间文件** ```bash - compiler -O1 -ast ast.txt -ir ir.txt -asm output.s test.sy + compiler -O1 -ast test.ast -ir test.ll -asm test.s test.sy ``` 3. **仅生成汇编并输出到文件**(比赛功能用例的编译命令) diff --git a/doc/Lab1语法树构建.md b/doc/Lab1语法树构建.md new file mode 100644 index 0000000..e5108dc --- /dev/null +++ b/doc/Lab1语法树构建.md @@ -0,0 +1,154 @@ +# Lab1:语法树构建 + +## 1. Lab1 要求 + +- Lab1实验要求完成从源程序到语法树的构建,这一步骤借助自动化词法/语法分析器实现。对于Rust,我们推荐使用的工具是LALRPOP。LALRPOP 是一个专门为 Rust 语言设计的解析器生成器。它的主要目标是让开发者能够以一种简洁、可读性强的方式来定义语法,并自动生成高效、类型安全的解析器代码。 +- 具体来讲Rust版本的实验,主要有两个文件需要完成: + 1. LALRPOP的语法定义文件 + 2. 语法树中数据结构的定义文件 + +## 2. 相关文件 + +以下文件与本实验内容相关,建议优先阅读。 + +- `./build.rs` +- `./src/main.rs` +- `src/frontend/ast.rs` +- `src/frontend/sysy.lalrpop` + +## 3. 实现说明 +- LALRPOP工具在使用前需要先引入依赖,在`Cargo,toml`文件中添加: + ```toml + [build-dependencies] + lalrpop = "0.22.1" + + [dependencies] + lalrpop = "0.22.1" + lalrpop-util = "0.23.1" + ``` + 示例项目已经添加过了,如果你是自己新建的空白项目那么还是需要重添加的。 +- LALRPOP工具需要先于编译器中其他代码完成构建,所以需要一个`build.rs`文件`build.rs`扮演的就是类似的“预处理器”角色,在rustc正式编译你的代码之前,`build.rs`会先运行它读取你的`.lalrpop`文件,生成对应的Rust解析器代码,然后 rustc 再编译这个生成的代码和你手写的其他Rust代码。 + build.rs文件内容如下: + ```rust + fn main() { + lalrpop::process_src().unwrap(); + } + ``` + 如果需要指定生成代码位置和名称可以使用下面代码,具体可以见`main.rs`: + ```rust + //指定生成的代码的位置 + lalrpop_mod!(#[allow(clippy::all)] pub sysy, "/frontend/lalrpop/sysy.rs"); + ``` + + +- 在LALRPOP工具的理解上,它和Antlr不同,Antlr在使用的时候需要的是一个独立于编程语言(C++、Java)之外的g4文件来定义需要解析的语法,然后再编译器中编写一部分的驱动代码,语法树中的数据结构也是由Antlr自动构建的。但是LALRPOP是和Rust语言深度绑定,其语法文件(.lalrpop文件)是需要嵌入Rust代码指导语法树的构建的,语法树所运用的数据结构也需要自行定义。以下式为例 + ```text + // 编译单元 + CompUnit ::= { GlobalItem } + ``` + 我们首先需要在ast.rs文件中定义: + ```rust + pub struct CompUnit { + pub items: Vec, + } + ``` + 在.lalrpop文件中,会有下面的代码: + ```rsut + // CompUnit -> [CompUnit](Decl|FuncDef) + pub CompUnit: CompUnit = => CompUnit{ <> }; + ``` + .lalrpop这一行代码其实是描述了语法树是如何构建的,它包含了三个关键部分: +1. **左侧 `pub CompUnit: CompUnit`**: + - 第一个`CompUnit`是语法规则的名字,在解析时可以通过`CompUnitParser`调用 + - 第二个`CompUnit`表示这条规则返回的Rust类型,也就是我们在`ast.rs`中定义的`CompUnit`结构体 + +2. **中间 ``**: + - 这是语法规则的产生式,描述了如何从终结符和非终结符构造出`CompUnit` + - `(GlobalItem)*`表示0个或多个`GlobalItem`,对应AST中的`Vec` + - `items:`给这个语法片段命名,方便在后面的Rust代码中引用 + +3. **右侧 `=> CompUnit{ <> }`**: + - `=>`后面的部分是Rust代码,描述了如何将匹配到的语法片段转换为AST节点 + - `CompUnit{ <> }`是一种特殊的语法,`<>`是一个占位符,表示将匹配到的所有字段(这里只有`items`)自动填入结构体中 + - 这种写法等价于手动写`CompUnit{ items: items }`,但更加简洁 + + +## 4. 当前示例实现说明 + +本实例代码面向的时Sysy的简化子集,只支持int和void类型以及加法运算,扩展的巴克斯范式表示如下所示,具体的一个示例可以参考`test/test_case/functional/simple_add.sy` +```text +// 编译单元 +CompUnit ::= { GlobalItem } + +GlobalItem ::= Decl | FuncDef + +// 声明 +Decl ::= VarDecl + +// 类型 +BType ::= "int" | "void" + +// 变量声明 +VarDecl ::= BType VarDef { "," VarDef } ";" + +// 变量定义 +VarDef ::= Ident [ "=" InitVal ] + +// 初始值 +InitVal ::= Exp + +// 函数定义 +FuncDef ::= BType Ident "(" [ FuncFParams ] ")" Block + +// 函数形参列表 +FuncFParams ::= FuncFParam { "," FuncFParam } + +// 函数形参 +FuncFParam ::= BType Ident + +// 语句块 +Block ::= "{" { BlockItem } "}" + +// 语句块项 +BlockItem ::= Decl | Stmt + +// 语句 +Stmt ::= LVal "=" Exp ";" + | [ Exp ] ";" + | Block + | "return" [ Exp ] ";" + +// 左值 +LVal ::= Ident + +// 基本表达式 +PrimaryExp ::= "(" Exp ")" | LVal | Number + +// 一元表达式 +UnaryExp ::= PrimaryExp | "+" UnaryExp + +// 加法表达式 +AddExp ::= UnaryExp | AddExp "+" UnaryExp + +// 表达式 +Exp ::= AddExp + +// 常数表达式(用于常量计算) +ConstExp ::= AddExp + +// 终结符定义 +Ident ::= 字母 (字母 | 数字)* +Int ::= 十进制整数 | 八进制整数 | 十六进制整数 +Number ::= IntConst +``` + +## 5. 构建与生成流程 + +运行实例代码,查看实验1的具体效果可以使用下面的命令,在项目根目录下: +```bash +cargo build -r +./target/release/compiler -ast ./test/output/simple_add.sy ./test/test_case/functional/simple_add.sy +``` +这样解析出来的语法树会输出到`./test/output/simple_add.ast`文件中。 + + diff --git a/doc/Lab2中间表示生成.md b/doc/Lab2中间表示生成.md new file mode 100644 index 0000000..6de40f0 --- /dev/null +++ b/doc/Lab2中间表示生成.md @@ -0,0 +1,122 @@ +# Lab2:中间表示生成 + +## 1. Lab2 要求 + +Lab2要求可以将Sysy源程序转化为中间代码表示,经过Lab1理论上你目前的编译器已经可以将任意的Sysy程序转化为语法树,也就是Lab2的任务是完成遍历语法树生成中间代码的工作。 + +## 2. 相关文件 + +以下文件与本实验内容相关,可以优先看`./src/frontend/irgen.rs`,这个文件是整个IR生成的核心生成逻辑文件。 + +- 用到的数据结构 + ```bash + ./src/utils/storage.rs + ./src/utils/linked_list.rs + ``` +- 定义的IR数据结构 + ```bash + ./src/frontend/ir/context.rs + ./src/frontend/ir/function.rs + ./src/frontend/ir/basicblock.rs + ./src/frontend/ir/instruction.rs + ./src/frontend/ir/global.rs + ./src/typ.rs + ./src/value.rs + ./src/defuse.rs + ``` +- IR生成器 + ```bash + ./src/typecheck.rs + ./src/frontend/irgen.rs + ./src/frontend/ir2string.rs + ``` +- 符号表 + ```bash + ./src/frontend/symboltable.rs + ``` + +## 3. 实现说明 + +### 3.1 关于`storage.rs`和`linked_list.rs` + +> 这两个文件构成了一个**专门为编译器实现设计的基础数据结构库**,主要用于解决编译过程中抽象语法树(AST)和中间表示(IR)的存储、访问和遍历问题。**将存储管理和组织结构分离**,`storage.rs`解决的是“节点存放在哪里”的问题,通过内存池提供稳定的存储位置,`linked_list.rs`解决的是“节点之间如何组织”的问题,通过侵入式链表提供灵活的结构关系这种分离使得一个节点可以同时属于多个不同的组织结构,而无需为每种组织结构重复存储节点数据,这正符合编译器实现中处理复杂语法结构的需求。 +1. `storage.rs` 是一个内存池分配器,这是一个**自定义的内存管理基础设施**,其核心思想是通过索引而非指针来引用数据。 + - `GenericPtr`是一个**智能句柄**,本质上是对数组索引的封装。它不直接持有数据的引用,而是记录了数据在内存池中的位置。这样做的好处是句柄本身可以自由复制而无需考虑生命周期问题,且不受Rust借用检查器的约束。 + - `GenericArena`是一个**连续存储的内存池**,内部使用`Vec`管理所有数据条目。它维护了一个空闲列表(`free_head`)来记录已被释放的位置,实现了O(1)的分配和释放操作。与普通`Vec`不同,Arena中数据的地址(即索引)一旦分配就保持稳定,不会因为后续的内存分配而改变。 + - `UniqueArena`是一个**值唯一的内存池**,在`GenericArena`的基础上增加了哈希索引。它确保相同的值在内存池中只存储一份,实现了类似字符串驻留的机制。这在编译器中用于标识符、字面量等需要唯一化存储的场景。 +2. `linked_list.rs` 是一个侵入式链表,其特点是链接信息直接存储在节点内部,而非由容器管理。 + - `LinkedListNode`特性定义了节点作为链表元素所需具备的行为,包括获取和设置前驱后继节点,以及节点所属容器的信息。任何需要被组织成链表的类型都必须实现这个trait。 + - `LinkedListContainer` 特性定义了链表容器所需的行为,包括管理链表的头尾节点,以及在链表层面进行操作的接口。它提供了在链表前后添加节点、合并链表、分割链表等操作。 + - `LinkedListIterator` 和 `LinkedListCursor`是两种遍历链表的方式: + - **迭代器**提供了标准的双端迭代能力 + - **游标**提供了更灵活的遍历控制,支持不同的访问策略(先访问后移动或先移动后访问)和遍历方向 + +### 3.2 关于`defuse.rs` + +> 这个文件实现了一个**定义-使用链(Def-Use Chain)**的数据结构,定义-使用链记录了程序中**值的定义**与其**使用位置**之间的关系。在编译器中间表示(IR)中,每条指令通常会定义一个新的值(如运算结果),同时也会使用一些已经定义的值(如操作数)。建立这些关系对于数据流分析、死代码消除、常量传播等优化都很重要。 +1. `Useable` 特性是一个**标记接口**,表明一个类型可以作为“被使用的值”。任何实现了这个trait的类型(如指令、基本块参数等)都可以维护自己的使用者列表。 +2. `User`是一个**使用位置描述符**,记录了某个值在何处被使用。它由**使用者指令**(哪条指令使用了这个值)和**操作数索引**(在该指令的哪个操作数位置使用了这个值)组成。值得注意的是,`User`是一个泛型结构,但其泛型参数`T`只存在于类型标记(`PhantomData`)中,并不实际存储T类型的值。这意味着同一个`User`结构可以适用于不同类型的被使用值,类型系统可以确保类型安全,但运行时没有额外开销 + +以一条加法指令为例: +``` +%3 = add %1 %2 +``` +这条指令定义了一个新值`%3`,同时使用了两个已经存在的值`%1`和`%2`。在Def-Use链中: +- 对于`%1`的值,会记录一个使用者`User { instruction: %3所在指令, index: 1 }` +- 对于`%2`的值,会记录一个使用者`User { instruction: %3所在指令, index: 2 }` +- 对于`%3`的值,如果它在后续指令中被使用,也会记录相应的使用者 + +> **说明:** 对于一个最小化的Demo来说原本可以省略这个的实现,,但是还是加上了,是想说明中间代码的生成可以是很灵活的,在生成的过程中可以根据自己具体设计的数据结构加入很多自己的想法,比如在生成过程中直接维护好定义使用关系,不需要使用额外的遍来做这一步工作。整个中间代码的生成可以概括为遍历语法树创建IR,只要不影响这条主线的正确性,可以额外的自己添加维护一些感觉对后续机器代码生成或者优化实现有用的信息。 + +### 3.3 关于`irgen.rs` + +> 这个文件实现了一个**从抽象语法树(AST)到中间表示(IR)的转换器**,是编译器前端与中端的核心接口。IR生成器负责将经过语法分析得到的抽象语法树转换为编译器内部使用的中间表示。这个过程本质上是一种**语义保留的结构变换**,将源语言的层次化结构转化为更适合分析与优化的线性化表示。 +1. `IrGenResult`是一个**统一的结果类型**,用于区分两种不同的IR实体,这种区分是因为全局变量和局部值的访问方式不同。 + - **全局变量**(`Global`):存储在静态数据区,需要通过地址访问 + - **普通值**(`Value`):可以是常量、临时变量或指令结果 +2. `IrGenContext`是**IR生成过程中的上下文环境**,维护了生成过程中需要的所有状态信息,这里的iv、v因为示例代码不支持函数调用和循环或者if指令,所以其实没有用到,给出是想更清晰的展示IrGenContext存在的意义。 + 1. **核心上下文**(`ctx`):整个IR模块的全局信息 + 2. **符号表**(`symboltable`):记录变量定义与使用的关系 + 3. **当前函数状态**(`cur_func`、`cur_func_id`、`cur_bbk`):跟踪正在生成的函数及其基本块 + 4. **循环控制栈**(`loop_entry_stack`、`loop_exit_stack`):处理循环嵌套时的跳转目标 + 5. **返回地址信息**(`cur_ret_slot`、`cur_ret_bbk`):统一函数出口的管理。 + +### 3.4 关于`ir2string.rs` + +> 这个文件实现了一个**将IR转换为字符串的接口**,用于调试和测试。从这个文件我们可以得到一个认识是我们编译器内存中的IR和我们输出的IR其实是可以不一致的,我们编译器中的IR可以并不完全遵循LLVM IR的规范,但是只要输出的文本格式符合LLVM IR的.ll文件的语法要求就可以借助LLVM的工具。 + +## 4. 当前示例实现说明 + +同Lab1,本实例代码面向的时Sysy的简化子集,只支持int和void类型以及加法运算,扩展的巴克斯范式可以参见Lab1的相关文档,具体的一个示例可以参考`test/test_case/functional/simple_add.sy`,IR命令的支持也仅支持Sysy简化子集,仅包含如下指令: +| 类别 | 助记符 | 完整格式 | 操作数 | 返回值 | +|------|--------|----------|--------|--------| +| 终结 | `ret` | `ret void` | 0 | 无 | +| 终结 | `ret` | `ret ` | 1 (值) | 无 | +| 终结 | `br` | `br label ` | 1 (基本块) | 无 | +| 双目 | `add` | ` = add , ` | 2 (左值, 右值) | 运算结果 | +| 访存 | `alloca` | ` = alloca ` | 0 | 指针 | +| 访存 | `store` | `store , * ` | 2 (值, 指针) | 无 | +| 访存 | `load` | ` = load , * ` | 1 (指针) | 加载的值 | + +## 5. 构建与生成流程 + +运行实例代码,查看实验1的具体效果可以使用下面的命令,在项目根目录下: +```bash +cargo build -r +./target/release/compiler -ir ./test/output/simple_add.ll ./test/test_case/functional/simple_add.sy +``` +这样解析出来的语法树会输出到`./test/output/simple_add.ll`文件中。 + +如果输出的IR符合LLVM IR的语法(实验示例代码所提供的IR生成器生成的IR符合LLVM IR语法),可以使用下面的命令运行.ll文件: +```bash +lli ./test/output/simple_add.ll +``` +然后可以通过下面这条命令产看运行main函数的返回值,这条命令的本质是查看上一个运行程序的结束状态 +```bash +echo $? +``` +效果可以参见下图: +![alt text](./img/image1.png) +当然,我们更建议通过写Python或者.sh脚本的方式将运行测试流程自动化 + +