diff --git "a/docs/topics/09-DSL\351\224\231\350\257\257\346\217\220\347\244\272\347\276\216\345\214\226\345\231\250-\345\274\200\345\217\221\346\226\207\346\241\243.md" "b/docs/topics/09-DSL\351\224\231\350\257\257\346\217\220\347\244\272\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 100644 index 0000000..dd5e243 --- /dev/null +++ "b/docs/topics/09-DSL\351\224\231\350\257\257\346\217\220\347\244\272\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,691 @@ +# 课题9:DSL错误提示美化器开发文档 + +> **文档类型**:开发指南 | **状态**:草案 | **版本**:v0.1 +> **调研基线**:`main@997d2aa` | **建议周期**:12 周 +> **重要说明**:本文是后续开发指导,不表示文中功能已经在当前仓库实现 + +--- + +## 1. 课题目标 + +本课题在现有 `scratchv/frontend/dsl_errors.py` 基础上,把结构化错误诊断真正接入: + +- `DSLParser`; +- `ExtendedDSLParser`; +- `CompilerDriver` 的 DSL 编译路径; +- ScratchV CLI 的错误输出。 + +最终效果是:用户提交错误 DSL 时,能够看到准确位置、源码高亮、稳定错误码和可信的修复建议;需要检查整个文件时,可以一次报告多个独立错误。 + +详细架构和接口约束见 [设计文档](09-DSL错误提示美化器-设计文档.md)。 + +--- + +## 2. 当前起点 + +### 2.1 已经存在的代码 + +| 文件 | 需要先理解的内容 | +|------|------------------| +| `scratchv/frontend/dsl_errors.py` | `DSLSyntaxError`、`format_error()`、`ErrorCollector`、建议表 | +| `scratchv/frontend/dsl_parser.py` | 基础 DSL 的逐行正则解析与 IRBuilder 调用 | +| `scratchv/frontend/dsl_extended.py` | `if/while` 块解析、块索引和嵌套逻辑 | +| `scratchv/compiler.py` | DSL/ONNX 分流、扩展解析器回退、`CompileResult.errors` | +| `scratchv/main.py` | CLI 参数、编译结果和 stderr 输出 | +| `tests/test_dsl_errors.py` | 现有错误模块的单元测试风格 | +| `tests/test_parser.py` | 基础 DSL 成功路径 | +| `tests/test_dsl_extended.py` | 扩展语法及 `DSLParseError` 兼容要求 | + +### 2.2 当前能力边界 + +当前错误美化模块可以独立构造和打印错误: + +```python +from scratchv.frontend.dsl_errors import make_error, format_error + +error = make_error( + line=2, + col=10, + message="unsupported operation 'ad'", + source_line="result = ad(a, b)", + filename="bad.dsl", + error_code="E200", +) +print(format_error(error, use_color=False)) +``` + +但是解析器并不会自动产生这个错误对象。`ErrorCollector` 也只是容器,当前解析流程没有错误恢复机制。 + +### 2.3 开发前必须复现的问题 + +开始改代码前,至少记录以下三类基线输出: + +```text +1. 无法识别的普通语句 +2. 不支持的算子 +3. 缺失 endif/endwhile 的扩展 DSL +``` + +基线记录应包含:输入 DSL、调用入口、异常类型、异常文本和退出码。后续用相同输入证明错误质量确实改善。 + +--- + +## 3. 前置知识 + +开始课题前,建议掌握: + +1. Python 异常继承、`dataclass`、`enum` 和类型注解; +2. 正则表达式的 `match`、`fullmatch` 和捕获组; +3. 编译器前端中的源码位置、错误恢复和级联错误; +4. ScratchV 的 `IRBuilder` 与 `Program`; +5. pytest 参数化、异常断言和文本快照; +6. ANSI 转义码、TTY 和 stderr; +7. Git 小步提交和回归测试。 + +不要求先实现完整 tokenizer、AST 或 LSP。 + +--- + +## 4. 推荐阅读顺序 + +### 第一步:跑通正确 DSL + +阅读并运行: + +```bash +python -m pytest tests/test_parser.py tests/test_dsl_extended.py -q +``` + +目标:理解正确程序如何从 DSL 进入 `IRBuilder`,不要先看错误模块就直接修改异常类型。 + +### 第二步:单独理解错误模块 + +```bash +python -m pytest tests/test_dsl_errors.py -q +``` + +重点回答: + +- `DSLSyntaxError` 的字段哪些是 1-based? +- `format_error()` 如何计算 caret 长度? +- `ErrorCollector` 达到上限后怎样计数? +- 自动建议来自显式 `fix_hint` 还是启发式规则? + +### 第三步:追踪 CLI 数据流 + +```text +scratchv/main.py + → CompilerDriver.compile() + → CompilerDriver._parse() + → ExtendedDSLParser 或 DSLParser + → CompileResult.diagnostics / diagnostic_limit_reached / errors + → stderr +``` + +特别关注 `_parse()` 中捕获所有异常再回退的逻辑。它是错误信息被覆盖的主要风险点。 + +### 第四步:阅读设计约束 + +阅读 [设计文档](09-DSL错误提示美化器-设计文档.md) 的第 3、5、7、9、12 和 16 节,再开始编码。 + +--- + +## 5. 建议目录与改动范围 + +后续实现建议只修改与课题直接相关的文件: + +```text +scratchv/frontend/ +├── dsl_errors.py # 扩展位置、渲染和收集语义 +├── dsl_grammar.py # 拟新增:Parser/Validator 共享语法与算子签名 +├── dsl_validator.py # 拟新增:无 IR 副作用的验证器 +├── dsl_parser.py # 接入 SourceBuffer/validator +├── dsl_extended.py # 接入控制流块验证 +└── __init__.py # 必要时导出稳定公共接口 + +scratchv/ +├── compiler.py # 保留结构化诊断,收窄回退条件 +└── main.py # 终端颜色与 stderr 输出 + +tests/ +├── test_dsl_errors.py +├── test_dsl_validator.py # 拟新增 +├── test_parser.py +├── test_dsl_extended.py +└── test_dsl_diagnostics_cli.py # 拟新增端到端测试 +``` + +不要在本课题中顺便重构 IR、优化器或后端。 + +--- + +## 6. 开发策略 + +采用测试先行和小步集成。每一步都应保持正确 DSL 可编译,不能等到最后一次性跑测试。 + +建议顺序: + +```text +锁定输出契约 + → SourceBuffer + → 错误继承兼容 + → 基础行级验证 + → 算子签名验证 + → 扩展块验证 + → Parser 集成 + → CompilerDriver/CLI 集成 + → 多错误与颜色策略 + → 全量回归 +``` + +--- + +## 7. 阶段一:锁定诊断输出契约 + +### 7.1 先写失败测试 + +为以下输出建立无颜色精确测试: + +```text +bad.dsl:5:1: error[E100]: cannot parse statement + 5 | retrun result + | ^~~~~~ +note: did you mean 'return'? +``` + +至少断言: + +- 文件名; +- 1-based 行列; +- 错误码; +- 源码行; +- caret 起点和长度; +- 无 ANSI 转义码。 + +### 7.2 保持旧接口 + +已有调用仍应工作: + +```python +DSLSyntaxError(1, 1, "message") +format_error(error, use_color=False) +ErrorCollector(filename="test.dsl", max_errors=20) +``` + +不要为了新设计删除现有参数或更改已有字段顺序。 + +### 7.3 完成标准 + +- 新输出契约测试失败的原因是功能尚未实现,而不是测试写错; +- 现有 `tests/test_dsl_errors.py` 仍通过; +- 文档中的示例与测试期望完全一致。 + +--- + +## 8. 阶段二:实现 SourceBuffer + +### 8.1 目的 + +解析器当前过早执行 `strip()`,导致缩进和原始列信息丢失。`SourceBuffer` 应成为唯一的源码位置来源。 + +建议接口: + +```python +@dataclass(frozen=True) +class SourceBuffer: + text: str + filename: str = "" + + def line_text(self, line: int) -> str: + ... +``` + +### 8.2 测试矩阵 + +| 输入 | 预期 | +|------|------| +| `a\nb` | 第 1 行 `a`,第 2 行 `b` | +| `a\r\nb` | 与 LF 行号一致 | +| 空字符串 | 不越界,不虚构源码 | +| 末尾换行 | 不产生错误的额外语句 | +| 中文标识符或注释 | 列号按 Python 字符索引计算 | +| 制表符 | 原始列稳定,渲染时 caret 对齐 | + +### 8.3 常见错误 + +- 在保存原始行之前调用 `strip()`; +- 混用 0-based 内部索引和 1-based 用户位置; +- 用字节偏移计算 Unicode 列号; +- 只在扩展解析器保留空行,基础解析器仍丢失行号。 + +--- + +## 9. 阶段三:统一异常兼容关系 + +当前 `DSLParseError` 定义在 `dsl_parser.py`,`DSLSyntaxError` 独立继承 `Exception`。直接改为抛出新异常可能破坏捕获 `DSLParseError` 的调用方和测试。 + +建议目标: + +```python +try: + DSLParser().parse(bad_source) +except DSLParseError as error: + assert isinstance(error, DSLSyntaxError) +``` + +实现时应避免 `dsl_parser.py` 与 `dsl_errors.py` 的循环导入。可选择: + +1. 把兼容基类移到 `dsl_errors.py`,再从旧路径重新导出; +2. 把共享异常基类放进一个小型无依赖模块。 + +不建议通过捕获任意异常再用字符串包装的方式伪造兼容,因为它会丢失原始错误类别和位置。 + +--- + +## 10. 阶段四:基础 DSL 验证器 + +### 10.1 为什么不能在错误后继续构建 IR + +一条语句可能已经调用了部分 `IRBuilder` 方法才失败。继续解析会让 builder 状态不可预测。因此验证器只检查源码,不创建 `Value`、Block 或 Program。 + +### 10.2 建议验证顺序 + +对每个物理行: + +1. 跳过空行和注释; +2. 判断语句类别; +3. 检查外层结构; +4. 检查关键字和块栈; +5. 检查算子是否存在; +6. 检查位置参数与关键字参数; +7. 记录诊断或进入下一行。 + +### 10.3 使用 `fullmatch` + +验证语法时优先使用 `fullmatch`,避免只匹配行首后忽略尾部垃圾。例如: + +```text +x = add(a, b) unexpected +``` + +不能因为前半段匹配成功而被当成合法语句。 + +### 10.4 算子签名表 + +不要在多个 `if/elif` 中重复参数规则。建议集中描述: + +```python +OP_SIGNATURES = { + "add": OpSignature(positional=2), + "relu": OpSignature(positional=1), + "softmax": OpSignature(positional=1, optional_kwargs={"axis"}), + "matmul": OpSignature( + positional=2, + optional_kwargs={"rows", "cols", "inner", "m", "n", "k"}, + ), +} +``` + +`OP_SIGNATURES`、语句正则和关键字集合必须放在 Parser 与 Validator 都导入的共享模块中,不能各自维护副本。Parser 的执行 handler 可以独立存在,但必须增加自动测试:`set(OP_SIGNATURES) == set(OP_HANDLERS)`。这样新增算子时,只要漏改一侧,测试会立即失败。 + +### 10.5 完成标准 + +- 不支持算子不再泄漏普通 `DSLParseError` 文本; +- 参数不足不再泄漏 `IndexError`; +- 一行结构错误不会产生多个级联错误; +- 正确基础 DSL 的 Program 与基线一致。 + +--- + +## 11. 阶段五:扩展 DSL 块验证 + +### 11.1 块栈 + +使用独立的验证栈,不复用 IR builder 的 `_loop_stack`: + +```python +@dataclass +class BlockFrame: + kind: Literal["if", "while", "for"] + line: int + col: int + saw_else: bool = False +``` + +### 11.2 必测规则 + +- `endif` 只关闭 `if`; +- `endwhile` 只关闭 `while`; +- `endfor` 只关闭 `for`; +- `else` 必须位于 `if` 内; +- 同一个 `if` 只能有一个 `else`; +- 文件结束时每个未关闭块都报告开始位置; +- 嵌套块错误不能破坏后续独立语句的位置。 + +结束符类型不匹配统一使用 `E110`。例如 `while ... endif` 应在 `endif` 处报告“期望 `endwhile`”,随后把该 `endif` 作为 `while` 的恢复性结束符并弹栈,EOF 不重复报告 `E111`。对于 `if ... while ... endif`,报告一个 `E110` 后弹出内层 `while` 和匹配的外层 `if`。只有真正留到 EOF 的块才报告 `E111`。 + +### 11.3 不要静默接受文件结尾 + +当前扩展解析流程在找不到结束关键字时可能走到文件尾。验证器必须把这种情况转为 `E111`,并指向块开始处,而不是最后一行。 + +--- + +## 12. 阶段六:接入 Parser + +### 12.1 成功路径 + +成功调用保持不变: + +```python +program = DSLParser().parse(source) +program = ExtendedDSLParser().parse(source) +``` + +建议增加 keyword-only 文件名: + +```python +program = DSLParser().parse(source, filename="model.dsl") +``` + +### 12.2 失败路径 + +默认 `parse()` 在验证失败时抛出第一个 `DSLSyntaxError`。它不返回 Program,也不进入 IR 生成阶段。 + +多错误调用使用显式入口: + +```python +collector = ExtendedDSLParser().validate( + source, + filename="bad.dsl", + max_errors=20, +) + +if collector.has_errors: + print(collector.report(), file=sys.stderr) +``` + +### 12.3 Parser 状态重置 + +每次 `parse()` 前确认 builder、变量表、循环栈和标签计数器处于干净状态。诊断集成不能让同一个 parser 实例第二次解析时继承上次失败状态。 + +--- + +## 13. 阶段七:接入 CompilerDriver 与 CLI + +### 13.1 收窄解析器回退 + +当前 `_parse()` 会捕获扩展解析器的任意异常后尝试基础解析器。修改时应遵循: + +- 用户语法错误直接返回,不回退; +- 由于扩展解析器继承基础语法,编译器驱动优先统一使用 `ExtendedDSLParser`;如需保留两种模式,使用显式配置选择,不扫描关键字猜测; +- 只有明确的“解析器不适用”状态允许回退; +- `_parse()` 不捕获 `IndexError`、`AssertionError` 等实现错误。 + +还要修改外层 `CompilerDriver.compile()`:只把 `DSLSyntaxError` 等已知用户错误转换为失败结果,意外异常继续抛出,让库测试直接失败。CLI 顶层负责在正常模式输出 `internal compiler error` 并返回 2;只有显式调试模式打印 traceback。 + +### 13.2 CompileResult + +建议给 `CompileResult` 增加 `diagnostics: list[DSLSyntaxError]`、`diagnostic_limit_reached: bool` 和 `diagnostic_limit: int`。结构化诊断供 CLI 最终渲染,后两个字段把错误抑制状态从 collector 传到 renderer;`errors` 继续保存完整的无颜色文本,兼容现有调用方。`diagnostics` 与 `errors` 同时存在时,CLI 只渲染 `diagnostics`,避免重复输出,但仍根据 `diagnostic_limit_reached` 输出 footer。 + +不要再次添加模糊前缀: + +```text +不建议:Parse error: bad.dsl:5:1: error... +建议: bad.dsl:5:1: error[E100]: ... +``` + +避免出现 `Error: Parse error: ...` 的重复层级。 + +### 13.3 CLI + +CLI 负责: + +- 把诊断写到 stderr; +- 失败返回 1; +- 把 `sys.stderr` 传给 renderer,终端交互时允许颜色; +- 输出重定向或 `NO_COLOR` 存在时关闭颜色; +- 不打印 traceback,除非用户显式启用调试模式。 + +建议的手工验收命令: + +```bash +scratchv bad.dsl -o output.s +scratchv bad.dsl -o output.s 2> error.txt +``` + +检查 `error.txt` 中没有 `\x1b[` ANSI 序列。 + +--- + +## 14. 阶段八:多错误收集 + +### 14.1 只恢复到可信边界 + +基础 DSL 的可信边界是下一物理行;扩展 DSL 还包括 `else`、`endif`、`endwhile` 和 `endfor`。 + +不要在参数列表中盲目寻找下一个逗号后继续,因为当前解析器不是 token 流,容易把后续字符误认为新语句。 + +### 14.2 错误上限 + +建议默认最多报告 20 个真实错误。达到上限后: + +- 停止继续验证; +- 设置 `collector.limit_reached = True`; +- renderer 输出一条无源码位置的 `note: error limit ...` footer; +- 转换为 `CompileResult` 时同步复制 `diagnostic_limit_reached` 和 `diagnostic_limit`; +- 标题中的错误数量仍为 20; +- 不把抑制消息放进 `errors`,也不当作第 21 个源码错误。 + +### 14.3 排序和去重 + +报告按 `(line, col, error_code)` 排序。同一位置、同一错误码、同一消息只保留一次。 + +--- + +## 15. 阶段九:修复建议 + +### 15.1 建议来源 + +```text +显式上下文提示 > 精确拼写表 > 错误码固定提示 > 无提示 +``` + +示例: + +| 输入 | 错误 | 建议 | +|------|------|------| +| `retrun x` | 未识别关键字 | `did you mean 'return'?` | +| `x = ad(a, b)` | 不支持算子 | `did you mean 'add'?` | +| `if (a > b` | 缺少右括号 | `add the missing ')'` | +| 随机未知单词 | 未识别语句 | 不猜测 | + +### 15.2 误报测试 + +不仅要测试“应该出现建议”,也要测试“这里不应出现建议”。例如变量名与关键字相似时,不应建议把变量改成关键字。 + +--- + +## 16. 测试计划 + +### 16.1 快速测试 + +开发过程中每个小步骤运行: + +```bash +python -m pytest tests/test_dsl_errors.py tests/test_dsl_validator.py -q +``` + +### 16.2 解析器回归 + +```bash +python -m pytest \ + tests/test_parser.py \ + tests/test_dsl_extended.py \ + tests/test_dsl_errors.py \ + tests/test_dsl_validator.py \ + tests/test_dsl_diagnostics_cli.py -q +``` + +Windows PowerShell 可把路径放在同一行执行。 + +```powershell +python -m pytest tests/test_parser.py tests/test_dsl_extended.py tests/test_dsl_errors.py tests/test_dsl_validator.py tests/test_dsl_diagnostics_cli.py -q +``` + +### 16.3 项目验证 + +```bash +python .Codex/harness/verify/run.py --level L1 +python .Codex/harness/verify/run.py --level L2 +``` + +如果本地专属 harness 不存在,应明确记录环境缺失,并运行仓库可用的等价检查: + +```bash +make test +python scripts/build_docs_html.py --output-dir benchmark_reports/docs +``` + +不能因为 harness 缺失就声称 L2 已通过。 + +### 16.4 测试用例清单 + +| 类别 | 最少用例 | +|------|----------| +| 正确基础 DSL | 算术、一元算子、matmul、for | +| 正确扩展 DSL | if/else、while、嵌套块 | +| 行列定位 | 首行、中间行、缩进、CRLF、Unicode、tab | +| 语句结构 | 缺赋值号、括号、逗号、冒号、尾部垃圾 | +| 块结构 | 多余结束符、错误类型结束符、缺失结束符、重复 else | +| 算子签名 | 未知算子、参数不足、参数过多、未知 kwarg、非法值 | +| 多错误 | 3 个独立错误、级联抑制、达到上限 | +| 输出 | ANSI 开关、`NO_COLOR`、stderr、退出码 | +| 兼容性 | `DSLParseError` 捕获、原 parse 调用、正确 IR 不变 | + +--- + +## 17. 调试指南 + +### 17.1 caret 偏移一列 + +检查: + +- 内部索引是否 0-based; +- 对外 `col` 是否 1-based; +- 行号前缀宽度是否计入了源码列; +- 原始行是否被 `strip()`; +- tab 是否经过显示宽度映射。 + +不要通过随意加减常数修复单个样例,应先写多个不同列位置的参数化测试。 + +### 17.2 错误行号总是 1 + +确认基础解析器没有使用 `text.strip().split("\n")` 后丢弃原始索引。应对原始物理行执行 `enumerate(..., start=1)`。 + +### 17.3 扩展错误变成普通解析错误 + +检查 `CompilerDriver._parse()` 是否仍然捕获所有异常并回退。结构化用户错误不应触发回退。 + +### 17.4 一处错误产生很多错误 + +验证器可能在外层结构失败后继续执行参数检查。为每行定义“主要结构错误后停止本行”的规则。 + +### 17.5 正确 DSL 的 IR 改变 + +错误诊断改动不应改变成功语义。比较改动前后的 IRPrinter 输出和指令结构,定位验证阶段是否误改了 parser 或 builder 状态。 + +--- + +## 18. 代码评审清单 + +### 正确性 + +- [ ] 所有行列均为 1-based,并有边界测试。 +- [ ] 原始源码行在位置计算前未被破坏。 +- [ ] 有错误时不生成或返回部分 IR。 +- [ ] 扩展语法错误不会被回退逻辑覆盖。 +- [ ] 用户错误不会泄漏 Python 内部异常。 +- [ ] 错误码与设计文档一致。 + +### 兼容性 + +- [ ] `DSLParser().parse(text)` 成功调用保持不变。 +- [ ] `DSLParseError` 旧捕获方式仍有效。 +- [ ] `format_error()` 现有参数仍可使用。 +- [ ] 正确 DSL 的 IR 与基线一致。 + +### 用户体验 + +- [ ] 消息说明问题而不是描述实现细节。 +- [ ] caret 指向真正错误 token。 +- [ ] 修复建议可信且没有明显误报。 +- [ ] 非 TTY 输出不含 ANSI。 +- [ ] 多错误报告没有明显级联噪音。 + +### 测试 + +- [ ] 单元、解析器集成、CompilerDriver 和 CLI 均有覆盖。 +- [ ] LF、CRLF、Unicode、tab 和空文件有覆盖。 +- [ ] 错误上限和去重有覆盖。 +- [ ] L1、L2 或明确记录的等价验证已执行。 + +--- + +## 19. 12 周开发计划 + +| 周次 | 目标 | 可验收产物 | +|------|------|------------| +| W1 | 熟悉 DSL、IRBuilder 和当前错误路径 | 调研笔记、3 个基线错误样例 | +| W2 | 锁定纯文本输出和错误码 | 失败测试、输出规范 | +| W3 | 实现并测试 SourceBuffer | LF/CRLF/Unicode/tab 单元测试 | +| W4 | 统一异常继承和兼容导出 | `DSLParseError` 兼容测试 | +| W5 | 实现基础语句结构验证 | `E100`–`E103` 测试 | +| W6 | 实现算子签名验证 | `E200`–`E203` 测试 | +| W7 | 实现 if/while/for 块栈验证 | `E110`–`E112` 测试 | +| W8 | 接入基础和扩展解析器 | 正确 IR 回归、结构化首错 | +| W9 | 实现多错误恢复、去重和上限 | 3 错误样例、`limit_reached` footer 测试 | +| W10 | 接入 CompilerDriver 和 CLI | stderr、退出码、无 traceback 测试 | +| W11 | 完善颜色、建议和边界用例 | TTY/NO_COLOR、误报测试 | +| W12 | 全量验证、性能测量和文档收尾 | L2 结果、评审报告、演示样例 | + +--- + +## 20. 交付产物 + +建议最终提交包含: + +- 结构化错误与渲染改进; +- `SourceBuffer` 和无副作用 DSL validator; +- 基础、扩展解析器集成; +- CompilerDriver 和 CLI 集成; +- 单元、集成、端到端测试; +- 至少 10 个错误 DSL 示例及预期输出; +- 错误码参考表; +- L1/L2 或等价验证记录; +- 性能测量结果; +- 最终 self-review 报告。 + +--- + +## 21. 最终验收演示 + +准备一个包含三个独立错误的 `bad.dsl`: + +```text +x = ad(a, b) +y = relu(x, 1) +endwhile +``` + +期望演示: + +1. `validate()` 一次报告 3 个错误; +2. 每个错误的位置和错误码正确; +3. `ad` 获得可信的 `add` 拼写建议; +4. `relu` 参数数量错误不泄漏 `IndexError`; +5. `endwhile` 报告无匹配开始块; +6. 编译流程失败且不产生输出汇编; +7. 重定向到文件时没有 ANSI; +8. 修正三处错误后,同一程序正常生成 IR 和目标代码。 + +这组演示同时覆盖位置、建议、签名检查、块检查、多错误、CLI 和成功回归,是本课题最小但完整的验收闭环。 diff --git "a/docs/topics/09-DSL\351\224\231\350\257\257\346\217\220\347\244\272\347\276\216\345\214\226\345\231\250-\350\256\276\350\256\241\346\226\207\346\241\243.md" "b/docs/topics/09-DSL\351\224\231\350\257\257\346\217\220\347\244\272\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 100644 index 0000000..c21faef --- /dev/null +++ "b/docs/topics/09-DSL\351\224\231\350\257\257\346\217\220\347\244\272\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,588 @@ +# 课题9:DSL错误提示美化器设计文档 + +> **文档类型**:技术设计 | **状态**:草案 | **版本**:v0.1 +> **调研基线**:`main@997d2aa` | **调研日期**:2026-07-15 +> **关联模块**:`scratchv/frontend/dsl_errors.py`、`dsl_parser.py`、`dsl_extended.py`、`scratchv/compiler.py` + +--- + +## 1. 文档定位 + +本文描述 ScratchV DSL 错误提示美化器的**拟议迭代方案**,用于指导后续开发和评审。 + +本文不是已完成实现的说明。当前仓库已经具备独立的错误对象、文本格式化器和错误收集器,但尚未把这些能力完整接入基础 DSL 解析器、扩展 DSL 解析器和编译器驱动。文中标为“建议”“拟新增”的接口均属于设计提案。 + +配套文档: + +- [课题9:DSL错误提示美化器](09-DSL错误提示美化器.md):现有课程概览 +- [课题9:DSL错误提示美化器开发文档](09-DSL错误提示美化器-开发文档.md):建议开发顺序、测试方法和交付标准 +- [课题1:DSL前端增强器](01-DSL前端增强器.md):扩展 DSL 的控制流语法 + +--- + +## 2. 背景与问题 + +ScratchV 提供两套 DSL 解析器: + +- `DSLParser`:解析逐行表达式、`return` 和 `for/endfor`。 +- `ExtendedDSLParser`:在基础语法上增加 `if/else/endif` 和 `while/endwhile`。 + +当前解析失败时,用户通常只能得到类似下面的消息: + +```text +Error: Parse error: Cannot parse line: retrun result +``` + +这条消息没有文件名、行号、列号和修复建议。更重要的是,编译器驱动会先尝试扩展解析器,并用宽泛的 `except Exception` 回退到基础解析器。扩展解析器产生的原始错误可能被覆盖,最终报告与真正失败位置不一致。 + +目标体验如下: + +```text +bad.dsl:5:1: error[E100]: cannot parse statement + 5 | retrun result + | ^~~~~~ +note: did you mean 'return'? +``` + +好的诊断应回答四个问题: + +1. 哪个文件、哪一行、哪一列出错? +2. 编译器实际发现了什么问题? +3. 哪段源码与问题直接相关? +4. 用户下一步应该怎样修复? + +--- + +## 3. 当前实现审计 + +### 3.1 已有能力 + +| 位置 | 当前能力 | 结论 | +|------|----------|------| +| `dsl_errors.py` | `DSLSyntaxError` 保存行、列、消息、源码行、文件名、提示和错误码 | 可复用 | +| `dsl_errors.py` | `format_error()` 输出 gcc/clang 风格文本并支持 ANSI 颜色 | 可复用,需补充自动颜色策略 | +| `dsl_errors.py` | `ErrorCollector` 支持收集、上限、报告和清空 | 可复用,需明确恢复与计数语义 | +| `dsl_errors.py` | `_SUGGESTIONS` 和 `_COMMON_FIXES` 提供少量启发式建议 | 可复用,需避免误报 | +| `frontend/__init__.py` | 导出 `DSLSyntaxError`、`format_error` 和 `ErrorCollector` | 已形成部分公共接口 | +| `tests/test_dsl_errors.py` | 覆盖错误对象、颜色、格式化、收集器和工厂函数 | 单模块测试基础较好 | + +### 3.2 主要差距 + +| 差距 | 当前表现 | 用户影响 | +|------|----------|----------| +| 解析器未集成 | `DSLParser` 和 `ExtendedDSLParser` 仍抛出 `DSLParseError` 或底层异常 | 美化器只可手工调用 | +| 原始位置丢失 | 基础解析器对每行执行 `strip()`,没有保存原始行号和缩进 | 无法可靠计算列号 | +| 扩展错误被吞掉 | `CompilerDriver._parse()` 捕获扩展解析器的所有异常后回退 | 真正错误原因可能被替换 | +| 参数错误泄漏 | 参数数量不足可能触发 `IndexError` | 用户看到 Python 内部异常 | +| 块结构不完整 | 缺少 `endif` 或 `endwhile` 时可能静默到文件结尾 | 错误程序可能产生不完整 IR | +| 多错误仅有容器 | `ErrorCollector` 能保存多个错误,但解析器没有同步和恢复机制 | 遇到首错仍无法继续 | +| 上下文不完整 | `context_lines` 没有完整源文件,只能输出空的上下文行号 | 无法显示真实前后文 | +| 颜色默认开启 | `use_color=True` 不检查 TTY 或 `NO_COLOR` | 重定向到文件时出现转义码 | +| 测试缺少端到端覆盖 | 没有“错误 DSL → 解析器 → CompileResult/CLI”测试 | 集成回归无法被发现 | + +### 3.3 语义约束 + +基础解析器的 `_resolve()` 会把首次出现的名字创建为输入值,因此当前 DSL 没有严格的“变量未定义”错误语义。错误美化器不应在未改变语言规则的前提下报告“未定义变量”。如需该能力,应另立语言语义课题。 + +--- + +## 4. 设计目标与非目标 + +### 4.1 设计目标 + +1. 基础和扩展 DSL 的解析错误均携带稳定、准确的位置。 +2. 默认库调用保持失败即停止,避免返回被错误污染的 IR。 +3. 显式验证模式可以一次报告多个相互独立的错误。 +4. 所有用户输入错误转换为结构化诊断,不暴露 `IndexError`、`KeyError` 等实现异常。 +5. 纯文本输出稳定,适合单元测试、CI、日志和文件重定向。 +6. 现有 `DSLParser().parse(text)` 的成功路径和 IR 结果保持兼容。 +7. 错误码、位置规则和恢复规则可测试、可扩展。 + +### 4.2 非目标 + +本课题不包含: + +- 重写完整词法器或引入第三方解析框架; +- 修改 DSL 的变量定义、类型检查或算子语义; +- 自动修改用户源码; +- LSP、编辑器插件或 IDE 实时诊断; +- JSON/SARIF 报告协议; +- 修改 ONNX 解析器的错误体系; +- 在语法错误存在时生成或执行部分 IR。 + +--- + +## 5. 设计原则 + +### 5.1 先验证,后生成 IR + +当前解析器在读取源码的同时调用 `IRBuilder`。如果在错误后强行继续,builder 中可能残留半个循环或基本块,后续错误也容易成为连锁误报。 + +因此多错误模式采用“两阶段”策略: + +```text +源文件 ──▶ 轻量语法验证 ──▶ 0 个错误? ──是──▶ 现有 IR 生成流程 + │ + └────────否──▶ ErrorCollector ──▶ 格式化报告 +``` + +验证阶段只检查可从源码直接确定的规则,不创建 IR。只有验证通过后才进入现有解析和 IR 构建流程。 + +### 5.2 精确错误优先于大量错误 + +多错误收集不是越多越好。一个缺失的右括号可能导致同一行出现多个派生错误。验证器应在确认一条语句的外层结构错误后停止分析该语句,只在下一条独立语句继续。 + +### 5.3 公共接口渐进兼容 + +现有 `dsl_errors.py` 的公开字段和常用函数继续保留。新增字段必须提供默认值;成功解析的调用方式不变;旧的 `DSLParseError` 导入路径继续有效。 + +--- + +## 6. 总体架构 + +```text +┌──────────────────────────────────────────────────────────────┐ +│ DSL source / filename │ +└─────────────────────────────┬────────────────────────────────┘ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ SourceBuffer │ +│ 保留原始文本、物理行、文件名;负责 offset/line/column 映射 │ +└─────────────────────────────┬────────────────────────────────┘ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ DSLValidator │ +│ 行级语法、算子签名、控制流块栈;不调用 IRBuilder │ +└───────────────┬───────────────────────────────┬──────────────┘ + │ 诊断 │ 无诊断 + ▼ ▼ +┌────────────────────────────┐ ┌───────────────────────────┐ +│ ErrorCollector │ │ DSLParser / Extended... │ +│ 去重、排序、上限、抑制提示 │ │ 复用现有 IR 生成流程 │ +└───────────────┬────────────┘ └─────────────┬─────────────┘ + ▼ ▼ +┌────────────────────────────┐ ┌───────────────────────────┐ +│ DiagnosticRenderer │ │ Program │ +│ ANSI / plain text │ │ 后续优化与代码生成 │ +└────────────────────────────┘ └───────────────────────────┘ +``` + +### 6.1 组件职责 + +| 组件 | 职责 | 不负责 | +|------|------|--------| +| `SourceBuffer` | 保存原始源码、获取行文本、映射位置 | 判断语法是否正确 | +| `DSLValidator` | 产生结构化错误并执行有限同步 | 创建 IR、修复源码 | +| `DSLSyntaxError` | 表示一个诊断 | 读取文件、打印到终端 | +| `ErrorCollector` | 收集、去重、排序和限制诊断 | 决定解析恢复点 | +| `format_error()` | 把单个诊断渲染为文本 | 推断语言语义 | +| `DSLParser` | 验证通过后生成 IR | 在错误状态下返回部分 Program | +| `CompilerDriver` | 选择解析器并把诊断交给 `CompileResult` | 吞掉异常后盲目回退 | + +--- + +## 7. 数据模型设计 + +### 7.1 SourceBuffer(拟新增内部类型) + +```python +@dataclass(frozen=True) +class SourceBuffer: + text: str + filename: str = "" + + def line_text(self, line: int) -> str: ... + def line_count(self) -> int: ... +``` + +约束: + +- 行号和列号均从 1 开始。 +- 列号以 Python 字符索引为基础,即按 Unicode code point 计数。 +- 保留原始行,不在位置计算前执行 `strip()`。 +- 渲染器把制表符按 4 列展开,但错误对象中的列号仍指向原始字符位置。caret 的显示列必须逐字符换算:普通字符增加 1 列,tab 增加到下一个 4 列制表位,不能直接把 raw column 当作显示空格数。 +- Windows `\r\n` 与 Unix `\n` 均映射到相同的物理行。 + +### 7.2 DSLSyntaxError(兼容扩展) + +建议保留现有必需字段,并增加可选的结束列: + +```python +@dataclass +class DSLSyntaxError(DSLParseError): + line: int + col: int + message: str + source_line: str = "" + filename: Optional[str] = None + fix_hint: Optional[str] = None + error_code: Optional[str] = None + end_col: Optional[int] = None # 1-based, exclusive +``` + +兼容策略: + +- `DSLSyntaxError` 继承或等价兼容 `DSLParseError`,保留旧代码的捕获行为。 +- 新字段放在已有字段之后并提供默认值。 +- `end_col is None` 时继续使用当前 token 长度估算。 +- `__str__()` 始终输出无颜色文本,避免异常字符串携带终端控制码。 + +### 7.3 验证结果 + +多错误收集使用显式验证入口,避免改变 `parse()` 的返回类型: + +```python +def validate( + self, + text: str, + *, + filename: Optional[str] = None, + max_errors: int = 20, +) -> ErrorCollector: ... +``` + +调用约定: + +- `validate()` 不生成 IR。 +- `collector.has_errors` 为真时,调用者不得继续编译。 +- `parse()` 可先调用同一验证逻辑;发现错误时抛出第一个 `DSLSyntaxError`。 +- CLI 或教学工具需要一次展示多个错误时,先调用 `validate()`,再调用 `report()`。 + +### 7.4 编译结果中的结构化诊断 + +CLI 需要根据实际 stderr 是否为 TTY 决定颜色,因此 `CompilerDriver` 不能只返回已经格式化的字符串。建议为 `CompileResult` 增加兼容字段: + +```python +@dataclass +class CompileResult: + # 已有字段保持不变 + errors: list[str] + diagnostics: list[DSLSyntaxError] = field(default_factory=list) + diagnostic_limit_reached: bool = False + diagnostic_limit: int = 20 +``` + +数据流约定: + +- `diagnostics` 保存结构化错误,是 CLI 渲染的首选数据源。 +- `diagnostic_limit_reached` 和 `diagnostic_limit` 把 collector 的抑制状态传到 CLI,保证 renderer 能输出 footer。 +- `errors` 保留无 ANSI 文本,兼容现有库调用方和序列化逻辑。 +- 两个字段同时存在时,CLI 只渲染 `diagnostics`,不得重复打印 `errors`。 +- 只有无法表示为 DSL 诊断的预期业务错误才仅写入 `errors`。 +- 错误上限属于报告元数据,不伪造为带 `line=0, col=0` 的 `DSLSyntaxError`。 + +--- + +## 8. 错误分类与错误码 + +错误码用于测试、文档检索和未来扩展。消息文字可以改进,错误码语义必须保持稳定。 + +| 错误码 | 分类 | 触发条件 | 建议高亮 | +|--------|------|----------|----------| +| `E100` | 未识别语句 | 整行不符合任何 DSL 语句 | 第一个非空 token | +| `E101` | 括号不配对 | 调用或条件缺少左右括号 | 缺失点或多余括号 | +| `E102` | 缺少分隔符 | 赋值号、逗号或控制流冒号缺失 | 邻近 token | +| `E103` | 非法标识符 | 目标名或循环变量不符合规则 | 完整标识符 | +| `E110` | 块结束符不匹配 | `endif`、`endwhile`、`endfor` 无匹配开始,或与当前栈顶块类型不一致 | 结束关键字 | +| `E111` | 块未闭合 | 到文件结尾仍存在打开的块 | 对应开始关键字 | +| `E112` | `else` 位置错误 | 没有对应 `if` 或同一块重复 `else` | `else` token | +| `E200` | 不支持的算子 | 算子名不在注册表 | 算子名 | +| `E201` | 参数数量错误 | 位置参数数量不符合算子签名 | 调用参数区 | +| `E202` | 关键字参数错误 | 未知、重复或缺失的 kwarg | 参数名 | +| `E203` | 参数值错误 | `rows`、`axis` 等需要数值但格式非法 | 参数值 | + +不在本课题中使用的错误: + +- “变量未定义”:现有语言把首次出现的名字视为输入值。 +- 类型不匹配:当前 DSL 前端没有完整类型检查阶段。 +- IR 验证错误:应由 `IRVerifier` 报告,而不是 DSL 语法层报告。 + +--- + +## 9. 验证与错误恢复 + +### 9.1 基础 DSL + +基础 DSL 以物理行为自然同步边界。单行验证流程建议如下: + +```text +跳过空行/注释 + ├─ for 语句 → 检查变量、范围和 block stack + ├─ endfor → 检查栈顶是否为 for + ├─ return → 检查关键字后是否具有非空操作数 + └─ assignment → 检查 lhs、op、括号、参数与 kwargs +``` + +如果一行外层结构已经错误,验证器记录一条主要错误后直接进入下一物理行,不继续检查该行的算子参数。 + +### 9.2 扩展 DSL + +扩展语法使用块栈: + +```python +BlockFrame(kind="if", line=4, saw_else=False) +BlockFrame(kind="while", line=9) +BlockFrame(kind="for", line=12) +``` + +同步规则: + +1. 普通语句失败后跳到下一物理行。 +2. `else` 只与最近且尚未出现 `else` 的 `if` 匹配。 +3. 结束关键字与栈顶匹配时正常弹栈;栈为空时报告 `E110` 后继续。 +4. 栈非空但类型不匹配时,在当前结束符报告一个 `E110`,消息同时写明“发现什么”和“期望什么”。为避免 EOF 级联:若该结束符能匹配更外层块,则弹出到该外层块(含它);若栈中没有可匹配块,则把它作为栈顶块的恢复性结束符并弹出栈顶。被恢复弹出的块不再报告 `E111`。 +5. 到达文件尾时,只为仍留在栈中的每个未闭合块报告 `E111`。 +6. 达到错误上限后停止验证,并把 `collector.limit_reached` 设为真;抑制提示作为报告 footer 输出,不加入 `errors` 列表。 + +恢复示例: + +| 源码结构 | 诊断序列 | 恢复后栈 | +|----------|----------|----------| +| `while ... endif` | 当前 `endif` 报 1 个 `E110`:期望 `endwhile` | 弹出 `while`,EOF 不再报 `E111` | +| `if ... while ... endif` | 当前 `endif` 报 1 个 `E110`:应先出现 `endwhile` | 弹出 `while` 和匹配的 `if` | +| `if ... while ... EOF` | `while`、`if` 的开始位置各报 1 个 `E111` | 文件结束 | + +### 9.3 去重和级联抑制 + +建议用以下键去重: + +```text +(filename, line, col, error_code, message) +``` + +同一行最多报告一个结构错误和一个独立的算子签名错误。由结构错误直接导致的后续错误不再报告。 + +--- + +## 10. 修复建议策略 + +修复建议应当保守。错误建议错误时,比没有建议更影响用户判断。 + +优先级从高到低: + +1. 解析器在明确上下文中提供的 `fix_hint`; +2. 精确拼写表,例如 `retrun → return`; +3. 基于错误码的固定建议,例如 `E101 → add the missing ')'`; +4. 没有足够信息时不输出建议。 + +建议规则: + +- 拼写建议仅比较同类关键字或算子名。 +- 不对任意源代码单词做全局替换建议。 +- 不建议会改变程序语义的操作。 +- 提示文字不参与控制流判断;逻辑只依赖错误码和结构化字段。 + +--- + +## 11. 渲染设计 + +### 11.1 纯文本格式 + +```text +{filename}:{line}:{col}: error[{code}]: {message} + {line_width} | {source_line} + {padding} | {caret_and_tildes} +note: {fix_hint} +``` + +约束: + +- 文件名未知时显示 ``,不输出空位置前缀。 +- 行号宽度按本次报告的最大行号计算。 +- `end_col` 存在时按跨度绘制 `^~~~`;不存在时才估算 token 长度。 +- `source_line` 为空时不显示源码和 caret。 +- 测试和 `str(error)` 强制无颜色。 + +### 11.2 ANSI 颜色 + +保留 `format_error(err, use_color: bool)` 作为兼容的纯格式化函数;另提供面向输出流的 renderer: + +```python +def render_error( + err: DSLSyntaxError, + *, + stream: TextIO, + use_color: Optional[bool] = None, +) -> str: ... +``` + +`render_error()` 的颜色规则: + +- `True`:强制开启; +- `False`:强制关闭; +- `None`:仅当传入的 `stream.isatty()` 为真且未设置 `NO_COLOR` 时开启。 + +颜色只改变显示,不得改变可见字符内容、错误码或换行数量。 + +### 11.3 多错误报告 + +`ErrorCollector.report()` 应区分真实错误和抑制消息: + +```text +--- 3 error(s) found --- +... +note: error limit (3) reached; further errors suppressed +``` + +标题中的数量只统计真实源码错误。错误上限提示在验证阶段来自 `ErrorCollector.limit_reached`,进入编译驱动后复制到 `CompileResult.diagnostic_limit_reached`;它不占用错误位置,也不计入 `error_count`。 + +--- + +## 12. 解析器与编译器驱动集成 + +### 12.1 基础解析器 + +建议的成功路径保持不变: + +```python +program = DSLParser().parse(source) +``` + +拟议扩展: + +```python +program = DSLParser().parse(source, filename="model.dsl") +collector = DSLParser().validate(source, filename="model.dsl") +``` + +`_parse_line()` 应接收带原始位置的行上下文,而不是只接收 `strip()` 后的字符串。 + +### 12.2 扩展解析器 + +`ExtendedDSLParser` 与基础解析器共享 `SourceBuffer`、算子签名验证和错误工厂,只增加控制流块规则。不要复制一套错误消息和错误码。 + +Validator 与 Parser 还必须共享语法事实源,例如 `STATEMENT_PATTERNS`、`OP_SIGNATURES` 和关键字集合。Parser 的执行分派可以保留独立 handler,但其算子名集合必须由测试断言与 `OP_SIGNATURES` 完全一致,避免“验证通过、执行失败”或“Parser 接受、Validator 拒绝”。 + +### 12.3 CompilerDriver + +当前“扩展解析失败后捕获所有异常并尝试基础解析器”的策略需要移除或收窄: + +- 由于 `ExtendedDSLParser` 已继承基础语法,编译器驱动可统一使用扩展解析器;如果未来保留两种模式,则必须由显式配置选择,不能扫描关键字猜测,也不能通过异常回退选择; +- `DSLSyntaxError` 属于用户输入错误,必须原样保留,不得触发回退; +- 只有明确表示“该解析器不适用”的内部信号才允许回退; +- `CompileResult.diagnostics` 保存结构化诊断,`diagnostic_limit_reached`/`diagnostic_limit` 保存抑制状态,`errors` 保存兼容的无 ANSI 文本; +- CLI 把 `sys.stderr` 传给 renderer,由 renderer 根据该流的 TTY 状态决定颜色。 + +`CompilerDriver.compile()` 的异常边界也必须同步调整:它只捕获 `DSLSyntaxError` 等已知用户输入错误并生成失败的 `CompileResult`。`IndexError`、`AssertionError` 等意外异常不应被包装成普通 Parse error;库调用时让它们抛出以便测试发现,CLI 顶层再把它们转换为明确的 `internal compiler error` 和退出码 2。正常模式不打印 traceback,显式调试模式才打印。 + +这样可以避免扩展语法错误被基础解析器的次生错误覆盖。 + +--- + +## 13. 测试设计 + +### 13.1 单元测试 + +| 测试对象 | 重点 | +|----------|------| +| `SourceBuffer` | LF/CRLF、空文件、末尾换行、Unicode、制表符 | +| `DSLSyntaxError` | 兼容构造、继承关系、`end_col`、无颜色 `str()` | +| `format_error()` / renderer | 对齐、跨度、无文件名、无源码、指定 stream 的颜色自动策略 | +| `ErrorCollector` | 去重、排序、错误上限、真实错误计数、清空 | +| 建议规则 | 精确命中、大小写、无误报、显式提示优先 | + +### 13.2 解析器集成测试 + +至少覆盖: + +- 无法识别的语句; +- `retrun` 等拼写错误; +- 缺少左右括号; +- 不支持的算子; +- 一元、二元和带 kwarg 算子的参数数量错误; +- 多余 `endfor`、`endif`、`endwhile`; +- 缺失块结束符; +- `else` 无匹配或重复; +- 嵌套 `if/while/for` 的恢复; +- 同一文件多个独立错误; +- 正确 DSL 的 IR 与改动前一致。 + +### 13.3 端到端测试 + +```text +bad.dsl → CompilerDriver.compile() → CompileResult(success=False) +``` + +断言: + +- 包含文件名、行、列、错误码和源码行; +- 不包含 Python traceback、`IndexError` 或 `KeyError`; +- 扩展语法错误没有被基础解析器错误覆盖; +- CLI 失败退出码为 1; +- 重定向输出时没有 ANSI 转义码。 + +### 13.4 快照测试原则 + +只对纯文本格式使用快照。每个快照应尽量只包含一个概念,避免所有错误共用一个巨大 golden 文件。错误码和位置做精确断言,建议文本可以单独断言。 + +--- + +## 14. 兼容性与迁移 + +| 风险 | 兼容措施 | +|------|----------| +| 调用方捕获 `DSLParseError` | 让 `DSLSyntaxError` 保持其子类或兼容别名关系 | +| 调用方只传 `text` | `filename` 使用 keyword-only 可选参数 | +| 测试依赖无颜色 `str(e)` | `__str__()` 固定调用 `use_color=False` | +| 正确程序 IR 发生变化 | 增加改动前后 IR 快照或结构对比测试 | +| 旧格式没有错误码 | 错误码作为新增内容,文档标明输出格式版本变化 | +| 外部调用直接使用 `format_error()` | 保留现有参数,新增行为通过可选参数提供 | + +--- + +## 15. 性能要求 + +错误诊断不是编译热点,但验证阶段不能明显拖慢批量基准: + +- 源码扫描时间复杂度为 `O(n)`,`n` 为源码字符数。 +- 每行最多进行常数次正则匹配和算子表查询。 +- 拼写建议优先使用字典精确匹配,不对所有词执行无界编辑距离搜索。 +- 对 `benchmarks/cases/*.dsl` 执行 5 次预热,再执行 10 组测量;每组依次解析全部用例 100 次,使用组耗时中位数。相同机器、Python 版本和进程配置下,“验证 + IR 生成”的中位数目标不超过原解析流程的 1.5 倍。 +- 错误数量达到上限后立即停止进一步分析。 + +报告必须同时给出基线、改动后中位数、倍率和测试环境;不能仅凭主观判断声明达成。 + +--- + +## 16. 验收标准 + +满足以下条件时,课题可判定完成: + +1. 基础和扩展解析器的用户输入错误均转换为 `DSLSyntaxError`。 +2. 每个错误至少包含文件名、1-based 行列、稳定错误码和可读消息。 +3. 至少覆盖第 8 节列出的 `E100`、`E101`、`E110`、`E111`、`E200`、`E201`。 +4. 显式验证模式能从一个文件报告至少 3 个独立错误。 +5. 有语法错误时不返回可执行的部分 IR。 +6. `CompilerDriver` 不再用基础解析器错误覆盖扩展解析器错误。 +7. 纯文本输出不含 ANSI,终端自动颜色遵守 TTY 和 `NO_COLOR`。 +8. 现有正确 DSL 示例、解析器测试和 L2 验证全部通过。 +9. 新增单元、集成和端到端测试均通过。 +10. 文档说明如何增加新的错误码、验证规则和修复建议。 + +--- + +## 17. 风险与权衡 + +| 选择 | 收益 | 代价 | +|------|------|------| +| 验证与 IR 生成分离 | 多错误收集安全,不污染 builder | 部分语法规则会被检查两次 | +| 保留正则解析 | 改动小,适合教学课题 | 复杂语法扩展能力有限 | +| 稳定错误码 | 测试与文档可长期引用 | 新错误分类需要谨慎评审 | +| 保守修复建议 | 降低误导用户的概率 | 可提供的建议数量较少 | +| 不做部分 IR | 保证下游阶段输入可信 | 无法展示错误后的局部编译结果 | + +如果未来 DSL 语法明显增长,应把本方案视为迁移到真正 tokenizer/parser 之前的过渡层,而不是无限扩展行级正则验证器。 + +--- + +## 18. 待后续课题讨论 + +以下方向保留为未来工作,不作为本课题验收项: + +- 统一 ONNX、DSL、IR 验证器的 `Diagnostic` 协议; +- JSON、SARIF 和机器可读错误输出; +- LSP 诊断与编辑器下划线; +- 跨行 SourceSpan 和相关位置(related locations); +- 基于算子注册表自动生成参数诊断; +- 国际化错误消息; +- 将 DSL 正式迁移到 token 流和语法树。