parser-gap-fix
Fix a zsh-lint parser gap or false accept end to end, from proving it with both oracles to the pull request's verification table. Use for any change under internal/parse that makes valid Zsh parse or invalid Zsh fail. Advisory; the contract is docs/project/parser-gap-workflow.md.
来源信息
- 仓库
- z-shell/zsh-lint
- 最近来源活动
- 2026年9月29日 01:16
- 检测到的 SKILL.md 语言
- 英语
- 星标
- 36
- 分支
- 6
安装方式
默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。
检查来源文件
决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。
正在显示 SKILL.md
SKILL.md
来源说明 · 只读预览- name
- parser-gap-fix
- description
- Fix a zsh-lint parser gap or false accept end to end, from proving it with both oracles to the pull request's verification table. Use for any change under internal/parse that makes valid Zsh parse or invalid Zsh fail. Advisory; the contract is docs/project/parser-gap-workflow.md.
# Parser-gap fix
This skill is the working order for a parser change.
It adds no rule: the contract is `docs/project/parser-gap-workflow.md`, the adapter invariants are in `.github/instructions/parser-front-end.instructions.md`, and Zsh semantics follow the released Zsh manual as the organization's `zsh-scripting.instructions.md` requires.
Where this file and those disagree, they win.
## 0. Prepare the environment
Run `bash .github/scripts/agent-setup.sh` once per session.
It installs `zsh` when it is missing, installs `golangci-lint` v2.12.2 built with the local toolchain, and warms the module cache.
## 1. Prove the gap with both oracles
Write the smallest script that shows the construct, as a file (not a `-c` string: an unterminated loop header at end of input is valid only in a file).
```sh
go run ./cmd/zsh-lint-survey -judge gap.zsh # GAP, FALSE-ACCEPT or AGREE, with both verdicts
go run ./cmd/zsh-lint-survey -reduce big.zsh > gap.zsh # shrink a real file to the construct
```
- `-judge` prints the `zsh -f -n` diagnostic and the zsh-lint diagnostic and classifies the file: valid Zsh that zsh-lint rejects is a parser gap (`GAP`); invalid Zsh that zsh-lint accepts is a false accept (`FALSE-ACCEPT`).
It judges `zsh -f -n` by stderr, not exit status (`! true` exits 1 with no diagnostic), and never runs the file.
- `-reduce` shrinks one gap or false accept by lines, then shell tokens and blocks, keeping a candidate only while the class, the first message and the error's place in the original file stay the same; it never runs a candidate.
`-fixed <binary>` also keeps only candidates that build fixes, to stay on the defect a fix addresses; `-candidate <binary>` judges with another build.
Check the result by hand: the smallest source with the same message can still be a different construct, and a leftover word shortened to `a` may deserve a meaningful name in the issue.
- `zsh -f -n` skips arithmetic evaluation and assignment-word expansion (#287); a source Zsh rejects only when it runs is runtime-tier (step 3).
- Name the language feature from the released manual (`zshmisc`, `zshexpn`, `zshparam`, `zshoptions`), not from memory, another shell, or what mvdan/sh accepts, and keep one feature per issue.
- Keep the section's URL, `https://zsh.sourceforge.io/Doc/Release/<Page>.html#<Section>`: the issue body and every fixture cite it. When the manual and `zsh -f -n` disagree, the binary decides; record both and `zsh --version` in the issue.
## 2. Check the issue and the backlog
- Search open `parser-gap` issues for the same construct before filing; the survey reports only the first error per file, so a gap is often already open under another file.
- File with the parser-gap issue form; it asks for the minimized script, the `zsh -f -n` output, the zsh-lint diagnostic, and the manual section.
## 3. Add fixtures before the fix
- Every fixture carries a `# Manual: <url>` line; `TestFixturesCiteManual` (`internal/manualcite`) enforces it. A fixture renamed away from a name in `internal/manualcite/testdata/fixture-exemptions.txt` gains the line and leaves the list, and `exemptionCeiling` drops, in the same change.
- Name the language feature from the manual section (`zshmisc`, `zshexpn`, `zshparam`, `zshoptions`) and keep one feature per issue.
- Invalid Zsh that must stay rejected: `internal/parse/testdata/invalid-<issue>-<slug>.txt`, read by a focused parser test that asserts the error family and position.
- A source `zsh -f -n` accepts but Zsh rejects at run time goes in `runtimeTierInvalidFixtures` (`internal/survey/native_oracle_test.go`) with its runtime error. Never execute an invalid source.
- `go test ./internal/survey -run TestCorpusFixturesAgreeWithNativeZsh` re-checks every fixture against `zsh -f -n`.
- `go test ./internal/survey -run TestStructuralOracle` checks that every `ok-*` fixture's tree is the program Zsh reads; a new `ok-*` fixture must pass it, and a fix that makes a fixture listed in `structuralOracleKnownDifferences` agree removes that entry.
## 4. Implement
- Fix the gap in the parser fork (`third_party/mvdan-sh`, ADR-0030), behind `LangZsh`, and list the change in its `FORK.md`; run upstream's tests there (`cd third_party/mvdan-sh && go test ./syntax/`). Never add an adapter. When the construct belongs to an adapter whose family has not moved into the fork, fix that adapter through its shared scanner (`internal/parse/double_quote.go`, `internal/parse/heredoc_scan.go`), or migrate the family.
- In an adapter, follow the invariants: register once in `adapterChain`, retry through `parseWithAdapters`, gate on one construct and one parser error, map every byte back, restore the typed AST, return the parser error when unsure.
- Resolve every site of the construct in one pass; masking one site per pass and re-entering the chain costs one whole-file parse per site (#366).
- For non-trivial scanner or grammar logic, use the organization's generator-verifier workflow (`.github/instructions/generator-verifier-workflow.instructions.md` in z-shell/.github): draft, then verify adversarially against native Zsh.
## 5. Verify
Write the construct's valid and invalid variants to a body file, separated by `---` lines, then run every check in one command:
```sh
bash .github/scripts/verify-parser-change.sh --bodies bodies.txt [--rows rows.txt] \
[--list files.txt] [--regression-corpus DIR] [base-ref] <file.zsh ...>
```
It runs build, vet and tests, the fork's tests, `golangci-lint` with the toolchain `go.mod` names, a base build exported with `git archive` (default `origin/main`), `-compare -native` over the corpus fixtures and the given files (#412), the probe grid with `-known`, `-trace-parses` on both builds (#408), and `.github/scripts/mutation.sh`.
It prints one line per check, the tail of any failing log, and a table plus the compare, probe, trace and mutation output for the pull request; each check's full log stays in the directory it names.
A file outside the repository appears by its absolute path; shorten it before pasting.
It exits 1 when a check fails, 3 when only the mutation run was inconclusive, and 2 when it cannot start.
`--skip-mutation` leaves out the slowest check while iterating.
- Run it after `git fetch origin` and a rebase: with the default base it refuses to start unless `origin/main` is origin's `main` and the checkout contains it, and it prints both commit ids (#545).
A named base-ref skips the remote check but not the containment check.
- `--rows FILE` adds a grid of complete sources, one per line, for shapes the probe contexts cannot express, such as words in a subscript.
`internal/probe/testdata/rows-538.txt` is the grid #539 reported.
- `--root DIR` and `--list FILE` add consumer or workspace files to the comparison; a listed path that is not a file fails the check.
`--regression-corpus DIR`, a directory holding `F-Sy-H/` and `zsh/` at the revisions `regression-corpus.txt` pins, runs the Corpus Gate's own comparison.
- `--candidate REV` judges another commit's build instead of the working tree, to re-run a merged change's numbers with today's tools; the Go checks and mutation are then skipped.
- `-compare -native` must show no `REGRESSED` and no `FALSE-ACCEPT`; every `FIXED` and `MOVED` line belongs in the pull request.
- A grid is also judged with `-runtime`: a row `zsh -f -n` accepts that the change now rejects, and that Zsh rejects when it runs, is `RUNTIME-REJECTED` rather than `REGRESSED` and does not fail the check.
These are assignments and command substitution bodies, which `zsh -n` does not check (#519, #539); list the family in the pull request.
`-runtime` runs the rows, so the script uses it on grids only.
- Each grid's changed rows are written as a survey-record table, `probe.md` (or `probe-N.md`) next to the logs.
- The probe summary counts known gaps and false accepts that the change leaves in place; `-known` lists them, and each one is an issue to file.
- The mutation check fails when a mutant survives; a mutant that hangs the suite counts as caught, and a run where timeouts outnumber the decided mutants is inconclusive (raise `MUTATION_TIMEOUT_COEFFICIENT`). List the surviving and uncovered lines it prints in the pull request, with a reason for any that stay.
- `mutation.sh` does not mutate `third_party/mvdan-sh`, and a changed `case` expression line reads as not covered; hand-mutate such a change and report the result.
- The Parse Cost workflow repeats the retry-cost comparison on the pull request and adds a notice above a 10 percent rise.
## 6. Record and open the pull request
- When a corpus or workspace verdict changes, add a dated survey record under `docs/project/` in the format of the existing ones and index it in `docs/project/README.md`.
- Pull request body: Summary, Change (per file), Verification (the `-compare` lines, the probe counts, the retry-cost numbers, the checks run), and Out of scope (gaps the fix uncovered, each filed as its own issue).
- File every newly exposed gap before merging; the next first error in a file is usually a different feature.
在 GitHub 查看