parseman 0.45.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/analysis/choice-cost-gate.d.ts +158 -0
- package/dist/analysis/choice-cost-gate.d.ts.map +1 -0
- package/dist/analysis/choice-cost-render.d.ts +96 -0
- package/dist/analysis/choice-cost-render.d.ts.map +1 -0
- package/dist/analysis/choice-cost.d.ts +383 -0
- package/dist/analysis/choice-cost.d.ts.map +1 -0
- package/dist/analysis/commitment.d.ts +131 -0
- package/dist/analysis/commitment.d.ts.map +1 -0
- package/dist/analysis/corpus.d.ts +73 -0
- package/dist/analysis/corpus.d.ts.map +1 -0
- package/dist/analysis/diagnose-render.d.ts +65 -0
- package/dist/analysis/diagnose-render.d.ts.map +1 -0
- package/dist/analysis/diagnose.d.ts +15 -0
- package/dist/analysis/diagnose.d.ts.map +1 -1
- package/dist/analysis/diagnostics.cjs +13736 -0
- package/dist/analysis/diagnostics.cjs.map +6 -0
- package/dist/analysis/diagnostics.d.ts +27 -0
- package/dist/analysis/diagnostics.d.ts.map +1 -0
- package/dist/analysis/diagnostics.js +13713 -0
- package/dist/analysis/diagnostics.js.map +6 -0
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/fix-render.d.ts +43 -0
- package/dist/analysis/fix-render.d.ts.map +1 -0
- package/dist/analysis/fix.d.ts +172 -0
- package/dist/analysis/fix.d.ts.map +1 -0
- package/dist/analysis/format-number.d.ts +23 -0
- package/dist/analysis/format-number.d.ts.map +1 -0
- package/dist/analysis/gating.d.ts +47 -2
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/rebuild.d.ts +61 -0
- package/dist/analysis/rebuild.d.ts.map +1 -0
- package/dist/analysis/terminal.d.ts +145 -0
- package/dist/analysis/terminal.d.ts.map +1 -0
- package/dist/cli/index.cjs +14837 -0
- package/dist/cli/index.cjs.map +6 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +14814 -0
- package/dist/cli/index.js.map +6 -0
- package/dist/combinators/adjacency.d.ts +100 -0
- package/dist/combinators/adjacency.d.ts.map +1 -0
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/completions.d.ts.map +1 -1
- package/dist/combinators/dispatch.d.ts +8 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/gate.d.ts.map +1 -1
- package/dist/combinators/grammar.d.ts +22 -0
- package/dist/combinators/grammar.d.ts.map +1 -1
- package/dist/combinators/literal.d.ts +8 -0
- package/dist/combinators/literal.d.ts.map +1 -1
- package/dist/combinators/map.d.ts +0 -1
- package/dist/combinators/map.d.ts.map +1 -1
- package/dist/combinators/node.d.ts +13 -3
- package/dist/combinators/node.d.ts.map +1 -1
- package/dist/combinators/parser.d.ts +1 -1
- package/dist/combinators/parser.d.ts.map +1 -1
- package/dist/combinators/ref.d.ts.map +1 -1
- package/dist/combinators/regex.d.ts +1 -16
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/repeat.d.ts +30 -2
- package/dist/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/scanTo.d.ts +71 -1
- package/dist/combinators/scanTo.d.ts.map +1 -1
- package/dist/combinators/sequence.d.ts.map +1 -1
- package/dist/combinators/token.d.ts.map +1 -1
- package/dist/combinators/trivia-boundary.d.ts +26 -0
- package/dist/combinators/trivia-boundary.d.ts.map +1 -0
- package/dist/combinators/trivia-skip.d.ts +40 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/combinators/withCtx.d.ts.map +1 -1
- package/dist/compiler/build-arity.d.ts.map +1 -1
- package/dist/compiler/compile-linkable-table.d.ts +104 -0
- package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
- package/dist/compiler/degradation.d.ts +0 -2
- package/dist/compiler/degradation.d.ts.map +1 -1
- package/dist/compiler/fields.d.ts +13 -0
- package/dist/compiler/fields.d.ts.map +1 -1
- package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
- package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +79 -47
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +81 -0
- package/dist/compiler/token-alphabet.d.ts.map +1 -0
- package/dist/compiler/token-dispatch.d.ts +95 -0
- package/dist/compiler/token-dispatch.d.ts.map +1 -0
- package/dist/compiler/token-scanner.d.ts +53 -0
- package/dist/compiler/token-scanner.d.ts.map +1 -0
- package/dist/compiler/value-usage.d.ts.map +1 -1
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/capture-buffer.d.ts +35 -0
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/cst/reflection-attach.d.ts +8 -0
- package/dist/cst/reflection-attach.d.ts.map +1 -0
- package/dist/cst/reflection-symbols.d.ts +10 -0
- package/dist/cst/reflection-symbols.d.ts.map +1 -0
- package/dist/cst/reflection.d.ts +2 -4
- package/dist/cst/reflection.d.ts.map +1 -1
- package/dist/cst/root-trivia-scope.d.ts +26 -0
- package/dist/cst/root-trivia-scope.d.ts.map +1 -0
- package/dist/cst/trivia-charscan.d.ts +18 -0
- package/dist/cst/trivia-charscan.d.ts.map +1 -0
- package/dist/cst/trivia-kinds.d.ts +19 -0
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/functional/doc.d.ts.map +1 -1
- package/dist/functional/run-tabled.d.ts +12 -0
- package/dist/functional/run-tabled.d.ts.map +1 -0
- package/dist/functional/run.d.ts +25 -42
- package/dist/functional/run.d.ts.map +1 -1
- package/dist/grammar-metadata.d.ts +3 -0
- package/dist/grammar-metadata.d.ts.map +1 -0
- package/dist/index.cjs +10499 -7478
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +24 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10499 -7478
- package/dist/index.js.map +3 -4
- package/dist/language-service/index.cjs +311 -121
- package/dist/language-service/index.cjs.map +3 -4
- package/dist/language-service/index.js +311 -121
- package/dist/language-service/index.js.map +3 -4
- package/dist/oracle/index.cjs.map +0 -1
- package/dist/oracle/index.js.map +0 -1
- package/dist/parse-context.d.ts +49 -0
- package/dist/parse-context.d.ts.map +1 -0
- package/dist/plugin/evaluator.d.ts +41 -2
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/free-identifiers.d.ts +23 -0
- package/dist/plugin/free-identifiers.d.ts.map +1 -0
- package/dist/plugin/index.cjs +11381 -9442
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11381 -9442
- package/dist/plugin/index.js.map +3 -4
- package/dist/plugin/reducer-resolver.d.ts +17 -1
- package/dist/plugin/reducer-resolver.d.ts.map +1 -1
- package/dist/regex/classes.d.ts +28 -0
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/regex/first-set.d.ts +14 -0
- package/dist/regex/first-set.d.ts.map +1 -1
- package/dist/run/index.cjs +136 -51
- package/dist/run/index.cjs.map +2 -3
- package/dist/run/index.js +136 -51
- package/dist/run/index.js.map +2 -3
- package/dist/spec/index.cjs +2898 -1622
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2898 -1622
- package/dist/spec/index.js.map +3 -4
- package/dist/spec/model.d.ts +8 -0
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/table/assemble.d.ts +307 -0
- package/dist/table/assemble.d.ts.map +1 -0
- package/dist/table/cell.d.ts +42 -0
- package/dist/table/cell.d.ts.map +1 -0
- package/dist/table/child-slots.d.ts +30 -0
- package/dist/table/child-slots.d.ts.map +1 -0
- package/dist/table/compile-rule-map.d.ts +136 -0
- package/dist/table/compile-rule-map.d.ts.map +1 -0
- package/dist/table/compile.d.ts +56 -0
- package/dist/table/compile.d.ts.map +1 -0
- package/dist/table/duplication-hook.d.ts +32 -0
- package/dist/table/duplication-hook.d.ts.map +1 -0
- package/dist/table/emit-assembly.d.ts +139 -0
- package/dist/table/emit-assembly.d.ts.map +1 -0
- package/dist/table/emit.d.ts +113 -0
- package/dist/table/emit.d.ts.map +1 -0
- package/dist/table/encode-baseline.d.ts +16 -0
- package/dist/table/encode-baseline.d.ts.map +1 -0
- package/dist/table/encode.d.ts +71 -0
- package/dist/table/encode.d.ts.map +1 -0
- package/dist/table/exec-baseline.d.ts +9 -0
- package/dist/table/exec-baseline.d.ts.map +1 -0
- package/dist/table/exec.d.ts +72 -0
- package/dist/table/exec.d.ts.map +1 -0
- package/dist/table/fold.d.ts +25 -0
- package/dist/table/fold.d.ts.map +1 -0
- package/dist/table/index.cjs +11375 -0
- package/dist/table/index.cjs.map +6 -0
- package/dist/table/index.d.ts +100 -0
- package/dist/table/index.d.ts.map +1 -0
- package/dist/table/index.js +11352 -0
- package/dist/table/index.js.map +6 -0
- package/dist/table/inspect.d.ts +17 -0
- package/dist/table/inspect.d.ts.map +1 -0
- package/dist/table/ops.d.ts +446 -0
- package/dist/table/ops.d.ts.map +1 -0
- package/dist/table/program.d.ts +493 -0
- package/dist/table/program.d.ts.map +1 -0
- package/dist/table/run-support.d.ts +38 -0
- package/dist/table/run-support.d.ts.map +1 -0
- package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
- package/dist/table/scan-shapes.d.ts.map +1 -0
- package/dist/table/site-labels.d.ts +58 -0
- package/dist/table/site-labels.d.ts.map +1 -0
- package/dist/table/stamp.d.ts +70 -0
- package/dist/table/stamp.d.ts.map +1 -0
- package/dist/types.d.ts +80 -24
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +41 -3
- package/src/analysis/choice-cost-gate.ts +359 -0
- package/src/analysis/choice-cost-render.ts +293 -0
- package/src/analysis/choice-cost.ts +1008 -0
- package/src/analysis/commitment.ts +557 -0
- package/src/analysis/corpus.ts +130 -0
- package/src/analysis/diagnose-render.ts +625 -0
- package/src/analysis/diagnose.ts +317 -0
- package/src/analysis/diagnostics.ts +33 -0
- package/src/analysis/duplication.ts +1955 -0
- package/src/analysis/fix-render.ts +217 -0
- package/src/analysis/fix.ts +669 -0
- package/src/analysis/format-number.ts +31 -0
- package/src/analysis/gating.ts +792 -0
- package/src/analysis/grammar.ts +72 -0
- package/src/analysis/rebuild.ts +287 -0
- package/src/analysis/terminal.ts +336 -0
- package/src/cli/index.ts +413 -0
- package/src/combinators/adjacency.ts +171 -0
- package/src/combinators/attempt.ts +41 -0
- package/src/combinators/case-fold.ts +90 -0
- package/src/combinators/choice.ts +466 -0
- package/src/combinators/completions.ts +64 -0
- package/src/combinators/dispatch.ts +358 -0
- package/src/combinators/expect.ts +159 -0
- package/src/combinators/first-set.ts +322 -0
- package/src/combinators/gate.ts +50 -0
- package/src/combinators/grammar.ts +272 -0
- package/src/combinators/keywords.ts +145 -0
- package/src/combinators/literal.ts +102 -0
- package/src/combinators/map.ts +106 -0
- package/src/combinators/node.ts +345 -0
- package/src/combinators/not.ts +55 -0
- package/src/combinators/parser.ts +266 -0
- package/src/combinators/peek.ts +63 -0
- package/src/combinators/probe.ts +25 -0
- package/src/combinators/ref.ts +109 -0
- package/src/combinators/regex.ts +170 -0
- package/src/combinators/repeat.ts +561 -0
- package/src/combinators/scanTo.ts +393 -0
- package/src/combinators/sequence.ts +221 -0
- package/src/combinators/token.ts +120 -0
- package/src/combinators/trivia-boundary.ts +48 -0
- package/src/combinators/trivia-skip.ts +821 -0
- package/src/combinators/withCtx.ts +45 -0
- package/src/compiler/build-arity.ts +330 -0
- package/src/compiler/compile-linkable-table.ts +209 -0
- package/src/compiler/degradation.ts +266 -0
- package/src/compiler/fields.ts +191 -0
- package/src/compiler/grammar-coverage-ids.ts +129 -0
- package/src/compiler/ir-serialize.ts +615 -0
- package/src/compiler/linker.ts +954 -0
- package/src/compiler/token-alphabet.ts +209 -0
- package/src/compiler/token-dispatch.ts +353 -0
- package/src/compiler/token-scanner.ts +318 -0
- package/src/compiler/value-usage.ts +147 -0
- package/src/coverage.ts +383 -0
- package/src/cst/capture-buffer.ts +297 -0
- package/src/cst/host-mode.ts +69 -0
- package/src/cst/reflection-attach.ts +11 -0
- package/src/cst/reflection-symbols.ts +9 -0
- package/src/cst/reflection.ts +149 -0
- package/src/cst/relative-spans.ts +207 -0
- package/src/cst/root-trivia-scope.ts +30 -0
- package/src/cst/trivia-charscan.ts +312 -0
- package/src/cst/trivia-entries.ts +285 -0
- package/src/cst/trivia-index.ts +131 -0
- package/src/cst/trivia-kinds.ts +358 -0
- package/src/cst/types.ts +76 -0
- package/src/cst/walk.ts +101 -0
- package/src/functional/doc.ts +959 -0
- package/src/functional/run-tabled.ts +153 -0
- package/src/functional/run.ts +558 -0
- package/src/grammar-metadata.ts +2 -0
- package/src/index.ts +189 -0
- package/src/language-service/index.ts +221 -0
- package/src/line-index.ts +131 -0
- package/src/oracle/digest.ts +620 -0
- package/src/oracle/index.ts +55 -0
- package/src/parse-context.ts +91 -0
- package/src/plugin/direct-builder-static.ts +102 -0
- package/src/plugin/evaluator.ts +1505 -0
- package/src/plugin/free-identifiers.ts +338 -0
- package/src/plugin/index.ts +2281 -0
- package/src/plugin/reducer-resolver.ts +606 -0
- package/src/recovery/scan.ts +155 -0
- package/src/regex/classes.ts +183 -0
- package/src/regex/first-set.ts +316 -0
- package/src/run/index.ts +31 -0
- package/src/spec/ebnf.ts +128 -0
- package/src/spec/index.ts +54 -0
- package/src/spec/model.ts +636 -0
- package/src/spec/railroad-lib.ts +559 -0
- package/src/spec/railroad.ts +256 -0
- package/src/table/assemble.ts +2994 -0
- package/src/table/cell.ts +42 -0
- package/src/table/child-slots.ts +113 -0
- package/src/table/compile-rule-map.ts +336 -0
- package/src/table/compile.ts +244 -0
- package/src/table/duplication-hook.ts +89 -0
- package/src/table/emit-assembly.ts +1766 -0
- package/src/table/emit.ts +418 -0
- package/src/table/encode-baseline.ts +340 -0
- package/src/table/encode.ts +1569 -0
- package/src/table/exec-baseline.ts +537 -0
- package/src/table/exec.ts +1672 -0
- package/src/table/fold.ts +38 -0
- package/src/table/index.ts +108 -0
- package/src/table/inspect.ts +52 -0
- package/src/table/ops.ts +459 -0
- package/src/table/program.ts +855 -0
- package/src/table/run-support.ts +66 -0
- package/src/table/scan-shapes.ts +1578 -0
- package/src/table/site-labels.ts +228 -0
- package/src/table/stamp.ts +229 -0
- package/src/types.ts +563 -0
- package/src/version.ts +16 -0
- package/dist/compiler/codegen.d.ts +0 -334
- package/dist/compiler/codegen.d.ts.map +0 -1
- package/dist/compiler/inline-build.d.ts +0 -11
- package/dist/compiler/inline-build.d.ts.map +0 -1
- package/dist/compiler/inline-callback.d.ts +0 -21
- package/dist/compiler/inline-callback.d.ts.map +0 -1
- package/dist/compiler/line-index.d.ts +0 -2
- package/dist/compiler/line-index.d.ts.map +0 -1
- package/dist/compiler/scannable-run.d.ts.map +0 -1
- package/dist/compiler/scannable-terminal.d.ts +0 -23
- package/dist/compiler/scannable-terminal.d.ts.map +0 -1
- package/dist/compiler/trivia-fast-path.d.ts +0 -38
- package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Degradation diagnostics — "parseman could not do the fast/complete thing here".
|
|
3
|
+
*
|
|
4
|
+
* A fallback that is CORRECT but slower (or less complete) and announces nothing is
|
|
5
|
+
* indistinguishable from working properly. Every such path in the compiler and the
|
|
6
|
+
* analysis APIs reports through this one channel so a consumer's build gate can
|
|
7
|
+
* assert ZERO degradations, exactly the way jess's `check:macro` already asserts zero
|
|
8
|
+
* `"falling back to runtime"` lines.
|
|
9
|
+
*
|
|
10
|
+
* Machine-greppable contract (stable across minor versions):
|
|
11
|
+
*
|
|
12
|
+
* [parseman] degraded [<code>] <where>: <subject> — <fell back to>; otherwise <what>
|
|
13
|
+
*
|
|
14
|
+
* Grep token is the literal `[parseman] degraded`. The `<code>` is a stable kebab-case
|
|
15
|
+
* identifier; new codes may be added, existing ones are not renamed without a note in
|
|
16
|
+
* CHANGELOG.md.
|
|
17
|
+
*
|
|
18
|
+
* NOT for: user errors (those throw), un-taken optimizations that cost nothing, or
|
|
19
|
+
* anything a grammar author cannot act on. A diagnostic that fires on every rule gets
|
|
20
|
+
* filtered out and becomes the same silence we are trying to end — so a code that is
|
|
21
|
+
* common and unavoidable AGGREGATES (see `DETAIL_CAP`) into one summary line with a
|
|
22
|
+
* count instead of one line per rule.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Off / print / throw. */
|
|
26
|
+
export type DegradationLevel = 'off' | 'warn' | 'error'
|
|
27
|
+
|
|
28
|
+
/** Stable, greppable code per degradation class. */
|
|
29
|
+
export type DegradationCode =
|
|
30
|
+
/** A node build's formal parameter list could not be read → all capture tiers kept. */
|
|
31
|
+
| 'build-arity-unconfirmed'
|
|
32
|
+
/** A composed/carried piece is an opaque artifact → its rules were not analysed. */
|
|
33
|
+
| 'opaque-artifact'
|
|
34
|
+
/** A coverage-definition request could not read the grammar → empty is NOT a zero. */
|
|
35
|
+
| 'coverage-definitions-unavailable'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Severity. `warn` = a real, measurable cost the author can remove. `info` = the author
|
|
39
|
+
* has nothing to act on (e.g. an imported reducer in someone else's package), but the
|
|
40
|
+
* fact still has to be visible and countable.
|
|
41
|
+
*/
|
|
42
|
+
export type DegradationSeverity = 'warn' | 'info'
|
|
43
|
+
|
|
44
|
+
export type Degradation = {
|
|
45
|
+
code: DegradationCode
|
|
46
|
+
severity: DegradationSeverity
|
|
47
|
+
/** Rule / node type / artifact the cost lands on. Never a bare "<unknown>" if avoidable. */
|
|
48
|
+
where: string
|
|
49
|
+
/** The reducer, identifier, or input that could not be analysed. */
|
|
50
|
+
subject: string
|
|
51
|
+
/** What parseman did instead. */
|
|
52
|
+
fellBackTo: string
|
|
53
|
+
/** What it would have done had the input been analysable. */
|
|
54
|
+
otherwise: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One line per finding, in the documented greppable shape. */
|
|
58
|
+
export function formatDegradation(d: Degradation): string {
|
|
59
|
+
return `[parseman] degraded [${d.code}] ${d.where}: ${d.subject} — ${d.fellBackTo}; otherwise ${d.otherwise}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Per-code detail cap. Above it the remaining sites collapse to one counted summary
|
|
64
|
+
* line. Chosen small on purpose: the point of a detail line is to give an author a
|
|
65
|
+
* concrete place to start, and eight starting points is already more than anyone acts
|
|
66
|
+
* on in one pass. The COUNT is what makes the rest visible.
|
|
67
|
+
*/
|
|
68
|
+
const DETAIL_CAP = 8
|
|
69
|
+
|
|
70
|
+
/** Group by code, cap the detail lines, and append a counted summary for the rest. */
|
|
71
|
+
export function formatDegradations(list: readonly Degradation[]): string[] {
|
|
72
|
+
const byCode = new Map<DegradationCode, Degradation[]>()
|
|
73
|
+
for (const d of list) {
|
|
74
|
+
const bucket = byCode.get(d.code)
|
|
75
|
+
if (bucket) bucket.push(d)
|
|
76
|
+
else byCode.set(d.code, [d])
|
|
77
|
+
}
|
|
78
|
+
const out: string[] = []
|
|
79
|
+
for (const [code, items] of byCode) {
|
|
80
|
+
for (const d of items.slice(0, DETAIL_CAP)) out.push(formatDegradation(d))
|
|
81
|
+
if (items.length > DETAIL_CAP) {
|
|
82
|
+
out.push(
|
|
83
|
+
`[parseman] degraded [${code}] +${items.length - DETAIL_CAP} more site(s) not listed `
|
|
84
|
+
+ `(${items.length} total). Set PARSEMAN_DEGRADATION=error to fail the build on these.`,
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Capture sink
|
|
93
|
+
//
|
|
94
|
+
// Compilation is single-module and synchronous, so a module-level sink (opened per
|
|
95
|
+
// transform, drained after) records findings without threading a field through every
|
|
96
|
+
// Ctx and every compile*() return — the same shape as the regex-lowering sink in
|
|
97
|
+
// codegen.ts, for the same reason.
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
//
|
|
100
|
+
// A STACK, not a single slot. `transformMacro` re-enters itself when it lowers a private
|
|
101
|
+
// source module, so a single slot let the inner call's `endDegradationCapture()` close
|
|
102
|
+
// the OUTER module's sink — every later finding in the outer pass then bypassed capture
|
|
103
|
+
// and printed instead. A stack scopes each frame to its own module.
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
const _sinks: Array<Map<string, Degradation>> = []
|
|
106
|
+
|
|
107
|
+
/** Begin collecting degradations instead of printing them. */
|
|
108
|
+
export function beginDegradationCapture(): void {
|
|
109
|
+
_sinks.push(new Map())
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Stop collecting and return the findings (deduped, insertion-ordered). */
|
|
113
|
+
export function endDegradationCapture(): Degradation[] {
|
|
114
|
+
const sink = _sinks.pop()
|
|
115
|
+
return sink ? [...sink.values()] : []
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Open-sink depth, for a caller that must unwind exactly its own frames. */
|
|
119
|
+
export function degradationCaptureDepth(): number {
|
|
120
|
+
return _sinks.length
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Close every sink opened above `depth` and return their findings, outermost first.
|
|
125
|
+
*
|
|
126
|
+
* The safety net for an ABORTED capture. `beginDegradationCapture()` and its matching
|
|
127
|
+
* `end` used to sit on the straight-line path of a function that throws between them, so
|
|
128
|
+
* one failed macro transform left the sink open for the REST OF THE PROCESS: every later
|
|
129
|
+
* `recordDegradation` — including from an unrelated runtime `compile()` — went into a
|
|
130
|
+
* dead Map and printed nothing. Calling this from a `finally` bounds the damage to the
|
|
131
|
+
* frame that failed, and returns what it had collected so it can still be reported
|
|
132
|
+
* rather than silently dropped.
|
|
133
|
+
*/
|
|
134
|
+
export function unwindDegradationCapture(depth: number): Degradation[] {
|
|
135
|
+
const found: Degradation[] = []
|
|
136
|
+
while (_sinks.length > depth) {
|
|
137
|
+
const sink = _sinks.pop()!
|
|
138
|
+
found.unshift(...sink.values())
|
|
139
|
+
}
|
|
140
|
+
return found
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolve the level: explicit argument wins, else `PARSEMAN_DEGRADATION`, else
|
|
145
|
+
* default-on `'warn'`. Default-on is the whole point — this exists because the
|
|
146
|
+
* default was silence.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveDegradationLevel(explicit?: DegradationLevel): DegradationLevel {
|
|
149
|
+
if (explicit !== undefined) return explicit
|
|
150
|
+
const env = typeof process !== 'undefined' ? (process.env?.PARSEMAN_DEGRADATION as DegradationLevel | undefined) : undefined
|
|
151
|
+
if (env === 'off' || env === 'warn' || env === 'error') return env
|
|
152
|
+
return 'warn'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The dedup key for one finding.
|
|
157
|
+
*
|
|
158
|
+
* `JSON.stringify` of the tuple, NOT a delimiter-joined string. Two of the three
|
|
159
|
+
* components are unbounded author text — `where` embeds a node type and `subject` is a
|
|
160
|
+
* verbatim slice of reducer SOURCE — so no single delimiter character can be shown absent
|
|
161
|
+
* from them, and picking a rarer one is the same bug one notch weaker. A JSON array is
|
|
162
|
+
* injective by construction and needs no such argument.
|
|
163
|
+
*
|
|
164
|
+
* It is also PRINTABLE, which is the property this file lost twice. The key was built
|
|
165
|
+
* with RAW 0x00 bytes in a template literal, which makes the whole source file BINARY:
|
|
166
|
+
* `git diff --numstat` reported `- -` for it, GitHub refused to render it, and `grep -rn`
|
|
167
|
+
* skipped it SILENTLY — no "binary file matches", no output, exit 0. The largest new file
|
|
168
|
+
* in the release was invisible to ordinary review. Commit `ed81612` had already removed
|
|
169
|
+
* exactly this pattern from `gating.ts` and `duplication.ts`; it came straight back here.
|
|
170
|
+
* `scripts/check-control-bytes.mjs` now fails CI on it, because a fix without a gate got
|
|
171
|
+
* us a second occurrence and would get us a third.
|
|
172
|
+
*
|
|
173
|
+
* This runs only when a degradation is actually recorded — never per match — so the
|
|
174
|
+
* encoding cost is irrelevant.
|
|
175
|
+
*/
|
|
176
|
+
function degradationKey(d: Degradation): string {
|
|
177
|
+
return JSON.stringify([d.code, d.where, d.subject])
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Record one degradation. Deduped on `code + where + subject`, so the four capture-tier
|
|
182
|
+
* probes that all consult the same unreadable parameter list produce ONE finding, not
|
|
183
|
+
* four.
|
|
184
|
+
*
|
|
185
|
+
* With a sink open (a macro transform) the finding is collected and the drain site
|
|
186
|
+
* decides what to do with it. With NO sink open — a runtime `compile()` — this is the
|
|
187
|
+
* only place that ever sees the finding, so it is also the only place that can honour
|
|
188
|
+
* `'error'`. It therefore throws here. `docs/guide/degradation-diagnostics.md` documents
|
|
189
|
+
* `PARSEMAN_DEGRADATION=error` as "fail the build" without qualification, and until this
|
|
190
|
+
* throw existed that was false in library mode: `error` silently behaved as `warn`,
|
|
191
|
+
* because `endDegradationCapture()` had exactly one call site, in the macro plugin.
|
|
192
|
+
*/
|
|
193
|
+
export function recordDegradation(d: Degradation): void {
|
|
194
|
+
const level = resolveDegradationLevel()
|
|
195
|
+
if (level === 'off') return
|
|
196
|
+
const key = degradationKey(d)
|
|
197
|
+
const sink = _sinks[_sinks.length - 1]
|
|
198
|
+
if (sink) {
|
|
199
|
+
if (!sink.has(key)) sink.set(key, d)
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
if (_immediate.has(key)) return
|
|
203
|
+
_immediate.add(key)
|
|
204
|
+
if (level === 'error') throw new Error(`parseman: degraded compilation path\n${formatDegradation(d)}`)
|
|
205
|
+
console.warn(formatDegradation(d))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Dedup memo for the sink-less (runtime `compile()`) path. */
|
|
209
|
+
const _immediate = new Set<string>()
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Open a per-`compile()` drain and return the function that closes it.
|
|
213
|
+
*
|
|
214
|
+
* This channel stays LOUD. `compile()` no longer prints gating advice — that is a
|
|
215
|
+
* deliberate diagnostic now (`diagnoseGrammar`) — but a degradation is not advice: it
|
|
216
|
+
* is parseman reporting that it could not do the thing the caller asked for, and this
|
|
217
|
+
* release exists to stop that happening silently. What changes here is only the SHAPE.
|
|
218
|
+
*
|
|
219
|
+
* Before, the sink-less path printed one ~500-character line per site as it went, with
|
|
220
|
+
* no aggregation — 31 of them for a single code in one `pnpm perf:workloads` run, each
|
|
221
|
+
* repeating the same advice. The macro drain has always aggregated (`formatDegradations`,
|
|
222
|
+
* `DETAIL_CAP`); the runtime path simply had no drain to aggregate at. It has one now,
|
|
223
|
+
* so both paths report the same way and a real count survives instead of a wall.
|
|
224
|
+
*
|
|
225
|
+
* Nested inside a macro transform this is a NO-OP: that transform's sink owns the whole
|
|
226
|
+
* module's findings and returns them on the bundler's warning channel. A per-compile
|
|
227
|
+
* drain underneath it would steal them and print them instead.
|
|
228
|
+
*
|
|
229
|
+
* @returns `drain(report)` — always unwinds the sink (so a `compile()` that throws
|
|
230
|
+
* cannot leave it open for the rest of the process), and reports only when
|
|
231
|
+
* `report` is true, so a failed compile does not mask its own error with a second one.
|
|
232
|
+
*/
|
|
233
|
+
export function beginCompileDegradationDrain(): (report: boolean) => void {
|
|
234
|
+
if (resolveDegradationLevel() === 'off' || _sinks.length > 0) return () => {}
|
|
235
|
+
const depth = degradationCaptureDepth()
|
|
236
|
+
beginDegradationCapture()
|
|
237
|
+
return (report: boolean) => {
|
|
238
|
+
const found = unwindDegradationCapture(depth)
|
|
239
|
+
if (!report || found.length === 0) return
|
|
240
|
+
// Process-level dedup, preserved from the immediate path: compiling the same grammar
|
|
241
|
+
// twice must not re-print what the author has already been told once.
|
|
242
|
+
const fresh = found.filter(d => {
|
|
243
|
+
const k = degradationKey(d)
|
|
244
|
+
if (_immediate.has(k)) return false
|
|
245
|
+
_immediate.add(k)
|
|
246
|
+
return true
|
|
247
|
+
})
|
|
248
|
+
if (fresh.length === 0) return
|
|
249
|
+
const lines = formatDegradations(fresh)
|
|
250
|
+
if (resolveDegradationLevel() === 'error') {
|
|
251
|
+
throw new Error(`parseman: ${fresh.length} degraded compilation path(s)\n${lines.join('\n')}`)
|
|
252
|
+
}
|
|
253
|
+
for (const l of lines) console.warn(l)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Test-only: forget the sink-less dedup memo. */
|
|
258
|
+
export function resetDegradationMemo(): void {
|
|
259
|
+
_immediate.clear()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Test-only: is a sink open? Guards against a leaked capture between test files. */
|
|
263
|
+
export function hasOpenDegradationSink(): boolean {
|
|
264
|
+
return _sinks.length > 0
|
|
265
|
+
}
|
|
266
|
+
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { Combinator, FieldMap, ParserDef } from '../types.ts'
|
|
2
|
+
import { confirmedArityForDef } from './build-arity.ts'
|
|
3
|
+
|
|
4
|
+
type NodeDef = Extract<ParserDef, { tag: 'node' }>
|
|
5
|
+
|
|
6
|
+
export function buildReadsFields(def: NodeDef): boolean {
|
|
7
|
+
if (!def.build) return true
|
|
8
|
+
const arity = confirmedArityForDef(def)
|
|
9
|
+
if (arity === null) return true
|
|
10
|
+
return arity >= 2
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parserHasOwnFields(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
|
|
14
|
+
if (seen.has(p)) return false
|
|
15
|
+
seen.add(p)
|
|
16
|
+
const d = p._def
|
|
17
|
+
switch (d.tag) {
|
|
18
|
+
case 'field': return true
|
|
19
|
+
case 'node': return false
|
|
20
|
+
case 'lazy': {
|
|
21
|
+
try { return parserHasOwnFields(d.thunk(), seen) } catch { return false }
|
|
22
|
+
}
|
|
23
|
+
case 'sequence':
|
|
24
|
+
case 'choice': return d.parsers.some(x => parserHasOwnFields(x, seen))
|
|
25
|
+
case 'dispatch': return parserHasOwnFields(d.selector, seen) || d.cases.some(x => parserHasOwnFields(x.parser, seen)) || (d.matchers ? d.matchers.some(entry => parserHasOwnFields(entry.parser, seen)) : false) || (d.otherwise ? parserHasOwnFields(d.otherwise, seen) : false)
|
|
26
|
+
case 'sepBy': return parserHasOwnFields(d.parser, seen) || parserHasOwnFields(d.separator, seen)
|
|
27
|
+
case 'grammar': return parserHasOwnFields(d.parser, seen) || (d.triviaParser ? parserHasOwnFields(d.triviaParser, seen) : false)
|
|
28
|
+
case 'scanTo': return parserHasOwnFields(d.sentinel, seen) || d.skip.some(x => parserHasOwnFields(x, seen))
|
|
29
|
+
case 'recover': return parserHasOwnFields(d.parser, seen) || parserHasOwnFields(d.sentinel, seen)
|
|
30
|
+
case 'routed': return d.fallback ? parserHasOwnFields(d.fallback, seen) : false
|
|
31
|
+
case 'many':
|
|
32
|
+
case 'oneOrMore':
|
|
33
|
+
case 'optional':
|
|
34
|
+
case 'attempt':
|
|
35
|
+
case 'transform':
|
|
36
|
+
case 'trivia':
|
|
37
|
+
case 'token':
|
|
38
|
+
case 'leaf':
|
|
39
|
+
case 'label':
|
|
40
|
+
case 'expect':
|
|
41
|
+
case 'withCtx':
|
|
42
|
+
case 'not':
|
|
43
|
+
case 'peek':
|
|
44
|
+
return parserHasOwnFields(d.parser, seen)
|
|
45
|
+
default:
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Does the node's OWN parse frame ever have a trivia run logged into it?
|
|
52
|
+
* Trivia is logged only by the trivia-skip fn, which runs at `sequence`/`many`/
|
|
53
|
+
* repeat boundaries. A bare terminal (regex/literal/keywords/…) has NO trivia
|
|
54
|
+
* site, so its `_cstTriviaLog` stays empty and its `captureTrivia`/`_cstTriviaLog`/
|
|
55
|
+
* `_triviaCaptureMask` save+install+restore is dead work — the per-node scope
|
|
56
|
+
* trivia frame can be elided for it (see emitNode `needsTriviaFrame`).
|
|
57
|
+
*
|
|
58
|
+
* Stops at a nested `node()` (it manages its OWN trivia frame; trivia inside it
|
|
59
|
+
* never logs to THIS node's frame). CONSERVATIVE: any unknown / trivia-bearing
|
|
60
|
+
* shape returns `true` (keep the frame) — we only elide when provably safe.
|
|
61
|
+
*/
|
|
62
|
+
export function parserHasTriviaSite(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
|
|
63
|
+
return hasTriviaSite(p, seen, false)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The same question asked of the ROOT trivia log (`_ctx._rootTriviaLog`) instead
|
|
68
|
+
* of the current node's frame.
|
|
69
|
+
*
|
|
70
|
+
* The two differ in exactly one case, and it is the case that matters: a nested
|
|
71
|
+
* `node()` manages its own FRAME, so nothing inside it reaches the enclosing
|
|
72
|
+
* frame's log — but the root log is not a frame, and every trivia run inside that
|
|
73
|
+
* node still lands in it. Reusing `parserHasTriviaSite` for the root sink
|
|
74
|
+
* therefore under-reports: it dropped the mark on `node()` arms whose trivia does
|
|
75
|
+
* accumulate, and a parse-tree diff caught it as trivia entries that were no
|
|
76
|
+
* longer rewound on a failed choice arm (`length: 3` becoming `length: 6`).
|
|
77
|
+
*/
|
|
78
|
+
export function parserHasRootTriviaSite(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
|
|
79
|
+
return hasTriviaSite(p, seen, true)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hasTriviaSite(p: Combinator<unknown>, seen: Set<Combinator<unknown>>, throughNode: boolean): boolean {
|
|
83
|
+
if (seen.has(p)) return true // cycle → conservative (keep the frame)
|
|
84
|
+
seen.add(p)
|
|
85
|
+
const parserHasTriviaSite = (
|
|
86
|
+
x: Combinator<unknown>,
|
|
87
|
+
s: Set<Combinator<unknown>> = new Set(),
|
|
88
|
+
): boolean => hasTriviaSite(x, s, throughNode)
|
|
89
|
+
const d = p._def
|
|
90
|
+
switch (d.tag) {
|
|
91
|
+
// Trivia is skipped between/around elements or iterations → a site.
|
|
92
|
+
case 'sequence':
|
|
93
|
+
case 'many':
|
|
94
|
+
case 'oneOrMore':
|
|
95
|
+
case 'sepBy':
|
|
96
|
+
case 'scanTo':
|
|
97
|
+
case 'recover':
|
|
98
|
+
return true
|
|
99
|
+
// Nested node manages its own trivia frame; none logs into THIS frame. The
|
|
100
|
+
// root log has no frames, so it must look inside.
|
|
101
|
+
case 'node': return throughNode ? parserHasTriviaSite(d.parser, seen) : false
|
|
102
|
+
// Pure terminals + trivia-suppressing token: no site.
|
|
103
|
+
case 'regex':
|
|
104
|
+
case 'literal':
|
|
105
|
+
case 'keywords':
|
|
106
|
+
case 'guard':
|
|
107
|
+
// The adjacency probe scans with capture OFF, so it logs nothing.
|
|
108
|
+
case 'adjacency':
|
|
109
|
+
case 'token':
|
|
110
|
+
case 'leaf':
|
|
111
|
+
return false
|
|
112
|
+
// In dispatch position routed() reuses the already-consumed selector and has
|
|
113
|
+
// no trivia boundary. Outside dispatch, only its concrete fallback runs.
|
|
114
|
+
case 'routed': return d.fallback ? parserHasTriviaSite(d.fallback, seen) : false
|
|
115
|
+
// Transparent single-child wrappers: recurse.
|
|
116
|
+
case 'optional':
|
|
117
|
+
case 'attempt':
|
|
118
|
+
case 'transform':
|
|
119
|
+
case 'trivia':
|
|
120
|
+
case 'label':
|
|
121
|
+
case 'expect':
|
|
122
|
+
case 'withCtx':
|
|
123
|
+
case 'not':
|
|
124
|
+
case 'peek':
|
|
125
|
+
case 'field':
|
|
126
|
+
return parserHasTriviaSite(d.parser, seen)
|
|
127
|
+
case 'choice': return d.parsers.some(x => parserHasTriviaSite(x, seen))
|
|
128
|
+
case 'dispatch': return parserHasTriviaSite(d.selector, seen) || d.cases.some(x => parserHasTriviaSite(x.parser, seen)) || (d.matchers ? d.matchers.some(entry => parserHasTriviaSite(entry.parser, seen)) : false) || (d.otherwise ? parserHasTriviaSite(d.otherwise, seen) : false)
|
|
129
|
+
case 'grammar': return parserHasTriviaSite(d.parser, seen)
|
|
130
|
+
case 'lazy': {
|
|
131
|
+
try { return parserHasTriviaSite(d.thunk(), seen) } catch { return true }
|
|
132
|
+
}
|
|
133
|
+
default: return true // unknown shape → conservative
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Can this node's inner grammar explicitly enable trivia capture? A surrounding
|
|
139
|
+
* node owns the collector, so a nested `parser({ captureTrivia: true })` must
|
|
140
|
+
* allocate that collector before it enters the grammar scope. Nested node()
|
|
141
|
+
* rules own their separate collector and therefore do not contribute here.
|
|
142
|
+
*/
|
|
143
|
+
export function parserEnablesTriviaCapture(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
|
|
144
|
+
if (seen.has(p)) return false
|
|
145
|
+
seen.add(p)
|
|
146
|
+
const d = p._def
|
|
147
|
+
switch (d.tag) {
|
|
148
|
+
case 'grammar': return d.captureTrivia === true || parserEnablesTriviaCapture(d.parser, seen)
|
|
149
|
+
case 'node': return false
|
|
150
|
+
case 'sequence':
|
|
151
|
+
case 'choice': return d.parsers.some(x => parserEnablesTriviaCapture(x, seen))
|
|
152
|
+
case 'dispatch': return parserEnablesTriviaCapture(d.selector, seen) || d.cases.some(x => parserEnablesTriviaCapture(x.parser, seen)) || (d.matchers ? d.matchers.some(entry => parserEnablesTriviaCapture(entry.parser, seen)) : false) || (d.otherwise ? parserEnablesTriviaCapture(d.otherwise, seen) : false)
|
|
153
|
+
case 'sepBy': return parserEnablesTriviaCapture(d.parser, seen) || parserEnablesTriviaCapture(d.separator, seen)
|
|
154
|
+
case 'scanTo': return parserEnablesTriviaCapture(d.sentinel, seen) || d.skip.some(x => parserEnablesTriviaCapture(x, seen))
|
|
155
|
+
case 'recover': return parserEnablesTriviaCapture(d.parser, seen) || parserEnablesTriviaCapture(d.sentinel, seen)
|
|
156
|
+
case 'routed': return d.fallback ? parserEnablesTriviaCapture(d.fallback, seen) : false
|
|
157
|
+
case 'many':
|
|
158
|
+
case 'oneOrMore':
|
|
159
|
+
case 'optional':
|
|
160
|
+
case 'attempt':
|
|
161
|
+
case 'transform':
|
|
162
|
+
case 'trivia':
|
|
163
|
+
case 'token':
|
|
164
|
+
case 'label':
|
|
165
|
+
case 'field':
|
|
166
|
+
case 'expect':
|
|
167
|
+
case 'withCtx':
|
|
168
|
+
case 'not':
|
|
169
|
+
case 'peek': return parserEnablesTriviaCapture(d.parser, seen)
|
|
170
|
+
// leaf() clears the inner CST/trivia collector and publishes only its own
|
|
171
|
+
// terminal result. An inner parser() capture cannot reach this node.
|
|
172
|
+
case 'leaf': return false
|
|
173
|
+
case 'lazy': {
|
|
174
|
+
try { return parserEnablesTriviaCapture(d.thunk(), seen) } catch { return false }
|
|
175
|
+
}
|
|
176
|
+
default: return false
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function buildFieldMap(captures: ReadonlyArray<{ name: string; value: unknown; span: { start: number; end: number } }> | undefined): FieldMap | undefined {
|
|
181
|
+
if (!captures?.length) return undefined
|
|
182
|
+
const out: FieldMap = {}
|
|
183
|
+
for (const cap of captures) {
|
|
184
|
+
const entry = { value: cap.value, span: cap.span }
|
|
185
|
+
const current = out[cap.name]
|
|
186
|
+
if (current === undefined) out[cap.name] = entry
|
|
187
|
+
else if (Array.isArray(current)) current.push(entry)
|
|
188
|
+
else out[cap.name] = [current, entry]
|
|
189
|
+
}
|
|
190
|
+
return out
|
|
191
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Combinator, ParserDef } from '../types.ts'
|
|
2
|
+
import { recordDegradation } from './degradation.ts'
|
|
3
|
+
|
|
4
|
+
export type GrammarCoverageDefinition = { id: string; kind: 'rule' | 'choice-arm' | 'dispatch-arm' | 'label' }
|
|
5
|
+
export type GrammarCoveragePlan = {
|
|
6
|
+
definitions: readonly GrammarCoverageDefinition[]
|
|
7
|
+
choices: Map<Combinator<unknown>, readonly string[]>
|
|
8
|
+
dispatches: Map<Combinator<unknown>, readonly string[]>
|
|
9
|
+
/** Trace-only transaction identity. Attempts are not coverage definitions: a
|
|
10
|
+
* rejected transaction must not create a semantic winner or denominator. */
|
|
11
|
+
attempts: Map<Combinator<unknown>, string>
|
|
12
|
+
labels: Map<Combinator<unknown>, readonly string[]>
|
|
13
|
+
rules: Map<Combinator<unknown>, string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function idPart(value: string): string {
|
|
17
|
+
return encodeURIComponent(value)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function dispatchArmIds(def: Extract<ParserDef, { tag: 'dispatch' }>, path: string): string[] {
|
|
21
|
+
const ids: string[] = []
|
|
22
|
+
for (const entry of def.cases) ids.push(`dispatch:${path}/when:${entry.keys.map(idPart).join('|')}`)
|
|
23
|
+
for (const entry of def.matchers ?? []) {
|
|
24
|
+
ids.push(`dispatch:${path}/matcher:${entry.kind}:${idPart(entry.value)}${entry.kind === 'matches' && entry.flags ? `:${idPart(entry.flags)}` : ''}`)
|
|
25
|
+
}
|
|
26
|
+
if (def.otherwise) ids.push(`dispatch:${path}/otherwise`)
|
|
27
|
+
return ids
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function children(def: ParserDef, winners?: Record<string, Combinator<unknown>>): Combinator<unknown>[] {
|
|
31
|
+
switch (def.tag) {
|
|
32
|
+
case 'sequence': case 'choice': return def.parsers
|
|
33
|
+
case 'dispatch': return [def.selector, ...def.cases.map(entry => entry.parser), ...(def.matchers ? def.matchers.map(entry => entry.parser) : []), ...(def.otherwise ? [def.otherwise] : [])]
|
|
34
|
+
case 'many': case 'oneOrMore': case 'optional': case 'attempt': case 'transform': case 'trivia': case 'token': case 'leaf': case 'label': case 'field': case 'grammar': case 'not': case 'peek': case 'node': case 'guard': case 'adjacency': case 'withCtx': case 'recover': case 'expect': return 'parser' in def ? [def.parser] : []
|
|
35
|
+
case 'sepBy': return [def.parser, def.separator]
|
|
36
|
+
case 'scanTo': return [def.sentinel, ...def.skip]
|
|
37
|
+
case 'lazy': {
|
|
38
|
+
let resolved: Combinator<unknown>
|
|
39
|
+
try { resolved = def.thunk() } catch (e) {
|
|
40
|
+
// An unresolvable `ref()` / cross-artifact hole truncates the walk, so every
|
|
41
|
+
// definition beneath it is missing from the plan. That SHRINKS the coverage
|
|
42
|
+
// denominator, which pushes the reported ratio UP — a blind spot that reads as
|
|
43
|
+
// better coverage. `analyzeGatingRules` routes the same situation into
|
|
44
|
+
// `unanalysable`; the coverage walk has no such field, so it reports here.
|
|
45
|
+
recordDegradation({
|
|
46
|
+
code: 'coverage-definitions-unavailable',
|
|
47
|
+
severity: 'warn',
|
|
48
|
+
where: '<lazy rule reference>',
|
|
49
|
+
subject: `unresolvable reference (${(e as Error).message})`,
|
|
50
|
+
fellBackTo: 'the coverage walk stopped at this reference, so every definition below '
|
|
51
|
+
+ 'it is ABSENT from the denominator — which raises the reported ratio rather than '
|
|
52
|
+
+ 'lowering it',
|
|
53
|
+
otherwise: 'the referenced rule and its whole subtree would contribute definitions',
|
|
54
|
+
})
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
57
|
+
const name = (resolved as Combinator<unknown> & { _ruleName?: string })._ruleName
|
|
58
|
+
return name && winners?.[name] ? [winners[name]!] : [resolved]
|
|
59
|
+
}
|
|
60
|
+
default: return []
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Canonical deterministic IDs for one explicit entry closure, or every public
|
|
65
|
+
* entry in a `rules()` map. Multiple roots share identity maps so a shared
|
|
66
|
+
* subtree still has one stable owner. */
|
|
67
|
+
export function buildGrammarPlan(entry: Combinator<unknown> | readonly Combinator<unknown>[], winners?: Record<string, Combinator<unknown>>): GrammarCoveragePlan {
|
|
68
|
+
const definitions = new Map<string, GrammarCoverageDefinition>()
|
|
69
|
+
// This plan is per grammar compilation and returned with its definitions; it
|
|
70
|
+
// does not need a process-global weak side cache. Ordinary Maps make that
|
|
71
|
+
// finite ownership explicit and keep WeakMap out of the shipped table graph.
|
|
72
|
+
const choices = new Map<Combinator<unknown>, readonly string[]>()
|
|
73
|
+
const dispatches = new Map<Combinator<unknown>, readonly string[]>()
|
|
74
|
+
const attempts = new Map<Combinator<unknown>, string>()
|
|
75
|
+
const labels = new Map<Combinator<unknown>, readonly string[]>()
|
|
76
|
+
const rules = new Map<Combinator<unknown>, string>()
|
|
77
|
+
// A `rules()` map stores named lazy proxies while code generation may emit
|
|
78
|
+
// either that proxy or its resolved body. Final composed winners are the
|
|
79
|
+
// authority for both identities; relying on `_ruleName` alone loses direct
|
|
80
|
+
// local leaf rules and cross-piece references after IR hydration.
|
|
81
|
+
const winnerNames = new Map<Combinator<unknown>, string>()
|
|
82
|
+
if (winners) {
|
|
83
|
+
for (const [name, winner] of Object.entries(winners)) {
|
|
84
|
+
winnerNames.set(winner, name)
|
|
85
|
+
if (winner._def.tag === 'lazy') {
|
|
86
|
+
try { winnerNames.set(winner._def.thunk(), name) } catch { /* unresolved external stays absent */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const seen = new Set<Combinator<unknown>>()
|
|
91
|
+
const visit = (parser: Combinator<unknown>, path: string): void => {
|
|
92
|
+
if (seen.has(parser)) return
|
|
93
|
+
seen.add(parser)
|
|
94
|
+
const rule = winnerNames.get(parser) ?? (parser as Combinator<unknown> & { _ruleName?: string })._ruleName
|
|
95
|
+
// `rules()` references are tagged too for linker naming, but only the final
|
|
96
|
+
// rule definition owns execution coverage; otherwise ref + target double-hit.
|
|
97
|
+
if (rule && (winnerNames.has(parser) || parser._def.tag !== 'lazy')) {
|
|
98
|
+
const id = `rule:${rule}`
|
|
99
|
+
definitions.set(id, { id, kind: 'rule' })
|
|
100
|
+
rules.set(parser, id)
|
|
101
|
+
}
|
|
102
|
+
if (parser._def.tag === 'choice') {
|
|
103
|
+
const ids = parser._def.parsers.map((_, index) => `choice:${path}/arm:${index}`)
|
|
104
|
+
choices.set(parser, ids)
|
|
105
|
+
ids.forEach(id => definitions.set(id, { id, kind: 'choice-arm' }))
|
|
106
|
+
}
|
|
107
|
+
if (parser._def.tag === 'dispatch') {
|
|
108
|
+
const ids = dispatchArmIds(parser._def, path)
|
|
109
|
+
dispatches.set(parser, ids)
|
|
110
|
+
ids.forEach(id => definitions.set(id, { id, kind: 'dispatch-arm' }))
|
|
111
|
+
}
|
|
112
|
+
if (parser._def.tag === 'attempt') attempts.set(parser, `attempt:${path}`)
|
|
113
|
+
if (parser._def.tag === 'label') {
|
|
114
|
+
const id = `label:${path}`
|
|
115
|
+
labels.set(parser, [id])
|
|
116
|
+
definitions.set(id, { id, kind: 'label' })
|
|
117
|
+
}
|
|
118
|
+
children(parser._def, winners).forEach((child, index) => visit(child, `${path}/${parser._def.tag}:${index}`))
|
|
119
|
+
}
|
|
120
|
+
const roots = Array.isArray(entry) ? entry : [entry]
|
|
121
|
+
for (const root of roots) {
|
|
122
|
+
// Rule-map compilation can receive ordinary, unannotated combinators. The
|
|
123
|
+
// caller's winner map is then the only durable public identity for each
|
|
124
|
+
// root; using the generic `entry` path would merge separate roots' choice
|
|
125
|
+
// IDs and omit their rule definitions.
|
|
126
|
+
visit(root, winnerNames.get(root) ?? (root as Combinator<unknown> & { _ruleName?: string })._ruleName ?? 'entry')
|
|
127
|
+
}
|
|
128
|
+
return { definitions: [...definitions.values()].sort((a, b) => a.id.localeCompare(b.id)), choices, dispatches, attempts, labels, rules }
|
|
129
|
+
}
|