parseman 0.45.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/analysis/choice-cost-gate.d.ts +158 -0
- package/dist/analysis/choice-cost-gate.d.ts.map +1 -0
- package/dist/analysis/choice-cost-render.d.ts +96 -0
- package/dist/analysis/choice-cost-render.d.ts.map +1 -0
- package/dist/analysis/choice-cost.d.ts +383 -0
- package/dist/analysis/choice-cost.d.ts.map +1 -0
- package/dist/analysis/commitment.d.ts +131 -0
- package/dist/analysis/commitment.d.ts.map +1 -0
- package/dist/analysis/corpus.d.ts +73 -0
- package/dist/analysis/corpus.d.ts.map +1 -0
- package/dist/analysis/diagnose-render.d.ts +65 -0
- package/dist/analysis/diagnose-render.d.ts.map +1 -0
- package/dist/analysis/diagnose.d.ts +15 -0
- package/dist/analysis/diagnose.d.ts.map +1 -1
- package/dist/analysis/diagnostics.cjs +13736 -0
- package/dist/analysis/diagnostics.cjs.map +6 -0
- package/dist/analysis/diagnostics.d.ts +27 -0
- package/dist/analysis/diagnostics.d.ts.map +1 -0
- package/dist/analysis/diagnostics.js +13713 -0
- package/dist/analysis/diagnostics.js.map +6 -0
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/fix-render.d.ts +43 -0
- package/dist/analysis/fix-render.d.ts.map +1 -0
- package/dist/analysis/fix.d.ts +172 -0
- package/dist/analysis/fix.d.ts.map +1 -0
- package/dist/analysis/format-number.d.ts +23 -0
- package/dist/analysis/format-number.d.ts.map +1 -0
- package/dist/analysis/gating.d.ts +47 -2
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/rebuild.d.ts +61 -0
- package/dist/analysis/rebuild.d.ts.map +1 -0
- package/dist/analysis/terminal.d.ts +145 -0
- package/dist/analysis/terminal.d.ts.map +1 -0
- package/dist/cli/index.cjs +14837 -0
- package/dist/cli/index.cjs.map +6 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +14814 -0
- package/dist/cli/index.js.map +6 -0
- package/dist/combinators/adjacency.d.ts +100 -0
- package/dist/combinators/adjacency.d.ts.map +1 -0
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/completions.d.ts.map +1 -1
- package/dist/combinators/dispatch.d.ts +8 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/gate.d.ts.map +1 -1
- package/dist/combinators/grammar.d.ts +22 -0
- package/dist/combinators/grammar.d.ts.map +1 -1
- package/dist/combinators/literal.d.ts +8 -0
- package/dist/combinators/literal.d.ts.map +1 -1
- package/dist/combinators/map.d.ts +0 -1
- package/dist/combinators/map.d.ts.map +1 -1
- package/dist/combinators/node.d.ts +13 -3
- package/dist/combinators/node.d.ts.map +1 -1
- package/dist/combinators/parser.d.ts +1 -1
- package/dist/combinators/parser.d.ts.map +1 -1
- package/dist/combinators/ref.d.ts.map +1 -1
- package/dist/combinators/regex.d.ts +1 -16
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/repeat.d.ts +30 -2
- package/dist/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/scanTo.d.ts +71 -1
- package/dist/combinators/scanTo.d.ts.map +1 -1
- package/dist/combinators/sequence.d.ts.map +1 -1
- package/dist/combinators/token.d.ts.map +1 -1
- package/dist/combinators/trivia-boundary.d.ts +26 -0
- package/dist/combinators/trivia-boundary.d.ts.map +1 -0
- package/dist/combinators/trivia-skip.d.ts +40 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/combinators/withCtx.d.ts.map +1 -1
- package/dist/compiler/build-arity.d.ts.map +1 -1
- package/dist/compiler/compile-linkable-table.d.ts +104 -0
- package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
- package/dist/compiler/degradation.d.ts +0 -2
- package/dist/compiler/degradation.d.ts.map +1 -1
- package/dist/compiler/fields.d.ts +13 -0
- package/dist/compiler/fields.d.ts.map +1 -1
- package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
- package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +79 -47
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +81 -0
- package/dist/compiler/token-alphabet.d.ts.map +1 -0
- package/dist/compiler/token-dispatch.d.ts +95 -0
- package/dist/compiler/token-dispatch.d.ts.map +1 -0
- package/dist/compiler/token-scanner.d.ts +53 -0
- package/dist/compiler/token-scanner.d.ts.map +1 -0
- package/dist/compiler/value-usage.d.ts.map +1 -1
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/capture-buffer.d.ts +35 -0
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/cst/reflection-attach.d.ts +8 -0
- package/dist/cst/reflection-attach.d.ts.map +1 -0
- package/dist/cst/reflection-symbols.d.ts +10 -0
- package/dist/cst/reflection-symbols.d.ts.map +1 -0
- package/dist/cst/reflection.d.ts +2 -4
- package/dist/cst/reflection.d.ts.map +1 -1
- package/dist/cst/root-trivia-scope.d.ts +26 -0
- package/dist/cst/root-trivia-scope.d.ts.map +1 -0
- package/dist/cst/trivia-charscan.d.ts +18 -0
- package/dist/cst/trivia-charscan.d.ts.map +1 -0
- package/dist/cst/trivia-kinds.d.ts +19 -0
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/functional/doc.d.ts.map +1 -1
- package/dist/functional/run-tabled.d.ts +12 -0
- package/dist/functional/run-tabled.d.ts.map +1 -0
- package/dist/functional/run.d.ts +25 -42
- package/dist/functional/run.d.ts.map +1 -1
- package/dist/grammar-metadata.d.ts +3 -0
- package/dist/grammar-metadata.d.ts.map +1 -0
- package/dist/index.cjs +10499 -7478
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +24 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10499 -7478
- package/dist/index.js.map +3 -4
- package/dist/language-service/index.cjs +311 -121
- package/dist/language-service/index.cjs.map +3 -4
- package/dist/language-service/index.js +311 -121
- package/dist/language-service/index.js.map +3 -4
- package/dist/oracle/index.cjs.map +0 -1
- package/dist/oracle/index.js.map +0 -1
- package/dist/parse-context.d.ts +49 -0
- package/dist/parse-context.d.ts.map +1 -0
- package/dist/plugin/evaluator.d.ts +41 -2
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/free-identifiers.d.ts +23 -0
- package/dist/plugin/free-identifiers.d.ts.map +1 -0
- package/dist/plugin/index.cjs +11381 -9442
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11381 -9442
- package/dist/plugin/index.js.map +3 -4
- package/dist/plugin/reducer-resolver.d.ts +17 -1
- package/dist/plugin/reducer-resolver.d.ts.map +1 -1
- package/dist/regex/classes.d.ts +28 -0
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/regex/first-set.d.ts +14 -0
- package/dist/regex/first-set.d.ts.map +1 -1
- package/dist/run/index.cjs +136 -51
- package/dist/run/index.cjs.map +2 -3
- package/dist/run/index.js +136 -51
- package/dist/run/index.js.map +2 -3
- package/dist/spec/index.cjs +2898 -1622
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2898 -1622
- package/dist/spec/index.js.map +3 -4
- package/dist/spec/model.d.ts +8 -0
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/table/assemble.d.ts +307 -0
- package/dist/table/assemble.d.ts.map +1 -0
- package/dist/table/cell.d.ts +42 -0
- package/dist/table/cell.d.ts.map +1 -0
- package/dist/table/child-slots.d.ts +30 -0
- package/dist/table/child-slots.d.ts.map +1 -0
- package/dist/table/compile-rule-map.d.ts +136 -0
- package/dist/table/compile-rule-map.d.ts.map +1 -0
- package/dist/table/compile.d.ts +56 -0
- package/dist/table/compile.d.ts.map +1 -0
- package/dist/table/duplication-hook.d.ts +32 -0
- package/dist/table/duplication-hook.d.ts.map +1 -0
- package/dist/table/emit-assembly.d.ts +139 -0
- package/dist/table/emit-assembly.d.ts.map +1 -0
- package/dist/table/emit.d.ts +113 -0
- package/dist/table/emit.d.ts.map +1 -0
- package/dist/table/encode-baseline.d.ts +16 -0
- package/dist/table/encode-baseline.d.ts.map +1 -0
- package/dist/table/encode.d.ts +71 -0
- package/dist/table/encode.d.ts.map +1 -0
- package/dist/table/exec-baseline.d.ts +9 -0
- package/dist/table/exec-baseline.d.ts.map +1 -0
- package/dist/table/exec.d.ts +72 -0
- package/dist/table/exec.d.ts.map +1 -0
- package/dist/table/fold.d.ts +25 -0
- package/dist/table/fold.d.ts.map +1 -0
- package/dist/table/index.cjs +11375 -0
- package/dist/table/index.cjs.map +6 -0
- package/dist/table/index.d.ts +100 -0
- package/dist/table/index.d.ts.map +1 -0
- package/dist/table/index.js +11352 -0
- package/dist/table/index.js.map +6 -0
- package/dist/table/inspect.d.ts +17 -0
- package/dist/table/inspect.d.ts.map +1 -0
- package/dist/table/ops.d.ts +446 -0
- package/dist/table/ops.d.ts.map +1 -0
- package/dist/table/program.d.ts +493 -0
- package/dist/table/program.d.ts.map +1 -0
- package/dist/table/run-support.d.ts +38 -0
- package/dist/table/run-support.d.ts.map +1 -0
- package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
- package/dist/table/scan-shapes.d.ts.map +1 -0
- package/dist/table/site-labels.d.ts +58 -0
- package/dist/table/site-labels.d.ts.map +1 -0
- package/dist/table/stamp.d.ts +70 -0
- package/dist/table/stamp.d.ts.map +1 -0
- package/dist/types.d.ts +80 -24
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +41 -3
- package/src/analysis/choice-cost-gate.ts +359 -0
- package/src/analysis/choice-cost-render.ts +293 -0
- package/src/analysis/choice-cost.ts +1008 -0
- package/src/analysis/commitment.ts +557 -0
- package/src/analysis/corpus.ts +130 -0
- package/src/analysis/diagnose-render.ts +625 -0
- package/src/analysis/diagnose.ts +317 -0
- package/src/analysis/diagnostics.ts +33 -0
- package/src/analysis/duplication.ts +1955 -0
- package/src/analysis/fix-render.ts +217 -0
- package/src/analysis/fix.ts +669 -0
- package/src/analysis/format-number.ts +31 -0
- package/src/analysis/gating.ts +792 -0
- package/src/analysis/grammar.ts +72 -0
- package/src/analysis/rebuild.ts +287 -0
- package/src/analysis/terminal.ts +336 -0
- package/src/cli/index.ts +413 -0
- package/src/combinators/adjacency.ts +171 -0
- package/src/combinators/attempt.ts +41 -0
- package/src/combinators/case-fold.ts +90 -0
- package/src/combinators/choice.ts +466 -0
- package/src/combinators/completions.ts +64 -0
- package/src/combinators/dispatch.ts +358 -0
- package/src/combinators/expect.ts +159 -0
- package/src/combinators/first-set.ts +322 -0
- package/src/combinators/gate.ts +50 -0
- package/src/combinators/grammar.ts +272 -0
- package/src/combinators/keywords.ts +145 -0
- package/src/combinators/literal.ts +102 -0
- package/src/combinators/map.ts +106 -0
- package/src/combinators/node.ts +345 -0
- package/src/combinators/not.ts +55 -0
- package/src/combinators/parser.ts +266 -0
- package/src/combinators/peek.ts +63 -0
- package/src/combinators/probe.ts +25 -0
- package/src/combinators/ref.ts +109 -0
- package/src/combinators/regex.ts +170 -0
- package/src/combinators/repeat.ts +561 -0
- package/src/combinators/scanTo.ts +393 -0
- package/src/combinators/sequence.ts +221 -0
- package/src/combinators/token.ts +120 -0
- package/src/combinators/trivia-boundary.ts +48 -0
- package/src/combinators/trivia-skip.ts +821 -0
- package/src/combinators/withCtx.ts +45 -0
- package/src/compiler/build-arity.ts +330 -0
- package/src/compiler/compile-linkable-table.ts +209 -0
- package/src/compiler/degradation.ts +266 -0
- package/src/compiler/fields.ts +191 -0
- package/src/compiler/grammar-coverage-ids.ts +129 -0
- package/src/compiler/ir-serialize.ts +615 -0
- package/src/compiler/linker.ts +954 -0
- package/src/compiler/token-alphabet.ts +209 -0
- package/src/compiler/token-dispatch.ts +353 -0
- package/src/compiler/token-scanner.ts +318 -0
- package/src/compiler/value-usage.ts +147 -0
- package/src/coverage.ts +383 -0
- package/src/cst/capture-buffer.ts +297 -0
- package/src/cst/host-mode.ts +69 -0
- package/src/cst/reflection-attach.ts +11 -0
- package/src/cst/reflection-symbols.ts +9 -0
- package/src/cst/reflection.ts +149 -0
- package/src/cst/relative-spans.ts +207 -0
- package/src/cst/root-trivia-scope.ts +30 -0
- package/src/cst/trivia-charscan.ts +312 -0
- package/src/cst/trivia-entries.ts +285 -0
- package/src/cst/trivia-index.ts +131 -0
- package/src/cst/trivia-kinds.ts +358 -0
- package/src/cst/types.ts +76 -0
- package/src/cst/walk.ts +101 -0
- package/src/functional/doc.ts +959 -0
- package/src/functional/run-tabled.ts +153 -0
- package/src/functional/run.ts +558 -0
- package/src/grammar-metadata.ts +2 -0
- package/src/index.ts +189 -0
- package/src/language-service/index.ts +221 -0
- package/src/line-index.ts +131 -0
- package/src/oracle/digest.ts +620 -0
- package/src/oracle/index.ts +55 -0
- package/src/parse-context.ts +91 -0
- package/src/plugin/direct-builder-static.ts +102 -0
- package/src/plugin/evaluator.ts +1505 -0
- package/src/plugin/free-identifiers.ts +338 -0
- package/src/plugin/index.ts +2281 -0
- package/src/plugin/reducer-resolver.ts +606 -0
- package/src/recovery/scan.ts +155 -0
- package/src/regex/classes.ts +183 -0
- package/src/regex/first-set.ts +316 -0
- package/src/run/index.ts +31 -0
- package/src/spec/ebnf.ts +128 -0
- package/src/spec/index.ts +54 -0
- package/src/spec/model.ts +636 -0
- package/src/spec/railroad-lib.ts +559 -0
- package/src/spec/railroad.ts +256 -0
- package/src/table/assemble.ts +2994 -0
- package/src/table/cell.ts +42 -0
- package/src/table/child-slots.ts +113 -0
- package/src/table/compile-rule-map.ts +336 -0
- package/src/table/compile.ts +244 -0
- package/src/table/duplication-hook.ts +89 -0
- package/src/table/emit-assembly.ts +1766 -0
- package/src/table/emit.ts +418 -0
- package/src/table/encode-baseline.ts +340 -0
- package/src/table/encode.ts +1569 -0
- package/src/table/exec-baseline.ts +537 -0
- package/src/table/exec.ts +1672 -0
- package/src/table/fold.ts +38 -0
- package/src/table/index.ts +108 -0
- package/src/table/inspect.ts +52 -0
- package/src/table/ops.ts +459 -0
- package/src/table/program.ts +855 -0
- package/src/table/run-support.ts +66 -0
- package/src/table/scan-shapes.ts +1578 -0
- package/src/table/site-labels.ts +228 -0
- package/src/table/stamp.ts +229 -0
- package/src/types.ts +563 -0
- package/src/version.ts +16 -0
- package/dist/compiler/codegen.d.ts +0 -334
- package/dist/compiler/codegen.d.ts.map +0 -1
- package/dist/compiler/inline-build.d.ts +0 -11
- package/dist/compiler/inline-build.d.ts.map +0 -1
- package/dist/compiler/inline-callback.d.ts +0 -21
- package/dist/compiler/inline-callback.d.ts.map +0 -1
- package/dist/compiler/line-index.d.ts +0 -2
- package/dist/compiler/line-index.d.ts.map +0 -1
- package/dist/compiler/scannable-run.d.ts.map +0 -1
- package/dist/compiler/scannable-terminal.d.ts +0 -23
- package/dist/compiler/scannable-terminal.d.ts.map +0 -1
- package/dist/compiler/trivia-fast-path.d.ts +0 -38
- package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { Combinator, FirstSet, ParseContext, ParseError, ParseResult, RecoveryHelpers } from '../types.ts'
|
|
2
|
+
import { cstCaptureActive, pushCstChild } from '../cst/capture-buffer.ts'
|
|
3
|
+
import { annotateSpanFromLineContext, recordLineRangeFromContext } from '../line-index.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Recovery MECHANISM — sync-source-agnostic. These primitives implement "scan
|
|
7
|
+
* forward to a sync token and emit a ParseError" and "is the sync token here?".
|
|
8
|
+
* They are deliberately independent of WHERE the sync sentinel comes from: the
|
|
9
|
+
* grammar never carries recovery config. Sync points are inferred automatically
|
|
10
|
+
* from grammar structure (see `infer.ts`) and may be overridden externally by the
|
|
11
|
+
* language service via `ctx._listSync`. This is the salvaged core of the old
|
|
12
|
+
* `combinators/recover-scan.ts`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Run `sentinel` at `pos` as PURE lookahead: succeed/fail only, with every
|
|
17
|
+
* side-effecting sink suppressed (CST leaves/children/trivia, the trivia log,
|
|
18
|
+
* the recovery error sink, and the completions probe). A recovery scan probes the
|
|
19
|
+
* sentinel at many positions and must not capture leaves, log trivia, or pollute
|
|
20
|
+
* `completionsAt`'s furthest-failure set as a byproduct of looking ahead.
|
|
21
|
+
*/
|
|
22
|
+
function probeAt(sentinel: Combinator<unknown>, input: string, pos: number, ctx: ParseContext): boolean {
|
|
23
|
+
const s = {
|
|
24
|
+
_probe: ctx._probe, _errors: ctx._errors,
|
|
25
|
+
_cstLeaves: ctx._cstLeaves, _cstChildren: ctx._cstChildren, _cstRawChildren: ctx._cstRawChildren,
|
|
26
|
+
_cstTriviaLog: ctx._cstTriviaLog, _triviaLog: ctx._triviaLog, _rootTriviaLog: ctx._rootTriviaLog, _fields: ctx._fields, _cstBuf: ctx._cstBuf,
|
|
27
|
+
_tolerant: ctx._tolerant, _sync: ctx._sync,
|
|
28
|
+
}
|
|
29
|
+
ctx._probe = undefined; ctx._errors = undefined
|
|
30
|
+
ctx._cstLeaves = undefined; ctx._cstChildren = undefined; ctx._cstRawChildren = undefined
|
|
31
|
+
ctx._cstTriviaLog = undefined; ctx._triviaLog = undefined; ctx._rootTriviaLog = undefined; ctx._fields = undefined; ctx._cstBuf = undefined
|
|
32
|
+
// A sentinel is pure lookahead: it must not itself enter tolerant recovery (nor
|
|
33
|
+
// read a stale outer `_sync`). Otherwise a sentinel that composes many/sepBy would
|
|
34
|
+
// run recovery on every probed position — wasted work, and it could resync to the
|
|
35
|
+
// enclosing list's sync token. Cleared here and restored in `finally`.
|
|
36
|
+
ctx._tolerant = undefined; ctx._sync = undefined
|
|
37
|
+
try {
|
|
38
|
+
return sentinel.parse(input, pos, ctx).ok
|
|
39
|
+
} finally {
|
|
40
|
+
ctx._probe = s._probe; ctx._errors = s._errors
|
|
41
|
+
ctx._cstLeaves = s._cstLeaves; ctx._cstChildren = s._cstChildren; ctx._cstRawChildren = s._cstRawChildren
|
|
42
|
+
ctx._cstTriviaLog = s._cstTriviaLog; ctx._triviaLog = s._triviaLog; ctx._rootTriviaLog = s._rootTriviaLog; ctx._fields = s._fields; ctx._cstBuf = s._cstBuf
|
|
43
|
+
ctx._tolerant = s._tolerant; ctx._sync = s._sync
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Combine two recovery sentinels into one that matches when either does — used by
|
|
49
|
+
* tolerant `sepBy` to resync to its own separator OR the enclosing delimiter. Cold
|
|
50
|
+
* path only; a bare wrapper so we don't pull `choice()` (and its analysis) in.
|
|
51
|
+
*/
|
|
52
|
+
export function orSentinel(
|
|
53
|
+
a: Combinator<unknown>,
|
|
54
|
+
b: Combinator<unknown> | undefined,
|
|
55
|
+
): Combinator<unknown> {
|
|
56
|
+
if (b === undefined) return a
|
|
57
|
+
return {
|
|
58
|
+
_tag: 'orSentinel',
|
|
59
|
+
_meta: { firstSet: { kind: 'any' }, canMatchNewline: false, isTrivia: false },
|
|
60
|
+
_def: { tag: 'unknown' },
|
|
61
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<unknown> {
|
|
62
|
+
const ra = a.parse(input, pos, ctx)
|
|
63
|
+
if (ra.ok) return ra
|
|
64
|
+
return b.parse(input, pos, ctx)
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A lightweight zero-width sentinel that "matches" (succeeds, consuming nothing)
|
|
71
|
+
* when the input code point at `pos` starts the given first set. Sync inference
|
|
72
|
+
* builds one from a list's follow set so a nested list can resync to the enclosing
|
|
73
|
+
* delimiter with no grammar annotation. Returns `null` when the first set is `any`
|
|
74
|
+
* or `empty` (no usable inference).
|
|
75
|
+
*/
|
|
76
|
+
export function firstSetSentinel(fs: FirstSet): Combinator<null> | null {
|
|
77
|
+
if (fs.kind !== 'ranges' || fs.ranges.length === 0) return null
|
|
78
|
+
const ranges = fs.ranges
|
|
79
|
+
return {
|
|
80
|
+
_tag: 'firstSetSentinel',
|
|
81
|
+
_meta: { firstSet: fs, canMatchNewline: false, isTrivia: false },
|
|
82
|
+
_def: { tag: 'unknown' },
|
|
83
|
+
parse(input: string, pos: number): ParseResult<null> {
|
|
84
|
+
const code = pos < input.length ? input.codePointAt(pos) : undefined
|
|
85
|
+
if (code !== undefined) {
|
|
86
|
+
for (const r of ranges) {
|
|
87
|
+
if (code >= r.lo && code <= r.hi) return { ok: true, value: null, span: { start: pos, end: pos } }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { ok: false, expected: [], span: { start: pos, end: pos } }
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Cold-path recovery scan shared by tolerant `many()` / `sepBy()`. Scans forward
|
|
97
|
+
* from `from` until `sync` matches (or EOF) — probing the sentinel as pure
|
|
98
|
+
* lookahead — emits a {@link ParseError} spanning the skipped range, and pushes it
|
|
99
|
+
* to `ctx._errors`. The sync token is NOT consumed; the caller's loop resumes from
|
|
100
|
+
* `end`. Only ever reached after an element has failed, so entirely off the hot path.
|
|
101
|
+
*
|
|
102
|
+
* The caller owns the loop guard: `sepBy` progress is driven by its separator (a
|
|
103
|
+
* zero-width error for a missing element is fine); `many` checks `matchesAt(sync)`
|
|
104
|
+
* itself and breaks when sitting on the terminator, so a zero-width failure can
|
|
105
|
+
* never spin.
|
|
106
|
+
*/
|
|
107
|
+
export function recoverScan(
|
|
108
|
+
input: string,
|
|
109
|
+
from: number,
|
|
110
|
+
ctx: ParseContext,
|
|
111
|
+
sync: Combinator<unknown>,
|
|
112
|
+
expected: string[],
|
|
113
|
+
): { error: ParseError; end: number } {
|
|
114
|
+
let scanPos = from
|
|
115
|
+
while (scanPos < input.length && !probeAt(sync, input, scanPos, ctx)) scanPos++
|
|
116
|
+
const span = { start: from, end: scanPos }
|
|
117
|
+
if (ctx.trackLines) recordLineRangeFromContext(ctx, input, from, scanPos)
|
|
118
|
+
const error: ParseError = { _tag: 'parseError', span: ctx.trackLines ? annotateSpanFromLineContext(span, ctx) : span, expected }
|
|
119
|
+
ctx._errors?.push(error)
|
|
120
|
+
return { error, end: scanPos }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Does `sentinel` match at `pos` (pure lookahead)? Used by the tolerant list loops
|
|
125
|
+
* to detect "we are sitting on the sync/terminator token" without consuming it or
|
|
126
|
+
* capturing anything.
|
|
127
|
+
*/
|
|
128
|
+
export function matchesAt(
|
|
129
|
+
sentinel: Combinator<unknown>,
|
|
130
|
+
input: string,
|
|
131
|
+
pos: number,
|
|
132
|
+
ctx: ParseContext,
|
|
133
|
+
): boolean {
|
|
134
|
+
return probeAt(sentinel, input, pos, ctx)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Embed a recovered error as a `parseError` CST child at the recovery point, so the
|
|
139
|
+
* error lives IN the tree (rides reused subtrees across incremental edits; a tree
|
|
140
|
+
* walk finds every diagnostic) — not only in the flat `ctx._errors` side-channel.
|
|
141
|
+
* No-op when CST capture is off (value/AST mode). Both the interpreter list loops
|
|
142
|
+
* and the compiled recovery branches call this (the latter via `_ctx._rec.capture`),
|
|
143
|
+
* so the embedded node is identical on both paths.
|
|
144
|
+
*/
|
|
145
|
+
export function captureError(ctx: ParseContext, error: ParseError): void {
|
|
146
|
+
if (cstCaptureActive(ctx)) pushCstChild(ctx, error, error)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The recovery-helper bundle handed to a tolerant parse via `ctx._rec`. A COMPILED
|
|
151
|
+
* grammar can't import these, so the driver (`run`, `parseDoc`) injects them; the
|
|
152
|
+
* interpreter list loops read the same bundle, guaranteeing byte-identical recovery
|
|
153
|
+
* across paths. Shared here so every driver installs the exact same instance.
|
|
154
|
+
*/
|
|
155
|
+
export const REC: RecoveryHelpers = { scan: recoverScan, at: matchesAt, or: orSentinel, sentinel: firstSetSentinel, capture: captureError }
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level regex char-class primitives, shared by every hand-rolled regex
|
|
3
|
+
* analysis in the codebase — the interpreter's first-set walker
|
|
4
|
+
* (`./first-set.ts`), `regex()`'s short-scan fast path, and codegen's scannable
|
|
5
|
+
* lowering (`../compiler/scannable-run.ts`). Kept dependency-free (no combinator,
|
|
6
|
+
* no codegen imports) so it lands in even the leanest interpreter bundle.
|
|
7
|
+
*
|
|
8
|
+
* Everything here parses a regex STRUCTURE to code-point ranges; nothing encodes
|
|
9
|
+
* a specific byte meaning ("this is whitespace"). `\d`/`\w` lower to their ASCII
|
|
10
|
+
* ranges (correct for the default, non-`u` engine); `\s` uses the fixed
|
|
11
|
+
* `SPACE_RANGES` set (unaffected by the `u` flag).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Single-char escapes whose code point is fixed regardless of context. */
|
|
15
|
+
export const CLASS_ESCAPES: Record<string, number> = { t: 9, n: 10, r: 13, f: 12, v: 11, '0': 0 }
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `\s`'s code-point set per the spec's `WhiteSpace` + `LineTerminator`
|
|
19
|
+
* productions — TAB/LF/VT/FF/CR, SPACE, NBSP, and the Unicode `Zs` space
|
|
20
|
+
* separators. Fixed regardless of the `u` flag, so always safe to lower.
|
|
21
|
+
*/
|
|
22
|
+
export const SPACE_RANGES: Array<[number, number]> = [
|
|
23
|
+
[9, 13], [32, 32], [160, 160], [5760, 5760], [8192, 8202],
|
|
24
|
+
[8232, 8232], [8233, 8233], [8239, 8239], [8287, 8287], [12288, 12288], [65279, 65279],
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* ASCII code-point ranges for the shorthand classes we lower. `\d`/`\w` are
|
|
29
|
+
* ASCII-only in the default (non-`u`) engine; `\s` maps to `SPACE_RANGES`.
|
|
30
|
+
*/
|
|
31
|
+
export function shorthandRanges(ch: 'd' | 'w' | 's'): Array<[number, number]> {
|
|
32
|
+
if (ch === 'd') return [[48, 57]]
|
|
33
|
+
if (ch === 's') return SPACE_RANGES
|
|
34
|
+
// Order matches the canonical `scannable-run` lowering (underscore last), so
|
|
35
|
+
// `ScanShape` output and generated code stay byte-identical. Membership is
|
|
36
|
+
// order-independent (ranges are iterated / the FirstSet union sorts), so the
|
|
37
|
+
// non-sorted order is purely representational — see PR #16 discussion.
|
|
38
|
+
return [[48, 57], [65, 90], [97, 122], [95, 95]]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `\uXXXX` at `body[i]` → its code point and the index past it, or null. */
|
|
42
|
+
export function readUnicodeEscape(body: string, i: number): { cp: number; next: number } | null {
|
|
43
|
+
if (body[i] !== '\\' || body[i + 1] !== 'u') return null
|
|
44
|
+
const hex = body.slice(i + 2, i + 6)
|
|
45
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return null
|
|
46
|
+
return { cp: Number.parseInt(hex, 16), next: i + 6 }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Regex syntax characters that cannot appear in a LITERAL fragment unescaped. */
|
|
50
|
+
const META = new Set('()[]{}*+?|^$.'.split(''))
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A regex fragment that is a plain run of literal characters → its code points,
|
|
54
|
+
* or null when the fragment contains any operator (so a caller can never mistake
|
|
55
|
+
* `a*` for the two-char literal `a*`). An empty fragment returns null, because
|
|
56
|
+
* every caller wants "at least one char to match".
|
|
57
|
+
*/
|
|
58
|
+
export function literalCodePoints(frag: string): number[] | null {
|
|
59
|
+
const out: number[] = []
|
|
60
|
+
let i = 0
|
|
61
|
+
while (i < frag.length) {
|
|
62
|
+
const ch = frag[i]!
|
|
63
|
+
if (ch === '\\') {
|
|
64
|
+
const e = frag[i + 1]
|
|
65
|
+
if (e === undefined) return null
|
|
66
|
+
out.push(e in CLASS_ESCAPES ? CLASS_ESCAPES[e]! : e.codePointAt(0)!)
|
|
67
|
+
i += 2
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
if (META.has(ch)) return null
|
|
71
|
+
out.push(ch.codePointAt(0)!)
|
|
72
|
+
i += 1
|
|
73
|
+
}
|
|
74
|
+
return out.length ? out : null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Index of the `]` that CLOSES the class opening at `body[0]`, honouring `\]`,
|
|
79
|
+
* or −1 if the class is unterminated. A leading `^` is negation, never a member;
|
|
80
|
+
* every other char up to the first unescaped `]` is. This is JS's own non-`u`
|
|
81
|
+
* reading, under which `[]` is the empty class — so in `[]]` the FIRST `]`
|
|
82
|
+
* closes and a literal `]` follows, and this returns 1, not 2.
|
|
83
|
+
*/
|
|
84
|
+
function classCloseIndex(body: string): number {
|
|
85
|
+
let i = 1
|
|
86
|
+
if (body[i] === '^') i++
|
|
87
|
+
while (i < body.length) {
|
|
88
|
+
const ch = body[i]
|
|
89
|
+
if (ch === '\\') { i += 2; continue }
|
|
90
|
+
if (ch === ']') return i
|
|
91
|
+
i++
|
|
92
|
+
}
|
|
93
|
+
return -1
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A single-character MATCHER fragment as a (possibly negated) range set: a
|
|
98
|
+
* bracketed class `[…]`/`[^…]`, a `\d`/`\w`/`\s` shorthand, or one literal char.
|
|
99
|
+
* Anything wider (a group, a multi-char literal, `.`) returns null.
|
|
100
|
+
*
|
|
101
|
+
* "Bracketed class" means the WHOLE fragment is ONE class. Testing only that it
|
|
102
|
+
* opens with `[` and ends with `]` is a different, weaker question, and
|
|
103
|
+
* `[ \t\n\r\f]*[\$(]` answers it while being a SEQUENCE — a whitespace run, then
|
|
104
|
+
* one of `$(`. Read as a single class its members become the garbage union of
|
|
105
|
+
* everything between the OUTER brackets, whitespace and `*` and `[` included, so
|
|
106
|
+
* scss's `\+(?=[ \t\n\r\f]*[\$(])` matched a `+` before a space; the shape oracle
|
|
107
|
+
* caught it at 203 positions of the scss corpus. Every caller here asks "is this
|
|
108
|
+
* ONE char matcher", so a fragment that is not gets null — declining costs a
|
|
109
|
+
* lowering or widens a first-set to `any()`, both of which only forgo a fast
|
|
110
|
+
* path, whereas accepting yields a wrong member set, which is a wrong scan or a
|
|
111
|
+
* wrong dispatch.
|
|
112
|
+
*/
|
|
113
|
+
export function parseClassOperand(body: string): { ranges: Array<[number, number]>; negated: boolean } | null {
|
|
114
|
+
if (body === '\\d' || body === '\\w' || body === '\\s') {
|
|
115
|
+
return { ranges: shorthandRanges(body[1] as 'd' | 'w' | 's'), negated: false }
|
|
116
|
+
}
|
|
117
|
+
if (body.length >= 2 && body[0] === '[') {
|
|
118
|
+
// The class must close at the LAST char; anything after it is a second token
|
|
119
|
+
// (a quantifier, another class, a literal) that this fragment cannot express.
|
|
120
|
+
if (classCloseIndex(body) !== body.length - 1) return null
|
|
121
|
+
let inner = body.slice(1, -1)
|
|
122
|
+
const negated = inner.startsWith('^')
|
|
123
|
+
if (negated) inner = inner.slice(1)
|
|
124
|
+
const ranges = parseClassRanges(inner)
|
|
125
|
+
return ranges ? { ranges, negated } : null
|
|
126
|
+
}
|
|
127
|
+
const cps = literalCodePoints(body)
|
|
128
|
+
if (cps && cps.length === 1) return { ranges: [[cps[0]!, cps[0]!]], negated: false }
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
type ClassAtom = { cp: number } | { set: Array<[number, number]> }
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Parse a regex char-class body (the chars BETWEEN `[` and `]`, negation `^`
|
|
136
|
+
* already stripped by the caller) to code-point ranges. `\d`/`\w`/`\s` expand to
|
|
137
|
+
* their ranges; `\uXXXX` and the fixed single-char escapes resolve to their code
|
|
138
|
+
* point; any other letter escape (`\D`, `\W`, `\S`, `\b`, …) returns null rather
|
|
139
|
+
* than being mis-read as a literal letter, so callers fall back to a safe
|
|
140
|
+
* over-approximation instead of a wrong set.
|
|
141
|
+
*/
|
|
142
|
+
export function parseClassRanges(body: string): Array<[number, number]> | null {
|
|
143
|
+
const ranges: Array<[number, number]> = []
|
|
144
|
+
let i = 0
|
|
145
|
+
const readAtom = (): ClassAtom | null => {
|
|
146
|
+
const ch = body[i]
|
|
147
|
+
if (ch === undefined) return null
|
|
148
|
+
if (ch === '\\') {
|
|
149
|
+
const uni = readUnicodeEscape(body, i)
|
|
150
|
+
if (uni) {
|
|
151
|
+
i = uni.next
|
|
152
|
+
return { cp: uni.cp }
|
|
153
|
+
}
|
|
154
|
+
const e = body[i + 1]
|
|
155
|
+
if (e === undefined) return null
|
|
156
|
+
i += 2
|
|
157
|
+
if (e in CLASS_ESCAPES) return { cp: CLASS_ESCAPES[e]! }
|
|
158
|
+
if (e === 'd' || e === 'w' || e === 's') return { set: shorthandRanges(e) }
|
|
159
|
+
// Any other letter escape is a class we can't safely lower (\D, \W, \S, …).
|
|
160
|
+
if ((e >= 'a' && e <= 'z') || (e >= 'A' && e <= 'Z')) return null
|
|
161
|
+
return { cp: e.codePointAt(0)! }
|
|
162
|
+
}
|
|
163
|
+
i += ch.length
|
|
164
|
+
return { cp: ch.codePointAt(0)! }
|
|
165
|
+
}
|
|
166
|
+
while (i < body.length) {
|
|
167
|
+
const lo = readAtom()
|
|
168
|
+
if (lo === null) return null
|
|
169
|
+
if ('set' in lo) {
|
|
170
|
+
ranges.push(...lo.set)
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
if (body[i] === '-' && body[i + 1] !== undefined && body[i + 1] !== ']') {
|
|
174
|
+
i += 1
|
|
175
|
+
const hi = readAtom()
|
|
176
|
+
if (hi === null || 'set' in hi) return null
|
|
177
|
+
ranges.push([lo.cp, hi.cp])
|
|
178
|
+
} else {
|
|
179
|
+
ranges.push([lo.cp, lo.cp])
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return ranges.length ? ranges : null
|
|
183
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-rolled first-set analysis for `regex()` terminals — the interpreter's
|
|
3
|
+
* replacement for the old `regexp-tree`-backed analyzer (~264 KB). A `regex()`
|
|
4
|
+
* terminal's first-set is used ONLY to let `choice()` do first-char dispatch
|
|
5
|
+
* (see `choice.ts`); it is never consulted for whether a match succeeds. So this
|
|
6
|
+
* analyzer only has to be SOUND as an OVER-approximation: every char the pattern
|
|
7
|
+
* can actually start with must be in the returned set. Any construct it can't
|
|
8
|
+
* pin down widens to `any()` (— "could start with anything" —), which only
|
|
9
|
+
* disables a dispatch fast path, never changes a parse. Under-approximating
|
|
10
|
+
* (dropping a real start char) WOULD be a bug: dispatch would skip a matching
|
|
11
|
+
* arm. The soundness fuzz test (`test/unit/regex-first-set.test.ts`) checks this
|
|
12
|
+
* against the real `RegExp` engine as an oracle.
|
|
13
|
+
*
|
|
14
|
+
* It parses the regex into a tiny AST (single-char matchers, sequence,
|
|
15
|
+
* alternation, repetition, zero-width) reusing the shared char-class primitives
|
|
16
|
+
* in `./classes.ts`, then walks the AST unioning first-sets through the nullable
|
|
17
|
+
* prefix — the same shape the `regexp-tree` walker used.
|
|
18
|
+
*
|
|
19
|
+
* Note: flags are not consulted (matching the prior `regexp-tree` seam, which
|
|
20
|
+
* parsed `/${source}/` with no flags), so an `/i` pattern's first-set is its
|
|
21
|
+
* literal-case set. Sound for dispatch because the interpreter's `/i` regex arms
|
|
22
|
+
* are compared case-sensitively at the same seam.
|
|
23
|
+
*/
|
|
24
|
+
import type { FirstSet } from '../types.ts'
|
|
25
|
+
import { any, empty, fromRange, union } from '../combinators/first-set.ts'
|
|
26
|
+
import { shorthandRanges, parseClassRanges } from './classes.ts'
|
|
27
|
+
|
|
28
|
+
// A minimal regex AST. Each node answers two questions: what first chars can a
|
|
29
|
+
// non-empty match begin with (`firstSet`), and can the node match zero chars
|
|
30
|
+
// (`nullable`). `any` is the over-approximation sentinel for anything we don't
|
|
31
|
+
// model precisely.
|
|
32
|
+
type Node =
|
|
33
|
+
| { t: 'char'; set: FirstSet; nl: boolean } // matches exactly one char from `set`
|
|
34
|
+
| { t: 'any' } // unknown 1-char-ish construct: could start with anything
|
|
35
|
+
| { t: 'empty' } // zero-width: anchors, lookaround, word boundary
|
|
36
|
+
| { t: 'seq'; parts: Node[] }
|
|
37
|
+
| { t: 'alt'; arms: Node[] }
|
|
38
|
+
| { t: 'rep'; node: Node; min: number } // quantified; min 0 ⇒ nullable
|
|
39
|
+
|
|
40
|
+
const ANY: Node = { t: 'any' }
|
|
41
|
+
const EMPTY: Node = { t: 'empty' }
|
|
42
|
+
|
|
43
|
+
function charNode(set: FirstSet, nl: boolean): Node {
|
|
44
|
+
return { t: 'char', set, nl }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function rangesToSet(ranges: Array<[number, number]>): FirstSet {
|
|
48
|
+
let fs: FirstSet = empty()
|
|
49
|
+
for (const [lo, hi] of ranges) fs = union(fs, fromRange(lo, hi))
|
|
50
|
+
return fs
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function rangesHaveNewline(ranges: Array<[number, number]>): boolean {
|
|
54
|
+
return ranges.some(([lo, hi]) => lo <= 0x0a && 0x0a <= hi)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Parser: recursive descent over a raw regex source (no surrounding delimiters).
|
|
59
|
+
// Throws `Bail` on a structure it can't parse (unbalanced group/class); the
|
|
60
|
+
// entry point catches it and degrades the WHOLE pattern to `any()`. A construct
|
|
61
|
+
// that parses but isn't modeled (a backreference, an exotic escape) becomes an
|
|
62
|
+
// `any` NODE instead — sound and more precise, since it only pollutes the
|
|
63
|
+
// first-set if reached in the nullable prefix.
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/** Thrown on a structure the parser can't handle (unbalanced group/class); the
|
|
67
|
+
* entry point catches it and degrades the whole pattern to `any()`. */
|
|
68
|
+
const BAIL = Symbol('regex-bail')
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse a raw regex `src` (no delimiters) into a `Node`, or throw `BAIL`.
|
|
72
|
+
* Closure over a single cursor `i` — recursive-descent: alt → seq → quantified
|
|
73
|
+
* → atom.
|
|
74
|
+
*/
|
|
75
|
+
function parseRegex(src: string): Node {
|
|
76
|
+
let i = 0
|
|
77
|
+
|
|
78
|
+
const parseAlt = (): Node => {
|
|
79
|
+
const arms: Node[] = [parseSeq()]
|
|
80
|
+
while (src[i] === '|') { i++; arms.push(parseSeq()) }
|
|
81
|
+
return arms.length === 1 ? arms[0]! : { t: 'alt', arms }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const parseSeq = (): Node => {
|
|
85
|
+
const parts: Node[] = []
|
|
86
|
+
while (i < src.length) {
|
|
87
|
+
const ch = src[i]!
|
|
88
|
+
if (ch === '|' || ch === ')') break
|
|
89
|
+
parts.push(parseQuantified())
|
|
90
|
+
}
|
|
91
|
+
if (parts.length === 0) return EMPTY
|
|
92
|
+
return parts.length === 1 ? parts[0]! : { t: 'seq', parts }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const parseQuantified = (): Node => {
|
|
96
|
+
const atom = parseAtom()
|
|
97
|
+
const ch = src[i]
|
|
98
|
+
let min: number | null = null
|
|
99
|
+
if (ch === '*') { min = 0; i++ }
|
|
100
|
+
else if (ch === '+') { min = 1; i++ }
|
|
101
|
+
else if (ch === '?') { min = 0; i++ }
|
|
102
|
+
else if (ch === '{') { min = parseBraceQuantifier() }
|
|
103
|
+
if (min === null) return atom
|
|
104
|
+
// Consume a lazy `?` on the quantifier — it changes greediness, not first-set.
|
|
105
|
+
if (src[i] === '?') i++
|
|
106
|
+
return { t: 'rep', node: atom, min }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Parse `{n}` / `{n,}` / `{n,m}` at `i` (on `{`), returning the minimum; null
|
|
110
|
+
// (leaving `{` unconsumed) if it isn't a well-formed brace quantifier, so the
|
|
111
|
+
// caller keeps the `{` as the literal atom it already parsed.
|
|
112
|
+
const parseBraceQuantifier = (): number | null => {
|
|
113
|
+
const close = src.indexOf('}', i)
|
|
114
|
+
if (close === -1) return null
|
|
115
|
+
const m = /^(\d+)(?:,(\d*))?$/.exec(src.slice(i + 1, close))
|
|
116
|
+
if (!m) return null
|
|
117
|
+
i = close + 1
|
|
118
|
+
return Number.parseInt(m[1]!, 10)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const parseAtom = (): Node => {
|
|
122
|
+
const ch = src[i]!
|
|
123
|
+
switch (ch) {
|
|
124
|
+
case '(': return parseGroup()
|
|
125
|
+
case '[': return parseClass()
|
|
126
|
+
case '.': i++; return charNode(any(), false) // `.` matches any char except line terminators
|
|
127
|
+
case '\\': return parseEscape()
|
|
128
|
+
case '^':
|
|
129
|
+
case '$': i++; return EMPTY // anchors are zero-width
|
|
130
|
+
case '{': i++; return charNode(fromRange(0x7b, 0x7b), false) // a `{` with nothing to quantify is a literal brace
|
|
131
|
+
default:
|
|
132
|
+
i += ch.length
|
|
133
|
+
return charNode(fromRange(ch.codePointAt(0)!, ch.codePointAt(0)!), ch === '\n')
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const parseGroup = (): Node => {
|
|
138
|
+
i++ // consume '('
|
|
139
|
+
// Group prefixes: (?:…) (?=…) (?!…) (?<=…) (?<!…) (?<name>…)
|
|
140
|
+
let lookaround = false
|
|
141
|
+
if (src[i] === '?') {
|
|
142
|
+
const c1 = src[i + 1]
|
|
143
|
+
if (c1 === ':') i += 2
|
|
144
|
+
else if (c1 === '=' || c1 === '!') { i += 2; lookaround = true }
|
|
145
|
+
else if (c1 === '<') {
|
|
146
|
+
const c2 = src[i + 2]
|
|
147
|
+
if (c2 === '=' || c2 === '!') { i += 3; lookaround = true } // lookbehind
|
|
148
|
+
else { // named group (?<name>…): skip to '>'
|
|
149
|
+
const gt = src.indexOf('>', i)
|
|
150
|
+
if (gt === -1) throw BAIL
|
|
151
|
+
i = gt + 1
|
|
152
|
+
}
|
|
153
|
+
} else throw BAIL // (? followed by something unexpected
|
|
154
|
+
}
|
|
155
|
+
const inner = parseAlt()
|
|
156
|
+
if (src[i] !== ')') throw BAIL
|
|
157
|
+
i++ // consume ')'
|
|
158
|
+
return lookaround ? EMPTY : inner // lookaround is zero-width
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const parseClass = (): Node => {
|
|
162
|
+
// Read `[ ... ]`, honoring `\]`; detect leading `^` negation.
|
|
163
|
+
let k = i + 1
|
|
164
|
+
const negated = src[k] === '^'
|
|
165
|
+
if (negated) k++
|
|
166
|
+
let body = ''
|
|
167
|
+
while (k < src.length && src[k] !== ']') {
|
|
168
|
+
if (src[k] === '\\') { body += src[k]! + (src[k + 1] ?? ''); k += 2 }
|
|
169
|
+
else { body += src[k]!; k++ }
|
|
170
|
+
}
|
|
171
|
+
if (src[k] !== ']') throw BAIL
|
|
172
|
+
i = k + 1
|
|
173
|
+
const ranges = parseClassRanges(body)
|
|
174
|
+
// A negated class can start with (almost) any char, so the first-set widens to
|
|
175
|
+
// `any`, but its newline capability is still cheap and useful to know:
|
|
176
|
+
// `[^,\r\n]*` is a workhorse "field until line end" regex and must not force
|
|
177
|
+
// line-tracking code to rescan every field.
|
|
178
|
+
if (negated) return charNode(any(), !ranges || !rangesHaveNewline(ranges))
|
|
179
|
+
if (!ranges) return charNode(any(), true) // unparseable class → widen
|
|
180
|
+
return charNode(rangesToSet(ranges), rangesHaveNewline(ranges))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const parseEscape = (): Node => {
|
|
184
|
+
const e = src[i + 1]
|
|
185
|
+
if (e === undefined) throw BAIL
|
|
186
|
+
// \uXXXX
|
|
187
|
+
if (e === 'u' && /^[0-9a-fA-F]{4}$/.test(src.slice(i + 2, i + 6))) {
|
|
188
|
+
const cp = Number.parseInt(src.slice(i + 2, i + 6), 16)
|
|
189
|
+
i += 6
|
|
190
|
+
return charNode(fromRange(cp, cp), cp === 0x0a)
|
|
191
|
+
}
|
|
192
|
+
// \xHH
|
|
193
|
+
if (e === 'x' && /^[0-9a-fA-F]{2}$/.test(src.slice(i + 2, i + 4))) {
|
|
194
|
+
const cp = Number.parseInt(src.slice(i + 2, i + 4), 16)
|
|
195
|
+
i += 4
|
|
196
|
+
return charNode(fromRange(cp, cp), cp === 0x0a)
|
|
197
|
+
}
|
|
198
|
+
i += 2
|
|
199
|
+
switch (e) {
|
|
200
|
+
case 'd': return charNode(rangesToSet(shorthandRanges('d')), false)
|
|
201
|
+
case 'w': return charNode(rangesToSet(shorthandRanges('w')), false)
|
|
202
|
+
case 's': return charNode(rangesToSet(shorthandRanges('s')), true)
|
|
203
|
+
case 'D': case 'W': case 'S': return charNode(any(), true) // negated shorthand → widen
|
|
204
|
+
case 'b': case 'B': return EMPTY // word boundary: zero-width
|
|
205
|
+
case 'n': return charNode(fromRange(0x0a, 0x0a), true)
|
|
206
|
+
case 'r': return charNode(fromRange(0x0d, 0x0d), false)
|
|
207
|
+
case 't': return charNode(fromRange(0x09, 0x09), false)
|
|
208
|
+
case 'f': return charNode(fromRange(0x0c, 0x0c), false)
|
|
209
|
+
case 'v': return charNode(fromRange(0x0b, 0x0b), false)
|
|
210
|
+
case '0': return charNode(fromRange(0, 0), false)
|
|
211
|
+
default:
|
|
212
|
+
// A digit escape is a backreference we don't resolve → widen.
|
|
213
|
+
if (e >= '1' && e <= '9') return ANY
|
|
214
|
+
// Any other escape is a literal char (escaped metachar, `\/`, etc.).
|
|
215
|
+
return charNode(fromRange(e.codePointAt(0)!, e.codePointAt(0)!), e === '\n')
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const node = parseAlt()
|
|
220
|
+
if (i < src.length) throw BAIL // leftover (e.g. an unbalanced `)`)
|
|
221
|
+
return node
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Walkers over the AST.
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
/** Can `n` succeed consuming zero characters? Err toward `true` when unsure —
|
|
229
|
+
* over-estimating nullability only widens the (over-approximated) first-set. */
|
|
230
|
+
function nullable(n: Node): boolean {
|
|
231
|
+
switch (n.t) {
|
|
232
|
+
case 'char': return false
|
|
233
|
+
case 'any': return true
|
|
234
|
+
case 'empty': return true
|
|
235
|
+
case 'seq': return n.parts.every(nullable)
|
|
236
|
+
case 'alt': return n.arms.some(nullable)
|
|
237
|
+
case 'rep': return n.min === 0 || nullable(n.node)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** First chars a non-empty match of `n` can begin with (over-approximated). */
|
|
242
|
+
function firstSet(n: Node): FirstSet {
|
|
243
|
+
switch (n.t) {
|
|
244
|
+
case 'char': return n.set
|
|
245
|
+
case 'any': return any()
|
|
246
|
+
case 'empty': return empty()
|
|
247
|
+
case 'rep': return firstSet(n.node)
|
|
248
|
+
case 'alt': {
|
|
249
|
+
let fs: FirstSet = empty()
|
|
250
|
+
for (const arm of n.arms) fs = union(fs, firstSet(arm))
|
|
251
|
+
return fs
|
|
252
|
+
}
|
|
253
|
+
case 'seq': {
|
|
254
|
+
// Union each part's first-set through the nullable prefix: a leading
|
|
255
|
+
// nullable part lets a LATER part's first chars start the sequence.
|
|
256
|
+
let fs: FirstSet = empty()
|
|
257
|
+
for (const part of n.parts) {
|
|
258
|
+
fs = union(fs, firstSet(part))
|
|
259
|
+
if (!nullable(part)) break
|
|
260
|
+
}
|
|
261
|
+
return fs
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Whether a successful match can consume a newline anywhere. This is tracked
|
|
267
|
+
* separately from `firstSet`: dispatch only cares about the first consumed char,
|
|
268
|
+
* while optional line tracking cares whether the matched span needs scanning. */
|
|
269
|
+
function canConsumeNewline(n: Node): boolean {
|
|
270
|
+
switch (n.t) {
|
|
271
|
+
case 'char': return n.nl
|
|
272
|
+
case 'any': return true
|
|
273
|
+
case 'empty': return false
|
|
274
|
+
case 'rep': return canConsumeNewline(n.node)
|
|
275
|
+
case 'alt': return n.arms.some(canConsumeNewline)
|
|
276
|
+
case 'seq': return n.parts.some(canConsumeNewline)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Compute a regex terminal's first-set and newline-start flag from its SOURCE
|
|
282
|
+
* (no delimiters, no flags). Over-approximates: an unparseable pattern degrades
|
|
283
|
+
* to `{ any, canMatchNewline: true }` — the same conservative value the prior
|
|
284
|
+
* `regexp-tree` seam returned on a parse failure.
|
|
285
|
+
*/
|
|
286
|
+
/**
|
|
287
|
+
* Can `source` match ZERO characters?
|
|
288
|
+
*
|
|
289
|
+
* Structural, over the same AST `firstSetFromRegex` uses — which already models
|
|
290
|
+
* every zero-width construct as `EMPTY`: anchors (`^`/`$`), lookaround
|
|
291
|
+
* (`(?=…)`/`(?!…)`/`(?<=…)`/`(?<!…)`) and word boundaries. That is what makes
|
|
292
|
+
* this usable as a proof of consumption: a pattern is zero-width exactly when
|
|
293
|
+
* the AST says it is nullable, so a lookaround BURIED inside an otherwise
|
|
294
|
+
* mandatory pattern no longer taints the whole answer.
|
|
295
|
+
*
|
|
296
|
+
* Errs toward `true` (nullable) — an unparseable pattern is assumed able to
|
|
297
|
+
* match empty, so callers that delete code on a `false` keep their proof.
|
|
298
|
+
*/
|
|
299
|
+
export function regexCanMatchEmpty(source: string): boolean {
|
|
300
|
+
try { return nullable(parseRegex(source)) } catch { return true }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function firstSetFromRegex(source: string): { firstSet: FirstSet; canMatchNewline: boolean } {
|
|
304
|
+
let ast: Node
|
|
305
|
+
try {
|
|
306
|
+
ast = parseRegex(source)
|
|
307
|
+
} catch {
|
|
308
|
+
return { firstSet: any(), canMatchNewline: true }
|
|
309
|
+
}
|
|
310
|
+
// A NULLABLE pattern (can match zero chars) must widen to `any()`: as a
|
|
311
|
+
// `choice()` arm it can succeed with an empty match at ANY position, so a
|
|
312
|
+
// narrow first-set would let first-char dispatch wrongly skip it. Only a
|
|
313
|
+
// pattern that must consume ≥1 char has a meaningful first-set.
|
|
314
|
+
if (nullable(ast)) return { firstSet: any(), canMatchNewline: canConsumeNewline(ast) }
|
|
315
|
+
return { firstSet: firstSet(ast), canMatchNewline: canConsumeNewline(ast) }
|
|
316
|
+
}
|
package/src/run/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `parseman/run` — the minimal entry for EXECUTING an already-compiled grammar.
|
|
3
|
+
*
|
|
4
|
+
* The macro removes the combinators, the compiler and the codegen from your
|
|
5
|
+
* bundle, but not the driver: running a compiled grammar is still a `run()` call,
|
|
6
|
+
* so a package that SHIPS a parser keeps a runtime import of parseman. Importing
|
|
7
|
+
* that driver from the main entry drags the whole library along — the combinator
|
|
8
|
+
* set, `compile()`, the first-set analysis, the CST builders — none of which a
|
|
9
|
+
* compiled parser touches.
|
|
10
|
+
*
|
|
11
|
+
* This entry exists so it doesn't have to. `run()`'s complete module closure is
|
|
12
|
+
* intentionally tiny (this, `functional/run.ts`, recovery helpers, CST capture /
|
|
13
|
+
* trivia views, host-mode checks, and the line-index helper) — a fraction of the
|
|
14
|
+
* main entry.
|
|
15
|
+
* `test/unit/run-entry-closure.test.ts` asserts that stays true, so a future
|
|
16
|
+
* import into `functional/run.ts` that widens the closure fails the suite rather
|
|
17
|
+
* than quietly re-inflating everyone's bundle.
|
|
18
|
+
*
|
|
19
|
+
* Use the main entry when you BUILD grammars; use this one when you only run
|
|
20
|
+
* output the macro (or `compile()`) already produced.
|
|
21
|
+
*
|
|
22
|
+
* import { run } from 'parseman/run'
|
|
23
|
+
* import { myGrammar } from './grammar.js' // macro-compiled
|
|
24
|
+
*
|
|
25
|
+
* const result = run(myGrammar, source, { trivia })
|
|
26
|
+
*/
|
|
27
|
+
export { buildRootTriviaIndex } from '../cst/trivia-entries.ts'
|
|
28
|
+
export { run } from '../functional/run.ts'
|
|
29
|
+
export type { RootTriviaCapture, Runnable, RunOptions, RunResult } from '../functional/run.ts'
|
|
30
|
+
export type { RootTriviaGap, RootTriviaIndex, TriviaEntriesView } from '../cst/trivia-entries.ts'
|
|
31
|
+
export type { ParseContext, ParseError, ParseResult } from '../types.ts'
|