parseman 0.46.0 → 0.47.1
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-render.d.ts +12 -4
- package/dist/analysis/choice-cost-render.d.ts.map +1 -1
- package/dist/analysis/choice-cost.d.ts +2 -2
- package/dist/analysis/choice-cost.d.ts.map +1 -1
- package/dist/analysis/commitment.d.ts +131 -0
- package/dist/analysis/commitment.d.ts.map +1 -0
- package/dist/analysis/diagnostics.cjs +10339 -7787
- package/dist/analysis/diagnostics.cjs.map +3 -4
- package/dist/analysis/diagnostics.d.ts +2 -2
- package/dist/analysis/diagnostics.js +10339 -7787
- package/dist/analysis/diagnostics.js.map +3 -4
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/fix.d.ts +3 -3
- package/dist/analysis/fix.d.ts.map +1 -1
- 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 +39 -2
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/rebuild.d.ts.map +1 -1
- package/dist/analysis/terminal.d.ts +5 -2
- package/dist/analysis/terminal.d.ts.map +1 -1
- package/dist/cli/index.cjs +10924 -8506
- package/dist/cli/index.cjs.map +3 -4
- package/dist/cli/index.js +10924 -8506
- package/dist/cli/index.js.map +3 -4
- 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 +45 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/combinators/withCtx.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 +47 -53
- 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 +9290 -7181
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +18 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9300 -7191
- package/dist/index.js.map +3 -4
- package/dist/language-service/index.cjs +313 -121
- package/dist/language-service/index.cjs.map +3 -4
- package/dist/language-service/index.js +313 -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 +40 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.cjs +11175 -9534
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11175 -9534
- package/dist/plugin/index.js.map +3 -4
- 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 +2934 -1611
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2934 -1611
- 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 +11463 -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 +11440 -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 +453 -0
- package/dist/table/ops.d.ts.map +1 -0
- package/dist/table/program.d.ts +496 -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 +26 -2
- 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 +348 -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 +237 -0
- package/src/combinators/token.ts +120 -0
- package/src/combinators/trivia-boundary.ts +48 -0
- package/src/combinators/trivia-skip.ts +852 -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 +3006 -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 +1777 -0
- package/src/table/emit.ts +429 -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 +1680 -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 +467 -0
- package/src/table/program.ts +876 -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/module-hoist.d.ts +0 -106
- package/dist/compiler/module-hoist.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
package/src/coverage.ts
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { run, type RunOptions, type RunResult, type Runnable } from './functional/run.ts'
|
|
2
|
+
import { GRAMMAR_COVERAGE_DEFINITIONS } from './grammar-metadata.ts'
|
|
3
|
+
export { GRAMMAR_COVERAGE_DEFINITIONS } from './grammar-metadata.ts'
|
|
4
|
+
import type { Combinator, DispatchCase, DispatchMatcherCase, ParseContext, ParseResult } from './types.ts'
|
|
5
|
+
import { choice } from './combinators/choice.ts'
|
|
6
|
+
import { dispatch, endsWith, matches, otherwise, startsWith, when, type DispatchArm, type DispatchStringMatcher } from './combinators/dispatch.ts'
|
|
7
|
+
import { attempt } from './combinators/attempt.ts'
|
|
8
|
+
import { getCoreLiteralValue } from './combinators/choice.ts'
|
|
9
|
+
import { not } from './combinators/not.ts'
|
|
10
|
+
import { peek } from './combinators/peek.ts'
|
|
11
|
+
import { node } from './combinators/node.ts'
|
|
12
|
+
import { parser as grammarParser } from './combinators/grammar.ts'
|
|
13
|
+
import { expect } from './combinators/expect.ts'
|
|
14
|
+
import { gate } from './combinators/gate.ts'
|
|
15
|
+
import { label, field, transform, trivia } from './combinators/map.ts'
|
|
16
|
+
import { many, oneOrMore, optional, sepBy } from './combinators/repeat.ts'
|
|
17
|
+
import { scanTo } from './combinators/scanTo.ts'
|
|
18
|
+
import { sequence } from './combinators/sequence.ts'
|
|
19
|
+
import { token } from './combinators/token.ts'
|
|
20
|
+
import { leaf } from './combinators/token.ts'
|
|
21
|
+
import { withCtx } from './combinators/withCtx.ts'
|
|
22
|
+
import { composedCoverageRules } from './compiler/linker.ts'
|
|
23
|
+
import { buildGrammarPlan, type GrammarCoverageDefinition, type GrammarCoveragePlan } from './compiler/grammar-coverage-ids.ts'
|
|
24
|
+
|
|
25
|
+
export type { GrammarCoverageDefinition } from './compiler/grammar-coverage-ids.ts'
|
|
26
|
+
|
|
27
|
+
function dispatchMatcher(entry: DispatchMatcherCase): DispatchStringMatcher {
|
|
28
|
+
switch (entry.kind) {
|
|
29
|
+
case 'startsWith':
|
|
30
|
+
return startsWith(entry.value)
|
|
31
|
+
case 'endsWith':
|
|
32
|
+
return endsWith(entry.value)
|
|
33
|
+
case 'matches':
|
|
34
|
+
return matches(new RegExp(entry.value, entry.flags))
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
export type GrammarCoverageSnapshot = {
|
|
40
|
+
definitions: readonly GrammarCoverageDefinition[]
|
|
41
|
+
hits: readonly string[]
|
|
42
|
+
unhit: readonly string[]
|
|
43
|
+
/**
|
|
44
|
+
* Hit fraction, or `NaN` when there was NOTHING TO MEASURE.
|
|
45
|
+
*
|
|
46
|
+
* An empty definition set is not full coverage. This used to report `1` for it, so a
|
|
47
|
+
* grammar whose definitions failed to load — the exact case
|
|
48
|
+
* `'coverage-definitions-unavailable'` exists for — presented as 100% covered, and any
|
|
49
|
+
* consumer gate of the shape `ratio >= threshold` passed on zero evidence. `NaN`
|
|
50
|
+
* compares false against every threshold, so the gate now FAILS CLOSED by default
|
|
51
|
+
* instead of passing by default. Read `measurable` to tell "nothing to measure" from a
|
|
52
|
+
* genuine 0%.
|
|
53
|
+
*/
|
|
54
|
+
ratio: number
|
|
55
|
+
/** False when the definition set was empty, i.e. `ratio` is not a measurement. */
|
|
56
|
+
measurable: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type GrammarCoverageCollector = {
|
|
60
|
+
hit(id: string): void
|
|
61
|
+
snapshot(): GrammarCoverageSnapshot
|
|
62
|
+
reset(): void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Metadata emitted only by a coverage-enabled macro grammar. It is attached to
|
|
66
|
+
* the grammar map, rather than each rule function, so a caller chooses the
|
|
67
|
+
* explicit public start rule before collecting results. */
|
|
68
|
+
export function compiledGrammarCoverageDefinitions(grammar: Record<string, unknown>): readonly GrammarCoverageDefinition[] {
|
|
69
|
+
const definitions = (grammar as Record<symbol, unknown>)[GRAMMAR_COVERAGE_DEFINITIONS]
|
|
70
|
+
// `definitions.length === 0` is checked EXPLICITLY. `[].every(...)` is vacuously true,
|
|
71
|
+
// so an empty array sailed through this guard and was returned as a valid definition
|
|
72
|
+
// set — the one input whose name the error message spells out was the one input it let
|
|
73
|
+
// past, and it went on to report 100% coverage downstream.
|
|
74
|
+
if (!Array.isArray(definitions)
|
|
75
|
+
|| definitions.length === 0
|
|
76
|
+
|| !definitions.every((definition): definition is GrammarCoverageDefinition => typeof definition === 'object'
|
|
77
|
+
&& definition !== null
|
|
78
|
+
&& typeof definition.id === 'string'
|
|
79
|
+
&& (definition.kind === 'rule' || definition.kind === 'choice-arm' || definition.kind === 'dispatch-arm' || definition.kind === 'label'))) {
|
|
80
|
+
throw new TypeError('grammar has no coverage definitions; enable grammarCoverage for this macro build')
|
|
81
|
+
}
|
|
82
|
+
return definitions
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type GrammarTracePhase = 'enter' | 'attempt' | 'selected' | 'success' | 'failure' | 'backtrack' | 'rollback'
|
|
86
|
+
export type GrammarTraceEvent = { id: string; phase: GrammarTracePhase; offset: number; end?: number }
|
|
87
|
+
export type GrammarTraceSnapshot = { events: readonly GrammarTraceEvent[]; truncated: boolean; dropped: number }
|
|
88
|
+
export type GrammarTraceSink = { write(event: GrammarTraceEvent): void; snapshot(): GrammarTraceSnapshot }
|
|
89
|
+
/** Typed context for a coverage-enabled compiled or macro-generated parser. */
|
|
90
|
+
export type GrammarInstrumentationContext = ParseContext & {
|
|
91
|
+
_grammarCoverage?: (id: string) => void
|
|
92
|
+
_grammarTrace?: GrammarTraceSink
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Create the opt-in context consumed by coverage-enabled compiled output. */
|
|
96
|
+
export function createGrammarInstrumentationContext(options: {
|
|
97
|
+
collector?: GrammarCoverageCollector
|
|
98
|
+
trace?: GrammarTraceSink
|
|
99
|
+
trackLines?: boolean
|
|
100
|
+
state?: unknown
|
|
101
|
+
} = {}): GrammarInstrumentationContext {
|
|
102
|
+
return {
|
|
103
|
+
trackLines: options.trackLines ?? false,
|
|
104
|
+
...(options.state === undefined ? {} : { state: options.state }),
|
|
105
|
+
...(options.collector === undefined ? {} : { _grammarCoverage: (id: string) => options.collector!.hit(id) }),
|
|
106
|
+
...(options.trace === undefined ? {} : { _grammarTrace: options.trace }),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Bounded first-N trace sink. Exceptions and a false callback detach without
|
|
111
|
+
* changing parser results; post-detach events count as dropped. */
|
|
112
|
+
export function createGrammarTraceSink(options: { capacity: number; write?: (event: GrammarTraceEvent) => boolean | void }): GrammarTraceSink {
|
|
113
|
+
if (!Number.isInteger(options.capacity) || options.capacity < 0) throw new TypeError('trace capacity must be a finite non-negative integer')
|
|
114
|
+
const events: GrammarTraceEvent[] = []
|
|
115
|
+
let detached = false
|
|
116
|
+
let truncated = false
|
|
117
|
+
let dropped = 0
|
|
118
|
+
return {
|
|
119
|
+
write(event) {
|
|
120
|
+
if (detached || events.length >= options.capacity) {
|
|
121
|
+
detached = true; truncated = true; dropped++; return
|
|
122
|
+
}
|
|
123
|
+
events.push(Object.freeze({ ...event }))
|
|
124
|
+
try {
|
|
125
|
+
if (options.write?.(event) === false) { detached = true; truncated = true }
|
|
126
|
+
} catch { detached = true; truncated = true }
|
|
127
|
+
},
|
|
128
|
+
snapshot() { return Object.freeze({ events: Object.freeze([...events]), truncated, dropped }) },
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createGrammarCoverageCollector(definitions: readonly GrammarCoverageDefinition[]): GrammarCoverageCollector {
|
|
133
|
+
const ordered = [...definitions].sort((a, b) => a.id.localeCompare(b.id))
|
|
134
|
+
const known = new Set(ordered.map(definition => definition.id))
|
|
135
|
+
const hits = new Set<string>()
|
|
136
|
+
return {
|
|
137
|
+
hit(id) { if (known.has(id)) hits.add(id) },
|
|
138
|
+
snapshot() {
|
|
139
|
+
const hitList = [...hits].sort()
|
|
140
|
+
const unhit = ordered.map(definition => definition.id).filter(id => !hits.has(id))
|
|
141
|
+
// No `ordered.length === 0 ? 1` default. 0/0 is NaN, which is the honest answer and
|
|
142
|
+
// fails every `>=` threshold; reporting 1 asserted total coverage of nothing.
|
|
143
|
+
return {
|
|
144
|
+
definitions: ordered,
|
|
145
|
+
hits: hitList,
|
|
146
|
+
unhit,
|
|
147
|
+
ratio: hitList.length / ordered.length,
|
|
148
|
+
measurable: ordered.length > 0,
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
reset() { hits.clear() },
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function grammarCoverageDefinitions(entry: Combinator<unknown>, winners?: Record<string, Combinator<unknown>>): readonly GrammarCoverageDefinition[] {
|
|
156
|
+
return buildGrammarPlan(entry, winners).definitions
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Definitions for a runtime `compose()` result, normalized through its final
|
|
160
|
+
* IR winner map. Opaque precompiled pieces intentionally fail rather than
|
|
161
|
+
* reporting source-piece identities as though they were final grammar IDs. */
|
|
162
|
+
export function composedGrammarCoverageDefinitions(grammar: Record<string, unknown>, startRule: string): readonly GrammarCoverageDefinition[] {
|
|
163
|
+
const winners = composedCoverageRules(grammar)
|
|
164
|
+
if (!winners) throw new TypeError('semantic coverage needs re-lowerable composed IR; this composition contains an opaque artifact')
|
|
165
|
+
const entry = winners[startRule]
|
|
166
|
+
if (!entry) throw new TypeError(`semantic coverage start rule ${JSON.stringify(startRule)} is not a final winner`)
|
|
167
|
+
return grammarCoverageDefinitions(entry, winners)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
type CoverageMaps = Pick<GrammarCoveragePlan, 'choices' | 'dispatches' | 'attempts' | 'labels' | 'rules'>
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build a separate interpreter graph for the coverage run. It never mutates the
|
|
174
|
+
* source grammar and ordinary Parseman combinators never branch on coverage.
|
|
175
|
+
*
|
|
176
|
+
* Every structural ParserDef is rebuilt from coverage-aware children. Terminal
|
|
177
|
+
* recognizers are safely shared because they contain no child execution.
|
|
178
|
+
*/
|
|
179
|
+
function coverageEntry(entry: Combinator<unknown>, collector: GrammarCoverageCollector, maps: CoverageMaps, trace?: GrammarTraceSink): Combinator<unknown> {
|
|
180
|
+
const cache = new WeakMap<Combinator<unknown>, Combinator<unknown>>()
|
|
181
|
+
const build = (parser: Combinator<unknown>): Combinator<unknown> => {
|
|
182
|
+
const existing = cache.get(parser)
|
|
183
|
+
if (existing) return existing
|
|
184
|
+
const rule = maps.rules.get(parser)
|
|
185
|
+
const wrap = (parse: (input: string, pos: number, ctx: ParseContext) => ParseResult<unknown>): Combinator<unknown> => {
|
|
186
|
+
const wrapped: Combinator<unknown> = {
|
|
187
|
+
_tag: parser._tag,
|
|
188
|
+
_meta: parser._meta,
|
|
189
|
+
_def: parser._def,
|
|
190
|
+
parse(input, pos, ctx) {
|
|
191
|
+
if (rule) { collector.hit(rule); trace?.write({ id: rule, phase: 'enter', offset: pos }) }
|
|
192
|
+
const result = parse(input, pos, ctx)
|
|
193
|
+
if (rule) trace?.write(result.ok ? { id: rule, phase: 'success', offset: pos, end: result.span.end } : { id: rule, phase: 'failure', offset: result.span.end })
|
|
194
|
+
return result
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
cache.set(parser, wrapped)
|
|
198
|
+
return wrapped
|
|
199
|
+
}
|
|
200
|
+
const def = parser._def
|
|
201
|
+
if (def.tag === 'lazy') {
|
|
202
|
+
return wrap((input, pos, ctx) => build(def.thunk()).parse(input, pos, ctx))
|
|
203
|
+
}
|
|
204
|
+
if (def.tag === 'label') {
|
|
205
|
+
const base = label(def.label, build(def.parser))
|
|
206
|
+
return wrap((input, pos, ctx) => {
|
|
207
|
+
const result = base.parse(input, pos, ctx)
|
|
208
|
+
if (result.ok) for (const id of maps.labels.get(parser) ?? []) { collector.hit(id); trace?.write({ id, phase: 'success', offset: pos, end: result.span.end }) }
|
|
209
|
+
return result
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
if (def.tag === 'choice') {
|
|
213
|
+
const ids = maps.choices.get(parser) ?? []
|
|
214
|
+
type ChoiceState = { lastSuccessfulArm: number | undefined; successfulArms: number[] }
|
|
215
|
+
const invocations: ChoiceState[] = []
|
|
216
|
+
const arms = def.parsers.map((arm, index) => {
|
|
217
|
+
const child = build(arm)
|
|
218
|
+
return {
|
|
219
|
+
_tag: child._tag,
|
|
220
|
+
_meta: child._meta,
|
|
221
|
+
_def: child._def,
|
|
222
|
+
parse(input: string, pos: number, ctx: ParseContext) {
|
|
223
|
+
const id = ids[index]
|
|
224
|
+
if (id) trace?.write({ id, phase: 'attempt', offset: pos })
|
|
225
|
+
const result = child.parse(input, pos, ctx)
|
|
226
|
+
if (result.ok) {
|
|
227
|
+
// Greedy classification runs its super arm as an implementation
|
|
228
|
+
// detail, then chooses the final semantic arm below. Only that
|
|
229
|
+
// final arm belongs in coverage.
|
|
230
|
+
if (def.strategy.tag !== 'greedyClassify') collector.hit(ids[index]!)
|
|
231
|
+
const invocation = invocations[invocations.length - 1]
|
|
232
|
+
if (invocation) { invocation.lastSuccessfulArm = index; invocation.successfulArms.push(index) }
|
|
233
|
+
}
|
|
234
|
+
else if (id) { trace?.write({ id, phase: 'failure', offset: result.span.end }); trace?.write({ id, phase: 'backtrack', offset: pos }) }
|
|
235
|
+
return result
|
|
236
|
+
},
|
|
237
|
+
} satisfies Combinator<unknown>
|
|
238
|
+
})
|
|
239
|
+
const args = arms.map((arm, index) => def.gates[index] ? { combinator: arm, gate: def.gates[index]! } : arm)
|
|
240
|
+
const unsafeChoice = choice as unknown as (...items: unknown[]) => Combinator<unknown>
|
|
241
|
+
const baseChoice = unsafeChoice(...args)
|
|
242
|
+
return wrap((input, pos, ctx) => {
|
|
243
|
+
const invocation: ChoiceState = { lastSuccessfulArm: undefined, successfulArms: [] }
|
|
244
|
+
invocations.push(invocation)
|
|
245
|
+
const result = baseChoice.parse(input, pos, ctx)
|
|
246
|
+
const strategy = def.strategy
|
|
247
|
+
if (result.ok && strategy.tag === 'greedyClassify') {
|
|
248
|
+
const text = input.slice(pos, result.span.end)
|
|
249
|
+
const classified = def.parsers.findIndex((arm, index) => index !== strategy.superIndex && getCoreLiteralValue(arm) === text)
|
|
250
|
+
if (classified !== -1) invocation.lastSuccessfulArm = classified
|
|
251
|
+
else invocation.lastSuccessfulArm = strategy.superIndex
|
|
252
|
+
}
|
|
253
|
+
if (result.ok && invocation.lastSuccessfulArm !== undefined) {
|
|
254
|
+
const id = ids[invocation.lastSuccessfulArm]
|
|
255
|
+
for (const rejected of invocation.successfulArms) {
|
|
256
|
+
if (rejected !== invocation.lastSuccessfulArm) {
|
|
257
|
+
const rejectedId = ids[rejected]
|
|
258
|
+
if (rejectedId) { trace?.write({ id: rejectedId, phase: 'failure', offset: pos }); trace?.write({ id: rejectedId, phase: 'backtrack', offset: pos }) }
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (id) {
|
|
262
|
+
collector.hit(id)
|
|
263
|
+
trace?.write({ id, phase: 'selected', offset: pos, end: result.span.end })
|
|
264
|
+
trace?.write({ id, phase: 'success', offset: pos, end: result.span.end })
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
invocations.pop()
|
|
268
|
+
return result
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
const base: Combinator<unknown> = (() => {
|
|
272
|
+
switch (def.tag) {
|
|
273
|
+
case 'sequence': {
|
|
274
|
+
const unsafeSequence = sequence as unknown as (...items: Combinator<unknown>[]) => Combinator<unknown>
|
|
275
|
+
return unsafeSequence(...def.parsers.map(build))
|
|
276
|
+
}
|
|
277
|
+
case 'dispatch': {
|
|
278
|
+
const ids = maps.dispatches.get(parser) ?? []
|
|
279
|
+
let armIndex = 0
|
|
280
|
+
const dispatchStarts: number[] = []
|
|
281
|
+
const wrapArm = (arm: Combinator<unknown>, id: string | undefined): Combinator<unknown> => {
|
|
282
|
+
const child = build(arm)
|
|
283
|
+
return {
|
|
284
|
+
_tag: child._tag,
|
|
285
|
+
_meta: child._meta,
|
|
286
|
+
_def: child._def,
|
|
287
|
+
parse(input: string, pos: number, ctx: ParseContext) {
|
|
288
|
+
const dispatchStart = dispatchStarts[dispatchStarts.length - 1] ?? pos
|
|
289
|
+
if (id) trace?.write({ id, phase: 'attempt', offset: dispatchStart })
|
|
290
|
+
const result = child.parse(input, pos, ctx)
|
|
291
|
+
if (result.ok) {
|
|
292
|
+
if (id) {
|
|
293
|
+
collector.hit(id)
|
|
294
|
+
trace?.write({ id, phase: 'selected', offset: dispatchStart, end: result.span.end })
|
|
295
|
+
trace?.write({ id, phase: 'success', offset: dispatchStart, end: result.span.end })
|
|
296
|
+
}
|
|
297
|
+
} else if (id) {
|
|
298
|
+
trace?.write({ id, phase: 'failure', offset: result.span.end })
|
|
299
|
+
}
|
|
300
|
+
return result
|
|
301
|
+
},
|
|
302
|
+
} satisfies Combinator<unknown>
|
|
303
|
+
}
|
|
304
|
+
const arms: DispatchArm<unknown>[] = def.cases.map((entry: DispatchCase) =>
|
|
305
|
+
when(entry.keys, wrapArm(entry.parser, ids[armIndex++]), entry.caseInsensitive ? { caseInsensitive: true } : undefined)
|
|
306
|
+
)
|
|
307
|
+
if (def.matchers) {
|
|
308
|
+
for (const entry of def.matchers) {
|
|
309
|
+
arms.push(when(dispatchMatcher(entry), wrapArm(entry.parser, ids[armIndex++]), entry.caseInsensitive ? { caseInsensitive: true } : undefined))
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (def.otherwise) arms.push(otherwise(wrapArm(def.otherwise, ids[armIndex++])))
|
|
313
|
+
const unsafeDispatch = dispatch as (selector: Combinator<string>, ...items: DispatchArm<unknown>[]) => Combinator<unknown>
|
|
314
|
+
const base = unsafeDispatch(build(def.selector) as Combinator<string>, ...arms)
|
|
315
|
+
return {
|
|
316
|
+
...base,
|
|
317
|
+
parse(input, pos, ctx) {
|
|
318
|
+
dispatchStarts.push(pos)
|
|
319
|
+
try {
|
|
320
|
+
return base.parse(input, pos, ctx)
|
|
321
|
+
} finally {
|
|
322
|
+
dispatchStarts.pop()
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
case 'many': return many(build(def.parser))
|
|
328
|
+
case 'oneOrMore': return oneOrMore(build(def.parser))
|
|
329
|
+
case 'optional': return optional(build(def.parser))
|
|
330
|
+
case 'attempt': {
|
|
331
|
+
const base = attempt(build(def.parser))
|
|
332
|
+
const id = maps.attempts.get(parser)
|
|
333
|
+
return id === undefined ? base : {
|
|
334
|
+
...base,
|
|
335
|
+
parse(input, pos, ctx) {
|
|
336
|
+
const result = base.parse(input, pos, ctx)
|
|
337
|
+
if (!result.ok) trace?.write({ id, phase: 'rollback', offset: pos })
|
|
338
|
+
return result
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
case 'sepBy': return sepBy(build(def.parser), build(def.separator), { min: def.min, ...(def.max === undefined ? {} : { max: def.max }), ...(def.trailing === undefined ? {} : { trailing: def.trailing }) })
|
|
343
|
+
case 'transform': return transform(build(def.parser), def.fn)
|
|
344
|
+
case 'trivia': return trivia(build(def.parser))
|
|
345
|
+
case 'token': return token(build(def.parser))
|
|
346
|
+
case 'leaf': return leaf(build(def.parser), def.fn)
|
|
347
|
+
case 'field': return field(def.name, build(def.parser))
|
|
348
|
+
case 'grammar': return grammarParser({
|
|
349
|
+
...(def.triviaParser === undefined ? (def.clearTrivia ? { trivia: null } : {}) : { trivia: build(def.triviaParser) }),
|
|
350
|
+
...(def.captureTrivia ? { captureTrivia: true } : {}),
|
|
351
|
+
...(def.trackLines ? { trackLines: true } : {}),
|
|
352
|
+
}, build(def.parser))
|
|
353
|
+
case 'not': return not(build(def.parser))
|
|
354
|
+
case 'peek': return peek(build(def.parser))
|
|
355
|
+
case 'node': {
|
|
356
|
+
const opts = { ...(def.unwrap ? { unwrap: true } : {}), ...(def.collapse ? { collapse: true } : {}), ...(def.project !== undefined ? { project: def.project } : {}), ...(def.captureTrivia ? { captureTrivia: true } : {}), ...(def.trailingTrivia ? { trailingTrivia: true } : {}) }
|
|
357
|
+
return def.type === undefined ? node(build(def.parser), def.build, opts) : node(def.type, build(def.parser), def.build, opts)
|
|
358
|
+
}
|
|
359
|
+
case 'guard': return gate(def.predicate)
|
|
360
|
+
case 'withCtx': return withCtx(def.extra, build(def.parser))
|
|
361
|
+
case 'expect': return expect(build(def.parser), def.label)
|
|
362
|
+
case 'scanTo': return scanTo(build(def.sentinel), { skip: def.skip.map(build), orEOF: def.orEOF })
|
|
363
|
+
// Zero-width assertion with no coverage-bearing child; reuse it verbatim so
|
|
364
|
+
// the rebuilt sequence still recognises the boundary marker.
|
|
365
|
+
case 'adjacency': return parser
|
|
366
|
+
case 'literal': case 'regex': case 'keywords': return parser
|
|
367
|
+
case 'recover': case 'unknown': throw new TypeError(`runWithGrammarCoverage does not yet support ${def.tag}`)
|
|
368
|
+
default: return parser
|
|
369
|
+
}
|
|
370
|
+
})()
|
|
371
|
+
return wrap((input, pos, ctx) => base.parse(input, pos, ctx))
|
|
372
|
+
}
|
|
373
|
+
return build(entry)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function runWithGrammarCoverage(entry: Runnable, input: string, options: RunOptions & { collector?: GrammarCoverageCollector; trace?: GrammarTraceSink } = {}): { result: RunResult; coverage: GrammarCoverageSnapshot } {
|
|
377
|
+
if (typeof entry === 'function') throw new TypeError('runWithGrammarCoverage currently requires an interpreter combinator entry')
|
|
378
|
+
const plan = buildGrammarPlan(entry)
|
|
379
|
+
const definitions = plan.definitions
|
|
380
|
+
const collector = options.collector ?? createGrammarCoverageCollector(definitions)
|
|
381
|
+
const { collector: _collector, trace, ...runOptions } = options
|
|
382
|
+
return { result: run(coverageEntry(entry, collector, plan, trace), input, runOptions), coverage: collector.snapshot() }
|
|
383
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import type { ParseContext, Span } from '../types.ts'
|
|
2
|
+
import { annotateSpanFromLineContext } from '../line-index.ts'
|
|
3
|
+
|
|
4
|
+
/** Lazy per-node capture state — arrays materialized on first push. */
|
|
5
|
+
export type CstCaptureBuf = {
|
|
6
|
+
/** Table-host specialisation: collect source-order raw entries without the
|
|
7
|
+
* duplicate semantic children view. Selected once for the enclosing node. */
|
|
8
|
+
rawOnly?: true | undefined
|
|
9
|
+
single?: unknown | undefined
|
|
10
|
+
ch?: unknown[] | undefined
|
|
11
|
+
rawSingle?: unknown | undefined
|
|
12
|
+
raw?: unknown[] | undefined
|
|
13
|
+
tl?: number[] | undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const EMPTY: unknown[] = []
|
|
17
|
+
const EMPTY_TL: number[] = []
|
|
18
|
+
|
|
19
|
+
export function cstCaptureActive(ctx: ParseContext): boolean {
|
|
20
|
+
return ctx._cstBuf !== undefined || ctx._cstLeaves !== undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type CstNodeCaptureSaved = {
|
|
24
|
+
ch: ParseContext['_cstChildren']
|
|
25
|
+
lv: ParseContext['_cstLeaves']
|
|
26
|
+
raw: ParseContext['_cstRawChildren']
|
|
27
|
+
tl: ParseContext['_cstTriviaLog']
|
|
28
|
+
cap: boolean | undefined
|
|
29
|
+
buf: CstCaptureBuf | undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function beginCstNodeCapture(ctx: ParseContext): CstNodeCaptureSaved {
|
|
33
|
+
const saved: CstNodeCaptureSaved = {
|
|
34
|
+
ch: ctx._cstChildren,
|
|
35
|
+
lv: ctx._cstLeaves,
|
|
36
|
+
raw: ctx._cstRawChildren,
|
|
37
|
+
tl: ctx._cstTriviaLog,
|
|
38
|
+
cap: ctx.captureTrivia,
|
|
39
|
+
buf: ctx._cstBuf,
|
|
40
|
+
}
|
|
41
|
+
ctx._cstBuf = {}
|
|
42
|
+
ctx._cstChildren = undefined
|
|
43
|
+
ctx._cstLeaves = undefined
|
|
44
|
+
ctx._cstRawChildren = undefined
|
|
45
|
+
ctx._cstTriviaLog = undefined
|
|
46
|
+
ctx.captureTrivia = true
|
|
47
|
+
return saved
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function finishCstBuf(buf: CstCaptureBuf | undefined): {
|
|
51
|
+
children: unknown[]
|
|
52
|
+
rawChildren: unknown[]
|
|
53
|
+
triviaLog: number[]
|
|
54
|
+
} {
|
|
55
|
+
if (!buf) return { children: EMPTY, rawChildren: EMPTY, triviaLog: EMPTY_TL }
|
|
56
|
+
const children = buf.ch ?? (buf.single !== undefined ? [buf.single] : EMPTY)
|
|
57
|
+
const rawChildren = buf.raw ?? (buf.rawSingle !== undefined ? [buf.rawSingle] : EMPTY)
|
|
58
|
+
const triviaLog = buf.tl ?? EMPTY_TL
|
|
59
|
+
return { children, rawChildren, triviaLog }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function endCstNodeCapture(
|
|
63
|
+
ctx: ParseContext,
|
|
64
|
+
saved: CstNodeCaptureSaved,
|
|
65
|
+
): { children: unknown[]; rawChildren: unknown[]; triviaLog: number[] } {
|
|
66
|
+
const materialized = finishCstBuf(ctx._cstBuf)
|
|
67
|
+
ctx._cstBuf = saved.buf
|
|
68
|
+
ctx._cstChildren = saved.ch
|
|
69
|
+
ctx._cstLeaves = saved.lv
|
|
70
|
+
ctx._cstRawChildren = saved.raw
|
|
71
|
+
ctx._cstTriviaLog = saved.tl
|
|
72
|
+
ctx.captureTrivia = saved.cap
|
|
73
|
+
return materialized
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function pushCstLeaf(ctx: ParseContext, leaf: unknown): void {
|
|
77
|
+
if (!ctx.trackLines) {
|
|
78
|
+
pushCstChild(ctx, leaf, leaf)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
const annotated = leaf && typeof leaf === 'object' && (leaf as { span?: unknown }).span
|
|
82
|
+
? { ...(leaf as Record<string, unknown>), span: annotateSpanFromLineContext((leaf as { span: Span }).span, ctx) }
|
|
83
|
+
: leaf
|
|
84
|
+
pushCstChild(ctx, annotated, annotated)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Record a built sub-node into the active collector (children vs rawChildren may differ). */
|
|
88
|
+
export function pushCstChild(ctx: ParseContext, built: unknown, rawEntry: unknown): void {
|
|
89
|
+
const b = ctx._cstBuf
|
|
90
|
+
if (b) {
|
|
91
|
+
if (b.rawOnly !== true) {
|
|
92
|
+
if (b.ch) b.ch.push(built)
|
|
93
|
+
else if (b.single !== undefined) { b.ch = [b.single, built]; b.single = undefined }
|
|
94
|
+
else b.single = built
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (b.raw) b.raw.push(rawEntry)
|
|
98
|
+
else if (b.rawSingle !== undefined) { b.raw = [b.rawSingle, rawEntry]; b.rawSingle = undefined }
|
|
99
|
+
else b.rawSingle = rawEntry
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
if (ctx._cstChildren) ctx._cstChildren.push(built)
|
|
103
|
+
else if (ctx._cstLeaves) ctx._cstLeaves.push(built)
|
|
104
|
+
if (ctx._cstRawChildren) ctx._cstRawChildren.push(rawEntry)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function cstRawLen(ctx: ParseContext): number {
|
|
108
|
+
const b = ctx._cstBuf
|
|
109
|
+
if (b) {
|
|
110
|
+
if (b.raw) return b.raw.length
|
|
111
|
+
return b.rawSingle !== undefined ? 1 : 0
|
|
112
|
+
}
|
|
113
|
+
return ctx._cstRawChildren?.length ?? 0
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function cstLeavesLen(ctx: ParseContext): number {
|
|
117
|
+
const b = ctx._cstBuf
|
|
118
|
+
if (b) {
|
|
119
|
+
if (b.ch) return b.ch.length
|
|
120
|
+
return b.single !== undefined ? 1 : 0
|
|
121
|
+
}
|
|
122
|
+
return ctx._cstLeaves?.length ?? 0
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function cstTlLen(ctx: ParseContext): number {
|
|
126
|
+
const b = ctx._cstBuf
|
|
127
|
+
if (b) return b.tl?.length ?? 0
|
|
128
|
+
return ctx._cstTriviaLog?.length ?? 0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type CstRollbackMark = { raw: number; tlog: number; leaves: number; fields: number; errors: number }
|
|
132
|
+
|
|
133
|
+
export function saveCstMark(ctx: ParseContext): CstRollbackMark {
|
|
134
|
+
return {
|
|
135
|
+
raw: cstRawLen(ctx),
|
|
136
|
+
tlog: cstTlLen(ctx),
|
|
137
|
+
leaves: cstLeavesLen(ctx),
|
|
138
|
+
fields: ctx._fields?.length ?? 0,
|
|
139
|
+
// Speculative rollback must truncate the flat recovery-error sink in lockstep
|
|
140
|
+
// with the CST leaves: a branch that recovered (pushing a ParseError to
|
|
141
|
+
// ctx._errors AND embedding it as a CST child via captureError) then failed
|
|
142
|
+
// must remove BOTH, or the flat error survives as a GHOST that a tree walk
|
|
143
|
+
// can't see (flat run().errors would disagree with the embedded set).
|
|
144
|
+
errors: ctx._errors?.length ?? 0,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function rollbackBufList(
|
|
149
|
+
b: CstCaptureBuf,
|
|
150
|
+
keyMulti: 'ch' | 'raw',
|
|
151
|
+
keySingle: 'single' | 'rawSingle',
|
|
152
|
+
len: number,
|
|
153
|
+
): void {
|
|
154
|
+
const arr = b[keyMulti]
|
|
155
|
+
if (arr) {
|
|
156
|
+
if (len === 0) b[keyMulti] = undefined
|
|
157
|
+
else if (len === 1) { b[keySingle] = arr[0]; b[keyMulti] = undefined }
|
|
158
|
+
// Guarded like every other truncation — see rollbackCstCapture. This is the
|
|
159
|
+
// buffered node-capture path, so it is reached only by the INTERPRETER (the
|
|
160
|
+
// compiled engine inlines its own capture and never calls here), and it is
|
|
161
|
+
// where the redundant-store rate is highest: over a backtracking fixture
|
|
162
|
+
// this branch ran 32,800 times and 31,198 of those — 95% — restored a
|
|
163
|
+
// length that had not moved.
|
|
164
|
+
else if (arr.length !== len) arr.length = len
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
if (len === 0) b[keySingle] = undefined
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Demote everything captured since `childrenLen` from the STRUCTURAL `children`
|
|
172
|
+
* array to `rawChildren` only — it stays in the tree, it stops being a child.
|
|
173
|
+
*
|
|
174
|
+
* This is the one mechanism behind the rule that a list-producing combinator
|
|
175
|
+
* contributes the items of the list and NOTHING ELSE. `sepBy`'s separator is
|
|
176
|
+
* matched by a real combinator, so it pushes through the same nine push sites
|
|
177
|
+
* every terminal does; there is no separate channel to route it into at match
|
|
178
|
+
* time. Demoting after the fact is the cheapest correct answer: it reuses the
|
|
179
|
+
* mark the loop already took for rollback, truncates one array, and leaves
|
|
180
|
+
* `rawChildren` — the source-order record — untouched, so the separator is
|
|
181
|
+
* still recoverable WITHOUT re-reading source bytes.
|
|
182
|
+
*
|
|
183
|
+
* `raw` is deliberately NOT truncated. That asymmetry is the whole point, and it
|
|
184
|
+
* is the same asymmetry trivia already has: consumed, absent from `children`,
|
|
185
|
+
* still reachable.
|
|
186
|
+
*/
|
|
187
|
+
export function demoteCapturedToRaw(ctx: ParseContext, childrenLen: number): void {
|
|
188
|
+
const b = ctx._cstBuf
|
|
189
|
+
if (b) {
|
|
190
|
+
rollbackBufList(b, 'ch', 'single', childrenLen)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
// `_cstChildren` and `_cstLeaves` are installed as the SAME array by both the
|
|
194
|
+
// compiled prologue (codegen `_ctx._cstChildren = chV; _ctx._cstLeaves = chV`)
|
|
195
|
+
// and `beginCstNodeCapture`, and `cstLeavesLen`/`rollbackCstCapture` measure and
|
|
196
|
+
// truncate `_cstLeaves` alone. Mirror them exactly — a second truncation here
|
|
197
|
+
// would be a no-op on every real path and a silent divergence on any path where
|
|
198
|
+
// it was not.
|
|
199
|
+
if (ctx._cstLeaves && ctx._cstLeaves.length !== childrenLen) ctx._cstLeaves.length = childrenLen
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function rollbackCstCapture(ctx: ParseContext, mark: CstRollbackMark): void {
|
|
203
|
+
rollbackCstCaptureAt(ctx, mark.raw, mark.tlog, mark.leaves, mark.fields, mark.errors)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The same rollback, taking the five marks as SCALARS.
|
|
208
|
+
*
|
|
209
|
+
* `saveCstMark` allocates a five-field object, and the table driver took one per
|
|
210
|
+
* choice attempt and per repetition item — on `benchmark.less` that is 5.0%
|
|
211
|
+
* (`saveCstMark`) plus 2.1% (`saveTriviaMark`, which allocates this one AND a
|
|
212
|
+
* seven-field wrapper) of the whole parse. The compiled engine holds the same
|
|
213
|
+
* five numbers in scalar locals and allocates nothing, which is the only
|
|
214
|
+
* difference; a caller that keeps them in locals can now do the same.
|
|
215
|
+
*
|
|
216
|
+
* `rollbackCstCapture` stays as the object-taking entry point because the
|
|
217
|
+
* interpreter's call sites genuinely carry a mark around.
|
|
218
|
+
*/
|
|
219
|
+
export function rollbackCstCaptureAt(
|
|
220
|
+
ctx: ParseContext,
|
|
221
|
+
raw: number,
|
|
222
|
+
tlog: number,
|
|
223
|
+
leaves: number,
|
|
224
|
+
fields: number,
|
|
225
|
+
errors: number,
|
|
226
|
+
): void {
|
|
227
|
+
// Truncate the flat recovery-error sink alongside the CST, so a rolled-back
|
|
228
|
+
// speculative branch leaves no ghost error (see saveCstMark). Guarded on a
|
|
229
|
+
// present sink + a defined mark (rollbackTrivia forwards the same field).
|
|
230
|
+
//
|
|
231
|
+
// Each truncation is guarded on `length !== mark`, mirroring the compiled
|
|
232
|
+
// engine (codegen.ts `captureRestoreBody`). Assigning `array.length` runs V8's
|
|
233
|
+
// length setter — a backing-store trim decision — and costs the same when the
|
|
234
|
+
// value is unchanged, which is the overwhelmingly common case here: a
|
|
235
|
+
// speculative branch that captured nothing still pays for every store. The
|
|
236
|
+
// compare is an in-object load. Both engines must carry the guard or the
|
|
237
|
+
// interpreted path silently keeps the cost the compiled path shed.
|
|
238
|
+
if (ctx._errors && errors !== undefined && ctx._errors.length !== errors) ctx._errors.length = errors
|
|
239
|
+
const b = ctx._cstBuf
|
|
240
|
+
if (b) {
|
|
241
|
+
rollbackBufList(b, 'raw', 'rawSingle', raw)
|
|
242
|
+
rollbackBufList(b, 'ch', 'single', leaves)
|
|
243
|
+
if (b.tl) {
|
|
244
|
+
if (tlog === 0) b.tl = undefined
|
|
245
|
+
else if (b.tl.length !== tlog) b.tl.length = tlog
|
|
246
|
+
}
|
|
247
|
+
if (ctx._fields && ctx._fields.length !== fields) ctx._fields.length = fields
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
if (ctx._cstRawChildren && ctx._cstRawChildren.length !== raw) ctx._cstRawChildren.length = raw
|
|
251
|
+
if (ctx._cstTriviaLog && ctx._cstTriviaLog.length !== tlog) ctx._cstTriviaLog.length = tlog
|
|
252
|
+
if (ctx._cstLeaves && ctx._cstLeaves.length !== leaves) ctx._cstLeaves.length = leaves
|
|
253
|
+
if (ctx._fields && ctx._fields.length !== fields) ctx._fields.length = fields
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function pushCstTriviaEntry(
|
|
257
|
+
ctx: ParseContext,
|
|
258
|
+
start: number,
|
|
259
|
+
end: number,
|
|
260
|
+
kindIndex?: number,
|
|
261
|
+
): void {
|
|
262
|
+
const insertIdx = cstRawLen(ctx)
|
|
263
|
+
const b = ctx._cstBuf
|
|
264
|
+
const withKind = ctx.triviaKindLabels !== undefined && kindIndex !== undefined
|
|
265
|
+
if (b) {
|
|
266
|
+
if (!b.tl) {
|
|
267
|
+
b.tl = withKind ? [start, end, insertIdx, kindIndex] : [start, end, insertIdx]
|
|
268
|
+
} else if (withKind) {
|
|
269
|
+
b.tl.push(start, end, insertIdx, kindIndex)
|
|
270
|
+
} else {
|
|
271
|
+
b.tl.push(start, end, insertIdx)
|
|
272
|
+
}
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
if (ctx._cstTriviaLog) {
|
|
276
|
+
if (withKind) ctx._cstTriviaLog.push(start, end, insertIdx, kindIndex)
|
|
277
|
+
else ctx._cstTriviaLog.push(start, end, insertIdx)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function pushTriviaLogEntry(
|
|
282
|
+
ctx: ParseContext,
|
|
283
|
+
start: number,
|
|
284
|
+
end: number,
|
|
285
|
+
kindIndex?: number,
|
|
286
|
+
): void {
|
|
287
|
+
if (!ctx._triviaLog) return
|
|
288
|
+
if (ctx.triviaKindLabels !== undefined && kindIndex !== undefined) {
|
|
289
|
+
ctx._triviaLog.push(start, end, kindIndex)
|
|
290
|
+
} else {
|
|
291
|
+
ctx._triviaLog.push(start, end)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function cstDeferredTriviaActive(ctx: ParseContext): boolean {
|
|
296
|
+
return ctx._triviaLog !== undefined || ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined
|
|
297
|
+
}
|