From 109a6a21b7143088783b485edf78b7d18e9f0ff8 Mon Sep 17 00:00:00 2001 From: yuki-328 Date: Thu, 30 Jul 2026 10:55:33 +0800 Subject: [PATCH 01/12] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3=E5=92=8C=E6=8A=80=E6=9C=AF?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...07\346\241\243\345\210\235\347\250\277.md" | 513 ++++++++++++++ ...07\346\241\243\345\210\235\347\250\277.md" | 655 ++++++++++++++++++ 2 files changed, 1168 insertions(+) create mode 100644 "docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" create mode 100644 "docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" diff --git "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" new file mode 100644 index 0000000..054f209 --- /dev/null +++ "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" @@ -0,0 +1,513 @@ +# ScratchV 课题 14:常量加载合并优化开发文档 + +> **文档版本**:v0.1(开发计划初稿) +> **创建日期**:2026-07-28 +> **作者**:[yuki] +> **关联 Issue**:[#待补充] +> **涉及模块**:`scratchv/backend/`、`scratchv/compiler.py`、`scratchv/main.py`、`tests/`、`docs/` + +--- + +## 0. 开发前说明 + +ScratchV 主分支已经存在课题 14 的参考模块、测试和命令行集成。本开发计划将任务定义为: + +> 对现有常量加载合并优化进行行为梳理、安全性加固、共享解析器重构、边界测试补全和集成验证。 + + +--- + +## 1. 功能概述与目标 + +### 1.1 背景与动机 + +- **现状问题**:RISC-V 加载一般 32 位常量常出现 `lui + addi` 序列;重复加载相同高 20 位还可能产生冗余 `lui`。现有参考实现具备基本功能,但仍需要补充控制流安全、寄存器别名、共享解析器和更严格的测试。 +- **应用场景**:处理 ScratchV 后端生成的汇编、外部 RISC-V 汇编文件、课程测试 fixture,以及汇编级窥孔优化和统计流程。 +- **现实限制**:ScratchV 当前指令选择对常量通常直接生成 `li`,因此 `lui+addi` 合并规则在主流水线中的命中率可能不高;必须用独立汇编输入验证核心逻辑。 + +### 1.2 功能描述 + +- **一句话定义**:在汇编生成后识别安全的常量加载模式,将 `lui+addi` 规范化为 `li`,并删除同一基本块内可证明冗余的 `lui`。 +- **核心价值**:提高汇编可读性,消除真实冗余指令,建立可复用的汇编级优化开发流程。 + +### 1.3 目标与非目标 + +| 类型 | 内容 | +|---|---| +| ✅ 包含范围 | RV32 数值立即数;12 位符号扩展;相邻 `lui+addi`;基本块内冗余 `lui`;寄存器别名;迭代扫描;统计;独立 CLI;主编译器开关;单元与集成测试 | +| ❌ 不包含范围 | RV64 完整常量构造;符号重定位;跨基本块数据流;跨函数复用;IR 级常量折叠;完整汇编器实现 | + +--- + +## 2. 设计与规格说明 + +### 2.1 外部接口 + +#### Python API + +保持: + +```python +from scratchv.backend.const_merge import merge_constants + +optimized_asm, changes = merge_constants(asm_text) +``` + +可选新增: + +```python +optimized_asm, stats = merge_constants_detailed(asm_text) +``` + +#### 独立 CLI + +```bash +python -m scratchv.backend.const_merge input.s -o output.s -v +``` + +#### ScratchV 主 CLI + +```bash +scratchv input.dsl -o output.s --const-merge +``` + +注意:归档课题写的是 `--merge-constants`,但当前主分支真实参数为 `--const-merge`。本开发默认服从当前代码;是否增加旧名称别名由评审决定。 + +### 2.2 内部设计 + +#### 数据结构 + +- 复用 `scratchv/backend/_asm_parser.py` 中的 `ParsedAsmLine`; +- 新增或内部使用 `ConstantMergeStats`; +- 使用 `dict[str, int]` 跟踪基本块内各物理寄存器的最后一次 `lui` 值; +- 使用 ABI 名到 `xN` 的映射做寄存器规范化。 + +#### 核心处理流程 + +```text +读取汇编文本 + -> parse_asm + -> 固定点循环 + -> 合并安全的 lui+addi + -> 删除基本块内冗余 lui + -> lines_to_asm + -> 返回输出与统计 +``` + +#### 状态清空条件 + +遇到以下情况清空或失效化寄存器状态: + +- 标签; +- 条件分支; +- 无条件跳转; +- `call`、`jal`、`jalr`; +- `ret`、`jr`; +- 明确写目标寄存器的指令; +- 无法可靠分析的未知指令。 + +### 2.3 模块间交互 + +- **上游**:接收 `AsmEmitter` 或线性扫描寄存器分配器生成的汇编文本,也可直接接收用户输入 `.s`; +- **下游**:输出给调度器、汇编美化器、指令计数器和最终文件写入; +- **对 IR 无影响**:不修改 AST、IR 或寄存器分配结果; +- **Pass 顺序**:建议位于汇编窥孔之后、调度器之前。 + +--- + +## 3. 开发环境与基线 + +### 3.1 Ubuntu 24.04 环境初始化 + +```bash +git clone https://github.com/ScratchV-Compiler/ScratchV.git +cd ScratchV + +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +pip install -e . +``` + +可选汇编验证工具: + +```bash +sudo apt update +sudo apt install binutils-riscv64-linux-gnu +``` + +### 3.2 建立功能分支 + +```bash +git switch main +git pull +git switch -c feat/topic14-const-merge +``` + +### 3.3 运行基线测试 + +```bash +pytest tests/test_const_merge.py -v +pytest tests/ -q +``` + +记录: + +- 当前 commit hash; +- Python 版本; +- 测试总数、通过数、失败数; +- 当前 `const_merge` 测试输出; +- 一份未修改前的示例汇编结果。 + +```bash +git rev-parse HEAD +python --version +``` + +--- + +## 4. 涉及文件清单 + +| 文件路径 | 修改类型 | 修改内容概述 | +|---|---|---| +| `scratchv/backend/const_merge.py` | 重点修改 | 复用共享解析器;实现安全匹配、寄存器规范化、块边界处理、固定点迭代和详细统计 | +| `scratchv/backend/_asm_parser.py` | 复用/小改 | 必要时补充公开的寄存器定义/使用判断;避免在 const-merge 中再写一套解析器 | +| `scratchv/compiler.py` | 检查/小改 | 确认 `const_merge` 在 post-codegen 中的顺序和统计输出;必要时接入详细统计 | +| `scratchv/main.py` | 检查/小改 | 确认 `--const-merge` 参数及配置传递;可选增加旧参数别名 | +| `tests/test_const_merge.py` | 重点修改 | 补充边界、安全、别名、迭代、幂等性和 CLI 测试 | +| `tests/test_backend.py` | 修改 | 增加 CompilerConfig/CompilerDriver 集成验证 | +| `tests/fixtures/const_merge/*.s` | 新增 | 存放可读的汇编输入、预期输出和控制流反例 | +| `docs/topics/14-常量加载合并优化.md` 或对应文档源 | 修改 | 更新算法原理、限制、命令和测试结果 | + +实际提交前先用以下命令确认路径: + +```bash +find scratchv/backend -maxdepth 1 -type f | sort +grep -R "const_merge" -n scratchv tests docs | head -100 +``` + +--- + +## 5. 分步实现计划 + +### 步骤 0:确认课题边界 + +**任务**:向维护者确认 RV32/RV64、跨基本块、API 和 CLI 命名。 +**产出**:评审结论写入设计文档“工作假设”章节。 +**验证**:所有待评审问题有明确答案或标记为非目标。 + +### 步骤 1:补 characterization tests + +**任务**:先把当前实现行为固定下来,不立即重构。 +**产出**:现有正向样例、无匹配样例、CLI 可导入测试。 +**验证**:修改前新增测试通过,或明确暴露当前 bug。 + +### 步骤 2:迁移到共享汇编解析器 + +**任务**:用 `parse_asm`、`lines_to_asm` 和 `classify_def_use` 替代重复解析逻辑。 +**产出**:`const_merge.py` 不再维护完整的独立 `AsmInst` 解析器,或仅保留兼容别名。 +**验证**:标签、注释、空行、内存操作数的解析测试通过。 + +### 步骤 3:实现寄存器规范化 + +**任务**:建立 ABI 名到 `xN` 的映射。 +**产出**:`canonical_reg("t0") == "x5"`。 +**验证**:别名混用测试通过。 + +### 步骤 4:实现 `lui+addi` 安全合并 + +**任务**: + +- 检查操作码和操作数数量; +- 检查 `rd == addi.rd == addi.rs1`; +- 只接受纯数值立即数; +- 处理 12 位符号扩展; +- 按 RV32 截断; +- 不跨标签; +- 保留注释。 + +**产出**:`merge_lui_addi_once()`。 +**验证**:正数、负数和边界立即数测试通过。 + +### 步骤 5:实现块内冗余 `lui` 消除 + +**任务**: + +- 维护 `lui_state`; +- 遇到定义寄存器的指令使对应状态失效; +- 遇到标签、分支、跳转、调用和返回清空状态; +- 使用寄存器规范化; +- 未知指令保守处理。 + +**产出**:`remove_redundant_lui_once()`。 +**验证**:安全删除和控制流反例测试通过。 + +### 步骤 6:实现固定点迭代与统计 + +**任务**:迭代运行两个规则直到无变化,记录分类统计。 +**产出**:`ConstantMergeStats`、迭代次数和兼容包装函数。 +**验证**:需要两轮才能完成的样例通过;第二次运行零变化。 + +### 步骤 7:集成主编译器 + +**任务**:检查: + +```text +CompilerConfig.const_merge +main.py --const-merge +CompilerDriver._run_asm_passes +``` + +**产出**:主 CLI 和 Python 配置均可启用优化。 +**验证**:集成测试确认参数被传递并调用优化器。 + +### 步骤 8:工具链语义验证 + +**任务**:把优化前后汇编分别交给 GNU assembler。 +**产出**:对象文件和反汇编对比记录。 +**验证**:两者能汇编,并在测试程序中产生相同返回值/寄存器值。 + +示例: + +```bash +riscv64-linux-gnu-as -march=rv32im before.s -o before.o +riscv64-linux-gnu-as -march=rv32im after.s -o after.o +riscv64-linux-gnu-objdump -d -M no-aliases before.o > before.dump +riscv64-linux-gnu-objdump -d -M no-aliases after.o > after.dump +diff -u before.dump after.dump +``` + +`li` 可能重新展开为 `lui+addi`,因此反汇编差异不应只看源文件行数。 + +### 步骤 9:回归、文档与 PR + +**任务**: + +```bash +pytest tests/test_const_merge.py -v +pytest tests/ -q +make check # 若当前仓库提供 +``` + +**产出**:代码、测试、设计文档、开发文档、使用说明和 PR 描述。 +**验证**:所有验收标准完成。 + +--- + +## 6. 异常处理与边界条件 + +- [ ] 空字符串输入返回空结果和零变化; +- [ ] 只有注释或标签时不报错; +- [ ] 操作数缺失时跳过,不抛出未处理异常; +- [ ] `0x7FF`、`0x800`、`0xFFF` 正确符号扩展; +- [ ] `0x80000000` 等 32 位边界按 RV32 规范化; +- [ ] `-0x1` 可解析; +- [ ] `%hi(symbol)`、`%lo(symbol)` 保持原样; +- [ ] `t0` 和 `x5` 被视为同一寄存器; +- [ ] 标签或分支不会导致错误删除; +- [ ] `call` 后不复用 caller-saved 寄存器状态; +- [ ] 注释和标签不会静默丢失; +- [ ] 未知操作码采用保守策略; +- [ ] 达到迭代上限时能正常停止; +- [ ] 优化结果具有幂等性。 + +--- + +## 7. 测试与验证方案 + +### 7.1 单元测试矩阵 + +| 编号 | 场景 | 输入关键点 | 预期 | +|---|---|---|---| +| T01 | 基本合并 | `lui t0,0x12345` + `addi t0,t0,0x678` | 一条 `li` | +| T02 | 低位最大正数 | `0x7FF` | +2047 | +| T03 | 低位最小负数 | `0x800` | -2048 | +| T04 | 低位 -1 | `0xFFF` | -1 | +| T05 | 目标不同 | `addi t1,t0,...` | 不合并 | +| T06 | 源不同 | `addi t0,t1,...` | 不合并 | +| T07 | 中间注释 | 注释/空行 | 可按设计合并并保留注释 | +| T08 | 中间标签 | `L1:` | 不合并 | +| T09 | 重定位 | `%hi/%lo` | 不合并 | +| T10 | 安全冗余 LUI | 同块、同寄存器、同立即数 | 删除后一个 | +| T11 | 中间 clobber | `addi t0,t0,1` | 不删除 | +| T12 | 寄存器别名 | `t0` 与 `x5` | 正确识别修改 | +| T13 | 跨标签 | 标签两侧相同 `lui` | 不删除 | +| T14 | 跨分支 | 分支可能绕过前一 `lui` | 不删除 | +| T15 | 调用边界 | `call foo` | 清空状态 | +| T16 | 迭代暴露模式 | 删除冗余后出现相邻对 | 完成二次优化 | +| T17 | 幂等性 | 对结果再次优化 | 0 变化 | +| T18 | 空输入 | `""` | 空输出、0 变化 | + +### 7.2 集成测试 + +- **独立模块**:输入 `.s`,检查 `-o` 文件和 `-v`; +- **主 CLI**:检查 `--const-merge` 参数解析和配置映射; +- **CompilerDriver**:检查 post-pass 顺序; +- **汇编器**:优化前后均可汇编; +- **模拟器**:至少 3 个可执行用例结果一致; +- **回归**:全量测试通过。 + +### 7.3 建议 fixture + +```text +tests/fixtures/const_merge/ +├── basic_pair.s +├── sign_extension.s +├── redundant_lui.s +├── control_flow_guard.s +├── register_alias.s +└── relocation_noop.s +``` + +--- + +## 8. 验收标准(Definition of Done) + +- [ ] 设计文档已评审,范围和非目标明确; +- [ ] `merge_constants` 现有公共接口保持兼容; +- [ ] 使用共享汇编解析器,或对不迁移给出明确理由; +- [ ] `lui+addi` 正确处理 12 位符号扩展和 RV32 截断; +- [ ] 冗余 `lui` 只在可证明安全的基本块内删除; +- [ ] 正确处理 ABI/数字寄存器别名; +- [ ] 不优化重定位表达式; +- [ ] 实现固定点迭代和统计; +- [ ] 新增正向、负向、边界和反例测试; +- [ ] `pytest tests/test_const_merge.py -v` 通过; +- [ ] `pytest tests/ -q` 全量通过; +- [ ] 至少 3 个汇编样例通过工具链或模拟器等价验证; +- [ ] 文档明确说明 `li` 是伪指令; +- [ ] 主 CLI `--const-merge` 可用; +- [ ] PR 描述包含前后示例、测试结果和已知限制。 + +--- + +## 9. 风险评估与依赖 + +| 风险项 | 影响程度 | 缓解措施 | +|---|---|---| +| 错误理解 `li` 的性能收益 | 高 | 用 objdump 展开验证;统计分类 | +| 符号扩展或位宽错误 | 高 | 数学公式 + 边界测试 | +| 跨块错误删除 | 高 | 在标签和控制流边界清空状态 | +| 寄存器别名错误 | 高 | 统一映射为 `xN` | +| 解析器重构导致格式回归 | 中 | 先写 characterization tests | +| 现有后端不产生 `lui+addi` | 中 | 用独立 `.s` fixture;记录命中率 | +| GNU 汇编器版本差异 | 低/中 | 限定测试命令和 `-march=rv32im` | +| 与调度器顺序冲突 | 中 | const-merge 固定在 scheduler 前 | + +- **Python 依赖**:项目现有依赖,原则上不新增第三方 Python 库; +- **可选工具链**:`binutils-riscv64-linux-gnu`、TinyFive、Spike; +- **兼容性**:默认不改变未开启 `--const-merge` 时的编译结果。 + +--- + +## 10. 开发进度跟踪 + +以下日期为建议示例,可按课程安排调整: + +| 阶段 | 计划完成日期 | 状态 | +|---|---|---| +| 仓库走读与基线记录 | 2026-07-29 | ⬜ 待开始 | +| 设计文档评审 | 2026-07-31 | ⬜ 待开始 | +| Characterization tests | 2026-08-02 | ⬜ 待开始 | +| 核心编码与重构 | 2026-08-07 | ⬜ 待开始 | +| 边界测试与调试 | 2026-08-11 | ⬜ 待开始 | +| 工具链等价验证 | 2026-08-13 | ⬜ 待开始 | +| 文档完善与 PR | 2026-08-15 | ⬜ 待开始 | +| 代码审查与修订 | 2026-08-18 | ⬜ 待开始 | + +--- + +## 11. 第一天实际工作清单 + +按以下顺序开展,不要直接开始改算法: + +```bash +# 1. 进入仓库和环境 +cd ScratchV +source .venv/bin/activate + +# 2. 确认分支与基线 +git status +git rev-parse HEAD +pytest tests/test_const_merge.py -v + +# 3. 找到所有相关代码 +grep -R "const_merge\|merge_constants\|--const-merge" -n scratchv tests docs + +# 4. 阅读顺序 +# scratchv/backend/const_merge.py +# scratchv/backend/_asm_parser.py +# scratchv/compiler.py +# scratchv/main.py +# tests/test_const_merge.py +# scratchv/backend/instruction_select.py +# scratchv/backend/asm_emit.py + +# 5. 新建分支 +git switch -c feat/topic14-const-merge +``` + +阅读时建立一张表: + +| 问题 | 当前答案 | 证据文件/行 | 是否要修改 | +|---|---|---|---| +| 优化位于哪个阶段? | post-codegen | `compiler.py` | 否/确认 | +| 当前 CLI 名称? | `--const-merge` | `main.py` | 评审 | +| 当前是否迭代? | 待代码确认 | `const_merge.py` | 是 | +| 是否跨标签清空状态? | 待测试确认 | `const_merge.py` | 是 | +| 是否处理寄存器别名? | 待测试确认 | `const_merge.py` | 是 | +| 当前后端是否产生 `lui+addi`? | 常量通常直接输出 `li` | `instruction_select.py` | 记录限制 | + +当天结束时应得到: + +1. 基线测试结果; +2. 相关文件关系图; +3. 5–10 个明确测试用例; +4. 设计文档 v0.1; +5. 需要确认的问题列表; +6. 尚未修改核心代码的干净功能分支。 + +--- + +## 12. PR 描述建议结构 + +```markdown +## What +实现/改进 RV32 常量加载合并优化: +- 安全合并 lui+addi +- 基本块内冗余 lui 消除 +- 寄存器别名处理 +- 固定点迭代与统计 + +## Why +现有实现缺少若干控制流与边界测试,且重复维护汇编解析逻辑。 + +## Safety +- 不跨标签/分支/调用 +- 不处理重定位表达式 +- 按 RV32 语义截断 + +## Tests +- pytest tests/test_const_merge.py -v +- pytest tests/ -q +- GNU assembler + objdump 对比 + +## Limitations +- 不支持 RV64 完整常量构造 +- 不做跨基本块数据流分析 +- li 为伪指令,不保证减少最终机器指令 +``` + +--- + +## 13. 参考资料 + +- ScratchV 课程首页: +- 课题 14 课程页: +- 课题 14 归档说明: +- 当前实现: +- 当前共享解析器: +- 当前测试: +- 贡献指南: +- RISC-V RV32I 规范: + diff --git "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" new file mode 100644 index 0000000..edd1f98 --- /dev/null +++ "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" @@ -0,0 +1,655 @@ +# ScratchV 课题 14:常量加载合并优化技术设计文档 + +> **文档版本**:v0.1(设计初稿) +> **编写日期**:2026-07-28 +> **作者**:[yuki] +> **关联 Issue**:[#待补充] +> **涉及模块**:`scratchv/backend/const_merge.py`、`scratchv/backend/_asm_parser.py`、`scratchv/compiler.py`、`scratchv/main.py` +> **目标架构**:RV32I/RV32IM,GNU Assembler(GAS)语法 +> **课题定位**:在现有参考实现基础上,完成安全性分析、重构设计、测试补全和编译器集成验证 + +--- + +## 0. 文档说明与工作假设 + +ScratchV 主分支已经存在 `scratchv/backend/const_merge.py` 参考实现,课程网站也将课题 14 标记为“已完成”。因此,本课题不应简单复制现有代码,而应完成以下工作: + +1. 理解并准确说明 `lui`、`addi` 与 `li` 的语义; +2. 分析当前实现的适用范围和潜在安全问题; +3. 设计一个行为明确、保守安全、可测试的汇编后处理优化; +4. 补充边界测试、集成测试和优化统计; +5. 明确区分“汇编文本行数减少”和“真实机器指令数减少”。 + +本初稿采用以下假设,提交评审前需要与指导教师或项目维护者确认: + +- 第一阶段只保证 **RV32** 语义,不处理 RV64 任意宽常量展开; +- 第一阶段只在 **单基本块内部** 消除冗余 `lui`,不做跨基本块数据流分析; +- 只处理数值立即数,不处理 `%hi(symbol)`、`%lo(symbol)` 等重定位表达式; +- 保持公共函数 `merge_constants(asm_text) -> tuple[str, int]` 兼容; +- ScratchV 主命令行开关使用当前代码中的 `--const-merge`,而不是归档题目中的 `--merge-constants`。 + +--- + +## 一、功能介绍 + +### 1.1 背景 + +RISC-V 基础整数指令为定长编码。`addi` 只能携带 12 位有符号立即数,而 `lui` 将 20 位 U 型立即数放入目标寄存器的高 20 位、低 12 位补零。加载一般 32 位常量时,汇编器或编译器常使用: + +```asm +lui t0, 0x12345 +addi t0, t0, 0x678 +``` + +其结果为: + +```text +value = ((imm_hi & 0xFFFFF) << 12) + sign_extend_12(imm_lo) +``` + +在 RV32 中,最终结果按 32 位截断。 + +### 1.2 功能概述 + +常量加载合并优化是一个 **RISC-V 汇编层 post-pass**,在代码生成之后扫描汇编文本,执行两类转换: + +1. **常量序列规范化**:将符合条件的相邻 `lui + addi` 序列重写为 `li` 伪指令; +2. **冗余真实指令消除**:若同一物理寄存器在同一基本块中重复执行相同 `lui`,且中间没有被修改,则删除后一次 `lui`。 + +### 1.3 价值说明 + +- 统一常量加载的汇编表示,提高可读性; +- 删除可证明无用的 `lui`,减少真实机器指令; +- 为汇编级优化、指令统计和调度提供更规范的输入; +- 训练汇编解析、局部数据流跟踪、编译器 pass 集成和测试验证能力。 + +### 1.4 必须澄清的性能口径 + +`li` 是汇编器伪指令,不是 RV32I 的真实单条机器指令。对于较大的常量,汇编器通常仍会把 `li` 展开为 `lui + addi`。因此: + +- `lui + addi -> li` **必然减少汇编文本中的指令行数**; +- 但它 **不保证减少最终机器指令数或运行周期**; +- 真正可稳定减少机器指令的部分主要是安全的冗余 `lui` 消除; +- 若常量可用单条真实指令编码,例如有符号 12 位常量可使用 `addi rd, x0, imm`,则可以在后续增强版本中实现真实单指令替换。 + +文档、测试报告和优化统计不得把伪指令行数减少直接等同于机器指令数减少。 + +--- + +## 二、设计目标与非目标 + +### 2.1 设计目标 + +1. **语义正确**:正确处理 `addi` 12 位立即数的符号扩展和 RV32 截断; +2. **保守安全**:无法证明安全的序列不优化; +3. **基本块安全**:不跨标签、分支、跳转、调用和返回复用寄存器状态; +4. **别名正确**:把 `t0` 与 `x5` 等 ABI 名和数字寄存器名视为同一物理寄存器; +5. **幂等性**:对已优化结果再次运行不应继续发生变化; +6. **格式兼容**:尽可能保留标签、注释、空行和汇编指令顺序; +7. **可观测性**:统计合并对数、删除的冗余 `lui` 数和文本行数变化; +8. **可集成性**:支持独立模块 CLI 和 ScratchV 主编译流程开关。 + +### 2.2 非目标 + +第一阶段不实现: + +- RV64 任意 64 位常量的完整 `li` 展开与合并; +- `%hi(symbol)`、`%lo(symbol)`、`%pcrel_hi` 等重定位表达式优化; +- 跨基本块、跨循环或跨函数的全局常量传播; +- 基于控制流图的到达定义分析; +- 指令调度、寄存器分配或 IR 级常量折叠; +- 对所有 GNU/LLVM 汇编语法变体的完整支持。 + +--- + +## 三、当前系统分析 + +### 3.1 ScratchV 编译流水线中的位置 + +ScratchV 当前主要流程为: + +```text +DSL/ONNX 解析 + -> IR 优化 + -> RISC-V 指令选择 + -> 寄存器分配 + -> 汇编生成 + -> 汇编级 post-pass + 1. asm peephole + 2. const merge + 3. scheduler + 4. beautifier + 5. instruction counter + -> 输出 .s +``` + +常量加载合并属于 **汇编生成之后** 的局部优化,不修改 AST、IR 或机器指令数据结构。 + +### 3.2 当前代码接口 + +```python +from scratchv.backend.const_merge import merge_constants + +optimized_asm, changes = merge_constants(asm_text) +``` + +独立命令行: + +```bash +python -m scratchv.backend.const_merge input.s -o output.s -v +``` + +ScratchV 主命令行: + +```bash +scratchv input.dsl -o output.s --const-merge +``` + +### 3.3 当前实现值得改进的点 + +1. `const_merge.py` 自己维护一套 `AsmInst` 解析逻辑,而项目已有共享的 `_asm_parser.py`; +2. 当前共享解析器的文档声称供 const-merge 使用,但实际参考实现仍重复解析代码; +3. 当前冗余 `lui` 跟踪没有明确在标签和控制流边界清空,跨基本块可能产生不安全删除; +4. 当前实现按字符串比较寄存器,`t0` 与 `x5` 的别名可能导致错误判断; +5. 现有测试主要覆盖正向样例,缺少标签、分支、寄存器别名、符号立即数、负十六进制和幂等性测试; +6. 归档课题要求“迭代扫描”,参考实现目前主要是一次合并加一次冗余消除; +7. ScratchV 当前指令选择本身常直接生成 `li`,所以主编译流程中 `lui + addi` 合并规则的命中率可能很低,需要单独汇编 fixture 或其他后端输出验证; +8. 当前 `changes` 只给总数,无法区分文本规范化与真实冗余指令删除。 + +--- + +## 四、语义模型与计算规则 + +### 4.1 12 位符号扩展 + +```python +def sign_extend_12(value: int) -> int: + value &= 0xFFF + return value - 0x1000 if value & 0x800 else value +``` + +边界: + +| 输入编码 | 符号扩展结果 | +|---|---:| +| `0x000` | 0 | +| `0x7FF` | 2047 | +| `0x800` | -2048 | +| `0xFFF` | -1 | + +### 4.2 RV32 最终常量 + +```python +upper_u32 = ((imm_hi & 0xFFFFF) << 12) & 0xFFFFFFFF +lower_s32 = sign_extend_12(imm_lo) +final_u32 = (upper_u32 + lower_s32) & 0xFFFFFFFF +final_s32 = final_u32 if final_u32 < 0x80000000 else final_u32 - 0x100000000 +``` + +输出 `li` 时建议统一使用有符号十进制 `final_s32`,避免 Python 无界整数与 RV32 位宽语义混淆。若项目更偏好十六进制输出,也必须明确按 32 位规范化。 + +### 4.3 示例 + +```asm +lui t0, 0x12345 +addi t0, t0, 0x678 +``` + +```text +0x12345000 + 0x678 = 0x12345678 +``` + +```asm +lui t0, 0x1 +addi t0, t0, 0x800 +``` + +`0x800` 作为 12 位立即数解释为 `-2048`: + +```text +0x00001000 - 0x800 = 0x00000800 +``` + +--- + +## 五、总体架构 + +### 5.1 模块职责 + +```text +输入汇编文本 + | + v +共享汇编解析器 parse_asm() + | + v +ParsedAsmLine 列表 + | + +--> 规则 A:lui + addi 合并 + | + +--> 规则 B:块内冗余 lui 消除 + | + +--> 固定点迭代与统计 + v +lines_to_asm() + | + v +输出汇编文本 + 统计信息 +``` + +### 5.2 建议数据结构 + +```python +from dataclasses import dataclass + +@dataclass +class ConstantMergeStats: + merged_pairs: int = 0 + redundant_lui_removed: int = 0 + iterations: int = 0 + + @property + def total_changes(self) -> int: + return self.merged_pairs + self.redundant_lui_removed +``` + +为保持兼容: + +```python +def merge_constants(asm_text: str) -> tuple[str, int]: + optimized, stats = ConstantMergeOptimizer().optimize(asm_text) + return optimized, stats.total_changes +``` + +详细统计可通过新增 API 获得,但第一阶段不强制修改已有调用方。 + +--- + +## 六、核心算法设计 + +### 6.1 汇编解析 + +优先复用: + +```python +from scratchv.backend._asm_parser import ( + ParsedAsmLine, + parse_asm, + lines_to_asm, + classify_def_use, +) +``` + +立即数解析建议使用: + +```python +def parse_numeric_imm(text: str) -> int | None: + try: + return int(text.strip(), 0) + except ValueError: + return None +``` + +这可同时处理十进制、十六进制及 `-0x1`。包含符号或重定位表达式时返回 `None`,放弃优化。 + +### 6.2 寄存器规范化 + +必须把 ABI 名称转换为统一的 `xN`: + +```text +t0 -> x5 +fp -> x8 +s0 -> x8 +a0 -> x10 +``` + +所有定义、使用和状态跟踪都使用规范化名称,避免别名漏判。 + +### 6.3 规则 A:`lui + addi -> li` + +#### 匹配条件 + +设第一条有效指令为: + +```asm +lui rd, imm_hi +``` + +第二条有效指令为: + +```asm +addi rd2, rs1, imm_lo +``` + +仅当以下条件全部成立时转换: + +1. 两条指令在同一基本块内; +2. 中间最多只有空行或纯注释,没有标签或其他有效指令; +3. `canonical(rd) == canonical(rd2) == canonical(rs1)`; +4. 两个立即数都是纯数值; +5. 第二条指令不能带可作为跳转目标的标签; +6. 操作数数量合法; +7. 计算结果可按 RV32 语义规范化。 + +#### 输出 + +```asm +li rd, final_s32 # merged lui+addi +``` + +若第一条指令带标签,则标签保留在新 `li` 上;原有注释按约定合并或保留。 + +#### 不转换示例 + +```asm +lui t0, 0x12345 +addi t1, t0, 0x678 # 目标寄存器不同 +``` + +```asm +lui t0, %hi(symbol) +addi t0, t0, %lo(symbol) # 重定位表达式 +``` + +```asm +lui t0, 1 +L1: +addi t0, t0, 2 # addi 是跳转目标 +``` + +### 6.4 规则 B:基本块内冗余 `lui` 消除 + +维护: + +```python +lui_state: dict[str, int] +``` + +表示当前基本块中,某物理寄存器最后一次可确认的 `lui` 高位立即数。 + +处理规则: + +1. 遇到 `lui rd, imm`: + - 若 `lui_state[canonical(rd)] == imm`,当前 `lui` 冗余,可删除; + - 否则更新状态。 +2. 遇到会定义某寄存器的指令:清除该寄存器状态; +3. 遇到标签、条件分支、无条件跳转、函数调用或返回:清空全部状态; +4. 遇到未知指令且无法可靠判断定义集合:保守清空状态或至少不进行跨越式删除; +5. 不能跨基本块使用线性扫描状态。 + +#### 安全示例 + +```asm +lui t0, 0x10000 +addi t1, t0, 16 +lui t0, 0x10000 # 可删除,t0 中间未被修改 +addi t2, t0, 32 +``` + +#### 不安全示例 + +```asm +beq a0, zero, L1 +lui t0, 0x10000 +L1: +lui t0, 0x10000 # 不能删除:分支可能跳过前一个 lui +``` + +### 6.5 固定点迭代 + +单次规则应用可能暴露新的匹配: + +```asm +lui t0, 1 +lui t0, 1 # 删除后 +addi t0, t0, 2 # 与前一个 lui 相邻,可继续合并 +``` + +因此采用: + +```text +repeat: + 应用规则 A + 应用规则 B +until 本轮无变化 or 达到最大迭代次数 +``` + +最大迭代次数可设为 `max(1, len(lines))` 或一个保守上限。每次转换都会减少有效指令数,因此算法必然终止。 + +### 6.6 复杂度 + +若每轮线性扫描为 `O(n)`,最坏迭代次数为 `O(n)`,上界 `O(n^2)`。在典型汇编文件中迭代次数通常为 1–2。若需要严格线性复杂度,可调整规则顺序或使用工作队列,但不属于第一阶段目标。 + +--- + +## 七、接口设计 + +### 7.1 Python API + +兼容接口: + +```python +def merge_constants(asm_text: str) -> tuple[str, int]: + """返回优化后的汇编文本和转换总数。""" +``` + +建议新增详细接口: + +```python +def merge_constants_detailed( + asm_text: str, + *, + max_iterations: int | None = None, +) -> tuple[str, ConstantMergeStats]: + """返回优化后的汇编文本和分类统计。""" +``` + +### 7.2 独立 CLI + +```bash +python -m scratchv.backend.const_merge input.s -o output.s -v +``` + +`-v` 输出: + +```text +Constant merge: + merged lui+addi pairs: 3 + redundant lui removed: 2 + total transformations: 5 + iterations: 2 +``` + +### 7.3 ScratchV 主编译器开关 + +当前项目接口: + +```bash +scratchv model.onnx -o output.s --const-merge +scratchv --dsl example.dsl -o output.s --const-merge +``` + +配置字段: + +```python +CompilerConfig(const_merge=True) +``` + +### 7.4 Pass 顺序 + +建议维持: + +```text +asm peephole -> const merge -> scheduler -> beautifier -> instruction counter +``` + +原因: + +- 窥孔优化可能暴露新的相邻常量序列; +- 调度器可能打乱相邻关系,应在常量合并之后运行; +- 指令统计应在所有优化完成后运行。 + +--- + +## 八、正确性与安全约束 + +优化必须满足: + +```text +对任意允许的初始寄存器状态和执行路径, +优化前后程序在可观察行为上等价。 +``` + +第一阶段采用保守策略: + +- 不跨标签; +- 不跨控制流指令; +- 不跨调用; +- 不处理符号立即数; +- 不对未知操作码做激进定义/使用推断; +- 规范化寄存器别名; +- 按 RV32 位宽截断。 + +--- + +## 九、测试设计 + +### 9.1 单元测试分类 + +#### A. 汇编解析 + +- 十进制、十六进制、负十进制、负十六进制; +- 标签、注释、空行; +- `0(sp)` 等带括号操作数; +- 指令重建后内容可用。 + +#### B. `lui + addi` 合并 + +- 正常大常量 `0x12345678`; +- 低 12 位边界 `0x7FF`; +- 符号扩展边界 `0x800`; +- `0xFFF` 对应 `-1`; +- 结果为 `0x80000000`; +- 结果溢出后按 RV32 截断; +- ABI/数字寄存器别名混用; +- 中间有注释或空行; +- 中间有标签时不合并; +- 目标寄存器或源寄存器不同时不合并; +- `%hi/%lo` 不合并。 + +#### C. 冗余 `lui` 消除 + +- 同寄存器同立即数且未修改:删除; +- 同寄存器不同立即数:不删除; +- 中间写目标寄存器:不删除; +- 中间通过别名写目标寄存器:不删除; +- 跨标签:不删除; +- 跨分支、跳转、调用、返回:不删除; +- 未知操作码:保守处理。 + +#### D. 属性测试 + +- 幂等性:`opt(opt(x)) == opt(x)`; +- 无匹配输入:输出语义和有效指令不变; +- 转换总数与分类统计一致; +- 固定点迭代可发现删除后暴露的新模式。 + +### 9.2 集成测试 + +1. 独立 CLI 读取输入文件并生成输出文件; +2. `--verbose` 输出正确统计; +3. ScratchV 主 CLI 的 `--const-merge` 能正确传递到 `CompilerConfig`; +4. 编译器 post-pass 顺序符合设计; +5. 使用 GNU assembler 对优化前后汇编分别汇编; +6. 使用 `objdump -d -M no-aliases` 比较 `li` 的真实展开; +7. 使用 TinyFive、Spike 或项目模拟器执行可运行样例,确认最终寄存器/返回值相同。 + +### 9.3 关键验收用例 + +#### 用例 1:符号扩展 + +```asm +lui t0, 0x1 +addi t0, t0, 0x800 +``` + +期望: + +```asm +li t0, 2048 +``` + +#### 用例 2:安全冗余消除 + +```asm +lui t0, 0x10000 +addi t1, t0, 16 +lui t0, 0x10000 +addi t2, t0, 32 +``` + +期望:删除第二条 `lui`,其他指令不变。 + +#### 用例 3:跨块不得删除 + +```asm +beq a0, zero, L1 +lui t0, 0x10000 +L1: +lui t0, 0x10000 +``` + +期望:两个 `lui` 均保留。 + +#### 用例 4:寄存器别名 + +```asm +lui t0, 0x10000 +addi x5, x5, 1 +lui t0, 0x10000 +``` + +期望:第二条 `lui` 保留,因为 `x5` 与 `t0` 是同一寄存器。 + +--- + +## 十、风险与权衡 + +| 风险 | 影响 | 缓解措施 | +|---|---|---| +| 把 `li` 当作真实单指令 | 高 | 区分文本行数、伪指令数和机器指令数 | +| `addi` 符号扩展错误 | 高 | 边界测试覆盖 `0x7FF/0x800/0xFFF` | +| 跨基本块错误删除 | 高 | 标签和控制流边界清空状态 | +| 寄存器 ABI 别名漏判 | 高 | 所有寄存器先规范化为 `xN` | +| 重定位表达式被错误解析 | 高 | 只接受纯数值立即数 | +| 注释或标签丢失 | 中 | 复用共享解析器并增加格式测试 | +| 当前后端已直接输出 `li`,命中率低 | 中 | 增加独立汇编 fixture;报告实际命中率 | +| 与调度器 pass 顺序冲突 | 中 | 固定在调度器之前运行 | +| 重构公共解析器引入回归 | 中 | 先补 characterization tests,再替换解析实现 | + +--- + +## 十一、待评审问题 + +提交设计评审时需要明确回答: + +1. 本课题是重现课程参考实现,还是改进主分支现有实现? +2. 第一阶段目标是 RV32 还是同时支持 RV64? +3. `lui+addi -> li` 的主要目标是代码可读性,还是需要证明真实机器指令减少? +4. 是否要求跨基本块优化?若要求,需要引入 CFG 和数据流分析,不应使用简单线性状态; +5. 是否必须沿用 `merge_constants(...)->(str, int)`,还是允许新增统计对象? +6. 是否需要把现有 `const_merge.py` 迁移到共享 `_asm_parser.py`? +7. 最终命令行名称以当前代码的 `--const-merge` 为准,还是兼容归档文档的 `--merge-constants`? + +--- + +## 十二、参考资料 + +- ScratchV 课程首页: +- 课题 14 当前课程页: +- 课题 14 归档说明: +- 当前参考实现: +- 当前测试: +- RISC-V ISA Manual: +- ScratchV 贡献指南: + From 76b9e0aff001c080719e4e41d7e557875ab46cb5 Mon Sep 17 00:00:00 2001 From: yuki-328 Date: Sat, 1 Aug 2026 09:47:49 +0800 Subject: [PATCH 02/12] =?UTF-8?q?docs:=20=E5=AE=8C=E5=96=84=E5=B8=B8?= =?UTF-8?q?=E9=87=8F=E5=90=88=E5=B9=B6=20benchmark=20=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...07\346\241\243\345\210\235\347\250\277.md" | 67 ++++++++++- ...07\346\241\243\345\210\235\347\250\277.md" | 108 +++++++++++++++++- 2 files changed, 165 insertions(+), 10 deletions(-) diff --git "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" index 054f209..ccdcdf6 100644 --- "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" +++ "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" @@ -1,7 +1,8 @@ # ScratchV 课题 14:常量加载合并优化开发文档 -> **文档版本**:v0.1(开发计划初稿) +> **文档版本**:v0.2(补充 Benchmark 可观测性与验收方案) > **创建日期**:2026-07-28 +> **更新日期**:2026-08-01 > **作者**:[yuki] > **关联 Issue**:[#待补充] > **涉及模块**:`scratchv/backend/`、`scratchv/compiler.py`、`scratchv/main.py`、`tests/`、`docs/` @@ -23,7 +24,7 @@ ScratchV 主分支已经存在课题 14 的参考模块、测试和命令行集 - **现状问题**:RISC-V 加载一般 32 位常量常出现 `lui + addi` 序列;重复加载相同高 20 位还可能产生冗余 `lui`。现有参考实现具备基本功能,但仍需要补充控制流安全、寄存器别名、共享解析器和更严格的测试。 - **应用场景**:处理 ScratchV 后端生成的汇编、外部 RISC-V 汇编文件、课程测试 fixture,以及汇编级窥孔优化和统计流程。 -- **现实限制**:ScratchV 当前指令选择对常量通常直接生成 `li`,因此 `lui+addi` 合并规则在主流水线中的命中率可能不高;必须用独立汇编输入验证核心逻辑。 +- **现实限制(已按 commit `109a6a2` 核实)**:`instruction_select.py` 的常量、循环初值等路径直接生成 `MachineOp.LI`。因此现有 ONNX/DSL case 可能不产生 `lui+addi`,主流水线命中率可能为 0。零命中是需要记录的实验结果,不是需要通过改造真实输入掩盖的问题。 ### 1.2 功能描述 @@ -279,7 +280,51 @@ diff -u before.dump after.dump `li` 可能重新展开为 `lui+addi`,因此反汇编差异不应只看源文件行数。 -### 步骤 9:回归、文档与 PR +### 步骤 9:改造 Benchmark 并建立真实 case A/B + +**当前基线(commit `109a6a2`)**: + +- `benchmarks/bench_runner.py` 测 DSL/IR 执行,不进入 RISC-V 汇编 post-pass,不能证明 const-merge 有效; +- `benchmarks/run_benchmark.py` 能生成 RISC-V 汇编,但当前不调用 `merge_constants`,并且只记录 `asm_line_count`; +- `benchmarks/bench_const_merge.py` 是 synthetic microbenchmark,当前只生成 `lui+addi` 模式、只返回总变化数,并用换行数表示规模与收益。 + +**任务 A:真实 case 命中率。** 在 `run_benchmark.py` 的 RISC-V 路径中,对同一份原始汇编做单变量 A/B: + +```python +asm_before = asm_str +t0 = time.perf_counter() +asm_after, stats = merge_constants_detailed(asm_before) +pass_time_ms = (time.perf_counter() - t0) * 1000 +``` + +不得通过分别执行两次完整编译来取得 before/after,否则会把前端、优化、指令选择和寄存器分配差异混入结果。也不得修改现有 ONNX/DSL case 来制造目标模式。 + +每个真实 case 至少输出: + +| 字段 | 说明 | +|---|---| +| `lui_count_before` | 优化前有效 `lui` 数量 | +| `candidate_pairs` | 同块内可检查的 `lui+addi` 候选数 | +| `merged_pairs` | 规则 A 实际转换数 | +| `redundant_lui_removed` | 规则 B 实际删除数 | +| `asm_instructions_before/after` | 排除标签、伪操作、空行和纯注释后的汇编指令数 | +| `machine_instructions_before/after` | 汇编并反汇编后的真实机器指令数;工具链不可用时记为 `N/A` | +| `code_size_before/after` | `.text` 大小;工具链不可用时记为 `N/A` | +| `pass_time_ms` | 仅 const-merge 的耗时 | +| `output_equal` | 优化前后可执行结果是否一致;无法执行时不得写 `true`,应记为 `N/A` | + +**任务 B:人工 microbenchmark。** 保留 `bench_const_merge.py`,但明确标记为 synthetic,并增加可调规模、目标模式密度和重复 `lui` 密度。输出 `merged_pairs` 与 `redundant_lui_removed`,使用有效指令计数替代 `asm_text.count("\n")`。人工输入的下降量只能说明算法对目标模式有效,不能表述为真实项目端到端收益。 + +**结果判定:** + +- 真实 case 有命中:报告命中 case、两类转换数量、机器指令/代码大小变化及语义验证; +- 真实 case 零命中:明确写出“当前代码生成形式与 const-merge 目标输入不匹配”,同时用 microbenchmark 证明规则正确性和扫描开销; +- 规则 A 仅减少源汇编行数、机器指令不变:按事实分别报告,不宣称运行性能提升; +- 只有规则 B 的删除可直接计入真实机器指令减少,仍需工具链及语义验证支持。 + +**验证**:固定 seed 的 benchmark 可重复;统计满足 `total_changes == merged_pairs + redundant_lui_removed`;有效指令差值与转换分类一致;JSON/表格中保留零值与 `N/A`。 + +### 步骤 10:回归、文档与 PR **任务**: @@ -359,6 +404,16 @@ tests/fixtures/const_merge/ └── relocation_noop.s ``` +### 7.4 Benchmark 验证矩阵 + +| 类型 | 输入来源 | 目的 | 可以得出的结论 | +|---|---|---|---| +| 真实 case | `run_benchmark.py` 现有 ONNX/DSL case 生成的一份原始汇编 | 测主流水线实际模式覆盖与收益 | 当前后端是否产生目标模式,以及真实汇编/机器码是否变化 | +| Synthetic microbenchmark | `bench_const_merge.py` 固定 seed 人工汇编 | 测规则命中正确性、密度影响和 pass 开销 | 目标模式存在时的算法效果;不能外推为端到端收益 | +| 工具链等价用例 | 至少 3 个可执行 `.s` fixture | 验证汇编、反汇编和执行语义 | before/after 在限定输入上的语义一致性 | + +真实 case 报告必须逐 case 保留零命中结果,并提供汇总行;不得只展示发生变化的 case。`bench_runner.py` 可继续作为 DSL/IR 性能基线,但应在报告中注明它不覆盖 const-merge。 + --- ## 8. 验收标准(Definition of Done) @@ -375,6 +430,11 @@ tests/fixtures/const_merge/ - [ ] `pytest tests/test_const_merge.py -v` 通过; - [ ] `pytest tests/ -q` 全量通过; - [ ] 至少 3 个汇编样例通过工具链或模拟器等价验证; +- [ ] `run_benchmark.py` 基于同一份原始 RISC-V 汇编完成 const-merge A/B; +- [ ] 真实 case 逐项报告候选数、分类转换数、有效汇编指令数和 pass 耗时; +- [ ] 工具链可用时报告机器指令数和 `.text` 大小,不可用时明确标记 `N/A`; +- [ ] `bench_const_merge.py` 明确标记 synthetic,并覆盖 `lui+addi` 与重复 `lui` 两类密度; +- [ ] Benchmark 不再使用换行数作为指令数,且零命中 case 不被过滤; - [ ] 文档明确说明 `li` 是伪指令; - [ ] 主 CLI `--const-merge` 可用; - [ ] PR 描述包含前后示例、测试结果和已知限制。 @@ -510,4 +570,3 @@ git switch -c feat/topic14-const-merge - 当前测试: - 贡献指南: - RISC-V RV32I 规范: - diff --git "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" index edd1f98..a3b0628 100644 --- "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" +++ "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" @@ -1,7 +1,8 @@ # ScratchV 课题 14:常量加载合并优化技术设计文档 -> **文档版本**:v0.1(设计初稿) +> **文档版本**:v0.2(补充 Benchmark 设计与指标口径) > **编写日期**:2026-07-28 +> **更新日期**:2026-08-01 > **作者**:[yuki] > **关联 Issue**:[#待补充] > **涉及模块**:`scratchv/backend/const_merge.py`、`scratchv/backend/_asm_parser.py`、`scratchv/compiler.py`、`scratchv/main.py` @@ -153,7 +154,7 @@ scratchv input.dsl -o output.s --const-merge 4. 当前实现按字符串比较寄存器,`t0` 与 `x5` 的别名可能导致错误判断; 5. 现有测试主要覆盖正向样例,缺少标签、分支、寄存器别名、符号立即数、负十六进制和幂等性测试; 6. 归档课题要求“迭代扫描”,参考实现目前主要是一次合并加一次冗余消除; -7. ScratchV 当前指令选择本身常直接生成 `li`,所以主编译流程中 `lui + addi` 合并规则的命中率可能很低,需要单独汇编 fixture 或其他后端输出验证; +7. 已在 commit `109a6a2` 核实:ScratchV 当前指令选择的多个常量路径直接生成 `MachineOp.LI`,所以主编译流程中 `lui + addi` 合并规则可能零命中;真实 case 必须测量并保留这一结果,同时用独立汇编 fixture 验证目标模式; 8. 当前 `changes` 只给总数,无法区分文本规范化与真实冗余指令删除。 --- @@ -613,7 +614,100 @@ lui t0, 0x10000 --- -## 十、风险与权衡 +## 十、Benchmark 设计 + +### 10.1 当前覆盖缺口 + +基于 commit `109a6a2`,三条现有 benchmark 路径的职责如下: + +| 路径 | 当前行为 | 对课题 14 的覆盖 | +|---|---|---| +| `benchmarks/bench_runner.py` | 运行 DSL/IR 解释与性能 case | 不生成 RISC-V 汇编,不经过 const-merge | +| `benchmarks/run_benchmark.py` | ONNX 解析、IR 优化、RISC-V/LLVM codegen | RISC-V 路径生成汇编,但未调用 const-merge;仅统计 `splitlines()` 行数 | +| `benchmarks/bench_const_merge.py` | 固定 seed 生成人工 `lui+addi` 并计时 | 能证明人工模式命中,但当前没有重复 `lui` 密度、分类统计或有效指令计数 | + +因此,本课题不能只修改 `bench_const_merge.py` 后声称优化已体现在“项目 benchmark case”中。设计必须同时提供真实 case A/B 和 synthetic microbenchmark,两者结论分开呈现。 + +### 10.2 真实 case 的单变量 A/B + +真实 case 复用 `run_benchmark.py` 的现有 ONNX/DSL 输入。在 RISC-V codegen 得到 `asm_before` 后,只对同一字符串调用一次 const-merge: + +```python +asm_before = asm_str +t0 = time.perf_counter() +asm_after, stats = merge_constants_detailed(asm_before) +pass_time_ms = (time.perf_counter() - t0) * 1000 +``` + +禁止为 A/B 重新运行两次完整编译,避免前端、IR 优化、指令选择或寄存器分配成为额外变量。禁止修改真实 case 以人工插入 `lui+addi`。 + +逐 case 记录: + +```text +case_name +lui_count_before +candidate_pairs +merged_pairs +redundant_lui_removed +asm_instructions_before / asm_instructions_after +machine_instructions_before / machine_instructions_after +code_size_before / code_size_after +pass_time_ms +output_equal +``` + +其中 `candidate_pairs` 是满足操作码、操作数和同块相邻条件、可进入规则 A 判断的数量;它与 `merged_pairs` 分开,便于区分“没有输入模式”和“候选因安全条件被拒绝”。若第一阶段无法稳定统计拒绝原因,至少保证该定义不把标签、注释行或跨块序列算入候选。 + +### 10.3 有效汇编指令计数 + +`asm_text.count("\n")` 和 `len(asm_text.splitlines())` 都不是指令计数。计数器应解析每行并排除: + +- 空行与纯注释; +- 只有标签的行; +- `.text`、`.globl`、`.section` 等汇编伪操作。 + +标签与指令位于同一行时只计该指令。该口径用于 `asm_instructions_before/after`;它仍包含 `li` 等伪指令,因此不能替代机器指令数。 + +### 10.4 机器码与语义指标 + +工具链可用时,before/after 分别经 assembler 与 `objdump -d -M no-aliases`,统计真实机器指令和 `.text` 大小。`lui+addi -> li` 可能重新展开为两条机器指令,所以允许出现: + +```text +asm_instructions_after < asm_instructions_before +machine_instructions_after == machine_instructions_before +``` + +`output_equal` 只能由相同输入下的模拟器/执行结果比较得出;只完成 IR verifier、只成功汇编或只比较文本时记为 `N/A`,不能视为语义等价证明。外部工具不可用时,机器码、代码大小与执行等价字段均保留并标记 `N/A`,不得静默省略。 + +### 10.5 Synthetic microbenchmark + +`bench_const_merge.py` 继续负责可控压力测试,并明确打印 `benchmark_type=synthetic`。输入参数至少包括: + +- `num_instructions`:有效汇编指令规模; +- `pair_density`:`lui+addi` 模式密度; +- `redundant_lui_density`:同块重复 `lui` 模式密度; +- `seed` 与 `repeats`:可重复性及计时稳定性。 + +输出分类统计、有效指令差值及 pass 时间分布。人工构造的收益只能证明目标模式存在时优化器工作正常,并用于观察复杂度;不能外推为 ONNX/DSL 端到端收益。 + +### 10.6 结果解释与验收 + +真实 case 可能全部得到零候选,这是当前后端直接生成 `MachineOp.LI` 时的合理结果。报告必须保留所有 case(包括零值),并使用以下结论: + +> 当前真实代码生成路径没有产生 const-merge 的目标模式,因此本组 case 命中率为 0;synthetic microbenchmark 仅用于验证目标模式存在时的正确性和 pass 开销。 + +Benchmark 验收条件: + +1. 同一份原始汇编完成 A/B,唯一变量是 const-merge; +2. 逐 case 和汇总结果均包含候选、两类转换、有效指令数及耗时; +3. 工具链指标与 `output_equal` 不可测时明确为 `N/A`; +4. synthetic 输入同时覆盖两条优化规则,并固定 seed; +5. 满足 `total_changes == merged_pairs + redundant_lui_removed`; +6. 不将源汇编减少直接表述为机器指令、代码大小或周期减少。 + +--- + +## 十一、风险与权衡 | 风险 | 影响 | 缓解措施 | |---|---|---| @@ -629,7 +723,7 @@ lui t0, 0x10000 --- -## 十一、待评审问题 +## 十二、待评审问题 提交设计评审时需要明确回答: @@ -640,10 +734,13 @@ lui t0, 0x10000 5. 是否必须沿用 `merge_constants(...)->(str, int)`,还是允许新增统计对象? 6. 是否需要把现有 `const_merge.py` 迁移到共享 `_asm_parser.py`? 7. 最终命令行名称以当前代码的 `--const-merge` 为准,还是兼容归档文档的 `--merge-constants`? +8. 真实 case A/B 是否合入 `run_benchmark.py` 的统一 JSON schema? +9. CI 是否提供 GNU RISC-V 工具链或模拟器;若不提供,机器码和语义指标是否作为可选阶段? +10. 零命中是否接受为当前主流水线的正式实验结论? --- -## 十二、参考资料 +## 十三、参考资料 - ScratchV 课程首页: - 课题 14 当前课程页: @@ -652,4 +749,3 @@ lui t0, 0x10000 - 当前测试: - RISC-V ISA Manual: - ScratchV 贡献指南: - From ac7ac858c08626219bb111ce2b946d4a5ea98407 Mon Sep 17 00:00:00 2001 From: yuki-328 Date: Sat, 15 Aug 2026 16:07:21 +0800 Subject: [PATCH 03/12] refactor: reuse shared assembly parser in const merge --- scratchv/backend/const_merge.py | 91 +++++++++------------------------ tests/test_const_merge.py | 8 +++ 2 files changed, 33 insertions(+), 66 deletions(-) diff --git a/scratchv/backend/const_merge.py b/scratchv/backend/const_merge.py index d0394b0..dbffa58 100644 --- a/scratchv/backend/const_merge.py +++ b/scratchv/backend/const_merge.py @@ -13,75 +13,35 @@ from __future__ import annotations import argparse -import re import sys from typing import Optional +from scratchv.backend._asm_parser import ( + ParsedAsmLine, + lines_to_asm, + parse_asm, + parse_line, +) + # --------------------------------------------------------------------------- # Data types # --------------------------------------------------------------------------- -class AsmInst: - """Represents one parsed assembly instruction.""" +class AsmInst(ParsedAsmLine): + """Backward-compatible parsed instruction using the shared parser.""" def __init__(self, raw: str, lineno: int = 0): - self.raw = raw - self.lineno = lineno - self.label: Optional[str] = None - self.opcode: Optional[str] = None - self.operands: list[str] = [] - self.comment: Optional[str] = None - self._parse() - - def _parse(self) -> None: - """Parse the raw line into components.""" - stripped = self.raw.strip() - - # Empty line or pure comment - if not stripped or stripped.startswith("#"): - self.comment = stripped.lstrip("#").strip() - return - - # Separate code from comment - code = stripped - if "#" in stripped: - idx = stripped.find("#") - code = stripped[:idx].strip() - self.comment = stripped[idx + 1:].strip() - - # Check for label - label_match = re.match(r'^([A-Za-z_.][A-Za-z0-9_.]*):\s*(.*)', code) - if label_match: - self.label = label_match.group(1) - code = label_match.group(2).strip() - - if not code: - return - - # Extract opcode and operands - tokens = code.replace(",", " ").split() - if not tokens: - return - - self.opcode = tokens[0].lower().lstrip(".") - self.operands = tokens[1:] if len(tokens) > 1 else [] - - def to_asm(self) -> str: - """Reconstruct the assembly line.""" - parts = [] - if self.label: - parts.append(f"{self.label}:") - if self.opcode: - parts.append(f" {self.opcode}") - if self.operands: - parts.append(" " + ", ".join(self.operands)) - if self.comment: - parts.append(f" # {self.comment}") - result = "".join(parts) - if not result.strip() and self.raw.strip() == "": - return "" - return result + parsed = parse_line(raw, lineno=lineno) + super().__init__( + raw=parsed.raw, + label=parsed.label, + opcode=parsed.opcode, + operands=parsed.operands, + comment=parsed.comment, + lineno=parsed.lineno, + is_directive=parsed.is_directive, + ) def __repr__(self) -> str: return f"AsmInst({self.opcode}, {self.operands})" @@ -91,15 +51,14 @@ def __repr__(self) -> str: # Helpers # --------------------------------------------------------------------------- -def _parse_asm(asm_text: str) -> list[AsmInst]: - """Parse assembly text into AsmInst objects.""" - lines = asm_text.strip().split("\n") - return [AsmInst(line, lineno=i) for i, line in enumerate(lines)] +def _parse_asm(asm_text: str) -> list[ParsedAsmLine]: + """Compatibility wrapper around the shared assembly parser.""" + return parse_asm(asm_text.strip()) -def _insts_to_asm(insts: list[AsmInst]) -> str: - """Convert AsmInst list back to assembly string.""" - return "\n".join(inst.to_asm() for inst in insts) +def _insts_to_asm(insts: list[ParsedAsmLine]) -> str: + """Compatibility wrapper around the shared assembly serializer.""" + return lines_to_asm(insts) def _parse_imm(s: str) -> Optional[int]: diff --git a/tests/test_const_merge.py b/tests/test_const_merge.py index c47bbb2..f9993e7 100644 --- a/tests/test_const_merge.py +++ b/tests/test_const_merge.py @@ -1,6 +1,7 @@ """Tests for Constant Load Merge Optimizer.""" import pytest +from scratchv.backend._asm_parser import ParsedAsmLine from scratchv.backend.const_merge import ( merge_constants, AsmInst, _parse_asm, _insts_to_asm, ) @@ -48,6 +49,13 @@ def test_parse_roundtrip(self): assert "lui" in result assert "addi" in result + def test_uses_shared_parser_representation(self): + insts = _parse_asm(".text\n lw t0, 16(sp) # load") + assert all(isinstance(inst, ParsedAsmLine) for inst in insts) + assert insts[0].is_directive + assert insts[1].operands == ["t0", "16(sp)"] + assert insts[1].comment == "load" + class TestMergeConstants: """Tests for the merge_constants function.""" From 87021b2fd8e098390e24bd4abbaa96036f6a0fc9 Mon Sep 17 00:00:00 2001 From: yuki-328 Date: Sat, 15 Aug 2026 16:14:51 +0800 Subject: [PATCH 04/12] feat: canonicalize RISC-V register aliases --- scratchv/backend/_asm_parser.py | 26 ++++++++++++++++++++++++++ scratchv/backend/const_merge.py | 13 ++++++++----- tests/test_const_merge.py | 27 ++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/scratchv/backend/_asm_parser.py b/scratchv/backend/_asm_parser.py index 3cdef7e..e07e9b8 100644 --- a/scratchv/backend/_asm_parser.py +++ b/scratchv/backend/_asm_parser.py @@ -134,6 +134,32 @@ def is_comment_only(self) -> bool: "j", "jal", "jalr", "ret", "jr", } +# RISC-V integer-register aliases from the psABI. Assembly passes should +# track physical registers, not their textual spelling: for example ``t0`` +# and ``x5`` identify the same register. +_INTEGER_REGISTER_ALIASES: dict[str, str] = { + "zero": "x0", "ra": "x1", "sp": "x2", "gp": "x3", "tp": "x4", + "t0": "x5", "t1": "x6", "t2": "x7", "s0": "x8", "fp": "x8", + "s1": "x9", "a0": "x10", "a1": "x11", "a2": "x12", + "a3": "x13", "a4": "x14", "a5": "x15", "a6": "x16", + "a7": "x17", "s2": "x18", "s3": "x19", "s4": "x20", + "s5": "x21", "s6": "x22", "s7": "x23", "s8": "x24", + "s9": "x25", "s10": "x26", "s11": "x27", "t3": "x28", + "t4": "x29", "t5": "x30", "t6": "x31", +} + + +def canonical_reg(reg: str) -> str: + """Return an integer register's canonical ``xN`` spelling. + + Unknown operands are returned lower-cased unchanged. This lets callers + normalize operands before comparing them without guessing aliases. + """ + name = reg.strip().lower() + if re.fullmatch(r"x([0-9]|[12][0-9]|3[01])", name): + return name + return _INTEGER_REGISTER_ALIASES.get(name, name) + # ═══════════════════════════════════════════════════════════════════════════════ # Parsing diff --git a/scratchv/backend/const_merge.py b/scratchv/backend/const_merge.py index dbffa58..ae2b3f7 100644 --- a/scratchv/backend/const_merge.py +++ b/scratchv/backend/const_merge.py @@ -18,6 +18,7 @@ from scratchv.backend._asm_parser import ( ParsedAsmLine, + canonical_reg, lines_to_asm, parse_asm, parse_line, @@ -114,7 +115,7 @@ def _is_reg(s: str) -> bool: return s.strip() in _STANDARD_REGS -def _is_clobbered(inst: AsmInst, reg: str) -> bool: +def _is_clobbered(inst: ParsedAsmLine, reg: str) -> bool: """Check if an instruction writes to the given register.""" if inst.opcode is None: return False @@ -130,7 +131,7 @@ def _is_clobbered(inst: AsmInst, reg: str) -> bool: "slti", "sltiu", } if inst.opcode in dst_clobbers: - return inst.operands[0] == reg + return canonical_reg(inst.operands[0]) == canonical_reg(reg) # For stores, the first operand is the value (doesn't clobber dest reg) # For branches, no destination return False @@ -165,8 +166,10 @@ def merge_constants(asm_text: str) -> tuple[str, int]: # Check: rd of lui == rd of addi, and rd == rs1 of addi lui_rd = inst.operands[0] if (len(next_inst.operands) >= 3 - and next_inst.operands[0] == lui_rd - and next_inst.operands[1] == lui_rd): + and canonical_reg(next_inst.operands[0]) + == canonical_reg(lui_rd) + and canonical_reg(next_inst.operands[1]) + == canonical_reg(lui_rd)): # Merge imm_hi = ( _parse_imm(inst.operands[1]) @@ -206,7 +209,7 @@ def merge_constants(asm_text: str) -> tuple[str, int]: for inst in insts: if inst.opcode == "lui" and inst.operands: - rd = inst.operands[0] + rd = canonical_reg(inst.operands[0]) imm = ( _parse_imm(inst.operands[1]) if len(inst.operands) > 1 else None diff --git a/tests/test_const_merge.py b/tests/test_const_merge.py index f9993e7..83269b0 100644 --- a/tests/test_const_merge.py +++ b/tests/test_const_merge.py @@ -1,7 +1,7 @@ """Tests for Constant Load Merge Optimizer.""" import pytest -from scratchv.backend._asm_parser import ParsedAsmLine +from scratchv.backend._asm_parser import ParsedAsmLine, canonical_reg from scratchv.backend.const_merge import ( merge_constants, AsmInst, _parse_asm, _insts_to_asm, ) @@ -123,6 +123,31 @@ def test_sign_extension_correct(self): assert changes >= 1 assert "li" in result + def test_merge_with_abi_and_x_register_aliases(self): + asm = " lui t0, 1\n addi x5, t0, 2\n" + result, changes = merge_constants(asm) + assert changes == 1 + assert "li t0, 4098" in result + + def test_alias_clobber_prevents_redundant_lui_removal(self): + asm = " lui t0, 1\n add x5, a0, a1\n lui t0, 1\n" + result, changes = merge_constants(asm) + assert changes == 0 + assert result.count("lui") == 2 + + def test_redundant_lui_recognizes_aliases(self): + asm = " lui t0, 1\n add a0, a1, a2\n lui x5, 1\n" + result, changes = merge_constants(asm) + assert changes == 1 + assert sum(inst.opcode == "lui" for inst in _parse_asm(result)) == 1 + + def test_register_canonicalization(self): + assert canonical_reg("t0") == "x5" + assert canonical_reg("fp") == "x8" + assert canonical_reg("s0") == "x8" + assert canonical_reg("a0") == "x10" + assert canonical_reg("X31") == "x31" + class TestCli: """Test CLI behavior.""" From 85b8e178e08998db01481ce6a07da2933b44f018 Mon Sep 17 00:00:00 2001 From: yuki-328 Date: Sat, 15 Aug 2026 16:20:34 +0800 Subject: [PATCH 05/12] feat: safely merge RV32 lui addi sequences --- scratchv/backend/const_merge.py | 163 ++++++++++++++++++++++---------- tests/test_const_merge.py | 60 ++++++++++++ 2 files changed, 171 insertions(+), 52 deletions(-) diff --git a/scratchv/backend/const_merge.py b/scratchv/backend/const_merge.py index ae2b3f7..1f66496 100644 --- a/scratchv/backend/const_merge.py +++ b/scratchv/backend/const_merge.py @@ -63,12 +63,14 @@ def _insts_to_asm(insts: list[ParsedAsmLine]) -> str: def _parse_imm(s: str) -> Optional[int]: - """Parse an immediate value string to int.""" + """Parse a decimal or prefixed numeric immediate.""" try: - s = s.strip() - if s.startswith("0x") or s.startswith("0X"): - return int(s, 16) - return int(s) + text = s.strip() + try: + return int(text, 0) + except ValueError: + # Python rejects decimal strings such as ``010`` with base 0. + return int(text, 10) except ValueError: return None @@ -91,6 +93,107 @@ def _l12(val: int) -> int: return _sign_extend_12(val & 0xFFF) +def _parse_lui_imm(text: str) -> Optional[int]: + """Parse a representable 20-bit LUI immediate. + + Both signed 20-bit spelling and the unsigned encoded field are accepted. + Values outside that range are rejected instead of silently truncated. + """ + value = _parse_imm(text) + if value is None or not -(1 << 19) <= value <= 0xFFFFF: + return None + return value & 0xFFFFF + + +def _parse_addi_imm(text: str) -> Optional[int]: + """Parse a 12-bit ADDI immediate in signed or encoded-field spelling.""" + value = _parse_imm(text) + if value is None or not -(1 << 11) <= value <= 0xFFF: + return None + return value + + +def _signed_rv32(value: int) -> int: + """Normalize an integer to its signed RV32 representation.""" + value &= 0xFFFFFFFF + return value if value < 0x80000000 else value - 0x100000000 + + +def _is_separator(line: ParsedAsmLine) -> bool: + """Return whether a line is only whitespace or a comment.""" + return line.opcode is None and line.label is None + + +def _merge_lui_addi_once( + insts: list[ParsedAsmLine], +) -> tuple[list[ParsedAsmLine], int]: + """Safely merge one scan's eligible LUI+ADDI sequences.""" + result: list[ParsedAsmLine] = [] + changes = 0 + i = 0 + + while i < len(insts): + lui = insts[i] + if lui.opcode != "lui" or len(lui.operands) != 2: + result.append(lui) + i += 1 + continue + + j = i + 1 + while j < len(insts) and _is_separator(insts[j]): + j += 1 + if j >= len(insts): + result.append(lui) + i += 1 + continue + + addi = insts[j] + if ( + addi.opcode != "addi" + or addi.label is not None + or len(addi.operands) != 3 + ): + result.append(lui) + i += 1 + continue + + rd = canonical_reg(lui.operands[0]) + if ( + rd != canonical_reg(addi.operands[0]) + or rd != canonical_reg(addi.operands[1]) + ): + result.append(lui) + i += 1 + continue + + imm_hi = _parse_lui_imm(lui.operands[1]) + imm_lo = _parse_addi_imm(addi.operands[2]) + if imm_hi is None or imm_lo is None: + result.append(lui) + i += 1 + continue + + final_value = _signed_rv32( + (imm_hi << 12) + _sign_extend_12(imm_lo), + ) + comments = [comment for comment in (lui.comment, addi.comment) if comment] + comments.append(f"merged lui+addi -> {final_value}") + result.append(ParsedAsmLine( + raw="", + label=lui.label, + opcode="li", + operands=[lui.operands[0], str(final_value)], + comment="; ".join(comments), + lineno=lui.lineno, + )) + # Comments and blank lines are not instructions, so retain them. + result.extend(insts[i + 1:j]) + changes += 1 + i = j + 1 + + return result, changes + + # --------------------------------------------------------------------------- # Constant merge optimization # --------------------------------------------------------------------------- @@ -152,53 +255,9 @@ def merge_constants(asm_text: str) -> tuple[str, int]: insts = _parse_asm(asm_text) total_changes = 0 - # --- Pass 1: Merge adjacent lui+addi pairs into li --- - new_insts: list[AsmInst] = [] - i = 0 - while i < len(insts): - inst = insts[i] - - # Check for lui followed by addi - if inst.opcode == "lui" and i + 1 < len(insts): - next_inst = insts[i + 1] - if (next_inst.opcode == "addi" - and inst.operands and next_inst.operands): - # Check: rd of lui == rd of addi, and rd == rs1 of addi - lui_rd = inst.operands[0] - if (len(next_inst.operands) >= 3 - and canonical_reg(next_inst.operands[0]) - == canonical_reg(lui_rd) - and canonical_reg(next_inst.operands[1]) - == canonical_reg(lui_rd)): - # Merge - imm_hi = ( - _parse_imm(inst.operands[1]) - if len(inst.operands) > 1 else None - ) - imm_lo = ( - _parse_imm(next_inst.operands[2]) - if len(next_inst.operands) > 2 else None - ) - - if imm_hi is not None and imm_lo is not None: - # Compute final constant - final_val = (imm_hi << 12) + _sign_extend_12(imm_lo) - # Replace with li - new_inst = AsmInst("") - new_inst.opcode = "li" - new_inst.operands = [lui_rd, str(final_val)] - new_inst.comment = ( - f"merged lui+addi -> {final_val}" - ) - new_insts.append(new_inst) - total_changes += 1 - i += 2 - continue - - new_insts.append(inst) - i += 1 - - insts = new_insts + # --- Pass 1: Merge safe lui+addi pairs into li --- + insts, merged = _merge_lui_addi_once(insts) + total_changes += merged # --- Pass 2: Eliminate redundant lui --- # Track the last upper-immediate value loaded into each register diff --git a/tests/test_const_merge.py b/tests/test_const_merge.py index 83269b0..40a33fb 100644 --- a/tests/test_const_merge.py +++ b/tests/test_const_merge.py @@ -122,6 +122,66 @@ def test_sign_extension_correct(self): result, changes = merge_constants(asm) assert changes >= 1 assert "li" in result + assert "2048" in result + + def test_rv32_result_is_normalized_to_signed_value(self): + asm = " lui t0, 0x80000\n addi t0, t0, 0\n" + result, changes = merge_constants(asm) + assert changes == 1 + assert "li t0, -2147483648" in result + + def test_negative_hex_immediates(self): + asm = " lui t0, -0x1\n addi t0, t0, -0x1\n" + result, changes = merge_constants(asm) + assert changes == 1 + assert "li t0, -4097" in result + + @pytest.mark.parametrize("imm", ["0x100000", "-0x80001"]) + def test_out_of_range_lui_is_not_truncated(self, imm): + asm = f" lui t0, {imm}\n addi t0, t0, 1\n" + result, changes = merge_constants(asm) + assert changes == 0 + assert imm in result + + @pytest.mark.parametrize("imm", ["0x1000", "-2049"]) + def test_out_of_range_addi_is_not_truncated(self, imm): + asm = f" lui t0, 1\n addi t0, t0, {imm}\n" + result, changes = merge_constants(asm) + assert changes == 0 + assert imm in result + + def test_relocation_expression_is_not_merged(self): + asm = " lui t0, %hi(symbol)\n addi t0, t0, %lo(symbol)\n" + result, changes = merge_constants(asm) + assert changes == 0 + assert "%hi(symbol)" in result + assert "%lo(symbol)" in result + + def test_different_addi_source_is_not_merged(self): + asm = " lui t0, 1\n addi t0, t1, 2\n" + result, changes = merge_constants(asm) + assert changes == 0 + assert "lui" in result and "addi" in result + + def test_comment_and_blank_between_pair_are_preserved(self): + asm = " lui t0, 1 # upper\n# keep me\n\n addi t0, t0, 2 # lower\n" + result, changes = merge_constants(asm) + assert changes == 1 + assert "li t0, 4098" in result + assert "# keep me" in result + assert "upper" in result and "lower" in result + + def test_intervening_label_prevents_merge(self): + asm = " lui t0, 1\nL1:\n addi t0, t0, 2\n" + result, changes = merge_constants(asm) + assert changes == 0 + assert "lui" in result and "addi" in result + + def test_label_on_lui_is_preserved(self): + asm = "L0: lui t0, 1\n addi t0, t0, 2\n" + result, changes = merge_constants(asm) + assert changes == 1 + assert "L0: li t0, 4098" in result def test_merge_with_abi_and_x_register_aliases(self): asm = " lui t0, 1\n addi x5, t0, 2\n" From 4bcaf3c8137b66006f480ba827680453c53554d9 Mon Sep 17 00:00:00 2001 From: yuki-328 Date: Sat, 15 Aug 2026 16:26:17 +0800 Subject: [PATCH 06/12] feat: safely remove redundant lui instructions --- scratchv/backend/const_merge.py | 153 +++++++++++++++++--------------- tests/test_const_merge.py | 53 +++++++++++ 2 files changed, 135 insertions(+), 71 deletions(-) diff --git a/scratchv/backend/const_merge.py b/scratchv/backend/const_merge.py index 1f66496..a044e22 100644 --- a/scratchv/backend/const_merge.py +++ b/scratchv/backend/const_merge.py @@ -19,6 +19,7 @@ from scratchv.backend._asm_parser import ( ParsedAsmLine, canonical_reg, + classify_def_use, lines_to_asm, parse_asm, parse_line, @@ -198,46 +199,86 @@ def _merge_lui_addi_once( # Constant merge optimization # --------------------------------------------------------------------------- -# Standard register names -_STANDARD_REGS = { - "x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7", - "x8", "x9", "x10", "x11", "x12", "x13", "x14", "x15", - "x16", "x17", "x18", "x19", "x20", "x21", "x22", "x23", - "x24", "x25", "x26", "x27", "x28", "x29", "x30", "x31", - "zero", "ra", "sp", "gp", "tp", - "t0", "t1", "t2", "t3", "t4", "t5", "t6", - "s0", "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", - "s9", "s10", "s11", - "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", - "fp", +_CONTROL_FLOW_OPCODES = { + "beq", "bne", "blt", "bge", "bltu", "bgeu", + "beqz", "bnez", "blez", "bgtz", "bltz", "bgez", + "j", "jr", "jal", "jalr", "call", "tail", "ret", } +_KNOWN_OPCODES = { + "add", "addi", "sub", "mul", "div", "divu", "rem", "remu", + "sll", "slli", "srl", "srli", "sra", "srai", + "xor", "xori", "or", "ori", "and", "andi", + "slt", "slti", "sltu", "sltiu", + "lui", "auipc", "li", "mv", "neg", "not", "seqz", "snez", + "lw", "lh", "lb", "lbu", "lhu", "sw", "sh", "sb", "nop", + "max", "min", "maxu", "minu", +} | _CONTROL_FLOW_OPCODES -def _is_reg(s: str) -> bool: - """Check if a string is a known register name.""" - return s.strip() in _STANDARD_REGS - - -def _is_clobbered(inst: ParsedAsmLine, reg: str) -> bool: - """Check if an instruction writes to the given register.""" - if inst.opcode is None: - return False - if not inst.operands: - return False - # For most instructions, the first operand is the destination - dst_clobbers = { - "add", "addi", "sub", "mul", "div", "rem", "sll", "srl", "sra", - "xor", "or", "and", "slt", "sltu", - "lui", "li", "mv", "lw", "lh", "lb", "lbu", "lhu", - "auipc", "jal", "jalr", - "xori", "ori", "andi", "slli", "srli", "srai", - "slti", "sltiu", - } - if inst.opcode in dst_clobbers: - return canonical_reg(inst.operands[0]) == canonical_reg(reg) - # For stores, the first operand is the value (doesn't clobber dest reg) - # For branches, no destination - return False + +def _remove_redundant_lui_once( + insts: list[ParsedAsmLine], +) -> tuple[list[ParsedAsmLine], int]: + """Delete provably redundant LUI instructions within basic blocks.""" + result: list[ParsedAsmLine] = [] + lui_state: dict[str, int] = {} + changes = 0 + + for inst in insts: + # A label starts a new basic block, including ``label: instruction``. + if inst.label is not None: + lui_state.clear() + + if inst.opcode is None: + result.append(inst) + continue + + # Directives can change sections or assembler state. Do not carry + # register facts through a directive whose semantics are not modeled. + if inst.is_directive: + lui_state.clear() + result.append(inst) + continue + + opcode = inst.opcode + if opcode == "lui" and len(inst.operands) == 2: + rd = canonical_reg(inst.operands[0]) + imm = _parse_lui_imm(inst.operands[1]) + if imm is not None: + if lui_state.get(rd) == imm: + comment = ( + f"peephole: removed redundant lui " + f"{inst.operands[0]}, {inst.operands[1]}" + ) + if inst.comment: + comment += f"; {inst.comment}" + result.append(ParsedAsmLine( + raw=f" # {comment}", + comment=comment, + lineno=inst.lineno, + )) + changes += 1 + # The earlier LUI still defines the same value, so state + # deliberately remains unchanged after deleting this one. + continue + lui_state[rd] = imm + result.append(inst) + continue + + if opcode in _CONTROL_FLOW_OPCODES: + lui_state.clear() + elif opcode not in _KNOWN_OPCODES: + # An unknown instruction may write any register. Clearing all + # facts loses an optimization opportunity but preserves safety. + lui_state.clear() + else: + defines, _ = classify_def_use(inst) + for reg in defines: + lui_state.pop(canonical_reg(reg), None) + + result.append(inst) + + return result, changes def merge_constants(asm_text: str) -> tuple[str, int]: @@ -259,41 +300,11 @@ def merge_constants(asm_text: str) -> tuple[str, int]: insts, merged = _merge_lui_addi_once(insts) total_changes += merged - # --- Pass 2: Eliminate redundant lui --- - # Track the last upper-immediate value loaded into each register - # If a new lui loads the same value into the same register (and the - # register hasn't been clobbered in between), the second lui is redundant. - new_insts = [] - lui_state: dict[str, Optional[int]] = {} # reg -> upper imm value - - for inst in insts: - if inst.opcode == "lui" and inst.operands: - rd = canonical_reg(inst.operands[0]) - imm = ( - _parse_imm(inst.operands[1]) - if len(inst.operands) > 1 else None - ) - if rd in lui_state and lui_state[rd] == imm: - # Redundant: skip it, add a comment to the next instruction - total_changes += 1 - # Replace with a comment - comment_inst = AsmInst("") - comment_inst.comment = ( - f"peephole: removed redundant lui {rd}, {imm}" - ) - new_insts.append(comment_inst) - continue - else: - lui_state[rd] = imm - else: - # If this instruction writes to a tracked register, clear tracking - for reg in list(lui_state.keys()): - if _is_clobbered(inst, reg): - lui_state[reg] = None - - new_insts.append(inst) + # --- Pass 2: Eliminate redundant lui within basic blocks --- + insts, removed = _remove_redundant_lui_once(insts) + total_changes += removed - return _insts_to_asm(new_insts), total_changes + return _insts_to_asm(insts), total_changes # --------------------------------------------------------------------------- diff --git a/tests/test_const_merge.py b/tests/test_const_merge.py index 40a33fb..22f114f 100644 --- a/tests/test_const_merge.py +++ b/tests/test_const_merge.py @@ -195,6 +195,59 @@ def test_alias_clobber_prevents_redundant_lui_removal(self): assert changes == 0 assert result.count("lui") == 2 + def test_different_lui_value_is_not_redundant(self): + asm = " lui t0, 1\n add a0, a1, a2\n lui t0, 2\n" + result, changes = merge_constants(asm) + assert changes == 0 + assert sum(inst.opcode == "lui" for inst in _parse_asm(result)) == 2 + + def test_redundant_lui_does_not_cross_label(self): + asm = " lui t0, 1\nL1:\n lui t0, 1\n" + result, changes = merge_constants(asm) + assert changes == 0 + assert sum(inst.opcode == "lui" for inst in _parse_asm(result)) == 2 + + @pytest.mark.parametrize( + "boundary", + [ + " beq a0, zero, L1", + " j L1", + " call helper", + " jal ra, helper", + " jalr ra, 0(t1)", + " ret", + " jr ra", + ], + ) + def test_redundant_lui_does_not_cross_control_flow(self, boundary): + asm = f" lui t0, 1\n{boundary}\n lui t0, 1\n" + result, changes = merge_constants(asm) + assert changes == 0 + assert sum(inst.opcode == "lui" for inst in _parse_asm(result)) == 2 + + def test_unknown_instruction_clears_lui_state(self): + asm = " lui t0, 1\n custom.op a0, a1\n lui t0, 1\n" + result, changes = merge_constants(asm) + assert changes == 0 + assert sum(inst.opcode == "lui" for inst in _parse_asm(result)) == 2 + + def test_directive_clears_lui_state(self): + asm = " lui t0, 1\n.section .text\n lui t0, 1\n" + result, changes = merge_constants(asm) + assert changes == 0 + assert sum(inst.opcode == "lui" for inst in _parse_asm(result)) == 2 + + def test_removed_redundant_lui_preserves_comment(self): + asm = ( + " lui t0, 1\n" + " add a0, a1, a2\n" + " lui t0, 1 # duplicate high bits\n" + ) + result, changes = merge_constants(asm) + assert changes == 1 + assert "duplicate high bits" in result + assert sum(inst.opcode == "lui" for inst in _parse_asm(result)) == 1 + def test_redundant_lui_recognizes_aliases(self): asm = " lui t0, 1\n add a0, a1, a2\n lui x5, 1\n" result, changes = merge_constants(asm) From cc41fe4cdd300986b858cd0bc7f25af5d84e9e5b Mon Sep 17 00:00:00 2001 From: yuki-328 Date: Sat, 15 Aug 2026 16:29:14 +0800 Subject: [PATCH 07/12] feat: iterate constant merge with detailed stats --- scratchv/backend/const_merge.py | 80 +++++++++++++++++++++++++++++---- tests/test_const_merge.py | 51 ++++++++++++++++++++- 2 files changed, 121 insertions(+), 10 deletions(-) diff --git a/scratchv/backend/const_merge.py b/scratchv/backend/const_merge.py index a044e22..d9f20dd 100644 --- a/scratchv/backend/const_merge.py +++ b/scratchv/backend/const_merge.py @@ -14,6 +14,7 @@ import argparse import sys +from dataclasses import dataclass from typing import Optional from scratchv.backend._asm_parser import ( @@ -199,6 +200,20 @@ def _merge_lui_addi_once( # Constant merge optimization # --------------------------------------------------------------------------- +@dataclass +class ConstantMergeStats: + """Categorized results from one constant-merge optimization run.""" + + candidate_pairs: int = 0 + merged_pairs: int = 0 + redundant_lui_removed: int = 0 + iterations: int = 0 + + @property + def total_changes(self) -> int: + """Return all transformations while preserving the legacy count.""" + return self.merged_pairs + self.redundant_lui_removed + _CONTROL_FLOW_OPCODES = { "beq", "bne", "blt", "bge", "bltu", "bgeu", "beqz", "bnez", "blez", "bgtz", "bltz", "bgez", @@ -216,6 +231,35 @@ def _merge_lui_addi_once( } | _CONTROL_FLOW_OPCODES +def _count_merge_candidates(insts: list[ParsedAsmLine]) -> int: + """Count initially mergeable LUI+ADDI sequences.""" + candidates = 0 + for i, lui in enumerate(insts): + if lui.opcode != "lui" or len(lui.operands) != 2: + continue + j = i + 1 + while j < len(insts) and _is_separator(insts[j]): + j += 1 + if j >= len(insts): + continue + addi = insts[j] + if ( + addi.opcode != "addi" + or addi.label is not None + or len(addi.operands) != 3 + ): + continue + rd = canonical_reg(lui.operands[0]) + if ( + rd == canonical_reg(addi.operands[0]) + and rd == canonical_reg(addi.operands[1]) + and _parse_lui_imm(lui.operands[1]) is not None + and _parse_addi_imm(addi.operands[2]) is not None + ): + candidates += 1 + return candidates + + def _remove_redundant_lui_once( insts: list[ParsedAsmLine], ) -> tuple[list[ParsedAsmLine], int]: @@ -293,18 +337,36 @@ def merge_constants(asm_text: str) -> tuple[str, int]: ------- Tuple of (optimized_assembly_string, number_of_changes_made). """ - insts = _parse_asm(asm_text) - total_changes = 0 + optimized, stats = merge_constants_detailed(asm_text) + return optimized, stats.total_changes - # --- Pass 1: Merge safe lui+addi pairs into li --- - insts, merged = _merge_lui_addi_once(insts) - total_changes += merged - # --- Pass 2: Eliminate redundant lui within basic blocks --- - insts, removed = _remove_redundant_lui_once(insts) - total_changes += removed +def merge_constants_detailed( + asm_text: str, + *, + max_iterations: Optional[int] = None, +) -> tuple[str, ConstantMergeStats]: + """Optimize assembly to a fixed point and return detailed statistics. - return _insts_to_asm(insts), total_changes + Redundant LUI removal runs before pair merging so deleting a duplicate can + expose the earlier LUI to a following ADDI in the same iteration. + """ + insts = _parse_asm(asm_text) + stats = ConstantMergeStats( + candidate_pairs=_count_merge_candidates(insts), + ) + limit = max_iterations if max_iterations is not None else max(1, len(insts)) + + for _ in range(max(0, limit)): + insts, removed = _remove_redundant_lui_once(insts) + insts, merged = _merge_lui_addi_once(insts) + stats.iterations += 1 + stats.redundant_lui_removed += removed + stats.merged_pairs += merged + if removed == 0 and merged == 0: + break + + return _insts_to_asm(insts), stats # --------------------------------------------------------------------------- diff --git a/tests/test_const_merge.py b/tests/test_const_merge.py index 22f114f..dc86305 100644 --- a/tests/test_const_merge.py +++ b/tests/test_const_merge.py @@ -3,7 +3,8 @@ import pytest from scratchv.backend._asm_parser import ParsedAsmLine, canonical_reg from scratchv.backend.const_merge import ( - merge_constants, AsmInst, _parse_asm, _insts_to_asm, + AsmInst, ConstantMergeStats, _insts_to_asm, _parse_asm, + merge_constants, merge_constants_detailed, ) @@ -261,6 +262,54 @@ def test_register_canonicalization(self): assert canonical_reg("a0") == "x10" assert canonical_reg("X31") == "x31" + def test_fixed_point_exposes_pair_after_redundant_lui(self): + asm = " lui t0, 1\n lui x5, 1\n addi t0, x5, 2\n" + result, stats = merge_constants_detailed(asm) + assert isinstance(stats, ConstantMergeStats) + assert stats.candidate_pairs == 1 + assert stats.redundant_lui_removed == 1 + assert stats.merged_pairs == 1 + assert stats.total_changes == 2 + assert stats.iterations == 2 + assert "li t0, 4098" in result + assert not any(inst.opcode == "lui" for inst in _parse_asm(result)) + + def test_detailed_statistics_distinguish_rule_types(self): + asm = ( + " lui t0, 1\n" + " addi t0, t0, 2\n" + " lui t1, 3\n" + " add a0, a1, a2\n" + " lui x6, 3\n" + ) + _, stats = merge_constants_detailed(asm) + assert stats.candidate_pairs == 1 + assert stats.merged_pairs == 1 + assert stats.redundant_lui_removed == 1 + assert stats.total_changes == 2 + + def test_legacy_api_returns_detailed_total(self): + asm = " lui t0, 1\n lui x5, 1\n addi t0, x5, 2\n" + detailed_result, stats = merge_constants_detailed(asm) + legacy_result, changes = merge_constants(asm) + assert legacy_result == detailed_result + assert changes == stats.total_changes == 2 + + def test_optimization_is_idempotent(self): + asm = " lui t0, 1\n lui x5, 1\n addi t0, x5, 2\n" + once, first_stats = merge_constants_detailed(asm) + twice, second_stats = merge_constants_detailed(once) + assert first_stats.total_changes == 2 + assert twice == once + assert second_stats.total_changes == 0 + + def test_max_iterations_zero_disables_transformations(self): + asm = " lui t0, 1\n addi t0, t0, 2\n" + result, stats = merge_constants_detailed(asm, max_iterations=0) + assert stats.iterations == 0 + assert stats.total_changes == 0 + assert "lui" in result and "addi" in result + class TestCli: """Test CLI behavior.""" From 297f20a6f2002673cd52a92f4543c65c1d40ede5 Mon Sep 17 00:00:00 2001 From: yuki-328 Date: Sat, 15 Aug 2026 16:39:24 +0800 Subject: [PATCH 08/12] feat: integrate detailed constant merge statistics --- scratchv/backend/const_merge.py | 9 +++++++-- scratchv/compiler.py | 12 ++++++++---- tests/test_backend.py | 29 +++++++++++++++++++++++++++++ tests/test_const_merge.py | 22 ++++++++++++++++++++++ 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/scratchv/backend/const_merge.py b/scratchv/backend/const_merge.py index d9f20dd..f471788 100644 --- a/scratchv/backend/const_merge.py +++ b/scratchv/backend/const_merge.py @@ -396,11 +396,16 @@ def main() -> None: with open(args.input, "r") as f: asm_text = f.read() - result, changes = merge_constants(asm_text) + result, stats = merge_constants_detailed(asm_text) if args.verbose: print( - f"Constant merge: {changes} change(s) applied", + "Constant merge:\n" + f" candidate pairs: {stats.candidate_pairs}\n" + f" merged lui+addi pairs: {stats.merged_pairs}\n" + f" redundant lui removed: {stats.redundant_lui_removed}\n" + f" total transformations: {stats.total_changes}\n" + f" iterations: {stats.iterations}", file=sys.stderr, ) diff --git a/scratchv/compiler.py b/scratchv/compiler.py index a3484d2..bd5648a 100644 --- a/scratchv/compiler.py +++ b/scratchv/compiler.py @@ -450,10 +450,14 @@ def _run_asm_passes(self, asm_text: str, warnings: list[str]) -> str: warnings.append(f"Asm peephole: {changes} changes") if self.config.const_merge: - from scratchv.backend.const_merge import merge_constants - asm_text, changes = merge_constants(asm_text) - if changes: - warnings.append(f"Const merge: {changes} changes") + from scratchv.backend.const_merge import merge_constants_detailed + asm_text, stats = merge_constants_detailed(asm_text) + if stats.total_changes: + warnings.append( + f"Const merge: {stats.total_changes} changes " + f"({stats.merged_pairs} pairs, " + f"{stats.redundant_lui_removed} redundant lui)" + ) if self.config.schedule: from scratchv.backend.inst_scheduler import ( diff --git a/tests/test_backend.py b/tests/test_backend.py index 85f3111..9ded06a 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -5,6 +5,8 @@ from scratchv.backend.register_alloc import RegisterAllocator, MachineOp from scratchv.backend.asm_emit import AsmEmitter from scratchv.frontend.dsl_parser import DSLParser +from scratchv.compiler import CompilerConfig, CompilerDriver +from scratchv.main import args_to_config, build_arg_parser class TestInstructionSelect: @@ -108,3 +110,30 @@ def test_emit_relu(self): asm = emitter.emit() assert "max" in asm + + +class TestConstMergeIntegration: + def test_main_cli_flag_enables_compiler_config(self): + args = build_arg_parser().parse_args(["input.dsl", "--const-merge"]) + config = args_to_config(args) + assert config.const_merge is True + + def test_compiler_driver_runs_const_merge_post_pass(self): + driver = CompilerDriver(CompilerConfig(const_merge=True)) + warnings: list[str] = [] + result = driver._run_asm_passes( + " lui t0, 1\n addi t0, t0, 2\n", + warnings, + ) + assert "li t0, 4098" in result + assert len(warnings) == 1 + assert "1 changes" in warnings[0] + assert "1 pairs" in warnings[0] + + def test_disabled_config_leaves_assembly_unchanged(self): + driver = CompilerDriver(CompilerConfig(const_merge=False)) + source = " lui t0, 1\n addi t0, t0, 2\n" + warnings: list[str] = [] + result = driver._run_asm_passes(source, warnings) + assert result == source + assert warnings == [] diff --git a/tests/test_const_merge.py b/tests/test_const_merge.py index dc86305..94cc6d4 100644 --- a/tests/test_const_merge.py +++ b/tests/test_const_merge.py @@ -318,6 +318,28 @@ def test_main_importable(self): from scratchv.backend.const_merge import main assert callable(main) + def test_cli_writes_output_and_verbose_stats( + self, tmp_path, capsys, monkeypatch, + ): + from scratchv.backend.const_merge import main + + source = tmp_path / "input.s" + output = tmp_path / "output.s" + source.write_text(" lui t0, 1\n addi t0, t0, 2\n") + monkeypatch.setattr( + "sys.argv", + ["const_merge", str(source), "-o", str(output), "-v"], + ) + + main() + + captured = capsys.readouterr() + assert "candidate pairs: 1" in captured.err + assert "merged lui+addi pairs: 1" in captured.err + assert "redundant lui removed: 0" in captured.err + assert "total transformations: 1" in captured.err + assert "li t0, 4098" in output.read_text() + if __name__ == "__main__": pytest.main([__file__, "-v"]) From e5d87a6109ea6cc07a0878ed928618721b3e78ba Mon Sep 17 00:00:00 2001 From: yuki-328 Date: Sat, 15 Aug 2026 17:03:51 +0800 Subject: [PATCH 09/12] feat: measure constant merge on real benchmarks --- benchmarks/run_benchmark.py | 47 +++++++++++++++++++++++++++++++++++- benchmarks/test_benchmark.py | 17 +++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/benchmarks/run_benchmark.py b/benchmarks/run_benchmark.py index f473214..ec6e47f 100644 --- a/benchmarks/run_benchmark.py +++ b/benchmarks/run_benchmark.py @@ -36,6 +36,7 @@ from scratchv.frontend.dsl_parser import DSLParser from scratchv.ir.builder import IRBuilder from scratchv.ir.types import Program +from scratchv.backend._asm_parser import ParsedAsmLine, parse_asm # --------------------------------------------------------------------------- @@ -55,6 +56,18 @@ class BenchResult: ir_opt_inst_count: int = 0 codegen_time_s: float = 0.0 asm_line_count: int = 0 + lui_count_before: int = 0 + candidate_pairs: int = 0 + merged_pairs: int = 0 + redundant_lui_removed: int = 0 + asm_instructions_before: int = 0 + asm_instructions_after: int = 0 + machine_instructions_before: Optional[int] = None + machine_instructions_after: Optional[int] = None + code_size_before: Optional[int] = None + code_size_after: Optional[int] = None + pass_time_ms: float = 0.0 + output_equal: Optional[bool] = None total_time_s: float = 0.0 verified: bool = False error: Optional[str] = None @@ -70,6 +83,19 @@ def _count_ir(program: Program) -> tuple[int, int]: return inst, bb +def _count_parsed_asm_instructions(lines: list[ParsedAsmLine]) -> int: + """Count real assembly statements, excluding labels and directives.""" + return sum( + line.opcode is not None and not line.is_directive + for line in lines + ) + + +def _count_asm_instructions(asm_text: str) -> int: + """Parse and count assembly instructions using the shared parser.""" + return _count_parsed_asm_instructions(parse_asm(asm_text)) + + def _parse_onnx(path: str) -> Program: parser = ONNXParser() return parser.parse(path) @@ -173,6 +199,24 @@ def run_benchmark(model_name: str, model_path: str, *, asm_str, result.codegen_time_s = _codegen_llvm(program) else: asm_str, result.codegen_time_s = _codegen_riscv(program) + from scratchv.backend.const_merge import merge_constants_detailed + + # Single-variable A/B: both sides come from this exact codegen + # result, so frontend, optimization and allocation are identical. + parsed_before = parse_asm(asm_str) + result.lui_count_before = sum( + line.opcode == "lui" for line in parsed_before + ) + result.asm_instructions_before = _count_parsed_asm_instructions( + parsed_before, + ) + t0 = time.perf_counter() + asm_after, merge_stats = merge_constants_detailed(asm_str) + result.pass_time_ms = (time.perf_counter() - t0) * 1000 + result.candidate_pairs = merge_stats.candidate_pairs + result.merged_pairs = merge_stats.merged_pairs + result.redundant_lui_removed = merge_stats.redundant_lui_removed + result.asm_instructions_after = _count_asm_instructions(asm_after) result.asm_line_count = len(asm_str.splitlines()) # 4. Verify @@ -199,7 +243,8 @@ def run_all_benchmarks(models: dict[str, str], backend: str = "riscv", print(f"ERROR: {r.error}") else: print(f"done ({r.total_time_s:.3f}s, {r.ir_inst_count} IR inst, " - f"{r.asm_line_count} asm lines)") + f"{r.asm_line_count} asm lines, " + f"{r.merged_pairs + r.redundant_lui_removed} const changes)") results.append(r) return results diff --git a/benchmarks/test_benchmark.py b/benchmarks/test_benchmark.py index 74e08c2..7ac0f25 100644 --- a/benchmarks/test_benchmark.py +++ b/benchmarks/test_benchmark.py @@ -23,6 +23,7 @@ from benchmarks.generate_models import ensure_all_models from benchmarks.run_benchmark import run_benchmark +from benchmarks.run_benchmark import _count_asm_instructions # --------------------------------------------------------------------------- @@ -38,6 +39,11 @@ def benchmark_models() -> dict[str, str]: BACKEND_PARAMS = ["riscv"] +def test_effective_asm_instruction_count(): + asm = ".text\nmain:\n li t0, 1\n# comment\n add t1, t0, t0\n" + assert _count_asm_instructions(asm) == 2 + + def _model_id(name: str) -> str: return name @@ -151,6 +157,17 @@ def test_perf_pipeline(model_name: str, benchmark_models: dict[str, str]): assert result.error is None, f"Benchmark failed: {result.error}" assert result.ir_inst_count > 0 + assert isinstance(result.asm_instructions_before, int) + assert isinstance(result.asm_instructions_after, int) + reduction = ( + result.asm_instructions_before - result.asm_instructions_after + ) + tracked_changes = result.merged_pairs + result.redundant_lui_removed + assert reduction == tracked_changes, ( + f"instruction reduction {reduction} != tracked changes " + f"{tracked_changes}" + ) + assert result.pass_time_ms >= 0 print(f"\n {model_name}:") print(f" parse: {result.parse_time_s:.4f}s") From ae56e9a39ba8a1a3edaf2d4dbe9f5af45817cfe5 Mon Sep 17 00:00:00 2001 From: yuki-328 Date: Sat, 15 Aug 2026 17:10:32 +0800 Subject: [PATCH 10/12] feat: complete constant merge benchmarks and docs --- benchmarks/bench_const_merge.py | 143 +++++++++++++----- benchmarks/run_benchmark.py | 20 +++ benchmarks/test_benchmark.py | 84 +++++++++- ...07\346\241\243\345\210\235\347\250\277.md" | 80 +++++----- ...07\346\241\243\345\210\235\347\250\277.md" | 29 ++-- 5 files changed, 270 insertions(+), 86 deletions(-) diff --git a/benchmarks/bench_const_merge.py b/benchmarks/bench_const_merge.py index 21d837b..ddc37ee 100644 --- a/benchmarks/bench_const_merge.py +++ b/benchmarks/bench_const_merge.py @@ -12,56 +12,91 @@ from __future__ import annotations import argparse +import os import statistics +import sys import time -from typing import Optional -from scratchv.backend.const_merge import merge_constants +BENCH_DIR = os.path.dirname(__file__) +PROJ_DIR = os.path.dirname(BENCH_DIR) +sys.path.insert(0, PROJ_DIR) +from scratchv.backend._asm_parser import parse_asm +from scratchv.backend.const_merge import merge_constants_detailed -def _gen_synthetic_asm(num_instrs: int, seed: int = 42, - lui_ratio: float = 0.3) -> str: - """Generate synthetic assembly with lui+addi patterns. + +def _gen_synthetic_asm( + num_instructions: int, + seed: int = 42, + pair_density: float = 0.3, + redundant_lui_density: float = 0.1, +) -> str: + """Generate controlled synthetic assembly covering both optimization rules. Parameters ---------- - num_instrs: + num_instructions: Target number of instructions. seed: Random seed for reproducibility. - lui_ratio: + pair_density: Fraction of instructions that form lui+addi pairs. + redundant_lui_density: + Fraction of generated groups that contain a redundant LUI pattern. """ import random - random.seed(seed) + + if num_instructions < 0: + raise ValueError("num_instructions must be non-negative") + if not 0.0 <= pair_density <= 1.0: + raise ValueError("pair_density must be between 0 and 1") + if not 0.0 <= redundant_lui_density <= 1.0: + raise ValueError("redundant_lui_density must be between 0 and 1") + if pair_density + redundant_lui_density > 1.0: + raise ValueError( + "pair_density + redundant_lui_density must not exceed 1", + ) + + rng = random.Random(seed) lines = [".text", "synthetic_func:"] i = 0 - while i < num_instrs: - use_lui = random.random() < lui_ratio - - if use_lui and i + 1 < num_instrs: + while i < num_instructions: + choice = rng.random() + + if choice < redundant_lui_density and i + 2 < num_instructions: + regs = ["t0", "t1", "t2", "s0", "s1", "a0", "a1"] + reg = rng.choice(regs) + imm_hi = rng.choice([0x10000, 0x20000, 0x12345]) + lines.append(f" lui {reg}, {hex(imm_hi)}") + lines.append(" add a4, a5, a6") + lines.append(f" lui {reg}, {hex(imm_hi)}") + i += 3 + elif ( + choice < redundant_lui_density + pair_density + and i + 1 < num_instructions + ): regs = ["t0", "t1", "t2", "s0", "s1", "a0", "a1", "a2", "a3"] - r = random.choice(regs) - imm_hi = random.choice([0x10000, 0x20000, 0x12345, 0xABCDE, 0xFFFFF]) - imm_lo = random.choice([0x000, 0x100, 0x678, 0xFFF, 0x800]) + r = rng.choice(regs) + imm_hi = rng.choice([0x10000, 0x20000, 0x12345, 0xABCDE, 0xFFFFF]) + imm_lo = rng.choice([0x000, 0x100, 0x678, 0xFFF, 0x800]) lines.append(f" lui {r}, {hex(imm_hi)}") lines.append(f" addi {r}, {r}, {hex(imm_lo)}") i += 2 else: - op = random.choice(["add", "sub", "lw", "sw", "mv", "mul", "xor", - "li", "addi", "beq", "j", "ret"]) + op = rng.choice(["add", "sub", "lw", "sw", "mv", "mul", "xor", + "li", "addi", "beq", "j", "ret"]) regs = ["t0", "t1", "t2", "t3", "t4", "s0", "s1", "a0", "a1", "a2", "a3", "sp", "ra"] - r1 = random.choice(regs) - r2 = random.choice(regs) - r3 = random.choice(regs) + r1 = rng.choice(regs) + r2 = rng.choice(regs) + r3 = rng.choice(regs) if op == "li": - lines.append(f" {op} {r1}, {random.randint(0, 4096)}") + lines.append(f" {op} {r1}, {rng.randint(0, 4096)}") elif op == "addi": - lines.append(f" {op} {r1}, {r2}, {random.randint(-2048, 2047)}") + lines.append(f" {op} {r1}, {r2}, {rng.randint(-2048, 2047)}") elif op in ("lw", "sw"): - lines.append(f" {op} {r1}, {random.randint(0, 16)}(sp)") + lines.append(f" {op} {r1}, {rng.randint(0, 16)}(sp)") elif op in ("beq", "bne", "blt", "bge"): lines.append(f" {op} {r1}, {r2}, label_{i}") elif op == "j": @@ -78,24 +113,39 @@ def _gen_synthetic_asm(num_instrs: int, seed: int = 42, def bench_merge(asm_text: str, repeats: int = 50) -> dict: """Benchmark the constant merge optimizer.""" + if repeats < 1: + raise ValueError("repeats must be at least 1") times = [] results = [] for _ in range(repeats): t0 = time.perf_counter() - result, changes = merge_constants(asm_text) + result, stats = merge_constants_detailed(asm_text) t1 = time.perf_counter() times.append(t1 - t0) - results.append((result, changes)) - - changes_list = [r[1] for r in results] - input_lines = asm_text.count("\n") - output_lines = results[0][0].count("\n") if results else 0 + results.append((result, stats)) + + changes_list = [r[1].total_changes for r in results] + first_stats = results[0][1] + parsed_input = parse_asm(asm_text) + parsed_output = parse_asm(results[0][0]) + input_instructions = sum( + line.opcode is not None and not line.is_directive + for line in parsed_input + ) + output_instructions = sum( + line.opcode is not None and not line.is_directive + for line in parsed_output + ) return { - "input_lines": input_lines, - "output_lines": output_lines, - "line_reduction": input_lines - output_lines, + "benchmark_type": "synthetic", + "input_instructions": input_instructions, + "output_instructions": output_instructions, + "instruction_reduction": input_instructions - output_instructions, + "candidate_pairs": first_stats.candidate_pairs, + "merged_pairs": first_stats.merged_pairs, + "redundant_lui_removed": first_stats.redundant_lui_removed, "changes_mean": statistics.mean(changes_list), "changes_stdev": statistics.stdev(changes_list) if len(changes_list) > 1 else 0, "repeats": repeats, @@ -111,35 +161,50 @@ def main(): parser = argparse.ArgumentParser(description="Constant Merge Benchmark") parser.add_argument("--repeats", type=int, default=50, help="Number of repeat measurements") + parser.add_argument("--seed", type=int, default=42) + parser.add_argument("--pair-density", type=float, default=0.3) + parser.add_argument("--redundant-lui-density", type=float, default=0.1) args = parser.parse_args() sizes = [100, 500, 1000, 2000, 5000] print("=" * 80) print("RISC-V Constant Load Merge Optimizer Benchmark") + print("benchmark_type=synthetic") print("=" * 80) print(f"\n{'Size':>8} {'Mean(ms)':>10} {'Stdev(ms)':>10} " - f"{'Changes':>8} {'InpLines':>10} {'OutLines':>10} {'Reduc':>8}") + f"{'Pairs':>8} {'RedLUI':>8} {'InpInst':>10} {'OutInst':>10}") print("-" * 80) for size in sizes: - asm = _gen_synthetic_asm(size, lui_ratio=0.3) + asm = _gen_synthetic_asm( + size, + seed=args.seed, + pair_density=args.pair_density, + redundant_lui_density=args.redundant_lui_density, + ) stats = bench_merge(asm, repeats=args.repeats) print(f"{size:>8} {stats['mean_s'] * 1000:>10.3f} " f"{stats['stdev_s'] * 1000:>10.3f} " - f"{stats['changes_mean']:>8.1f} " - f"{stats['input_lines']:>10} {stats['output_lines']:>10} " - f"{stats['line_reduction']:>8}") + f"{stats['merged_pairs']:>8} " + f"{stats['redundant_lui_removed']:>8} " + f"{stats['input_instructions']:>10} " + f"{stats['output_instructions']:>10}") # Test different lui densities print(f"\nLUI Density Impact (2000 instructions):") print("-" * 60) for ratio in [0.0, 0.1, 0.3, 0.5]: - asm = _gen_synthetic_asm(2000, lui_ratio=ratio) + asm = _gen_synthetic_asm( + 2000, + seed=args.seed, + pair_density=ratio, + redundant_lui_density=args.redundant_lui_density, + ) stats = bench_merge(asm, repeats=args.repeats) print(f" ratio={ratio:.1f} {stats['mean_s'] * 1000:.3f} ms " f"changes: {stats['changes_mean']:.1f} " - f"reduction: {stats['line_reduction']}") + f"reduction: {stats['instruction_reduction']}") if __name__ == "__main__": diff --git a/benchmarks/run_benchmark.py b/benchmarks/run_benchmark.py index ec6e47f..8d5bad6 100644 --- a/benchmarks/run_benchmark.py +++ b/benchmarks/run_benchmark.py @@ -283,6 +283,26 @@ def print_summary(results: list[BenchResult]): for r in errors: print(f" - {r.model_name}: {r.error}") + riscv_results = [r for r in results if r.backend == "riscv"] + if riscv_results: + print("\nCONST-MERGE A/B") + print("-" * 90) + print( + f"{'Model':<16} {'LUI':>6} {'Candidates':>10} {'Pairs':>8} " + f"{'RedLUI':>8} {'Asm before→after':>18} {'Pass(ms)':>10}" + ) + for r in riscv_results: + asm_counts = ( + f"{r.asm_instructions_before}→{r.asm_instructions_after}" + ) + print( + f"{r.model_name:<16} {r.lui_count_before:>6} " + f"{r.candidate_pairs:>10} {r.merged_pairs:>8} " + f"{r.redundant_lui_removed:>8} {asm_counts:>18} " + f"{r.pass_time_ms:>10.3f}" + ) + print("Machine instructions/code size/output equality: N/A without toolchain") + def save_results(results: list[BenchResult], output_path: str): data = [asdict(r) for r in results] diff --git a/benchmarks/test_benchmark.py b/benchmarks/test_benchmark.py index 7ac0f25..c6091de 100644 --- a/benchmarks/test_benchmark.py +++ b/benchmarks/test_benchmark.py @@ -12,6 +12,7 @@ from __future__ import annotations import os +import json import sys import time @@ -22,7 +23,8 @@ sys.path.insert(0, PROJ_DIR) from benchmarks.generate_models import ensure_all_models -from benchmarks.run_benchmark import run_benchmark +from benchmarks.bench_const_merge import _gen_synthetic_asm, bench_merge +from benchmarks.run_benchmark import BenchResult, print_summary, run_benchmark, save_results from benchmarks.run_benchmark import _count_asm_instructions @@ -44,6 +46,86 @@ def test_effective_asm_instruction_count(): assert _count_asm_instructions(asm) == 2 +def test_synthetic_benchmark_covers_both_rules(): + asm = _gen_synthetic_asm( + 200, + seed=42, + pair_density=0.3, + redundant_lui_density=0.2, + ) + stats = bench_merge(asm, repeats=2) + assert stats["benchmark_type"] == "synthetic" + assert stats["merged_pairs"] > 0 + assert stats["redundant_lui_removed"] > 0 + assert stats["instruction_reduction"] == ( + stats["merged_pairs"] + stats["redundant_lui_removed"] + ) + assert stats["input_instructions"] == 200 + + +@pytest.mark.parametrize( + "pair_density,redundant_density", + [(-0.1, 0.1), (0.1, -0.1), (1.1, 0.0), (0.6, 0.5)], +) +def test_synthetic_density_validation(pair_density, redundant_density): + with pytest.raises(ValueError): + _gen_synthetic_asm( + 10, + pair_density=pair_density, + redundant_lui_density=redundant_density, + ) + + +def test_synthetic_seed_is_reproducible(): + kwargs = { + "num_instructions": 100, + "seed": 7, + "pair_density": 0.2, + "redundant_lui_density": 0.1, + } + assert _gen_synthetic_asm(**kwargs) == _gen_synthetic_asm(**kwargs) + + +def test_synthetic_repeats_validation(): + with pytest.raises(ValueError, match="repeats"): + bench_merge(" nop\n", repeats=0) + + +def _zero_hit_result() -> BenchResult: + return BenchResult( + model_name="zero_hit", + model_path="model.onnx", + backend="riscv", + optimize_level="all", + parse_time_s=0.0, + ir_inst_count=1, + ir_bb_count=1, + asm_instructions_before=3, + asm_instructions_after=3, + ) + + +def test_summary_keeps_zero_hit_case(capsys): + print_summary([_zero_hit_result()]) + output = capsys.readouterr().out + assert "CONST-MERGE A/B" in output + assert "zero_hit" in output + assert "3→3" in output + assert "N/A without toolchain" in output + + +def test_json_keeps_zero_and_na_fields(tmp_path, capsys): + output = tmp_path / "results.json" + save_results([_zero_hit_result()], str(output)) + capsys.readouterr() + data = json.loads(output.read_text())[0] + assert data["candidate_pairs"] == 0 + assert data["merged_pairs"] == 0 + assert data["redundant_lui_removed"] == 0 + assert data["machine_instructions_before"] is None + assert data["output_equal"] is None + + def _model_id(name: str) -> str: return name diff --git "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" index ccdcdf6..94b38ce 100644 --- "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" +++ "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" @@ -1,8 +1,8 @@ # ScratchV 课题 14:常量加载合并优化开发文档 -> **文档版本**:v0.2(补充 Benchmark 可观测性与验收方案) +> **文档版本**:v0.3(实现与本地验证状态回填) > **创建日期**:2026-07-28 -> **更新日期**:2026-08-01 +> **更新日期**:2026-08-15 > **作者**:[yuki] > **关联 Issue**:[#待补充] > **涉及模块**:`scratchv/backend/`、`scratchv/compiler.py`、`scratchv/main.py`、`tests/`、`docs/` @@ -15,6 +15,14 @@ ScratchV 主分支已经存在课题 14 的参考模块、测试和命令行集 > 对现有常量加载合并优化进行行为梳理、安全性加固、共享解析器重构、边界测试补全和集成验证。 +### 0.1 当前实现状态 + +已完成共享解析器迁移、寄存器别名规范化、安全的 RV32 `lui+addi` 合并、基本块内冗余 `lui` 删除、固定点迭代、分类统计、独立 CLI 与 CompilerDriver 集成、真实 case A/B 和 synthetic benchmark。旧接口 `merge_constants(...)->(str, int)` 保持兼容。 + +真实 case 抽查为零 `lui`、零候选和零转换,符合 `instruction_select.py` 直接生成 `MachineOp.LI` 的现状。当前环境没有 GNU RISC-V assembler 或 Spike,机器指令数、代码大小和执行等价字段按设计保留为 `N/A`。 + +本地回归结果为 410 项通过、4 项跳过;另有 2 项既有 TinyFive stub 测试因 `_m` 为 `None` 仍调用真实机器 `_set_pc()` 而失败,该问题位于未修改的模拟器模块,与本课题无关。 + --- @@ -22,7 +30,7 @@ ScratchV 主分支已经存在课题 14 的参考模块、测试和命令行集 ### 1.1 背景与动机 -- **现状问题**:RISC-V 加载一般 32 位常量常出现 `lui + addi` 序列;重复加载相同高 20 位还可能产生冗余 `lui`。现有参考实现具备基本功能,但仍需要补充控制流安全、寄存器别名、共享解析器和更严格的测试。 +- **现状问题**:RISC-V 加载一般 32 位常量常出现 `lui + addi` 序列;重复加载相同高 20 位还可能产生冗余 `lui`。`li` 是汇编器伪指令,不是 RV32I 的单条真实机器指令。现有参考实现具备基本功能,但仍需要补充控制流安全、寄存器别名、共享解析器和更严格的测试。 - **应用场景**:处理 ScratchV 后端生成的汇编、外部 RISC-V 汇编文件、课程测试 fixture,以及汇编级窥孔优化和统计流程。 - **现实限制(已按 commit `109a6a2` 核实)**:`instruction_select.py` 的常量、循环初值等路径直接生成 `MachineOp.LI`。因此现有 ONNX/DSL case 可能不产生 `lui+addi`,主流水线命中率可能为 0。零命中是需要记录的实验结果,不是需要通过改造真实输入掩盖的问题。 @@ -54,7 +62,7 @@ from scratchv.backend.const_merge import merge_constants optimized_asm, changes = merge_constants(asm_text) ``` -可选新增: +已新增: ```python optimized_asm, stats = merge_constants_detailed(asm_text) @@ -89,8 +97,8 @@ scratchv input.dsl -o output.s --const-merge 读取汇编文本 -> parse_asm -> 固定点循环 - -> 合并安全的 lui+addi -> 删除基本块内冗余 lui + -> 合并安全的 lui+addi -> lines_to_asm -> 返回输出与统计 ``` @@ -104,15 +112,15 @@ scratchv input.dsl -o output.s --const-merge - 无条件跳转; - `call`、`jal`、`jalr`; - `ret`、`jr`; -- 明确写目标寄存器的指令; -- 无法可靠分析的未知指令。 +- 对已跟踪寄存器产生定义的普通指令使该寄存器状态失效;这不妨碍规则 A 对相邻 `lui+addi` 进行整体匹配; +- 无法可靠分析的未知指令一律清空全部状态。 ### 2.3 模块间交互 - **上游**:接收 `AsmEmitter` 或线性扫描寄存器分配器生成的汇编文本,也可直接接收用户输入 `.s`; - **下游**:输出给调度器、汇编美化器、指令计数器和最终文件写入; - **对 IR 无影响**:不修改 AST、IR 或寄存器分配结果; -- **Pass 顺序**:建议位于汇编窥孔之后、调度器之前。 +- **Pass 顺序**:位于汇编窥孔之后、调度器之前,使窥孔优化暴露的模式可以被合并,并避免调度器打散待匹配的相邻序列。 --- @@ -282,7 +290,7 @@ diff -u before.dump after.dump ### 步骤 9:改造 Benchmark 并建立真实 case A/B -**当前基线(commit `109a6a2`)**: +**历史基线(commit `109a6a2`,以下缺口现已完成整改)**: - `benchmarks/bench_runner.py` 测 DSL/IR 执行,不进入 RISC-V 汇编 post-pass,不能证明 const-merge 有效; - `benchmarks/run_benchmark.py` 能生成 RISC-V 汇编,但当前不调用 `merge_constants`,并且只记录 `asm_line_count`; @@ -419,24 +427,24 @@ tests/fixtures/const_merge/ ## 8. 验收标准(Definition of Done) - [ ] 设计文档已评审,范围和非目标明确; -- [ ] `merge_constants` 现有公共接口保持兼容; -- [ ] 使用共享汇编解析器,或对不迁移给出明确理由; -- [ ] `lui+addi` 正确处理 12 位符号扩展和 RV32 截断; -- [ ] 冗余 `lui` 只在可证明安全的基本块内删除; -- [ ] 正确处理 ABI/数字寄存器别名; -- [ ] 不优化重定位表达式; -- [ ] 实现固定点迭代和统计; -- [ ] 新增正向、负向、边界和反例测试; -- [ ] `pytest tests/test_const_merge.py -v` 通过; -- [ ] `pytest tests/ -q` 全量通过; +- [x] `merge_constants` 现有公共接口保持兼容; +- [x] 使用共享汇编解析器; +- [x] `lui+addi` 正确处理 12 位符号扩展和 RV32 截断; +- [x] 冗余 `lui` 只在可证明安全的基本块内删除; +- [x] 正确处理 ABI/数字寄存器别名; +- [x] 不优化重定位表达式; +- [x] 实现固定点迭代和统计; +- [x] 新增正向、负向、边界和反例测试; +- [x] `pytest tests/test_const_merge.py -v` 通过; +- [x] 不含已知 TinyFive 环境问题的全量回归通过; - [ ] 至少 3 个汇编样例通过工具链或模拟器等价验证; -- [ ] `run_benchmark.py` 基于同一份原始 RISC-V 汇编完成 const-merge A/B; -- [ ] 真实 case 逐项报告候选数、分类转换数、有效汇编指令数和 pass 耗时; -- [ ] 工具链可用时报告机器指令数和 `.text` 大小,不可用时明确标记 `N/A`; -- [ ] `bench_const_merge.py` 明确标记 synthetic,并覆盖 `lui+addi` 与重复 `lui` 两类密度; -- [ ] Benchmark 不再使用换行数作为指令数,且零命中 case 不被过滤; -- [ ] 文档明确说明 `li` 是伪指令; -- [ ] 主 CLI `--const-merge` 可用; +- [x] `run_benchmark.py` 基于同一份原始 RISC-V 汇编完成 const-merge A/B; +- [x] 真实 case 逐项报告候选数、分类转换数、有效汇编指令数和 pass 耗时; +- [x] 工具链不可用时明确标记机器指令、代码大小和执行等价为 `N/A`; +- [x] `bench_const_merge.py` 明确标记 synthetic,并覆盖 `lui+addi` 与重复 `lui` 两类密度; +- [x] Benchmark 不再使用换行数作为指令数,且零命中 case 不被过滤; +- [x] 文档明确说明 `li` 是伪指令; +- [x] 主 CLI `--const-merge` 可用; - [ ] PR 描述包含前后示例、测试结果和已知限制。 --- @@ -466,20 +474,20 @@ tests/fixtures/const_merge/ | 阶段 | 计划完成日期 | 状态 | |---|---|---| -| 仓库走读与基线记录 | 2026-07-29 | ⬜ 待开始 | -| 设计文档评审 | 2026-07-31 | ⬜ 待开始 | -| Characterization tests | 2026-08-02 | ⬜ 待开始 | -| 核心编码与重构 | 2026-08-07 | ⬜ 待开始 | -| 边界测试与调试 | 2026-08-11 | ⬜ 待开始 | -| 工具链等价验证 | 2026-08-13 | ⬜ 待开始 | -| 文档完善与 PR | 2026-08-15 | ⬜ 待开始 | -| 代码审查与修订 | 2026-08-18 | ⬜ 待开始 | +| 仓库走读与基线记录 | 2026-07-29 | ✅ 已完成 | +| 设计文档评审 | 2026-07-31 | ✅ 已完成 | +| Characterization tests | 2026-08-02 | ✅ 已完成 | +| 核心编码与重构 | 2026-08-15 | ✅ 已完成 | +| 边界测试与调试 | 2026-08-15 | ✅ 已完成 | +| 工具链等价验证 | 待工具链可用 | ⏸ N/A | +| 文档完善 | 2026-08-15 | ✅ 已完成 | +| 代码审查与修订 | 2026-08-15 | ✅ 已完成 | --- -## 11. 第一天实际工作清单 +## 11. 第一天实际工作清单(历史记录) -按以下顺序开展,不要直接开始改算法: +以下内容保留为开发过程记录,不代表当前仍需新建分支或保持核心代码未修改: ```bash # 1. 进入仓库和环境 diff --git "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" index a3b0628..02945e7 100644 --- "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" +++ "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" @@ -1,8 +1,8 @@ # ScratchV 课题 14:常量加载合并优化技术设计文档 -> **文档版本**:v0.2(补充 Benchmark 设计与指标口径) +> **文档版本**:v0.3(实现与本地验证状态回填) > **编写日期**:2026-07-28 -> **更新日期**:2026-08-01 +> **更新日期**:2026-08-15 > **作者**:[yuki] > **关联 Issue**:[#待补充] > **涉及模块**:`scratchv/backend/const_merge.py`、`scratchv/backend/_asm_parser.py`、`scratchv/compiler.py`、`scratchv/main.py` @@ -29,6 +29,10 @@ ScratchV 主分支已经存在 `scratchv/backend/const_merge.py` 参考实现, - 保持公共函数 `merge_constants(asm_text) -> tuple[str, int]` 兼容; - ScratchV 主命令行开关使用当前代码中的 `--const-merge`,而不是归档题目中的 `--merge-constants`。 +### 0.1 已实现结论 + +第一阶段已按 RV32、单基本块、纯数值立即数和兼容旧 API 的假设完成。固定点循环采用“先删除冗余 `lui`,再合并 `lui+addi`”,从而让删除重复加载后暴露出的序列可以继续合并。详细接口为 `merge_constants_detailed()`,寄存器别名由共享解析器的 `canonical_reg()` 统一处理。 + --- ## 一、功能介绍 @@ -146,7 +150,9 @@ ScratchV 主命令行: scratchv input.dsl -o output.s --const-merge ``` -### 3.3 当前实现值得改进的点 +### 3.3 历史基线问题(现已整改) + +以下问题记录的是 commit `109a6a2` 的开发前状态: 1. `const_merge.py` 自己维护一套 `AsmInst` 解析逻辑,而项目已有共享的 `_asm_parser.py`; 2. 当前共享解析器的文档声称供 const-merge 使用,但实际参考实现仍重复解析代码; @@ -165,6 +171,7 @@ scratchv input.dsl -o output.s --const-merge ```python def sign_extend_12(value: int) -> int: + """把 12 位编码(0..0xFFF)解释为有符号值。""" value &= 0xFFF return value - 0x1000 if value & 0x800 else value ``` @@ -371,11 +378,11 @@ lui_state: dict[str, int] 处理规则: 1. 遇到 `lui rd, imm`: - - 若 `lui_state[canonical(rd)] == imm`,当前 `lui` 冗余,可删除; + - 若 `lui_state[canonical(rd)] == imm`,当前 `lui` 冗余,可删除,且保持原有状态不变; - 否则更新状态。 2. 遇到会定义某寄存器的指令:清除该寄存器状态; 3. 遇到标签、条件分支、无条件跳转、函数调用或返回:清空全部状态; -4. 遇到未知指令且无法可靠判断定义集合:保守清空状态或至少不进行跨越式删除; +4. 遇到未知指令且无法可靠判断定义集合:一律清空全部状态; 5. 不能跨基本块使用线性扫描状态。 #### 安全示例 @@ -410,11 +417,13 @@ addi t0, t0, 2 # 与前一个 lui 相邻,可继续合并 ```text repeat: - 应用规则 A 应用规则 B + 应用规则 A until 本轮无变化 or 达到最大迭代次数 ``` +先应用规则 B,是为了在删除重复 `lui` 后暴露前一条 `lui` 与后续 `addi` 的匹配机会。 + 最大迭代次数可设为 `max(1, len(lines))` 或一个保守上限。每次转换都会减少有效指令数,因此算法必然终止。 ### 6.6 复杂度 @@ -616,15 +625,15 @@ lui t0, 0x10000 ## 十、Benchmark 设计 -### 10.1 当前覆盖缺口 +### 10.1 历史覆盖缺口与当前状态 -基于 commit `109a6a2`,三条现有 benchmark 路径的职责如下: +基于 commit `109a6a2` 的历史缺口及当前整改结果如下: | 路径 | 当前行为 | 对课题 14 的覆盖 | |---|---|---| | `benchmarks/bench_runner.py` | 运行 DSL/IR 解释与性能 case | 不生成 RISC-V 汇编,不经过 const-merge | -| `benchmarks/run_benchmark.py` | ONNX 解析、IR 优化、RISC-V/LLVM codegen | RISC-V 路径生成汇编,但未调用 const-merge;仅统计 `splitlines()` 行数 | -| `benchmarks/bench_const_merge.py` | 固定 seed 生成人工 `lui+addi` 并计时 | 能证明人工模式命中,但当前没有重复 `lui` 密度、分类统计或有效指令计数 | +| `benchmarks/run_benchmark.py` | ONNX 解析、IR 优化、RISC-V/LLVM codegen | 已对同一原始汇编执行 const-merge A/B,并保留零命中和 `N/A` | +| `benchmarks/bench_const_merge.py` | 固定 seed 生成人工模式并计时 | 已覆盖两条规则、两类密度、分类统计和有效指令计数 | 因此,本课题不能只修改 `bench_const_merge.py` 后声称优化已体现在“项目 benchmark case”中。设计必须同时提供真实 case A/B 和 synthetic microbenchmark,两者结论分开呈现。 From 179f8853ffe5d602862eab61e60284d4b88fdfa6 Mon Sep 17 00:00:00 2001 From: yuki-328 Date: Sat, 15 Aug 2026 17:31:44 +0800 Subject: [PATCH 11/12] fix: harden constant merge validation and tests --- ...07\346\241\243\345\210\235\347\250\277.md" | 39 ++++++------ ...07\346\241\243\345\210\235\347\250\277.md" | 11 +++- scratchv/backend/_asm_parser.py | 16 ++++- scratchv/backend/const_merge.py | 32 ++++++---- scratchv/simulator/tinyfive.py | 29 +++++++-- tests/test_const_merge.py | 59 ++++++++++++++++++- tests/test_simulator.py | 19 ++++++ 7 files changed, 165 insertions(+), 40 deletions(-) diff --git "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" index 94b38ce..a84f953 100644 --- "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" +++ "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" @@ -21,7 +21,7 @@ ScratchV 主分支已经存在课题 14 的参考模块、测试和命令行集 真实 case 抽查为零 `lui`、零候选和零转换,符合 `instruction_select.py` 直接生成 `MachineOp.LI` 的现状。当前环境没有 GNU RISC-V assembler 或 Spike,机器指令数、代码大小和执行等价字段按设计保留为 `N/A`。 -本地回归结果为 410 项通过、4 项跳过;另有 2 项既有 TinyFive stub 测试因 `_m` 为 `None` 仍调用真实机器 `_set_pc()` 而失败,该问题位于未修改的模拟器模块,与本课题无关。 +本轮维护修复了 TinyFive stub 在 `_m` 为 `None` 时误用真实机器方法的问题,并补全汇编计数、指令上限、寄存器索引及有符号 32 位内存读写测试。当前本地全量回归为 430 项通过、4 项按环境条件跳过、零失败。 --- @@ -312,7 +312,7 @@ pass_time_ms = (time.perf_counter() - t0) * 1000 | 字段 | 说明 | |---|---| | `lui_count_before` | 优化前有效 `lui` 数量 | -| `candidate_pairs` | 同块内可检查的 `lui+addi` 候选数 | +| `candidate_pairs` | 同块内操作码和操作数数量匹配、可进入安全检查的 `lui+addi` 结构候选数;即使随后因寄存器或立即数校验失败也保留统计 | | `merged_pairs` | 规则 A 实际转换数 | | `redundant_lui_removed` | 规则 B 实际删除数 | | `asm_instructions_before/after` | 排除标签、伪操作、空行和纯注释后的汇编指令数 | @@ -349,20 +349,21 @@ make check # 若当前仓库提供 ## 6. 异常处理与边界条件 -- [ ] 空字符串输入返回空结果和零变化; -- [ ] 只有注释或标签时不报错; -- [ ] 操作数缺失时跳过,不抛出未处理异常; -- [ ] `0x7FF`、`0x800`、`0xFFF` 正确符号扩展; -- [ ] `0x80000000` 等 32 位边界按 RV32 规范化; -- [ ] `-0x1` 可解析; -- [ ] `%hi(symbol)`、`%lo(symbol)` 保持原样; -- [ ] `t0` 和 `x5` 被视为同一寄存器; -- [ ] 标签或分支不会导致错误删除; -- [ ] `call` 后不复用 caller-saved 寄存器状态; -- [ ] 注释和标签不会静默丢失; -- [ ] 未知操作码采用保守策略; -- [ ] 达到迭代上限时能正常停止; -- [ ] 优化结果具有幂等性。 +- [x] 空字符串输入返回空结果和零变化; +- [x] 只有注释或标签时不报错; +- [x] 操作数缺失时跳过,不抛出未处理异常; +- [x] `0x7FF`、`0x800`、`0xFFF` 正确符号扩展; +- [x] `0x80000000` 等 32 位边界按 RV32 规范化; +- [x] `-0x1` 可解析; +- [x] `%hi(symbol)`、`%lo(symbol)` 保持原样; +- [x] `t0` 和 `x5` 被视为同一寄存器; +- [x] 非法名称以及浮点、向量寄存器不参与整数常量合并; +- [x] 标签或分支不会导致错误删除; +- [x] `call` 后不复用 caller-saved 寄存器状态; +- [x] 注释、标签、首尾空行和空白行缩进不会静默丢失; +- [x] 未知操作码采用保守策略; +- [x] 达到迭代上限时能正常停止; +- [x] 优化结果具有字符串级幂等性。 --- @@ -390,6 +391,8 @@ make check # 若当前仓库提供 | T16 | 迭代暴露模式 | 删除冗余后出现相邻对 | 完成二次优化 | | T17 | 幂等性 | 对结果再次优化 | 0 变化 | | T18 | 空输入 | `""` | 空输出、0 变化 | +| T19 | 格式保持 | 首尾空行、缩进注释、纯空白行 | 零转换时逐字节不变,转换后再次运行逐字节不变 | +| T20 | 非整数寄存器 | `foo`、`x32`、`f0`、`v0` | 计为结构候选但安全拒绝,不转换 | ### 7.2 集成测试 @@ -435,8 +438,10 @@ tests/fixtures/const_merge/ - [x] 不优化重定位表达式; - [x] 实现固定点迭代和统计; - [x] 新增正向、负向、边界和反例测试; +- [x] 零转换时保留首尾空白,转换结果满足字符串级幂等性; +- [x] 只接受合法 RV32 整数寄存器参与两类转换; - [x] `pytest tests/test_const_merge.py -v` 通过; -- [x] 不含已知 TinyFive 环境问题的全量回归通过; +- [x] 全量回归通过;TinyFive stub 的汇编加载与 32 位内存读写回归已修复; - [ ] 至少 3 个汇编样例通过工具链或模拟器等价验证; - [x] `run_benchmark.py` 基于同一份原始 RISC-V 汇编完成 const-merge A/B; - [x] 真实 case 逐项报告候选数、分类转换数、有效汇编指令数和 pass 耗时; diff --git "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" index 02945e7..95a7a3f 100644 --- "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" +++ "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" @@ -31,7 +31,7 @@ ScratchV 主分支已经存在 `scratchv/backend/const_merge.py` 参考实现, ### 0.1 已实现结论 -第一阶段已按 RV32、单基本块、纯数值立即数和兼容旧 API 的假设完成。固定点循环采用“先删除冗余 `lui`,再合并 `lui+addi`”,从而让删除重复加载后暴露出的序列可以继续合并。详细接口为 `merge_constants_detailed()`,寄存器别名由共享解析器的 `canonical_reg()` 统一处理。 +第一阶段已按 RV32、单基本块、纯数值立即数和兼容旧 API 的假设完成。固定点循环采用“先删除冗余 `lui`,再合并 `lui+addi`”,从而让删除重复加载后暴露出的序列可以继续合并。详细接口为 `merge_constants_detailed()`,寄存器别名由共享解析器的 `canonical_reg()` 统一处理,`is_integer_reg()` 在转换前拒绝非法名称以及浮点、向量等非整数寄存器。 --- @@ -518,8 +518,11 @@ asm peephole -> const merge -> scheduler -> beautifier -> instruction counter - 不处理符号立即数; - 不对未知操作码做激进定义/使用推断; - 规范化寄存器别名; +- 仅允许 `x0` 至 `x31` 及其 psABI 别名参与整数常量转换; - 按 RV32 位宽截断。 +立即数安全范围也在变换前显式验证:`lui` 接受有符号 20 位写法或 `0..0xFFFFF` 的编码字段写法,`addi` 接受有符号 12 位写法或 `0..0xFFF` 的编码字段写法。超出范围的数值和 `%hi/%lo` 等符号表达式均保持原样,不做静默截断。 + --- ## 九、测试设计 @@ -532,6 +535,7 @@ asm peephole -> const merge -> scheduler -> beautifier -> instruction counter - 标签、注释、空行; - `0(sp)` 等带括号操作数; - 指令重建后内容可用。 +- 零转换时首尾空行、纯空白行和注释缩进逐字节保留。 #### B. `lui + addi` 合并 @@ -546,6 +550,7 @@ asm peephole -> const merge -> scheduler -> beautifier -> instruction counter - 中间有标签时不合并; - 目标寄存器或源寄存器不同时不合并; - `%hi/%lo` 不合并。 +- 非法名称、`x32`、浮点和向量寄存器不合并。 #### C. 冗余 `lui` 消除 @@ -559,7 +564,7 @@ asm peephole -> const merge -> scheduler -> beautifier -> instruction counter #### D. 属性测试 -- 幂等性:`opt(opt(x)) == opt(x)`; +- 字符串级幂等性:`opt(opt(x)) == opt(x)`,包括转换后位于文件末尾的注释和空行; - 无匹配输入:输出语义和有效指令不变; - 转换总数与分类统计一致; - 固定点迭代可发现删除后暴露的新模式。 @@ -665,7 +670,7 @@ pass_time_ms output_equal ``` -其中 `candidate_pairs` 是满足操作码、操作数和同块相邻条件、可进入规则 A 判断的数量;它与 `merged_pairs` 分开,便于区分“没有输入模式”和“候选因安全条件被拒绝”。若第一阶段无法稳定统计拒绝原因,至少保证该定义不把标签、注释行或跨块序列算入候选。 +其中 `candidate_pairs` 是在同一基本块内满足 `lui`/`addi` 操作码、操作数数量及相邻条件(中间只允许注释或空行)、可进入规则 A 安全检查的结构候选数。寄存器不一致、寄存器名称非法、立即数不可解析或越界的结构候选仍计入该字段,但不计入 `merged_pairs`。因此两者之差表示被安全检查拒绝的候选;标签、指令或跨块序列不计入候选。 ### 10.3 有效汇编指令计数 diff --git a/scratchv/backend/_asm_parser.py b/scratchv/backend/_asm_parser.py index e07e9b8..302a3f7 100644 --- a/scratchv/backend/_asm_parser.py +++ b/scratchv/backend/_asm_parser.py @@ -57,7 +57,7 @@ def to_asm(self) -> str: """Reconstruct the assembly line.""" # Empty lines if self.opcode is None and self.label is None and not self.raw.strip(): - return "" + return self.raw # Comment-only lines if self.opcode is None and self.label is None and self.comment: return self.raw @@ -161,6 +161,20 @@ def canonical_reg(reg: str) -> str: return _INTEGER_REGISTER_ALIASES.get(name, name) +def is_integer_reg(reg: str) -> bool: + """Return whether *reg* names one of the 32 integer registers. + + Both canonical ``xN`` names and psABI aliases are accepted. Keeping this + check separate from :func:`canonical_reg` lets conservative optimization + passes reject unknown operands instead of treating matching typos or + extension-specific names as registers. + """ + return ( + re.fullmatch(r"x([0-9]|[12][0-9]|3[01])", canonical_reg(reg)) + is not None + ) + + # ═══════════════════════════════════════════════════════════════════════════════ # Parsing # ═══════════════════════════════════════════════════════════════════════════════ diff --git a/scratchv/backend/const_merge.py b/scratchv/backend/const_merge.py index f471788..0aca4c4 100644 --- a/scratchv/backend/const_merge.py +++ b/scratchv/backend/const_merge.py @@ -2,7 +2,7 @@ Detects and merges lui+addi instruction pairs into single li pseudo-instructions, and eliminates redundant lui instructions -across basic blocks. +within basic blocks. Usage:: @@ -21,6 +21,7 @@ ParsedAsmLine, canonical_reg, classify_def_use, + is_integer_reg, lines_to_asm, parse_asm, parse_line, @@ -56,7 +57,7 @@ def __repr__(self) -> str: def _parse_asm(asm_text: str) -> list[ParsedAsmLine]: """Compatibility wrapper around the shared assembly parser.""" - return parse_asm(asm_text.strip()) + return parse_asm(asm_text) def _insts_to_asm(insts: list[ParsedAsmLine]) -> str: @@ -161,7 +162,10 @@ def _merge_lui_addi_once( rd = canonical_reg(lui.operands[0]) if ( - rd != canonical_reg(addi.operands[0]) + not is_integer_reg(lui.operands[0]) + or not is_integer_reg(addi.operands[0]) + or not is_integer_reg(addi.operands[1]) + or rd != canonical_reg(addi.operands[0]) or rd != canonical_reg(addi.operands[1]) ): result.append(lui) @@ -232,7 +236,14 @@ def total_changes(self) -> int: def _count_merge_candidates(insts: list[ParsedAsmLine]) -> int: - """Count initially mergeable LUI+ADDI sequences.""" + """Count structural LUI+ADDI candidates before safety checks. + + A candidate has the expected opcodes and operand counts in one basic + block, with only comments or blank lines between the instructions. + Register equality, register validity, and immediate representability are + deliberately checked by the transformation and reflected in + ``merged_pairs`` instead. + """ candidates = 0 for i, lui in enumerate(insts): if lui.opcode != "lui" or len(lui.operands) != 2: @@ -249,14 +260,7 @@ def _count_merge_candidates(insts: list[ParsedAsmLine]) -> int: or len(addi.operands) != 3 ): continue - rd = canonical_reg(lui.operands[0]) - if ( - rd == canonical_reg(addi.operands[0]) - and rd == canonical_reg(addi.operands[1]) - and _parse_lui_imm(lui.operands[1]) is not None - and _parse_addi_imm(addi.operands[2]) is not None - ): - candidates += 1 + candidates += 1 return candidates @@ -286,6 +290,10 @@ def _remove_redundant_lui_once( opcode = inst.opcode if opcode == "lui" and len(inst.operands) == 2: + if not is_integer_reg(inst.operands[0]): + lui_state.clear() + result.append(inst) + continue rd = canonical_reg(inst.operands[0]) imm = _parse_lui_imm(inst.operands[1]) if imm is not None: diff --git a/scratchv/simulator/tinyfive.py b/scratchv/simulator/tinyfive.py index 2d288b8..b333968 100644 --- a/scratchv/simulator/tinyfive.py +++ b/scratchv/simulator/tinyfive.py @@ -215,13 +215,17 @@ class StubProfiledMachine(ProfiledMachine): """Always-available stub for testing without TinyFive installed.""" def __init__(self): - super().__init__() + # Do not initialize and then discard a real TinyFive machine. The + # stub owns simple Python state and must remain independent of whether + # the optional dependency happens to be installed. self._available = True self._m = None + self.mem_size = 4096 self.regs = [0] * 32 self.memory: dict[int, int] = {} self._pc = 0 self.instr_count = 0 + self._code_words: list[int] = [] def load_binary(self, words: list[int], origin: int = 0): self._pc = origin @@ -231,25 +235,38 @@ def load_data(self, data: bytes, addr: int): for i, b in enumerate(data): self.memory[addr + i] = b + def load_asm(self, asm_lines: list[str], origin: int = 0x200): + """Record executable source lines for deterministic stub counting.""" + self._pc = origin + self._code_words = [ + 0 + for source in asm_lines + if (line := source.split("#", 1)[0].strip()) + and not line.endswith(":") + ] + def run(self, instructions=None, start=0): # Count words as executed instructions words = getattr(self, '_code_words', []) - self.instr_count = min(len(words), instructions or len(words)) + limit = len(words) if instructions is None else max(0, instructions) + self.instr_count = min(len(words), limit) def get_reg(self, idx: int) -> int: - return self.regs[idx] if idx < len(self.regs) else 0 + return self.regs[idx] if 0 <= idx < len(self.regs) else 0 def set_reg(self, idx: int, value: int): - if idx < len(self.regs): + if 0 <= idx < len(self.regs): self.regs[idx] = value def write_mem_i32(self, addr: int, value: int): - b = np.uint32(value).tobytes() + b = (value & 0xFFFFFFFF).to_bytes(4, "little") for i, byte in enumerate(b): self.memory[addr + i] = byte def read_mem_i32(self, addr: int) -> int: - return self.memory.get(addr, 0) + raw = bytes(self.memory.get(addr + i, 0) for i in range(4)) + value = int.from_bytes(raw, "little") + return value if value < 0x80000000 else value - 0x100000000 @property def pc(self) -> int: diff --git a/tests/test_const_merge.py b/tests/test_const_merge.py index 94cc6d4..a834eb0 100644 --- a/tests/test_const_merge.py +++ b/tests/test_const_merge.py @@ -1,7 +1,9 @@ """Tests for Constant Load Merge Optimizer.""" import pytest -from scratchv.backend._asm_parser import ParsedAsmLine, canonical_reg +from scratchv.backend._asm_parser import ( + ParsedAsmLine, canonical_reg, is_integer_reg, +) from scratchv.backend.const_merge import ( AsmInst, ConstantMergeStats, _insts_to_asm, _parse_asm, merge_constants, merge_constants_detailed, @@ -107,6 +109,20 @@ def test_no_changes_without_lui(self): asm = " add t0, t1, t2\n sub t3, t4, t5\n ret\n" result, changes = merge_constants(asm) assert changes == 0 + assert result == asm + + @pytest.mark.parametrize( + "asm", + [ + "\n\n add t0, t1, t2\n\n", + " \n\t\n", + " # indented comment\n", + ], + ) + def test_no_change_preserves_whitespace_exactly(self, asm): + result, changes = merge_constants(asm) + assert changes == 0 + assert result == asm def test_preserves_non_lui_addi(self): asm = "main:\n addi sp, sp, -16\n sw ra, 12(sp)\n ret\n" @@ -190,6 +206,15 @@ def test_merge_with_abi_and_x_register_aliases(self): assert changes == 1 assert "li t0, 4098" in result + @pytest.mark.parametrize("register", ["foo", "x32", "v0", "f0"]) + def test_invalid_or_non_integer_register_is_not_merged(self, register): + asm = f" lui {register}, 1\n addi {register}, {register}, 2\n" + result, stats = merge_constants_detailed(asm) + assert result == asm + assert stats.candidate_pairs == 1 + assert stats.merged_pairs == 0 + assert stats.total_changes == 0 + def test_alias_clobber_prevents_redundant_lui_removal(self): asm = " lui t0, 1\n add x5, a0, a1\n lui t0, 1\n" result, changes = merge_constants(asm) @@ -261,6 +286,16 @@ def test_register_canonicalization(self): assert canonical_reg("s0") == "x8" assert canonical_reg("a0") == "x10" assert canonical_reg("X31") == "x31" + assert is_integer_reg("t0") + assert is_integer_reg("X31") + assert not is_integer_reg("x32") + assert not is_integer_reg("foo") + + def test_invalid_register_lui_is_not_tracked_as_redundant(self): + asm = " lui foo, 1\n lui foo, 1\n" + result, changes = merge_constants(asm) + assert result == asm + assert changes == 0 def test_fixed_point_exposes_pair_after_redundant_lui(self): asm = " lui t0, 1\n lui x5, 1\n addi t0, x5, 2\n" @@ -303,6 +338,28 @@ def test_optimization_is_idempotent(self): assert twice == once assert second_stats.total_changes == 0 + def test_optimization_is_textually_idempotent_with_trailing_separator(self): + asm = " lui t0, 1\n# between\n\n addi t0, t0, 2\n" + once, first_stats = merge_constants_detailed(asm) + twice, second_stats = merge_constants_detailed(once) + assert first_stats.total_changes == 1 + assert twice == once + assert second_stats.total_changes == 0 + + @pytest.mark.parametrize( + "asm", + [ + " lui t0, 1\n addi t1, t0, 2\n", + " lui t0, %hi(symbol)\n addi t0, t0, %lo(symbol)\n", + " lui t0, 0x100000\n addi t0, t0, 1\n", + ], + ) + def test_candidates_include_structural_pairs_rejected_for_safety(self, asm): + result, stats = merge_constants_detailed(asm) + assert result == asm + assert stats.candidate_pairs == 1 + assert stats.merged_pairs == 0 + def test_max_iterations_zero_disables_transformations(self): asm = " lui t0, 1\n addi t0, t0, 2\n" result, stats = merge_constants_detailed(asm, max_iterations=0) diff --git a/tests/test_simulator.py b/tests/test_simulator.py index dc102f9..18c7b95 100644 --- a/tests/test_simulator.py +++ b/tests/test_simulator.py @@ -19,14 +19,26 @@ def test_instruction_counting(self): self.m.run() assert self.m.instr_count == 3 + self.m.run(instructions=0) + assert self.m.instr_count == 0 + def test_empty_asm(self): self.m.load_asm([]) self.m.run() assert self.m.instr_count == 0 + def test_asm_count_ignores_comments_labels_and_blank_lines(self): + self.m.load_asm(["entry:", "", "# note", "addi 10, 0, 42 # value"]) + self.m.run() + assert self.m.instr_count == 1 + assert self.m.pc == 0x200 + def test_register_access(self): self.m.regs[10] = 42 assert self.m.get_reg(10) == 42 + self.m.set_reg(-1, 99) + assert self.m.get_reg(-1) == 0 + assert self.m.regs[-1] == 0 def test_memory_access(self): self.m.write_mem_i32(100, 42) @@ -34,6 +46,13 @@ def test_memory_access(self): assert self.m.read_mem_i32(200) == 0 + def test_memory_access_preserves_full_signed_i32(self): + self.m.write_mem_i32(100, 0x12345678) + self.m.write_mem_i32(104, -2) + assert self.m.read_mem_i32(100) == 0x12345678 + assert self.m.read_mem_i32(104) == -2 + + class TestVerifyAssembly: def test_verify_without_tinyfive(self): """Should return error result when tinyfive is not installed.""" From e350729e3eae16f4c5c4c5f3790e54082c0f2316 Mon Sep 17 00:00:00 2001 From: yuki-328 Date: Sun, 16 Aug 2026 15:18:31 +0800 Subject: [PATCH 12/12] fix: address const merge review and harden CI --- .github/workflows/ci.yml | 63 ++++++++++++------- ...07\346\241\243\345\210\235\347\250\277.md" | 13 ++-- ...07\346\241\243\345\210\235\347\250\277.md" | 8 ++- scratchv/backend/_asm_parser.py | 14 ++--- scratchv/backend/const_merge.py | 20 ++++-- scratchv/simulator/tinyfive.py | 21 ++++--- tests/test_const_merge.py | 43 +++++++++++++ tests/test_simulator.py | 11 +++- 8 files changed, 139 insertions(+), 54 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d19f288..42059a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,18 +11,29 @@ env: permissions: contents: read - pages: write - id-token: write jobs: # ═════════════════════════════════════════════════════════════════════════ # 课题功能测试:所有14个模块的单元测试 + 集成测试 # ═════════════════════════════════════════════════════════════════════════ test: - runs-on: self-hosted + runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'self-hosted' }} timeout-minutes: 20 steps: + - name: Checkout pull request on isolated runner + if: github.event_name == 'pull_request' + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - name: Set up Python for pull request + if: github.event_name == 'pull_request' + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.12" + - name: Sync from local mirror (skip unstable GitHub checkout) + if: github.event_name != 'pull_request' run: | echo "=== CI Sync ===" retry() { @@ -46,12 +57,6 @@ jobs: cd "$WORKSPACE" echo "Fetching origin main..." retry "git fetch origin main" git fetch origin main || exit 1 - if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then - PR_NUMBER="${GITHUB_REF#refs/pull/}" - PR_NUMBER="${PR_NUMBER%/merge}" - echo "Fetching PR #${PR_NUMBER} merge ref..." - retry "git fetch PR #${PR_NUMBER}" git fetch origin "refs/pull/${PR_NUMBER}/merge" || exit 1 - fi echo "Checking out $GITHUB_SHA..." if git -c advice.detachedHead=false checkout -f "$GITHUB_SHA"; then echo "::notice::Checkout successful: $(git log -1 --format='%h %ai %s')" @@ -64,6 +69,7 @@ jobs: run: | python3.12 -m pip install --upgrade pip python3.12 -m pip install -e ".[all]" + python3.12 -m pip install "pytest>=7,<10" - name: Run all topic tests run: | @@ -72,6 +78,14 @@ jobs: --junit-xml=benchmark_reports/test_results.xml \ --ignore=tests/test_simulator.py + - name: Run PR #35 constant-merge regressions + run: | + python3.12 -m pytest \ + tests/test_const_merge.py \ + tests/test_backend.py::TestConstMergeIntegration \ + tests/test_simulator.py::TestStubProfiledMachine \ + -v --tb=short + - name: Generate test visualization page if: github.ref == 'refs/heads/main' run: | @@ -81,7 +95,7 @@ jobs: - name: Upload test reports if: github.ref == 'refs/heads/main' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: test-reports path: | @@ -93,10 +107,23 @@ jobs: # 模型性能测试:ONNX模型管线 + DSL用例 + CNN RISC-V编译 # ═════════════════════════════════════════════════════════════════════════ benchmark: - runs-on: self-hosted + runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'self-hosted' }} timeout-minutes: 30 steps: + - name: Checkout pull request on isolated runner + if: github.event_name == 'pull_request' + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - name: Set up Python for pull request + if: github.event_name == 'pull_request' + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.12" + - name: Sync from local mirror (skip unstable GitHub checkout) + if: github.event_name != 'pull_request' run: | echo "=== CI Sync ===" retry() { @@ -120,12 +147,6 @@ jobs: cd "$WORKSPACE" echo "Fetching origin main..." retry "git fetch origin main" git fetch origin main || exit 1 - if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then - PR_NUMBER="${GITHUB_REF#refs/pull/}" - PR_NUMBER="${PR_NUMBER%/merge}" - echo "Fetching PR #${PR_NUMBER} merge ref..." - retry "git fetch PR #${PR_NUMBER}" git fetch origin "refs/pull/${PR_NUMBER}/merge" || exit 1 - fi echo "Checking out $GITHUB_SHA..." if git -c advice.detachedHead=false checkout -f "$GITHUB_SHA"; then echo "::notice::Checkout successful: $(git log -1 --format='%h %ai %s')" @@ -138,7 +159,7 @@ jobs: run: | python3.12 -m pip install --upgrade pip python3.12 -m pip install -e ".[all]" - python3.12 -m pip install markdown + python3.12 -m pip install markdown "pytest>=7,<10" # ── 3.1 ONNX 模型管线基准测试 ────────────────────────────────────── - name: ONNX model pipeline benchmarks @@ -208,7 +229,7 @@ jobs: # ── 上报 ────────────────────────────────────────────────────────── - name: Upload benchmark reports - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: benchmark-reports path: benchmark_reports/ @@ -253,7 +274,7 @@ jobs: # ── 上传 Pages 产物 ────────────────────────────────────────────── - name: Upload Pages artifact if: github.ref == 'refs/heads/main' - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 with: path: benchmark_reports/ @@ -284,4 +305,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" index a84f953..30e89ed 100644 --- "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" +++ "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" @@ -1,8 +1,8 @@ # ScratchV 课题 14:常量加载合并优化开发文档 -> **文档版本**:v0.3(实现与本地验证状态回填) +> **文档版本**:v0.4(实现、AI review 与本地验证状态回填) > **创建日期**:2026-07-28 -> **更新日期**:2026-08-15 +> **更新日期**:2026-08-16 > **作者**:[yuki] > **关联 Issue**:[#待补充] > **涉及模块**:`scratchv/backend/`、`scratchv/compiler.py`、`scratchv/main.py`、`tests/`、`docs/` @@ -21,7 +21,7 @@ ScratchV 主分支已经存在课题 14 的参考模块、测试和命令行集 真实 case 抽查为零 `lui`、零候选和零转换,符合 `instruction_select.py` 直接生成 `MachineOp.LI` 的现状。当前环境没有 GNU RISC-V assembler 或 Spike,机器指令数、代码大小和执行等价字段按设计保留为 `N/A`。 -本轮维护修复了 TinyFive stub 在 `_m` 为 `None` 时误用真实机器方法的问题,并补全汇编计数、指令上限、寄存器索引及有符号 32 位内存读写测试。当前本地全量回归为 430 项通过、4 项按环境条件跳过、零失败。 +本轮维护修复了 TinyFive stub 在 `_m` 为 `None` 时误用真实机器方法的问题,并补全汇编计数、指令上限、寄存器索引及有符号 32 位内存读写测试。AI review 进一步修复了未修改汇编文本被规范化重写、数字/美元标签识别、未知行边界、大小写寄存器定义识别、stub 伪操作计数和 `x0` 写入等问题。当前本地全量回归为 439 项通过、4 项按环境条件跳过、零失败。 --- @@ -361,6 +361,7 @@ make check # 若当前仓库提供 - [x] 标签或分支不会导致错误删除; - [x] `call` 后不复用 caller-saved 寄存器状态; - [x] 注释、标签、首尾空行和空白行缩进不会静默丢失; +- [x] 发生转换时,未修改的字符串伪操作、数字/美元标签和其他原始汇编文本逐字保留; - [x] 未知操作码采用保守策略; - [x] 达到迭代上限时能正常停止; - [x] 优化结果具有字符串级幂等性。 @@ -438,7 +439,7 @@ tests/fixtures/const_merge/ - [x] 不优化重定位表达式; - [x] 实现固定点迭代和统计; - [x] 新增正向、负向、边界和反例测试; -- [x] 零转换时保留首尾空白,转换结果满足字符串级幂等性; +- [x] 未修改汇编文本逐字保留,转换结果满足字符串级幂等性; - [x] 只接受合法 RV32 整数寄存器参与两类转换; - [x] `pytest tests/test_const_merge.py -v` 通过; - [x] 全量回归通过;TinyFive stub 的汇编加载与 32 位内存读写回归已修复; @@ -480,13 +481,13 @@ tests/fixtures/const_merge/ | 阶段 | 计划完成日期 | 状态 | |---|---|---| | 仓库走读与基线记录 | 2026-07-29 | ✅ 已完成 | -| 设计文档评审 | 2026-07-31 | ✅ 已完成 | +| 设计文档评审 | 待维护者确认 | ⏳ 待评审 | | Characterization tests | 2026-08-02 | ✅ 已完成 | | 核心编码与重构 | 2026-08-15 | ✅ 已完成 | | 边界测试与调试 | 2026-08-15 | ✅ 已完成 | | 工具链等价验证 | 待工具链可用 | ⏸ N/A | | 文档完善 | 2026-08-15 | ✅ 已完成 | -| 代码审查与修订 | 2026-08-15 | ✅ 已完成 | +| 本地 AI review 与修订 | 2026-08-16 | ✅ 已完成 | --- diff --git "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" index 95a7a3f..aa5ceef 100644 --- "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" +++ "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" @@ -519,6 +519,8 @@ asm peephole -> const merge -> scheduler -> beautifier -> instruction counter - 不对未知操作码做激进定义/使用推断; - 规范化寄存器别名; - 仅允许 `x0` 至 `x31` 及其 psABI 别名参与整数常量转换; +- 数字标签、包含 `$` 的标签及无法分类的非空汇编行均作为状态边界; +- 仅重建被转换的指令,其他源文本逐字保留,避免改变字符串伪操作等数据; - 按 RV32 位宽截断。 立即数安全范围也在变换前显式验证:`lui` 接受有符号 20 位写法或 `0..0xFFFFF` 的编码字段写法,`addi` 接受有符号 12 位写法或 `0..0xFFF` 的编码字段写法。超出范围的数值和 `%hi/%lo` 等符号表达式均保持原样,不做静默截断。 @@ -532,10 +534,10 @@ asm peephole -> const merge -> scheduler -> beautifier -> instruction counter #### A. 汇编解析 - 十进制、十六进制、负十进制、负十六进制; -- 标签、注释、空行; +- 普通标签、数字标签、包含 `$` 的标签、注释和空行; - `0(sp)` 等带括号操作数; -- 指令重建后内容可用。 -- 零转换时首尾空行、纯空白行和注释缩进逐字节保留。 +- 指令重建后内容可用; +- 无论是否发生转换,未修改的字符串伪操作、标签、首尾空行、纯空白行和注释缩进逐字节保留。 #### B. `lui + addi` 合并 diff --git a/scratchv/backend/_asm_parser.py b/scratchv/backend/_asm_parser.py index 302a3f7..d92f3d7 100644 --- a/scratchv/backend/_asm_parser.py +++ b/scratchv/backend/_asm_parser.py @@ -108,7 +108,7 @@ def is_comment_only(self) -> bool: _LINE_RE = re.compile( r'^\s*' - r'(?P