Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ jobs:
python3.12 -m pip install -e ".[all]"
python3.12 -m pip install "pytest>=7,<10"

# Includes Topic 9 parser, diagnostic, CLI and benchmark regression tests.
- name: Run all topic tests
run: |
mkdir -p benchmark_reports
Expand Down Expand Up @@ -114,7 +115,7 @@ jobs:
-o benchmark_reports/tests.html

- name: Upload test reports
if: github.ref == 'refs/heads/main'
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: test-reports
Expand Down Expand Up @@ -187,6 +188,24 @@ jobs:
python3.12 -m pip install -e ".[all]"
python3.12 -m pip install markdown "pytest>=7,<10"

- name: Topic 9 DSL diagnostics benchmark
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: |
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
echo "::error::No baseline commit available for DSL comparison."
exit 1
fi
git fetch origin "$BASE_SHA" --depth=1
BASELINE=$(mktemp -d "$RUNNER_TEMP/dsl-baseline.XXXXXX")
git worktree add --detach "$BASELINE" "$BASE_SHA"
trap 'git worktree remove --force "$BASELINE"' EXIT
python3.12 -m benchmarks.bench_dsl_diagnostics \
--baseline-root "$BASELINE" \
--json-output benchmark_reports/dsl_diagnostics.json \
--markdown benchmark_reports/dsl_diagnostics.md \
--html benchmark_reports/dsl_diagnostics.html

# ── 3.1 ONNX 模型管线基准测试 ──────────────────────────────────────
- name: ONNX model pipeline benchmarks
run: python3.12 -m pytest benchmarks/test_benchmark.py -v --tb=short
Expand Down Expand Up @@ -338,6 +357,9 @@ jobs:
- name: Write job summary
if: always()
run: |
if [ -f benchmark_reports/dsl_diagnostics.md ]; then
cat benchmark_reports/dsl_diagnostics.md >> "$GITHUB_STEP_SUMMARY"
fi
if [ -f benchmark_reports/const_merge_report.md ]; then
cat benchmark_reports/const_merge_report.md >> $GITHUB_STEP_SUMMARY
fi
Expand Down
364 changes: 364 additions & 0 deletions benchmarks/bench_dsl_diagnostics.py

Large diffs are not rendered by default.

231 changes: 231 additions & 0 deletions docs/superpowers/plans/2026-08-12-dsl-diagnostics-implementation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# DSL Diagnostics Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Integrate structured, location-aware diagnostics into ScratchV's base and extended DSL paths while preserving successful DSL IR output and leaving all non-DSL behavior unchanged.

**Architecture:** Add a DSL-only source/validation layer shared by both parsers. Validation runs before IR generation, reports stable diagnostic codes through `DSLSyntaxError` and `ErrorCollector`, and passes structured diagnostics through only the DSL branches of `CompilerDriver` and the CLI.

**Tech Stack:** Python 3.12-compatible type hints, dataclasses, regular expressions, argparse, pytest.

---

## File map and scope boundary

- Create `scratchv/frontend/dsl_validator.py`: source preservation, shared operator signatures, line validation, block-stack recovery, multi-error collection.
- Modify `scratchv/frontend/dsl_errors.py`: compatible diagnostic spans, plain/automatic rendering, deterministic collector semantics.
- Modify `scratchv/frontend/dsl_parser.py`: pre-IR validation and filename-aware errors; preserve successful IR generation.
- Modify `scratchv/frontend/dsl_extended.py`: use shared validation before extended IR generation; preserve successful control-flow IR.
- Modify `scratchv/frontend/__init__.py`: export new DSL-only public diagnostic interfaces.
- Modify `scratchv/compiler.py`: only DSL parsing and diagnostic result fields.
- Modify `scratchv/main.py`: only structured DSL diagnostic rendering and top-level internal-error exit handling.
- Create `tests/test_dsl_validator.py`: validator and parser integration tests.
- Create `tests/test_dsl_diagnostics_cli.py`: CompilerDriver/CLI DSL diagnostics tests.
- Modify `tests/test_dsl_errors.py`: error model, renderer and collector regression tests.
- Do not modify ONNX parsing, IR definitions, optimizer passes, backends, simulators, benchmarks, or their tests.

### Task 1: Diagnostic model and rendering

**Files:**
- Modify: `scratchv/frontend/dsl_errors.py`
- Test: `tests/test_dsl_errors.py`

- [ ] **Step 1: Write failing tests for compatibility, spans, tabs and automatic color**

Add tests asserting that `DSLSyntaxError` remains catchable as `DSLParseError`, accepts exclusive `end_col`, renders `error[E100]`, expands tabs at four-column stops, keeps `str(error)` ANSI-free, and `render_error(..., use_color=None)` honors `isatty()` and `NO_COLOR`.

- [ ] **Step 2: Verify the new renderer tests fail for missing behavior**

Run: `python -m pytest tests/test_dsl_errors.py -q`

Expected: failures for `end_col`, `render_error`, header placement, tab alignment, and collector limit metadata.

- [ ] **Step 3: Implement the minimal compatible diagnostic API**

Implement these interfaces without changing existing positional constructor compatibility:

```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

def render_error(
err: DSLSyntaxError,
*,
stream: TextIO,
use_color: Optional[bool] = None,
) -> str: ...
```

Keep `format_error()` available, default unknown filenames to `<dsl>`, render codes as `error[E100]:`, and calculate display columns by expanding tabs to four-column stops.

- [ ] **Step 4: Make collector semantics deterministic**

Add `limit_reached`; stop at exactly `max_errors`; do not append a fake line-zero error; deduplicate by `(filename, line, col, error_code, message)`; sort reports by `(line, col, error_code or "")`; reset all state in `clear()`.

- [ ] **Step 5: Verify Task 1 is green**

Run: `python -m pytest tests/test_dsl_errors.py -q`

Expected: all tests in the file pass with no warnings.

### Task 2: Shared source buffer and base DSL validation

**Files:**
- Create: `scratchv/frontend/dsl_validator.py`
- Modify: `scratchv/frontend/dsl_parser.py`
- Test: `tests/test_dsl_validator.py`
- Test: `tests/test_parser.py`

- [ ] **Step 1: Write failing source-buffer and E100/E101/E200/E201 tests**

Cover LF/CRLF, blank input, trailing newline, Unicode and tabs. Add parser-facing cases for `retrun x`, missing call parenthesis, unsupported `ad`, and incorrect arity for unary/binary/keyword operators. Assert exact 1-based line/column, source line, filename and stable code.

- [ ] **Step 2: Verify base-validator RED behavior**

Run: `python -m pytest tests/test_dsl_validator.py tests/test_parser.py -q`

Expected: new tests fail because `SourceBuffer`, `DSLValidator.validate()` and filename-aware `parse()` do not exist.

- [ ] **Step 3: Add the shared syntax facts**

Define immutable `SourceBuffer`, `OpSignature`, base statement patterns, keyword sets, and `OP_SIGNATURES` for every operator dispatched by `DSLParser`. Validate assignment structure with `fullmatch`, split arguments without creating IR, and stop validation of a line after its first structural error.

- [ ] **Step 4: Add base block and signature validation**

Validate `for/endfor`, `return`, identifiers, parentheses, supported operations, positional counts, known/duplicate kwargs and numeric kwargs. Preserve the language rule that first-use variable names are valid inputs.

- [ ] **Step 5: Gate base parsing before IR generation**

Expose:

```python
def validate(
self,
text: str,
*,
filename: Optional[str] = None,
max_errors: int = 20,
) -> ErrorCollector: ...

def parse(
self,
text: str,
*,
filename: Optional[str] = None,
) -> Program: ...
```

`parse()` raises the first `DSLSyntaxError` before resetting and using `IRBuilder`; successful programs retain their previous `Program.dump()` structure.

- [ ] **Step 6: Verify base validation and successful IR are green**

Run: `python -m pytest tests/test_dsl_validator.py tests/test_parser.py tests/test_dsl_errors.py -q`

Expected: all selected tests pass.

### Task 3: Extended block validation and recovery

**Files:**
- Modify: `scratchv/frontend/dsl_validator.py`
- Modify: `scratchv/frontend/dsl_extended.py`
- Test: `tests/test_dsl_validator.py`
- Test: `tests/test_dsl_extended.py`

- [ ] **Step 1: Write failing E110/E111/E112 recovery tests**

Cover stray closers, missing closers, `else` without `if`, duplicate `else`, `while ... endif`, `if ... while ... endif`, nested blocks at EOF, and three independent errors in one file. Assert mismatched closers do not also create recovered-block E111 errors.

- [ ] **Step 2: Verify extended-validator RED behavior**

Run: `python -m pytest tests/test_dsl_validator.py tests/test_dsl_extended.py -q`

Expected: new block-validation cases fail under the current silent-EOF behavior.

- [ ] **Step 3: Implement one shared block stack**

Use `BlockFrame(kind, line, col, saw_else)` in the validator. Match `endif/endwhile/endfor` against the stack; on mismatch, emit one E110 and recover by popping through an outer matching block or treating the closer as recovery for the top frame. Emit E111 only for frames left at EOF.

- [ ] **Step 4: Gate extended parsing with the shared validator**

Override validation only to enable extended rules, then validate before any labels, blocks or instructions are created. Keep the existing successful `if/else/while/for` IR path and reset parser state on every call.

- [ ] **Step 5: Verify extended validation and IR regression**

Run: `python -m pytest tests/test_dsl_validator.py tests/test_dsl_extended.py tests/test_parser.py -q`

Expected: all selected tests pass.

### Task 4: DSL-only CompilerDriver and CLI integration

**Files:**
- Modify: `scratchv/compiler.py`
- Modify: `scratchv/main.py`
- Modify: `scratchv/frontend/__init__.py`
- Create: `tests/test_dsl_diagnostics_cli.py`

- [ ] **Step 1: Write failing CompilerDriver and CLI tests**

Test a temporary bad `.dsl` through `CompilerDriver.compile()` and `main(argv)`. Assert `success is False`, `diagnostics` contains the original `DSLSyntaxError`, `errors` contains the complete plain rendering without `Parse error:` duplication, CLI exits 1, redirected stderr has no ANSI, and missing `endif` is not overwritten by a base-parser error.

- [ ] **Step 2: Verify integration RED behavior**

Run: `python -m pytest tests/test_dsl_diagnostics_cli.py -q`

Expected: failures for missing structured result fields, broad parser fallback and CLI rendering.

- [ ] **Step 3: Add compatible result metadata**

Append defaulted `diagnostics`, `diagnostic_limit_reached`, and `diagnostic_limit` fields to `CompileResult`. In the DSL branch only, use `ExtendedDSLParser.parse(text, filename=input_path)` and preserve `DSLSyntaxError` without broad fallback. Do not alter ONNX parsing or successful result semantics.

- [ ] **Step 4: Render diagnostics once at the CLI boundary**

When `result.diagnostics` is non-empty, render them to `sys.stderr` with automatic color and do not print `result.errors` again. Preserve exit 1 for input diagnostics. Convert unexpected top-level exceptions to `internal compiler error` and exit 2 without traceback in normal mode.

- [ ] **Step 5: Verify end-to-end DSL diagnostics**

Run: `python -m pytest tests/test_dsl_diagnostics_cli.py tests/test_dsl_errors.py tests/test_dsl_validator.py tests/test_parser.py tests/test_dsl_extended.py -q`

Expected: all selected tests pass with no ANSI in captured output.

### Task 5: Scope, regression and project verification

**Files:**
- Modify only files listed in the file map if verification exposes a DSL-specific defect.

- [ ] **Step 1: Run every DSL-related test**

Run: `python -m pytest tests/test_dsl_errors.py tests/test_dsl_validator.py tests/test_dsl_diagnostics_cli.py tests/test_parser.py tests/test_dsl_extended.py -q`

Expected: all pass.

- [ ] **Step 2: Run L1/L2 Harness if present**

Run: `python .Codex/harness/verify/run.py --level L1` and `python .Codex/harness/verify/run.py --level L2`.

If the Harness is absent, record that fact and run the repository-equivalent full command `python -m pytest tests -q` in the project environment containing declared dependencies.

- [ ] **Step 3: Prove the scope boundary**

Run: `git diff --name-only` and `git diff --check`.

Expected: only the DSL files, DSL sections of `compiler.py`/`main.py`, DSL tests, and this plan appear; no whitespace errors.

- [ ] **Step 4: Self-review diagnostic requirements**

Confirm E100, E101, E110, E111, E200 and E201; three-error collection; no partial IR; no parser fallback masking; plain redirected output; successful DSL IR regression; and no non-DSL behavior changes.

- [ ] **Step 5: Update shared memory only if a novel reusable lesson exists**

If `memory/memory.md` exists, merge one non-duplicate entry in `[2026-08-12] ...` format. If it does not exist, record the missing Harness facility and do not create unrelated infrastructure in this DSL patch.

- [ ] **Step 6: Stage, commit and push after all verification is green**

Run `git add` with the explicit reviewed file list, commit with an English message such as `feat: integrate structured DSL diagnostics`, then push the current branch without force.
75 changes: 75 additions & 0 deletions docs/topics/09-DSL诊断-CI与Benchmark.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 课题 9:DSL 诊断 CI 与 benchmark

课题 9 接入原有 `.github/workflows/ci.yml`,不新增 workflow 或 job。
现有 `test` job 运行 DSL 测试,现有 `benchmark` job 增加
`Topic 9 DSL diagnostics benchmark` 步骤。沿用上游 runner 配置(PR 使用 ubuntu-latest,
push 使用 self-hosted)、Python 3.12
和触发条件:指向 main 的 PR,以及 main、wjy_dev、jzj_dev 的 push。

## 专项测试

```powershell
python -m pip install -e . "pytest>=7,<10"
python -m pytest tests/test_dsl_errors.py tests/test_dsl_validator.py tests/test_dsl_diagnostics_cli.py tests/test_parser.py tests/test_dsl_extended.py tests/test_dsl_diagnostics_benchmark.py -v --tb=short
```

测试覆盖错误模型、位置、提示、块恢复、多错误、颜色、CLI 和正常解析回归,
以及 benchmark 的失败传播、基线导入隔离和报告产物。原有 `pytest tests/` 会自动
发现这些文件,不重复执行专项测试。结果合并到 `benchmark_reports/test_results.xml`,
由原有 `test-reports` artifact 上传,PR 和失败运行也保留测试报告。

## 本地 benchmark

先为基线建立独立 checkout。以下 `997d2aa` 是本 PR 加入诊断实现之前的历史版本;
rebase 后与最新上游比较时,应改用对应的 PR base SHA。

```powershell
git worktree add --detach ../ScratchV-dsl-baseline 997d2aa
python -m benchmarks.bench_dsl_diagnostics --baseline-root ../ScratchV-dsl-baseline --json-output benchmark_reports/dsl_diagnostics.json --markdown benchmark_reports/dsl_diagnostics.md --html benchmark_reports/dsl_diagnostics.html
```

`--json` 是布尔开关,仅向 stdout 输出 JSON;文件输出使用 `--json-output`。
HTML 使用标准库生成,不依赖外部样式、脚本或可视化库。报告同时包含成功与失败信息。

### 正确 DSL 解析性能

- 两个版本均使用**当前 checkout 的同一批** `benchmarks/cases/*.dsl`,包含基础语法、
for、if/else、while。统一调用 `ExtendedDSLParser().parse(source)`,覆盖 CLI 使用的解析器。
- 同一脚本通过两个独立 Python 进程导入各自 checkout 的解析器,并验证模块路径。
不用关闭当前 validator 的方式冒充旧版解析器。
- 两侧复用当前项目安装的依赖及同一个 Python。每侧预热 5 次,再测量 10 组;
每组解析完整语料 100 次。计时包含创建解析器和解析,排除进程启动、导入、文件读取、
IR 摘要计算和每组开始前的显式 GC;计时期间保留 Python 默认 GC。
- 比较每个文件的源码 SHA-256 和 `Program.dump()` SHA-256,任何解析异常、空语料、
源码差异或 IR 差异均使检查失败,不跳过失败用例。
- JSON 记录原始组耗时、中位数、倍率、实际基线/当前 SHA、模块路径、Python、系统和依赖版本。
两个 checkout 的提交信息不包含未提交修改,正式报告应使用干净 checkout。

### 错误输入诊断

12 个内置固定用例独立验收错误码和行列、源码、文件名、提示、无 ANSI 输出、CLI 退出码 1、
诊断不重复和不产生汇编文件。包含 CRLF/tab/Unicode、三个独立错误以及 25 个错误输入
触发 20 条诊断上限的情况。分别测量验证/收集和纯文本渲染;CLI 用于正确性验收,不计时。

错误样例不放入正常 `benchmarks/cases/`,避免通用 DSL runner 将预期错误当作编译失败。
旧版本不具备新诊断能力,不参与错误输入的等价性能比较。

## 基线与结果判定

- PR:基线为事件中的 `pull_request.base.sha`,当前为事件的 PR 合并测试提交。
- push:基线为事件中的 `before`,当前为该 push 提交。新分支首次 push 的全零
`before` 无法用于对比,该步骤会明确失败;打开指向 main 的 PR 后使用 PR base。
- 基线通过临时 Git worktree 准备,退出步骤时清理,不混入报告 artifact。
- 两个 job 都必须 checkout 到事件的 `GITHUB_SHA`;获取提交、准备基线或 worker
执行失败均使相应步骤失败,不静默回退到 main。

**诊断验收、IR 一致性及报告执行错误是硬门禁。** 设计文档的解析倍率目标是 1.5x,
默认明确显示 `target_met` 和超标提示,但不因共享 runner 的计时波动阻止合并。
因此 CI 绿色不等于已经达到 1.5x 性能目标。需要严格执行该目标时添加
`--enforce-performance`,阈值由 `--max-parse-ratio` 指定,默认 1.5。

benchmark 结束后,Markdown 写入 Job Summary;JSON、Markdown 和 HTML 上传至
原有 `benchmark-reports`。上传及汇总步骤使用 `always()`,功能失败仍保留诊断证据。
Markdown 摘要和本地 HTML 中,每个用例的完整诊断日志默认折叠,点击用例名称展开。
汇总表、失败检查和性能提示直接可见;JSON 保留完整诊断内容。
这里不计算常量合并次数、TinyFive 指令减少量或 LLVM 指令数收益。
Loading
Loading