# Lab4：寄存器分配

## 1. 本实验定位

本仓库当前提供了一个“最小可运行”的 IR -> AArch64 汇编示例链路。  
Lab4 的目标是在 Lab3 示例基础上，把“固定寄存器 + 栈槽”的最小后端实现推进为“虚拟寄存器 -> 物理寄存器”的真实后端阶段，为完整 SysY 后端打基础。

## 2. Lab4 要求

需要同学完成：

1. 熟悉 MIR 中寄存器、操作数、栈槽与机器函数之间的关系。
2. 理解当前 IR -> MIR -> 汇编输出流程中寄存器相关部分的最小实现现状。
3. 扩展当前 MIR 表达，使指令选择阶段能够产出虚拟寄存器，而不是继续固定使用 `w0`、`w8`、`w9`。
4. 在现有框架上实现真实寄存器分配，并处理 spill/reload、栈槽管理、callee-saved 保存恢复等后续问题。
5. 图着色寄存器分配与线性扫描寄存器分配均可作为实现路线，同学可自行选择其中一种完成。
6. 在 `test/test_case` 提供的全部测试用例上验证正确性，并在保证功能正确的前提下尽量减少冗余 spill/reload 与访存，提升生成代码质量。

## 3. 当前代码框架（与 Lab4 直接相关）

1. MIR 定义与寄存器相关抽象
   - `include/mir/MIR.h`
   - `src/mir/MIRContext.cpp`
   - `src/mir/MIRFunction.cpp`
   - `src/mir/MIRBasicBlock.cpp`
   - `src/mir/MIRInstr.cpp`
   - `src/mir/Register.cpp`

2. IR -> MIR、寄存器分配与后续落地
   - `src/mir/Lowering.cpp`
   - `src/mir/RegAlloc.cpp`
   - `src/mir/FrameLowering.cpp`
   - `src/mir/AsmPrinter.cpp`

3. 入口流程
   - `src/main.cpp`
   - `src/utils/CLI.h`
   - `src/utils/CLI.cpp`

## 4. Lab4 需要补充的内容

1. 必须修改的文件
   - `include/mir/MIR.h`（扩展寄存器、操作数、机器函数等数据结构，使其能够表示虚拟寄存器与分配结果）
   - `src/mir/Register.cpp`（补充物理/虚拟寄存器表示、可分配寄存器集合等）
   - `src/mir/MIRInstr.cpp`（当需要扩展机器指令或操作数表达时）
   - `src/mir/MIRFunction.cpp`（当需要维护虚拟寄存器、spill 栈槽、callee-saved 等状态时）
   - `src/mir/Lowering.cpp`（将当前固定寄存器写法改造为生成虚拟寄存器形式的 MIR）
   - `src/mir/RegAlloc.cpp`（实现真实寄存器分配主逻辑，可选择图着色或线性扫描）
   - `src/mir/FrameLowering.cpp`（根据寄存器分配结果完成栈帧布局、spill 栈槽计算与保存恢复）
   - `src/mir/AsmPrinter.cpp`（保证寄存器分配后的 MIR 能正确打印为最终汇编）

2. 视实现需要可能修改
   - `src/mir/MIRBasicBlock.cpp`（当需要扩展基本块级活跃性或辅助接口时）
   - `src/main.cpp`（当需要调整后端阶段行为时）
   - `src/utils/CLI.cpp`（当需要扩展后端调试相关命令行选项时）
   - `scripts/verify_asm_with_qemu.sh`（当需要扩展统一验证脚本时）

## 5. 当前最小示例实现说明

当前后端中的寄存器相关实现仍停留在最小示例阶段：

1. `Lowering.cpp` 当前直接使用固定物理寄存器 `w0`、`w8`、`w9` 生成机器指令，而不是先生成虚拟寄存器。
2. `RegAlloc.cpp` 当前仅执行最小一致性检查，不实现真实寄存器分配。
3. 当前 MIR 主要围绕单函数 `main`、单基本块与最小指令子集工作，尚未形成完整课程版本所需的寄存器分配基础设施。
4. `FrameLowering.cpp` 与 `AsmPrinter.cpp` 当前默认前面阶段已经给出可直接落地的固定寄存器结果，并未围绕完整 RA 流程展开。
5. 因此，当前代码实际上**没有实现寄存器分配**，这一部分需要同学自行完成。


说明：本阶段不应继续沿用 Lab3 的“所有中间值统一写回栈槽 + 固定寄存器临时搬运”的做法，而应先把指令选择结果改造成带虚拟寄存器的 MIR，再进入寄存器分配阶段。无论选择哪一种算法，都需要先解决几个共同前提：为机器指令补充 `use/def` 信息、能够遍历机器基本块与控制流关系、为虚拟寄存器维护分配状态，并在 spill 后为新引入的访存指令重新参与后续流程。

可选的两条常见实现路线如下：

1. 图着色寄存器分配
   - 整体思路：把“两个虚拟寄存器若在某一程序点同时活跃，则不能分配到同一个物理寄存器”转化为图着色问题。图中的结点表示虚拟寄存器，边表示二者互相干涉；若有 `K` 个可分配物理寄存器，则目标是对干涉图进行 `K` 着色。
   - 典型步骤：
     1. 先对 MIR 做活跃性分析，计算各基本块或各指令位置的 live-in/live-out。
     2. 根据活跃信息构建干涉图；若需要优化 move，也可以额外记录可合并关系。
     3. 按照可分配寄存器数 `K` 对图执行 simplify/select，必要时结合启发式选择 spill 候选。
     4. 若图可以着色，则回填每个虚拟寄存器对应的物理寄存器；若不能着色，则把选中的虚拟寄存器重写为 spill/reload 形式，并重新进行分析与分配。
     5. 分配完成后，把使用到的 callee-saved 寄存器、额外 spill 栈槽等信息交给 `FrameLowering.cpp` 与 `AsmPrinter.cpp` 继续处理。
   - 说明：图着色方法更接近经典教材中的完整后端流程，适合面向完整 SysY 后端逐步扩展；但实现成本通常更高，需要同学自己补齐活跃性分析、干涉图维护与 spill 重试机制。

2. 线性扫描寄存器分配
   - 整体思路：先把每个虚拟寄存器的活跃范围抽象为一个区间，再按照区间起点顺序扫描程序，动态维护当前正在占用物理寄存器的活跃区间集合；若出现寄存器不够用，再选择某个区间 spill。
   - 典型步骤：
     1. 为机器指令建立稳定顺序，并结合活跃性信息计算每个虚拟寄存器的 live interval。
     2. 按区间起点排序后顺序扫描，维护当前仍然活跃的区间集合 `active`。
     3. 每处理到一个新区间时，先移除已经结束的区间并释放其占用的物理寄存器。
     4. 若存在空闲物理寄存器，则直接分配；若没有空闲寄存器，则比较当前区间与 `active` 中已有区间的结束位置，选择 spill 当前区间或 spill 一个结束更晚的旧区间。
     5. 对 spill 的虚拟寄存器插入 reload/store 后，需要重新计算受影响区间，再继续后续分配与汇编落地。
   - 说明：线性扫描通常更容易先做出一个可运行版本，在函数数量较多、实现周期较紧的课程环境中也较常见；但如果要把效果做得更好，仍然需要认真处理区间切分、调用点约束、callee/caller-saved 寄存器使用策略等问题。

无论采用图着色还是线性扫描，都不应把寄存器分配理解为“把虚拟寄存器简单替换成物理寄存器名字”。真正完整的实现还需要和 spill/reload、栈帧布局、callee-saved 保存恢复以及最终汇编输出联动，否则后端仍然无法支撑完整 SysY 程序。

## 6. 构建与运行

```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j "$(nproc)"
```

## 7. Lab4 验证方式

项目编译后可先按当前最小样例检查后端链路是否仍能运行：

```bash
./build/bin/compiler --emit-asm test/test_case/simple_add.sy
```

推荐继续使用统一脚本验证 “源码 -> 汇编 -> 可执行程序” 整体链路，用于做最小回归：

```bash
./scripts/verify_asm_with_qemu.sh test/test_case/simple_add.sy out/asm --run
```

完成 Lab4 后，最终不应只停留在 `simple_add` 这一最小示例，而应对 `test/test_case` 下全部测试用例逐个回归，确保生成代码功能正确；在此基础上，再尽量减少不必要的 spill/reload、冗余访存与低效寄存器使用，以提升最终性能表现。
