diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5d957ca..8a649b6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -84,6 +84,16 @@ jobs:
--junit-xml=benchmark_reports/test_results.xml \
--ignore=tests/test_simulator.py
+ - name: Run assembly-beautifier regressions
+ run: |
+ python3.12 -m pytest \
+ tests/test_parser_for_beautifier.py \
+ tests/test_asm_beautifier_comments.py \
+ tests/test_asm_beautifier_formatting.py \
+ tests/test_asm_beautifier_integration.py \
+ tests/test_asm_beautifier_blackbox.py \
+ -v --tb=short
+
- name: Run constant-merge regressions
run: |
python3.12 -m pytest \
@@ -208,7 +218,19 @@ jobs:
python3.12 scripts/gen_minimal_cnn.py 2>&1
fi
python3.12 scratchv/standalone/onnx_to_riscv_standalone.py \
- "$MODEL" -o /tmp/cnn_riscv.bin --estimate --report --const-merge
+ "$MODEL" -o /tmp/cnn_riscv.bin \
+ --asm benchmark_reports/cnn_scratchv.s \
+ --estimate --report --const-merge
+
+ # ── 3.3.1 汇编美化器:真实 CNN 输出 + Actions Summary ───────────
+ - name: CNN assembly beautifier benchmark
+ run: |
+ python3.12 -m benchmarks.bench_asm_beautifier \
+ --input benchmark_reports/cnn_scratchv.s \
+ --output-asm benchmark_reports/cnn_pretty.s \
+ --markdown benchmark_reports/asm_beautifier_summary.md \
+ --json benchmark_reports/asm_beautifier_summary.json \
+ --repeats 20
# ── 3.4 LLVM vs ScratchV + TinyFive + Dashboard ──────────────────
- name: LLVM vs ScratchV comparison
@@ -324,6 +346,10 @@ jobs:
cat benchmark_reports/github_summary.md >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
+ if [ -f benchmark_reports/asm_beautifier_summary.md ]; then
+ cat benchmark_reports/asm_beautifier_summary.md >> "$GITHUB_STEP_SUMMARY"
+ fi
+ echo "" >> "$GITHUB_STEP_SUMMARY"
echo "### LLVM vs ScratchV" >> $GITHUB_STEP_SUMMARY
if [ -f benchmark_reports/llvm_vs_scratchv.md ]; then
tail -20 benchmark_reports/llvm_vs_scratchv.md >> $GITHUB_STEP_SUMMARY
diff --git a/benchmarks/bench_asm_beautifier.py b/benchmarks/bench_asm_beautifier.py
old mode 100644
new mode 100755
index 86642a5..9cbc151
--- a/benchmarks/bench_asm_beautifier.py
+++ b/benchmarks/bench_asm_beautifier.py
@@ -1,5 +1,5 @@
# flake8: noqa
-"""Benchmark for RISC-V Assembly Beautifier.
+"""Stress benchmark for RISC-V Assembly Beautifier.
Measures beautification time and output size for assembly files
of varying complexity.
@@ -7,14 +7,34 @@
Usage:
python benchmarks/bench_asm_beautifier.py
python benchmarks/bench_asm_beautifier.py --repeats 100
+ python -m benchmarks.bench_asm_beautifier \
+ --input benchmark_reports/cnn_scratchv.s \
+ --output-asm benchmark_reports/cnn_pretty.s \
+ --markdown benchmark_reports/asm_beautifier_summary.md \
+ --json benchmark_reports/asm_beautifier_summary.json
"""
from __future__ import annotations
import argparse
-import time
+import json
+import random
import statistics
-from typing import Optional
+import sys
+import time
+
+from pathlib import Path
+from typing import Any
+
+
+PROJECT_ROOT = Path(__file__).resolve().parents[1]
+
+# 仅当以脚本方式直接运行时,才把项目根加入导入路径,避免该模块被当作
+# 普通模块导入时污染全局 sys.path 并改变其它导入顺序。推荐改用
+# `python -m benchmarks.bench_asm_beautifier` 运行,此时无需修改 sys.path。
+if __name__ == "__main__":
+ if str(PROJECT_ROOT) not in sys.path:
+ sys.path.insert(0, str(PROJECT_ROOT))
from scratchv.backend.asm_beautifier import beautify_asm
@@ -23,8 +43,7 @@
# Test programs of varying complexity
# ---------------------------------------------------------------------------
-_SIMPLE_ASM = """
-.text
+_SIMPLE_ASM = """.text
main:
addi sp, sp, -16
sw ra, 12(sp)
@@ -34,8 +53,7 @@
ret
"""
-_MODERATE_ASM = """
-.text
+_MODERATE_ASM = """.text
main:
addi sp, sp, -32
sw ra, 28(sp)
@@ -60,40 +78,95 @@
ret
"""
-_LARGE_ASM = _MODERATE_ASM * 20 # Duplicate for size
+_LARGE_ASM = _MODERATE_ASM * 20
def _gen_random_asm(num_instrs: int, seed: int = 42) -> str:
- """Generate synthetic RISC-V assembly of a given size."""
- import random
- random.seed(seed)
-
- ops = ["add", "sub", "addi", "lw", "sw", "beq", "j", "li", "mv", "mul",
- "xor", "or", "and", "slli", "srli", "slt", "div", "jal", "ret"]
- regs = ["t0", "t1", "t2", "t3", "t4", "t5", "t6",
- "a0", "a1", "a2", "a3", "s0", "s1", "s2"]
-
- lines = [".text", "synthetic_func:"]
- for i in range(num_instrs):
- op = random.choice(ops)
- if op in ("j", "jal"):
- lines.append(f" {op} label_{i % 10}")
- elif op in ("beq", "bne", "blt", "bge"):
- lines.append(f" {op} {random.choice(regs)}, {random.choice(regs)}, label_{i % 10}")
- elif op == "li":
- lines.append(f" {op} {random.choice(regs)}, {random.randint(0, 4096)}")
- elif op == "mv":
- lines.append(f" {op} {random.choice(regs)}, {random.choice(regs)}")
- elif op in ("lw", "sw"):
- lines.append(f" {op} {random.choice(regs)}, {random.randint(0, 16)}(sp)")
- elif op in ("addi", "slli", "srli"):
- lines.append(f" {op} {random.choice(regs)}, {random.choice(regs)}, {random.randint(0, 31)}")
+ """Generate deterministic synthetic RISC-V assembly of a given size."""
+
+ generator = random.Random(seed)
+ arithmetic_ops = [
+ "add",
+ "sub",
+ "mul",
+ "xor",
+ "or",
+ "and",
+ "sll",
+ "slt",
+ "div",
+ ]
+ immediate_ops = ["addi", "slli", "srli"]
+ registers = [
+ "t0",
+ "t1",
+ "t2",
+ "t3",
+ "t4",
+ "t5",
+ "t6",
+ "a0",
+ "a1",
+ "a2",
+ "a3",
+ "s0",
+ "s1",
+ "s2",
+ ]
+ ops = [
+ *arithmetic_ops,
+ *immediate_ops,
+ "lw",
+ "sw",
+ "beq",
+ "j",
+ "li",
+ "mv",
+ "ret",
+ ]
+
+ lines = [".text", ".globl synthetic_func", "synthetic_func:"]
+ for index in range(num_instrs):
+ opcode = generator.choice(ops)
+ label = f"label_{index % 10}"
+ if opcode == "j":
+ lines.append(f" {opcode} {label}")
+ elif opcode == "beq":
+ lines.append(
+ f" {opcode} {generator.choice(registers)}, "
+ f"{generator.choice(registers)}, {label}"
+ )
+ elif opcode == "li":
+ lines.append(
+ f" {opcode} {generator.choice(registers)}, "
+ f"{generator.randint(0, 4096)}"
+ )
+ elif opcode == "mv":
+ lines.append(
+ f" {opcode} {generator.choice(registers)}, "
+ f"{generator.choice(registers)}"
+ )
+ elif opcode in {"lw", "sw"}:
+ lines.append(
+ f" {opcode} {generator.choice(registers)}, "
+ f"{generator.randint(0, 16)}(sp)"
+ )
+ elif opcode in immediate_ops:
+ lines.append(
+ f" {opcode} {generator.choice(registers)}, "
+ f"{generator.choice(registers)}, {generator.randint(0, 31)}"
+ )
+ elif opcode == "ret":
+ lines.append(" ret")
else:
- lines.append(f" {op} {random.choice(regs)}, {random.choice(regs)}, {random.choice(regs)}")
- # Occasionally add labels
- if i % 15 == 0:
- lines.append(f"label_{i % 10}:")
- lines.append(" ret\n")
+ lines.append(
+ f" {opcode} {generator.choice(registers)}, "
+ f"{generator.choice(registers)}, {generator.choice(registers)}"
+ )
+
+ if index % 15 == 0:
+ lines.append(f"label_{index % 10}:")
+ lines.append(" ret")
return "\n".join(lines)
@@ -101,23 +174,41 @@ def _gen_random_asm(num_instrs: int, seed: int = 42) -> str:
# Benchmarks
# ---------------------------------------------------------------------------
-def bench_beautify(asm_text: str, align: bool = True,
- add_comments: bool = True,
- repeats: int = 50) -> dict:
- """Run beautify benchmark and return timing statistics."""
- times = []
+def bench_beautify(
+ asm_text: str,
+ align: bool = True,
+ add_comments: bool = True,
+ abi_register_names: bool = False,
+ repeats: int = 50,
+) -> dict[str, Any]:
+ """Run beautification repeatedly and return timing statistics."""
+
+ if repeats < 1:
+ raise ValueError("repeats must be at least 1")
+
+ times: list[float] = []
output_size = 0
+ expected_result: str | None = None
for _ in range(repeats):
- t0 = time.perf_counter()
- result = beautify_asm(asm_text, align=align, add_comments=add_comments)
- t1 = time.perf_counter()
- times.append(t1 - t0)
+ start = time.perf_counter()
+ result = beautify_asm(
+ asm_text,
+ align=align,
+ add_comments=add_comments,
+ abi_register_names=abi_register_names,
+ )
+ times.append(time.perf_counter() - start)
output_size = len(result)
+ if expected_result is None:
+ expected_result = result
+ elif result != expected_result:
+ raise RuntimeError("beautifier output changed between runs")
return {
- "input_lines": asm_text.count("\n"),
+ "input_lines": len(asm_text.splitlines()),
"input_chars": len(asm_text),
+ "output_lines": len((expected_result or "").splitlines()),
"output_chars": output_size,
"ratio": output_size / max(len(asm_text), 1),
"repeats": repeats,
@@ -125,12 +216,14 @@ def bench_beautify(asm_text: str, align: bool = True,
"max_s": max(times),
"mean_s": statistics.mean(times),
"median_s": statistics.median(times),
- "stdev_s": statistics.stdev(times) if len(times) > 1 else 0,
+ "stdev_s": statistics.stdev(times) if len(times) > 1 else 0.0,
+ "deterministic_output": True,
}
-def run_all_benchmarks(repeats: int = 50):
- """Run all beautifier benchmarks."""
+def run_all_benchmarks(repeats: int = 50) -> None:
+ """Run fixed-size and synthetic beautifier benchmarks."""
+
benchmarks = {
"simple": _SIMPLE_ASM,
"moderate": _MODERATE_ASM,
@@ -142,38 +235,248 @@ def run_all_benchmarks(repeats: int = 50):
print("=" * 80)
print("RISC-V Assembly Beautifier Benchmark")
print("=" * 80)
- print(f"{'Test':<20} {'Input':>8} {'Output':>8} {'Ratio':>7} {'Mean(ms)':>10} {'Stdev(ms)':>10}")
+ print(
+ f"{'Test':<20} {'Input':>8} {'Output':>8} {'Ratio':>7} "
+ f"{'Mean(ms)':>10} {'Stdev(ms)':>10}"
+ )
print("-" * 80)
- for name, asm in benchmarks.items():
- stats = bench_beautify(asm, repeats=repeats)
+ for name, asm_text in benchmarks.items():
+ stats = bench_beautify(asm_text, repeats=repeats)
print(
f"{name:<20} {stats['input_chars']:>8} "
f"{stats['output_chars']:>8} {stats['ratio']:>6.2f}x "
- f"{stats['mean_s'] * 1000:>10.3f} {stats['stdev_s'] * 1000:>10.3f}"
+ f"{stats['mean_s'] * 1000:>10.3f} "
+ f"{stats['stdev_s'] * 1000:>10.3f}"
)
- # Compare with and without features
print()
print("Feature Impact (on synthetic_1k):")
print("-" * 60)
- asm = _gen_random_asm(1000)
+ synthetic_1k = benchmarks["synthetic_1k"]
for align in (True, False):
for comments in (True, False):
- stats = bench_beautify(asm, align=align, add_comments=comments,
- repeats=repeats)
+ stats = bench_beautify(
+ synthetic_1k,
+ align=align,
+ add_comments=comments,
+ repeats=repeats,
+ )
label = f"align={align}, comments={comments}"
- print(f" {label:<30} {stats['mean_s'] * 1000:>8.3f} ms "
- f"output: {stats['output_chars']} chars")
+ print(
+ f" {label:<30} {stats['mean_s'] * 1000:>8.3f} ms "
+ f"output: {stats['output_chars']} chars"
+ )
+
+
+def _write_text(path: Path, text: str) -> None:
+ """Write a UTF-8 report, creating its parent directory when needed."""
+
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(text, encoding="utf-8")
+
+def _generate_markdown_summary(
+ input_path: Path,
+ output_path: Path | None,
+ output_text: str,
+ stats: dict[str, Any],
+ preview_lines: int,
+) -> str:
+ """Build the compact Markdown shown in the GitHub Actions job summary."""
-def main():
+ output_name = (
+ output_path.name if output_path is not None else "beautified output"
+ )
+ deterministic = "Yes" if stats["deterministic_output"] else "No"
+ lines = [
+ f"### Assembly Beautifier Benchmark — {input_path.name}",
+ "",
+ f"Input: `{input_path.name}` · Output: `{output_name}`",
+ "",
+ "| Metric | Result |",
+ "|---|---:|",
+ f"| Input lines | {stats['input_lines']:,} |",
+ f"| Output lines | {stats['output_lines']:,} |",
+ f"| Input size | {stats['input_chars']:,} chars |",
+ f"| Output size | {stats['output_chars']:,} chars |",
+ f"| Size ratio | {stats['ratio']:.2f}x |",
+ f"| Mean time | {stats['mean_s'] * 1000:.3f} ms |",
+ f"| Median time | {stats['median_s'] * 1000:.3f} ms |",
+ f"| Maximum time | {stats['max_s'] * 1000:.3f} ms |",
+ f"| Standard deviation | {stats['stdev_s'] * 1000:.3f} ms |",
+ f"| Repeats | {stats['repeats']} |",
+ f"| Deterministic output | {deterministic} |",
+ ]
+
+ if preview_lines:
+ preview = "\n".join(output_text.splitlines()[:preview_lines])
+ # Keep an unexpected fence in an assembly comment from breaking Markdown.
+ preview = preview.replace("```", "`` `")
+ lines.extend(
+ [
+ "",
+ "",
+ (
+ f"Preview of {output_name} "
+ f"(first {preview_lines} lines)
"
+ ),
+ "",
+ "```asm",
+ preview,
+ "```",
+ "",
+ " ",
+ ]
+ )
+
+ return "\n".join(lines) + "\n"
+
+
+def run_file_benchmark(
+ input_path: Path,
+ *,
+ output_path: Path | None = None,
+ markdown_path: Path | None = None,
+ json_path: Path | None = None,
+ repeats: int = 50,
+ preview_lines: int = 40,
+ align: bool = True,
+ add_comments: bool = True,
+ abi_register_names: bool = False,
+) -> dict[str, Any]:
+ """Benchmark one real assembly file and optionally write CI artifacts."""
+
+ asm_text = input_path.read_text(encoding="utf-8")
+ output_text = beautify_asm(
+ asm_text,
+ align=align,
+ add_comments=add_comments,
+ abi_register_names=abi_register_names,
+ )
+ stats = bench_beautify(
+ asm_text,
+ align=align,
+ add_comments=add_comments,
+ abi_register_names=abi_register_names,
+ repeats=repeats,
+ )
+ report: dict[str, Any] = {
+ "benchmark": "assembly_beautifier",
+ "input_path": str(input_path),
+ "output_path": str(output_path) if output_path is not None else None,
+ "align": align,
+ "add_comments": add_comments,
+ "abi_register_names": abi_register_names,
+ **stats,
+ }
+
+ if output_path is not None:
+ _write_text(output_path, output_text)
+ if markdown_path is not None:
+ _write_text(
+ markdown_path,
+ _generate_markdown_summary(
+ input_path,
+ output_path,
+ output_text,
+ stats,
+ preview_lines,
+ ),
+ )
+ if json_path is not None:
+ _write_text(
+ json_path,
+ json.dumps(report, indent=2, ensure_ascii=False) + "\n",
+ )
+
+ print(
+ f"Beautified {stats['input_lines']} lines in "
+ f"{stats['mean_s'] * 1000:.3f} ms average "
+ f"({stats['ratio']:.2f}x output size, {repeats} repeats)"
+ )
+ return report
+
+
+def _positive_int(value: str) -> int:
+ repeats = int(value)
+ if repeats < 1:
+ raise argparse.ArgumentTypeError("repeats must be at least 1")
+ return repeats
+
+
+def main() -> None:
parser = argparse.ArgumentParser(description="Beautifier Benchmark")
- parser.add_argument("--repeats", type=int, default=50,
- help="Number of repeat measurements")
+ parser.add_argument(
+ "--repeats",
+ type=_positive_int,
+ default=50,
+ help="number of repeat measurements",
+ )
+ parser.add_argument(
+ "--input",
+ type=Path,
+ help="benchmark a real assembly file instead of synthetic cases",
+ )
+ parser.add_argument(
+ "--output-asm",
+ type=Path,
+ help="write the beautified assembly to this path",
+ )
+ parser.add_argument(
+ "--markdown",
+ type=Path,
+ help="write a GitHub Actions Markdown summary",
+ )
+ parser.add_argument(
+ "--json",
+ type=Path,
+ help="write machine-readable benchmark results",
+ )
+ parser.add_argument(
+ "--preview-lines",
+ type=_positive_int,
+ default=40,
+ help="number of beautified assembly lines to preview in Markdown",
+ )
+ parser.add_argument(
+ "--no-align",
+ action="store_true",
+ help="disable field alignment",
+ )
+ parser.add_argument(
+ "--no-comments",
+ action="store_true",
+ help="disable generated semantic comments",
+ )
+ parser.add_argument(
+ "--abi-register-names",
+ action="store_true",
+ help="use ABI register names in generated comments",
+ )
args = parser.parse_args()
- run_all_benchmarks(args.repeats)
+
+ file_outputs = (args.output_asm, args.markdown, args.json)
+ if args.input is None:
+ if any(path is not None for path in file_outputs):
+ parser.error("--output-asm, --markdown and --json require --input")
+ run_all_benchmarks(args.repeats)
+ return
+
+ if not args.input.is_file():
+ parser.error(f"input assembly file does not exist: {args.input}")
+ run_file_benchmark(
+ args.input,
+ output_path=args.output_asm,
+ markdown_path=args.markdown,
+ json_path=args.json,
+ repeats=args.repeats,
+ preview_lines=args.preview_lines,
+ align=not args.no_align,
+ add_comments=not args.no_comments,
+ abi_register_names=args.abi_register_names,
+ )
if __name__ == "__main__":
diff --git "a/docs/topic5\346\261\207\347\274\226\344\273\243\347\240\201\347\276\216\345\214\226\345\231\250\345\274\200\345\217\221\346\226\207\346\241\243.md" "b/docs/topic5\346\261\207\347\274\226\344\273\243\347\240\201\347\276\216\345\214\226\345\231\250\345\274\200\345\217\221\346\226\207\346\241\243.md"
new file mode 100755
index 0000000..f0f0189
--- /dev/null
+++ "b/docs/topic5\346\261\207\347\274\226\344\273\243\347\240\201\347\276\216\345\214\226\345\231\250\345\274\200\345\217\221\346\226\207\346\241\243.md"
@@ -0,0 +1,337 @@
+# ScratchV RISC-V 汇编代码美化器开发文档
+
+> **文档版本**:v1.2
+> **创建日期**:2026-07-13
+> **更新日期**:2026-08-02
+> **涉及模块**:`scratchv/backend/asm_beautifier.py`、`scratchv/backend/asm_parser_for_beautifier.py`
+
+---
+
+## 1. 功能概述与目标
+
+### 1.1 背景与动机
+- **现状问题**:RISC-V 汇编代码通常难以直观表达程序行为,机器生成代码还可能存在字段位置不统一、标签与指令混排等问题,阅读门槛较高。
+- **应用场景**:编译器开发、调试和教学过程中需要阅读、理解汇编代码;使用美化器可提高阅读效率,降低理解门槛。
+
+### 1.2 功能描述
+- **一句话定义**:美化器在保持汇编语义不变的前提下,对字段进行列对齐、为已支持指令生成可读的语义注释,并插入段与函数分隔标题。
+- **核心价值**:提升汇编代码的可读性、调试效率和教学展示效果。
+
+### 1.3 目标与非目标
+| 类型 | 内容 |
+|------|------|
+| ✅ 包含范围 | 对 RISC-V 汇编进行安全解析、列对齐、语义注释和段/函数标记,并提供 Python API 与命令行工具。 |
+| ❌ 不包含范围 | 不改变汇编语义,也不承担汇编、链接、非法输入修复或未知指令语义推断。 |
+
+---
+
+## 2. 设计与规格说明
+
+### 2.1 用户视角(外部接口)
+
+- **输入语法基准**:
+
+BNF 表示:
+```
+asm_line ::= [label ":"] [opcode [operands]] [comment]
+label ::= standard_label | metadata_label
+standard_label ::= identifier
+metadata_label ::= "_op_/" metadata_path
+ | "_op_PPQ_Operation_" number "_" number
+metadata_path ::= metadata_char { metadata_char }
+metadata_char ::= any non-whitespace character except ":"
+opcode ::= instruction | pseudo_instruction | directive
+pseudo_instruction ::= "li" | "mv" | "call" | "ret" | ...
+operands ::= operand { top_level_comma operand }
+top_level_comma ::= "," (* only when parenthesis depth is zero *)
+comment ::= "#" text
+```
+
+实现时,解析器应提取标签、操作码、原始操作数字符串、操作数列表和注释;操作数按顶层逗号拆分,括号内的逗号不得作为操作数分隔符。字符串字面量外的 `#` 始终开始行尾注释,不受括号深度影响。`metadata_path` 可包含 `/`,但不得包含空白或 `:`。`_op_/...:` 和 `_op_PPQ_Operation__:` 作为算子元数据标签完整保留,不得识别为函数入口。标准指令、汇编器伪指令和汇编器指示必须分类处理。
+
+
+- **新增/修改的 API**:
+
+```python
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Literal, TypedDict
+
+ParseStatus = Literal[
+ "valid",
+ "metadata_label",
+ "incomplete_operands",
+ "unknown_opcode",
+ "malformed",
+]
+
+class FieldLengths(TypedDict):
+ label: int
+ opcode: int
+ operands_str: int
+ comment: int
+
+@dataclass
+class ParsedAsmLine:
+ raw: str
+ label: str | None
+ opcode: str | None
+ operands_str: str
+ operands: list[str]
+ comment: str | None
+ lineno: int
+ parse_status: ParseStatus
+ field_lengths: FieldLengths
+
+def beautify_asm(
+ asm_text: str,
+ align: bool = True,
+ add_comments: bool = True,
+ abi_register_names: bool = False,
+) -> str:
+ """返回美化后的 RISC-V 汇编文本。"""
+
+def parse_asm_line(
+ line: str,
+ lineno: int = 0,
+) -> ParsedAsmLine:
+ """解析单行汇编并返回结构化结果。"""
+
+def parse_asm(
+ asm_text: str,
+) -> list[ParsedAsmLine]:
+ """按原始顺序解析完整汇编文本,保留空行和行号。"""
+
+def beautify_file(
+ input_path: str | Path,
+ output_path: str | Path | None = None,
+ align: bool = True,
+ add_comments: bool = True,
+ abi_register_names: bool = False,
+ encoding: str = "utf-8",
+) -> str:
+ """读取并美化汇编;可选地原子写入目标文件,并返回结果。"""
+```
+
+| 参数 | 类型 | 默认值 | 行为 |
+|------|------|--------|------|
+| `asm_text` | `str` | 无 | 待处理的完整汇编文本。 |
+| `align` | `bool` | `True` | 是否对可安全格式化的行进行列对齐。 |
+| `add_comments` | `bool` | `True` | 是否为已登记指令添加自动语义注释。 |
+| `abi_register_names` | `bool` | `False` | 是否仅在自动注释中将 `x0` 至 `x31` 转为 ABI 别名。 |
+
+`beautify_asm()` 返回值始终为字符串,不得修改输入对象或执行文件写入。空输入和仅含空白的输入必须稳定返回字符串且不抛异常,具体换行形式由兼容性测试固定。
+
+`ParsedAsmLine`、`parse_asm_line()` 与 `parse_asm()` 由美化器专用模块 `scratchv/backend/asm_parser_for_beautifier.py` 提供。该模块需要保留原始操作数字符串、字符串字面量中的 `#` 与逗号、元数据标签、五种 `parse_status` 和字段长度;这些能力超出现有共享 `_asm_parser.py` 的契约,因此不得通过修改 `_asm_parser.py` 强行满足美化器需求,以免影响 peephole、const-merge、inst-counter、inst-scheduler 等既有调用方。`beautify_file()` 同步暴露 `align`、`add_comments`、`abi_register_names`,默认使用 UTF-8。指定 `output_path` 时先写入同目录临时文件,成功后原子替换目标;未指定时只返回字符串。`abi_register_names` 默认值为 `False`,且仅影响自动生成的语义注释。
+
+#### 独立命令行接口
+
+```bash
+python3 -m scratchv.backend.asm_beautifier input.s
+python3 -m scratchv.backend.asm_beautifier input.s -o output.s
+python3 -m scratchv.backend.asm_beautifier input.s --no-align
+python3 -m scratchv.backend.asm_beautifier input.s --no-comments
+python3 -m scratchv.backend.asm_beautifier input.s --abi-register-names
+```
+
+| 参数 | 是否必需 | 说明 |
+|------|----------|------|
+| `input` | 是 | 输入 `.s` 文件路径。 |
+| `-o` | 否 | 输出路径;未指定时写入标准输出。 |
+| `--no-align` | 否 | 关闭列宽扫描和列对齐。 |
+| `--no-comments` | 否 | 关闭自动语义注释,但保留用户原始注释。 |
+| `--abi-register-names` | 否 | 仅在自动注释中启用 ABI 寄存器别名;默认关闭。 |
+
+### 2.2 内部设计(核心逻辑)
+
+#### 数据结构变更
+
+不新增 AST 节点或 IR 指令,仅新增逐行解析结果,保存 `raw`、`label`、`opcode`、`operands_str`、`operands`、`comment`、从 1 开始的 `lineno`、`parse_status` 和 `field_lengths`。指令规格、语义注释模板及 ABI 寄存器映射使用模块级只读常量。
+
+#### 关键算法/流程
+
+```text
+输入文本
+ → 切分行尾注释并保留原始行
+ → 识别完整标签并分类普通标签/算子元数据标签
+ → 解析操作码及原始操作数字段
+ → 按顶层逗号拆分操作数(识别括号深度)
+ → 分类 parse_status
+ → 统计合法行各字段长度
+ → 第一遍收集段状态、.type/.globl/.global 声明、本地 call 目标、main 特例和安全列宽
+ → 第二遍插入段/函数标题并格式化有效行
+ → 合并原始注释与自动语义注释
+ → 输出文本
+```
+
+格式化器仅把括号深度为零的操作数分隔符规范化为 `, `(无前置空白、后置一个空格),括号内部格式和操作数词法内容保持不变。标签填充宽度按 `min(max_label, 30)` 计算,操作码填充宽度按 `min(max(max_opcode, 8), 12)` 计算,操作数填充宽度按 `min(max(max_operands, 15), 40)` 计算;因此操作码和操作数的最小宽度分别为 8 和 15。超长字段不截断、不填充,其后字段允许自然右移。
+
+#### 状态管理
+
+不新增全局可变状态。单次调用仅维护解析结果、段与函数信息、列宽和输出缓冲区,多次调用之间互不影响。
+
+函数识别仅针对本文件 `.text` 段内定义且不是 `metadata_label` 的标签:显式 `.type symbol, @function` 和 `.globl/.global symbol` 声明优先,其次识别本文件中直接 `call symbol` 的目标,并将 `.text` 段内的 `main` 作为程序入口特例。局部或控制流标签、仅作为分支目标的标签、数据段标签和无法确认用途的普通标签不生成函数标题。`align=False` 时仍插入函数标题,但保持原标签行布局。函数标题不是输出第一行时,写入前检查上一行:上一行非空则补一个空行,已经为空则不追加;重复美化不得叠加同名标题或前置空行。
+
+段标题与函数标题均具备幂等性:当输入已紧邻包含对应的 `# ... SECTION` 段标题或 `# --- Function: ... ---` 标题时,美化器不再重复插入,重复美化输入不会产生叠加的标题或额外的空行。因此同一份 `.s` 文件连续美化多次输出稳定不变。
+
+### 2.3 接口定义(模块间交互)
+
+- **上游依赖**:接收 ScratchV 汇编发射器或用户文件产生的 RISC-V 文本。美化器依赖最终文本、显式选项,以及专用模块 `scratchv/backend/asm_parser_for_beautifier.py` 提供的 `ParsedAsmLine`、`parse_asm_line()` 与 `parse_asm()`。
+- **解析器并存边界**:`scratchv/backend/_asm_parser.py` 保持现有接口和行为,继续供 peephole、const-merge、inst-counter、inst-scheduler 等汇编级 pass 使用;`asm_parser_for_beautifier.py` 只供 `asm_beautifier.py` 使用。两个模块不得互相导入,也不得让其他 pass 在本课题中迁移到专用解析器。
+- **重复逻辑控制**:`asm_beautifier.py` 本身不得再实现逐行解析正则或操作数拆分逻辑;所有美化器专用解析行为集中在 `asm_parser_for_beautifier.py`。两套解析器允许因契约不同而并存,但公共合法汇编子集应通过兼容性测试保持字段含义一致。
+- **下游影响**:输出仍是可供 RISC-V 汇编器读取的 `.s` 文本,也可交给指令统计、终端输出和人工审阅。新增标题必须全部使用 `#` 注释,不能形成伪指令。
+
+---
+
+## 3. 模块修改与实现步骤
+
+### 3.1 涉及的文件清单
+
+| 文件路径 | 修改类型 | 修改内容概述 |
+|----------|----------|--------------|
+| `scratchv/backend/asm_beautifier.py` | 重构 | 完善指令规格表、列对齐、注释生成、段/函数识别、文件 API 和 CLI。 |
+| `scratchv/backend/asm_parser_for_beautifier.py` | 新建 | 实现美化器专用安全解析器、原文字段保留、字符串感知、元数据标签识别和解析状态;不修改 `_asm_parser.py`。 |
+| `tests/test_parser_for_beautifier.py` | 新增 | 功能单元测试:验证 `parse_asm_line()` 的字段提取、字段长度、操作数拆分、字符串边界、元数据标签和状态分类。 |
+| `tests/test_asm_beautifier_comments.py` | 新增 | 功能单元测试:验证注释模板、伪指令和 ABI 别名。 |
+| `tests/test_asm_beautifier_formatting.py` | 新增 | 功能单元测试:验证列宽、操作数规范化、注释列、指示原样保留和换行。 |
+| `tests/test_asm_beautifier_integration.py` | 新增 | 功能集成测试:验证解析、格式化、异常处理、段/函数标记、文件 API 和完整程序。 |
+| `tests/test_asm_beautifier_blackbox.py` | 新增 | 黑盒测试:通过 CLI 子进程验证参数、标准流、文件输出和退出码。 |
+| `benchmarks/bench_asm_beautifier.py` | 修改 | 使用固定样例和合成大输入统计耗时、波动、输出大小及膨胀比例。 |
+
+### 3.2 分步实现计划
+
+每个模块均遵循“先编写至少一个对应测试并确认失败,再编写实现使其通过”的顺序,测试不单独作为实施步骤。
+
+| 步骤 | 任务描述 | 预期产出 | 验证方式 |
+|------|----------|----------|----------|
+| 1 | 先编写解析测试,再实现逐行解析、操作数拆分及 `parse_status` 分类。 | 能正确提取汇编字段,并安全处理未知、缺失和异常输入。 | 解析与状态分类测试通过。 |
+| 2 | 先编写格式化测试,再实现两遍列宽扫描、字段对齐和保守输出。 | 普通标签独占一行;状态正常的运行时指令与伪指令对齐,顶层操作数分隔符统一为 `, `;汇编器指示保持原样,三种异常行的注释前代码部分逐字保留,仅追加状态警告注释。 | 输入输出对比及格式化测试通过。 |
+| 3 | 先编写语义注释测试,再实现指令模板、伪指令、注释合并和 ABI 别名转换。 | 已支持指令生成正确注释;三种异常状态输出明确警告,且不生成或拼接指令模板结果。 | 指令语义、伪指令、异常警告及 ABI 测试通过。 |
+| 4 | 先编写结构识别测试,再实现段标题、函数入口、元数据标签和数据定义处理。 | 汇编段与函数结构清晰,算子元数据和数据内容保持安全。 | 段、函数、元数据及数据测试通过。 |
+| 5 | 先编写文件与 CLI 测试,再实现 `beautify_file()`、原子写入、命令行参数和错误处理。 | Python API 与命令行工具可稳定处理正常及异常文件。 | 文件、CLI 子进程及错误路径测试通过。 |
+| 6 | 先补充端到端验收用例,再进行模块联调、重构和性能检查。 | 完整实现、测试结果、基准数据及回归结论。 | 目标测试、全量测试和 benchmark 通过。 |
+
+### 3.3 异常处理与边界条件
+
+- [ ] 空输入和仅含空白的输入稳定返回字符串,不抛异常;具体换行形式由兼容性测试固定。
+- [ ] `28(sp)`、嵌套括号和负偏移不会被错误拆分。
+- [ ] `add a0`、`lw t0`、`beq a0,a1` 标记为 `incomplete_operands`,不崩溃、不补写原始操作数;输出注释为 `[warning: operand missing]`,不进入指令模板匹配。
+- [ ] `li,,a0,,3`、`sw ra,,28(sp)`、`main::`、未闭合字符串/括号和 `this is not asm` 等非汇编文本标记为 `malformed`;注释前的代码部分逐字保持不变,输出注释为 `[warning: malformed instruction]`。
+- [ ] 未知操作码标记为 `unknown_opcode`;注释前的代码部分逐字保持不变,输出注释为 `[warning: unknown opcode]`,不得使用具体指令模板或 fallback 生成语义说明。
+- [ ] `_op_/layer1.0/Conv_5:` 和 `_op_PPQ_Operation_6_29:` 标记为 `metadata_label`,完整保留且不生成函数标题;`_input_copy_done:` 仍是普通标签。
+- [ ] 已有算子说明注释的 `nop` 只保留原注释,不追加 `no operation`;无原注释的普通 `nop` 仍可生成该自动注释。
+- [ ] 数据定义行不生成运行时指令注释,超长字段不被截断。
+- [ ] 输入输出路径不存在、权限不足或编码错误时,CLI 向标准错误输出路径和原因,并以非零状态码退出。
+- [ ] 输出写入失败时不覆盖原文件,不残留被误认为成功结果的部分文件。
+
+三种异常状态采用统一的“保留指令字段、替换自动注释”策略:
+
+| `parse_status` | 输出注释 | 模板处理 |
+|----------------|----------|----------|
+| `incomplete_operands` | `[warning: operand missing]` | 跳过模板匹配,不用空字符串填充占位符 |
+| `unknown_opcode` | `[warning: unknown opcode]` | 跳过模板匹配和 fallback 推测 |
+| `malformed` | `[warning: malformed instruction]` | 跳过模板匹配,不解释局部片段 |
+
+状态判定先处理字段边界、括号或字符串异常并标记为 `malformed`;字段结构完整后,未知 opcode 优先于操作数数量检查。仅已登记 opcode 的操作数不足才标记为 `incomplete_operands`,因此 `custom_add a0` 必须输出 `[warning: unknown opcode]`。
+
+异常警告不受自动注释或 ABI 别名开关影响。开启列对齐且异常行没有用户注释时,以正常指令的统一注释列作为警告 `#` 的最小起始位置;异常代码已经达到或超过该位置时,在原代码后保留至少两个空格。若输入已有用户注释,必须保留其原始位置,再用 ` | ` 追加上述警告。若注释已经以当前状态对应的同类警告结尾,则保持原行,不得重复追加。任何异常行都不得追加指令模板匹配结果,标签、操作码、已有操作数及其空白组成的代码部分不得因生成警告而改变。例如:
+
+```asm
+L1: add a0 # [warning: operand missing]
+L2: custom_op x1,x2,x3 # [warning: unknown opcode]
+L3: li,,a0,,3 # [warning: malformed instruction]
+```
+
+---
+
+## 4. 测试与验证方案
+
+### 4.1 测试概述
+
+测试资产不拆分为独立汇编样例文件,按功能单元测试、功能集成测试、黑盒测试和压力测试组织:
+
+| 测试 | 描述 | 预期 |
+|------|------|------|
+| 功能单元:解析 | `tests/test_parser_for_beautifier.py` 直接测试字段提取、字符串/括号扫描、元数据标签、行号和 `parse_status`。 | 解析字段和五种状态正确。 |
+| 功能单元:注释 | `tests/test_asm_beautifier_comments.py` 直接测试模板填充、ABI 别名和注释合并规则。 | 指令语义准确,不生成未知或不完整模板。 |
+| 功能单元:格式化 | `tests/test_asm_beautifier_formatting.py` 测试列宽、顶层操作数规范化、注释列、指示原样保留和换行。 | 格式化规则独立且稳定。 |
+| 功能集成 | `tests/test_asm_beautifier_integration.py` 验证解析、格式化、异常警告、段/函数标记、文件 API 与完整程序输出。 | 各模块组合后保持语义、格式和幂等性。 |
+| 黑盒 | `tests/test_asm_beautifier_blackbox.py` 通过子进程运行 CLI,验证参数、标准流、文件输出和退出码。 | 用户可观察行为符合 CLI 契约。 |
+| 压力测试 | `benchmarks/bench_asm_beautifier.py` 使用固定和合成汇编输入记录耗时、波动和输出大小。 | 性能稳定,无无法解释的退化。 |
+
+### 4.2 压力测试基线
+
+基准脚本使用固定的 simple、moderate、large 汇编样例,以及确定性的 synthetic_1k 和 synthetic_5k 输入。每个样例重复美化并校验输出一致,记录输入/输出字符数、输出膨胀比例、最小值、最大值、均值、中位数和标准差;同时比较 `align` 与自动注释开关的功能开销。
+
+执行命令:
+
+```bash
+python benchmarks/bench_asm_beautifier.py --repeats 3
+```
+
+输出中的均值和标准差用于同一环境下的回归比较,不作为跨机器的固定性能阈值。
+
+### 4.3 验收标准(Definition of Done)
+
+- [ ] 新实现不再依赖单个宽松正则表达式完成整行解析。
+- [ ] `asm_beautifier.py` 只从 `asm_parser_for_beautifier.py` 导入解析能力,自身不保留第二份逐行解析或操作数拆分实现。
+- [ ] `_asm_parser.py` 及其 peephole、const-merge、inst-counter、inst-scheduler 等既有调用方的接口和行为不变,相关回归测试通过。
+- [ ] 五种 `parse_status` 均有直接单元测试和明确输出策略。
+- [ ] 测试概述表中的各类测试均达到预期。
+- [ ] 功能单元、功能集成、黑盒测试和压力测试均通过,且各类别职责清晰、无重复主断言。
+- [ ] `python3 -m pytest tests/ -v` 全部通过,不引入项目回归。
+- [ ] 元数据标签保持原样;三种异常行的注释前代码部分逐字保持不变,输出对应警告注释,不生成函数标题或指令模板结果。
+- [ ] 已有算子说明的 `nop` 不追加 `no operation`,无注释的普通 `nop` 行为保持不变。
+- [ ] 数据字符串、标签、操作数词法内容、括号内部格式和用户注释不被破坏;仅将顶层操作数分隔符统一为 `, `。
+- [ ] 开启 ABI 别名仅影响自动注释,默认输出保持向后兼容。
+- [ ] `beautify_file()` 同步暴露 `align`、`add_comments`、`abi_register_names`,CLI 使用设计规定的 `--abi-register-names` 正向开关。
+- [ ] 四类段标题映射、标题插入顺序和 60 个 `=` 分隔线与设计文档第 2.2 节一致;函数入口识别与标题插入位置与设计文档第 2.4 节一致。
+- [ ] 标签填充宽度为 `min(max_label, 30)`,操作码填充宽度为 `min(max(max_opcode, 8), 12)`,操作数填充宽度为 `min(max(max_operands, 15), 40)`;短字段按最小宽度对齐,超长字段保持原样且允许后续字段右移,不与常规字段对齐。
+- [ ] CLI 文件错误返回非零状态码,失败写入不会留下不完整目标文件。
+- [ ] 基准结果已记录且没有无法解释的明显时间或输出体积退化。
+- [ ] 公共 API、CLI 和相关用户文档已更新,代码包含必要类型标注和文档字符串。
+
+---
+
+## 5. 风险评估与依赖
+
+| 风险项 | 影响程度 | 缓解措施 |
+|--------|----------|----------|
+| 字符串、注释和操作数边界处理错误,导致合法数据被改写 | 高 | 使用状态扫描器并补充边界与异常输入测试。 |
+| 未知扩展指令被误分类并生成错误语义 | 高 | 使用显式指令规格表;未登记操作码保留标签、操作码和操作数内容,只输出 `[warning: unknown opcode]`,不提供 fallback 或模板注释。 |
+| 专用解析器与 `_asm_parser.py` 随时间产生无意漂移 | 中 | 明确调用边界;公共合法汇编子集增加字段兼容性测试;美化器特有差异在设计文档和测试名称中显式记录。 |
+| 普通跳转标签被误判为函数 | 中 | 结合段状态、符号指示和调用目标分析函数,不确定时不标记。 |
+| 算子元数据标签被拆分或误判为函数 | 高 | 在普通标签和操作码解析前识别元数据标签,并覆盖正确识别与误判场景。 |
+| 数据定义被当作运行时指令处理 | 高 | 独立维护汇编器指示和数据定义集合,并结合当前段状态禁用语义注释。 |
+| 不同指令的操作数角色映射错误 | 高 | 由 `InstructionSpec.operand_roles` 显式驱动,分别测试算术、访存、分支、跳转和伪指令。 |
+| 新 ABI 参数破坏已有位置参数调用或默认输出 | 中 | 新参数追加到签名末尾且默认 `False`,增加兼容性测试。 |
+| 全文件两遍扫描增加大输入内存和耗时 | 中 | 复用解析结果,复杂度保持 O(n);用 1000/5000 条输入监测耗时和输出膨胀。 |
+| 文件输出失败留下部分文件 | 中 | 同目录临时文件写入成功后使用原子替换,并在失败路径清理临时文件。 |
+
+- **外部依赖**:实现仅使用 Python 3.12+ 标准库,不新增第三方运行时依赖。
+- **对现有功能的兼容性**:保留现有函数名、默认参数和 CLI 开关;新 ABI 选项默认关闭。
+
+---
+
+## 6. 开发进度跟踪
+
+| 阶段 | 计划完成日期 | 状态(待开始/进行中/已完成) |
+|------|--------------|------------------------------|
+| 技术设计编写 | 2026-07-10 | ✅ 已完成 |
+| 测试用例编写 | 2026-07-24 | ✅ 已完成 |
+| 编码实现 | 2026-08-23 | ✅ 已完成 |
+| 自测与调试 | 2026-08-31 | ✅ 已完成 |
+| 代码审查(PR) | 待排期 | ⬜ 待开始 |
+| 合并主分支 | 待排期 | ⬜ 待开始 |
+
+---
+
+## 7. 附录
+
+### 7.1 参考资料
+
+- 《ScratchV RISC-V 汇编代码美化器技术设计文档》v1.2
+- `docs/topics/05-汇编代码美化器.md`
+- RISC-V Assembly Programmer's Manual
+- RISC-V ELF psABI Specification
+- RISC-V Instruction Set Manual
diff --git "a/docs/topic5\346\261\207\347\274\226\344\273\243\347\240\201\347\276\216\345\214\226\345\231\250\350\256\276\350\256\241\346\226\207\346\241\243.md" "b/docs/topic5\346\261\207\347\274\226\344\273\243\347\240\201\347\276\216\345\214\226\345\231\250\350\256\276\350\256\241\346\226\207\346\241\243.md"
new file mode 100755
index 0000000..53da3b0
--- /dev/null
+++ "b/docs/topic5\346\261\207\347\274\226\344\273\243\347\240\201\347\276\216\345\214\226\345\231\250\350\256\276\350\256\241\346\226\207\346\241\243.md"
@@ -0,0 +1,408 @@
+# ScratchV RISC-V 汇编代码美化器技术设计文档
+
+> 文档版本:v1.2
+> 更新日期:2026-08-02
+> 涉及模块:`scratchv/backend/asm_beautifier.py`(汇编代码美化器)、`scratchv/backend/asm_parser_for_beautifier.py`(美化器专用汇编解析器)
+> 功能范围:RISC-V 汇编列对齐、语义注释、分段标记、命令行美化工具
+
+---
+
+## 一、功能介绍
+
+### 1.1 功能概述
+
+#### RISC-V 汇编列对齐
+
+- RISC-V 汇编列对齐用于将编译器生成的原始 `.s` 汇编文件整理为**字段清晰、缩进统一、列宽稳定**的文本格式。其核心功能是:识别汇编行中的标签、操作码、操作数和注释,先扫描全部汇编行得到标签列、操作码列和操作数列的最大宽度,再按固定宽度列重新排版,使汇编代码更适合人工阅读、调试和教学展示。
+- 列对齐解决了机器生成汇编中常见的字段位置混乱问题,例如指令缩进不一致、标签与指令混排、不同长度操作码导致操作数列不齐等。美化器只对标签、操作码和操作数字段做列级对齐;“不重写操作数内部格式”特指不改变操作数词法内容以及括号深度大于零时的逗号和空格。括号深度为零的逗号属于操作数分隔符,分隔符两侧空白统一规范化为无前置空白、后置一个空格,即 `, `,例如 `add a0 ,a1, a2` 输出为 `add a0, a1, a2`。
+
+#### RISC-V 指令语义注释
+
+- 语义注释用于为常见 RISC-V 指令自动生成清晰、可读的解释说明。其核心功能是:根据指令助记符和操作数角色,将 `add rd, rs1, rs2`、`lw rd, imm(rs1)`、`beq rs1, rs2, label` 等指令转换为接近高级语言表达式的注释。
+- 语义注释降低了初学者阅读汇编代码的门槛,也便于编译器开发者检查后端代码生成是否符合预期。例如 `addi sp, sp, -32` 可注释为 `sp = sp + -32`,`sw ra, 28(sp)` 可注释为 `MEM[sp + 28] = ra`。
+
+#### ABI 寄存器别名注释(可选)
+
+- 美化器可选择在**自动生成的语义注释**中,将通用寄存器编号转换为 RISC-V ABI 别名,例如 `x1 -> ra`、`x2 -> sp`、`x5 -> t0`、`x10 -> a0`。该功能帮助读者快速理解返回地址、栈指针、临时寄存器和参数寄存器的角色。
+- 该选项只影响自动注释中的寄存器显示方式,不得改写原始汇编的操作数字段、标签、用户注释或程序语义。默认关闭,以保持现有输出兼容;开启后,已使用 ABI 别名的操作数(如 `a0`、`sp`)保持不变。
+
+#### 汇编分段标记
+
+- 分段标记用于识别 `.text`、`.data`、`.bss`、`.rodata` 等汇编段,以及函数入口标签,并插入清晰的分隔标题。其核心功能是:将汇编文件划分为代码段、可写数据段、未初始化数据段、只读数据段和函数块,使长汇编文件结构更加清楚。
+- 段标题必须与实现保持一致:`.text` 输出 `CODE SECTION`,`.data` 输出 `DATA SECTION`,`.bss` 输出 `BSS SECTION`,`.rodata` 输出 `READ-ONLY DATA SECTION`。
+- 分段标记适用于大型模型或复杂 DSL 程序生成的汇编文件。对于包含大量函数、标签和数据声明的输出文件,分段标记可以帮助用户快速定位函数入口、代码段和数据段。
+
+### 1.2 设计目标
+
+- **语义保持**:美化器只改变汇编文本格式和注释内容,不改变指令顺序、标签名称、操作数含义和程序执行语义。
+- **阅读友好**:输出结果应便于人工阅读,标签、操作码、操作数和注释列清晰分离。
+- **寄存器可读性可选增强**:支持在自动语义注释中以 ABI 别名展示 `x0` 至 `x31`,同时保证原始汇编文本不被改写。
+- **指令覆盖充分**:内置常见 RISC-V 整数指令、访存指令、分支跳转指令的注释模板;对 `li`、`mv`、`call`、`ret` 等汇编器伪指令单独提供模板,避免把语法糖误当作真实机器指令解释。
+- **工具独立**:既支持 Python API 调用,也支持命令行方式处理 `.s` 文件。
+- **性能可预测**:能够处理不同规模的汇编文件,对大规模输入保持稳定的处理耗时。
+- **原始注释保留**:已有行尾注释必须保留;若同时生成自动注释,二者以 ` | ` 分隔。
+
+---
+
+## 二、语法规范
+
+### 2.1 汇编行的语法定义
+
+**BNF 表示**:
+```
+asm_line ::= [label ":"] [opcode [operands]] [comment]
+label ::= standard_label | metadata_label
+standard_label ::= identifier
+metadata_label ::= "_op_/" metadata_path
+ | "_op_PPQ_Operation_" number "_" number
+metadata_path ::= metadata_char { metadata_char }
+metadata_char ::= any non-whitespace character except ":"
+opcode ::= instruction | pseudo_instruction | directive
+pseudo_instruction ::= "li" | "mv" | "call" | "ret" | ...
+operands ::= operand { top_level_comma operand }
+top_level_comma ::= "," (* only when parenthesis depth is zero *)
+comment ::= "#" text
+```
+
+`metadata_path` 可包含 `/`(例如 `layer1.0/Conv_5`),但不得包含空白或 `:`。操作数只在括号深度为零时按逗号拆分;括号内的逗号属于当前操作数,不是分隔符。
+
+| 元素 | 说明 |
+|------|------|
+| `label` | 汇编标签,用于表示函数入口或跳转目标,例如 `main:`、`.L1:` |
+| `metadata_label` | ScratchV 生成的算子边界标签,例如 `_op_/layer1.0/Conv_5:`、`_op_PPQ_Operation_6_29:`;用于标记算子代码块,不是函数入口 |
+| `opcode` | 操作码、汇编器伪指令或汇编器指示,例如 `add`、`addi`、`lw`、`sw`、`li`、`mv`、`call`、`.text` |
+| `operands` | 操作数序列,可以是寄存器、立即数、标签或内存寻址表达式 |
+| `comment` | 原始注释内容,以 `#` 开始 |
+| `instruction` | RISC-V 真实指令,例如 `add`、`lw`、`addi` |
+| `pseudo_instruction` | 汇编器伪指令,是对真实 RISC-V 指令序列的语法糖,例如 `li`、`mv`、`call`、`ret` |
+| `directive` | 汇编器指示,不对应运行时指令,例如 `.text`、`.data`、`.bss`、`.rodata`、`.globl main` |
+
+**合法示例**:
+```
+main:
+addi sp,sp,-32
+sw ra,28(sp)
+li a5,3
+add a5,a5,a4
+```
+
+### 2.2 美化输出的格式定义
+
+**BNF 表示**:
+```
+pretty_line ::= [aligned_label] [aligned_opcode] [aligned_operands] [aligned_comment]
+section_mark ::= section_bar newline section_title newline section_bar
+section_bar ::= "# " "=" * 60
+section_title ::= "# " section_name " SECTION"
+function_mark ::= "# --- Function: " function_name " ---"
+function_block ::= function_mark newline function_label newline function_body
+function_label ::= function_name ":"
+```
+
+| 元素 | 说明 |
+|------|------|
+| `aligned_label` | 标签列左对齐,标签后保留冒号;填充宽度最大为 30 字符,超长标签保持原样输出 |
+| `aligned_opcode` | 操作码列左对齐;填充宽度最小为 8 字符、最大为 12 字符,超长操作码保持原样输出 |
+| `aligned_operands` | 操作数字符串作为整体左对齐;填充宽度最小为 15 字符、最大为 40 字符,超长操作数字段保持原样输出;仅将顶层逗号分隔符规范化为 `, `,不改变括号内部格式或操作数词法内容 |
+| `aligned_comment` | 原始注释或自动生成的语义注释 |
+| `section_bar` | 段标记用`=`隔开 |
+| `section_title` | 段标记 |
+| `function_mark` | 函数入口标记,用于突出 `main` 等函数标签;除位于整个输入或输出的第一行外,标题前至少保留一个空行 |
+| `function_block` | 函数标记后,函数标签独占一行;函数体无标签指令从行首开始,不与函数标签同行,也不添加前导缩进 |
+
+三列的实际填充宽度分别按 `label_width = min(max_label, 30)`、`opcode_width = min(max(max_opcode, 8), 12)`、`operands_width = min(max(max_operands, 15), 40)` 计算。操作码和操作数即使短于最小宽度,也分别保留 8 和 15 字符的固定字段,使短汇编片段的操作数列与注释列保持稳定。字段长度未超过填充宽度时使用空格补齐;字段长度超过上限时不截断也不强制填充,其后字段自然右移。因此,上限范围内的行保持列对齐,含超长字段的行允许局部溢出,以原文和汇编语义安全优先。
+
+段标题与原始 directive 的输出顺序是强制契约:段标题紧邻插入在对应 directive 之前,原始 `.text`、`.data`、`.bss`、`.rodata` 行完整保留,directive 之后插入一个空行。四类映射如下:
+
+| 原始 directive | `section_title` |
+|----------------|-----------------|
+| `.text` | `# CODE SECTION` |
+| `.data` | `# DATA SECTION` |
+| `.bss` | `# BSS SECTION` |
+| `.rodata` | `# READ-ONLY DATA SECTION` |
+
+对已经紧邻同类标题的 directive 不重复插入标题,保证重复运行美化器不会不断叠加段标题。
+
+**合法示例**:
+```
+# --- Function: main ---
+main:
+addi sp, sp, -32 # sp = sp + -32
+sw ra, 28(sp) # MEM[sp + 28] = ra
+li a5, 3 # a5 = 3
+add a5, a5, a4 # a5 = a5 + a4
+```
+
+### 2.3 Python API 与数据结构
+
+美化器所需的解析能力由专用模块 `scratchv/backend/asm_parser_for_beautifier.py` 提供。现有 `scratchv/backend/_asm_parser.py` 已被 peephole、const-merge、inst-counter、inst-scheduler 等汇编级 pass 使用,其数据结构和宽松解析行为属于既有兼容契约;美化器需要更强的原文保真、字符串感知、元数据标签和错误状态分类,因此不修改或替换 `_asm_parser.py`。专用解析器的目标接口如下:
+
+```python
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Literal, TypedDict
+
+ParseStatus = Literal[
+ "valid",
+ "metadata_label",
+ "incomplete_operands",
+ "unknown_opcode",
+ "malformed",
+]
+
+class FieldLengths(TypedDict):
+ label: int
+ opcode: int
+ operands_str: int
+ comment: int
+
+@dataclass
+class ParsedAsmLine:
+ raw: str
+ label: str | None
+ opcode: str | None
+ operands_str: str
+ operands: list[str]
+ comment: str | None
+ lineno: int
+ parse_status: ParseStatus
+ field_lengths: FieldLengths
+
+def parse_asm_line(line: str, lineno: int = 0) -> ParsedAsmLine: ...
+def parse_asm(asm_text: str) -> list[ParsedAsmLine]: ...
+
+def beautify_asm(
+ asm_text: str,
+ align: bool = True,
+ add_comments: bool = True,
+ abi_register_names: bool = False,
+) -> str: ...
+
+def beautify_file(
+ input_path: str | Path,
+ output_path: str | Path | None = None,
+ align: bool = True,
+ add_comments: bool = True,
+ abi_register_names: bool = False,
+ encoding: str = "utf-8",
+) -> str: ...
+```
+
+`raw` 始终保存不含行尾换行符的原始行;`label` 不含末尾冒号;`comment` 不含 `#` 和紧随其后的分隔空格;`lineno` 为从 1 开始的源文件行号。`field_lengths` 按上述规范化字段计算,缺失字段长度为 `0`。`beautify_file()` 始终返回美化结果;指定 `output_path` 时,先写入同目录临时文件,成功后原子替换目标文件,失败时不得留下部分输出。
+
+两个解析器的职责边界如下:
+
+| 模块 | 调用方 | 稳定契约 | 本课题处理方式 |
+|------|--------|----------|----------------|
+| `_asm_parser.py` | peephole、const-merge、inst-counter、inst-scheduler 等既有 pass | 通用标签、opcode、操作数和注释解析,以及现有 `ParsedAsmLine` 行为 | 不修改、不迁移调用方、不增加美化器专用字段 |
+| `asm_parser_for_beautifier.py` | 仅 `asm_beautifier.py` | `raw`/`operands_str` 精确保留、保留 directive 前导 `.` 的 `opcode`、引号与括号状态扫描、元数据标签、五种 `parse_status`、`field_lengths` | 新建并独立测试 |
+
+`asm_beautifier.py` 必须从专用模块导入 `ParsedAsmLine`、`parse_asm_line()` 和 `parse_asm()`,自身不得保留逐行解析正则、注释切分器或操作数拆分器。两个解析器不得互相导入。其他汇编级 pass 不得在本课题中改为依赖专用解析器,避免扩大变更范围。
+
+对于双方都支持的普通合法汇编子集,标签、opcode、操作数和注释的字段含义应保持一致;美化器特有的 `operands_str`、`parse_status`、`field_lengths`、字符串字面量保护和元数据标签规则属于有意差异,不要求反向加入 `_asm_parser.py`。
+
+### 2.4 段与函数入口识别
+
+函数标题只能在当前段为 `.text` 且目标标签存在于本文件时生成。识别按以下优先级执行:
+
+1. 匹配 `_op_/...` 或 `_op_PPQ_Operation__` 的元数据标签永远不是函数,即使名称同时出现在其他指示或调用中。
+2. `.type , @function` 明确声明的符号是函数。
+3. `.globl ` 或 `.global ` 声明且在 `.text` 段定义的符号是函数。
+4. 直接 `call ` 的目标在本文件 `.text` 段存在同名定义时,识别为函数。
+5. `.text` 段内定义的 `main` 作为程序入口特例,即使没有显式声明或本地 `call`,也识别为函数。
+6. `.L1`、`L1`、`loop1`、`_L1` 等局部或控制流标签,以及仅作为分支目标出现的普通标签,不生成函数标题;其他无法确定的标签采用保守策略,也不生成标题。
+
+显式声明优先于命名启发式;实现不得仅凭“标签不以点号开头”判定函数。函数标题插入在函数标签之前;`.globl`、`.type` 等原始指示保持原位置,不移动到函数块内部。
+开启 `--no-align` 时仍插入函数标题,但函数标签及其同行指令保持原始布局,不强制拆分;默认对齐模式下,函数标签独占一行,同行指令移动到紧随其后的函数体行。函数标题不是整个输入或输出的第一行时,其前面至少保留一个空行;已有空行时不得重复追加。对已经紧邻同名函数标题的标签不重复插入标题,保证重复运行美化器时不会叠加标题或空行。
+
+### 2.5 约束规则
+
+- 美化器不得删除、重排或替换原始指令。
+- 标签名称必须保持不变,跳转目标不得被改写。
+- `.text`、`.data`、`.bss`、`.rodata`、`.globl` 等汇编器指示必须保留。
+- **数据段处理**:美化器必须识别 `.data`、`.bss`、`.rodata` 段,并在段切换处插入对应标题;数据段内的 `.asciz`、`.string`、`.word`、`.half`、`.byte`、`.float`、`.double`、`.zero`、`.space`、`.align` 等数据定义行属于汇编器指示,不参与列宽扫描、字段对齐或操作数空格规范化,也不生成运行时语义注释,整行按 `raw` 原样输出。无法可靠分离字符串与注释的行标记为 `malformed`,其可识别的标签、操作码和操作数内容保持不变,仅在输出注释位置追加 `[warning: malformed instruction]`。
+- 对 `incomplete_operands`、`unknown_opcode` 与 `malformed` 行,不得导致程序崩溃;注释前由标签、操作码(操作符)、已有操作数及空白组成的代码部分必须逐字保留,自动注释只输出对应的警告信息。
+- 原始注释必须保留;异常行已有用户注释时,以 ` | ` 追加警告,不得覆盖原注释或拼接指令模板结果。
+- 内存操作数如 `28(sp)` 必须作为整体识别,不能被错误拆分。
+- **ABI 寄存器别名开关**:默认使用输入中的寄存器写法;启用 ABI 别名后,仅自动语义注释中的完整通用寄存器 token(`x0` 至 `x31`)按 ABI 约定替换,如 `x1 -> ra`、`x2 -> sp`、`x10 -> a0`。内存操作数中的基址寄存器也应适用该规则,例如 `0(x2)` 的注释显示为 `MEM[sp + 0]`;立即数、标签、字符串和原始汇编操作数不得转换。
+- **未知指令**:不在真实指令、汇编器伪指令或 ScratchV 已登记自定义伪指令模板中的操作码,必须保留标签、未知操作码和操作数内容,不进行列对齐;输出注释为 `[warning: unknown opcode]`,不得套用具体指令模板或 `opcode; operands` 等 fallback 注释。
+- **操作数数量不足**:对于 `add a0`、`lw t0`、`beq a0,a1` 等操作码可识别但操作数不足的行,不做严格合法性校验,也不得导致程序崩溃。输出必须保留原始标签、操作码和操作数字符串;注释生成阶段不得用空字符串补齐缺失位置,也不得执行指令模板匹配,只输出 `[warning: operand missing]`。不得删除、猜测或补写缺失的原始操作数。
+
+### 2.6 异常输入处理
+
+美化器的异常输入策略是“保守保留、不中断后续行”。解析器应为每行记录 `parse_status`,至少区分 `valid`、`metadata_label`、`incomplete_operands`、`unknown_opcode` 和 `malformed`,并按以下规则处理:
+
+| `parse_status` | 指令字段输出 | 输出注释 | 模板处理 |
+|----------------|--------------|----------|----------|
+| `incomplete_operands` | 注释前代码部分逐字保留 | `[warning: operand missing]` | 跳过模板匹配,不补齐占位符 |
+| `unknown_opcode` | 注释前代码部分逐字保留 | `[warning: unknown opcode]` | 跳过具体模板和 fallback 推测 |
+| `malformed` | 注释前代码部分逐字保留 | `[warning: malformed instruction]` | 跳过模板匹配,不解释局部片段 |
+
+状态判定按以下优先级执行:字段边界、括号或字符串异常时为 `malformed`;字段结构完整后,未登记 opcode 优先标记为 `unknown_opcode`;仅对已登记 opcode 检查操作数数量并标记 `incomplete_operands`。因此 `custom_add a0` 的状态为 `unknown_opcode`,而不是 `incomplete_operands`。
+
+警告注释替代自动语义注释,且不受 `add_comments` 或 ABI 寄存器别名开关影响。开启列对齐且异常行没有原始用户注释时,警告的 `#` 应与普通指令注释列对齐;异常代码超过统一注释列时,在原代码后至少保留两个空格再输出警告。若原行已有用户注释,保留其原始位置,并按“原注释 ` | ` 警告”的顺序合并。若原注释已经以当前状态对应的同类警告结尾,不得重复追加,保证重复美化输出稳定。三种异常状态均不得输出或拼接任何指令模板匹配结果。示例:
+
+```asm
+L1: add a0 # [warning: operand missing]
+L2: custom_op x1,x2,x3 # [warning: unknown opcode]
+L3: li,,a0,,3 # [warning: malformed instruction]
+```
+
+- **算子元数据标签**:完整匹配 `_op_/...:` 或 `_op_PPQ_Operation__:` 的标签标记为 `metadata_label`,整行原样保留且不得被识别为函数入口。`_input_copy_done:`、`_mp_done:` 等阶段或控制流标签仍按普通标签处理。
+- **缺少操作数**:操作码和已有操作数仍按原样输出,注释只输出 `[warning: operand missing]`;不得用空字符串填充模板占位符,也不得生成类似 `a0 = a0 + ` 的不完整语义结果。
+- **重复逗号、异常标签、非汇编文本或字段边界不明确**:例如 `li,,a0,,3`、`main::`、`this is not asm`。这类行标记为 `malformed`,保持标签、操作码和操作数内容不变,注释只输出 `[warning: malformed instruction]`,也不得把其中的一部分误识别为未知操作码。
+- **未知但结构完整的操作码**:例如 `custom_op x1,x2,x3`。这类行标记为 `unknown_opcode`,不属于 `malformed`;保持标签、未知操作码和操作数内容不变,注释只输出 `[warning: unknown opcode]`。已有用户注释先保留,再以 ` | ` 追加该警告。
+- **文件级错误**:输入文件不存在、无法读取或输出文件无法写入时,命令行工具应向标准错误输出清晰的路径和原因,并以非零状态码退出;不得输出不完整文件。
+
+### 2.7 命令行接口
+
+```bash
+python -m scratchv.backend.asm_beautifier input.s
+python -m scratchv.backend.asm_beautifier input.s -o output.s
+python -m scratchv.backend.asm_beautifier input.s --no-align
+python -m scratchv.backend.asm_beautifier input.s --no-comments
+python -m scratchv.backend.asm_beautifier input.s --abi-register-names
+```
+
+| 参数 | 默认值 | 行为 |
+|------|--------|------|
+| `input` | 无,必需 | UTF-8 汇编输入路径。 |
+| `-o`, `--output` | 未指定 | 指定时原子写入该路径;未指定时只向标准输出写入结果。 |
+| `--no-align` | 关闭 | 传入后设置 `align=False`。 |
+| `--no-comments` | 关闭 | 传入后设置 `add_comments=False`,关闭自动生成的**语义注释**,但保留用户原始注释。 |
+| `--abi-register-names` | 关闭 | 传入后设置 `abi_register_names=True`,仅改变自动注释。 |
+
+成功时退出码为 `0`。参数错误由 `argparse` 输出到标准错误并返回 `2`;文件读取、编码或写入失败输出包含目标路径与原因的单行错误并返回 `1`。失败时标准输出不得混入部分美化结果。
+
+`--no-comments` 仅关闭**自动语义注释**(`add_comments=False`)。第 2.6 节规定的三种解析警告注释(`[warning: operand missing]`、`[warning: unknown opcode]`、`[warning: malformed instruction]`)属于诊断信息而非语义注释,不受 `add_comments` 或 `--no-comments` 影响,即使传入该开关仍会输出,以保证异常行不被静默放过。原始用户注释也始终保留。
+
+---
+
+## 三、测试设计
+
+测试资产不拆分为独立汇编样例文件,按功能单元测试、功能集成测试、黑盒测试和压力测试组织:
+
+| 测试 | 描述 | 预期 |
+|------|------|------|
+| 功能单元:解析 | `tests/test_parser_for_beautifier.py` 直接测试字段提取、字符串/括号扫描、元数据标签、行号和 `parse_status`。 | 解析字段和五种状态正确。 |
+| 功能单元:注释 | `tests/test_asm_beautifier_comments.py` 直接测试模板填充、ABI 别名和注释合并规则。 | 指令语义准确,不生成未知或不完整模板。 |
+| 功能单元:格式化 | `tests/test_asm_beautifier_formatting.py` 测试列宽、顶层操作数规范化、注释列、指示原样保留和换行。 | 格式化规则独立且稳定。 |
+| 功能集成 | `tests/test_asm_beautifier_integration.py` 验证解析、格式化、异常警告、段/函数标记、文件 API 与完整程序输出。 | 各模块组合后保持语义、格式和幂等性。 |
+| 黑盒 | `tests/test_asm_beautifier_blackbox.py` 通过子进程运行 CLI,验证参数、标准流、文件输出和退出码。 | 用户可观察行为符合 CLI 契约。 |
+| 压力测试 | `benchmarks/bench_asm_beautifier.py` 使用固定和合成汇编输入记录耗时、波动和输出大小。 | 性能稳定,无无法解释的退化。 |
+
+## 四、修改模块与实现步骤
+
+### 4.1 涉及文件
+
+- [asm_beautifier.py](/ScratchV/scratchv/backend/asm_beautifier.py):负责逐行解析、列宽扫描、格式化输出、语义注释、段标题及命令行入口。
+- [asm_parser_for_beautifier.py](/ScratchV/scratchv/backend/asm_parser_for_beautifier.py):美化器专用解析模块,负责原文字段保留、字符串与括号状态扫描、元数据标签和解析状态;不替换现有 `_asm_parser.py`。
+- [test_parser_for_beautifier.py](/ScratchV/tests/test_parser_for_beautifier.py):功能单元测试,负责验证解析字段、字段长度、操作数拆分、元数据标签和异常状态分类。
+- [test_asm_beautifier_comments.py](/ScratchV/tests/test_asm_beautifier_comments.py):功能单元测试,负责验证注释模板、伪指令和 ABI 别名。
+- [test_asm_beautifier_formatting.py](/ScratchV/tests/test_asm_beautifier_formatting.py):功能单元测试,负责验证列宽、操作数规范化、注释列和换行保持。
+- [test_asm_beautifier_integration.py](/ScratchV/tests/test_asm_beautifier_integration.py):功能集成测试,负责验证格式化、结构标记、异常输出、文件 API 和完整程序处理。
+- [test_asm_beautifier_blackbox.py](/ScratchV/tests/test_asm_beautifier_blackbox.py):黑盒测试,负责验证 CLI 的参数、标准流、文件输出和错误退出码。
+- [bench_asm_beautifier.py](/ScratchV/benchmarks/bench_asm_beautifier.py):在内存中生成不同规模的汇编输入并记录性能指标,不依赖独立样例 `.s` 文件。
+
+### 4.2 语法与处理约定
+
+实现必须遵循第 2.1 至 2.7 节的输入语法、输出格式、API、结构识别、约束、异常策略和 CLI 契约;BNF 与行为规格只在第二章维护,避免设计与实现章节出现重复定义后发生漂移。
+
+主流程依次处理空行、纯注释、普通标签、算子元数据标签、指令、汇编器伪指令、汇编器指示和带行尾注释的指令,并为每行设置 `parse_status`。
+
+### 4.3 新增美化器专用解析器
+
+专用模块 `asm_parser_for_beautifier.py` 的 `parse_asm_line()` 需要把每一行汇编拆分为结构化字段,字段契约以第 2.3 节为准,包括:
+
+- `raw`:未经改写的原始输入行;
+- `label`:可选标签字段,去掉末尾冒号后保存;
+- `opcode`:真实指令、汇编器伪指令或汇编器指示字段;汇编器指示保留前导 `.`;
+- `operands_str`:原始操作数字符串,用于后续列宽统计和输出;
+- `operands`:按逗号拆分后的操作数列表,用于语义注释生成;
+- `comment`:去掉 `#` 及分隔空格后的行尾注释内容;
+- `lineno`: 从 1 开始的源文件行号,便于输出定位错误;
+- `parse_status`:解析状态,取值为 `valid`、`metadata_label`、`incomplete_operands`、`unknown_opcode` 或 `malformed`;
+- `field_lengths`:`label`、`opcode`、`operands_str`、`comment` 四个字段的字符长度,用于后续列宽统计。
+
+操作数拆分时需要识别括号深度,保证 `28(sp)`、`0(a0)` 这类内存寻址表达式作为一个整体处理,不被错误拆开。行尾注释分隔符 `#` 在字符串字面量之外始终生效,即使前方存在未闭合括号;确保 `.asciz "a#b"` 等数据定义保持完整,并将 `lw a0, 0(sp # comment` 保守标记为 `malformed`。解析器只识别字段边界并保留 `operands_str` 原文;后续格式化器仅将顶层操作数分隔符规范化为 `, `(移除逗号前空白,并把逗号后空白折叠为一个),不改写括号内部格式或操作数词法内容。
+
+解析前先识别完整标签行,再分类标签类型。完整匹配 `^_op_/[^:\s]+$` 或 `^_op_PPQ_Operation_\d+_\d+$` 的标签标记为 `metadata_label`;标签内容必须完整保存,不能按 `/` 或 `.` 拆分。`_input_copy_done`、`_mp_done` 等不匹配上述格式的内部标签仍标记为 `valid`。出现重复逗号、重复标签冒号、无法确定字段边界等情况时,标记为 `malformed`,注释前代码部分逐字保留,跳过列对齐和指令模板匹配,并在输出注释中写入 `[warning: malformed instruction]`。`field_lengths` 按解析后的字段值计算:标签不含冒号,注释不含 `#` 和分隔空格,缺失字段记为 `0`。
+
+本课题不得修改 `_asm_parser.py` 来承载上述字段和状态,也不得把 peephole、const-merge 等既有调用方迁移到专用解析器。为控制两套解析器的维护风险,测试应覆盖:导入边界、普通合法汇编的公共字段兼容性,以及字符串、元数据和异常输入等美化器有意扩展场景。
+
+### 4.4 列宽扫描与对齐输出
+
+列对齐采用两阶段处理:
+
+1. **第一阶段:扫描列宽**。先解析全部汇编行,仅对解析状态为 `valid` 的运行时指令与伪指令统计 `label`、`opcode` 和规范化后操作数字符串的最大长度,得到 `max_label`、`max_opcode` 和 `max_operands`。实际填充宽度按 `label_width = min(max_label, 30)`、`opcode_width = min(max(max_opcode, 8), 12)`、`operands_width = min(max(max_operands, 15), 40)` 计算。`DIRECTIVES` 集合中的汇编器指示以及 `incomplete_operands`、`unknown_opcode`、`malformed` 行不参与扫描。
+2. **第二阶段:格式化输出**。对参与扫描的指令行,将操作码和操作数字段按对应列宽左对齐,顶层操作数分隔符统一为 `, `。所有普通标签必须单独输出,不能与指令同行;无标签指令从行首开始,不添加前导缩进。已有行尾注释保留;自动注释存在时,以 ` | ` 分隔。纯注释补充前导空格,使 `#` 与指令注释列对齐。空行、汇编器指示和 `metadata_label` 行按原规则输出;`incomplete_operands`、`unknown_opcode` 和 `malformed` 行不参与列对齐或操作数空格规范化,注释前代码部分逐字保持不变,仅在注释位置输出对应警告。元数据标签不得触发函数标题。
+
+数据定义行和其他汇编器指示按第 2.5 节原样输出,不参与字段对齐。段标题必须在原始分段 directive 之前输出,directive 本身完整保留,随后输出一个空行。函数入口严格使用第 2.4 节的符号规则。
+
+操作码和操作数的最小填充宽度分别为 8 和 15,用于稳定操作数列及注释列的位置;列宽上限只限制填充宽度,不截断标签、操作码或操作数字符串。长度不超过填充宽度的字段使用空格补齐;超长字段不填充并保持原样,其后字段允许自然右移,不要求该行继续与普通行对齐。该降级行为以保证美化后的 `.s` 文件仍可被汇编并保持原有语义为优先。开启 `--no-align` 时跳过列宽扫描与对齐,按原字段顺序输出。
+
+### 4.5 语义注释模板与伪指令处理
+
+语义注释模板(使用字典实现)应按指令类别分组维护:
+
+- **真实 RISC-V 指令模板**:覆盖整数算术、位运算、访存、分支跳转、乘除法扩展和浮点访存/计算等指令,例如 `add`、`addi`、`lw`、`sw`、`beq`、`jal`、`mul`、`flw`。
+- **汇编器伪指令模板**:覆盖 `li`、`mv`、`call`、`ret`、`nop`、`not`、`neg`、`seqz`、`snez`、`bnez`、`beqz` 等语法糖。它们不是真正的 RISC-V 机器指令,通常会被汇编器展开为一条或多条真实指令,因此需要单独解释其高层含义。
+- **ScratchV 自定义伪指令模板**:覆盖项目内部扩展或教学用途的伪指令,例如 `max`,模板需明确标注其自定义性质。
+- **汇编器指示与未知指令处理**:`.text`、`.data`、`.bss`、`.rodata`、`.globl`、`.loc` 等 `DIRECTIVES` 集合中的指示不参与列宽扫描、字段对齐或操作数空格规范化,不按运行时指令解释,也不生成自动语义注释,始终按 `raw` 原样输出;其中分段指示须在原指示之前分别插入 `CODE`、`DATA`、`BSS`、`READ-ONLY DATA` 标题。未知 opcode 标记为 `unknown_opcode` 后保留标签、操作码和操作数内容,注释输出 `[warning: unknown opcode]`,不套用任何具体语义模板或 fallback 注释。
+
+模板匹配时先去掉 opcode 前导的 `.` 用于查表,但必须区分汇编器指示和真实指令/伪指令的语义类别。对 `li`、`mv`、`call` 等伪指令,不应按普通三操作数 RISC-V 指令规则解释,而应使用专门的操作数映射规则,例如 `li rd, imm`、`mv rd, rs`、`call symbol`。
+
+注释生成前必须检查 `parse_status`:只有 `valid` 行允许进入指令模板匹配;`incomplete_operands`、`unknown_opcode` 和 `malformed` 行必须跳过模板并返回第 2.6 节规定的警告注释。`metadata_label` 和汇编器指示不生成自动语义注释,也必须跳过模板匹配。
+
+若 `nop` 已经带有原始注释,说明该注释已经表达了该行用途,美化器只保留原始注释,不得再追加 `no operation`。无原始注释的普通 `nop` 仍可使用 `no operation` 自动注释。
+
+`ret` 的固定高层语义模板为 `return`。美化器用于解释程序行为,不负责展示伪指令展开,因此无需将注释改写为 `jalr x0, ra, 0`;如需观察展开结果,应交由汇编器或反汇编工具处理。
+
+对于操作数数量不足的已知指令,不得将操作数列表补齐为空字符串,也不得填充 `{rd}`、`{rs1}`、`{rs2}`、`{imm}` 等模板占位符。输入为 `add a0`、`lw t0`、`beq a0,a1` 时,应保持原标签、操作码和已有操作数内容,统一输出 `[warning: operand missing]`,禁止生成 `a0 = a0 + ` 等不完整或误导性的模板结果。
+
+ABI 寄存器别名应在模板占位符填充阶段处理。实现需维护 `x0` 至 `x31` 到 ABI 名称的映射,并仅对 `{rd}`、`{rs1}`、`{rs2}` 和内存寻址表达式中提取出的基址寄存器执行转换;`{imm}`、分支目标和调用目标不得转换。Python API 增加 `abi_register_names: bool = False` 参数,并由 `beautify_asm()` 传递至注释生成逻辑;`beautify_file()` 同步暴露该参数。命令行新增 `--abi-register-names` 开关,启用后传入 `abi_register_names=True`。
+
+### 4.6 集成与回归测试
+
+集成与回归测试按第三章表格规定的功能单元、功能集成、黑盒和压力测试分类组织;每个行为只在最贴合其职责的一类中保留主断言,避免重复覆盖造成维护漂移。
+
+---
+## 五、附录
+
+### 5.1 示例美化输出
+
+**输入汇编**(美化前):
+```
+.text
+main:
+addi sp,sp,-32
+sw ra,28(sp)
+li a5,3
+li a4,5
+add a5,a5,a4
+ret
+```
+
+**生成的美化汇编输出**:
+```
+# ============================================================
+# CODE SECTION
+# ============================================================
+.text
+
+# --- Function: main ---
+main:
+addi sp, sp, -32 # sp = sp + -32
+sw ra, 28(sp) # MEM[sp + 28] = ra
+li a5, 3 # a5 = 3
+li a4, 5 # a4 = 5
+add a5, a5, a4 # a5 = a5 + a4
+ret # return
+```
+
+### 5.2 参考资料
+
+- ScratchV 项目文档:`docs/topics/05-汇编代码美化器.md`
+- RISC-V Assembly Programmer's Manual
+- RISC-V ELF psABI 文档
+- RISC-V 指令集手册(分支与跳转指令)
diff --git a/scratchv/backend/asm_beautifier.py b/scratchv/backend/asm_beautifier.py
old mode 100644
new mode 100755
index 279881d..1760166
--- a/scratchv/backend/asm_beautifier.py
+++ b/scratchv/backend/asm_beautifier.py
@@ -1,534 +1,908 @@
-"""RISC-V Assembly Beautifier.
+"""基于美化器专用解析器实现 RISC-V 汇编字段对齐。
-Parses RISC-V assembly text and outputs a formatted, aligned version with
-semantic comments and section headers for improved readability.
-
-Usage as module::
-
- from scratchv.backend.asm_beautifier import beautify_asm
- pretty = beautify_asm(raw_asm)
-
-Usage as CLI::
-
- python -m scratchv.backend.asm_beautifier input.s -o output.s
"""
from __future__ import annotations
import argparse
+import os
import re
-from typing import Optional
-
-
-# ---------------------------------------------------------------------------
-# Instruction comment templates
-# ---------------------------------------------------------------------------
-
-_RV_REG_NAMES: dict[str, str] = {
- "x0": "zero", "x1": "ra", "x2": "sp", "x3": "gp",
- "x4": "tp", "x5": "t0", "x6": "t1", "x7": "t2",
- "x8": "s0/fp", "x9": "s1", "x10": "a0", "x11": "a1",
- "x12": "a2", "x13": "a3", "x14": "a4", "x15": "a5",
- "x16": "a6", "x17": "a7", "x18": "s2", "x19": "s3",
- "x20": "s4", "x21": "s5", "x22": "s6", "x23": "s7",
- "x24": "s8", "x25": "s9", "x26": "s10", "x27": "s11",
- "x28": "t3", "x29": "t4", "x30": "t5", "x31": "t6",
-}
-
-
-def _anon_reg(r: str) -> str:
- """Return ABI name for a register string like 'x5' or 't0'."""
- r = r.strip().lstrip("%")
- return _RV_REG_NAMES.get(r, r)
-
-
-# Mapping: instruction mnemonic -> comment template
-# {rd}, {rs1}, {rs2}, {imm} are replaced at format time.
-_INST_COMMENTS: dict[str, str] = {
- # Integer arithmetic
- "add": "{rd} = {rs1} + {rs2}",
- "sub": "{rd} = {rs1} - {rs2}",
- "addi": "{rd} = {rs1} + {imm}",
- "slli": "{rd} = {rs1} << {imm}",
- "srli": "{rd} = {rs1} >> {imm} (logical)",
- "srai": "{rd} = {rs1} >> {imm} (arithmetic)",
- "sll": "{rd} = {rs1} << {rs2}",
- "srl": "{rd} = {rs1} >> {rs2} (logical)",
- "sra": "{rd} = {rs1} >> {rs2} (arithmetic)",
- "mul": "{rd} = {rs1} * {rs2}",
- "div": "{rd} = {rs1} / {rs2}",
- "rem": "{rd} = {rs1} % {rs2}",
- "xor": "{rd} = {rs1} XOR {rs2}",
- "or": "{rd} = {rs1} | {rs2}",
- "and": "{rd} = {rs1} & {rs2}",
- "xori": "{rd} = {rs1} XOR {imm}",
- "ori": "{rd} = {rs1} | {imm}",
- "andi": "{rd} = {rs1} & {imm}",
- "slt": "{rd} = ({rs1} < {rs2}) ? 1 : 0",
- "sltu": "{rd} = ({rs1} < {rs2}) ? 1 : 0 (unsigned)",
- "slti": "{rd} = ({rs1} < {imm}) ? 1 : 0",
- "sltiu": "{rd} = ({rs1} < {imm}) ? 1 : 0 (unsigned)",
- # Memory
- "lw": "{rd} = MEM[{rs1} + {imm}]",
- "lh": "{rd} = MEM16[{rs1} + {imm}]",
- "lb": "{rd} = MEM8[{rs1} + {imm}]",
- "lbu": "{rd} = MEM8[{rs1} + {imm}] (unsigned)",
- "lhu": "{rd} = MEM16[{rs1} + {imm}] (unsigned)",
- "sw": "MEM[{rs1} + {imm}] = {rd}",
- "sh": "MEM16[{rs1} + {imm}] = {rd} (low 16b)",
- "sb": "MEM8[{rs1} + {imm}] = {rd} (low 8b)",
- # Upper immediate
- "lui": "{rd} = {imm} << 12",
- "auipc": "{rd} = PC + ({imm} << 12)",
- # Branches
- "beq": "if {rs1} == {rs2} goto {rd}",
- "bne": "if {rs1} != {rs2} goto {rd}",
- "blt": "if {rs1} < {rs2} goto {rd}",
- "bge": "if {rs1} >= {rs2} goto {rd}",
- "bltu": "if {rs1} < {rs2} goto {rd} (unsigned)",
- "bgeu": "if {rs1} >= {rs2} goto {rd} (unsigned)",
- # Jumps
- "j": "goto {rd}",
- "jal": "{rd} = PC+4; goto {imm}",
- "jalr": "{rd} = PC+4; goto {rs1}+{imm}",
- # Pseudo-instructions
- "li": "{rd} = {imm}",
- "mv": "{rd} = {rs1}",
- "not": "{rd} = ~{rs1}",
- "neg": "{rd} = -{rs1}",
- "seqz": "{rd} = ({rs1} == 0) ? 1 : 0",
- "snez": "{rd} = ({rs1} != 0) ? 1 : 0",
- "bnez": "if {rs1} != 0 goto {rd}",
- "beqz": "if {rs1} == 0 goto {rd}",
- "call": "call {rd}",
- "ret": "return",
- "nop": "no operation",
- # M-extension
- "mulh": "{rd} = ({rs1} * {rs2})[63:32]",
- "divu": "{rd} = {rs1} / {rs2} (unsigned)",
- "remu": "{rd} = {rs1} % {rs2} (unsigned)",
- # F/D extensions
- "fadd.s": "{rd} = {rs1} + {rs2} (f32)",
- "fsub.s": "{rd} = {rs1} - {rs2} (f32)",
- "fmul.s": "{rd} = {rs1} * {rs2} (f32)",
- "fdiv.s": "{rd} = {rs1} / {rs2} (f32)",
- "flw": "{rd} = MEM[{rs1} + {imm}] (f32)",
- "fsw": "MEM[{rs1} + {imm}] = {rd} (f32)",
- # Custom pseudo (ScratchV)
- "max": "{rd} = max({rs1}, {rs2})",
-}
-
-
-# ---------------------------------------------------------------------------
-# Line parsing
-# ---------------------------------------------------------------------------
-
-# Regex: optional label at start, then instruction, operands, comment
-_LINE_RE = re.compile(
- r'^\s*'
- r'(?P