parseman 0.46.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-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 +9929 -7465
- package/dist/analysis/diagnostics.cjs.map +3 -4
- package/dist/analysis/diagnostics.d.ts +2 -2
- package/dist/analysis/diagnostics.js +9929 -7465
- 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 +10865 -8535
- package/dist/cli/index.cjs.map +3 -4
- package/dist/cli/index.js +10865 -8535
- 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 +40 -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 +9224 -7203
- 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 +9219 -7198
- 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 +40 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.cjs +11146 -9593
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11146 -9593
- 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 +2898 -1623
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2898 -1623
- 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 +25 -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 +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/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
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COMMITMENT ANALYSIS — where a construct can still fail after it has consumed
|
|
3
|
+
* input or dirtied the capture buffers.
|
|
4
|
+
*
|
|
5
|
+
* This is a whole-grammar property of the rule graph, and it has exactly two
|
|
6
|
+
* consumers: the compiler, which bakes the decisions into the emitted source,
|
|
7
|
+
* and the interpreter, which must reach the SAME decisions at load time. It
|
|
8
|
+
* lives here, in one module, for that reason. Two hand-written copies of a
|
|
9
|
+
* predicate like `mayFail` would drift, and a drift here is not a size
|
|
10
|
+
* regression — it is the compiled and interpreted parsers accepting different
|
|
11
|
+
* languages. One module, two consumers.
|
|
12
|
+
*
|
|
13
|
+
* Every predicate in this file is a sound OVER-approximation of "something can
|
|
14
|
+
* go wrong": each MUST err toward `true`. A `false` is a proof obligation
|
|
15
|
+
* discharged on the caller's behalf, and callers delete code on the strength of
|
|
16
|
+
* it. Elide only where non-failure is PROVEN; when in doubt, say `true` and keep
|
|
17
|
+
* the machinery.
|
|
18
|
+
*/
|
|
19
|
+
import type { Combinator, ParserDef } from '../types.ts'
|
|
20
|
+
import { regexCanMatchEmpty } from '../regex/first-set.ts'
|
|
21
|
+
import { childrenOf } from './gating.ts'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Can `p` FAIL at all?
|
|
25
|
+
*
|
|
26
|
+
* `false` needs a construct whose definition is total: the empty literal,
|
|
27
|
+
* trivia (a scan of width 0 is a match), and the repeats — but the repeats only
|
|
28
|
+
* conditionally, see below.
|
|
29
|
+
*
|
|
30
|
+
* Everything else stays `true`, including two cases that look elidable and are
|
|
31
|
+
* not. A `choice` whose last arm is infallible is NOT infallible, because
|
|
32
|
+
* first-set gating decides at runtime which arms are entered at all and the
|
|
33
|
+
* total arm may be skipped. And a re-entered cycle is `true` rather than
|
|
34
|
+
* `false`: a recursive rule's own fallibility is exactly what the recursion is
|
|
35
|
+
* being asked about, so the safe fixpoint seed is the conservative one.
|
|
36
|
+
*
|
|
37
|
+
* TODO(0.47): `expect` is conservative here. It converts its inner parser's
|
|
38
|
+
* failure into a zero-width success carrying a parse-error node and therefore
|
|
39
|
+
* never fails, but it currently reports its inner parser's fallibility. Making
|
|
40
|
+
* it `false` would elide further; it needs its own pass through the tree-diff
|
|
41
|
+
* gate first, because `expect` sits at exactly the closing-delimiter positions
|
|
42
|
+
* where a wrong answer is most visible.
|
|
43
|
+
*/
|
|
44
|
+
export function mayFail(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
|
|
45
|
+
if (seen.has(p)) return true
|
|
46
|
+
seen.add(p)
|
|
47
|
+
const d = p._def as ParserDef
|
|
48
|
+
switch (d.tag) {
|
|
49
|
+
case 'literal': return d.value.length > 0
|
|
50
|
+
case 'trivia': return false
|
|
51
|
+
// `optional`/`many`/a min-0 `sepBy` swallow an ORDINARY failure — zero
|
|
52
|
+
// repetitions is a match. They do NOT swallow a COMMITTED one: a dispatch
|
|
53
|
+
// that has selected an arm and then failed inside it breaks out past every
|
|
54
|
+
// enclosing repeat, straight to the fallible boundary. So these are total
|
|
55
|
+
// exactly when their body cannot commit. (Measured: treating them as
|
|
56
|
+
// unconditionally total left an extra zero-width leaf in
|
|
57
|
+
// `BareInterpolatedSelector` on benchmark.less — a rollback the legacy
|
|
58
|
+
// build fired and this one had deleted.)
|
|
59
|
+
case 'optional':
|
|
60
|
+
case 'many': return mayCommitFailure(d.parser)
|
|
61
|
+
case 'sepBy': return d.min >= 1
|
|
62
|
+
? mayFail(d.parser, seen)
|
|
63
|
+
: mayCommitFailure(d.parser) || mayCommitFailure(d.separator)
|
|
64
|
+
case 'oneOrMore': return mayFail(d.parser, seen)
|
|
65
|
+
case 'sequence': return d.parsers.some(x => mayFail(x, seen))
|
|
66
|
+
// Delegating wrappers: as fallible as what they wrap.
|
|
67
|
+
case 'node':
|
|
68
|
+
case 'transform':
|
|
69
|
+
case 'label':
|
|
70
|
+
case 'field':
|
|
71
|
+
case 'expect':
|
|
72
|
+
case 'withCtx':
|
|
73
|
+
case 'grammar':
|
|
74
|
+
case 'token':
|
|
75
|
+
case 'leaf': return mayFail(d.parser, seen)
|
|
76
|
+
case 'lazy': { try { return mayFail(d.thunk(), seen) } catch { return true } }
|
|
77
|
+
// An adjacency assertion is a TEST — failing is its whole job.
|
|
78
|
+
case 'adjacency': return true
|
|
79
|
+
default: return true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Does this regex source consume at least one character on every match?
|
|
85
|
+
*
|
|
86
|
+
* `exec('')` alone is NOT this test, and the difference is not academic. A pure
|
|
87
|
+
* lookahead like `/(?=[ \t\n\r\f]*(?:[,{]))/` — the Less grammar's
|
|
88
|
+
* `bareInterpolatedSelectorEnd` — never matches the EMPTY STRING (there is no
|
|
89
|
+
* `,` or `{` to look at) yet matches ZERO-WIDTH wherever it succeeds. Reading
|
|
90
|
+
* `exec('') === null` as "always consumes" put an extra empty leaf inside
|
|
91
|
+
* `BareInterpolatedSelector` on benchmark.less.
|
|
92
|
+
*
|
|
93
|
+
* So the test is in two parts. A pattern built only from character-consuming
|
|
94
|
+
* constructs is position-INdependent: if it can match ε anywhere it can match ε
|
|
95
|
+
* in `''`, which `exec('')` detects. The constructs that break that equivalence
|
|
96
|
+
* — matching zero-width at a position inside a string but not at position 0 of
|
|
97
|
+
* `''` — are exactly lookaround and the word boundaries; `^`/`$` still match in
|
|
98
|
+
* `''`. Reject those syntactically and `exec('')` covers the rest.
|
|
99
|
+
*/
|
|
100
|
+
function regexAlwaysConsumes(source: string): boolean {
|
|
101
|
+
// STRUCTURAL, not a probe. The previous form rejected any pattern CONTAINING a
|
|
102
|
+
// lookaround and then asked `new RegExp(source).exec('')`. Both halves were
|
|
103
|
+
// imprecise in the same direction:
|
|
104
|
+
// - the blanket lookaround test fails a pattern whose lookaround is buried
|
|
105
|
+
// inside mandatory content — `/\/\*(?:[^*]|\*(?!\/))*\*\//` must consume
|
|
106
|
+
// `/*` and cannot match empty, yet was reported nullable;
|
|
107
|
+
// - `exec('')` cannot see a pattern that matches zero-width only at a
|
|
108
|
+
// NON-empty position, which is why the blanket test had to exist.
|
|
109
|
+
// `regexCanMatchEmpty` walks the same AST `firstSetFromRegex` uses, where
|
|
110
|
+
// anchors, lookaround and word boundaries are already `EMPTY`, so it answers
|
|
111
|
+
// both cases correctly and neither guard is needed.
|
|
112
|
+
return !regexCanMatchEmpty(source)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Does `p`, WHEN IT SUCCEEDS, always consume at least one character?
|
|
117
|
+
*
|
|
118
|
+
* The mirror of `mayFail`, and the opposite polarity from
|
|
119
|
+
* `first-set.ts`'s `matchesEmpty`: this one MUST err toward `false`. A `true`
|
|
120
|
+
* is a proof that the success path advanced the position, and the sequence
|
|
121
|
+
* emitter deletes a trivia rewind on the strength of it.
|
|
122
|
+
*
|
|
123
|
+
* It cannot be written as `!matchesEmpty(p)`. `matchesEmpty` answers a
|
|
124
|
+
* first-set question and is allowed to under-report nullability; for `expect`
|
|
125
|
+
* it does, and that case is exactly the one that matters here. `expect(X)`
|
|
126
|
+
* turns X's FAILURE into a zero-width SUCCESS carrying a parse-error node —
|
|
127
|
+
* so `expect(literal('}'))` succeeds having consumed nothing, while
|
|
128
|
+
* `matchesEmpty` looks through to the non-nullable literal and says "always
|
|
129
|
+
* consumes". Eliding on that answer moves a rule's span end past trailing
|
|
130
|
+
* trivia (measured: 38 tree mismatches across the css corpus, spans absorbing
|
|
131
|
+
* whitespace their legacy build excluded). `recover` manufactures the same
|
|
132
|
+
* zero-width success, and both are `false` here.
|
|
133
|
+
*
|
|
134
|
+
* `dispatch` is `false` rather than deferring to its selector: the selector's
|
|
135
|
+
* consumption is not unconditionally part of the dispatch's own span, and the
|
|
136
|
+
* few bytes are not worth the proof obligation.
|
|
137
|
+
*/
|
|
138
|
+
export function alwaysConsumes(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
|
|
139
|
+
if (seen.has(p)) return false
|
|
140
|
+
seen.add(p)
|
|
141
|
+
const d = p._def as ParserDef
|
|
142
|
+
switch (d.tag) {
|
|
143
|
+
case 'literal': return d.value.length > 0
|
|
144
|
+
case 'keywords': return true
|
|
145
|
+
case 'regex': return regexAlwaysConsumes(d.source)
|
|
146
|
+
case 'routed': return d.fallback === undefined ? true : alwaysConsumes(d.fallback, seen)
|
|
147
|
+
// One consuming term is enough for the whole sequence to consume.
|
|
148
|
+
case 'sequence': return d.parsers.some(x => alwaysConsumes(x, seen))
|
|
149
|
+
// A choice consumes only if EVERY arm it could take consumes.
|
|
150
|
+
case 'choice': return d.parsers.every(x => alwaysConsumes(x, seen))
|
|
151
|
+
case 'oneOrMore': return alwaysConsumes(d.parser, seen)
|
|
152
|
+
case 'sepBy': return d.min >= 1 && alwaysConsumes(d.parser, seen)
|
|
153
|
+
case 'node':
|
|
154
|
+
case 'transform':
|
|
155
|
+
case 'label':
|
|
156
|
+
case 'field':
|
|
157
|
+
case 'withCtx':
|
|
158
|
+
case 'grammar':
|
|
159
|
+
case 'token':
|
|
160
|
+
case 'leaf': return alwaysConsumes(d.parser, seen)
|
|
161
|
+
case 'lazy': { try { return alwaysConsumes(d.thunk(), seen) } catch { return false } }
|
|
162
|
+
// Zero-width by definition — it asserts over the gap and moves nothing. This
|
|
163
|
+
// `false` is load-bearing: it is what keeps the TRIVIA REWIND in place for the
|
|
164
|
+
// term that follows, so the assertion changes no span it does not mean to.
|
|
165
|
+
case 'adjacency': return false
|
|
166
|
+
// `expect`/`recover` synthesise a zero-width success; `optional`/`many`/
|
|
167
|
+
// `not`/`peek`/`trivia` are zero-width by definition; `dispatch`/`scanTo`/
|
|
168
|
+
// `guard`/`attempt`/`unknown` are unproven.
|
|
169
|
+
default: return false
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* True when parsing `p` may push a capture (leaf/child/trivia) into the active
|
|
175
|
+
* buffers and THEN fail, leaving partial state that an enclosing node() would
|
|
176
|
+
* wrongly absorb. Used to decide whether a fallible block needs CST-rollback.
|
|
177
|
+
*
|
|
178
|
+
* Sound over-approximation: the ONLY constructs that capture-then-fail are
|
|
179
|
+
* - a sequence whose non-final term captures before a later term can fail
|
|
180
|
+
* - a sepBy/oneOrMore item-then-separator partial (handled by their own
|
|
181
|
+
* dedicated rollback, so still covered conservatively here)
|
|
182
|
+
* Atomic terminals (literal/regex/keywords/charClass/guard/not) fail without
|
|
183
|
+
* having captured. node() buffers into a private sub-scope and discards it on
|
|
184
|
+
* failure, so it never leaks. choice/firstMatch roll back each failed arm
|
|
185
|
+
* internally. optional/many never "fail" with partial output. Delegating
|
|
186
|
+
* wrappers (transform/label/grammar/withCtx/expect) pass through to inner.
|
|
187
|
+
*/
|
|
188
|
+
export function mayLeavePartialCapture(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set(), triviaActive = true): boolean {
|
|
189
|
+
if (seen.has(p)) return false
|
|
190
|
+
seen.add(p)
|
|
191
|
+
const d = p._def
|
|
192
|
+
switch (d.tag) {
|
|
193
|
+
// Atomic / non-capturing-then-failing: a failure happens before any push.
|
|
194
|
+
case 'literal':
|
|
195
|
+
case 'regex':
|
|
196
|
+
case 'keywords':
|
|
197
|
+
case 'guard':
|
|
198
|
+
// Adjacency probes with `_cap = 0` / no `commit()`: it cannot push anything.
|
|
199
|
+
case 'adjacency':
|
|
200
|
+
case 'not':
|
|
201
|
+
// peek(): emitted under a non-capturing probe ctx and zero-width on both
|
|
202
|
+
// outcomes, so it can never leave a partial capture behind.
|
|
203
|
+
case 'peek':
|
|
204
|
+
case 'trivia':
|
|
205
|
+
case 'token':
|
|
206
|
+
case 'leaf':
|
|
207
|
+
case 'scanTo':
|
|
208
|
+
case 'unknown':
|
|
209
|
+
return false
|
|
210
|
+
// node() captures into its own private buffers and rolls them back on
|
|
211
|
+
// failure (emitNode restores _ctx.* and never pushes on the !ok path).
|
|
212
|
+
case 'node':
|
|
213
|
+
return false
|
|
214
|
+
// choice/firstMatch already roll back each failed arm; on overall failure
|
|
215
|
+
// nothing committed remains.
|
|
216
|
+
case 'choice':
|
|
217
|
+
return false
|
|
218
|
+
case 'dispatch':
|
|
219
|
+
// These are independent graph predicates. Sharing one mutable `seen` set
|
|
220
|
+
// between them made the first walk hide the arm from the second: an atomic
|
|
221
|
+
// literal arm, for example, was visited by `mayLeavePartialCapture` (false)
|
|
222
|
+
// and then skipped by `capturesLeaf` (also false because already seen).
|
|
223
|
+
// Fork the ancestor set for each proof so cycle protection remains, while
|
|
224
|
+
// one proof cannot erase evidence needed by another.
|
|
225
|
+
return capturesLeaf(d.selector, new Set(seen)) ||
|
|
226
|
+
d.cases.some(x => mayLeavePartialCapture(x.parser, new Set(seen), triviaActive) || capturesLeaf(x.parser, new Set(seen))) ||
|
|
227
|
+
(d.matchers ? d.matchers.some(x => mayLeavePartialCapture(x.parser, new Set(seen), triviaActive) || capturesLeaf(x.parser, new Set(seen))) : false) ||
|
|
228
|
+
(d.otherwise ? mayLeavePartialCapture(d.otherwise, new Set(seen), triviaActive) || capturesLeaf(d.otherwise, new Set(seen)) : false)
|
|
229
|
+
case 'attempt':
|
|
230
|
+
return false
|
|
231
|
+
// optional never fails; many/oneOrMore only "fail" with zero captured items.
|
|
232
|
+
case 'optional':
|
|
233
|
+
case 'many':
|
|
234
|
+
case 'oneOrMore':
|
|
235
|
+
return false
|
|
236
|
+
// sepBy emits its own per-iteration rollback in emitSepBy.
|
|
237
|
+
case 'sepBy':
|
|
238
|
+
return false
|
|
239
|
+
// A sequence is the real case: an earlier capturing term followed by a term
|
|
240
|
+
// that can fail leaves the earlier captures buffered.
|
|
241
|
+
//
|
|
242
|
+
// BOTH halves of that sentence are load-bearing, and the second half is what
|
|
243
|
+
// this walk exists to exploit. "Some earlier term captures" alone was the old
|
|
244
|
+
// test, and it is true of very nearly every sequence in a real grammar — a
|
|
245
|
+
// sequence almost always opens with a literal or a node. The rollback is only
|
|
246
|
+
// reachable if something AFTER the capture can still fail; once the remainder
|
|
247
|
+
// is total (`optional`/`many`/trivia/a min-0 `sepBy`), the sequence has no
|
|
248
|
+
// failure path left to roll back from, and the save+restore is dead code.
|
|
249
|
+
//
|
|
250
|
+
// `captured` tracks whether anything is in the buffers yet as the scan moves
|
|
251
|
+
// left to right. Two things put it there:
|
|
252
|
+
// - a term that captures (`capturesLeaf`/`hasNodeDef`), and
|
|
253
|
+
// - the inter-term TRIVIA scan, which under `capturing` pushes into
|
|
254
|
+
// `_cstRawChildren`/`_cstTriviaLog` BEFORE the term at each boundary
|
|
255
|
+
// i >= 1. `emitSeqValues` only rewinds that push on the term's SUCCESS
|
|
256
|
+
// path (the "matched empty, so leave the whitespace out of the span"
|
|
257
|
+
// branch); a term that FAILS breaks straight out with the trivia still
|
|
258
|
+
// buffered, so from boundary 1 onward the buffers are dirty regardless of
|
|
259
|
+
// what the earlier terms did.
|
|
260
|
+
// Each term is also asked about itself: a nested sequence can leave its own
|
|
261
|
+
// partial capture behind, and its terms are emitted inline into this same
|
|
262
|
+
// block with no boundary of their own to roll them back.
|
|
263
|
+
case 'sequence': {
|
|
264
|
+
const parts = d.parsers
|
|
265
|
+
let captured = false
|
|
266
|
+
for (let i = 0; i < parts.length; i++) {
|
|
267
|
+
const part = parts[i]!
|
|
268
|
+
if (i > 0 && triviaActive) captured = true
|
|
269
|
+
if (captured && mayFail(part)) return true
|
|
270
|
+
if (mayLeavePartialCapture(part, seen, triviaActive)) return true
|
|
271
|
+
if (capturesLeaf(part) || hasNodeDef(part)) captured = true
|
|
272
|
+
}
|
|
273
|
+
return false
|
|
274
|
+
}
|
|
275
|
+
// Delegating wrappers: defer to the wrapped parser.
|
|
276
|
+
case 'transform':
|
|
277
|
+
case 'label':
|
|
278
|
+
case 'field':
|
|
279
|
+
case 'expect':
|
|
280
|
+
case 'withCtx':
|
|
281
|
+
case 'grammar':
|
|
282
|
+
return mayLeavePartialCapture(d.parser, seen, triviaActive)
|
|
283
|
+
case 'recover':
|
|
284
|
+
return mayLeavePartialCapture(d.parser, seen, triviaActive)
|
|
285
|
+
case 'lazy': {
|
|
286
|
+
try { return mayLeavePartialCapture(d.thunk(), seen, triviaActive) } catch { return true }
|
|
287
|
+
}
|
|
288
|
+
// Unknown shapes: be safe and keep the rollback.
|
|
289
|
+
default:
|
|
290
|
+
return true
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** True when `p` can push a leaf/node into the capture buffers on success. */
|
|
295
|
+
export function capturesLeaf(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
|
|
296
|
+
if (seen.has(p)) return false
|
|
297
|
+
seen.add(p)
|
|
298
|
+
const d = p._def
|
|
299
|
+
switch (d.tag) {
|
|
300
|
+
case 'literal':
|
|
301
|
+
case 'regex':
|
|
302
|
+
case 'keywords':
|
|
303
|
+
case 'node':
|
|
304
|
+
case 'token':
|
|
305
|
+
case 'routed':
|
|
306
|
+
case 'leaf':
|
|
307
|
+
return true
|
|
308
|
+
case 'not':
|
|
309
|
+
case 'peek':
|
|
310
|
+
case 'guard':
|
|
311
|
+
case 'adjacency':
|
|
312
|
+
case 'trivia':
|
|
313
|
+
case 'unknown':
|
|
314
|
+
return false
|
|
315
|
+
case 'sequence':
|
|
316
|
+
case 'choice':
|
|
317
|
+
return d.parsers.some(x => capturesLeaf(x, seen))
|
|
318
|
+
case 'dispatch':
|
|
319
|
+
return capturesLeaf(d.selector, seen) ||
|
|
320
|
+
d.cases.some(x => capturesLeaf(x.parser, seen)) ||
|
|
321
|
+
(d.matchers ? d.matchers.some(entry => capturesLeaf(entry.parser, seen)) : false) ||
|
|
322
|
+
(d.otherwise ? capturesLeaf(d.otherwise, seen) : false)
|
|
323
|
+
case 'sepBy':
|
|
324
|
+
return capturesLeaf(d.parser, seen) || capturesLeaf(d.separator, seen)
|
|
325
|
+
case 'many':
|
|
326
|
+
case 'oneOrMore':
|
|
327
|
+
case 'optional':
|
|
328
|
+
case 'attempt':
|
|
329
|
+
case 'transform':
|
|
330
|
+
case 'label':
|
|
331
|
+
case 'field':
|
|
332
|
+
case 'expect':
|
|
333
|
+
case 'withCtx':
|
|
334
|
+
case 'grammar':
|
|
335
|
+
case 'recover':
|
|
336
|
+
return capturesLeaf(d.parser, seen)
|
|
337
|
+
case 'scanTo':
|
|
338
|
+
return true
|
|
339
|
+
case 'lazy': {
|
|
340
|
+
try { return capturesLeaf(d.thunk(), seen) } catch { return true }
|
|
341
|
+
}
|
|
342
|
+
default:
|
|
343
|
+
return true
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Does this combinator tree contain a node() anywhere (following ref/lazy
|
|
349
|
+
* thunks)? Determines whether the compile emits CST capture — so non-node
|
|
350
|
+
* grammars stay byte-identical. `seen` guards against recursion cycles.
|
|
351
|
+
*/
|
|
352
|
+
export function hasNodeDef(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
|
|
353
|
+
if (seen.has(p)) return false
|
|
354
|
+
seen.add(p)
|
|
355
|
+
const d = p._def
|
|
356
|
+
switch (d.tag) {
|
|
357
|
+
case 'node': return true
|
|
358
|
+
case 'lazy': { try { return hasNodeDef(d.thunk(), seen) } catch { return false } }
|
|
359
|
+
case 'grammar':
|
|
360
|
+
case 'trivia':
|
|
361
|
+
case 'token':
|
|
362
|
+
case 'leaf':
|
|
363
|
+
case 'label':
|
|
364
|
+
case 'field':
|
|
365
|
+
case 'optional':
|
|
366
|
+
case 'many':
|
|
367
|
+
case 'oneOrMore':
|
|
368
|
+
case 'not':
|
|
369
|
+
case 'peek':
|
|
370
|
+
case 'transform': return hasNodeDef(d.parser, seen)
|
|
371
|
+
case 'sequence':
|
|
372
|
+
case 'choice': return d.parsers.some(x => hasNodeDef(x, seen))
|
|
373
|
+
case 'dispatch': return hasNodeDef(d.selector, seen) || d.cases.some(x => hasNodeDef(x.parser, seen)) || (d.matchers ? d.matchers.some(entry => hasNodeDef(entry.parser, seen)) : false) || (d.otherwise ? hasNodeDef(d.otherwise, seen) : false)
|
|
374
|
+
case 'sepBy': return hasNodeDef(d.parser, seen) || hasNodeDef(d.separator, seen)
|
|
375
|
+
case 'scanTo': return hasNodeDef(d.sentinel, seen) || d.skip.some(x => hasNodeDef(x, seen))
|
|
376
|
+
case 'recover': return hasNodeDef(d.parser, seen) || hasNodeDef(d.sentinel, seen)
|
|
377
|
+
case 'expect': return hasNodeDef(d.parser, seen)
|
|
378
|
+
case 'withCtx': return hasNodeDef(d.parser, seen)
|
|
379
|
+
default: return false
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Whether a grammar tree owns a DIRECT semantic node reduction — a `node(..., build)`
|
|
385
|
+
* whose callback produces the value itself, as opposed to a purely structural
|
|
386
|
+
* `node(parser)`.
|
|
387
|
+
*
|
|
388
|
+
* It is the predicate behind `hostBranchElided`: an artifact only drops a
|
|
389
|
+
* positioned-CST branch if there was a direct builder to drop, so an all-structural
|
|
390
|
+
* grammar stays usable with either host (`cst/host-mode.ts`).
|
|
391
|
+
*
|
|
392
|
+
* It belongs in THIS module for the reason stated at the top of it: both lowerings
|
|
393
|
+
* must reach the same answer, and a stamp they disagree about is a driver that
|
|
394
|
+
* accepts a host it should refuse. It previously lived in `compiler/codegen.ts`,
|
|
395
|
+
* which forced `table/compile-rule-map.ts` to import the engine the table replaced.
|
|
396
|
+
*
|
|
397
|
+
* The descent mirrors the source lowering's own `childrenOf` exactly — including a
|
|
398
|
+
* `routed()` fallback and a `dispatch` matcher arm, both of which are real emit
|
|
399
|
+
* sites and neither of which `hasNodeDef` above walks.
|
|
400
|
+
*/
|
|
401
|
+
export function hasDirectBuildDef(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
|
|
402
|
+
if (seen.has(p)) return false
|
|
403
|
+
seen.add(p)
|
|
404
|
+
const d = p._def
|
|
405
|
+
switch (d.tag) {
|
|
406
|
+
case 'node': return d.build !== undefined || hasDirectBuildDef(d.parser, seen)
|
|
407
|
+
case 'lazy': { try { return hasDirectBuildDef(d.thunk(), seen) } catch { return false } }
|
|
408
|
+
case 'grammar':
|
|
409
|
+
case 'trivia':
|
|
410
|
+
case 'token':
|
|
411
|
+
case 'leaf':
|
|
412
|
+
case 'label':
|
|
413
|
+
case 'field':
|
|
414
|
+
case 'optional':
|
|
415
|
+
case 'many':
|
|
416
|
+
case 'oneOrMore':
|
|
417
|
+
case 'attempt':
|
|
418
|
+
case 'not':
|
|
419
|
+
case 'peek':
|
|
420
|
+
case 'withCtx':
|
|
421
|
+
case 'expect':
|
|
422
|
+
case 'transform': return hasDirectBuildDef(d.parser, seen)
|
|
423
|
+
case 'sequence':
|
|
424
|
+
case 'choice': return d.parsers.some(x => hasDirectBuildDef(x, seen))
|
|
425
|
+
case 'dispatch': return hasDirectBuildDef(d.selector, seen)
|
|
426
|
+
|| d.cases.some(x => hasDirectBuildDef(x.parser, seen))
|
|
427
|
+
|| (d.matchers ? d.matchers.some(x => hasDirectBuildDef(x.parser, seen)) : false)
|
|
428
|
+
|| (d.otherwise ? hasDirectBuildDef(d.otherwise, seen) : false)
|
|
429
|
+
case 'sepBy': return hasDirectBuildDef(d.parser, seen) || hasDirectBuildDef(d.separator, seen)
|
|
430
|
+
case 'scanTo': return hasDirectBuildDef(d.sentinel, seen) || d.skip.some(x => hasDirectBuildDef(x, seen))
|
|
431
|
+
case 'recover': return hasDirectBuildDef(d.parser, seen) || hasDirectBuildDef(d.sentinel, seen)
|
|
432
|
+
case 'routed': return d.fallback ? hasDirectBuildDef(d.fallback, seen) : false
|
|
433
|
+
default: return false
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** True when `p` can report a committed failure through emitFallible's failure channel. */
|
|
438
|
+
export function mayCommitFailure(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
|
|
439
|
+
if (seen.has(p)) return false
|
|
440
|
+
seen.add(p)
|
|
441
|
+
const d = p._def
|
|
442
|
+
switch (d.tag) {
|
|
443
|
+
case 'dispatch':
|
|
444
|
+
return true
|
|
445
|
+
case 'choice':
|
|
446
|
+
case 'sequence':
|
|
447
|
+
return d.parsers.some(x => mayCommitFailure(x, seen))
|
|
448
|
+
case 'many':
|
|
449
|
+
case 'oneOrMore':
|
|
450
|
+
case 'optional':
|
|
451
|
+
case 'attempt':
|
|
452
|
+
case 'transform':
|
|
453
|
+
case 'label':
|
|
454
|
+
case 'field':
|
|
455
|
+
case 'grammar':
|
|
456
|
+
case 'node':
|
|
457
|
+
return mayCommitFailure(d.parser, seen)
|
|
458
|
+
case 'sepBy':
|
|
459
|
+
return mayCommitFailure(d.parser, seen) || mayCommitFailure(d.separator, seen)
|
|
460
|
+
case 'token':
|
|
461
|
+
case 'leaf':
|
|
462
|
+
return mayCommitFailure(d.parser, seen)
|
|
463
|
+
case 'withCtx':
|
|
464
|
+
return mayCommitFailure(d.parser, seen)
|
|
465
|
+
case 'scanTo':
|
|
466
|
+
return mayCommitFailure(d.sentinel, seen) || d.skip.some(x => mayCommitFailure(x, seen))
|
|
467
|
+
case 'lazy': {
|
|
468
|
+
try { return mayCommitFailure(d.thunk(), seen) } catch { return true }
|
|
469
|
+
}
|
|
470
|
+
case 'unknown':
|
|
471
|
+
return true
|
|
472
|
+
default:
|
|
473
|
+
return false
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** A leaf-composed imported piece may carry Parseman's own structural balanced
|
|
478
|
+
* text reconstruction, but never a grammar-authored semantic callback.
|
|
479
|
+
*
|
|
480
|
+
* `externalRefs` are the unresolved NAMED refs `compileLinkable`'s pre-pass already
|
|
481
|
+
* classified as external (`g.Value` naming a rule this artifact doesn't define). They
|
|
482
|
+
* are the one case that fails OPEN: the ref is a HOLE, it holds no callback of its
|
|
483
|
+
* own, and codegen emits it as a by-name `_r_<Name>` call bound at fuse time by
|
|
484
|
+
* whichever piece supplies the name — either another pre-final piece (itself put
|
|
485
|
+
* through this same gate) or the local leaf (allowed to be semantic by design). So
|
|
486
|
+
* an artifact whose only "unknown" is a hole is genuinely recognition-only.
|
|
487
|
+
*
|
|
488
|
+
* EVERY other lazy failure still fails CLOSED. In particular an UNNAMED `ref()` that
|
|
489
|
+
* was never `.define()`d is NOT external — nobody can bind it by name — so it stays
|
|
490
|
+
* an opaque subtree of unknown semantics and the answer is "semantic". Catching all
|
|
491
|
+
* errors here instead would let that (and any future thunk failure) pass the
|
|
492
|
+
* recognition-only gate. */
|
|
493
|
+
function hasSemanticReduction(
|
|
494
|
+
roots: readonly Combinator<unknown>[],
|
|
495
|
+
externalRefs?: ReadonlySet<Combinator<unknown>>,
|
|
496
|
+
): boolean {
|
|
497
|
+
const seen = new Set<Combinator<unknown>>()
|
|
498
|
+
const visit = (parser: Combinator<unknown>): boolean => {
|
|
499
|
+
if (seen.has(parser)) return false
|
|
500
|
+
seen.add(parser)
|
|
501
|
+
const def = parser._def
|
|
502
|
+
if (def.tag === 'transform' && !def.recognitionOnly) return true
|
|
503
|
+
if (def.tag === 'choice' && def.gates.some(Boolean)) return true
|
|
504
|
+
if (def.tag === 'guard' || def.tag === 'withCtx') return true
|
|
505
|
+
if (def.tag === 'node' && def.build !== undefined) return true
|
|
506
|
+
if (def.tag === 'lazy') {
|
|
507
|
+
if (externalRefs?.has(parser)) return false
|
|
508
|
+
try { return visit(def.thunk()) } catch { return true }
|
|
509
|
+
}
|
|
510
|
+
return childrenOf(def).some(visit)
|
|
511
|
+
}
|
|
512
|
+
return roots.some(visit)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* `hasDirectBuilders` / `isRecognitionOnly` for a rule map WITHOUT lowering it.
|
|
517
|
+
*
|
|
518
|
+
* `composeLeaf()` gates on both: every pre-final grammar must prove recognition-only,
|
|
519
|
+
* and the local leaf's direct builders decide whether the recognition pieces need
|
|
520
|
+
* terminal capture. Both were only ever available as fields on `LinkablePieces`, so
|
|
521
|
+
* the gate forced a full `compileLinkable()` of every piece purely to read two
|
|
522
|
+
* booleans off the result — which is why the table lowering appeared to be blocked on
|
|
523
|
+
* porting the source lowering wholesale.
|
|
524
|
+
*
|
|
525
|
+
* They are not lowering products. Both are predicates over the COMBINATOR GRAPH, and
|
|
526
|
+
* this computes them from the graph directly, with the SAME `externalRefs` rule the
|
|
527
|
+
* lowering applies (`:6008`): a named `lazy` whose thunk throws is a HOLE, bound by
|
|
528
|
+
* name at fuse time, and is therefore not evidence of unknown semantics. An UNNAMED
|
|
529
|
+
* unresolved `ref()` stays semantic — nobody can bind it, so it fails closed.
|
|
530
|
+
*/
|
|
531
|
+
export function classifyRuleMap(
|
|
532
|
+
ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>,
|
|
533
|
+
): { hasDirectBuilders: boolean; isRecognitionOnly: boolean } {
|
|
534
|
+
const externalRefs = new Set<Combinator<unknown>>()
|
|
535
|
+
const scanned = new Set<Combinator<unknown>>()
|
|
536
|
+
const scanExternal = (p: Combinator<unknown>): void => {
|
|
537
|
+
if (scanned.has(p)) return
|
|
538
|
+
scanned.add(p)
|
|
539
|
+
const def = p._def
|
|
540
|
+
if (def.tag === 'lazy') {
|
|
541
|
+
let resolved: Combinator<unknown> | undefined
|
|
542
|
+
try { resolved = def.thunk() } catch { resolved = undefined }
|
|
543
|
+
if (resolved === undefined) {
|
|
544
|
+
if ((p as unknown as { _ruleName?: string })._ruleName) externalRefs.add(p)
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
scanExternal(resolved)
|
|
548
|
+
return
|
|
549
|
+
}
|
|
550
|
+
for (const child of childrenOf(def)) scanExternal(child)
|
|
551
|
+
}
|
|
552
|
+
for (const [, rule] of ruleMap) scanExternal(rule)
|
|
553
|
+
return {
|
|
554
|
+
hasDirectBuilders: ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
|
|
555
|
+
isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule), externalRefs),
|
|
556
|
+
}
|
|
557
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SECOND world.
|
|
3
|
+
*
|
|
4
|
+
* A type error has one source to point at. A grammar finding has two — the ordered
|
|
5
|
+
* choice whose arms cost the time, and the input that pays for it — and relating them is
|
|
6
|
+
* the whole diagnostic. `gating.ts` owns the first world and is entirely static. This
|
|
7
|
+
* module owns the second, and it is deliberately the smallest honest thing that can be
|
|
8
|
+
* said about a corpus without instrumenting a parse.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IS MEASURED, EXACTLY
|
|
11
|
+
* -------------------------
|
|
12
|
+
* For a choice, every corpus POSITION whose character is in some arm's first set. That
|
|
13
|
+
* is a count of characters, not of times the choice was reached, and the difference
|
|
14
|
+
* matters: a choice nested three rules deep is reached far less often than its first
|
|
15
|
+
* characters occur. So the number is reported as what it is — an upper bound on entries,
|
|
16
|
+
* derived from the same first sets the compiler dispatches on — and never as "this
|
|
17
|
+
* choice ran N times". Naming it precisely is what keeps it useful; a number that
|
|
18
|
+
* overstates itself is worse than no number, because the reader stops trusting all of
|
|
19
|
+
* them.
|
|
20
|
+
*
|
|
21
|
+
* The one thing this DOES settle, exactly and without any modelling: an arm whose first
|
|
22
|
+
* set is ANY is entered at every one of those positions, because no first character can
|
|
23
|
+
* exclude it. That is the cost the fix removes, and it is arithmetic, not a guess.
|
|
24
|
+
*
|
|
25
|
+
* Determinism: counts and the located example are a pure function of (report, corpus).
|
|
26
|
+
* No timings, no paths beyond the caller's own sample names.
|
|
27
|
+
*/
|
|
28
|
+
import type { Combinator, FirstSet } from '../types.ts'
|
|
29
|
+
import { firstSetOf } from '../combinators/first-set.ts'
|
|
30
|
+
import type { ChoiceGating } from './gating.ts'
|
|
31
|
+
|
|
32
|
+
export type CorpusSample = { name: string; text: string }
|
|
33
|
+
|
|
34
|
+
/** A located input position: the caller's sample name plus 1-based line/column. */
|
|
35
|
+
export type CorpusSite = {
|
|
36
|
+
sample: string
|
|
37
|
+
line: number
|
|
38
|
+
column: number
|
|
39
|
+
/** The source line, untrimmed, for a caret rendering. */
|
|
40
|
+
lineText: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ArmCorpusCost = {
|
|
44
|
+
index: number
|
|
45
|
+
/** ANY first set — no character excludes this arm. */
|
|
46
|
+
any: boolean
|
|
47
|
+
/**
|
|
48
|
+
* Corpus positions whose character this arm's first set accepts. For an `any` arm this
|
|
49
|
+
* equals the choice total, which is the point.
|
|
50
|
+
*/
|
|
51
|
+
positions: number
|
|
52
|
+
/** The first such position, for the two-world rendering. Absent when there are none. */
|
|
53
|
+
firstSite?: CorpusSite
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type ChoiceCorpusCost = {
|
|
57
|
+
choiceId: string
|
|
58
|
+
/** Positions whose character SOME arm accepts — the choice's own reach, bounded. */
|
|
59
|
+
positions: number
|
|
60
|
+
/** Every position in the corpus, so a share can be read. */
|
|
61
|
+
corpusPositions: number
|
|
62
|
+
arms: ArmCorpusCost[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const inSet = (fs: FirstSet, cp: number): boolean => {
|
|
66
|
+
if (fs.kind === 'any') return true
|
|
67
|
+
if (fs.kind === 'empty') return false
|
|
68
|
+
for (const r of fs.ranges) if (cp >= r.lo && cp <= r.hi) return true
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function siteAt(samples: readonly CorpusSample[], flatIndex: number): CorpusSite {
|
|
73
|
+
let i = flatIndex
|
|
74
|
+
for (const s of samples) {
|
|
75
|
+
if (i < s.text.length) {
|
|
76
|
+
let line = 1
|
|
77
|
+
let lineStart = 0
|
|
78
|
+
for (let j = 0; j < i; j++) if (s.text.charCodeAt(j) === 10) { line++; lineStart = j + 1 }
|
|
79
|
+
let lineEnd = s.text.indexOf('\n', lineStart)
|
|
80
|
+
if (lineEnd === -1) lineEnd = s.text.length
|
|
81
|
+
return { sample: s.name, line, column: i - lineStart + 1, lineText: s.text.slice(lineStart, lineEnd) }
|
|
82
|
+
}
|
|
83
|
+
i -= s.text.length
|
|
84
|
+
}
|
|
85
|
+
return { sample: '<eof>', line: 0, column: 0, lineText: '' }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Measure one choice against a corpus. `choice.anyArms` already knows which arms are
|
|
90
|
+
* broad; this adds how much input that costs.
|
|
91
|
+
*/
|
|
92
|
+
export function measureChoiceCost(c: ChoiceGating, samples: readonly CorpusSample[], arms: readonly { firstSet: FirstSet }[]): ChoiceCorpusCost {
|
|
93
|
+
const counts = arms.map(() => 0)
|
|
94
|
+
const first = arms.map<number>(() => -1)
|
|
95
|
+
let positions = 0
|
|
96
|
+
let corpusPositions = 0
|
|
97
|
+
let flat = 0
|
|
98
|
+
for (const s of samples) {
|
|
99
|
+
for (let i = 0; i < s.text.length; i++) {
|
|
100
|
+
const cp = s.text.charCodeAt(i)
|
|
101
|
+
let hitAny = false
|
|
102
|
+
for (let a = 0; a < arms.length; a++) {
|
|
103
|
+
if (inSet(arms[a]!.firstSet, cp)) {
|
|
104
|
+
counts[a]!++
|
|
105
|
+
if (first[a] === -1) first[a] = flat + i
|
|
106
|
+
hitAny = true
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (hitAny) positions++
|
|
110
|
+
}
|
|
111
|
+
flat += s.text.length
|
|
112
|
+
corpusPositions += s.text.length
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
choiceId: c.id,
|
|
116
|
+
positions,
|
|
117
|
+
corpusPositions,
|
|
118
|
+
arms: arms.map((a, i) => ({
|
|
119
|
+
index: i,
|
|
120
|
+
any: a.firstSet.kind === 'any',
|
|
121
|
+
positions: counts[i]!,
|
|
122
|
+
...(first[i]! >= 0 ? { firstSite: siteAt(samples, first[i]!) } : {}),
|
|
123
|
+
})),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The per-arm first sets a choice dispatches on, in arm order. */
|
|
128
|
+
export function armFirstSets(choiceArms: readonly Combinator<unknown>[]): { firstSet: FirstSet }[] {
|
|
129
|
+
return choiceArms.map(a => ({ firstSet: firstSetOf(a) }))
|
|
130
|
+
}
|