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,821 @@
|
|
|
1
|
+
import type { Combinator, ParseContext, ParseFail, ParseResult } from '../types.ts'
|
|
2
|
+
import { literalCodePoints, parseClassOperand, parseClassRanges } from '../regex/classes.ts'
|
|
3
|
+
import {
|
|
4
|
+
analyzeLabeledTrivia,
|
|
5
|
+
recordTriviaChunks,
|
|
6
|
+
scanLabeledTriviaEnd,
|
|
7
|
+
scanLabeledTriviaChunks,
|
|
8
|
+
visitLabeledTrivia,
|
|
9
|
+
} from '../cst/trivia-kinds.ts'
|
|
10
|
+
import {
|
|
11
|
+
pushCstTriviaEntry,
|
|
12
|
+
pushTriviaLogEntry,
|
|
13
|
+
rollbackCstCapture,
|
|
14
|
+
rollbackCstCaptureAt,
|
|
15
|
+
saveCstMark,
|
|
16
|
+
} from '../cst/capture-buffer.ts'
|
|
17
|
+
import { recordLineRangeFromContext } from '../line-index.ts'
|
|
18
|
+
import { createDetachedParseContext } from '../parse-context.ts'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Result of scanning trivia: the position after it, plus a `commit()` that
|
|
22
|
+
* records the matched trivia tokens into the active rawChildren collector.
|
|
23
|
+
* Recording is deferred so callers that may reject the following item (e.g.
|
|
24
|
+
* many()/oneOrMore() retries) can advance speculatively without recording
|
|
25
|
+
* trivia that doesn't actually sit between two accepted items.
|
|
26
|
+
*/
|
|
27
|
+
export type TriviaScan = { end: number; commit: () => void }
|
|
28
|
+
|
|
29
|
+
/** Saved lengths for rolling back speculative trivia commits. */
|
|
30
|
+
export type TriviaRollbackMark = { raw: number; tlog: number; leaves: number; fields: number; errors: number; log: number; rootLog: number }
|
|
31
|
+
|
|
32
|
+
const NOOP_COMMIT = () => {}
|
|
33
|
+
export type FastTriviaScanner = (input: string, cur: number) => number
|
|
34
|
+
/**
|
|
35
|
+
* Bounded structural cache for the handful of trivia grammars a process actively
|
|
36
|
+
* parses. A module-global WeakMap used to allocate on every table-runtime
|
|
37
|
+
* import, even though emitted artifacts only need this during construction.
|
|
38
|
+
*
|
|
39
|
+
* Four scalar slots keep the common grammar switches hot without a collection,
|
|
40
|
+
* a per-parser property transition, or an unbounded process-retaining cache.
|
|
41
|
+
* Cache misses merely rebuild a construction-time scanner and remain correct.
|
|
42
|
+
*/
|
|
43
|
+
let fastTrivia0: Combinator<unknown> | undefined
|
|
44
|
+
let fastTrivia1: Combinator<unknown> | undefined
|
|
45
|
+
let fastTrivia2: Combinator<unknown> | undefined
|
|
46
|
+
let fastTrivia3: Combinator<unknown> | undefined
|
|
47
|
+
let fastScanner0: FastTriviaScanner | null | undefined
|
|
48
|
+
let fastScanner1: FastTriviaScanner | null | undefined
|
|
49
|
+
let fastScanner2: FastTriviaScanner | null | undefined
|
|
50
|
+
let fastScanner3: FastTriviaScanner | null | undefined
|
|
51
|
+
|
|
52
|
+
/** True when trivia recording must be deferred until the following term commits. */
|
|
53
|
+
export function needsDeferredTriviaCommit(ctx: ParseContext): boolean {
|
|
54
|
+
return ctx._triviaLog !== undefined || ctx._rootTriviaLog !== undefined || ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function saveTriviaMark(ctx: ParseContext): TriviaRollbackMark {
|
|
58
|
+
const m = saveCstMark(ctx)
|
|
59
|
+
return {
|
|
60
|
+
raw: m.raw,
|
|
61
|
+
tlog: m.tlog,
|
|
62
|
+
leaves: m.leaves,
|
|
63
|
+
fields: m.fields,
|
|
64
|
+
errors: m.errors,
|
|
65
|
+
log: ctx._triviaLog ? ctx._triviaLog.length : 0,
|
|
66
|
+
rootLog: ctx._rootTriviaLog ? ctx._rootTriviaLog.length : 0,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Trivia rollback from SCALAR marks — the allocation-free twin of
|
|
72
|
+
* `rollbackTrivia`. See `rollbackCstCaptureAt`; `saveTriviaMark` allocates twice
|
|
73
|
+
* (this wrapper plus the CST mark it delegates to) and the table driver took one
|
|
74
|
+
* per sequence term and per repetition item.
|
|
75
|
+
*/
|
|
76
|
+
export function rollbackTriviaAt(
|
|
77
|
+
ctx: ParseContext,
|
|
78
|
+
raw: number,
|
|
79
|
+
tlog: number,
|
|
80
|
+
leaves: number,
|
|
81
|
+
fields: number,
|
|
82
|
+
errors: number,
|
|
83
|
+
log: number,
|
|
84
|
+
rootLog: number,
|
|
85
|
+
): void {
|
|
86
|
+
rollbackCstCaptureAt(ctx, raw, tlog, leaves, fields, errors)
|
|
87
|
+
if (ctx._triviaLog && ctx._triviaLog.length !== log) ctx._triviaLog.length = log
|
|
88
|
+
if (ctx._rootTriviaLog && ctx._rootTriviaLog.length !== rootLog) ctx._rootTriviaLog.length = rootLog
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function rollbackTrivia(ctx: ParseContext, mark: TriviaRollbackMark): void {
|
|
92
|
+
rollbackCstCapture(ctx, { raw: mark.raw, tlog: mark.tlog, leaves: mark.leaves, fields: mark.fields, errors: mark.errors })
|
|
93
|
+
// Guarded like every other truncation — see rollbackCstCapture.
|
|
94
|
+
if (ctx._triviaLog && ctx._triviaLog.length !== mark.log) ctx._triviaLog.length = mark.log
|
|
95
|
+
if (ctx._rootTriviaLog && ctx._rootTriviaLog.length !== mark.rootLog) ctx._rootTriviaLog.length = mark.rootLog
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseTriviaNoCapture(
|
|
99
|
+
triviaP: Combinator<unknown>,
|
|
100
|
+
input: string,
|
|
101
|
+
cur: number,
|
|
102
|
+
ctx: ParseContext,
|
|
103
|
+
): ParseResult<unknown> {
|
|
104
|
+
const probeCtx = createDetachedParseContext(ctx.trackLines, ctx.state)
|
|
105
|
+
probeCtx._lineIndex = ctx._lineIndex
|
|
106
|
+
probeCtx._lineStarts = ctx._lineStarts
|
|
107
|
+
probeCtx._lineScannedTo = ctx._lineScannedTo
|
|
108
|
+
const result = triviaP.parse(input, cur, probeCtx)
|
|
109
|
+
ctx._lineScannedTo = probeCtx._lineScannedTo
|
|
110
|
+
return result
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A ZERO-WIDTH LOOKAHEAD's mark: everything `saveTriviaMark` covers, plus the
|
|
115
|
+
* completions probe's current best failure.
|
|
116
|
+
*
|
|
117
|
+
* `_probe` is a sink like any other — `failAt` (probe.ts) replaces
|
|
118
|
+
* `ctx._probe.best` whenever a failure lands at or before the cursor — but it is
|
|
119
|
+
* deliberately NOT part of `rollbackTrivia`, because most rollbacks must leave it
|
|
120
|
+
* alone. A failed `choice` arm or `sequence` term SHOULD keep its contribution:
|
|
121
|
+
* merging the expectations of alternatives that failed at the cursor is exactly
|
|
122
|
+
* how `completionsAt` builds its set.
|
|
123
|
+
*
|
|
124
|
+
* A lookahead is the exception. `peek`/`not` consume nothing on EITHER outcome
|
|
125
|
+
* and promise to leave no observable trace, so expectations raised INSIDE the
|
|
126
|
+
* probe are not reachable from the enclosing grammar at the cursor:
|
|
127
|
+
* `peek(sequence(literal('a'), literal('zzz')))` over `"ab"` offered `"zzz"` as
|
|
128
|
+
* the ONLY completion, though no input can ever reach it there.
|
|
129
|
+
*/
|
|
130
|
+
export type LookaheadMark = TriviaRollbackMark & { probeBest: ParseFail | null }
|
|
131
|
+
|
|
132
|
+
export function saveLookaheadMark(ctx: ParseContext): LookaheadMark {
|
|
133
|
+
return { ...saveTriviaMark(ctx), probeBest: ctx._probe ? ctx._probe.best : null }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function rollbackLookahead(ctx: ParseContext, mark: LookaheadMark): void {
|
|
137
|
+
rollbackTrivia(ctx, mark)
|
|
138
|
+
if (ctx._probe) ctx._probe.best = mark.probeBest
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function scanWithLabels(input: string, cur: number, ctx: ParseContext): TriviaScan {
|
|
142
|
+
const triviaP = ctx.trivia!
|
|
143
|
+
const spec = analyzeLabeledTrivia(triviaP)
|
|
144
|
+
if (!spec) return { end: cur, commit: NOOP_COMMIT }
|
|
145
|
+
|
|
146
|
+
const log = ctx._triviaLog
|
|
147
|
+
const rootLog = ctx._rootTriviaLog
|
|
148
|
+
const rootKinds = ctx._rootTriviaKindIndex
|
|
149
|
+
const captureTl = ctx.captureTrivia && (ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined)
|
|
150
|
+
const mask = ctx._triviaCaptureMask
|
|
151
|
+
let fullRows: number[] | undefined
|
|
152
|
+
let rootRows: number[] | undefined
|
|
153
|
+
let cstRows: number[] | undefined
|
|
154
|
+
const visitedEnd = visitLabeledTrivia(input, cur, spec, ctx.state, (start, matchEnd, kindIndex) => {
|
|
155
|
+
if (log !== undefined) (fullRows ??= []).push(start, matchEnd, kindIndex)
|
|
156
|
+
const selectedKind = rootLog === undefined || rootKinds === undefined || ctx._rootTriviaCapture === false
|
|
157
|
+
? undefined
|
|
158
|
+
: rootKinds[spec.labels[kindIndex] ?? '']
|
|
159
|
+
if (selectedKind !== undefined) (rootRows ??= []).push(start, matchEnd, selectedKind)
|
|
160
|
+
if (captureTl && (mask === undefined || (mask & (1 << kindIndex)) !== 0)) {
|
|
161
|
+
(cstRows ??= []).push(start, matchEnd, kindIndex)
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
if (visitedEnd !== undefined) {
|
|
165
|
+
if (visitedEnd === cur) return { end: cur, commit: NOOP_COMMIT }
|
|
166
|
+
return {
|
|
167
|
+
end: visitedEnd,
|
|
168
|
+
commit: () => {
|
|
169
|
+
if (fullRows !== undefined && log !== undefined) log.push(...fullRows)
|
|
170
|
+
if (rootRows !== undefined && rootLog !== undefined) {
|
|
171
|
+
for (let i = 0; i < rootRows.length; i += 3) {
|
|
172
|
+
rootLog.push(cur, visitedEnd, rootRows[i]!, rootRows[i + 1]!, rootRows[i + 2]!)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (cstRows !== undefined) {
|
|
176
|
+
for (let i = 0; i < cstRows.length; i += 3) {
|
|
177
|
+
pushCstTriviaEntry(ctx, cstRows[i]!, cstRows[i + 1]!, cstRows[i + 2]!)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const { end, chunks } = scanLabeledTriviaChunks(input, cur, spec, ctx.state)
|
|
185
|
+
if (end === cur) return { end: cur, commit: NOOP_COMMIT }
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
end,
|
|
189
|
+
commit: () => recordTriviaChunks(ctx, chunks),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Skip classified trivia without constructing retained chunk records. */
|
|
194
|
+
function skipWithLabels(input: string, cur: number, ctx: ParseContext): number {
|
|
195
|
+
const spec = analyzeLabeledTrivia(ctx.trivia!)
|
|
196
|
+
if (spec) return scanLabeledTriviaEnd(input, cur, spec, ctx.state)
|
|
197
|
+
const tr = parseTriviaNoCapture(ctx.trivia!, input, cur, ctx)
|
|
198
|
+
return tr.ok && tr.span.end > cur ? tr.span.end : cur
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** End of the trivia run at `cur` via the interpreter — the no-scanner fallback. */
|
|
202
|
+
function triviaEndByParse(
|
|
203
|
+
triviaP: Combinator<unknown>,
|
|
204
|
+
input: string,
|
|
205
|
+
cur: number,
|
|
206
|
+
ctx: ParseContext,
|
|
207
|
+
trackLines: boolean,
|
|
208
|
+
): number {
|
|
209
|
+
const tr = triviaP.parse(input, cur, createDetachedParseContext(trackLines, ctx.state))
|
|
210
|
+
return tr.ok ? tr.span.end : cur
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Skip trivia at `cur` and return the new position. No recording, no wrapper
|
|
215
|
+
* object — use between sequence/repeat terms when CST trivia capture is off.
|
|
216
|
+
*/
|
|
217
|
+
export function advanceTrivia(input: string, cur: number, ctx: ParseContext): number {
|
|
218
|
+
const triviaP = ctx.trivia
|
|
219
|
+
if (!triviaP) return cur
|
|
220
|
+
if (!ctx.trackLines) {
|
|
221
|
+
const fast = fastTriviaScanner(triviaP)
|
|
222
|
+
if (fast) return fast(input, cur)
|
|
223
|
+
if (ctx.triviaKindLabels) return skipWithLabels(input, cur, ctx)
|
|
224
|
+
const tr = triviaP.parse(input, cur, createDetachedParseContext(ctx.trackLines, ctx.state))
|
|
225
|
+
return tr.ok && tr.span.end > cur ? tr.span.end : cur
|
|
226
|
+
}
|
|
227
|
+
const trackTriviaLines = ctx.trackLines && triviaP._meta.canMatchNewline
|
|
228
|
+
const fast = fastTriviaScanner(triviaP)
|
|
229
|
+
if (fast) {
|
|
230
|
+
const end = fast(input, cur)
|
|
231
|
+
if (trackTriviaLines) recordLineRangeFromContext(ctx, input, cur, end)
|
|
232
|
+
return end
|
|
233
|
+
}
|
|
234
|
+
if (ctx.triviaKindLabels) {
|
|
235
|
+
const end = skipWithLabels(input, cur, ctx)
|
|
236
|
+
if (trackTriviaLines) recordLineRangeFromContext(ctx, input, cur, end)
|
|
237
|
+
return end
|
|
238
|
+
}
|
|
239
|
+
const tr = parseTriviaNoCapture(triviaP, input, cur, ctx)
|
|
240
|
+
return tr.ok && tr.span.end > cur ? tr.span.end : cur
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Scan trivia at `cur` using `ctx.trivia`, WITHOUT recording it. Returns the
|
|
245
|
+
* position after the trivia (or `cur` if none) and a `commit()` to record it.
|
|
246
|
+
*/
|
|
247
|
+
export function scanTrivia(input: string, cur: number, ctx: ParseContext): TriviaScan {
|
|
248
|
+
const triviaP = ctx.trivia
|
|
249
|
+
if (!triviaP) return { end: cur, commit: NOOP_COMMIT }
|
|
250
|
+
|
|
251
|
+
const log = ctx._triviaLog
|
|
252
|
+
const rootLog = ctx._rootTriviaLog
|
|
253
|
+
const captureTl = ctx.captureTrivia && (ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined)
|
|
254
|
+
if (!ctx.trackLines) {
|
|
255
|
+
const fast = !ctx.triviaKindLabels ? fastTriviaScanner(triviaP) : null
|
|
256
|
+
if (fast && log === undefined && !captureTl) {
|
|
257
|
+
return { end: fast(input, cur), commit: NOOP_COMMIT }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (ctx.triviaKindLabels && (log !== undefined || rootLog !== undefined || captureTl)) {
|
|
261
|
+
return scanWithLabels(input, cur, ctx)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// LABELLED TRIVIA WITH NOTHING TO RECORD. The branch above took every sink,
|
|
265
|
+
// so no log, no root log and no capture sink is live here: the only thing
|
|
266
|
+
// that can escape this call is `end`. `advanceTrivia:205` has always answered
|
|
267
|
+
// that with the fused scanner, labels or not; this is the same answer in the
|
|
268
|
+
// twin that returns a `TriviaScan`, and it is the difference between one
|
|
269
|
+
// regex-shaped run and a per-character classify whose kind is then dropped.
|
|
270
|
+
if (ctx.triviaKindLabels) {
|
|
271
|
+
const labelledFast = fastTriviaScanner(triviaP)
|
|
272
|
+
return { end: labelledFast ? labelledFast(input, cur) : skipWithLabels(input, cur, ctx), commit: NOOP_COMMIT }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (log !== undefined || rootLog !== undefined || captureTl) {
|
|
276
|
+
// CAPTURE DOES NOT NEED THE INTERPRETER. What lands in either sink is the
|
|
277
|
+
// one `(cur, end)` row below — the trivia's internal structure is never
|
|
278
|
+
// read — so a scanner that agrees with `triviaP` on `end` produces byte-
|
|
279
|
+
// identical capture. Delegating to `triviaP.parse` cost a detached
|
|
280
|
+
// `ParseContext` plus a full re-entry into the combinator interpreter PER
|
|
281
|
+
// SEQUENCE TERM, and bought nothing that survived the call.
|
|
282
|
+
const end = fast
|
|
283
|
+
? fast(input, cur)
|
|
284
|
+
: triviaEndByParse(triviaP, input, cur, ctx, log !== undefined ? false : ctx.trackLines)
|
|
285
|
+
if (end === cur) return { end: cur, commit: NOOP_COMMIT }
|
|
286
|
+
return {
|
|
287
|
+
end,
|
|
288
|
+
commit: () => {
|
|
289
|
+
pushTriviaLogEntry(ctx, cur, end)
|
|
290
|
+
if (captureTl) pushCstTriviaEntry(ctx, cur, end)
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const tr = triviaP.parse(input, cur, createDetachedParseContext(ctx.trackLines, ctx.state))
|
|
296
|
+
return { end: tr.ok ? tr.span.end : cur, commit: NOOP_COMMIT }
|
|
297
|
+
}
|
|
298
|
+
const trackTriviaLines = ctx.trackLines && triviaP._meta.canMatchNewline
|
|
299
|
+
|
|
300
|
+
const fast = !ctx.triviaKindLabels ? fastTriviaScanner(triviaP) : null
|
|
301
|
+
if (fast && log === undefined && !captureTl) {
|
|
302
|
+
const end = fast(input, cur)
|
|
303
|
+
if (trackTriviaLines) recordLineRangeFromContext(ctx, input, cur, end)
|
|
304
|
+
return { end, commit: NOOP_COMMIT }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (ctx.triviaKindLabels && (log !== undefined || rootLog !== undefined || captureTl)) {
|
|
308
|
+
const scan = scanWithLabels(input, cur, ctx)
|
|
309
|
+
if (trackTriviaLines) recordLineRangeFromContext(ctx, input, cur, scan.end)
|
|
310
|
+
return scan
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// The `trackLines` twin of the branch above, same reasoning: nothing records,
|
|
314
|
+
// so `end` is the whole result and the fused scanner produces it. The line
|
|
315
|
+
// range is recorded from the SPAN either way, which is why the scanner loses
|
|
316
|
+
// nothing here that `skipWithLabels` was providing.
|
|
317
|
+
if (ctx.triviaKindLabels) {
|
|
318
|
+
const labelledFast = fastTriviaScanner(triviaP)
|
|
319
|
+
const end = labelledFast ? labelledFast(input, cur) : skipWithLabels(input, cur, ctx)
|
|
320
|
+
if (trackTriviaLines) recordLineRangeFromContext(ctx, input, cur, end)
|
|
321
|
+
return { end, commit: NOOP_COMMIT }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (log !== undefined || rootLog !== undefined || captureTl) {
|
|
325
|
+
// Same equivalence as the `trackLines`-off twin above; line ranges are
|
|
326
|
+
// recorded from the SPAN either way, so the scanner loses nothing.
|
|
327
|
+
let end: number
|
|
328
|
+
if (fast) {
|
|
329
|
+
end = fast(input, cur)
|
|
330
|
+
if (trackTriviaLines) recordLineRangeFromContext(ctx, input, cur, end)
|
|
331
|
+
} else {
|
|
332
|
+
const tr = parseTriviaNoCapture(triviaP, input, cur, ctx)
|
|
333
|
+
end = tr.ok ? tr.span.end : cur
|
|
334
|
+
}
|
|
335
|
+
if (end === cur) return { end: cur, commit: NOOP_COMMIT }
|
|
336
|
+
return {
|
|
337
|
+
end,
|
|
338
|
+
commit: () => {
|
|
339
|
+
pushTriviaLogEntry(ctx, cur, end)
|
|
340
|
+
if (captureTl) pushCstTriviaEntry(ctx, cur, end)
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const tr = parseTriviaNoCapture(triviaP, input, cur, ctx)
|
|
346
|
+
return { end: tr.ok ? tr.span.end : cur, commit: NOOP_COMMIT }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Skip trivia at `cur` through an INSTALLED scanner, recording it immediately.
|
|
351
|
+
*
|
|
352
|
+
* The table drivers install a scanner only for UNLABELLED trivia with
|
|
353
|
+
* `trackLines` off, and that is exactly the case where the deferred path's whole
|
|
354
|
+
* commit is the single row `(cur, end)` — so there is nothing to defer, and the
|
|
355
|
+
* `{ end, commit }` pair plus its closure that `scanTrivia(…).commit()` allocated
|
|
356
|
+
* on EVERY sequence term of EVERY capturing grammar buy nothing. This is that
|
|
357
|
+
* branch with the allocations removed, kept here so recording has one home.
|
|
358
|
+
*/
|
|
359
|
+
export function skipTriviaScanned(
|
|
360
|
+
s: FastTriviaScanner,
|
|
361
|
+
input: string,
|
|
362
|
+
cur: number,
|
|
363
|
+
ctx: ParseContext,
|
|
364
|
+
): number {
|
|
365
|
+
const end = s(input, cur)
|
|
366
|
+
if (end !== cur) {
|
|
367
|
+
// `pushTriviaLogEntry`'s kind-index branch is unreachable without labels.
|
|
368
|
+
if (ctx._triviaLog !== undefined) ctx._triviaLog.push(cur, end)
|
|
369
|
+
if (ctx.captureTrivia === true && (ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined)) {
|
|
370
|
+
pushCstTriviaEntry(ctx, cur, end)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return end
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Consume trivia at `cur`, recording it immediately. For callers that always
|
|
378
|
+
* accept the trivia between two committed terms (e.g. sequence/sepBy).
|
|
379
|
+
*/
|
|
380
|
+
export function consumeTrivia(input: string, cur: number, ctx: ParseContext): number {
|
|
381
|
+
if (!needsDeferredTriviaCommit(ctx)) return advanceTrivia(input, cur, ctx)
|
|
382
|
+
const scan = scanTrivia(input, cur, ctx)
|
|
383
|
+
scan.commit()
|
|
384
|
+
return scan.end
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* End of the trivia run at `cur`, WITHOUT recording any of it.
|
|
389
|
+
*
|
|
390
|
+
* The read-only twin of `consumeTrivia`, for a zero-width ASSERTION that wants the
|
|
391
|
+
* adjacency fact and must leave no trace: the deferred path returns a scan whose
|
|
392
|
+
* `commit()` is simply never called, so nothing lands in any buffer and there is
|
|
393
|
+
* nothing to roll back. The following term re-scans the same gap and owns the
|
|
394
|
+
* commit/rewind decision exactly as if the assertion were not there.
|
|
395
|
+
*/
|
|
396
|
+
export function probeTriviaEnd(input: string, cur: number, ctx: ParseContext): number {
|
|
397
|
+
if (!ctx.trivia) return cur
|
|
398
|
+
// "WITHOUT recording any of it" has to include LINE ranges, and it did not.
|
|
399
|
+
// Both paths call `recordLineRangeFromContext` whenever
|
|
400
|
+
// `ctx.trackLines && trivia.canMatchNewline`, so a rejected `notAdjacent()`
|
|
401
|
+
// left a line record behind for a gap the parse then re-scanned — the probe was
|
|
402
|
+
// read-only for buffers and read-write for line data. Probing through a context
|
|
403
|
+
// with `trackLines` off makes `trackTriviaLines` false in both scanners, so
|
|
404
|
+
// there is nothing to record and nothing to undo. Everything the scanners
|
|
405
|
+
// actually need to find the end (`trivia`, `state`, the deferred-commit sinks)
|
|
406
|
+
// is carried through unchanged.
|
|
407
|
+
const probeCtx: ParseContext = { ...ctx, trackLines: false }
|
|
408
|
+
if (needsDeferredTriviaCommit(probeCtx)) return scanTrivia(input, cur, probeCtx).end
|
|
409
|
+
return advanceTrivia(input, cur, probeCtx)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* The specialised scanner for a trivia combinator, or null when its shape has no
|
|
414
|
+
* lowering. Memoized on the combinator.
|
|
415
|
+
*
|
|
416
|
+
* EXPORTED because G5's leaf swap needs it at TABLE-BUILD time: the table driver
|
|
417
|
+
* resolves the scanner once, per trivia entry, and installs the closure at scope
|
|
418
|
+
* entry — where the generic path called this (a WeakMap lookup) plus a chain of
|
|
419
|
+
* option branches on EVERY sequence term.
|
|
420
|
+
*/
|
|
421
|
+
export function fastTriviaScanner(trivia: Combinator<unknown>): FastTriviaScanner | null {
|
|
422
|
+
if (trivia === fastTrivia0) return fastScanner0!
|
|
423
|
+
if (trivia === fastTrivia1) return fastScanner1!
|
|
424
|
+
if (trivia === fastTrivia2) return fastScanner2!
|
|
425
|
+
if (trivia === fastTrivia3) return fastScanner3!
|
|
426
|
+
const scanner = buildFastTriviaScanner(trivia)
|
|
427
|
+
fastTrivia3 = fastTrivia2
|
|
428
|
+
fastScanner3 = fastScanner2
|
|
429
|
+
fastTrivia2 = fastTrivia1
|
|
430
|
+
fastScanner2 = fastScanner1
|
|
431
|
+
fastTrivia1 = fastTrivia0
|
|
432
|
+
fastScanner1 = fastScanner0
|
|
433
|
+
fastTrivia0 = trivia
|
|
434
|
+
fastScanner0 = scanner
|
|
435
|
+
return scanner
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function buildFastTriviaScanner(trivia: Combinator<unknown>): FastTriviaScanner | null {
|
|
439
|
+
const core = trivia._def.tag === 'trivia' ? trivia._def.parser : trivia
|
|
440
|
+
const direct = regexTriviaScanner(core)
|
|
441
|
+
if (direct) return direct
|
|
442
|
+
|
|
443
|
+
const repeat = core._def.tag === 'oneOrMore' || (core._def.tag === 'many' && core._def.min >= 1)
|
|
444
|
+
? core._def.parser
|
|
445
|
+
: null
|
|
446
|
+
if (!repeat) return null
|
|
447
|
+
|
|
448
|
+
const one = regexTriviaScanner(repeat)
|
|
449
|
+
if (one) return loopScanner([one])
|
|
450
|
+
|
|
451
|
+
if (repeat._def.tag !== 'choice') return null
|
|
452
|
+
const arms = repeat._def.parsers.map(regexTriviaScanner)
|
|
453
|
+
if (arms.some(s => s === null)) return null
|
|
454
|
+
return loopScanner(arms as FastTriviaScanner[])
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function loopScanner(arms: FastTriviaScanner[]): FastTriviaScanner {
|
|
458
|
+
return (input, cur) => {
|
|
459
|
+
let pos = cur
|
|
460
|
+
scan: while (pos < input.length) {
|
|
461
|
+
for (const arm of arms) {
|
|
462
|
+
const end = arm(input, pos)
|
|
463
|
+
if (end > pos) {
|
|
464
|
+
pos = end
|
|
465
|
+
continue scan
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
break
|
|
469
|
+
}
|
|
470
|
+
return pos
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function regexTriviaScanner(parser: Combinator<unknown>): FastTriviaScanner | null {
|
|
475
|
+
// A `label()` WRAPPER IS A NAME, NOT A SHAPE. `classifiedTrivia()` builds
|
|
476
|
+
// `trivia(oneOrMore(choice(label(name, regex)…)))` (combinators/map.ts), and
|
|
477
|
+
// this declined the whole thing on the wrapper alone — every arm BODY is a
|
|
478
|
+
// plain regex, and `encode.ts:288` already reads them straight back out as
|
|
479
|
+
// `[label, source, flags]` triples.
|
|
480
|
+
//
|
|
481
|
+
// What a label carries is which KIND matched, and this scanner never answered
|
|
482
|
+
// that question for an unlabelled grammar either: it returns an end offset and
|
|
483
|
+
// nothing else. So unwrapping loses no information the result could hold. The
|
|
484
|
+
// caller decides whether kinds are needed and only reaches here when they are
|
|
485
|
+
// not — see `scanTrivia`, which still routes every recording path through
|
|
486
|
+
// `scanWithLabels`.
|
|
487
|
+
//
|
|
488
|
+
// The cost of declining was not a slower scan, it was NO scan: with
|
|
489
|
+
// `fastTriviaScanner` null for all four of jess's trivia slots, `program.ts:475`
|
|
490
|
+
// filled `triviaScan` with nulls, `advanceTrivia:205` fell past its fast arm,
|
|
491
|
+
// and every trivia gap in every dialect went through the labelled char scanner.
|
|
492
|
+
const d = parser._def.tag === 'label' ? parser._def.parser._def : parser._def
|
|
493
|
+
if (d.tag !== 'regex' || d.flags) return null
|
|
494
|
+
const source = d.source
|
|
495
|
+
return classRunSource(source)
|
|
496
|
+
?? altStarSource(source)
|
|
497
|
+
?? prefixRunSource(source)
|
|
498
|
+
?? delimitedSource(source)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Literal fragments are compared with `charCodeAt`, which reads CODE UNITS — the
|
|
503
|
+
* same granularity the non-`u` engine matches at (a `u` flag is rejected upstream
|
|
504
|
+
* with every other flag). An astral literal would have been folded to one code
|
|
505
|
+
* POINT by `literalCodePoints`, so it could never compare equal; declining keeps
|
|
506
|
+
* the scanner exact rather than silently under-matching.
|
|
507
|
+
*/
|
|
508
|
+
function bmpString(cps: number[] | null): string | null {
|
|
509
|
+
if (!cps) return null
|
|
510
|
+
for (let i = 0; i < cps.length; i++) if (cps[i]! > 0xffff) return null
|
|
511
|
+
return String.fromCharCode(...cps)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function inRanges(cp: number, ranges: Array<[number, number]>): boolean {
|
|
515
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
516
|
+
const r = ranges[i]!
|
|
517
|
+
if (cp >= r[0] && cp <= r[1]) return true
|
|
518
|
+
}
|
|
519
|
+
return false
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Greedy scanner for a positive char-class body (`^`-negated bodies are rejected upstream). */
|
|
523
|
+
function classScanner(classBody: string): FastTriviaScanner | null {
|
|
524
|
+
const ranges = parseClassRanges(classBody)
|
|
525
|
+
if (!ranges) return null
|
|
526
|
+
return (input, cur) => {
|
|
527
|
+
let pos = cur
|
|
528
|
+
while (pos < input.length && inRanges(input.charCodeAt(pos), ranges)) pos++
|
|
529
|
+
return pos
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Scanner for a `C[^\n\r]*` arm — consumes its leader through a line break. */
|
|
534
|
+
function untilLineBreakScanner(leaderCode: number): FastTriviaScanner {
|
|
535
|
+
return (input, cur) => {
|
|
536
|
+
if (input.charCodeAt(cur) !== leaderCode) return cur
|
|
537
|
+
let pos = cur + 1
|
|
538
|
+
while (pos < input.length) {
|
|
539
|
+
const cc = input.charCodeAt(pos)
|
|
540
|
+
if (cc === 10 || cc === 13) break
|
|
541
|
+
pos++
|
|
542
|
+
}
|
|
543
|
+
return pos
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** A bare positive char-class run `[class]*` / `[class]+` (`[ \t\n\r,]*`, …). */
|
|
548
|
+
function classRunSource(source: string): FastTriviaScanner | null {
|
|
549
|
+
const m = /^\[([^\]^](?:[^\]]|\\.)*)\][*+]$/.exec(source)
|
|
550
|
+
return m ? classScanner(m[1]!) : null
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* `<literal><class-run>` — a literal opener followed by a single char-class run,
|
|
555
|
+
* `//[^\n]*` and `#[^\n\r]*` being the line-comment spellings of it. The class may
|
|
556
|
+
* be negated or positive; `*` admits the bare opener, `+` requires one class char.
|
|
557
|
+
*
|
|
558
|
+
* This is the whole-arm generalisation of `classifyTriviaArm`'s `untilLineBreak`,
|
|
559
|
+
* which only ever saw ONE leader char inside a `(?:…)*` group — a two-char `//`
|
|
560
|
+
* opener had no lowering at all, so every Less trivia scan fell back to the
|
|
561
|
+
* interpreter.
|
|
562
|
+
*/
|
|
563
|
+
function prefixRunSource(source: string): FastTriviaScanner | null {
|
|
564
|
+
const m = /^((?:\\.|[^\\[\]()*+?|^$.{}])*)(\[(?:\\.|[^\]])+\])([*+])$/.exec(source)
|
|
565
|
+
if (!m) return null
|
|
566
|
+
const prefix = bmpString(literalCodePoints(m[1]!))
|
|
567
|
+
if (prefix === null) return null
|
|
568
|
+
const cls = parseClassOperand(m[2]!)
|
|
569
|
+
if (!cls) return null
|
|
570
|
+
const { ranges, negated } = cls
|
|
571
|
+
const minOne = m[3] === '+'
|
|
572
|
+
return (input, cur) => {
|
|
573
|
+
if (!input.startsWith(prefix, cur)) return cur
|
|
574
|
+
const start = cur + prefix.length
|
|
575
|
+
const len = input.length
|
|
576
|
+
let pos = start
|
|
577
|
+
while (pos < len && inRanges(input.charCodeAt(pos), ranges) !== negated) pos++
|
|
578
|
+
return minOne && pos === start ? cur : pos
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* `<open>(?:body)*<close>` scanned as "advance to the first `close`" — the block
|
|
584
|
+
* comment shape (`/*…*\/`), which no other recognizer here can classify, so css
|
|
585
|
+
* and Less trivia had NO fast scanner at all.
|
|
586
|
+
*
|
|
587
|
+
* The lowering ignores the body, so it is only legal when `(?:body)*` provably
|
|
588
|
+
* cannot span the `close` literal; `delimitedBodySound` is the proof, and an
|
|
589
|
+
* unproven body declines rather than scanning wrong. Ported from the archived
|
|
590
|
+
* `scannable-run.ts`, whose coverage is the specification for this shape.
|
|
591
|
+
*/
|
|
592
|
+
function delimitedSource(source: string): FastTriviaScanner | null {
|
|
593
|
+
// An escaped backslash in the body makes this a STRING shape, where "advance to
|
|
594
|
+
// the first close" would stop at an escaped delimiter. Not modelled; decline.
|
|
595
|
+
if (source.includes('\\\\')) return null
|
|
596
|
+
let i = 0
|
|
597
|
+
while (i < source.length && source[i] !== '(') i += source[i] === '\\' ? 2 : 1
|
|
598
|
+
if (source[i] !== '(' || source[i + 1] !== '?' || source[i + 2] !== ':') return null
|
|
599
|
+
let k = i + 3
|
|
600
|
+
let depth = 1
|
|
601
|
+
while (k < source.length && depth > 0) {
|
|
602
|
+
const ch = source[k]
|
|
603
|
+
if (ch === '\\') { k += 2; continue }
|
|
604
|
+
if (ch === '[') {
|
|
605
|
+
let j = k + 1
|
|
606
|
+
if (source[j] === '^') j++
|
|
607
|
+
while (j < source.length && source[j] !== ']') j += source[j] === '\\' ? 2 : 1
|
|
608
|
+
if (source[j] !== ']') return null
|
|
609
|
+
k = j + 1
|
|
610
|
+
continue
|
|
611
|
+
}
|
|
612
|
+
if (ch === '(') depth++
|
|
613
|
+
else if (ch === ')') depth--
|
|
614
|
+
k++
|
|
615
|
+
}
|
|
616
|
+
if (depth !== 0 || source[k] !== '*') return null
|
|
617
|
+
const body = source.slice(i + 3, k - 1)
|
|
618
|
+
const openCps = literalCodePoints(source.slice(0, i))
|
|
619
|
+
const closeCps = literalCodePoints(source.slice(k + 1))
|
|
620
|
+
if (!openCps || !closeCps || !delimitedBodySound(body, closeCps)) return null
|
|
621
|
+
const open = bmpString(openCps)
|
|
622
|
+
const close = bmpString(closeCps)
|
|
623
|
+
if (open === null || close === null) return null
|
|
624
|
+
return (input, cur) => {
|
|
625
|
+
if (!input.startsWith(open, cur)) return cur
|
|
626
|
+
const hit = input.indexOf(close, cur + open.length)
|
|
627
|
+
// Unterminated: the regex requires the close literal, so it does not match.
|
|
628
|
+
return hit === -1 ? cur : hit + close.length
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Split a `(?:…)` body on top-level `|`, respecting classes and nested groups. */
|
|
633
|
+
function splitDelimArms(body: string): string[] | null {
|
|
634
|
+
const arms: string[] = []
|
|
635
|
+
let depth = 0
|
|
636
|
+
let last = 0
|
|
637
|
+
let i = 0
|
|
638
|
+
while (i < body.length) {
|
|
639
|
+
const ch = body[i]
|
|
640
|
+
if (ch === '\\') { i += 2; continue }
|
|
641
|
+
if (ch === '[') {
|
|
642
|
+
let j = i + 1
|
|
643
|
+
if (body[j] === '^') j++
|
|
644
|
+
while (j < body.length && body[j] !== ']') j += body[j] === '\\' ? 2 : 1
|
|
645
|
+
if (body[j] !== ']') return null
|
|
646
|
+
i = j + 1
|
|
647
|
+
continue
|
|
648
|
+
}
|
|
649
|
+
if (ch === '(') { depth++; i++; continue }
|
|
650
|
+
if (ch === ')') { depth--; if (depth < 0) return null; i++; continue }
|
|
651
|
+
if (ch === '|' && depth === 0) { arms.push(body.slice(last, i)); last = i + 1 }
|
|
652
|
+
i++
|
|
653
|
+
}
|
|
654
|
+
if (depth !== 0) return null
|
|
655
|
+
arms.push(body.slice(last))
|
|
656
|
+
return arms
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Prove "scan to the first `close`" EQUIVALENT to `<open>(?:body)*<close>`. That
|
|
661
|
+
* holds iff `(?:body)*` can never match text containing `close`: greedy repetition
|
|
662
|
+
* then stops exactly before the first `close`, which is where the raw scan lands.
|
|
663
|
+
*
|
|
664
|
+
* Proven only for the delimiter-safe idiom — the sole body shape that says "any
|
|
665
|
+
* char that does not form the close":
|
|
666
|
+
*
|
|
667
|
+
* close is one char `l0`: body must be exactly `[^l0]`
|
|
668
|
+
* close is `l0 l1`: body must be exactly `[^l0] | l0(?!l1)`
|
|
669
|
+
*
|
|
670
|
+
* The bulk arm takes every char but `l0`; the guard arm takes `l0` only when `l1`
|
|
671
|
+
* does not follow. So the body admits `l0`, never `l0 l1`, and rejects nothing
|
|
672
|
+
* else — a body NARROWER than that would stop early while the raw scan ran on to
|
|
673
|
+
* the next close, and the two would diverge. Anything outside the template is
|
|
674
|
+
* declined. Sound by construction: true is only ever returned for a body proven
|
|
675
|
+
* not to contain `close`.
|
|
676
|
+
*/
|
|
677
|
+
function delimitedBodySound(body: string, close: number[]): boolean {
|
|
678
|
+
if (close.length > 2) return false
|
|
679
|
+
const l0 = close[0]!
|
|
680
|
+
const arms = splitDelimArms(body)
|
|
681
|
+
if (!arms) return false
|
|
682
|
+
let bulkSeen = false
|
|
683
|
+
let guardSeen = false
|
|
684
|
+
for (const arm of arms) {
|
|
685
|
+
const bulk = /^\[\^((?:\\.|[^\]])+)\]$/.exec(arm)
|
|
686
|
+
if (bulk) {
|
|
687
|
+
if (bulkSeen) return false
|
|
688
|
+
const ranges = parseClassRanges(bulk[1]!)
|
|
689
|
+
if (!ranges || ranges.length !== 1 || ranges[0]![0] !== l0 || ranges[0]![1] !== l0) return false
|
|
690
|
+
bulkSeen = true
|
|
691
|
+
continue
|
|
692
|
+
}
|
|
693
|
+
if (close.length === 2 && arm.endsWith(')')) {
|
|
694
|
+
const idx = arm.indexOf('(?!')
|
|
695
|
+
if (idx === -1 || guardSeen) return false
|
|
696
|
+
const prefix = literalCodePoints(arm.slice(0, idx))
|
|
697
|
+
const op = parseClassOperand(arm.slice(idx + 3, -1))
|
|
698
|
+
if (prefix?.length !== 1 || prefix[0] !== l0) return false
|
|
699
|
+
if (!op || op.negated || op.ranges.length !== 1
|
|
700
|
+
|| op.ranges[0]![0] !== close[1] || op.ranges[0]![1] !== close[1]) return false
|
|
701
|
+
guardSeen = true
|
|
702
|
+
continue
|
|
703
|
+
}
|
|
704
|
+
return false
|
|
705
|
+
}
|
|
706
|
+
return bulkSeen && guardSeen === (close.length === 2)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* One alternation arm of a `(?:…)*` trivia group: a positive char-class run
|
|
711
|
+
* `[class]` (a trailing `+`/`*` is redundant under the enclosing loop) or a line
|
|
712
|
+
* line-terminated arm `C[^\n\r]*` (`C` = one literal leader char, maybe escaped). Anything
|
|
713
|
+
* else is unclassifiable → no fast path, never a wrong one.
|
|
714
|
+
*/
|
|
715
|
+
type TriviaArm =
|
|
716
|
+
| { kind: 'class'; ranges: Array<[number, number]> }
|
|
717
|
+
| { kind: 'untilLineBreak'; code: number }
|
|
718
|
+
|
|
719
|
+
function classifyTriviaArm(arm: string): TriviaArm | null {
|
|
720
|
+
const cls = /^\[([^\]^](?:[^\]]|\\.)*)\][*+]?$/.exec(arm)
|
|
721
|
+
if (cls) {
|
|
722
|
+
const ranges = parseClassRanges(cls[1]!)
|
|
723
|
+
return ranges ? { kind: 'class', ranges } : null
|
|
724
|
+
}
|
|
725
|
+
const lc = /^(\\?.)\[\^\\n\\r\]\*$/.exec(arm)
|
|
726
|
+
if (lc) {
|
|
727
|
+
const marker = lc[1]!
|
|
728
|
+
return { kind: 'untilLineBreak', code: (marker.length === 2 ? marker[1]! : marker[0]!).charCodeAt(0) }
|
|
729
|
+
}
|
|
730
|
+
return null
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function armScanner(arm: TriviaArm): FastTriviaScanner {
|
|
734
|
+
if (arm.kind === 'untilLineBreak') return untilLineBreakScanner(arm.code)
|
|
735
|
+
const ranges = arm.ranges
|
|
736
|
+
return (input, cur) => {
|
|
737
|
+
let pos = cur
|
|
738
|
+
while (pos < input.length && inRanges(input.charCodeAt(pos), ranges)) pos++
|
|
739
|
+
return pos
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* A single tight loop over a merged char-class range list plus (usually one)
|
|
745
|
+
* line-terminated leader — the fast form of `(?:[class]|C[^\n\r]*)*`. Requires the
|
|
746
|
+
* caller to have checked marker/class disjointness (so a bare merged scan
|
|
747
|
+
* matches the regex regardless of arm order).
|
|
748
|
+
*/
|
|
749
|
+
function fusedTriviaScanner(ranges: Array<[number, number]>, leaders: number[]): FastTriviaScanner {
|
|
750
|
+
const c0 = leaders[0]!
|
|
751
|
+
const single = leaders.length === 1
|
|
752
|
+
return (input, cur) => {
|
|
753
|
+
let pos = cur
|
|
754
|
+
const len = input.length
|
|
755
|
+
for (;;) {
|
|
756
|
+
const c = input.charCodeAt(pos)
|
|
757
|
+
if (pos < len && inRanges(c, ranges)) { pos++; continue }
|
|
758
|
+
if (single ? c === c0 : leaders.includes(c)) {
|
|
759
|
+
pos++
|
|
760
|
+
while (pos < len) {
|
|
761
|
+
const cc = input.charCodeAt(pos)
|
|
762
|
+
if (cc === 10 || cc === 13) break
|
|
763
|
+
pos++
|
|
764
|
+
}
|
|
765
|
+
continue
|
|
766
|
+
}
|
|
767
|
+
break
|
|
768
|
+
}
|
|
769
|
+
return pos
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** Split a `(?:…)*` body on top-level `|`, respecting `[…]`; bail on nested groups. */
|
|
774
|
+
function splitTopLevelAlts(body: string): string[] | null {
|
|
775
|
+
const arms: string[] = []
|
|
776
|
+
let inClass = false
|
|
777
|
+
let start = 0
|
|
778
|
+
for (let i = 0; i < body.length; i++) {
|
|
779
|
+
const c = body[i]
|
|
780
|
+
if (c === '\\') { i++; continue }
|
|
781
|
+
if (inClass) { if (c === ']') inClass = false; continue }
|
|
782
|
+
if (c === '[') inClass = true
|
|
783
|
+
else if (c === '(') return null // nested group — leave it to RegExp.exec
|
|
784
|
+
else if (c === '|') { arms.push(body.slice(start, i)); start = i + 1 }
|
|
785
|
+
}
|
|
786
|
+
arms.push(body.slice(start))
|
|
787
|
+
return arms
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Trivia written as a single regex alternation-star, `(?:arm|arm|…)*` — e.g.
|
|
792
|
+
* `(?:[ \t\n\r,]|#[^\n\r]*)*`. Arms are classified independently and
|
|
793
|
+
* order-independently: char-class arms merge into one range list and line-terminated
|
|
794
|
+
* leaders are collected, so `(?:#…|[class])*` scans the same as `(?:[class]|#…)*`.
|
|
795
|
+
* The common (disjoint) case compiles to one fused loop; a leader that also sits
|
|
796
|
+
* inside a class is the one spot where arm order matters, so that falls back to
|
|
797
|
+
* the ordered `loopScanner`.
|
|
798
|
+
*/
|
|
799
|
+
function altStarSource(source: string): FastTriviaScanner | null {
|
|
800
|
+
const m = /^\(\?:(.*)\)[*+]$/.exec(source)
|
|
801
|
+
if (!m) return null
|
|
802
|
+
const armSrcs = splitTopLevelAlts(m[1]!)
|
|
803
|
+
if (!armSrcs || armSrcs.length < 2) return null
|
|
804
|
+
const arms: TriviaArm[] = []
|
|
805
|
+
for (const src of armSrcs) {
|
|
806
|
+
const arm = classifyTriviaArm(src)
|
|
807
|
+
if (!arm) return null
|
|
808
|
+
arms.push(arm)
|
|
809
|
+
}
|
|
810
|
+
const ranges: Array<[number, number]> = []
|
|
811
|
+
const leaders: number[] = []
|
|
812
|
+
for (const arm of arms) {
|
|
813
|
+
if (arm.kind === 'class') ranges.push(...arm.ranges)
|
|
814
|
+
else leaders.push(arm.code)
|
|
815
|
+
}
|
|
816
|
+
if (leaders.some(code => inRanges(code, ranges))) {
|
|
817
|
+
return loopScanner(arms.map(armScanner)) // order-significant overlap
|
|
818
|
+
}
|
|
819
|
+
if (leaders.length === 0) return armScanner({ kind: 'class', ranges })
|
|
820
|
+
return fusedTriviaScanner(ranges, leaders)
|
|
821
|
+
}
|