diff --git "a/docs/topics/13-\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250-\350\256\276\350\256\241\346\226\207\346\241\243.md" "b/docs/topics/13-\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250-\350\256\276\350\256\241\346\226\207\346\241\243.md" new file mode 100644 index 0000000..25264e6 --- /dev/null +++ "b/docs/topics/13-\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250-\350\256\276\350\256\241\346\226\207\346\241\243.md" @@ -0,0 +1,502 @@ +# 课题13:汇编窥孔优化器 — 设计文档 + +> **读者**:模块负责人、代码审查者、新加入的编译器学习者 +> **源文件**:`scratchv/backend/asm_peephole.py` +> **状态**:✅ 课题收尾完成 | **最后验证**:2026-08-01(**83/83** 测试,8 条默认规则) + +--- + +## 1. 文档目的 + +本文档描述 ScratchV **汇编层窥孔优化器(Assembly Peephole Optimizer)** 的设计目标、架构、算法与规则集,供人类阅读与评审。 + +- **课题完成目录(总入口)** → [`topic13/README.md`](../../topic13/README.md) +- **面向新手的课题教程** → [13-窥孔优化器.md](13-窥孔优化器.md) +- **面向 AI Agent 的实现指南** → [archive/topic13_asm_peephole_guide.md](archive/topic13_asm_peephole_guide.md) + +--- + +## 2. 问题定义 + +### 2.1 背景 + +ScratchV 后端按模板逐条生成 RISC-V 汇编,会产生大量**语义等价但指令更多**的序列,例如: + +```asm +li t0, 10 # 加载常量 10 +addi t0, t0, 5 # 再加 5 +addi t0, t0, 3 # 再加 3 +beq x0, x0, L # 永远成立的条件分支 +``` + +理想输出: + +```asm +li t0, 18 # 一条 li 搞定 +j L # 无条件跳转 +``` + +### 2.2 设计目标 + +| 目标 | 说明 | +|------|------| +| **减少指令数** | 合并连续同类操作、删除冗余 mv、简化分支 | +| **保持语义等价** | 优化前后程序行为不变 | +| **局部性** | 每次只看 1~2 条相邻指令(滑动窗口) | +| **可扩展** | 规则以数据驱动方式注册,便于新增 | +| **可观测** | 输出每条规则的命中次数与总变更数 | + +### 2.3 非目标(当前版本不做) + +- 跨基本块的全局数据流分析 +- 寄存器活跃性分析(Rule 4 mv 链仅为两指令局部改写,中间寄存器存活时可能不健全) +- 与共享汇编解析器 `_asm_parser.py` 的统一(技术债,见 §8) + +> **已纳入目标(勿再当作非目标)**:`addi+addi` 的 simm12 溢出拒绝;中窗标签拒绝融合;删除时保留标签;假交换对不删除。 + +--- + +## 3. 在编译管线中的位置 + +``` +ONNX/DSL + → IR 构建 + → IR 优化(含 scratchv/optimizer/peephole.py,**不同模块**) + → 指令选择 + → 寄存器分配 + → AsmEmitter 生成汇编文本 + → ┌─ _run_asm_passes() ─────────────────────┐ + │ 1. AsmPeepholeOptimizer ← 本模块 │ + │ 2. const_merge(常量加载合并) │ + │ 3. inst_scheduler(指令调度) │ + │ 4. asm_beautifier(美化) │ + └───────────────────────────────────────────┘ + → 输出 .s 文件 +``` + +**启用方式**: + +```bash +# 完整编译管线 +scratchv model.onnx --peephole-asm + +# 独立 CLI 工具 +python -m scratchv.backend.asm_peephole input.s -o output.s --report +``` + +**与 IR 层 peephole 的区别**: + +| 维度 | IR 层 `optimizer/peephole.py` | 汇编层 `backend/asm_peephole.py` | +|------|-------------------------------|----------------------------------| +| 输入 | 三地址码 IR 指令 | RISC-V 汇编文本 | +| 典型模式 | `x = mul x, 1` | `addi x,x,3; addi x,x,5` | +| 开关 | `--optimize` | `--peephole-asm` | + +--- + +## 4. 架构概览 + +``` +┌─────────────────────────────────────────────────────────┐ +│ AsmPeepholeOptimizer │ +├──────────────┬──────────────────┬───────────────────────┤ +│ 解析层 │ 规则引擎 │ 输出层 │ +│ _parse_asm │ _match_rule │ _apply_replacement │ +│ _parse_line │ _default_rules │ _lines_to_asm │ +│ AsmLine │ PeepholeRule │ report() │ +└──────────────┴──────────────────┴───────────────────────┘ +``` + +### 4.1 核心数据结构 + +**AsmLine** — 一条汇编的结构化表示: + +| 字段 | 类型 | 含义 | +|------|------|------| +| `raw` | str | 原始行文本 | +| `label` | str \| None | 标签名(不含冒号) | +| `opcode` | str \| None | 小写操作码 | +| `operands` | list[str] | 操作数列表 | +| `comment` | str \| None | 行尾注释 | +| `lineno` | int | 源行号 | + +**PeepholeRule** — 一条优化规则: + +| 字段 | 类型 | 含义 | +|------|------|------| +| `name` | str | 规则名称(用于报告与 `_apply_replacement` 分支) | +| `pattern` | list[str] | 连续 opcode 序列,`*` 为通配 | +| `replacement` | list[str] | 替换模板;空列表 = 删除 | +| `register_constraints` | list[tuple] | `(dst_idx, src_instr_idx, src_op_idx)` | + +--- + +## 5. 算法设计 + +### 5.1 不动点迭代 + +``` +lines ← parse(asm_text) +repeat (最多 50 轮): + changed ← false + new_lines ← [] + i ← 0 + while i < len(lines): + for rule in rules (按注册顺序): + window ← lines[i : i + len(rule.pattern)] + if match(rule, window): + new_lines += apply(rule, window) + i += len(rule.pattern) + changed ← true + break + else: + new_lines += lines[i] + i += 1 + lines ← new_lines + if not changed: break +return serialize(lines), total_changes +``` + +**设计选择**: + +- **贪心**:从左到右,命中第一条规则即应用,不保证全局最优 +- **不动点**:一轮替换可能产生新机会(如 3 条连续 addi 需 2 轮) +- **安全上限**:`max_iterations = 50`,防止规则循环导致死循环 + +### 5.2 模式匹配 + +对窗口内每条指令: + +1. **opcode 匹配**:pattern 与 line.opcode 相等(或 `*`) +2. **操作数绑定**:按位置绑定 `rd0`, `rs0_1`, `rs0_2`, `imm0` 等变量 +3. **寄存器约束**:跨指令检查,如两条 addi 必须修改同一寄存器 +4. **标签安全**:窗口内第 2 条及以后若带 label,拒绝匹配(避免丢掉跳转目标) +5. **特殊规则**:Rule 3(beq)额外要求操作数为 `x0` 或 `zero`;Rule 4(mv 链)排除 swap 形 `mv x,y; mv y,x` + +### 5.3 替换生成 + +`_apply_replacement()` 按规则名分支: + +- 用 `_parse_imm` 计算派生值(如 `imm_sum`,支持十进制/十六进制) +- 模板替换 `{rd}`, `{imm_sum}`, `{label}` 等 +- 窗口首条标签转移到替换结果(删除规则则保留裸标签行) +- 生成带注释 `# peephole: ` 的新 AsmLine + +--- + +## 6. 规则目录 + +| # | 名称 | 匹配模式 | 替换 | 约束 | 效果 | +|---|------|----------|------|------|------| +| 1 | addi+addi fusion | `addi; addi` | `addi rd, rs1, imm_sum` | 同 rd;**imm 和 ∈ [-2048,2047]** | 2→1 | +| 2 | li+addi fusion | `li; addi` | `li rd, imm_sum` | li 的 rd = addi 的 rd=rs1 | 2→1 | +| 3 | beq zero-zero to j | `beq` | `j label` | rs1,rs2 ∈ {x0,zero} | 语义简化 | +| 4 | redundant mv elimination | `mv; mv` | `mv c,b` | 第二条 rs = 第一条 rd;**排除** swap 形 | 2→1 | +| 5 | addi-zero self elimination | `addi` | (删除) | rd==rs 且 imm==0 | 1→0 | +| 6 | addi-zero to mv | `addi` | `mv rd, rs` | imm==0 且 rd≠rs | 1→1(更简) | +| 7 | nop elimination | `nop` | (删除) | — | 1→0 | +| 8 | mv-self elimination | `mv` | (删除) | rd==rs | 1→0 | + +**正确性**: + +- Rule 1:若 `imm1+imm2` 超出有符号 12 位范围,则**不合并**。 +- **已移除**旧规则 `mv x,y; mv y,x → 删除`:两条指令执行后两寄存器都等于原来的 `y`,不是空操作,删除会破坏语义。 +- Rule 4(mv 链):若中间寄存器在后续仍存活,改写可能不健全;测试用 `test_mv_chain_unsound_when_mid_live` 记录该限制。 + +### 6.1 规则示例 + +**Rule 1 — addi 合并** + +```asm +# Before + addi t0, t0, 3 + addi t0, t0, 5 + +# After + addi t0, t0, 8 # peephole: addi+addi fusion +``` + +**Rule 2 — li + addi 常量折叠** + +```asm +# Before + li t0, 10 + addi t0, t0, 5 + +# After + li t0, 15 # peephole: li+addi fusion +``` + +**Rule 3 — 无条件跳转简化** + +```asm +# Before + beq x0, x0, loop_start + +# After + j loop_start # peephole: beq zero-zero to jump +``` + +**(反例)禁止删除「假交换」** + +```asm +# t0=1, t1=2 执行后 → t0=2, t1=2(不是交换,也不是空操作) + mv t0, t1 + mv t1, t0 +# 不得删除;删除后仍为 t0=1,t1=2 → 语义错误 +``` + +**Rule 4 — 跳过中间 mv** + +```asm +# Before + mv t0, t1 + mv t2, t0 + +# After + mv t2, t1 # peephole: redundant mv elimination +# 注意:若后续仍使用 t0,此改写可能不健全(无活跃性分析时的 best-effort) +``` + +--- + +## 7. 测试体系 + +### 7.1 四类测试概览 + +| 类型 | 标记 | 文件 | 测什么 | 怎么跑 | +|------|------|------|--------|--------| +| **功能单元测试** | `@pytest.mark.unit` | `tests/test_asm_peephole.py` | 解析、匹配引擎、规则(含溢出/新消除)、Optimizer API | 见下方命令 | +| **功能集成测试** | `@pytest.mark.integration` | `tests/test_asm_peephole_integration.py` | CompilerDriver、`--peephole-asm`、后端链路 | 见下方命令 | +| **压力测试** | `@pytest.mark.stress` | `tests/test_asm_peephole_stress.py` | 500~5000 对 fusion、确定性、耗时上限 | 见下方命令 | +| **黑盒测试** | `@pytest.mark.blackbox` | `tests/test_asm_peephole_blackbox.py` | CLI 子进程、fixture 文件、公开 API | 见下方命令 | + +**Fixtures**:`tests/fixtures/asm_peephole/*.s`(黑盒输入样例) + +### 7.2 一键运行 + +```bash +source .venv/bin/activate + +# 全部窥孔测试(83 项) +python -m pytest tests/test_asm_peephole*.py -v + +# 按类型单独跑 +python -m pytest tests/ -m unit -k peephole -v +python -m pytest tests/ -m integration -k peephole -v +python -m pytest tests/ -m stress -k peephole -v +python -m pytest tests/ -m blackbox -k peephole -v +``` + +### 7.3 各类测试要点 + +**单元测试(白盒)** +- 解析 / 匹配引擎 / 8 条默认规则 +- 溢出、hex/负数立即数、标签阻挡融合 +- **语义等价**(`TestSemanticEquivalence`):假交换必须保留;mv 链局限有文档化用例 +- 幂等性、自定义规则 + +**集成测试** +- `CompilerDriver(peephole_asm=True/False)` 行为对比 +- `_run_asm_passes` 与 beautify / const_merge 联调 +- driver 结果与直接 `optimize()` 一致 + +**压力测试** +- 500 / 2000 / 5000 对 fusion;hex 大批量;中间标签不丢失 + +**黑盒测试** +- CLI + fixtures(含 overflow / hex / nop / mv-chain) +- `--list-rules` **不得**列出已移除的假交换删除规则 + +### 7.4 最新结果(2026-08-01) + +| 类别 | 结果 | +|------|------| +| 全部 `tests/test_asm_peephole*.py` | **83 PASSED**(约 1.3s) | +| 默认规则数 | **8**(假交换删除已移除) | + +| 测试类 | 覆盖点 | +|--------|--------| +| `TestParseAsm` / `TestMatchEngine` | 解析、匹配、中窗标签拒绝 | +| `TestDefaultRules` / `TestCorrectnessAndNewRules` | 规则 + 溢出/消除 | +| `TestSemanticEquivalence` | 寄存器状态等价 / 已知不健全点 | +| `TestCompilerPipelineIntegration` | CompilerDriver 集成 | +| `TestPeepholeStress` | 规模与标签压力 | +| `TestPeepholeCLI` | CLI 黑盒 | + +### 7.5 CLI 冒烟 + +```bash +python -m scratchv.backend.asm_peephole input.s -o output.s --report +``` + +### 7.6 性能基准 + +```bash +python benchmarks/bench_asm_peephole.py +``` + +| 规模 | 耗时(ms) | 变更次数 | 行数减少 | +|------|----------|----------|----------| +| 100 instr | 0.6 | 24 | 25 | +| 500 | 4.0 | 121 | 122 | +| 1000 | 7.8 | 240 | 241 | +| 2000 | 19.5 | 505 | 507 | +| 5000 | 48.2 | 1232 | 1235 | + +fusion_ratio=0.3 时,约 **25% 指令可被合并**(合成数据)。 + +### 7.7 窥孔优化前后对比(2026-08-01) + +```bash +python benchmarks/compare_peephole.py \ + --json benchmark_reports/peephole_compare.json \ + --markdown benchmark_reports/peephole_compare.md +``` + +#### 对比方法 + +| 维度 | 说明 | +|------|------| +| **优化前** | `peephole_asm=False` | +| **优化后** | `AsmPeepholeOptimizer` / `--peephole-asm` | +| **指标** | 静态指令数(`inst_counter`) | + +#### 结果一:DSL 基准(23 用例) + +| 指标 | 优化前 | 优化后 | 变化 | +|------|--------|--------|------| +| 静态指令合计 | 231 | 230 | **-1 (-0.43%)** | +| 有节省的用例 | — | 1 / 23 | 仅 `006_softmax`(mv 链) | + +小程序冗余少 → 整体收益低是预期现象。 + +#### 结果二:合成高 fusion 汇编 + +| 规模 | 优化前 | 优化后 | 节省率 | +|------|--------|--------|--------| +| 100 | 101 | 77 | 23.8% | +| 500 | 501 | 375 | 25.1% | +| 1000 | 1002 | 755 | 24.6% | +| 2000 | 2001 | 1486 | **25.7%** | + +#### 结果三:综合样例(手写冗余) + +| | 静态指令 | +|--|----------| +| 优化前 | 13 | +| 优化后 | **7(-46%)** | + +假交换 `mv a0,a1; mv a1,a0` **原样保留**(正确)。 + +#### 结论 + +| 场景 | 效果 | +|------|------| +| 小 DSL 基准 | -0.43%(验证集成) | +| 合成高冗余 | ~ **-25%** | +| 综合样例 | **-46%**,且不误删假交换 | + +完整报告:[`benchmark_reports/peephole_compare.md`](../../benchmark_reports/peephole_compare.md) + +--- + +## 8. 技术债与已知限制 + +| 项 | 说明 | 优先级 | +|----|------|--------| +| 解析器重复 | 本模块自建 `AsmLine`/`_parse_asm`,与 `_asm_parser.py` 未统一 | 中 | +| 立即数溢出 | ✅ 已修复:`addi+addi` 仅在结果 ∈ [-2048,2047] 时合并 | — | +| x0/zero 别名 | beq 规则接受 x0/zero;其他规则字符串比较 | 中 | +| 规则顺序敏感 | 贪心 + 规则列表顺序影响结果 | 低 | +| 假交换删除 | ✅ 已移除:`mv x,y; mv y,x → 删除` 不健全 | — | +| mv 链活跃性 | Rule 4 无活跃性分析,中间寄存器存活时可能不健全 | 中 | +| `--list-rules` CLI | 需传 dummy input 才能列出规则(argparse 设计问题) | 低 | + +--- + +## 9. 扩展路线图 + +### 9.1 建议新增规则 + +| 规则 | 模式 | 替换 | 难度 | +|------|------|------|------| +| addi-zero / nop / mv-self | — | ✅ 已实现(Rule 5–8) | — | +| li-zero | `li rd, 0` | 保留或 `mv rd, x0` | 低 | +| 连续 mv 链 | `mv a,b; mv b,c` | `mv a,c` | 中 | + +### 9.2 集成增强 + +- [ ] 优化后自动跑 TinyFive 验证语义 +- [ ] 与 `--count-instr` 联动报告节省的静态指令数 +- [ ] 迁移至共享 `_asm_parser.ParsedAsmLine` + +--- + +## 10. 相关文件索引 + +| 文件 | 关系 | +|------|------| +| `scratchv/backend/asm_peephole.py` | 主实现(8 条默认规则;假交换删除已移除) | +| `tests/test_asm_peephole.py` | 单元测试(含正确性/新规则) | +| `tests/test_asm_peephole_integration.py` | 集成测试 | +| `tests/test_asm_peephole_stress.py` | 压力测试 | +| `tests/test_asm_peephole_blackbox.py` | 黑盒/CLI 测试 | +| `tests/fixtures/asm_peephole/` | 黑盒样例汇编 | +| `benchmarks/bench_asm_peephole.py` | 性能基准 | +| `benchmarks/compare_peephole.py` | 前后对比脚本 | +| `benchmark_reports/peephole_compare.md` | 对比报告 | +| `scratchv/compiler.py` | `_run_asm_passes()` 集成 | +| `scratchv/main.py` | `--peephole-asm` CLI 开关 | +| `scratchv/optimizer/peephole.py` | IR 层同名模块(勿混淆) | + +--- + +## 11. 审查清单(收尾核对) + +- [x] 新规则有对应 pytest 用例 +- [x] `addi+addi` 立即数 12 位溢出检查 +- [x] 不动点迭代安全上限(≤50 轮) +- [x] `report()` / `total_matches` 可用 +- [x] CLI `--report` 可用 +- [x] 与 IR peephole 文档区分清晰 +- [x] 四类测试通过(83/83) +- [x] 假交换删除规则已移除,并由测试锁定 +- [x] 设计文档 + AI 开发文档就绪 + +--- + +## 12. 课题收尾总结(2026-08-01) + +### 一句话 + +汇编层窥孔优化已可用:**正确性优先**(假交换不删、溢出不合并、标签不丢),规则与测试齐全;小基准收益有限,冗余密集时约省 25% 静态指令。 + +### 已交付 + +| 项 | 内容 | +|----|------| +| 算法 | 解析 → 滑动窗口匹配 → 替换 → 不动点(≤50 轮) | +| 规则 | **8 条**默认规则;**移除**假交换删除 | +| 正确性 | simm12 溢出检查;中窗标签拒绝融合;删除时保留标签 | +| 测试 | **83 PASSED**(含语义等价与 CLI) | +| 集成 | `--peephole-asm` / `CompilerDriver.peephole_asm` | +| 文档 | 教程 / 设计文档 / AI 指南 / 对比报告 | +| 效果 | DSL -0.43%;合成 ~-25%;综合样例 -46% | + +### 刻意延期 + +- `x0`/`zero` 全量别名规范化 +- 与 `_asm_parser.py` 统一解析 +- CNN/standalone 大汇编再对比 +- mv 链完整活跃性分析 + +### 一键复验 + +```bash +source .venv/bin/activate +python -m pytest tests/test_asm_peephole*.py -q +python benchmarks/compare_peephole.py --markdown benchmark_reports/peephole_compare.md +``` + +> **课题 13 收尾完成。** 维护入口:[archive/topic13_asm_peephole_guide.md](archive/topic13_asm_peephole_guide.md) diff --git "a/docs/topics/13-\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250-\350\265\233\351\201\223A-\345\274\200\345\217\221\346\226\207\346\241\243.md" "b/docs/topics/13-\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250-\350\265\233\351\201\223A-\345\274\200\345\217\221\346\226\207\346\241\243.md" new file mode 100644 index 0000000..ea2789b --- /dev/null +++ "b/docs/topics/13-\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250-\350\265\233\351\201\223A-\345\274\200\345\217\221\346\226\207\346\241\243.md" @@ -0,0 +1,192 @@ +# ScratchV 课题13 赛道 A:Parser 复用与健壮性加固 — 开发文档 + +> **文档版本**:v1.0 +> **创建日期**:2026-08-04 +> **作者**:孟子旭(@zinoe-1) +> **关联**:[PR #39](https://github.com/ScratchV-Compiler/ScratchV/pull/39);设计文档见同目录 `13-窥孔优化器-赛道A-设计文档.md` +> **涉及模块**:`scratchv/backend/`、`tests/test_asm_peephole*.py`、`docs/topics/` + +--- + +## 1. 功能概述与目标 + +### 1.1 背景与动机 + +- **现状问题**:PR #39 的窥孔实现内嵌重复汇编 parser;AI review 指出 mv-swap 检查与 `x0`/`zero` 别名等技术债。 +- **应用场景**:保证 `--peephole-asm` 在畸形输入下不崩溃;与 beautifier 等 pass 共享解析语义,降低后续赛道 B/C/D 的集成成本。 + +### 1.2 功能描述 + +- **一句话定义**:让窥孔优化器复用 `_asm_parser`,并修掉 PR #39 review 中的关键健壮性问题。 +- **核心价值**:单一 parser、更少崩溃面、别名正确匹配,为后续 CI / Benchmark / 深度优化打底。 + +### 1.3 目标与非目标 + +| 类型 | 内容 | +|------|------| +| ✅ 包含范围 | Parser 复用、IndexError 防护、零寄存器别名、文档、回归测试、fixture 小清理 | +| ❌ 不包含范围 | 全链路 simulator CI、compare_peephole、CFG/liveness | + +--- + +## 2. 设计与规格说明 + +### 2.1 用户视角(外部接口) + +公开 API 不变: + +```python +from scratchv.backend.asm_peephole import AsmPeepholeOptimizer + +opt = AsmPeepholeOptimizer() +text, n = opt.optimize(asm) +print(opt.report()) +``` + +CLI 不变: + +```bash +python -m scratchv.backend.asm_peephole input.s -o out.s --report +python -m scratchv model.onnx -o out.s --peephole-asm --count-instr +``` + +兼容别名(供测试使用): + +```python +from scratchv.backend.asm_peephole import AsmLine, _parse_line, _parse_asm, _lines_to_asm +# AsmLine is ParsedAsmLine +``` + +### 2.2 内部设计 + +1. `asm_text` → `_parse_asm` → `list[ParsedAsmLine]` +2. 固定点滑动窗口匹配 `_match_rule` +3. `_apply_replacement` → `_lines_to_asm` + +关键辅助: + +- `_canon_reg` / `_regs_equal` / `_is_zero_reg` +- `_count_opcodes` 使用 `is_directive` + +### 2.3 模块间交互 + +- **上游**:`compiler._run_asm_passes` 在开启 `peephole_asm` 时调用。 +- **下游**:美化 / const-merge 等仍接收文本;不改变其输入契约。 +- **共享依赖**:`_asm_parser.ParsedAsmLine`。 + +--- + +## 3. 模块修改与实现步骤 + +### 3.1 文件清单 + +| 文件路径 | 修改类型 | 修改内容概述 | +|----------|----------|--------------| +| `scratchv/backend/asm_peephole.py` | 修改 | 复用 parser;别名;IndexError 防护 | +| `tests/test_asm_peephole.py` | 修改 | 新增赛道 A 回归用例 | +| `tests/fixtures/asm_peephole/*.s` | 修改 | `.globl` + 意图注释 | +| `docs/topics/13-窥孔优化器-赛道A-*.md` | 新增 | 设计 / 开发文档 | +| `topic13/README.md` | 修改 | 索引赛道 A 文档 | + +### 3.2 分步实现计划 + +| 步骤 | 任务 | 预期产出 | 验证 | +|------|------|----------|------| +| 1 | 基于 PR #39 tip 建分支 | `feat/topic13-peephole-parser-reuse` | `git log -1` | +| 2 | 替换本地 parser | 无 `_LINE_RE` | grep | +| 3 | 别名 + IndexError | `_regs_equal` / `len>=2` | 单元测试 | +| 4 | 文档 | 设计+开发 md | 人工 review | +| 5 | 全量 peephole 测试 | 全绿 | pytest | +| 6 | 提 PR | GitHub PR | 审阅 | + +### 3.3 边界条件 + +- [x] 畸形 `mv`(操作数不足)不崩溃 +- [x] `x0`/`zero` 混用 beq / 约束 +- [x] directive 不计入指令节省统计 +- [ ](已知限制)不做 `li rd,0` ↔ `mv rd,x0` 等价折叠 + +--- + +## 4. 测试与验证方案 + +### 4.1 单元测试 + +文件:`tests/test_asm_peephole.py` + +新增场景: + +1. 共享 parser:directive 不计入 `_count_opcodes` +2. 短操作数 mv:无异常 +3. `addi zero, x0, 0` 可消除 + +### 4.2 回归命令 + +```powershell +.\.venv\Scripts\Activate.ps1 +pytest tests/test_asm_peephole.py tests/test_asm_peephole_blackbox.py ` + tests/test_asm_peephole_integration.py tests/test_asm_peephole_stress.py -v +``` + +### 4.3 验收标准(Definition of Done) + +- [ ] 上述 pytest 全绿 +- [ ] `asm_peephole.py` 不再包含独立 `_LINE_RE` 实现 +- [ ] 设计文档 + 开发文档已提交,作者姓名经人工确认 +- [ ] PR 描述写明依赖 / 包含 PR #39 基线 + +--- + +## 5. 风险评估与依赖 + +| 风险项 | 影响 | 缓解 | +|--------|------|------| +| PR #39 未合入导致与 main 冲突 | 中 | 分支基于 `pull/39/head`;PR 说明合并顺序 | +| 共享 `lines_to_asm` 格式细微差异 | 低 | 黑盒 / 集成测试覆盖 | + +- **外部依赖**:无新增第三方库 +- **兼容性**:公开 `AsmPeepholeOptimizer` API 不变 + +--- + +## 6. 开发进度跟踪 + +| 阶段 | 计划完成日期 | 状态 | +|------|--------------|------| +| 设计评审(本文档) | 2026-08-04 | ✅ | +| 编码实现 | 2026-08-04 | ⏳ | +| 自测与调试 | 2026-08-04 | ⏳ | +| 代码审查(PR) | 2026-08-04 | ⬜ | +| 合并主分支 | TBD | ⬜ | + +--- + +## 7. 附录 + +### 7.1 参考资料 + +- [PR #39](https://github.com/ScratchV-Compiler/ScratchV/pull/39) +- 设计文档模板 / 开发文档模板(课程提供) +- [`_asm_parser.py`](../../scratchv/backend/_asm_parser.py) + +### 7.2 PR 标题建议 + +``` +feat(peephole): reuse shared _asm_parser and harden match guards (topic13 track A) +``` + +### 7.3 PR 正文草稿 + +```markdown +## Summary +- Reuse `scratchv/backend/_asm_parser.py` inside `asm_peephole` (remove duplicate parser) +- Harden `redundant mv elimination` swap check (operand length guards) +- Normalize `x0`/`zero` aliases in register constraint matching +- Add track-A design/dev docs + regression tests + +## Baseline +Based on PR #39 (`docs/topic13-peephole`). Please merge #39 first or review this as a stacked follow-up. + +## Test plan +- [x] pytest tests/test_asm_peephole*.py +``` diff --git "a/docs/topics/13-\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250-\350\265\233\351\201\223A-\350\256\276\350\256\241\346\226\207\346\241\243.md" "b/docs/topics/13-\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250-\350\265\233\351\201\223A-\350\256\276\350\256\241\346\226\207\346\241\243.md" new file mode 100644 index 0000000..562b578 --- /dev/null +++ "b/docs/topics/13-\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250-\350\265\233\351\201\223A-\350\256\276\350\256\241\346\226\207\346\241\243.md" @@ -0,0 +1,154 @@ +# ScratchV 课题13 赛道 A:窥孔 Parser 复用与健壮性加固 — 技术设计文档 + +> 文档版本:v1.0 +> 编写日期:2026-08-04 +> 作者:孟子旭(@zinoe-1) +> 涉及模块:`scratchv/backend/asm_peephole.py`、`scratchv/backend/_asm_parser.py` +> 功能范围:复用共享汇编解析器、删除重复 parser、修复 PR #39 review 中的健壮性问题 +> 关联:基于 [PR #39](https://github.com/ScratchV-Compiler/ScratchV/pull/39) 后续完善;导师建议赛道 A + +--- + +## 一、功能介绍 + +### 1.1 功能概述 + +PR #39 已为汇编层窥孔优化器打下基础(8 条默认规则、测试与文档)。本工作在其上做**工程化加固**: + +1. **Parser 复用**:`asm_peephole.py` 不再维护独立的 `AsmLine` / `_LINE_RE` / `_parse_*`,改为调用共享模块 `scratchv/backend/_asm_parser.py`。 +2. **健壮性**:修复 review 指出的 mv-swap 检查潜在 `IndexError`;在规则匹配中统一 `x0` / `zero` 别名。 +3. **文档与测试**:补充设计/开发文档,并为上述改动增加回归测试。 + +### 1.2 设计目标 + +- **单一真相来源**:后端所有汇编 pass 共用一套行解析语义(操作数括号感知、directive 标记)。 +- **向后兼容**:对外仍导出 `AsmLine`、`_parse_line`、`_parse_asm`、`_lines_to_asm`(薄封装),现有测试尽量零改动。 +- **安全优先**:短操作数 / 畸形行不得崩溃;零寄存器别名不得漏匹配或误匹配。 +- **范围可控**:本次不做 CFG / liveness / 深度优化(留给赛道 D)。 + +### 1.3 目标与非目标 + +| 类型 | 内容 | +|------|------| +| ✅ 包含 | 复用 `_asm_parser`;删重复 parser;IndexError 防护;`x0`/`zero` 规范化;赛道 A 文档;回归测试 | +| ❌ 不包含 | CI 全链路仿真对比(赛道 B);`compare_peephole.py`(赛道 C);CFG / liveness / coalescing(赛道 D) | + +--- + +## 二、设计与规格 + +### 2.1 现状问题 + +| 问题 | 影响 | +|------|------| +| `asm_peephole` 内嵌独立 parser | 与 beautifier / const-merge 行为漂移;导师明确要求复用 `_asm_parser` | +| mv-swap 排除仅检查 `window[1].operands` 非空 | 仅 1 个操作数时访问不安全(review 🔴) | +| 寄存器约束用裸字符串比较 | `addi x0, zero, 0` 等别名组合可能漏优化 | + +### 2.2 架构决策 + +``` +asm_text + │ + ▼ +_asm_parser.parse_asm ──► list[ParsedAsmLine] (= AsmLine 别名) + │ + ▼ +AsmPeepholeOptimizer.optimize(滑动窗口 + 固定点) + │ + ▼ +_asm_parser.lines_to_asm ──► optimized asm +``` + +**`PeepholeRule.register_constraints` 索引约定**(澄清 PR #39 review): + +- 三元组 `(dst_instr, src_instr, src_op)` 均为**匹配窗口内** 0-based 下标。 +- 语义:`window[dst_instr].operands[0] == window[src_instr].operands[src_op]`(经 `_canon_reg`)。 +- 示例:`(0, 1, 1)` → 第 0 条指令的 rd 必须等于第 1 条指令的第 1 个源操作数。 + +### 2.3 寄存器别名 + +| 输入 | 规范名 | +|------|--------| +| `x0`、`zero` | `x0` | + +用于:寄存器约束比较、beq 零零判定、addi-zero 的 rd/rs 相等判定、mv-swap 形状判定。 + +**刻意不做**:把 `li rd, 0` 与 `mv rd, x0` 视为同一模式(那是规则层面的扩展,超出本次范围;文档中记为已知限制)。 + +### 2.4 标签安全(与 PR #39 对齐) + +- 窗口内第 2 条及以后若带 `label` → **拒绝匹配**(避免吞掉跳转目标)。 +- 窗口首条若带 `label` → **允许匹配**;替换结果第一条继承该 label;删除规则时保留裸 label。 + +--- + +## 三、测试设计 + +### 用例 1:共享 parser 指令计数 + +- 输入含 `.text` directive + 真指令。 +- 预期:`_count_opcodes` 不计 directive(依赖 `is_directive`)。 + +### 用例 2:mv 短操作数不崩溃 + +- 输入:`mv t0`(畸形,仅 1 操作数)后接另一行。 +- 预期:优化器不抛 `IndexError`,结果可返回。 + +### 用例 3:`x0`/`zero` 混用 beq + +- 已有:`beq zero, x0, L` → `j`。 +- 补充:约束路径上 `addi zero, x0, 0` 可被 self-elimination 删除。 + +### 用例 4:回归 + +- `pytest tests/test_asm_peephole*.py` 全绿。 + +--- + +## 四、修改模块与实现步骤 + +### 4.1 涉及文件 + +| 文件 | 修改类型 | 概述 | +|------|----------|------| +| `scratchv/backend/asm_peephole.py` | 修改 | 删本地 parser;包装 `_asm_parser`;别名与 IndexError 修复 | +| `tests/test_asm_peephole.py` | 新增用例 | parser 复用 / 别名 / 短操作数 | +| `tests/fixtures/asm_peephole/*.s` | 小改 | `.globl main`、注释 | +| `docs/topics/13-窥孔优化器-赛道A-设计文档.md` | 新增 | 本文档 | +| `docs/topics/13-窥孔优化器-赛道A-开发文档.md` | 新增 | 实现与验收 | +| `topic13/README.md` | 修改 | 链到赛道 A 文档 | + +### 4.2 实现步骤 + +1. 引入 `ParsedAsmLine as AsmLine` 与共享 parse/format。 +2. 删除 `_LINE_RE`、本地 `_split_operands` 实现体。 +3. `_count_opcodes` 改为 `not al.is_directive`。 +4. `_regs_equal` / `_is_zero_reg` 接入约束与特殊规则。 +5. mv-swap 分支要求 `len(operands) >= 2`。 +6. 补测试并跑全量 peephole 测试。 + +--- + +## 五、风险评估 + +| 风险 | 程度 | 缓解 | +|------|------|------| +| 共享 parser 与旧 parser 细微差异导致测试失败 | 中 | 薄封装保留 `strip()`;跑全套 peephole 测试 | +| directive 表示变化(去点号) | 低 | 使用 `is_directive` 而非 `startswith('.')` | +| 与未合并的 PR #39 冲突 | 中 | **本 PR 基线即为 PR #39 tip**;合并顺序:先 #39 再本 PR,或本 PR 直接含 #39 变更 | + +### 基线说明 + +本工作基于 `refs/pull/39/head`(`d62acdf`)开发。向 `ScratchV-Compiler/ScratchV:main` 提 PR 时,若 #39 尚未合并,审阅者需知本分支已包含 #39 的窥孔基建;若 #39 已合并,则 rebase 到最新 `main` 即可。 + +--- + +## 六、附录 + +### 参考资料 + +- [PR #39](https://github.com/ScratchV-Compiler/ScratchV/pull/39) 及 AI review 评论 +- 导师群消息:完善评论点 + 复用 `_asm_parser.py` +- [`scratchv/backend/_asm_parser.py`](../../scratchv/backend/_asm_parser.py) +- 课题13 设计文档(PR #39):[`13-窥孔优化器-设计文档.md`](./13-窥孔优化器-设计文档.md) diff --git "a/docs/topics/13-\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250.md" "b/docs/topics/13-\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250.md" index 50d5fca..5ce0673 100644 --- "a/docs/topics/13-\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250.md" +++ "b/docs/topics/13-\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250.md" @@ -1,7 +1,17 @@ # 课题13:窥孔优化器 -> **难度**:低 | **类型**:项目实战 | **源文件**:`scratchv/backend/asm_peephole.py` | **行数**:~400 -> **状态**:✅ 已完成 +> **难度**:低 | **类型**:项目实战 | **源文件**:`scratchv/backend/asm_peephole.py` +> **状态**:✅ 课题收尾完成(2026-08-01,**83/83** 测试,8 条默认规则) + +**文档导航**: + +| 文档 | 读者 | 链接 | +|------|------|------| +| **完成目录** | 总入口(课题 13 + 全部路径) | [`topic13/README.md`](../../topic13/README.md) | +| 本页 | 新手入门、动手练习 | 当前文件 | +| 设计文档 | 负责人、审查者 | [13-窥孔优化器-设计文档.md](13-窥孔优化器-设计文档.md) | +| 开发文档(AI) | Agent / 维护者 | [archive/topic13_asm_peephole_guide.md](archive/topic13_asm_peephole_guide.md) | +| 前后对比报告 | 效果数据 | [`benchmark_reports/peephole_compare.md`](../../benchmark_reports/peephole_compare.md) | --- @@ -49,15 +59,21 @@ PeepholeRule( ) ``` -#### 2. 5 条默认规则 +#### 2. 默认规则(8 条) | 规则 | 匹配 | 替换 | 效果 | |------|------|------|------| -| **addi+addi fusion** | `addi x,a,N; addi x,x,M` | `addi x,a,N+M` | 两条合并为一条 | -| **redundant mv swap** | `mv x,y; mv y,x` | 删除 | 无意义的交换 | +| **addi+addi fusion** | `addi x,a,N; addi x,x,M` | `addi x,a,N+M` | 两条合并(和须 ∈ [-2048,2047]) | | **li+addi fusion** | `li x,N; addi x,x,M` | `li x,N+M` | 常量折叠 | | **beq zero-zero to j** | `beq x0,x0,label` | `j label` | 无条件跳转简化 | -| **redundant mv elimination** | `mv a,b; ... mv c,a` | `mv c,b` | 跳过中间寄存器 | +| **redundant mv elimination** | `mv a,b; mv c,a` | `mv c,b` | 跳过中间寄存器(中间寄存器若仍存活则不安全) | +| **addi-zero self** | `addi x,x,0` | 删除 | 加零无操作 | +| **addi-zero to mv** | `addi y,x,0` | `mv y,x` | 加零改成搬运 | +| **nop elimination** | `nop` | 删除 | 空指令 | +| **mv-self** | `mv x,x` | 删除 | 自己搬自己 | + +> ⚠️ **已移除的错误规则**:`mv x,y; mv y,x → 删除` +> 这两条**不是**真交换,也不是空操作(结果是两个寄存器都变成原来的 `y`)。删除会改变语义,故默认规则中不再包含。 #### 3. 固定点迭代 @@ -79,7 +95,8 @@ PeepholeRule( 1. 定义3~5个窥孔优化规则,例如: - `addi x1, x1, 1; addi x1, x1, 1` → `addi x1, x1, 2` - - `mv x1, x2; mv x2, x1` → 删除两条(如果可交换) + - ~~`mv x1, x2; mv x2, x1` → 删除~~(**错误**:非真交换,禁止删除) + - `mv a, b; mv c, a` → `mv c, b`(中间寄存器不再使用时) - `li x1, 0; addi x1, x1, 1` → `li x1, 1` - `beq x0, x0, label` → 无条件跳转`j label` 2. 编写汇编解析器,将每行解析为对象(标签、操作码、操作数列表)。 @@ -226,6 +243,8 @@ def _match_rule(rule, window): | 坑 | 说明 | |----|------| +| **假交换删除** | `mv x,y; mv y,x` **不是**空操作(结果两寄存器都等于原 `y`)。已从默认规则移除 | +| **mv 链活跃性** | `mv a,b; mv c,a → mv c,b` 在中间 `a` 后续仍使用时可能不健全 | | **寄存器别名** | `x0` 和 `zero` 是同一个寄存器,但字符串比较不相等。需要做规范化 | | **规则顺序** | 规则的应用顺序影响最终结果——可能规则 A 的替换产物正好被规则 B 匹配 | | **常量折叠的溢出** | `addi+addi fusion` 中两个立即数相加可能超出 12 位有符号范围(-2048~2047),需要检查 | @@ -249,9 +268,29 @@ def _match_rule(rule, window): - **W4**:实现模式匹配:滑动窗口大小等于规则长度,比较操作码和操作数(支持通配符如任意寄存器)。 - **W5**:实现替换:删除匹配窗口,插入新指令列表,重新扫描。 - **W6**:实现第一条规则:`addi x1,x1,1; addi x1,x1,1` → `addi x1,x1,2`。测试。 -- **W7**:实现规则:`mv x1, x2; mv x2, x1` → 删除两条(简单情况)。 +- **W7**:分析为何 `mv x,y; mv y,x` **不能**删除;实现 `mv a,b; mv c,a` → `mv c,b`(并写语义测试)。 - **W8**:实现规则:`li x1, 0; addi x1, x1, 1` → `li x1, 1`。 - **W9**:实现规则:`beq x0, x0, label` → `j label`(需要处理标签)。 - **W10**:增加优化报告,打印匹配次数、节省的指令数。 -- **W11**:集成到编译器后端(在汇编生成后自动调用),添加`--peephole`开关。 -- **W12**:测试10个以上汇编文件,用模拟器验证正确性,撰写文档。 +- **W11**:集成到编译器后端(在汇编生成后自动调用),添加`--peephole-asm`开关。 +- **W12**:测试10个以上汇编文件,用模拟器/语义检查验证正确性,撰写文档。 + +--- + +## 课题总结(收尾) + +| 项 | 现状 | +|----|------| +| 默认规则 | **8 条**(假交换删除已移除) | +| 测试 | **83 PASSED**(单元/集成/压力/黑盒/语义) | +| DSL 基准对比 | 231→230(-0.43%) | +| 合成高冗余对比 | 约 **-25%** 静态指令 | +| 综合样例 | 13→7(-46%),假交换保留 | + +**一句话**:汇编层局部优化已可用;小程序收益有限,冗余多时收益明显;正确性优先于盲目删指令。 + +```bash +source .venv/bin/activate +python -m pytest tests/test_asm_peephole*.py -q +python benchmarks/compare_peephole.py --markdown benchmark_reports/peephole_compare.md +``` diff --git a/docs/topics/archive/topic13_asm_peephole_guide.md b/docs/topics/archive/topic13_asm_peephole_guide.md new file mode 100644 index 0000000..787241f --- /dev/null +++ b/docs/topics/archive/topic13_asm_peephole_guide.md @@ -0,0 +1,316 @@ +# Assembly Peephole Optimizer — Agent / Developer Guide + +> **Audience**: AI coding agents, maintainers extending `asm_peephole.py` +> **Source**: `scratchv/backend/asm_peephole.py` +> **Topic index**: [../../../topic13/README.md](../../../topic13/README.md) +> **Human design doc**: [../13-窥孔优化器-设计文档.md](../13-窥孔优化器-设计文档.md) +> **Compare report**: [../../../benchmark_reports/peephole_compare.md](../../../benchmark_reports/peephole_compare.md) +> **Last verified**: 2026-08-01 — **83/83** tests pass; **8** default rules +> **Do NOT re-add**: `redundant mv pair elimination` (`mv x,y; mv y,x → delete`) — unsound + +--- + +## Module Map (symbol → responsibility) + +Line numbers drift; prefer symbols over exact lines. + +| Symbol | Role | +|--------|------| +| `AsmLine` | Parsed assembly line dataclass | +| `PeepholeRule` | Rule definition (pattern + replacement + constraints) | +| `_LINE_RE` / `_parse_line` | Single-line parse | +| `_parse_asm` / `_lines_to_asm` | Full text ↔ `list[AsmLine]` | +| `_fits_simm12` / `_parse_imm` | Signed 12-bit check; base-aware int parse (`0x`, `0b`) | +| `_default_rules` | **8** built-in rules (no fake-swap delete) | +| `_operand_matches` | Wildcard operand binding | +| `_match_rule` | Match rule against window (+ label / imm / beq / mv-chain guards) | +| `AsmPeepholeOptimizer` | Fixed-point sliding-window optimizer | +| `_apply_replacement` | Rule-specific template expansion + label preserve | +| `main` | CLI (`python -m scratchv.backend.asm_peephole`) | + +**Do NOT confuse with**: `scratchv/optimizer/peephole.py` (`IRPeepholeOptimizer`) — different layer, different API. + +--- + +## Public API Contract + +### Import + +```python +from scratchv.backend.asm_peephole import ( + AsmPeepholeOptimizer, + PeepholeRule, + AsmLine, +) +# also re-exported: from scratchv.backend import AsmPeepholeOptimizer +``` + +### Primary usage + +```python +opt = AsmPeepholeOptimizer() # default 8 rules +opt = AsmPeepholeOptimizer(rules=[...]) # custom rules + +optimized_text, num_changes = opt.optimize(asm_text) # -> tuple[str, int] +report_str = opt.report() # after optimize() +counts = opt.total_matches # dict[rule_name, int] +``` + +### Invariants + +1. `optimize()` is **pure** on input text (no file I/O; counters live on the instance). +2. `num_changes` = number of rule applications (not necessarily lines saved). +3. Empty `replacement=[]` means **delete** matched window; if the first line had a label, emit a bare `label:` line. +4. Fixed-point loop: max **50** iterations; stops when a full pass makes no match. +5. Application is **left-to-right greedy**; first matching rule in `self.rules` wins. +6. Mid-window labels (`window[i].label` for `i > 0`) **refuse** the match. +7. Immediate folding uses `_parse_imm` (not bare `int()`); never emit `(0x10+0x20)` style garbage. + +--- + +## Compiler Integration + +```python +# scratchv/compiler.py — _run_asm_passes() +if self.config.peephole_asm: + from scratchv.backend.asm_peephole import AsmPeepholeOptimizer + opt = AsmPeepholeOptimizer() + asm_text, changes = opt.optimize(asm_text) +``` + +CLI flag: `scratchv ... --peephole-asm` (`scratchv/main.py`). + +Pass order in `_run_asm_passes`: **peephole → const_merge → schedule → beautify**. + +--- + +## PeepholeRule Schema + +```python +PeepholeRule( + name: str, # MUST be unique; used in _apply_replacement branches + pattern: list[str], # opcodes, lowercase; len = window size + replacement: list[str], # templates; [] = delete + register_constraints: list[tuple[int, int, int]], # (dst_instr, src_instr, src_op_idx) +) +``` + +### register_constraints semantics + +Each tuple `(dst_idx, src_instr_idx, src_op_idx)` requires: + +``` +window[dst_idx].operands[0] == window[src_instr_idx].operands[src_op_idx] +``` + +### Replacement templates + +| Placeholder | Set by rule | +|-------------|-------------| +| `{rd}`, `{rs1}`, `{imm_sum}` | addi+addi fusion | +| `{rd}`, `{imm_sum}` | li+addi fusion | +| `{label}` | beq zero-zero to jump | +| `{rd1}`, `{rs2}` | redundant mv elimination | +| `{rd}`, `{rs}` | addi-zero to mv | + +If `_parse_imm` fails after a match (should be rare), `_apply_replacement` returns the original window unchanged. + +--- + +## Default Rules (quick reference) + +| # | name | pattern | replacement | constraints / notes | +|---|------|---------|-------------|---------------------| +| 1 | `addi+addi fusion` | addi, addi | addi {rd} {rs1} {imm_sum} | (0,1,0),(0,1,1); sum ∈ [-2048,2047] | +| 2 | `li+addi fusion` | li, addi | li {rd} {imm_sum} | (0,1,0),(0,1,1); imms parseable | +| 3 | `beq zero-zero to jump` | beq | j {label} | ops[0,1] ∈ {x0, zero} | +| 4 | `redundant mv elimination` | mv, mv | mv {rd1} {rs2} | (0,1,1); **excludes** swap shape; mid may stay live | +| 5 | `addi-zero self elimination` | addi | [] | (0,0,1); imm==0 | +| 6 | `addi-zero to mv` | addi | mv {rd} {rs} | imm==0; rd≠rs | +| 7 | `nop elimination` | nop | [] | — | +| 8 | `mv-self elimination` | mv | [] | (0,0,1) | + +**Removed (unsound)**: `redundant mv pair elimination` (`mv x,y; mv y,x → delete`). +Both regs become original `y` — not a no-op. Tests assert the pair is **preserved**. + +Helpers: `_fits_simm12`, `_parse_imm`. Mid-window labels refuse matching. + +--- + +## How to Add a New Rule + +### Step 1 — Define rule in `_default_rules()` or pass a custom list + +```python +PeepholeRule( + name="li-zero to mv", + pattern=["li"], + replacement=["mv {rd} x0"], + register_constraints=[], +) +``` + +### Step 2 — Add special logic if needed + +If replacement needs computed values, add a branch in `_apply_replacement()`: + +```python +elif rule.name == "my new rule": + derived["foo"] = ... +``` + +**Prefer**: keep logic generic; only add branches when template substitution is insufficient. + +### Step 3 — Extra checks in `_match_rule()` when opcode-only match is insufficient + +Example: beq rule checks `x0`/`zero` after generic matching; addi fusion checks simm12. + +### Step 4 — Test + +```python +# tests/test_asm_peephole.py +def test_my_rule(): + opt = AsmPeepholeOptimizer(rules=[my_rule]) + result, changes = opt.optimize(" ...\n") + assert changes >= 1 +``` + +Prefer also adding a `TestSemanticEquivalence` case when the rewrite changes values. + +### Step 5 — Update docs + +- Human: `docs/topics/13-窥孔优化器-设计文档.md` §6 rule table +- This file: Default Rules table +- Status index: `topic13/README.md` rule count / test count if they change + +--- + +## Verification Commands + +```bash +cd /home/z/ScratchV-main # or your repo root +source .venv/bin/activate + +# All Topic-13 peephole tests (83 cases) +python -m pytest tests/test_asm_peephole*.py -v --tb=short + +# By category +python -m pytest tests/ -m unit -k peephole -v +python -m pytest tests/ -m integration -k peephole -v +python -m pytest tests/ -m stress -k peephole -v +python -m pytest tests/ -m blackbox -k peephole -v + +# Benchmark / before-after (optional) +python benchmarks/bench_asm_peephole.py +python benchmarks/compare_peephole.py --markdown benchmark_reports/peephole_compare.md +``` + +**Expected**: **83 passed**; `--list-rules` must **not** print `redundant mv pair elimination`. + +### Test file map + +| Marker | File | Role | +|--------|------|------| +| `unit` | `tests/test_asm_peephole.py` | parse, match, 8 rules, semantic equivalence, labels/hex | +| `integration` | `tests/test_asm_peephole_integration.py` | CompilerDriver / passes / flag off | +| `stress` | `tests/test_asm_peephole_stress.py` | scale / hex batch / labels under load | +| `blackbox` | `tests/test_asm_peephole_blackbox.py` | CLI + fixtures; swap-delete absent | + +Fixtures: `tests/fixtures/asm_peephole/*.s` +(incl. `input_hex_fusion.s`, `input_addi_overflow.s`, `input_nop_mv_self.s`, `input_mv_chain.s`) + +--- + +## Pitfalls for Agents + +| Issue | Detail | Fix | +|-------|--------|-----| +| IR vs ASM peephole | Two modules, same concept | Edit `backend/asm_peephole.py` for Topic 13 | +| Re-adding fake swap delete | Looks clever, breaks semantics | Never restore `redundant mv pair elimination` | +| Duplicate parser | `_asm_parser.py` unused here | Do not unify unless task asks | +| Rule name typos | `_apply_replacement` branches on `rule.name` | Match strings exactly | +| addi imm overflow | RV addi imm is simm12 | Refuse fusion when sum out of range | +| hex immediates | Must use `_parse_imm` | Do not fold with bare `int()` | +| Labels | Mid-window label = refuse; lead label = preserve | Cover with tests | +| mv-chain liveness | Rule 4 best-effort without liveness | Document; see `test_mv_chain_unsound_when_mid_live` | +| x0 vs zero | Only beq special-cases aliases | Normalize if adding more zero checks | +| Infinite loop | Bad rules can oscillate | `max_iterations=50` + terminate tests | +| Greedy order | Rule A may block Rule B | Reorder or merge; document dependency | +| `_split_operands` | Defined but unused | Dead code; ignore or cleanup PR | +| CLI `--list-rules` | Still needs positional `input` | Known argparse limitation | + +--- + +## Test Coverage Matrix (key cases) + +| Test | Asserts | +|------|---------| +| `test_addi_addi_fusion` | imm merged to 8 | +| `test_li_addi_fusion` | li+addi → single li | +| `test_beq_zero_jump` / `test_beq_zero_alias` | beq x0/zero → j | +| `test_mv_swap_pair_not_deleted` | swap-shaped pair **preserved** (`changes == 0`) | +| `test_redundant_mv_elimination` | mv chain shortened | +| `test_addi_fusion_hex_immediates` | `0x10+0x20` → `48`, no `(` garbage | +| `test_label_preserved_on_fusion` / `_on_nop_deletion` | labels survive | +| `test_mid_label_blocks_fusion` | labeled 2nd insn blocks pair | +| `test_mv_chain_unsound_when_mid_live` | documents Rule 4 liveness gap | +| `TestSemanticEquivalence.*` | register-state checks for sound rewrites | +| `test_cli_list_rules` | removed rule name absent from stdout | + +When adding rules: input asm → `optimize()` → assert tokens + `changes`, and prefer a semantic check. + +--- + +## Dependencies + +``` +asm_peephole.py + ├── stdlib: re, sys, dataclasses, typing + └── (no scratchv internal imports) + +Consumers: + ├── scratchv/compiler.py (_run_asm_passes) + ├── scratchv/backend/__init__.py (re-export AsmPeepholeOptimizer) + ├── tests/test_asm_peephole*.py + ├── benchmarks/bench_asm_peephole.py + └── benchmarks/compare_peephole.py +``` + +--- + +## Modification Checklist (agents) + +Before marking task complete: + +- [ ] `python -m pytest tests/test_asm_peephole*.py -q` — all green (expect 83 unless count intentionally changed) +- [ ] New rule has ≥1 dedicated test (+ semantic case if values change) +- [ ] `rule.name` unique among `_default_rules()` +- [ ] If immediates folded: use `_parse_imm` + simm12 check where needed +- [ ] Labels: mid-window refuse / lead preserve / delete keeps bare label +- [ ] Did **not** re-introduce fake-swap delete +- [ ] `report()` / `total_matches` reflect the new rule +- [ ] Updated design doc §6 + this guide + `topic13/README.md` counts if rules/tests changed +- [ ] Did not break IR peephole (`optimizer/peephole.py`) + +--- + +## Example: End-to-end agent task + +**Task**: Add `li rd, 0` → `mv rd, x0` (optional micro-canonicalization). + +1. Add to `_default_rules()` with a unique `name`. +2. In `_match_rule`, require `_parse_imm(ops[1]) == 0`. +3. In `_apply_replacement`, set `{rd}` from `ops[0]`. +4. Tests: positive rewrite + semantic equivalence + ensure `li rd, 1` untouched. +5. Run `pytest tests/test_asm_peephole*.py -q`; bump README/design counts if defaults changed. + +--- + +## See Also + +- [../../../topic13/README.md](../../../topic13/README.md) — Topic 13 completion index +- [../13-窥孔优化器.md](../13-窥孔优化器.md) — beginner tutorial +- [../13-窥孔优化器-设计文档.md](../13-窥孔优化器-设计文档.md) — human design spec +- [../05-汇编代码美化器.md](../05-汇编代码美化器.md) — downstream asm pass +- [../14-常量加载合并.md](../14-常量加载合并.md) — adjacent pass in pipeline +- `scratchv/backend/_asm_parser.py` — shared parser (future unification target) diff --git "a/docs/topics/archive/\350\257\276\351\242\23013\357\274\232\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250.md" "b/docs/topics/archive/\350\257\276\351\242\23013\357\274\232\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250.md" index dd11dd7..93d05ae 100644 --- "a/docs/topics/archive/\350\257\276\351\242\23013\357\274\232\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250.md" +++ "b/docs/topics/archive/\350\257\276\351\242\23013\357\274\232\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250.md" @@ -1,24 +1,30 @@ ## 课题13:窥孔优化器 -**难度**:低 +**难度**:低 +**状态**:✅ 收尾完成(2026-08-01)— 8 条默认规则,83/83 测试通过 +**完成目录(总入口)**:[../../../topic13/README.md](../../../topic13/README.md) +**主文档**:[../13-窥孔优化器.md](../13-窥孔优化器.md) · [../13-窥孔优化器-设计文档.md](../13-窥孔优化器-设计文档.md) +**对比报告**:[../../../benchmark_reports/peephole_compare.md](../../../benchmark_reports/peephole_compare.md) **概述**:在生成的RISC-V汇编代码上,匹配并替换低效指令序列(如连续加法、冗余移动等),减少指令数。 **详细任务**: -1. 定义3~5个窥孔优化规则,例如: - - `addi x1, x1, 1; addi x1, x1, 1` → `addi x1, x1, 2` - - `mv x1, x2; mv x2, x1` → 删除两条(如果可交换) +1. 定义若干窥孔优化规则,例如: + - `addi x1, x1, 1; addi x1, x1, 1` → `addi x1, x1, 2`(和须落入 12 位有符号立即数) - `li x1, 0; addi x1, x1, 1` → `li x1, 1` - - `beq x0, x0, label` → 无条件跳转`j label` + - `beq x0, x0, label` → 无条件跳转 `j label` + - `mv a, b; mv c, a` → `mv c, b`(中间寄存器不再使用时;best-effort) + - `addi x, x, 0` / `nop` / `mv x, x` → 删除 + - ~~`mv x1, x2; mv x2, x1` → 删除~~(**禁止**:非真交换,删除会改语义) 2. 编写汇编解析器,将每行解析为对象(标签、操作码、操作数列表)。 -3. 实现滑动窗口扫描,匹配规则并替换,迭代直到不动点。 +3. 实现滑动窗口扫描,匹配规则并替换,迭代直到不动点;窗口中间带标签时不得融合。 4. 输出优化后的汇编,并统计匹配次数和节省的指令数。 -5. 集成到编译器后端,添加`--peephole`开关。 +5. 集成到编译器后端,添加`--peephole-asm`开关。 **交付产物**: -- 独立的`peephole.py`脚本或集成模块 -- 测试汇编文件及优化前后对比 -- 文档:规则列表、使用方法 +- 独立的`peephole`模块或集成模块 +- 测试汇编文件及优化前后对比(含语义/正确性用例) +- 文档:规则列表、使用方法、已知不健全点 **12周每周目标**: - **W1**:学习窥孔优化原理,收集常见低效汇编模式。 @@ -27,9 +33,9 @@ - **W4**:实现模式匹配:滑动窗口大小等于规则长度,比较操作码和操作数(支持通配符如任意寄存器)。 - **W5**:实现替换:删除匹配窗口,插入新指令列表,重新扫描。 - **W6**:实现第一条规则:`addi x1,x1,1; addi x1,x1,1` → `addi x1,x1,2`。测试。 -- **W7**:实现规则:`mv x1, x2; mv x2, x1` → 删除两条(简单情况)。 +- **W7**:分析为何 `mv x,y; mv y,x` **不能**删除;实现 `mv a,b; mv c,a` → `mv c,b` 并写语义测试。 - **W8**:实现规则:`li x1, 0; addi x1, x1, 1` → `li x1, 1`。 - **W9**:实现规则:`beq x0, x0, label` → `j label`(需要处理标签)。 - **W10**:增加优化报告,打印匹配次数、节省的指令数。 -- **W11**:集成到编译器后端(在汇编生成后自动调用),添加`--peephole`开关。 -- **W12**:测试10个以上汇编文件,用模拟器验证正确性,撰写文档。 \ No newline at end of file +- **W11**:集成到编译器后端(在汇编生成后自动调用),添加`--peephole-asm`开关。 +- **W12**:测试10个以上汇编文件,用模拟器/语义检查验证正确性,撰写文档。 diff --git a/scratchv/backend/asm_peephole.py b/scratchv/backend/asm_peephole.py index 5aa35c7..ff402ae 100644 --- a/scratchv/backend/asm_peephole.py +++ b/scratchv/backend/asm_peephole.py @@ -3,6 +3,10 @@ Applies peephole optimization rules to RISC-V assembly text using sliding-window pattern matching with register wildcards. +Parsing is delegated to the shared backend parser +(``scratchv.backend._asm_parser``) so beautifier / peephole / const-merge +share one AsmLine representation. + Usage:: from scratchv.backend.asm_peephole import AsmPeepholeOptimizer @@ -12,40 +16,24 @@ from __future__ import annotations -import re import sys from dataclasses import dataclass, field from typing import Optional +from scratchv.backend._asm_parser import ( + ParsedAsmLine, + lines_to_asm as _shared_lines_to_asm, + parse_asm as _shared_parse_asm, + parse_line as _shared_parse_line, +) + # --------------------------------------------------------------------------- # Data types # --------------------------------------------------------------------------- - -@dataclass -class AsmLine: - """Represents one parsed line of assembly.""" - raw: str - label: Optional[str] = None - opcode: Optional[str] = None - operands: list[str] = field(default_factory=list) - comment: Optional[str] = None - lineno: int = 0 - - def __str__(self) -> str: - if self.label is not None and self.opcode is None: - return self.raw - 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}") - return "".join(parts) +# Backward-compatible alias: peephole historically used ``AsmLine``. +AsmLine = ParsedAsmLine @dataclass @@ -64,10 +52,11 @@ class PeepholeRule: List of opcode strings for replacement. Use ``{0}``, ``{1}`` etc. to reference registers captured from the pattern. register_constraints: - Optional list of index-pair tuples ``(i, j)`` specifying that the - destination register of instruction i must equal some operand of - instruction j for the rule to fire. - Format: ``(dst_index, src_instruction_index, src_operand_index)``. + Optional list of tuples ``(dst_instr, src_instr, src_op)`` requiring + ``window[dst_instr].operands[0] == window[src_instr].operands[src_op]``. + Indices are **0-based positions inside the match window** (not global + line numbers). Example: ``(0, 1, 1)`` means "rd of window[0] must + equal operand 1 of window[1]". """ name: str pattern: list[str] @@ -81,110 +70,79 @@ def __repr__(self) -> str: # --------------------------------------------------------------------------- -# Parsing +# Parsing — thin wrappers over shared ``_asm_parser`` # --------------------------------------------------------------------------- -_LINE_RE = re.compile( - r'^\s*' - r'(?P