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.
- package/dist/analysis/diagnostics.js +1800 -962
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +1929 -1091
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +11 -1
- package/dist/compiler/token-alphabet.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +69 -16
- package/dist/compiler/token-capability.js.map +2 -2
- package/dist/cst/capture-buffer.d.ts +6 -0
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/index.js +1017 -177
- package/dist/index.js.map +3 -3
- package/dist/plugin/index.d.ts +25 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1210 -236
- package/dist/plugin/index.js.map +3 -3
- package/dist/run/index.d.ts +2 -0
- package/dist/run/index.d.ts.map +1 -1
- package/dist/run/index.js +9 -0
- package/dist/run/index.js.map +1 -1
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/emit-assembly.d.ts +1 -1
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/emit.d.ts.map +1 -1
- 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 +42 -0
- package/dist/table/factor-choice.d.ts.map +1 -0
- package/dist/table/fail-clean.d.ts +3 -0
- package/dist/table/fail-clean.d.ts.map +1 -0
- package/dist/table/hold-gap.d.ts +8 -0
- package/dist/table/hold-gap.d.ts.map +1 -0
- package/dist/table/index.js +1370 -259
- package/dist/table/index.js.map +3 -3
- package/dist/table/lex-program.d.ts +7 -3
- package/dist/table/lex-program.d.ts.map +1 -1
- package/dist/table/ops.d.ts +8 -0
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/run-support.d.ts +19 -0
- package/dist/table/run-support.d.ts.map +1 -1
- package/dist/table/scalar-terminal.d.ts +2 -0
- package/dist/table/scalar-terminal.d.ts.map +1 -1
- package/dist/table/site-labels.d.ts +66 -0
- package/dist/table/site-labels.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/combinators/dispatch.ts +12 -3
- package/src/compiler/token-alphabet.ts +57 -17
- package/src/cst/capture-buffer.ts +25 -0
- package/src/plugin/index.ts +230 -35
- package/src/run/index.ts +5 -0
- package/src/table/assemble.ts +252 -62
- package/src/table/child-slots.ts +2 -2
- package/src/table/compile-rule-map.ts +14 -5
- package/src/table/emit-assembly.ts +112 -23
- package/src/table/emit.ts +3 -1
- package/src/table/encode.ts +83 -14
- package/src/table/exec.ts +27 -8
- package/src/table/factor-choice.ts +173 -0
- package/src/table/fail-clean.ts +130 -0
- package/src/table/hold-gap.ts +68 -0
- package/src/table/lex-program.ts +197 -43
- package/src/table/ops.ts +9 -1
- package/src/table/run-support.ts +35 -0
- package/src/table/scalar-terminal.ts +8 -0
- package/src/table/site-labels.ts +135 -4
- 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
|
+
}
|