parseman 0.59.3 → 0.60.1
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/diagnostics.js +876 -190
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +876 -190
- package/dist/cli/index.js.map +3 -3
- 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/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/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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +896 -199
- package/dist/index.js.map +3 -3
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.js +903 -201
- 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/table/assemble.d.ts +15 -1
- 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 +1 -1
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/compile.d.ts +1 -1
- 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 +4 -3
- package/dist/table/emit.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/exec.d.ts +0 -35
- 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 +926 -201
- package/dist/table/index.js.map +3 -3
- package/dist/table/lex-program.d.ts.map +1 -1
- package/dist/table/ops.d.ts +35 -16
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +6 -1
- package/dist/table/program.d.ts.map +1 -1
- package/dist/types.d.ts +16 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +2 -4
- package/src/analysis/duplication.ts +1 -1
- package/src/combinators/attempt.ts +1 -1
- package/src/combinators/dispatch.ts +30 -6
- package/src/combinators/first-set.ts +55 -12
- 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/ir-serialize.ts +1 -1
- package/src/compiler/token-alphabet.ts +3 -1
- package/src/coverage.ts +1 -1
- package/src/index.ts +1 -1
- package/src/plugin/evaluator.ts +8 -3
- package/src/regex/first-set.ts +37 -3
- package/src/table/assemble.ts +176 -30
- package/src/table/child-slots.ts +8 -3
- package/src/table/compile-rule-map.ts +7 -4
- package/src/table/compile.ts +1 -1
- package/src/table/emit-assembly.ts +184 -33
- package/src/table/emit.ts +6 -4
- package/src/table/encode.ts +469 -74
- package/src/table/exec.ts +38 -82
- 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 +38 -16
- package/src/table/program.ts +7 -2
- package/src/table/select-hot.ts +7 -2
- package/src/types.ts +12 -5
- package/src/version.ts +1 -1
- package/dist/table/encode-baseline.d.ts +0 -16
- package/dist/table/encode-baseline.d.ts.map +0 -1
- package/dist/table/exec-baseline.d.ts +0 -9
- package/dist/table/exec-baseline.d.ts.map +0 -1
- package/src/table/encode-baseline.ts +0 -346
- package/src/table/exec-baseline.ts +0 -557
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
|
|
@@ -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)})`
|
|
@@ -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 {
|
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'
|
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/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
|
+
}
|