parseman 0.61.6 → 0.61.8
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 +117 -33
- package/dist/analysis/diagnostics.js.map +2 -2
- package/dist/cli/index.js +117 -33
- package/dist/cli/index.js.map +2 -2
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts +3 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +14 -10
- package/dist/compiler/token-capability.js.map +1 -1
- package/dist/index.js +117 -33
- package/dist/index.js.map +2 -2
- package/dist/plugin/index.js +117 -33
- package/dist/plugin/index.js.map +2 -2
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/fail-clean.d.ts.map +1 -1
- package/dist/table/hold-gap.d.ts +3 -1
- package/dist/table/hold-gap.d.ts.map +1 -1
- package/dist/table/index.js +117 -33
- package/dist/table/index.js.map +2 -2
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/combinators/expect.ts +3 -2
- package/src/combinators/first-set.ts +14 -7
- package/src/combinators/sequence.ts +1 -1
- package/src/table/assemble.ts +38 -0
- package/src/table/emit-assembly.ts +42 -5
- package/src/table/encode.ts +1 -1
- package/src/table/fail-clean.ts +8 -4
- package/src/table/hold-gap.ts +22 -13
- package/src/version.ts +1 -1
package/dist/version.d.ts
CHANGED
|
@@ -13,5 +13,5 @@
|
|
|
13
13
|
* Kept in sync with `package.json` by `test/unit/version-sync.test.ts`, which fails
|
|
14
14
|
* the build if this constant drifts from the published version.
|
|
15
15
|
*/
|
|
16
|
-
export declare const PARSEMAN_VERSION = "0.61.
|
|
16
|
+
export declare const PARSEMAN_VERSION = "0.61.8";
|
|
17
17
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Combinator, ParseContext, ParseResult, ParserMeta, ParseError } from '../types.ts'
|
|
2
2
|
import { captureError } from '../recovery/scan.ts'
|
|
3
3
|
import { annotateSpanFromLineContext } from '../line-index.ts'
|
|
4
|
-
import { matchesEmpty, isZeroWidthAssertion } from './first-set.ts'
|
|
4
|
+
import { any, matchesEmpty, isZeroWidthAssertion } from './first-set.ts'
|
|
5
5
|
|
|
6
6
|
export type { ParseError }
|
|
7
7
|
export { assertionFailureExpected, directTerminalFailureExpected } from './expected.ts'
|
|
@@ -133,7 +133,8 @@ function derive(c: Combinator<unknown>, seen: Set<Combinator<unknown>>): string[
|
|
|
133
133
|
export function expect<T>(combinator: Combinator<T>, label?: string): Combinator<T | ParseError> {
|
|
134
134
|
const expected = label !== undefined ? [label] : deriveExpected(combinator)
|
|
135
135
|
const meta: ParserMeta = {
|
|
136
|
-
|
|
136
|
+
// It runs on any character: on one its child can't start, it reports the error.
|
|
137
|
+
firstSet: any(),
|
|
137
138
|
canMatchNewline: combinator._meta.canMatchNewline,
|
|
138
139
|
isTrivia: false,
|
|
139
140
|
}
|
|
@@ -202,7 +202,8 @@ function matchesEmptyBody(
|
|
|
202
202
|
case 'many':
|
|
203
203
|
case 'optional':
|
|
204
204
|
case 'not':
|
|
205
|
-
case 'peek':
|
|
205
|
+
case 'peek':
|
|
206
|
+
case 'expect': return true // zero repetitions / absent / lookahead / recovered
|
|
206
207
|
// Default `sepBy` is `(item (sep item)*)?` — it MATCHES THE EMPTY STRING.
|
|
207
208
|
// Any `min >= 1` requires that many ITEMS, so it is nullable only when the
|
|
208
209
|
// item is. (Keying this off `min === 1` reported every `{ min: 2 }` list as
|
|
@@ -220,7 +221,6 @@ function matchesEmptyBody(
|
|
|
220
221
|
case 'trivia':
|
|
221
222
|
case 'token':
|
|
222
223
|
case 'leaf':
|
|
223
|
-
case 'expect':
|
|
224
224
|
case 'withCtx':
|
|
225
225
|
case 'node':
|
|
226
226
|
case 'grammar':
|
|
@@ -382,6 +382,8 @@ export function sequenceFirstSet(parsers: readonly Combinator<unknown>[]): First
|
|
|
382
382
|
* repeat item's, a gap's and a node's gate read a pad through its non-empty
|
|
383
383
|
* matches.
|
|
384
384
|
*
|
|
385
|
+
* `follow: true` asks where a resync sentinel stops, which reads through `expect`.
|
|
386
|
+
*
|
|
385
387
|
* `activeTrivia` is the live trivia, `null` for a scope known to have none (where
|
|
386
388
|
* a reference installs its rule's trivia, `triviaThroughRef`), and `undefined`
|
|
387
389
|
* where it is not known.
|
|
@@ -396,6 +398,7 @@ export function firstSetOf(
|
|
|
396
398
|
resolve?: RefResolver,
|
|
397
399
|
activeTrivia?: Combinator<unknown> | null,
|
|
398
400
|
consuming = true,
|
|
401
|
+
follow = false,
|
|
399
402
|
): FirstSet {
|
|
400
403
|
// `seen` is the current recursion path, not a global visited set. A completed
|
|
401
404
|
// shared child must be analysed again for a sibling; only a back-edge on this
|
|
@@ -403,7 +406,7 @@ export function firstSetOf(
|
|
|
403
406
|
if (seen.has(p)) return any() // cycle → any (safe over-approximation)
|
|
404
407
|
seen.add(p)
|
|
405
408
|
try {
|
|
406
|
-
return firstSetBody(p, seen, resolve, activeTrivia, consuming)
|
|
409
|
+
return firstSetBody(p, seen, resolve, activeTrivia, consuming, follow)
|
|
407
410
|
} finally {
|
|
408
411
|
seen.delete(p)
|
|
409
412
|
}
|
|
@@ -415,8 +418,9 @@ function firstSetBody(
|
|
|
415
418
|
resolve: RefResolver | undefined,
|
|
416
419
|
activeTrivia: Combinator<unknown> | null | undefined,
|
|
417
420
|
consuming: boolean,
|
|
421
|
+
follow: boolean,
|
|
418
422
|
): FirstSet {
|
|
419
|
-
const fs = (c: Combinator<unknown>): FirstSet => firstSetOf(c, seen, resolve, activeTrivia, consuming)
|
|
423
|
+
const fs = (c: Combinator<unknown>): FirstSet => firstSetOf(c, seen, resolve, activeTrivia, consuming, follow)
|
|
420
424
|
const empties = (c: Combinator<unknown>): boolean => matchesEmpty(c, new Set(), resolve)
|
|
421
425
|
const d = p._def as ParserDef
|
|
422
426
|
switch (d.tag) {
|
|
@@ -437,7 +441,7 @@ function firstSetBody(
|
|
|
437
441
|
// install; the interpreter's own callers (`sequence.ts` follow sentinels)
|
|
438
442
|
// read a reference as 0.58.0 did (#223).
|
|
439
443
|
const through = activeTrivia === null && resolve !== undefined
|
|
440
|
-
return firstSetOf(target, seen, resolve, through ? triviaThroughRef(p, target, null) : activeTrivia, consuming)
|
|
444
|
+
return firstSetOf(target, seen, resolve, through ? triviaThroughRef(p, target, null) : activeTrivia, consuming, follow)
|
|
441
445
|
}
|
|
442
446
|
case 'choice': {
|
|
443
447
|
let out: FirstSet = empty()
|
|
@@ -457,6 +461,9 @@ function firstSetBody(
|
|
|
457
461
|
// original cursor.
|
|
458
462
|
return sequenceSet(d.parsers, fs, empties, activeTrivia, consuming)
|
|
459
463
|
}
|
|
464
|
+
// Runs on any character: on one its child can't start, it reports the error. A
|
|
465
|
+
// resync sentinel still stops where the child could start.
|
|
466
|
+
case 'expect': return follow ? fs(d.parser) : any()
|
|
460
467
|
case 'peek': {
|
|
461
468
|
// Deep-resolve the body: a `ref()` reads `any()` at CONSTRUCTION, so the
|
|
462
469
|
// shallow `_meta.firstSet` baked into the assertion would lose the gate.
|
|
@@ -472,19 +479,19 @@ function firstSetBody(
|
|
|
472
479
|
case 'leaf':
|
|
473
480
|
case 'node':
|
|
474
481
|
case 'sepBy':
|
|
475
|
-
case 'expect':
|
|
476
482
|
// A transaction accepts exactly what its inner accepts. Its own `_meta` is
|
|
477
483
|
// the inner's construction-time set, which reads `any` when the inner opens
|
|
478
484
|
// with a rule reference — resolve through it like any other wrapper.
|
|
479
485
|
case 'attempt': return fs(d.parser)
|
|
480
486
|
// A token runs its body with trivia cleared (`token.ts`).
|
|
481
|
-
case 'token': return firstSetOf(d.parser, seen, resolve, activeTrivia === undefined ? undefined : null, consuming)
|
|
487
|
+
case 'token': return firstSetOf(d.parser, seen, resolve, activeTrivia === undefined ? undefined : null, consuming, follow)
|
|
482
488
|
case 'grammar': return firstSetOf(
|
|
483
489
|
d.parser,
|
|
484
490
|
seen,
|
|
485
491
|
resolve,
|
|
486
492
|
d.clearTrivia ? null : (d.triviaParser ?? activeTrivia),
|
|
487
493
|
consuming,
|
|
494
|
+
follow,
|
|
488
495
|
)
|
|
489
496
|
// A run starts with its first line's prefix; a nullable prefix starts with anything.
|
|
490
497
|
case 'lines': {
|
|
@@ -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, new Set(), undefined, undefined, false)), { kind: 'empty' },
|
|
111
|
+
(set, parser) => union(set, firstSetOf(parser, new Set(), undefined, undefined, false, true)), { kind: 'empty' },
|
|
112
112
|
),
|
|
113
113
|
))
|
|
114
114
|
}
|
package/src/table/assemble.ts
CHANGED
|
@@ -2844,8 +2844,46 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2844
2844
|
}
|
|
2845
2845
|
if (hasAdj) {
|
|
2846
2846
|
const first = linkFirst()
|
|
2847
|
+
// What a held gap needs undone if the next term recovers at zero width.
|
|
2848
|
+
// Written after the lookahead returns and read before the next term runs,
|
|
2849
|
+
// so nothing can re-enter this sequence in between.
|
|
2850
|
+
let heldFrom = 0, heldTl = 0, heldScanTl = 0, heldLog = 0, heldScanLog = 0, heldRoot = 0, heldScanRoot = 0
|
|
2847
2851
|
const runnerAt = (i: number): TermRunner => {
|
|
2848
2852
|
const kidIp = code[base + i]!
|
|
2853
|
+
if (i > 1 && holdsGap(code, k, base, n, i - 1, NULLABLE) && rowNullable(code, k, kidIp, NULLABLE)) {
|
|
2854
|
+
const kid = link(kidIp)
|
|
2855
|
+
return (input, cur, ctx) => {
|
|
2856
|
+
const from = heldFrom, tl = heldTl, scanTl = heldScanTl, log = heldLog
|
|
2857
|
+
const scanLog = heldScanLog, root = heldRoot, scanRoot = heldScanRoot
|
|
2858
|
+
const next = step(kid, input, cur, ctx)
|
|
2859
|
+
if (next !== cur || from === cur) return next
|
|
2860
|
+
rollbackScannedTriviaAt(ctx, tl, scanTl, log, scanLog, root, scanRoot)
|
|
2861
|
+
return from
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
if (holdsGap(code, k, base, n, i, NULLABLE) && rowNullable(code, k, code[base + i + 1]!, NULLABLE)) {
|
|
2865
|
+
const kid = link(kidIp)
|
|
2866
|
+
return (input, cur, ctx) => {
|
|
2867
|
+
if (ctx.trivia === undefined) {
|
|
2868
|
+
const v = kid(input, cur, ctx)
|
|
2869
|
+
if (v === FAIL) return -1
|
|
2870
|
+
TERMV = v
|
|
2871
|
+
heldFrom = cur
|
|
2872
|
+
return EC.e
|
|
2873
|
+
}
|
|
2874
|
+
const tl = cstTlLen(ctx), log = ctx._triviaLog?.length ?? 0, root = ctx._rootTriviaLog?.length ?? 0
|
|
2875
|
+
const scanEnd = skipTrivia(input, cur, ctx)
|
|
2876
|
+
const scanTl = cstTlLen(ctx), scanLog = ctx._triviaLog?.length ?? 0, scanRoot = ctx._rootTriviaLog?.length ?? 0
|
|
2877
|
+
const v = kid(input, scanEnd, ctx)
|
|
2878
|
+
if (v === FAIL) return -1
|
|
2879
|
+
TERMV = v
|
|
2880
|
+
heldFrom = cur
|
|
2881
|
+
heldTl = tl; heldScanTl = scanTl
|
|
2882
|
+
heldLog = log; heldScanLog = scanLog
|
|
2883
|
+
heldRoot = root; heldScanRoot = scanRoot
|
|
2884
|
+
return scanEnd
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2849
2887
|
if (holdsGap(code, k, base, n, i, NULLABLE)) {
|
|
2850
2888
|
const kid = link(kidIp)
|
|
2851
2889
|
return (input, cur, ctx) => {
|
|
@@ -467,16 +467,40 @@ ${scanned}
|
|
|
467
467
|
}`
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
+
const TRIVIA_LOG_LENGTH = 'ctx._cstBuf!==undefined?(ctx._cstBuf.tl!==undefined?ctx._cstBuf.tl.length:0):(ctx._cstTriviaLog!==undefined?ctx._cstTriviaLog.length:0)'
|
|
471
|
+
|
|
470
472
|
/**
|
|
471
473
|
* A lookahead term that HOLDS its gap (`hold-gap.ts`): the gap is scanned and
|
|
472
474
|
* kept, the lookahead runs at its end, and the cursor stays there for the next
|
|
473
|
-
* term, which cannot match empty
|
|
475
|
+
* term, which cannot match empty unless it `recovers`. Then the marks stay in
|
|
476
|
+
* scope for `emitHeldGapUndo`, which follows the next term.
|
|
474
477
|
*/
|
|
475
|
-
function emitHoldTerm(callee: string, dst: string, t: string, l: SiteLabel, skip: string): string {
|
|
478
|
+
function emitHoldTerm(callee: string, dst: string, t: string, l: SiteLabel, skip: string, recovers: boolean): string {
|
|
476
479
|
const fast = `const ${t}v=${callee}(input,cur,ctx)
|
|
477
480
|
if(${t}v===FAIL)return FAIL
|
|
478
481
|
${dst}=${t}v
|
|
479
482
|
cur=EC.e`
|
|
483
|
+
if (recovers) {
|
|
484
|
+
const marked = `let ${t}c=cur,${t}s=cur,${t}tl=0,${t}stl=0,${t}lg=0,${t}slg=0,${t}rt=0,${t}srt=0`
|
|
485
|
+
if (l.tri === TRI_NONE) return `${marked}\n${fast}`
|
|
486
|
+
const held = `${t}tl=${TRIVIA_LOG_LENGTH}
|
|
487
|
+
${t}lg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
|
|
488
|
+
${t}rt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0
|
|
489
|
+
${t}s=${skip}(input,cur,ctx)
|
|
490
|
+
${t}stl=${TRIVIA_LOG_LENGTH}
|
|
491
|
+
${t}slg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
|
|
492
|
+
${t}srt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0
|
|
493
|
+
const ${t}v=${callee}(input,${t}s,ctx)
|
|
494
|
+
if(${t}v===FAIL)return FAIL
|
|
495
|
+
${dst}=${t}v
|
|
496
|
+
cur=${t}s`
|
|
497
|
+
return l.tri !== TRI_UNKNOWN ? `${marked}\n${held}` : `${marked}
|
|
498
|
+
if(ctx.trivia===undefined){
|
|
499
|
+
${fast}
|
|
500
|
+
}else{
|
|
501
|
+
${held}
|
|
502
|
+
}`
|
|
503
|
+
}
|
|
480
504
|
if (l.tri === TRI_NONE) return fast
|
|
481
505
|
const held = `const ${t}s=${skip}(input,cur,ctx)
|
|
482
506
|
const ${t}v=${callee}(input,${t}s,ctx)
|
|
@@ -491,6 +515,11 @@ ${held}
|
|
|
491
515
|
}`
|
|
492
516
|
}
|
|
493
517
|
|
|
518
|
+
/** The next term recovered at zero width, so the gap held for it was never consumed. */
|
|
519
|
+
function emitHeldGapUndo(t: string): string {
|
|
520
|
+
return `if(cur===${t}s&&${t}s!==${t}c){rollbackScannedTriviaAt(ctx,${t}tl,${t}stl,${t}lg,${t}slg,${t}rt,${t}srt);cur=${t}c}`
|
|
521
|
+
}
|
|
522
|
+
|
|
494
523
|
/**
|
|
495
524
|
* The repetition's LEAD-TRIVIA skip, as a BLOCK.
|
|
496
525
|
*
|
|
@@ -2019,6 +2048,7 @@ return v
|
|
|
2019
2048
|
}
|
|
2020
2049
|
parts.push('let cur=EC.e')
|
|
2021
2050
|
const names: string[] = ['v0']
|
|
2051
|
+
let heldUndo: string | undefined
|
|
2022
2052
|
for (let i = 1; i < n; i++) {
|
|
2023
2053
|
const vn = `v${i}`
|
|
2024
2054
|
parts.push(`let ${vn}`)
|
|
@@ -2038,9 +2068,16 @@ return v
|
|
|
2038
2068
|
// The callee expression hands it over AFTER the scan statement, right
|
|
2039
2069
|
// before the call, so a field inside the trivia grammar cannot take it.
|
|
2040
2070
|
const callee = leadingTriviaFieldAt(code, k, childIp) ? `(LEAD.at=cur,${kids[i]!})` : kids[i]!
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2071
|
+
if (holdsGap(code, k, base, n, i, nullable)) {
|
|
2072
|
+
const t = tmp()
|
|
2073
|
+
const recovers = rowNullable(code, k, code[base + i + 1]!, nullable)
|
|
2074
|
+
parts.push(emitHoldTerm(callee, vn, t, L, skipFor(L), recovers))
|
|
2075
|
+
heldUndo = recovers ? t : undefined
|
|
2076
|
+
} else {
|
|
2077
|
+
parts.push(emitTerm(callee, vn, tmp(), L, skipFor(L), L.tri >= 0 ? triviaMiss(L.tri) : undefined))
|
|
2078
|
+
if (heldUndo !== undefined) parts.push(emitHeldGapUndo(heldUndo))
|
|
2079
|
+
heldUndo = undefined
|
|
2080
|
+
}
|
|
2044
2081
|
}
|
|
2045
2082
|
names.push(vn)
|
|
2046
2083
|
}
|
package/src/table/encode.ts
CHANGED
|
@@ -751,7 +751,7 @@ class Encoder {
|
|
|
751
751
|
for (let j = i + 1; j < parsers.length; j++) {
|
|
752
752
|
// As the interpreter's `followSentinels` derive them (`sequence.ts`): with no
|
|
753
753
|
// trivia, whatever scope the sequence runs in.
|
|
754
|
-
fs = union(fs, firstSetOf(parsers[j]!, new Set(), rr, undefined, false))
|
|
754
|
+
fs = union(fs, firstSetOf(parsers[j]!, new Set(), rr, undefined, false, true))
|
|
755
755
|
}
|
|
756
756
|
return this.charClass(fs)
|
|
757
757
|
}
|
package/src/table/fail-clean.ts
CHANGED
|
@@ -120,11 +120,15 @@ export function failCleanAnalysis(code: ArrayLike<number>): FailClean {
|
|
|
120
120
|
case OP_CONT:
|
|
121
121
|
out = clean(code[ip + 1]!) && neverFails(code[ip + 3]!)
|
|
122
122
|
break
|
|
123
|
-
// An arm's failure and
|
|
124
|
-
// not
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
// An arm's failure and a committing fallback's are committed; only the
|
|
124
|
+
// selector's is not. An uncommitted fallback's failure leaves the selector's
|
|
125
|
+
// capture, unless the fallback reads it through `routed()`, which rolls that
|
|
126
|
+
// capture back first. With no fallback, the dispatch fails after the selector wrote.
|
|
127
|
+
case OP_DISPATCH: {
|
|
128
|
+
const flags = code[ip + 4]!
|
|
129
|
+
out = code[ip + 3]! >= 0 && ((flags & 2) === 0 || (flags & 1) === 1) && clean(code[ip + 1]!)
|
|
127
130
|
break
|
|
131
|
+
}
|
|
128
132
|
default: {
|
|
129
133
|
const c = passChild(code, ip)
|
|
130
134
|
out = c >= 0 && clean(c)
|
package/src/table/hold-gap.ts
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Not before an adjacency assertion or a leading-trivia field, which read the
|
|
15
15
|
* cursor from before the gap.
|
|
16
|
+
*
|
|
17
|
+
* A next term that can match empty only by recovering (`expect`) holds the gap
|
|
18
|
+
* too, so valid text still scans it once. If it does recover, the gap is
|
|
19
|
+
* unrecorded and the cursor goes back, where only invalid text pays for it.
|
|
16
20
|
*/
|
|
17
21
|
import {
|
|
18
22
|
OP_ADJ, OP_ATTEMPT, OP_CHOICE, OP_COV, OP_EXPECT, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_LIT, OP_LIT_CI,
|
|
@@ -24,16 +28,21 @@ import { leadingTriviaFieldAt } from './field-spec.ts'
|
|
|
24
28
|
import { regexCanMatchEmpty } from '../regex/first-set.ts'
|
|
25
29
|
|
|
26
30
|
const WRAPPERS = new Set([OP_NODE, OP_XFORM, OP_LEAF, OP_SOURCE_LEAF, OP_TOKEN, OP_SCOPE, OP_SCOPE_CAP,
|
|
27
|
-
OP_SCOPE_PLAIN, OP_FIELD, OP_LABEL,
|
|
31
|
+
OP_SCOPE_PLAIN, OP_FIELD, OP_LABEL, OP_WITHCTX, OP_RULE, OP_ATTEMPT, OP_COV, OP_GAPGATE])
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
34
|
* Can the row at `ip` match empty? `true` wherever it cannot tell. A first-set
|
|
31
35
|
* gate row wraps only a child that cannot (`encode.ts` gates nothing nullable).
|
|
36
|
+
* An `expect` recovers at zero width whatever its child needs; with `recovers`
|
|
37
|
+
* false, this asks what the row can match empty without recovering.
|
|
32
38
|
*/
|
|
33
|
-
export function rowNullable(
|
|
34
|
-
|
|
39
|
+
export function rowNullable(
|
|
40
|
+
code: ArrayLike<number>, k: readonly unknown[], ip: number, memo: Map<number, boolean>, recovers = true,
|
|
41
|
+
): boolean {
|
|
42
|
+
const key = recovers ? ip : ~ip
|
|
43
|
+
const hit = memo.get(key)
|
|
35
44
|
if (hit !== undefined) return hit
|
|
36
|
-
memo.set(
|
|
45
|
+
memo.set(key, true) // a cycle is answered conservatively
|
|
37
46
|
const op = code[ip]!
|
|
38
47
|
const kids: number[] = []
|
|
39
48
|
let out = true
|
|
@@ -42,20 +51,20 @@ export function rowNullable(code: ArrayLike<number>, k: readonly unknown[], ip:
|
|
|
42
51
|
else if (op === OP_RX) { const re = k[code[ip + 1]!]; out = !(re instanceof RegExp) || regexCanMatchEmpty(re.source) }
|
|
43
52
|
else if (op === OP_SEQ || op === OP_SEQV || op === OP_SEQX) {
|
|
44
53
|
childSlots(code, ip, kids)
|
|
45
|
-
out = kids.every(c => rowNullable(code, k, c, memo))
|
|
54
|
+
out = kids.every(c => rowNullable(code, k, c, memo, recovers))
|
|
46
55
|
} else if (op === OP_CHOICE) {
|
|
47
56
|
childSlots(code, ip, kids)
|
|
48
|
-
out = kids.length === 0 || kids.some(c => rowNullable(code, k, c, memo))
|
|
57
|
+
out = kids.length === 0 || kids.some(c => rowNullable(code, k, c, memo, recovers))
|
|
49
58
|
} else if (op === OP_REP || op === OP_REPV) {
|
|
50
|
-
out = code[ip + 2] === 0 || rowNullable(code, k, code[ip + 1]!, memo)
|
|
59
|
+
out = code[ip + 2] === 0 || rowNullable(code, k, code[ip + 1]!, memo, recovers)
|
|
51
60
|
} else if (op === OP_SEED) {
|
|
52
|
-
out = rowNullable(code, k, code[ip + 1]!, memo)
|
|
61
|
+
out = rowNullable(code, k, code[ip + 1]!, memo, recovers)
|
|
53
62
|
} else if (op === OP_CONT) {
|
|
54
|
-
out = rowNullable(code, k, code[ip + 1]!, memo) && rowNullable(code, k, code[ip + 3]!, memo)
|
|
55
|
-
} else if (WRAPPERS.has(op)) {
|
|
56
|
-
if (childSlots(code, ip, kids) && kids.length === 1) out = rowNullable(code, k, kids[0]!, memo)
|
|
63
|
+
out = rowNullable(code, k, code[ip + 1]!, memo, recovers) && rowNullable(code, k, code[ip + 3]!, memo, recovers)
|
|
64
|
+
} else if (WRAPPERS.has(op) || (op === OP_EXPECT && !recovers)) {
|
|
65
|
+
if (childSlots(code, ip, kids) && kids.length === 1) out = rowNullable(code, k, kids[0]!, memo, recovers)
|
|
57
66
|
}
|
|
58
|
-
memo.set(
|
|
67
|
+
memo.set(key, out)
|
|
59
68
|
return out
|
|
60
69
|
}
|
|
61
70
|
|
|
@@ -68,5 +77,5 @@ export function holdsGap(
|
|
|
68
77
|
if (code[term] !== OP_NOT && code[term] !== OP_PEEK) return false
|
|
69
78
|
const next = code[base + i + 1]!
|
|
70
79
|
if (code[next] === OP_ADJ || leadingTriviaFieldAt(code, k, next)) return false
|
|
71
|
-
return !rowNullable(code, k, next, memo)
|
|
80
|
+
return !rowNullable(code, k, next, memo, false)
|
|
72
81
|
}
|
package/src/version.ts
CHANGED