parseman 0.59.2 → 0.60.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/dist/analysis/commitment.d.ts +1 -1
- package/dist/analysis/diagnostics.js +877 -191
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +877 -191
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/completions.d.ts +3 -3
- package/dist/combinators/completions.d.ts.map +1 -1
- package/dist/combinators/dispatch.d.ts +13 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts +35 -2
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/gate.d.ts +1 -1
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/trivia-boundary.d.ts +11 -0
- package/dist/combinators/trivia-boundary.d.ts.map +1 -1
- package/dist/combinators/trivia-skip.d.ts +1 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/compiler/compile-linkable-table.d.ts +2 -3
- package/dist/compiler/compile-linkable-table.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +2 -0
- package/dist/compiler/token-alphabet.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +65 -53
- package/dist/compiler/token-capability.js.map +2 -2
- package/dist/cst/host-mode.d.ts +1 -1
- package/dist/cst/root-trivia-scope.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +897 -200
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.d.ts +1 -1
- package/dist/language-service/index.js.map +1 -1
- package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.d.ts +3 -7
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +904 -202
- package/dist/plugin/index.js.map +3 -3
- package/dist/regex/first-set.d.ts +19 -0
- package/dist/regex/first-set.d.ts.map +1 -1
- package/dist/spec/index.js.map +1 -1
- package/dist/table/assemble.d.ts +16 -3
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/child-slots.d.ts.map +1 -1
- package/dist/table/compile-rule-map.d.ts +3 -2
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/compile.d.ts +2 -2
- package/dist/table/emit-assembly.d.ts +8 -2
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/emit.d.ts +8 -8
- package/dist/table/emit.d.ts.map +1 -1
- package/dist/table/encode.d.ts +6 -7
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/exec.d.ts.map +1 -1
- package/dist/table/factor-choice.d.ts +27 -0
- package/dist/table/factor-choice.d.ts.map +1 -1
- package/dist/table/gap-gate.d.ts +11 -0
- package/dist/table/gap-gate.d.ts.map +1 -0
- package/dist/table/index.js +927 -202
- package/dist/table/index.js.map +3 -3
- package/dist/table/lex-program.d.ts.map +1 -1
- package/dist/table/ops.d.ts +48 -20
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +13 -8
- package/dist/table/program.d.ts.map +1 -1
- package/dist/types.d.ts +17 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +6 -6
- package/package.json +1 -1
- package/src/analysis/commitment.ts +1 -1
- package/src/analysis/duplication.ts +1 -1
- package/src/analysis/terminal.ts +1 -1
- package/src/combinators/attempt.ts +1 -1
- package/src/combinators/choice.ts +1 -1
- package/src/combinators/completions.ts +3 -3
- package/src/combinators/dispatch.ts +30 -6
- package/src/combinators/first-set.ts +55 -12
- package/src/combinators/gate.ts +1 -1
- package/src/combinators/node.ts +1 -1
- package/src/combinators/regex.ts +9 -4
- package/src/combinators/repeat.ts +6 -6
- package/src/combinators/sequence.ts +1 -1
- package/src/combinators/trivia-boundary.ts +18 -0
- package/src/combinators/trivia-skip.ts +1 -1
- package/src/compiler/compile-linkable-table.ts +2 -3
- package/src/compiler/degradation.ts +2 -2
- package/src/compiler/ir-serialize.ts +1 -1
- package/src/compiler/linker.ts +5 -3
- package/src/compiler/token-alphabet.ts +3 -1
- package/src/coverage.ts +1 -1
- package/src/cst/capture-buffer.ts +2 -2
- package/src/cst/host-mode.ts +1 -1
- package/src/cst/root-trivia-scope.ts +1 -1
- package/src/index.ts +1 -1
- package/src/language-service/index.ts +1 -1
- package/src/plugin/entry-supercompiler.ts +2 -1
- package/src/plugin/evaluator.ts +8 -3
- package/src/plugin/index.ts +11 -16
- package/src/regex/first-set.ts +37 -3
- package/src/table/assemble.ts +180 -35
- package/src/table/child-slots.ts +8 -3
- package/src/table/compile-rule-map.ts +10 -6
- package/src/table/compile.ts +3 -3
- package/src/table/emit-assembly.ts +186 -35
- package/src/table/emit.ts +19 -18
- package/src/table/encode.ts +482 -88
- package/src/table/exec.ts +42 -18
- package/src/table/factor-choice.ts +62 -0
- package/src/table/fail-clean.ts +2 -2
- package/src/table/gap-gate.ts +55 -0
- package/src/table/hold-gap.ts +2 -2
- package/src/table/lex-program.ts +15 -6
- package/src/table/ops.ts +51 -20
- package/src/table/program.ts +14 -9
- package/src/table/select-hot.ts +7 -2
- package/src/table/stamp.ts +1 -1
- package/src/types.ts +13 -6
- package/src/version.ts +6 -6
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CharRange, FirstSet, Combinator, ParserDef } from '../types.ts'
|
|
2
2
|
import { implicitLeadsDropped, leadRanges } from '../lead-ranges.ts'
|
|
3
|
+
import { triviaThroughRef } from './trivia-boundary.ts'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Resolve a NAMED cross-artifact rule reference (`g.Foo`) whose own thunk does not
|
|
@@ -27,6 +28,13 @@ export function classifyFinalChoice(
|
|
|
27
28
|
arms: readonly Combinator<unknown>[],
|
|
28
29
|
resolve?: RefResolver,
|
|
29
30
|
activeTrivia?: Combinator<unknown> | null,
|
|
31
|
+
consuming = true,
|
|
32
|
+
/**
|
|
33
|
+
* Read each arm's construction-time `_meta.firstSet`, the set a disjoint
|
|
34
|
+
* interpreter `choice()` dispatches on (`choice.ts`), instead of deriving it:
|
|
35
|
+
* the table then lets in exactly the arms the interpreter lets in.
|
|
36
|
+
*/
|
|
37
|
+
constructionSets = false,
|
|
30
38
|
): FinalChoiceClassification {
|
|
31
39
|
const firstSets: FirstSet[] = []
|
|
32
40
|
const nullable: boolean[] = []
|
|
@@ -38,7 +46,7 @@ export function classifyFinalChoice(
|
|
|
38
46
|
for (const arm of arms) {
|
|
39
47
|
const empty = matchesEmpty(arm, new Set(), resolve)
|
|
40
48
|
nullable.push(empty)
|
|
41
|
-
firstSets.push(empty ? { kind: 'any' } : firstSetOf(arm, new Set(), resolve, activeTrivia))
|
|
49
|
+
firstSets.push(empty ? { kind: 'any' } : constructionSets ? arm._meta.firstSet : firstSetOf(arm, new Set(), resolve, activeTrivia, consuming))
|
|
42
50
|
if (empty) exclusive = false
|
|
43
51
|
}
|
|
44
52
|
if (exclusive) {
|
|
@@ -73,7 +81,7 @@ export function classifyFinalChoice(
|
|
|
73
81
|
* does not. `undefined` → no winner; fall back to the thunk. Monolithic: the winner IS
|
|
74
82
|
* the thunk's target, a no-op. A winner that recurses back to `p` (recursive or
|
|
75
83
|
* self-alias rule) is caught by the shared `seen` guard → `any`, a sound over-approx. */
|
|
76
|
-
function winnerForRef(p: Combinator<unknown>, resolve: RefResolver | undefined): Combinator<unknown> | undefined {
|
|
84
|
+
export function winnerForRef(p: Combinator<unknown>, resolve: RefResolver | undefined): Combinator<unknown> | undefined {
|
|
77
85
|
if (resolve === undefined) return undefined
|
|
78
86
|
const name = (p as unknown as { _ruleName?: string })._ruleName
|
|
79
87
|
const w = name === undefined ? undefined : resolve(name)
|
|
@@ -316,6 +324,7 @@ function sequenceSet(
|
|
|
316
324
|
fs: (p: Combinator<unknown>) => FirstSet,
|
|
317
325
|
empties: (p: Combinator<unknown>) => boolean,
|
|
318
326
|
trivia?: Combinator<unknown> | null,
|
|
327
|
+
consuming = true,
|
|
319
328
|
): FirstSet {
|
|
320
329
|
const afterBoundary = (i: number): FirstSet => {
|
|
321
330
|
const rest = at(i)
|
|
@@ -334,13 +343,23 @@ function sequenceSet(
|
|
|
334
343
|
}
|
|
335
344
|
const first = fs(p)
|
|
336
345
|
if (!empties(p) || i + 1 === parsers.length) return first
|
|
337
|
-
|
|
346
|
+
// A nullable regex reports `any` as its own set (it may match empty
|
|
347
|
+
// anywhere); here the empty match is the union with the rest, so only its
|
|
348
|
+
// non-empty matches' first chars are its own contribution.
|
|
349
|
+
return union((consuming ? p._meta.consumingFirstSet : undefined) ?? first, afterBoundary(i + 1))
|
|
338
350
|
}
|
|
339
351
|
return at(0)
|
|
340
352
|
}
|
|
341
353
|
|
|
354
|
+
/**
|
|
355
|
+
* A sequence's construction-time first set, which the interpreter's `choice()`
|
|
356
|
+
* dispatches on. Trivia is unknown here, and a choice may run where leading
|
|
357
|
+
* trivia has not been skipped, so a nullable regex stands as `any` (`consuming`
|
|
358
|
+
* false): read through its non-empty matches, `choice(sequence(pad, '*'), 'x')`
|
|
359
|
+
* would not enter the pad arm at a comment before `*`.
|
|
360
|
+
*/
|
|
342
361
|
export function sequenceFirstSet(parsers: readonly Combinator<unknown>[]): FirstSet {
|
|
343
|
-
return sequenceSet(parsers, p => p._meta.firstSet, matchesEmpty)
|
|
362
|
+
return sequenceSet(parsers, p => p._meta.firstSet, matchesEmpty, undefined, false)
|
|
344
363
|
}
|
|
345
364
|
|
|
346
365
|
/**
|
|
@@ -356,6 +375,17 @@ export function sequenceFirstSet(parsers: readonly Combinator<unknown>[]): First
|
|
|
356
375
|
* referenced rule can be replaced with a WIDER first-set, so a baked deep set
|
|
357
376
|
* would wrongly skip valid input — the compose path defers dispatch to fuse time.
|
|
358
377
|
*
|
|
378
|
+
* `consuming: false` reads a nullable regex as `any`, as first sets read before
|
|
379
|
+
* 0.60.0. Recovery derives its resync sentinels so (`sequence.ts`
|
|
380
|
+
* `followSentinels`, the table's follow and separator classes, with no trivia),
|
|
381
|
+
* as do a construction-time sequence set and a table choice's classes. Only a
|
|
382
|
+
* repeat item's, a gap's and a node's gate read a pad through its non-empty
|
|
383
|
+
* matches.
|
|
384
|
+
*
|
|
385
|
+
* `activeTrivia` is the live trivia, `null` for a scope known to have none (where
|
|
386
|
+
* a reference installs its rule's trivia, `triviaThroughRef`), and `undefined`
|
|
387
|
+
* where it is not known.
|
|
388
|
+
*
|
|
359
389
|
* `resolve` binds NAMED cross-artifact holes (`g.Foo`) against a fused winner map —
|
|
360
390
|
* see `RefResolver`. Diagnostic-only today: it is what lets the gating analysis ask
|
|
361
391
|
* the question at the site where the hole actually HAS an answer.
|
|
@@ -365,6 +395,7 @@ export function firstSetOf(
|
|
|
365
395
|
seen: Set<Combinator<unknown>> = new Set(),
|
|
366
396
|
resolve?: RefResolver,
|
|
367
397
|
activeTrivia?: Combinator<unknown> | null,
|
|
398
|
+
consuming = true,
|
|
368
399
|
): FirstSet {
|
|
369
400
|
// `seen` is the current recursion path, not a global visited set. A completed
|
|
370
401
|
// shared child must be analysed again for a sibling; only a back-edge on this
|
|
@@ -372,7 +403,7 @@ export function firstSetOf(
|
|
|
372
403
|
if (seen.has(p)) return any() // cycle → any (safe over-approximation)
|
|
373
404
|
seen.add(p)
|
|
374
405
|
try {
|
|
375
|
-
return firstSetBody(p, seen, resolve, activeTrivia)
|
|
406
|
+
return firstSetBody(p, seen, resolve, activeTrivia, consuming)
|
|
376
407
|
} finally {
|
|
377
408
|
seen.delete(p)
|
|
378
409
|
}
|
|
@@ -383,8 +414,9 @@ function firstSetBody(
|
|
|
383
414
|
seen: Set<Combinator<unknown>>,
|
|
384
415
|
resolve: RefResolver | undefined,
|
|
385
416
|
activeTrivia: Combinator<unknown> | null | undefined,
|
|
417
|
+
consuming: boolean,
|
|
386
418
|
): FirstSet {
|
|
387
|
-
const fs = (c: Combinator<unknown>): FirstSet => firstSetOf(c, seen, resolve, activeTrivia)
|
|
419
|
+
const fs = (c: Combinator<unknown>): FirstSet => firstSetOf(c, seen, resolve, activeTrivia, consuming)
|
|
388
420
|
const empties = (c: Combinator<unknown>): boolean => matchesEmpty(c, new Set(), resolve)
|
|
389
421
|
const d = p._def as ParserDef
|
|
390
422
|
switch (d.tag) {
|
|
@@ -393,10 +425,19 @@ function firstSetBody(
|
|
|
393
425
|
case 'keywords': return p._meta.firstSet // terminals: no refs, cached set is exact
|
|
394
426
|
case 'dispatch': return fs(d.selector)
|
|
395
427
|
case 'lazy': {
|
|
396
|
-
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
428
|
+
let target = winnerForRef(p, resolve)
|
|
429
|
+
if (target === undefined) {
|
|
430
|
+
try { target = d.thunk() }
|
|
431
|
+
catch { return any() }
|
|
432
|
+
}
|
|
433
|
+
// Under a scope KNOWN to have no trivia (`null`), the target runs under the
|
|
434
|
+
// trivia the reference installs for it (`triviaThroughRef`). `undefined`
|
|
435
|
+
// says nothing about the scope, and the target is read as it stands. Only
|
|
436
|
+
// the table encoder (which always passes `resolve`) reads through the
|
|
437
|
+
// install; the interpreter's own callers (`sequence.ts` follow sentinels)
|
|
438
|
+
// read a reference as 0.58.0 did (#223).
|
|
439
|
+
const through = activeTrivia === null && resolve !== undefined
|
|
440
|
+
return firstSetOf(target, seen, resolve, through ? triviaThroughRef(p, target, null) : activeTrivia, consuming)
|
|
400
441
|
}
|
|
401
442
|
case 'choice': {
|
|
402
443
|
let out: FirstSet = empty()
|
|
@@ -414,7 +455,7 @@ function firstSetBody(
|
|
|
414
455
|
// Recurse at every term boundary because a lookahead after another
|
|
415
456
|
// zero-width term runs after trivia has advanced, not at the sequence's
|
|
416
457
|
// original cursor.
|
|
417
|
-
return sequenceSet(d.parsers, fs, empties, activeTrivia)
|
|
458
|
+
return sequenceSet(d.parsers, fs, empties, activeTrivia, consuming)
|
|
418
459
|
}
|
|
419
460
|
case 'peek': {
|
|
420
461
|
// Deep-resolve the body: a `ref()` reads `any()` at CONSTRUCTION, so the
|
|
@@ -428,7 +469,6 @@ function firstSetBody(
|
|
|
428
469
|
case 'transform':
|
|
429
470
|
case 'label':
|
|
430
471
|
case 'trivia':
|
|
431
|
-
case 'token':
|
|
432
472
|
case 'leaf':
|
|
433
473
|
case 'node':
|
|
434
474
|
case 'sepBy':
|
|
@@ -437,11 +477,14 @@ function firstSetBody(
|
|
|
437
477
|
// the inner's construction-time set, which reads `any` when the inner opens
|
|
438
478
|
// with a rule reference — resolve through it like any other wrapper.
|
|
439
479
|
case 'attempt': return fs(d.parser)
|
|
480
|
+
// A token runs its body with trivia cleared (`token.ts`).
|
|
481
|
+
case 'token': return firstSetOf(d.parser, seen, resolve, activeTrivia === undefined ? undefined : null, consuming)
|
|
440
482
|
case 'grammar': return firstSetOf(
|
|
441
483
|
d.parser,
|
|
442
484
|
seen,
|
|
443
485
|
resolve,
|
|
444
486
|
d.clearTrivia ? null : (d.triviaParser ?? activeTrivia),
|
|
487
|
+
consuming,
|
|
445
488
|
)
|
|
446
489
|
// A run starts with its first line's prefix; a nullable prefix starts with anything.
|
|
447
490
|
case 'lines': {
|
package/src/combinators/gate.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types
|
|
|
15
15
|
* Naming: this matches the `gate:` field on a gated CHOICE arm
|
|
16
16
|
* (`choice({ gate, combinator }, other)`). Use the arm FIELD to SELECT a branch by
|
|
17
17
|
* a cheap state predicate; use this COMBINATOR to ASSERT a state predicate
|
|
18
|
-
* mid-sequence. Formerly named `guard()
|
|
18
|
+
* mid-sequence. Formerly named `guard()`; that alias was removed in 0.45.0.
|
|
19
19
|
*
|
|
20
20
|
* Note: like all state predicates its first-set is `any`, so a `gate(...)` as the
|
|
21
21
|
* LEADING term of a choice arm poisons that choice's first-char dispatch. Keep it
|
package/src/combinators/node.ts
CHANGED
|
@@ -187,7 +187,7 @@ export function node<N>(
|
|
|
187
187
|
if (project !== undefined && build !== undefined) {
|
|
188
188
|
throw new Error('node() options cannot combine project with a build callback')
|
|
189
189
|
}
|
|
190
|
-
const baseDef = { tag: 'node' as const, parser: combinator, ...(type === undefined ? {} : { type }), ...(build === undefined ? {} : { build }) }
|
|
190
|
+
const baseDef = { tag: 'node' as const, parser: combinator, ...(type === undefined ? {} : { type }), ...(build === undefined ? {} : { build }), ...(combinator._meta.firstSet.kind !== 'any' && !matchesEmpty(combinator) ? { guarded: true as const } : {}) }
|
|
191
191
|
const meta: ParserMeta = {
|
|
192
192
|
firstSet: combinator._meta.firstSet,
|
|
193
193
|
canMatchNewline: combinator._meta.canMatchNewline,
|
package/src/combinators/regex.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { failAt } from './probe.ts'
|
|
|
4
4
|
import { pushCstLeaf, cstCaptureActive } from '../cst/capture-buffer.ts'
|
|
5
5
|
import { recordLineRangeFromContext } from '../line-index.ts'
|
|
6
6
|
import { shorthandRanges, parseClassRanges } from '../regex/classes.ts'
|
|
7
|
-
import { firstSetFromRegex } from '../regex/first-set.ts'
|
|
7
|
+
import { consumingFirstSetFromRegex, firstSetFromRegex, regexCanMatchEmpty } from '../regex/first-set.ts'
|
|
8
8
|
import { directTerminalFailureExpected } from './expected.ts'
|
|
9
9
|
import type { ScalarParser } from './scalar.ts'
|
|
10
10
|
import { PlainLeaf, PlainSpan } from '../cst/plain.js'
|
|
@@ -156,13 +156,18 @@ export function regex(pattern: string | RegExp, flags = ''): Combinator<string>
|
|
|
156
156
|
// pairs, so gating on the ASCII set would false-exclude them. Fall back to `any()`
|
|
157
157
|
// (always-try) — sound, and only forfeits gating for the rare Unicode-mode `/i`
|
|
158
158
|
// recognizer, never the ASCII-only `/i` at-keywords the fix targets.
|
|
159
|
-
const
|
|
160
|
-
?
|
|
159
|
+
const forFlags = (fs: FirstSet): FirstSet => !resolvedFlags.includes('i')
|
|
160
|
+
? fs
|
|
161
161
|
: (resolvedFlags.includes('u') || resolvedFlags.includes('v'))
|
|
162
162
|
? any()
|
|
163
|
-
: asciiCaseFold(
|
|
163
|
+
: asciiCaseFold(fs)
|
|
164
|
+
const firstSet = forFlags(raw.firstSet)
|
|
164
165
|
const canMatchNewline = raw.canMatchNewline
|
|
165
166
|
const meta: ParserMeta = { firstSet, canMatchNewline, isTrivia: false }
|
|
167
|
+
if (firstSet.kind === 'any' && regexCanMatchEmpty(source)) {
|
|
168
|
+
const consuming = forFlags(consumingFirstSetFromRegex(source, resolvedFlags))
|
|
169
|
+
if (consuming.kind !== 'any') meta.consumingFirstSet = consuming
|
|
170
|
+
}
|
|
166
171
|
const def = { tag: 'regex', source, flags: resolvedFlags } as const
|
|
167
172
|
const expected = directTerminalFailureExpected(def)
|
|
168
173
|
const parseScalar: ScalarParser = (input, pos, ctx) => {
|
|
@@ -154,11 +154,11 @@ export function many<T>(combinator: Combinator<T>, opts: RepeatOptions = {}): Co
|
|
|
154
154
|
canMatchNewline: combinator._meta.canMatchNewline,
|
|
155
155
|
isTrivia: false,
|
|
156
156
|
}
|
|
157
|
-
const def: { tag: 'many'; parser: Combinator<unknown>; min: 0; max?: number; valueUnused?: boolean } =
|
|
158
|
-
{ tag: 'many', parser: combinator as Combinator<unknown>, min: 0, ...(max === Infinity ? {} : { max }) }
|
|
159
|
-
let expected: string[] | undefined
|
|
160
157
|
// A non-nullable body can be first-set-gated per loop iteration (see repItem).
|
|
161
158
|
const guardable = combinator._meta.firstSet.kind !== 'any' && !matchesEmpty(combinator)
|
|
159
|
+
const def: { tag: 'many'; parser: Combinator<unknown>; min: 0; max?: number; valueUnused?: boolean; guarded?: true } =
|
|
160
|
+
{ tag: 'many', parser: combinator as Combinator<unknown>, min: 0, ...(max === Infinity ? {} : { max }), ...(guardable ? { guarded: true } : {}) }
|
|
161
|
+
let expected: string[] | undefined
|
|
162
162
|
const scalarChild = scalarOf(combinator)
|
|
163
163
|
const parseScalar = (input: string, pos: number, ctx: ParseContext): number =>
|
|
164
164
|
repeatScalarTail(combinator, scalarChild, input, pos, ctx, guardable, def.valueUnused ? undefined : [], max)
|
|
@@ -229,11 +229,11 @@ function atLeast<T>(combinator: Combinator<T>, min: number, max: number): Combin
|
|
|
229
229
|
canMatchNewline: combinator._meta.canMatchNewline,
|
|
230
230
|
isTrivia: false,
|
|
231
231
|
}
|
|
232
|
-
const def: { tag: 'oneOrMore'; parser: Combinator<unknown>; min: number; max?: number; valueUnused?: boolean } =
|
|
233
|
-
{ tag: 'oneOrMore', parser: combinator as Combinator<unknown>, min, ...(max === Infinity ? {} : { max }) }
|
|
234
|
-
let expected: string[] | undefined
|
|
235
232
|
// A non-nullable body can be first-set-gated per loop iteration (see repItem).
|
|
236
233
|
const guardable = combinator._meta.firstSet.kind !== 'any' && !matchesEmpty(combinator)
|
|
234
|
+
const def: { tag: 'oneOrMore'; parser: Combinator<unknown>; min: number; max?: number; valueUnused?: boolean; guarded?: true } =
|
|
235
|
+
{ tag: 'oneOrMore', parser: combinator as Combinator<unknown>, min, ...(max === Infinity ? {} : { max }), ...(guardable ? { guarded: true } : {}) }
|
|
236
|
+
let expected: string[] | undefined
|
|
237
237
|
const scalarChild = scalarOf(combinator)
|
|
238
238
|
const parseScalar = (input: string, pos: number, ctx: ParseContext): number => {
|
|
239
239
|
let cur = scalarChild(input, pos, ctx)
|
|
@@ -108,7 +108,7 @@ export function sequence<T extends [Combinator<unknown>, ...Combinator<unknown>[
|
|
|
108
108
|
if (tolerant) {
|
|
109
109
|
followSentinels ??= parsers.map((_, i) => firstSetSentinel(
|
|
110
110
|
parsers.slice(i + 1).reduce<ReturnType<typeof firstSetOf>>(
|
|
111
|
-
(set, parser) => union(set, firstSetOf(parser)), { kind: 'empty' },
|
|
111
|
+
(set, parser) => union(set, firstSetOf(parser, new Set(), undefined, undefined, false)), { kind: 'empty' },
|
|
112
112
|
),
|
|
113
113
|
))
|
|
114
114
|
}
|
|
@@ -46,3 +46,21 @@ export function hasOwnTriviaBoundary(p: Combinator<unknown>, depth: number = MAX
|
|
|
46
46
|
default: return false
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The trivia `target` runs under when it is reached through the reference `ref`
|
|
52
|
+
* from a scope whose live trivia is `trivia` (`null` or `undefined`: none).
|
|
53
|
+
*
|
|
54
|
+
* A reference made from a region that cleared trivia re-installs the rule's own
|
|
55
|
+
* ambient trivia, where the rule has a boundary for it to repair (`ref.ts`,
|
|
56
|
+
* `Encoder.scopedRef`). Anything derived statically through a reference — a
|
|
57
|
+
* first set, a gate, a gap — must take the same scope, or it describes the
|
|
58
|
+
* caller's scope where the body runs under its own.
|
|
59
|
+
*/
|
|
60
|
+
export function triviaThroughRef(
|
|
61
|
+
ref: Combinator<unknown>, target: Combinator<unknown>, trivia: Combinator<unknown> | null | undefined,
|
|
62
|
+
): Combinator<unknown> | null | undefined {
|
|
63
|
+
if (trivia !== undefined && trivia !== null) return trivia
|
|
64
|
+
const amb = ref._meta.grammarTrivia ?? target._meta.grammarTrivia
|
|
65
|
+
return amb !== undefined && hasOwnTriviaBoundary(target) ? amb : trivia
|
|
66
|
+
}
|
|
@@ -413,7 +413,7 @@ export function scanTriviaCompact(input: string, cur: number, ctx: ParseContext)
|
|
|
413
413
|
return { end: tr.ok ? tr.span.end : cur, commit: NOOP_COMMIT }
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
/**
|
|
416
|
+
/** The object-shaped form of `scanTriviaCompact`. No engine calls it; the trivia-skip tests compare the two. */
|
|
417
417
|
export function scanTrivia(input: string, cur: number, ctx: ParseContext): TriviaScan {
|
|
418
418
|
const scan = scanTriviaCompact(input, cur, ctx)
|
|
419
419
|
return typeof scan === 'number' ? { end: scan, commit: NOOP_COMMIT } : scan
|
|
@@ -29,9 +29,8 @@ import { PARSEMAN_VERSION } from '../version.ts'
|
|
|
29
29
|
* artifact is a table, not a bag of compiled functions.
|
|
30
30
|
* `ir` — the piece's combinator graph, serialized. This is the
|
|
31
31
|
* composable half, and it is NOT new machinery: the plugin
|
|
32
|
-
* already carries pieces as IR (`plugin/index.ts
|
|
33
|
-
*
|
|
34
|
-
* `compileLinkable(evalRuleMapIR(p.ir), …)` at build time).
|
|
32
|
+
* already carries pieces as IR (`plugin/index.ts`) and the
|
|
33
|
+
* macro re-lowers them from it at build time.
|
|
35
34
|
*
|
|
36
35
|
* ── WHY THAT MAKES COMPOSITION THE EASY KIND ─────────────────────────────────
|
|
37
36
|
* The open question was whether table-to-table composition has to merge two
|
|
@@ -93,8 +93,8 @@ export function formatDegradations(list: readonly Degradation[]): string[] {
|
|
|
93
93
|
//
|
|
94
94
|
// Compilation is single-module and synchronous, so a module-level sink (opened per
|
|
95
95
|
// transform, drained after) records findings without threading a field through every
|
|
96
|
-
// Ctx and every compile*() return — the same shape as the regex-lowering
|
|
97
|
-
//
|
|
96
|
+
// Ctx and every compile*() return — the same shape as the source lowering's regex-lowering
|
|
97
|
+
// sink, for the same reason.
|
|
98
98
|
// ---------------------------------------------------------------------------
|
|
99
99
|
//
|
|
100
100
|
// A STACK, not a single slot. `transformMacro` re-enters itself when it lowers a private
|
|
@@ -461,7 +461,7 @@ class Serializer {
|
|
|
461
461
|
arms.push(`when(${matcherExpr(entry)}, ${kid(entry.parser)}${entry.caseInsensitive ? ', { caseInsensitive: true }' : ''})`)
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
|
-
if (def.otherwise) arms.push(`otherwise(${kid(def.otherwise)})`)
|
|
464
|
+
if (def.otherwise) arms.push(`otherwise(${kid(def.otherwise)}${def.otherwiseCommit === false ? ', { commit: false }' : ''})`)
|
|
465
465
|
return `dispatch(${kid(def.selector)}${arms.length === 0 ? '' : `, ${arms.join(', ')}`})`
|
|
466
466
|
}
|
|
467
467
|
case 'many': return `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`
|
package/src/compiler/linker.ts
CHANGED
|
@@ -25,9 +25,11 @@ import { copySpan } from '../line-index.ts'
|
|
|
25
25
|
/**
|
|
26
26
|
* Compile a `rules()` map to a **linkable artifact** — the composable, shippable
|
|
27
27
|
* form (RULE_ABI_PLAN §4). A package exports `linkable(rules(g => …))`; consumers
|
|
28
|
-
* import that artifact and `
|
|
29
|
-
* ever read**. Under the macro this is precompiled to static pieces
|
|
30
|
-
* interpreter it compiles here at load
|
|
28
|
+
* import that artifact and `compose([...])` it — **no source of the base grammar is
|
|
29
|
+
* ever read**. Under the macro this is precompiled to static pieces, and only a
|
|
30
|
+
* build-time `compose()` can link it. In the interpreter it compiles here at load
|
|
31
|
+
* (like `compile()`) and keeps the map's `RULES_RECIPE`, which is what a runtime
|
|
32
|
+
* `compose()` links; an artifact without one is refused there (`recipesOf`).
|
|
31
33
|
*
|
|
32
34
|
* `ns` is a per-artifact namespace; omit it to auto-assign a process-unique one
|
|
33
35
|
* (fine at runtime — the macro supplies a stable module-derived ns instead).
|
|
@@ -140,6 +140,8 @@ export type ExecutableLexNode =
|
|
|
140
140
|
readonly parsers: readonly Combinator<unknown>[]
|
|
141
141
|
/** Inside a no-trivia scope of the token, where the encoder's trivia is none. */
|
|
142
142
|
readonly cleared: boolean
|
|
143
|
+
/** The authored choice's own `disjoint`, which decides how the encoder classifies its arms. */
|
|
144
|
+
readonly disjoint: boolean
|
|
143
145
|
readonly armExpected: readonly (readonly string[])[]
|
|
144
146
|
readonly expected: readonly string[]
|
|
145
147
|
}
|
|
@@ -509,7 +511,7 @@ function directFixedLexNode(
|
|
|
509
511
|
if (arms.some(arm => arm === undefined)) return undefined
|
|
510
512
|
return {
|
|
511
513
|
kind: 'choice', arms: arms as ExecutableLexNode[],
|
|
512
|
-
parsers: def.parsers, cleared,
|
|
514
|
+
parsers: def.parsers, cleared, disjoint: def.disjoint,
|
|
513
515
|
armExpected: def.parsers.map(deriveExpected),
|
|
514
516
|
expected: def.parsers.flatMap(deriveExpected),
|
|
515
517
|
}
|
package/src/coverage.ts
CHANGED
|
@@ -309,7 +309,7 @@ function coverageEntry(entry: Combinator<unknown>, collector: GrammarCoverageCol
|
|
|
309
309
|
arms.push(when(dispatchMatcher(entry), wrapArm(entry.parser, ids[armIndex++]), entry.caseInsensitive ? { caseInsensitive: true } : undefined))
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
|
-
if (def.otherwise) arms.push(otherwise(wrapArm(def.otherwise, ids[armIndex++])))
|
|
312
|
+
if (def.otherwise) arms.push(otherwise(wrapArm(def.otherwise, ids[armIndex++]), def.otherwiseCommit === false ? { commit: false } : undefined))
|
|
313
313
|
const unsafeDispatch = dispatch as (selector: Combinator<string>, ...items: DispatchArm<unknown>[]) => Combinator<unknown>
|
|
314
314
|
const base = unsafeDispatch(build(def.selector) as Combinator<string>, ...arms)
|
|
315
315
|
return {
|
|
@@ -270,8 +270,8 @@ export function rollbackCstCaptureAt(
|
|
|
270
270
|
// speculative branch leaves no ghost error (see saveCstMark). Guarded on a
|
|
271
271
|
// present sink + a defined mark (rollbackTrivia forwards the same field).
|
|
272
272
|
//
|
|
273
|
-
// Each truncation is guarded on `length !== mark`,
|
|
274
|
-
//
|
|
273
|
+
// Each truncation is guarded on `length !== mark`, as the source lowering's
|
|
274
|
+
// `captureRestoreBody` did. Assigning `array.length` runs V8's
|
|
275
275
|
// length setter — a backing-store trim decision — and costs the same when the
|
|
276
276
|
// value is unchanged, which is the overwhelmingly common case here: a
|
|
277
277
|
// speculative branch that captured nothing still pays for every store. The
|
package/src/cst/host-mode.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The compiled-artifact ↔ host contract, in ONE place.
|
|
3
3
|
*
|
|
4
|
-
* This lives on its own — rather than in
|
|
4
|
+
* This lives on its own — rather than in the source lowering where it started —
|
|
5
5
|
* because the DRIVER has to enforce it too. `functional/run.ts` is the `parseman/run`
|
|
6
6
|
* entry, whose whole purpose is to stay small (see `test/unit/run-entry-closure.test.ts`),
|
|
7
7
|
* so it cannot import the compiler to reach an assertion. Neither can it carry its own
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The ROOT-CAPTURE POLICY of one local trivia scope, in one place.
|
|
3
3
|
*
|
|
4
4
|
* Three engines install a `parser({ trivia })` scope — the interpreter
|
|
5
|
-
* (`combinators/grammar.ts`), source lowering (
|
|
5
|
+
* (`combinators/grammar.ts`), the source lowering (removed in 0.47.0) and the
|
|
6
6
|
* table (`table/assemble.ts`, `table/exec.ts`) — and each of them has to answer
|
|
7
7
|
* the same two questions about selected root trivia. The refusal below was a
|
|
8
8
|
* string literal in two of them and ABSENT from the third, which is how a table
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type { KeywordsOptions, WordOptions } from './combinators/keywords.ts'
|
|
|
17
17
|
export { sequence } from './combinators/sequence.ts'
|
|
18
18
|
export { choice } from './combinators/choice.ts'
|
|
19
19
|
export { dispatch, endsWith, makeWhen, matches, otherwise, routed, startsWith, when } from './combinators/dispatch.ts'
|
|
20
|
-
export type { DispatchArm, DispatchOtherwise, DispatchStringMatcher, DispatchWhen, DispatchWhenFactory, DispatchWhenOptions, DispatchWhenMatcher } from './combinators/dispatch.ts'
|
|
20
|
+
export type { DispatchArm, DispatchOtherwise, DispatchOtherwiseOptions, DispatchStringMatcher, DispatchWhen, DispatchWhenFactory, DispatchWhenOptions, DispatchWhenMatcher } from './combinators/dispatch.ts'
|
|
21
21
|
export { attempt } from './combinators/attempt.ts'
|
|
22
22
|
export type { AttemptOptions } from './combinators/attempt.ts'
|
|
23
23
|
export { many, oneOrMore, optional, sepBy, oneOrMoreSep, keepSeparator } from './combinators/repeat.ts'
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* name = node type, without ever modifying the grammar:
|
|
7
7
|
*
|
|
8
8
|
* - `parse(src, { tolerant })` → CST + `ParseError[]` (recovers on the compiled
|
|
9
|
-
* fast path when `grammar` is a `compile(
|
|
9
|
+
* fast path when `grammar` is a `compile()`d grammar).
|
|
10
10
|
* - `diagnostics(src)` → structural parse errors + your per-node-type lint rules.
|
|
11
11
|
* - `completionsAt(src, offset)` → the grammar's expected-token set at the cursor,
|
|
12
12
|
* mapped through your per-rule semantic completion handlers.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Build-time Entry/SCC supercompiler for the selective assemblies a macro build
|
|
3
|
+
* emits (it runs on every macro build that selects sites).
|
|
3
4
|
*
|
|
4
5
|
* The table emitter remains the sole semantic backend. This pass parses that
|
|
5
6
|
* backend's ordinary JavaScript and joins uniquely-owned piece functions with
|
package/src/plugin/evaluator.ts
CHANGED
|
@@ -431,11 +431,16 @@ function dispatchArmValue(node: Expression, scope: XScope, code?: string, mfs?:
|
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
if (callee.name === 'otherwise') {
|
|
434
|
-
const [parserArg] = node.arguments
|
|
435
|
-
if (!parserArg || parserArg.type === 'SpreadElement') return null
|
|
434
|
+
const [parserArg, optsArg] = node.arguments
|
|
435
|
+
if (!parserArg || parserArg.type === 'SpreadElement' || optsArg?.type === 'SpreadElement') return null
|
|
436
436
|
const parserValue = anyValue(parserArg as Expression, scope, code, mfs)
|
|
437
437
|
if (!isCombinator(parserValue)) return null
|
|
438
|
-
return parseman.otherwise(parserValue)
|
|
438
|
+
if (optsArg === undefined) return parseman.otherwise(parserValue)
|
|
439
|
+
const opts = anyValue(optsArg as Expression, scope, code, mfs)
|
|
440
|
+
if (typeof opts !== 'object' || opts === null) return null
|
|
441
|
+
const commit = (opts as { commit?: unknown }).commit
|
|
442
|
+
if (commit !== undefined && typeof commit !== 'boolean') return null
|
|
443
|
+
return parseman.otherwise(parserValue, commit === undefined ? {} : { commit })
|
|
439
444
|
}
|
|
440
445
|
|
|
441
446
|
if (callee.name === 'when') {
|
package/src/plugin/index.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* For each such import, walks the file's AST, finds variable declarations
|
|
7
7
|
* whose RHS is a pure parseman combinator call, evaluates them at build time,
|
|
8
|
-
* compiles the result to
|
|
9
|
-
*
|
|
8
|
+
* compiles the result to a table program (plus generated code for its hottest
|
|
9
|
+
* sites, within `assemblyBudget`), and replaces the declaration. The macro import
|
|
10
|
+
* is removed; the module imports the shared driver from `parseman/table` instead.
|
|
10
11
|
*
|
|
11
12
|
* Usage:
|
|
12
13
|
* // vite.config.ts
|
|
@@ -61,7 +62,7 @@ import type {
|
|
|
61
62
|
* The PUBLIC subpath, not a deep path into `src/`: this string is written into a
|
|
62
63
|
* consumer's build output, so it has to be a specifier their resolver can see.
|
|
63
64
|
* `parseman/table` is declared in package.json `exports` and re-exports
|
|
64
|
-
* `tableRules`
|
|
65
|
+
* `tableRules` unrenamed (`src/table/index.ts`).
|
|
65
66
|
*/
|
|
66
67
|
const TABLE_RUNTIME_SPECIFIER = 'parseman/table'
|
|
67
68
|
|
|
@@ -119,13 +120,9 @@ export type ParsecraftPluginOptions = {
|
|
|
119
120
|
*/
|
|
120
121
|
warnUnloweredRegex?: boolean
|
|
121
122
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* parse is unaffected and allocates nothing). The output stays macro-inlinable
|
|
126
|
-
* (sentinels build via `_ctx._rec`, not `_rp`). Turn it on for grammars you drive
|
|
127
|
-
* from an editor/language service so `run(g, src, { tolerant: true })` recovers on
|
|
128
|
-
* the fast compiled path; leave it off for build-only grammars.
|
|
123
|
+
* @deprecated Has no effect: every compiled table carries recovery, and
|
|
124
|
+
* `run(g, src, { tolerant: true })` recovers on the compiled path whatever this is
|
|
125
|
+
* set to. Removed in the next minor release.
|
|
129
126
|
*/
|
|
130
127
|
recovery?: boolean
|
|
131
128
|
/** Emit grammar-coverage hooks in macro output. Off by default, so ordinary
|
|
@@ -1456,9 +1453,8 @@ function transformMacroImpl(
|
|
|
1456
1453
|
|
|
1457
1454
|
/**
|
|
1458
1455
|
* Compile a `rules(factory)` call into ONE shared replacement expression
|
|
1459
|
-
* for the whole call
|
|
1460
|
-
*
|
|
1461
|
-
* factory's entries commonly share large reachable sub-rule graphs; a
|
|
1456
|
+
* for the whole call: one shared `compileRuleMap()` pass instead of one
|
|
1457
|
+
* `compile()` per entry (a `rules()` factory's entries commonly share large reachable sub-rule graphs; a
|
|
1462
1458
|
* shared pass compiles each shared sub-rule once instead of once per entry
|
|
1463
1459
|
* that reaches it).
|
|
1464
1460
|
*/
|
|
@@ -1977,9 +1973,8 @@ function transformMacroImpl(
|
|
|
1977
1973
|
* shadows the base piece which actually defines `Pair` — the encoder then finds a
|
|
1978
1974
|
* hole where the winner should be and refuses the whole grammar with "ref() used
|
|
1979
1975
|
* before .define()". The same filter guards `compileLinkableTable`'s `isLocal`
|
|
1980
|
-
* (`compile-linkable-table.ts
|
|
1981
|
-
*
|
|
1982
|
-
* reason, and skipping it is why cross-piece composition looked blocked.
|
|
1976
|
+
* (`compile-linkable-table.ts`) and `recoverComposedRules`; this site needs it
|
|
1977
|
+
* for the same reason, and skipping it is why cross-piece composition looked blocked.
|
|
1983
1978
|
*
|
|
1984
1979
|
* The reference itself is not lost: it stays inside the delta's own rule bodies and
|
|
1985
1980
|
* `enc.winners` binds it BY NAME to whichever piece supplies the definition.
|
package/src/regex/first-set.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { shorthandRanges, parseClassRanges, readUnicodeEscape } from './classes.
|
|
|
32
32
|
type Node =
|
|
33
33
|
| { t: 'char'; set: FirstSet; nl: boolean } // matches exactly one char from `set`
|
|
34
34
|
| { t: 'any' } // unknown 1-char-ish construct: could start with anything
|
|
35
|
-
| { t: 'empty' }
|
|
35
|
+
| { t: 'empty'; look?: Node } // zero-width: anchors, lookaround, word boundary (`look`: a positive lookahead's body)
|
|
36
36
|
| { t: 'seq'; parts: Node[] }
|
|
37
37
|
| { t: 'alt'; arms: Node[] }
|
|
38
38
|
| { t: 'rep'; node: Node; min: number } // quantified; min 0 ⇒ nullable
|
|
@@ -142,10 +142,11 @@ function parseRegex(src: string, unicode = false, sets = false): Node {
|
|
|
142
142
|
i++ // consume '('
|
|
143
143
|
// Group prefixes: (?:…) (?=…) (?!…) (?<=…) (?<!…) (?<name>…)
|
|
144
144
|
let lookaround = false
|
|
145
|
+
let lookahead = false
|
|
145
146
|
if (src[i] === '?') {
|
|
146
147
|
const c1 = src[i + 1]
|
|
147
148
|
if (c1 === ':') i += 2
|
|
148
|
-
else if (c1 === '=' || c1 === '!') { i += 2; lookaround = true }
|
|
149
|
+
else if (c1 === '=' || c1 === '!') { lookahead = c1 === '='; i += 2; lookaround = true }
|
|
149
150
|
else if (c1 === '<') {
|
|
150
151
|
const c2 = src[i + 2]
|
|
151
152
|
if (c2 === '=' || c2 === '!') { i += 3; lookaround = true } // lookbehind
|
|
@@ -159,7 +160,10 @@ function parseRegex(src: string, unicode = false, sets = false): Node {
|
|
|
159
160
|
const inner = parseAlt()
|
|
160
161
|
if (src[i] !== ')') throw BAIL
|
|
161
162
|
i++ // consume ')'
|
|
162
|
-
|
|
163
|
+
// Lookaround is zero-width. A positive LOOKAHEAD keeps its body for
|
|
164
|
+
// `regexFirstTwo`; every other walker reads it as `EMPTY`.
|
|
165
|
+
if (lookaround) return lookahead ? { t: 'empty', look: inner } : EMPTY
|
|
166
|
+
return inner
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
const parseClass = (): Node => {
|
|
@@ -348,6 +352,17 @@ export function regexCanMatchEmpty(source: string): boolean {
|
|
|
348
352
|
try { return nullable(parseRegex(source)) } catch { return true }
|
|
349
353
|
}
|
|
350
354
|
|
|
355
|
+
/**
|
|
356
|
+
* The first chars of a NON-EMPTY match of `source`, nullable or not: the set
|
|
357
|
+
* `firstSetFromRegex` reports for a pattern that must consume, without its
|
|
358
|
+
* widening of a nullable one to `any()`. Only a caller that accounts for the
|
|
359
|
+
* empty match itself may use it (`sequenceSet`). `any()` when unparseable.
|
|
360
|
+
* `flags` reads atoms as `firstSetFromRegex` does.
|
|
361
|
+
*/
|
|
362
|
+
export function consumingFirstSetFromRegex(source: string, flags = ''): FirstSet {
|
|
363
|
+
try { return firstSet(parseRegex(source, flags.includes('u') || flags.includes('v'), flags.includes('v'))) } catch { return any() }
|
|
364
|
+
}
|
|
365
|
+
|
|
351
366
|
/**
|
|
352
367
|
* `flags` decides how atoms read: under `u`/`v` each is one code point, else one
|
|
353
368
|
* code unit, so a non-`u` set holds a high surrogate where an astral character
|
|
@@ -369,3 +384,22 @@ export function firstSetFromRegex(source: string, flags = ''): { firstSet: First
|
|
|
369
384
|
return { firstSet: firstSet(ast), canMatchNewline: canConsumeNewline(ast) }
|
|
370
385
|
}
|
|
371
386
|
|
|
387
|
+
/**
|
|
388
|
+
* The first TWO characters every match of `source` begins with, where the
|
|
389
|
+
* pattern says them outright: a one-character class and then another, or a
|
|
390
|
+
* positive lookahead over one (`:(?=[ \t])`). `undefined` otherwise. Every
|
|
391
|
+
* match that starts with a character of `first` has a second character, and it
|
|
392
|
+
* is in `second`. `flags` reads atoms as `firstSetFromRegex` does.
|
|
393
|
+
*/
|
|
394
|
+
export function regexFirstTwo(source: string, flags = ''): { first: FirstSet; second: FirstSet } | undefined {
|
|
395
|
+
let ast: Node
|
|
396
|
+
try { ast = parseRegex(source, flags.includes('u') || flags.includes('v'), flags.includes('v')) } catch { return undefined }
|
|
397
|
+
const parts = ast.t === 'seq' ? ast.parts : [ast]
|
|
398
|
+
const a = parts[0]
|
|
399
|
+
const b = parts[1]
|
|
400
|
+
if (a === undefined || a.t !== 'char' || a.set.kind !== 'ranges' || b === undefined) return undefined
|
|
401
|
+
const second = b.t === 'char'
|
|
402
|
+
? b.set
|
|
403
|
+
: b.t === 'empty' && b.look !== undefined && !nullable(b.look) ? firstSet(b.look) : undefined
|
|
404
|
+
return second !== undefined && second.kind === 'ranges' ? { first: a.set, second } : undefined
|
|
405
|
+
}
|