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,63 @@
|
|
|
1
|
+
import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
|
|
2
|
+
import { any, matchesEmpty } from './first-set.ts'
|
|
3
|
+
import { saveLookaheadMark, rollbackLookahead } from './trivia-skip.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Positive lookahead. Succeeds (consuming nothing) when `combinator` matches at
|
|
7
|
+
* this position; fails when it doesn't. The mirror of `not()`.
|
|
8
|
+
*
|
|
9
|
+
* // "a mixin reference, not a class selector": require the punctuation ahead,
|
|
10
|
+
* // then let the real production consume it.
|
|
11
|
+
* const mixinRef = sequence(peek(regex(/[.#]/)), mixinCall)
|
|
12
|
+
*
|
|
13
|
+
* WHY THIS EXISTS AS A PRIMITIVE (not `not(not(X))`):
|
|
14
|
+
*
|
|
15
|
+
* `not(not(X))` is behaviourally a positive lookahead, but `not()` reports
|
|
16
|
+
* `firstSet: any()` — it cannot know what it forbids — so `not(not(X))` reports
|
|
17
|
+
* `any()` too. An arm leading with it therefore POISONS its choice's first-char
|
|
18
|
+
* dispatch (`analyzeGating` flags it as the `double-not` anti-pattern), and among
|
|
19
|
+
* sibling arms that share a first char the hand-rolled gate miscompiles.
|
|
20
|
+
*
|
|
21
|
+
* `peek(X)` carries X's first-set instead, so a leading `peek()` GATES its arm:
|
|
22
|
+
* `choice(sequence(peek(regex(/[.#]/)), …), …)` still emits O(1) dispatch.
|
|
23
|
+
*
|
|
24
|
+
* The first-set is exact only when X is NON-NULLABLE. A nullable `peek(X)`
|
|
25
|
+
* succeeds on the empty string, so it constrains no first character at all — it
|
|
26
|
+
* reports `any()`, the sound over-approximation. (Same rule the sequence
|
|
27
|
+
* first-set uses; see `isPositiveLookahead` in `first-set.ts`.)
|
|
28
|
+
*/
|
|
29
|
+
export function peek(combinator: Combinator<unknown>): Combinator<null> {
|
|
30
|
+
const meta: ParserMeta = {
|
|
31
|
+
// A nullable body imposes no first-char constraint — `any()`, not its set.
|
|
32
|
+
firstSet: matchesEmpty(combinator) ? any() : combinator._meta.firstSet,
|
|
33
|
+
canMatchNewline: false,
|
|
34
|
+
isTrivia: false,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
_tag: 'peek',
|
|
39
|
+
_meta: meta,
|
|
40
|
+
_def: { tag: 'peek', parser: combinator },
|
|
41
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<null> {
|
|
42
|
+
// Zero-width on BOTH outcomes, so the probe must leave NO observable trace:
|
|
43
|
+
// whatever the inner attempt captured (CST leaves/rawChildren/per-node
|
|
44
|
+
// trivia/fields), recovered (`_errors`), or committed to the GLOBAL
|
|
45
|
+
// `_triviaLog` is rolled back either way.
|
|
46
|
+
//
|
|
47
|
+
// `rollbackTrivia`, not `rollbackCstCapture`: the latter's mark omits
|
|
48
|
+
// `_triviaLog`, which is a SEPARATE sink `scanTrivia().commit()` writes when
|
|
49
|
+
// the body skips ambient trivia between terms. Missing it duplicates every
|
|
50
|
+
// probed trivia span once the region is parsed for real — `_triviaLog` has no
|
|
51
|
+
// dedup anywhere (`triviaEntries()` is a positional view over the flat array),
|
|
52
|
+
// so the duplicate reaches output. Same failure mode as the one recorded in
|
|
53
|
+
// `test/parity/trivia-log-regression.test.ts`. The compiled `peek` emits its
|
|
54
|
+
// body under a non-capturing ctx and so never writes these sinks at all; this
|
|
55
|
+
// is what keeps the two engines at parity.
|
|
56
|
+
const mark = saveLookaheadMark(ctx)
|
|
57
|
+
const result = combinator.parse(input, pos, ctx)
|
|
58
|
+
rollbackLookahead(ctx, mark)
|
|
59
|
+
if (result.ok) return { ok: true, value: null, span: { start: pos, end: pos } }
|
|
60
|
+
return { ok: false, expected: [`peek(${combinator._tag})`], span: { start: pos, end: pos } }
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ParseContext, ParseFail } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a ParseFail and, if a completions probe is active, records it.
|
|
5
|
+
*
|
|
6
|
+
* Rules:
|
|
7
|
+
* - Only failures at or before the probe's target offset are eligible.
|
|
8
|
+
* - A deeper failure (higher pos) replaces the current best.
|
|
9
|
+
* - A tie (same pos) merges expected arrays — this is what makes choice arms
|
|
10
|
+
* aggregate their alternatives rather than each overwriting the last.
|
|
11
|
+
* - Shallower failures are ignored.
|
|
12
|
+
*/
|
|
13
|
+
export function failAt(ctx: ParseContext, expected: string[], pos: number): ParseFail {
|
|
14
|
+
const r: ParseFail = { ok: false, expected, span: { start: pos, end: pos } }
|
|
15
|
+
const probe = ctx._probe
|
|
16
|
+
if (probe !== undefined && pos <= probe.offset) {
|
|
17
|
+
const best = probe.best
|
|
18
|
+
if (best === null || pos > best.span.start) {
|
|
19
|
+
probe.best = r
|
|
20
|
+
} else if (pos === best.span.start) {
|
|
21
|
+
probe.best = { ...best, expected: [...best.expected, ...expected] }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return r
|
|
25
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
|
|
2
|
+
import { any } from './first-set.ts'
|
|
3
|
+
import { hasOwnTriviaBoundary } from './trivia-boundary.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a forward-declared parser slot for mutually recursive grammars.
|
|
7
|
+
*
|
|
8
|
+
* Because JS evaluates arguments eagerly, you can't reference a variable
|
|
9
|
+
* before it's declared. ref() creates a placeholder you fill in later:
|
|
10
|
+
*
|
|
11
|
+
* const value = ref<JSONValue>()
|
|
12
|
+
* const array = transform(sequence(literal('['), sepBy(value, literal(',')), literal(']')), ...)
|
|
13
|
+
* const object = transform(sequence(literal('{'), sepBy(pair, literal(',')), literal('}')), ...)
|
|
14
|
+
* value.define(choice(object, array, string, number, bool, nullVal))
|
|
15
|
+
*
|
|
16
|
+
* Unlike lazy(() => x), you use the ref directly — no wrapping at each call site.
|
|
17
|
+
*/
|
|
18
|
+
export function ref<T>(): Combinator<T> & { define(p: Combinator<T>): void } {
|
|
19
|
+
let resolved: Combinator<T> | null = null
|
|
20
|
+
/** `hasOwnTriviaBoundary(resolved)`, resolved at `define()`. See `parse`. */
|
|
21
|
+
let ownBoundary = false
|
|
22
|
+
|
|
23
|
+
const meta: ParserMeta = {
|
|
24
|
+
firstSet: any(),
|
|
25
|
+
canMatchNewline: true,
|
|
26
|
+
isTrivia: false,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const slot = {
|
|
30
|
+
_tag: 'lazy' as const,
|
|
31
|
+
_meta: meta,
|
|
32
|
+
_def: {
|
|
33
|
+
tag: 'lazy' as const,
|
|
34
|
+
thunk: () => {
|
|
35
|
+
if (!resolved) throw new Error('ref<T>() used before .define() was called')
|
|
36
|
+
return resolved as Combinator<unknown>
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<T> {
|
|
40
|
+
if (!resolved) throw new Error('ref<T>() used before .define() was called')
|
|
41
|
+
// A RULE CARRIES ITS OWN AMBIENT TRIVIA — see `rules()` in parser.ts, which
|
|
42
|
+
// stamps `grammarTrivia` on EVERY rule precisely so that entering any one of
|
|
43
|
+
// them installs it. Reading it only at the parse ENTRY (grammar.ts) made the
|
|
44
|
+
// interpreter scope trivia DYNAMICALLY: a rule referenced from underneath a
|
|
45
|
+
// `noTrivia(...)` ran without trivia, so its meaning depended on its caller.
|
|
46
|
+
// Codegen never had that behaviour — it binds each rule's trivia scanner at
|
|
47
|
+
// COMPILE time (the emitted `_tf0(input, cur, ctx, 1)` before every sequence
|
|
48
|
+
// boundary), so a `g.Foo` reference always runs under Foo's own scope.
|
|
49
|
+
//
|
|
50
|
+
// The two engines therefore parsed DIFFERENT LANGUAGES, and jess's Less
|
|
51
|
+
// grammar sits exactly on the seam: `StandardDeclaration` wraps its value in
|
|
52
|
+
// `noTrivia(...)` and the `!important` tail lives in the referenced rule
|
|
53
|
+
// `ValueListWithPriority`, so the interpreter and both table drivers could not
|
|
54
|
+
// cross the space in `color: red !important`. That one construct truncated a
|
|
55
|
+
// 107 KB stylesheet at 68.5% while reporting `ok: true` with no errors.
|
|
56
|
+
// Codegen shipped through 0.44-0.46 and was right; 0.47 made the table the
|
|
57
|
+
// shipping engine and the divergence became the product.
|
|
58
|
+
//
|
|
59
|
+
// ONLY WHERE THE SCOPE WAS CLEARED, not wherever it merely differs. The
|
|
60
|
+
// broader form (`ctx.trivia !== gt`) also OVERRIDES a caller that set a
|
|
61
|
+
// different live trivia, and that measurably broke recognition: over jess's
|
|
62
|
+
// four corpora it regressed `@less/test-data`'s `selectors.less` from 3791
|
|
63
|
+
// bytes to 1784. A caller running under its own live trivia is making a
|
|
64
|
+
// deliberate choice; a caller that CLEARED it is the `noTrivia(...)` case this
|
|
65
|
+
// fixes. Restricting to the cleared case keeps every repair and drops that
|
|
66
|
+
// regression.
|
|
67
|
+
//
|
|
68
|
+
// ONLY WHERE THE RULE HAS A BOUNDARY TO REPAIR. `ownBoundary` is resolved
|
|
69
|
+
// once in `define()` (see `hasOwnTriviaBoundary`): a rule whose body is a
|
|
70
|
+
// bare ALTERNATION, a dispatch, or a single terminal has no position of its
|
|
71
|
+
// own at which ambient trivia is consulted, so re-establishing the scope
|
|
72
|
+
// there cannot repair anything — it can only leak past the arms into
|
|
73
|
+
// whatever they delegate to.
|
|
74
|
+
//
|
|
75
|
+
// That leak is what jess's SCSS value grammar hit. `ValueTerm` clears, and
|
|
76
|
+
// `MathUnary` — `choice(noTrivia(…), noTrivia(…), g.ValueAtom)` — restored
|
|
77
|
+
// for a body that never uses it, handing its third arm a live scope.
|
|
78
|
+
// `ValueAtom` → `KeywordOrInterpolatedValue`, whose `many()` concatenates
|
|
79
|
+
// identifier chunks, then skipped the space between its terms:
|
|
80
|
+
// `gen-workload.scss` stopped at byte 218 of 287543, and `a{b: c d}` silently
|
|
81
|
+
// produced the ONE keyword `bc` with `ok: true` and no errors.
|
|
82
|
+
//
|
|
83
|
+
// Hot path is one boolean check.
|
|
84
|
+
const gt = meta.grammarTrivia
|
|
85
|
+
if (gt !== undefined && ownBoundary && ctx.trivia === undefined) {
|
|
86
|
+
const savedLabels = ctx.triviaKindLabels
|
|
87
|
+
ctx.trivia = gt
|
|
88
|
+
ctx.triviaKindLabels = gt._meta.triviaKindLabels
|
|
89
|
+
try { return resolved.parse(input, pos, ctx) }
|
|
90
|
+
finally { ctx.trivia = undefined; ctx.triviaKindLabels = savedLabels }
|
|
91
|
+
}
|
|
92
|
+
return resolved.parse(input, pos, ctx)
|
|
93
|
+
},
|
|
94
|
+
define(p: Combinator<T>): void {
|
|
95
|
+
if (resolved) throw new Error('ref<T>() already defined')
|
|
96
|
+
resolved = p
|
|
97
|
+
ownBoundary = hasOwnTriviaBoundary(p as Combinator<unknown>)
|
|
98
|
+
meta.firstSet = p._meta.firstSet
|
|
99
|
+
meta.canMatchNewline = p._meta.canMatchNewline
|
|
100
|
+
meta.isTrivia = p._meta.isTrivia
|
|
101
|
+
if (p._meta.triviaKindLabels !== undefined) meta.triviaKindLabels = p._meta.triviaKindLabels
|
|
102
|
+
else delete meta.triviaKindLabels
|
|
103
|
+
if (p._meta.disjoint !== undefined) meta.disjoint = p._meta.disjoint
|
|
104
|
+
else delete meta.disjoint
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return slot
|
|
109
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { Combinator, ParseContext, ParseResult, ParserMeta, FirstSet } from '../types.ts'
|
|
2
|
+
import { any } from './first-set.ts'
|
|
3
|
+
import { failAt } from './probe.ts'
|
|
4
|
+
import { pushCstLeaf, cstCaptureActive } from '../cst/capture-buffer.ts'
|
|
5
|
+
import { recordLineRangeFromContext } from '../line-index.ts'
|
|
6
|
+
import { shorthandRanges, parseClassRanges } from '../regex/classes.ts'
|
|
7
|
+
import { firstSetFromRegex } from '../regex/first-set.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A regex terminal's first-set (for choice-dispatch fast paths) is derived by
|
|
11
|
+
* `firstSetFromRegex` in `../regex/first-set.ts` — a small, dependency-free
|
|
12
|
+
* hand-rolled regex parser, imported DIRECTLY and unconditionally.
|
|
13
|
+
*
|
|
14
|
+
* ── WHY NOT INJECTED ────────────────────────────────────────────────────────────
|
|
15
|
+
* It used to be registered at run time by the library entry (`index.ts`) via a
|
|
16
|
+
* `registerRegexAnalyzer` seam, so that a bundle holding `regex()` without the
|
|
17
|
+
* library entry never pulled the analyzer in. That saved ~3 KB and cost a shipped
|
|
18
|
+
* export: `parseman/table` is its OWN module graph, so its private copy of this
|
|
19
|
+
* module never saw the registration, `regex()` there returned the permissive
|
|
20
|
+
* `any()` first-set, and `classifiedTrivia()` — which REQUIRES a concrete finite
|
|
21
|
+
* first set per arm — rejected every arm of every table-lowered grammar
|
|
22
|
+
* (`buildTrivia` in `../table/program.ts`). The failure was invisible from source,
|
|
23
|
+
* where `src/index.ts` is always in the graph.
|
|
24
|
+
*
|
|
25
|
+
* A mutable module-global that a *different* entry has to remember to write makes
|
|
26
|
+
* `regex()`'s result depend on which bundle it landed in. Deriving the first set
|
|
27
|
+
* intrinsically makes every module graph agree by construction. The analyzer adds
|
|
28
|
+
* no new leaf modules — it imports only `./first-set.ts` and `../regex/classes.ts`,
|
|
29
|
+
* both already reachable from here — so the cost is the analyzer's own bytes.
|
|
30
|
+
*/
|
|
31
|
+
const SCAN_BAIL_AT = 64
|
|
32
|
+
type ShortScanner = (input: string, pos: number) => number | null | undefined
|
|
33
|
+
|
|
34
|
+
function inRanges(cp: number, ranges: Array<[number, number]>): boolean {
|
|
35
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
36
|
+
const [lo, hi] = ranges[i]!
|
|
37
|
+
if (cp >= lo && cp <= hi) return true
|
|
38
|
+
}
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The positive char-class body (chars between `[` and `]`) as ranges for the
|
|
44
|
+
* short-scan fast path, or null if it can't be lowered — including a NEGATED
|
|
45
|
+
* class (`[^…]`), which the scan can't express. The class parsing itself is the
|
|
46
|
+
* shared `parseClassRanges` (see `../regex/classes.ts`).
|
|
47
|
+
*/
|
|
48
|
+
function readClassRanges(body: string): Array<[number, number]> | null {
|
|
49
|
+
if (body.startsWith('^')) return null
|
|
50
|
+
return parseClassRanges(body)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function shortScanner(source: string, flags: string): ShortScanner | null {
|
|
54
|
+
if (/[imsuvy]/.test(flags)) return null
|
|
55
|
+
let ranges: Array<[number, number]> | null = null
|
|
56
|
+
let quant = ''
|
|
57
|
+
if (source[0] === '[') {
|
|
58
|
+
let end = 1
|
|
59
|
+
while (end < source.length && source[end] !== ']') {
|
|
60
|
+
if (source[end] === '\\') end += 2
|
|
61
|
+
else end++
|
|
62
|
+
}
|
|
63
|
+
if (source[end] !== ']') return null
|
|
64
|
+
ranges = readClassRanges(source.slice(1, end))
|
|
65
|
+
quant = source.slice(end + 1)
|
|
66
|
+
} else if (source[0] === '\\' && (source[1] === 'd' || source[1] === 'w' || source[1] === 's')) {
|
|
67
|
+
ranges = shorthandRanges(source[1])
|
|
68
|
+
quant = source.slice(2)
|
|
69
|
+
}
|
|
70
|
+
if (!ranges || (quant !== '+' && quant !== '*')) return null
|
|
71
|
+
const minOne = quant === '+'
|
|
72
|
+
return (input, pos) => {
|
|
73
|
+
let end = pos
|
|
74
|
+
while (end < input.length && inRanges(input.charCodeAt(end), ranges)) {
|
|
75
|
+
end++
|
|
76
|
+
if (end - pos >= SCAN_BAIL_AT) return undefined
|
|
77
|
+
}
|
|
78
|
+
return minOne && end === pos ? null : end
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* ASCII-case-fold a first-set: for every ASCII letter reachable as a leading char,
|
|
84
|
+
* also admit its opposite-case twin (A–Z ↔ a–z). Non-letter code points and `any`/
|
|
85
|
+
* `empty` are unchanged. Only widens the set (sound superset) — used to correct a
|
|
86
|
+
* case-insensitive regex's flag-agnostic first-set for first-char dispatch. Unicode
|
|
87
|
+
* case-folding beyond ASCII is deliberately NOT applied (parseman first-sets are
|
|
88
|
+
* ASCII-BMP for dispatch); a non-ASCII letter keeps its own code point only, which
|
|
89
|
+
* stays sound (the interpreter's real `/i` match still runs).
|
|
90
|
+
*/
|
|
91
|
+
function asciiCaseFold(fs: FirstSet): FirstSet {
|
|
92
|
+
if (fs.kind !== 'ranges') return fs
|
|
93
|
+
const ranges: { lo: number; hi: number }[] = fs.ranges.map(r => ({ lo: r.lo, hi: r.hi }))
|
|
94
|
+
const add = (lo: number, hi: number): void => { if (lo <= hi) ranges.push({ lo, hi }) }
|
|
95
|
+
for (const r of fs.ranges) {
|
|
96
|
+
// Uppercase portion [A–Z] → lowercase twin (+32).
|
|
97
|
+
const uLo = Math.max(r.lo, 65), uHi = Math.min(r.hi, 90)
|
|
98
|
+
add(uLo + 32, uHi + 32)
|
|
99
|
+
// Lowercase portion [a–z] → uppercase twin (−32).
|
|
100
|
+
const lLo = Math.max(r.lo, 97), lHi = Math.min(r.hi, 122)
|
|
101
|
+
add(lLo - 32, lHi - 32)
|
|
102
|
+
}
|
|
103
|
+
// Normalize: sort + coalesce overlapping/adjacent ranges.
|
|
104
|
+
ranges.sort((a, b) => a.lo - b.lo)
|
|
105
|
+
const merged: { lo: number; hi: number }[] = []
|
|
106
|
+
for (const r of ranges) {
|
|
107
|
+
const last = merged[merged.length - 1]
|
|
108
|
+
if (last && r.lo <= last.hi + 1) last.hi = Math.max(last.hi, r.hi)
|
|
109
|
+
else merged.push({ lo: r.lo, hi: r.hi })
|
|
110
|
+
}
|
|
111
|
+
return { kind: 'ranges', ranges: merged }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function regex(pattern: string | RegExp, flags = ''): Combinator<string> {
|
|
115
|
+
const source = typeof pattern === 'string' ? pattern : pattern.source
|
|
116
|
+
const resolvedFlags = typeof pattern === 'string' ? flags : pattern.flags
|
|
117
|
+
|
|
118
|
+
const anchored = new RegExp(source, 'y' + resolvedFlags.replace(/[gy]/g, ''))
|
|
119
|
+
const scan = shortScanner(source, resolvedFlags)
|
|
120
|
+
|
|
121
|
+
const raw = firstSetFromRegex(source)
|
|
122
|
+
// The first-set analyzer is flag-agnostic, so for a CASE-INSENSITIVE pattern it
|
|
123
|
+
// returns only the literal-case leading chars — e.g. `/red|blue/i` → `{r,b}`, NOT
|
|
124
|
+
// `{r,R,b,B}`. Using that narrow set for first-char DISPATCH would false-EXCLUDE the
|
|
125
|
+
// opposite-case input (`ReD` gated out of a `/(?:red|…)/i` arm), so widen it under
|
|
126
|
+
// `i`. The widening is FLAG-AWARE because `i` alone and `i`+`u` fold different sets:
|
|
127
|
+
// - `/i` WITHOUT `u`: ECMAScript case-insensitive matching folds ONLY the ASCII
|
|
128
|
+
// Basic-Latin case pairs (`a`↔`A` … `z`↔`Z`) — a plain `/[a-z]/i` does NOT match
|
|
129
|
+
// `ſ`(U+017F) or `K`(U+212A). So `asciiCaseFold` is a SOUND, tight superset, and
|
|
130
|
+
// it preserves at-keyword dispatch (`/@media…/i` still gates on `@`).
|
|
131
|
+
// - `/ui` or `/iv` (Unicode mode — the `u` OR the ES2024 `v` Unicode-sets flag):
|
|
132
|
+
// matching uses Unicode *simple case folding*, so `/[a-z]/ui` (and `/[a-z]/iv`)
|
|
133
|
+
// ALSO match `ſ`→s and `K`→k. ASCII-folding can't enumerate those astral/BMP case
|
|
134
|
+
// pairs, so gating on the ASCII set would false-exclude them. Fall back to `any()`
|
|
135
|
+
// (always-try) — sound, and only forfeits gating for the rare Unicode-mode `/i`
|
|
136
|
+
// recognizer, never the ASCII-only `/i` at-keywords the fix targets.
|
|
137
|
+
const firstSet = !resolvedFlags.includes('i')
|
|
138
|
+
? raw.firstSet
|
|
139
|
+
: (resolvedFlags.includes('u') || resolvedFlags.includes('v'))
|
|
140
|
+
? any()
|
|
141
|
+
: asciiCaseFold(raw.firstSet)
|
|
142
|
+
const canMatchNewline = raw.canMatchNewline
|
|
143
|
+
const meta: ParserMeta = { firstSet, canMatchNewline, isTrivia: false }
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
_tag: 'regex',
|
|
147
|
+
_meta: meta,
|
|
148
|
+
_def: { tag: 'regex', source, flags: resolvedFlags },
|
|
149
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<string> {
|
|
150
|
+
const scanEnd = scan?.(input, pos)
|
|
151
|
+
if (scanEnd !== undefined) {
|
|
152
|
+
if (scanEnd === null) return failAt(ctx, [`/${source}/`], pos)
|
|
153
|
+
const value = input.slice(pos, scanEnd)
|
|
154
|
+
const span = { start: pos, end: scanEnd }
|
|
155
|
+
if (canMatchNewline && ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, scanEnd)
|
|
156
|
+
if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value, span })
|
|
157
|
+
return { ok: true, value, span }
|
|
158
|
+
}
|
|
159
|
+
anchored.lastIndex = pos
|
|
160
|
+
const m = anchored.exec(input)
|
|
161
|
+
if (m === null) {
|
|
162
|
+
return failAt(ctx, [`/${source}/`], pos)
|
|
163
|
+
}
|
|
164
|
+
const span = { start: pos, end: pos + m[0]!.length }
|
|
165
|
+
if (canMatchNewline && ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, span.end)
|
|
166
|
+
if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value: m[0]!, span })
|
|
167
|
+
return { ok: true, value: m[0]!, span }
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
}
|