parseman 0.57.0 → 0.58.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.
Files changed (69) hide show
  1. package/dist/analysis/diagnostics.js +1800 -962
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +1929 -1091
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/dispatch.d.ts.map +1 -1
  6. package/dist/compiler/token-alphabet.d.ts +11 -1
  7. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  8. package/dist/compiler/token-capability.js +69 -16
  9. package/dist/compiler/token-capability.js.map +2 -2
  10. package/dist/cst/capture-buffer.d.ts +6 -0
  11. package/dist/cst/capture-buffer.d.ts.map +1 -1
  12. package/dist/index.js +1017 -177
  13. package/dist/index.js.map +3 -3
  14. package/dist/plugin/index.d.ts +25 -1
  15. package/dist/plugin/index.d.ts.map +1 -1
  16. package/dist/plugin/index.js +1210 -236
  17. package/dist/plugin/index.js.map +3 -3
  18. package/dist/run/index.d.ts +2 -0
  19. package/dist/run/index.d.ts.map +1 -1
  20. package/dist/run/index.js +9 -0
  21. package/dist/run/index.js.map +1 -1
  22. package/dist/table/assemble.d.ts.map +1 -1
  23. package/dist/table/compile-rule-map.d.ts.map +1 -1
  24. package/dist/table/emit-assembly.d.ts +1 -1
  25. package/dist/table/emit-assembly.d.ts.map +1 -1
  26. package/dist/table/emit.d.ts.map +1 -1
  27. package/dist/table/encode.d.ts.map +1 -1
  28. package/dist/table/exec.d.ts.map +1 -1
  29. package/dist/table/factor-choice.d.ts +42 -0
  30. package/dist/table/factor-choice.d.ts.map +1 -0
  31. package/dist/table/fail-clean.d.ts +3 -0
  32. package/dist/table/fail-clean.d.ts.map +1 -0
  33. package/dist/table/hold-gap.d.ts +8 -0
  34. package/dist/table/hold-gap.d.ts.map +1 -0
  35. package/dist/table/index.js +1370 -259
  36. package/dist/table/index.js.map +3 -3
  37. package/dist/table/lex-program.d.ts +7 -3
  38. package/dist/table/lex-program.d.ts.map +1 -1
  39. package/dist/table/ops.d.ts +8 -0
  40. package/dist/table/ops.d.ts.map +1 -1
  41. package/dist/table/run-support.d.ts +19 -0
  42. package/dist/table/run-support.d.ts.map +1 -1
  43. package/dist/table/scalar-terminal.d.ts +2 -0
  44. package/dist/table/scalar-terminal.d.ts.map +1 -1
  45. package/dist/table/site-labels.d.ts +66 -0
  46. package/dist/table/site-labels.d.ts.map +1 -1
  47. package/dist/version.d.ts +1 -1
  48. package/package.json +1 -1
  49. package/src/combinators/dispatch.ts +12 -3
  50. package/src/compiler/token-alphabet.ts +57 -17
  51. package/src/cst/capture-buffer.ts +25 -0
  52. package/src/plugin/index.ts +230 -35
  53. package/src/run/index.ts +5 -0
  54. package/src/table/assemble.ts +252 -62
  55. package/src/table/child-slots.ts +2 -2
  56. package/src/table/compile-rule-map.ts +14 -5
  57. package/src/table/emit-assembly.ts +112 -23
  58. package/src/table/emit.ts +3 -1
  59. package/src/table/encode.ts +83 -14
  60. package/src/table/exec.ts +27 -8
  61. package/src/table/factor-choice.ts +173 -0
  62. package/src/table/fail-clean.ts +130 -0
  63. package/src/table/hold-gap.ts +68 -0
  64. package/src/table/lex-program.ts +197 -43
  65. package/src/table/ops.ts +9 -1
  66. package/src/table/run-support.ts +35 -0
  67. package/src/table/scalar-terminal.ts +8 -0
  68. package/src/table/site-labels.ts +135 -4
  69. package/src/version.ts +1 -1
@@ -0,0 +1,173 @@
1
+ /**
2
+ * SHARED ENDS, FACTORED — a choice whose arms differ in one term.
3
+ *
4
+ * `choice(leaf(noTrivia(sequence(gap, '*', gap))), leaf(noTrivia(sequence(gap, '+', gap))))`
5
+ * scans `gap` once per arm, and on a value with no operator (`red red`) both
6
+ * arms scan it and then fail. Every arm is the same wrappers over a sequence of
7
+ * the same terms except one, so the choice is that one term:
8
+ *
9
+ * leaf(noTrivia(sequence(gap, choice('*', '+'), gap)))
10
+ *
11
+ * The gap is scanned once, and the inner choice dispatches on the character
12
+ * after it.
13
+ *
14
+ * WHY IT IS THE SAME PARSE. Each arm is tried at the same position with the same
15
+ * context, so its terms before the differing one match the same text with the
16
+ * same value in every arm: running them once is what each arm's retry did. The
17
+ * inner choice tries the differing terms in arm order, as the outer one tried
18
+ * the arms. The terms after it may differ only as `optional`/`many`, which
19
+ * cannot fail, so the arm that matched its differing term is the arm the outer
20
+ * choice took. The sequence's values, the wrappers' spans and the leaf or node a
21
+ * wrapper publishes are what that arm produced.
22
+ *
23
+ * A choice that fails rolls its last arm back and reports the position it started
24
+ * at, so the factored form is an `attempt`, which does both. An arm that may
25
+ * commit is left as written: a committed choice reports without rolling back.
26
+ *
27
+ * WHAT "THE SAME" MEANS. Two terms are the same when they are one combinator, or
28
+ * their definitions are, field for field, down to shared combinators. A reducer
29
+ * is the same function, or the same printed source: a table artifact carries a
30
+ * reducer AS its source, so two reducers printed alike are one function there.
31
+ */
32
+ import type { Combinator, ParserDef } from '../types.ts'
33
+ import { choice } from '../combinators/choice.ts'
34
+ import { sequence } from '../combinators/sequence.ts'
35
+ import { leaf, sourceLeaf } from '../combinators/token.ts'
36
+ import { noTrivia, parser } from '../combinators/grammar.ts'
37
+ import { attempt } from '../combinators/attempt.ts'
38
+ import { mayCommitFailure } from '../analysis/commitment.ts'
39
+
40
+ type ChoiceDef = Extract<ParserDef, { tag: 'choice' }>
41
+
42
+ /** Own keys every combinator may carry beside its definition; all derived from it. */
43
+ const DERIVED = new Set(['_tag', '_meta', '_def', '_parseScalar', '_stickyRegex', 'parse'])
44
+
45
+ function isCombinator(v: unknown): v is Combinator<unknown> {
46
+ return typeof v === 'object' && v !== null && '_def' in v && '_meta' in v
47
+ }
48
+
49
+ function sameComb(a: Combinator<unknown>, b: Combinator<unknown>, depth: number): boolean {
50
+ if (a === b) return true
51
+ if (depth > 32 || a._tag !== b._tag) return false
52
+ for (const k of Object.keys(a)) if (!DERIVED.has(k)) return false
53
+ for (const k of Object.keys(b)) if (!DERIVED.has(k)) return false
54
+ return sameFields(a._def as Record<string, unknown>, b._def as Record<string, unknown>, depth, undefined)
55
+ }
56
+
57
+ function sameValue(u: unknown, v: unknown, depth: number): boolean {
58
+ if (u === v) return true
59
+ if (isCombinator(u) && isCombinator(v)) return sameComb(u, v, depth + 1)
60
+ if (Array.isArray(u) && Array.isArray(v)) return u.length === v.length && u.every((x, i) => sameValue(x, v[i], depth))
61
+ if (typeof u === 'object' && u !== null && typeof v === 'object' && v !== null
62
+ && Object.getPrototypeOf(u) === Object.prototype && Object.getPrototypeOf(v) === Object.prototype) {
63
+ return sameFields(u as Record<string, unknown>, v as Record<string, unknown>, depth, undefined)
64
+ }
65
+ return false
66
+ }
67
+
68
+ /** Every field of `x` and `y` the same, `skip` aside. A reducer matches its twin by printed source. */
69
+ function sameFields(x: Record<string, unknown>, y: Record<string, unknown>, depth: number, skip: string | undefined): boolean {
70
+ const kx = Object.keys(x).filter(k => k !== skip && x[k] !== undefined)
71
+ const ky = Object.keys(y).filter(k => k !== skip && y[k] !== undefined)
72
+ if (kx.length !== ky.length) return false
73
+ for (const k of kx) {
74
+ if (!(k in y)) return false
75
+ const u = x[k]
76
+ const v = y[k]
77
+ if (k === 'fn' && typeof u === 'function' && typeof v === 'function' && u !== v) {
78
+ if (typeof x.fnSrc !== 'string' || x.fnSrc === '' || x.fnSrc !== y.fnSrc) return false
79
+ continue
80
+ }
81
+ if (!sameValue(u, v, depth)) return false
82
+ }
83
+ return true
84
+ }
85
+
86
+ /** A wrapper the factoring can rebuild around a new body: a leaf, or a trivia scope. */
87
+ function rewrap(w: Combinator<unknown>, body: Combinator<unknown>): Combinator<unknown> | undefined {
88
+ const d = w._def
89
+ let out: Combinator<unknown>
90
+ if (d.tag === 'leaf') {
91
+ out = d.cstValue === 'source' ? sourceLeaf(body, d.fn) : leaf(body, d.fn)
92
+ if (d.fnSrc !== undefined) (out._def as { fnSrc?: string }).fnSrc = d.fnSrc
93
+ } else if (d.tag === 'grammar') {
94
+ out = d.clearTrivia === true ? noTrivia(body) : parser({ trivia: d.triviaParser ?? null }, body)
95
+ } else return undefined
96
+ // The rebuilt wrapper must be the original in everything but its body.
97
+ return sameFields(d as Record<string, unknown>, out._def as Record<string, unknown>, 0, 'parser') ? out : undefined
98
+ }
99
+
100
+ function unusedOf(seq: Combinator<unknown>): boolean {
101
+ return seq._def.tag === 'sequence' && seq._def.valueUnused === true
102
+ }
103
+
104
+ function never(c: Combinator<unknown>): boolean {
105
+ const t = c._def.tag
106
+ return t === 'optional' || t === 'many'
107
+ }
108
+
109
+ /**
110
+ * The factored form of `d`'s choice, or `undefined` when its arms are not one
111
+ * shape that differs in one term.
112
+ */
113
+ export function factorSharedEnds(d: ChoiceDef): Combinator<unknown> | undefined {
114
+ const arms = d.parsers
115
+ if (arms.length < 2) return undefined
116
+ if (d.strategy.tag !== 'firstMatch' && d.strategy.tag !== 'sharedPrefix') return undefined
117
+ if (d.gates.some(g => g !== null) || d.autoNot.some(a => a !== null)) return undefined
118
+ // Each arm: its wrappers, outermost first, and the sequence they end in.
119
+ const shapes: { wrappers: Combinator<unknown>[]; terms: Combinator<unknown>[]; seq: Combinator<unknown> }[] = []
120
+ for (const arm of arms) {
121
+ const wrappers: Combinator<unknown>[] = []
122
+ let c = arm
123
+ while ((c._def.tag === 'leaf' || c._def.tag === 'grammar') && wrappers.length < 4) {
124
+ wrappers.push(c)
125
+ c = c._def.parser
126
+ }
127
+ if (c._def.tag !== 'sequence' || c._def.parsers.length < 2) return undefined
128
+ shapes.push({ wrappers, terms: c._def.parsers, seq: c })
129
+ }
130
+ const first = shapes[0]!
131
+ const n = first.terms.length
132
+ for (const s of shapes) {
133
+ if (s.wrappers.length !== first.wrappers.length || s.terms.length !== n) return undefined
134
+ if (unusedOf(s.seq) !== unusedOf(first.seq)) return undefined
135
+ for (let j = 0; j < s.wrappers.length; j++) {
136
+ const a = first.wrappers[j]!._def as Record<string, unknown>
137
+ const b = s.wrappers[j]!._def as Record<string, unknown>
138
+ if (a.tag !== b.tag || !sameFields(a, b, 0, 'parser')) return undefined
139
+ }
140
+ }
141
+ // Exactly one term position differs, and nothing after it can fail.
142
+ let at = -1
143
+ for (let i = 0; i < n; i++) {
144
+ if (shapes.every(s => sameComb(s.terms[i]!, first.terms[i]!, 0))) continue
145
+ if (at >= 0) return undefined
146
+ at = i
147
+ }
148
+ if (at < 0) return undefined
149
+ for (let i = at + 1; i < n; i++) if (!never(first.terms[i]!)) return undefined
150
+ // A committed failure leaves a choice without rolling its arm back. Asked last:
151
+ // it walks everything the arms reach.
152
+ if (arms.some(a => mayCommitFailure(a))) return undefined
153
+ const middle = choice(...(shapes.map(s => s.terms[at]!) as [Combinator<unknown>, ...Combinator<unknown>[]]))
154
+ // The inner choice must keep arm order: a strategy that reorders (longest
155
+ // literal first) or classifies would pick a different arm. Nor may it add an
156
+ // automatic lookahead the arms as written do not have: `'start'` before
157
+ // `'started'` gets a `not('ed')` as one choice, which rejects the `!start` the
158
+ // written arms take on `!started`.
159
+ if (middle._def.tag === 'choice' && middle._def.autoNot.some(a => a !== null)) return undefined
160
+ const st = middle._def.tag === 'choice' ? middle._def.strategy : undefined
161
+ if (st === undefined || (st.tag !== 'firstMatch' && st.tag !== 'sharedPrefix'
162
+ && !(st.tag === 'literalsLongestFirst' && st.sortedIndices.every((x, i) => x === i)))) return undefined
163
+ let out: Combinator<unknown> | undefined = sequence(
164
+ ...([...first.terms.slice(0, at), middle, ...first.terms.slice(at + 1)] as [Combinator<unknown>, ...Combinator<unknown>[]]),
165
+ )
166
+ if (unusedOf(first.seq)) {
167
+ (out._def as { valueUnused?: boolean }).valueUnused = true
168
+ }
169
+ for (let j = first.wrappers.length - 1; j >= 0 && out !== undefined; j--) out = rewrap(first.wrappers[j]!, out)
170
+ // A choice that fails is rolled back and reports where it started; so does an
171
+ // `attempt`. The sequence alone would report where its inner choice failed.
172
+ return out === undefined ? undefined : attempt(out)
173
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * WHICH ROWS FAIL CLEAN.
3
+ *
4
+ * A choice, an `optional()` and a repeat item take a rollback mark before they
5
+ * try their child and restore it when the child fails, so a failed attempt leaves
6
+ * nothing in the capture sinks: the open frame's children, raw entries and trivia
7
+ * rows, the fields, the errors and the two trivia logs. Most children never write
8
+ * to a sink before they fail. A terminal fails before it pushes its leaf; a node
9
+ * that fails drops its own frame; a sequence whose later terms cannot fail can
10
+ * only fail in its first term, which has no trivia in front of it. For such a
11
+ * child the mark and the restore are work with nothing to undo.
12
+ *
13
+ * `failsClean(ip)` answers, from the table alone, whether every failure of the
14
+ * row at `ip` leaves every sink as the row found it. Only uncommitted failures
15
+ * count: a committed failure is propagated past every choice, optional and
16
+ * repeat without their restore, so the mark is never read for it.
17
+ *
18
+ * It is one memoised walk, run while the assembler links, so it is paid on the
19
+ * first parse. A rule reached again while it is still being answered counts as
20
+ * dirty, and so does a row whose behaviour is not modelled here: `false` keeps
21
+ * the mark, so every approximation errs towards the old cost, never a wrong tree.
22
+ */
23
+ import {
24
+ OP_ATTEMPT, OP_CHOICE, OP_COV, OP_DISPATCH, OP_EMPTY, OP_EXPECT, OP_FIELD, OP_GATE, OP_GUARD, OP_LABEL,
25
+ OP_LEAF, OP_LIT, OP_LIT_CI, OP_LIT_CI_TRACK, OP_LIT_TRACK, OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT, OP_PEEK,
26
+ OP_REP, OP_REPV, OP_ROUTED, OP_RULE, OP_RX, OP_RX_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ,
27
+ OP_SEQV, OP_SEQX, OP_SOURCE_LEAF, OP_TOKEN, OP_WITHCTX, OP_XFORM,
28
+ } from './ops.ts'
29
+
30
+ export type FailClean = (ip: number) => boolean
31
+
32
+ /**
33
+ * The child offset of a row that fails exactly when its one child fails and
34
+ * writes nothing of its own first, or −1.
35
+ */
36
+ function passChild(code: ArrayLike<number>, ip: number): number {
37
+ switch (code[ip]) {
38
+ case OP_RULE: case OP_LABEL: case OP_COV: case OP_TOKEN:
39
+ return code[ip + 1]!
40
+ case OP_GATE: case OP_NODE: case OP_NODE_TRACK: case OP_XFORM: case OP_FIELD: case OP_SCOPE:
41
+ case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_LEAF: case OP_SOURCE_LEAF:
42
+ return code[ip + 2]!
43
+ default:
44
+ return -1
45
+ }
46
+ }
47
+
48
+ /** Sequence terms: `[first offset, count]` packed as the offset of the count word, or −1. */
49
+ function seqCount(code: ArrayLike<number>, ip: number): number {
50
+ const op = code[ip]
51
+ return op === OP_SEQ || op === OP_SEQV ? ip + 1 : op === OP_SEQX ? ip + 2 : -1
52
+ }
53
+
54
+ export function failCleanAnalysis(code: ArrayLike<number>): FailClean {
55
+ // Per row: 0 unknown, 1 yes, 2 no, 3 being answered (answered as no).
56
+ const cleanAt = new Uint8Array(code.length)
57
+ const neverAt = new Uint8Array(code.length)
58
+
59
+ /** No uncommitted failure at all. */
60
+ const neverFails = (ip: number): boolean => {
61
+ const hit = neverAt[ip]!
62
+ if (hit !== 0) return hit === 1
63
+ neverAt[ip] = 3
64
+ let out = false
65
+ const op = code[ip]
66
+ if (op === OP_OPT || op === OP_EMPTY || op === OP_EXPECT) out = true
67
+ else if (op === OP_REP || op === OP_REPV) out = code[ip + 2] === 0
68
+ else {
69
+ const at = seqCount(code, ip)
70
+ if (at >= 0) {
71
+ out = true
72
+ for (let i = 1, n = code[at]!; out && i <= n; i++) out = neverFails(code[at + i]!)
73
+ } else if (op !== OP_GATE) {
74
+ const c = passChild(code, ip)
75
+ out = c >= 0 && neverFails(c)
76
+ }
77
+ }
78
+ neverAt[ip] = out ? 1 : 2
79
+ return out
80
+ }
81
+
82
+ const clean = (ip: number): boolean => {
83
+ const hit = cleanAt[ip]!
84
+ if (hit !== 0) return hit === 1
85
+ cleanAt[ip] = 3
86
+ let out = false
87
+ switch (code[ip]) {
88
+ // A terminal pushes its leaf only on a match; NOT/PEEK/ATTEMPT restore
89
+ // their own mark; OPT, EMPTY and EXPECT do not fail uncommitted.
90
+ case OP_LIT: case OP_RX: case OP_LIT_TRACK: case OP_RX_TRACK: case OP_LIT_CI: case OP_LIT_CI_TRACK:
91
+ case OP_GUARD: case OP_NOT: case OP_PEEK: case OP_ATTEMPT: case OP_EMPTY: case OP_OPT: case OP_EXPECT:
92
+ out = true
93
+ break
94
+ // Min 0 does not fail uncommitted; min 1 fails only with no item kept, and
95
+ // restores the failed item itself.
96
+ case OP_REP: case OP_REPV:
97
+ out = code[ip + 2]! <= 1
98
+ break
99
+ // Only the first term may fail: a later one fails after the terms before it
100
+ // wrote, and after the trivia scanned in front of it.
101
+ case OP_SEQ: case OP_SEQV: case OP_SEQX: {
102
+ const at = seqCount(code, ip)
103
+ const n = code[at]!
104
+ out = n > 0 && clean(code[at + 1]!)
105
+ for (let i = 2; out && i <= n; i++) out = neverFails(code[at + i]!)
106
+ break
107
+ }
108
+ case OP_CHOICE: {
109
+ out = true
110
+ for (let i = 0, n = code[ip + 2]!; out && i < n; i++) out = clean(code[ip + 4 + i]!)
111
+ break
112
+ }
113
+ case OP_ROUTED:
114
+ out = code[ip + 1]! < 0 || clean(code[ip + 1]!)
115
+ break
116
+ // An arm's failure and the fallback's are committed; only the selector's is
117
+ // not, and without a fallback an unknown key fails after the selector wrote.
118
+ case OP_DISPATCH:
119
+ out = code[ip + 3]! >= 0 && clean(code[ip + 1]!)
120
+ break
121
+ default: {
122
+ const c = passChild(code, ip)
123
+ out = c >= 0 && clean(c)
124
+ }
125
+ }
126
+ cleanAt[ip] = out ? 1 : 2
127
+ return out
128
+ }
129
+ return clean
130
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * A GAP HELD OVER A LOOKAHEAD.
3
+ *
4
+ * In a sequence, the trivia before each term is scanned and recorded, and when
5
+ * the term matches nothing it is unrecorded and the cursor goes back before it:
6
+ * the next term scans the same gap again. Where the term that matched nothing is
7
+ * a lookahead (`not`/`peek`, always zero-width) and the next term cannot match
8
+ * empty, that second scan reads the same characters from the same position under
9
+ * the same trivia and records the same rows, and the next term then either
10
+ * consumes past the gap or the sequence fails. So the gap is kept: the lookahead
11
+ * runs at the gap's end, the cursor stays there, and the next term starts there
12
+ * with nothing left to scan. One scan where there were two and an undo.
13
+ *
14
+ * Not before an adjacency assertion or a leading-trivia field, which read the
15
+ * cursor from before the gap.
16
+ */
17
+ import {
18
+ OP_ADJ, OP_ATTEMPT, OP_CHOICE, OP_COV, OP_EXPECT, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_LIT, OP_LIT_CI,
19
+ OP_NODE, OP_NOT, OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ,
20
+ OP_SEQV, OP_SEQX, OP_SOURCE_LEAF, OP_TOKEN, OP_WITHCTX, OP_XFORM,
21
+ } from './ops.ts'
22
+ import { childSlots } from './child-slots.ts'
23
+ import { leadingTriviaFieldAt } from './field-spec.ts'
24
+ import { regexCanMatchEmpty } from '../regex/first-set.ts'
25
+
26
+ 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, OP_EXPECT, OP_WITHCTX, OP_RULE, OP_ATTEMPT, OP_COV])
28
+
29
+ /**
30
+ * Can the row at `ip` match empty? `true` wherever it cannot tell. A first-set
31
+ * gate row wraps only a child that cannot (`encode.ts` gates nothing nullable).
32
+ */
33
+ export function rowNullable(code: ArrayLike<number>, k: readonly unknown[], ip: number, memo: Map<number, boolean>): boolean {
34
+ const hit = memo.get(ip)
35
+ if (hit !== undefined) return hit
36
+ memo.set(ip, true) // a cycle is answered conservatively
37
+ const op = code[ip]!
38
+ const kids: number[] = []
39
+ let out = true
40
+ if (op === OP_GATE) out = false
41
+ else if (op === OP_LIT || op === OP_LIT_CI) out = (k[code[ip + 1]!] as string).length === 0
42
+ else if (op === OP_RX) { const re = k[code[ip + 1]!]; out = !(re instanceof RegExp) || regexCanMatchEmpty(re.source) }
43
+ else if (op === OP_SEQ || op === OP_SEQV || op === OP_SEQX) {
44
+ childSlots(code, ip, kids)
45
+ out = kids.every(c => rowNullable(code, k, c, memo))
46
+ } else if (op === OP_CHOICE) {
47
+ childSlots(code, ip, kids)
48
+ out = kids.length === 0 || kids.some(c => rowNullable(code, k, c, memo))
49
+ } else if (op === OP_REP || op === OP_REPV) {
50
+ out = code[ip + 2] === 0 || rowNullable(code, k, code[ip + 1]!, memo)
51
+ } else if (WRAPPERS.has(op)) {
52
+ if (childSlots(code, ip, kids) && kids.length === 1) out = rowNullable(code, k, kids[0]!, memo)
53
+ }
54
+ memo.set(ip, out)
55
+ return out
56
+ }
57
+
58
+ /** Term `i` of the sequence at `seq` (terms from `base`, `n` of them) holds its gap. */
59
+ export function holdsGap(
60
+ code: ArrayLike<number>, k: readonly unknown[], base: number, n: number, i: number, memo: Map<number, boolean>,
61
+ ): boolean {
62
+ if (i < 1 || i + 1 >= n) return false
63
+ const term = code[base + i]!
64
+ if (code[term] !== OP_NOT && code[term] !== OP_PEEK) return false
65
+ const next = code[base + i + 1]!
66
+ if (code[next] === OP_ADJ || leadingTriviaFieldAt(code, k, next)) return false
67
+ return !rowNullable(code, k, next, memo)
68
+ }