parseman 0.61.0 → 0.61.2
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/backtrack.d.ts +50 -0
- package/dist/analysis/backtrack.d.ts.map +1 -0
- package/dist/analysis/diagnostics.d.ts +3 -0
- package/dist/analysis/diagnostics.d.ts.map +1 -1
- package/dist/analysis/diagnostics.js +1116 -123
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/analysis/rebuild.d.ts +17 -1
- package/dist/analysis/rebuild.d.ts.map +1 -1
- package/dist/cli/index.js +307 -34
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/dispatch.d.ts +2 -0
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/index.js +31 -25
- package/dist/index.js.map +1 -1
- package/dist/plugin/index.d.ts +3 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +53 -25
- package/dist/plugin/index.js.map +2 -2
- package/dist/table/backtrack-probe.d.ts +41 -0
- package/dist/table/backtrack-probe.d.ts.map +1 -0
- package/dist/table/child-slots.d.ts +10 -0
- package/dist/table/child-slots.d.ts.map +1 -1
- package/dist/table/index.js +21 -15
- package/dist/table/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +5 -1
- package/src/analysis/backtrack.ts +249 -0
- package/src/analysis/diagnostics.ts +4 -0
- package/src/analysis/rebuild.ts +63 -11
- package/src/combinators/dispatch.ts +9 -0
- package/src/coverage.ts +2 -14
- package/src/plugin/index.ts +30 -13
- package/src/table/backtrack-probe.ts +326 -0
- package/src/table/child-slots.ts +30 -14
- package/src/version.ts +1 -1
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE BACKTRACKING PROBE, for the two table engines.
|
|
3
|
+
*
|
|
4
|
+
* This module counts the rewinds visible among the terminals a parse matched, without
|
|
5
|
+
* touching either engine: it rewrites a COPY of a program so that every terminal row
|
|
6
|
+
* reports what it consumed, and runs that copy on the closure engine — or, in tests
|
|
7
|
+
* and benches, on the emitted engine from a build-time module
|
|
8
|
+
* (`bench/emitted-assembly.ts`); nothing here builds code from source. A program that
|
|
9
|
+
* is not rewritten carries no probe row, so an ordinary parse pays nothing for this — no branch, no field, no
|
|
10
|
+
* assembly key. That is the whole reason it is a program rewrite and not a `RunCfg`
|
|
11
|
+
* bit: a bit would still be a second assembly every engine has to know about.
|
|
12
|
+
*
|
|
13
|
+
* WHAT IS COUNTED, per parse (`BacktrackCounts`):
|
|
14
|
+
*
|
|
15
|
+
* - rewinds: a terminal matched starting BELOW the end of the previous matched
|
|
16
|
+
* terminal — the parse went backwards, because a failed arm, an
|
|
17
|
+
* `attempt()`, an `optional()` or a choice restored the position;
|
|
18
|
+
* - bytesReread: of those terminals' bytes, the ones below the furthest byte
|
|
19
|
+
* a matched terminal already reached;
|
|
20
|
+
* - lookaheads / lookaheadBytes: `not()`/`peek()` evaluations, and the bytes
|
|
21
|
+
* terminals matched inside them;
|
|
22
|
+
* - regexLookarounds / regexLookaroundBytes: runs of a regex terminal whose
|
|
23
|
+
* pattern holds a lookaround, and, for a run that matched, the fewest bytes past
|
|
24
|
+
* its match that the same match still needs when the input is cut short. That is
|
|
25
|
+
* a lower bound on what it read: `/a(?!bbbb)/` on `abbbc` reads past its match to
|
|
26
|
+
* decide and counts 0, and every failed run counts 0.
|
|
27
|
+
*
|
|
28
|
+
* A terminal is a row that reads input itself: `LIT`, `RX`, `LIT_CI`, `LEX_BODY`,
|
|
29
|
+
* `LEX_PROGRAM`, `SCAN`, `LIVE` and their line-tracking twins. Trivia between terms
|
|
30
|
+
* is not a terminal, and a lexical body or a scan is one read. `routed()` reads
|
|
31
|
+
* nothing: the `dispatch()` selector consumed the token.
|
|
32
|
+
*
|
|
33
|
+
* WHAT A ZERO DOES NOT MEAN. Zero rewinds says no matched terminal started behind the
|
|
34
|
+
* one before it. It does not say the parse wasted no work. Not counted:
|
|
35
|
+
*
|
|
36
|
+
* - failed terminal reads. Only a match reports. On 2,000 `a`s,
|
|
37
|
+
* `many(choice(regex(/[a-z]+;/), regex(/[a-z]/)))` scans about 2M characters in
|
|
38
|
+
* regexes that fail, and every count is 0 on both engines;
|
|
39
|
+
* - trivia re-scans: the same gap skipped again after a position is restored;
|
|
40
|
+
* - the choice predecide pretest, which reads an arm's leading terminal before the
|
|
41
|
+
* arm runs;
|
|
42
|
+
* - table `token()` bodies. A `token()` the table lowers to one `LEX_BODY` or
|
|
43
|
+
* `LEX_PROGRAM` row is one read, and its retries are hidden inside that row. On the
|
|
44
|
+
* same input, `many(token(choice(sequence(regex(/[a-z]+/), literal(';')),
|
|
45
|
+
* regex(/[a-z]/))))` reports 2,000 rewinds on the interpreter and 0 on the table,
|
|
46
|
+
* which does the same retries. A `token()` that does not lower still reports its
|
|
47
|
+
* terminals.
|
|
48
|
+
*
|
|
49
|
+
* HOW, row by row. A terminal `T` in a parent's slot becomes `XFORM(report, T)`,
|
|
50
|
+
* which hands the recorder `T`'s span and returns `T`'s value unchanged. A
|
|
51
|
+
* lookahead needs to know it is one, so `not(c)` becomes
|
|
52
|
+
*
|
|
53
|
+
* ARMGATE(enter, XFORM(exit, NOT(XFORM(exit, c))))
|
|
54
|
+
*
|
|
55
|
+
* whose `exit` runs once whether `c` matches or not, and `peek(c)` becomes `NOT`
|
|
56
|
+
* of that. A regex with a lookaround becomes `ARMGATE(attempt, XFORM(match, RX))`.
|
|
57
|
+
* No gadget is a sequence: a sequence skips trivia between its terms, which would
|
|
58
|
+
* move where the rewritten row ends.
|
|
59
|
+
*
|
|
60
|
+
* A scan reads its stop and skip subtrees as part of one read, and those may be
|
|
61
|
+
* rows the grammar shares. The rewrite gives the scan pool its own copy of them,
|
|
62
|
+
* left as it was, so the scan reports once, as the terminal it is.
|
|
63
|
+
*
|
|
64
|
+
* Every row there is one both engines already run. A rewritten parse must return
|
|
65
|
+
* what the plain one returns; `backtrackProbe` checks that on every parse it measures.
|
|
66
|
+
*/
|
|
67
|
+
import {
|
|
68
|
+
OP_ARMGATE, OP_GREEDY, OP_LEX_BODY, OP_LEX_PROGRAM, OP_LIT, OP_LIT_CI, OP_LIT_CI_TRACK,
|
|
69
|
+
OP_LIT_TRACK, OP_LIVE, OP_NOT, OP_PEEK, OP_RX, OP_RX_TRACK, OP_SCAN, OP_XFORM,
|
|
70
|
+
} from './ops.ts'
|
|
71
|
+
import { childSlotOffsets, childSlots } from './child-slots.ts'
|
|
72
|
+
import {
|
|
73
|
+
assemblyExtraIps, expandCompact, type CompactProgram, type SubtreeRef, type TableProgram,
|
|
74
|
+
} from './program.ts'
|
|
75
|
+
import type { RunCfg } from './assemble.ts'
|
|
76
|
+
|
|
77
|
+
export type BacktrackCounts = {
|
|
78
|
+
rewinds: number
|
|
79
|
+
bytesReread: number
|
|
80
|
+
lookaheads: number
|
|
81
|
+
lookaheadBytes: number
|
|
82
|
+
regexLookarounds: number
|
|
83
|
+
regexLookaroundBytes: number
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const LOOKAROUND = /(?<!\\)\(\?<?[=!]/
|
|
87
|
+
|
|
88
|
+
/** Does this pattern read input it does not consume? */
|
|
89
|
+
export function hasLookaround(re: RegExp): boolean {
|
|
90
|
+
return LOOKAROUND.test(re.source)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* How far past `end` a regex match at `start` depended on the input: the shortest
|
|
95
|
+
* extension of the input past `end` under which the match still ends at `end`.
|
|
96
|
+
* A lower bound for a negative lookaround, which is decided by what does NOT
|
|
97
|
+
* follow and so can hold on a shorter input than it read.
|
|
98
|
+
*/
|
|
99
|
+
function lookaroundExtent(re: RegExp, input: string, start: number, end: number): number {
|
|
100
|
+
const probe = new RegExp(re.source, re.flags.includes('y') ? re.flags : `${re.flags}y`)
|
|
101
|
+
const same = (n: number): boolean => {
|
|
102
|
+
probe.lastIndex = start
|
|
103
|
+
return probe.test(input.slice(0, end + n)) && probe.lastIndex === end
|
|
104
|
+
}
|
|
105
|
+
let lo = 0
|
|
106
|
+
let hi = input.length - end
|
|
107
|
+
while (lo < hi) {
|
|
108
|
+
const mid = (lo + hi) >>> 1
|
|
109
|
+
if (same(mid)) hi = mid
|
|
110
|
+
else lo = mid + 1
|
|
111
|
+
}
|
|
112
|
+
return lo
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The counters one probed parse writes. Shared by both table engines and the
|
|
117
|
+
* interpreter probe, so all three count by the same rule.
|
|
118
|
+
*/
|
|
119
|
+
export class BacktrackRecorder {
|
|
120
|
+
input = ''
|
|
121
|
+
private counts: BacktrackCounts = zeroCounts()
|
|
122
|
+
private look = 0
|
|
123
|
+
private opaque = 0
|
|
124
|
+
private last = 0
|
|
125
|
+
private furthest = 0
|
|
126
|
+
|
|
127
|
+
begin(input: string): void {
|
|
128
|
+
this.input = input
|
|
129
|
+
this.counts = zeroCounts()
|
|
130
|
+
this.look = 0
|
|
131
|
+
this.opaque = 0
|
|
132
|
+
this.last = 0
|
|
133
|
+
this.furthest = 0
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The counts for the parse `begin` opened. Throws if a region was left open. */
|
|
137
|
+
end(): BacktrackCounts {
|
|
138
|
+
if (this.look !== 0 || this.opaque !== 0) {
|
|
139
|
+
throw new Error(`backtrack probe: unbalanced region (lookahead ${this.look}, opaque ${this.opaque})`)
|
|
140
|
+
}
|
|
141
|
+
return this.counts
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
consume(start: number, end: number): void {
|
|
145
|
+
if (this.opaque > 0 || end <= start) return
|
|
146
|
+
const c = this.counts
|
|
147
|
+
if (this.look > 0) { c.lookaheadBytes += end - start; return }
|
|
148
|
+
if (start < this.last) c.rewinds++
|
|
149
|
+
if (start < this.furthest) c.bytesReread += Math.min(end, this.furthest) - start
|
|
150
|
+
this.last = end
|
|
151
|
+
if (end > this.furthest) this.furthest = end
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
enterLookahead(): void {
|
|
155
|
+
if (this.opaque === 0) this.counts.lookaheads++
|
|
156
|
+
this.look++
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
exitLookahead(): void { this.look-- }
|
|
160
|
+
|
|
161
|
+
regexAttempt(): void {
|
|
162
|
+
if (this.opaque === 0) this.counts.regexLookarounds++
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
regexMatch(re: RegExp, start: number, end: number): void {
|
|
166
|
+
if (this.opaque === 0) this.counts.regexLookaroundBytes += lookaroundExtent(re, this.input, start, end)
|
|
167
|
+
this.consume(start, end)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A read whose inner steps are not the grammar's (the interpreter's scans). */
|
|
171
|
+
enterOpaque(): void { this.opaque++ }
|
|
172
|
+
exitOpaque(): void { this.opaque-- }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function zeroCounts(): BacktrackCounts {
|
|
176
|
+
return { rewinds: 0, bytesReread: 0, lookaheads: 0, lookaheadBytes: 0, regexLookarounds: 0, regexLookaroundBytes: 0 }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type Span = { readonly start: number; readonly end: number }
|
|
180
|
+
|
|
181
|
+
/** Every row start reachable from `roots`, ascending, closed by `code.length`. */
|
|
182
|
+
function rowStarts(code: ArrayLike<number>, roots: Iterable<number>): number[] {
|
|
183
|
+
const seen = new Set<number>()
|
|
184
|
+
const stack = [...roots]
|
|
185
|
+
const kids: number[] = []
|
|
186
|
+
while (stack.length > 0) {
|
|
187
|
+
const ip = stack.pop()!
|
|
188
|
+
if (seen.has(ip)) continue
|
|
189
|
+
seen.add(ip)
|
|
190
|
+
kids.length = 0
|
|
191
|
+
childSlots(code, ip, kids)
|
|
192
|
+
for (const c of kids) stack.push(c)
|
|
193
|
+
}
|
|
194
|
+
return [...seen, code.length].sort((a, b) => a - b)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** A copy of `source` whose terminals and lookaheads report to `rec`. */
|
|
198
|
+
export function probeProgram(source: TableProgram | CompactProgram, rec: BacktrackRecorder): TableProgram {
|
|
199
|
+
const prog = expandCompact(source)
|
|
200
|
+
const code = [...prog.code]
|
|
201
|
+
const fns: unknown[] = [...prog.fns]
|
|
202
|
+
const fx: (readonly string[])[] = [...prog.fx, []]
|
|
203
|
+
const noFx = fx.length - 1
|
|
204
|
+
const row = (...words: number[]): number => {
|
|
205
|
+
const ip = code.length
|
|
206
|
+
code.push(...words)
|
|
207
|
+
return ip
|
|
208
|
+
}
|
|
209
|
+
const fn = (f: unknown): number => fns.push(f) - 1
|
|
210
|
+
const slots: number[] = []
|
|
211
|
+
|
|
212
|
+
// A scan's subtrees run as part of ONE read, and may be rows the grammar also
|
|
213
|
+
// reaches. They get their own uninstrumented copy, so the rewrite below never
|
|
214
|
+
// reaches them through a row it shares with the grammar.
|
|
215
|
+
const extraIps = assemblyExtraIps(prog)
|
|
216
|
+
const starts = rowStarts(prog.code, [...Object.values(prog.rules), ...extraIps])
|
|
217
|
+
const copies = new Map<number, number>()
|
|
218
|
+
function copyOf(ip: number): number {
|
|
219
|
+
const hit = copies.get(ip)
|
|
220
|
+
if (hit !== undefined) return hit
|
|
221
|
+
slots.length = 0
|
|
222
|
+
childSlotOffsets(prog.code, ip, slots)
|
|
223
|
+
if (slots.length === 0) { copies.set(ip, ip); return ip }
|
|
224
|
+
// Rows are laid out end to end, so a row runs to the next row that starts.
|
|
225
|
+
const next = starts[starts.findIndex(s => s > ip)]!
|
|
226
|
+
const at = code.length
|
|
227
|
+
copies.set(ip, at)
|
|
228
|
+
for (let i = ip; i < next; i++) code.push(prog.code[i]!)
|
|
229
|
+
const own = slots.map(s => s - ip)
|
|
230
|
+
for (const o of own) code[at + o] = copyOf(prog.code[ip + o]!)
|
|
231
|
+
return at
|
|
232
|
+
}
|
|
233
|
+
const ref = (r: SubtreeRef): SubtreeRef => [copyOf(r[0]), r[1]]
|
|
234
|
+
const scans = prog.scans?.map(s => ({
|
|
235
|
+
...s,
|
|
236
|
+
skip: s.skip.map(ref),
|
|
237
|
+
...(s.sentinel === undefined ? {} : { sentinel: ref(s.sentinel) }),
|
|
238
|
+
...(s.recoverAt === undefined ? {} : { recoverAt: ref(s.recoverAt) }),
|
|
239
|
+
...(s.stopAt === undefined ? {} : { stopAt: ref(s.stopAt) }),
|
|
240
|
+
...(s.prefix === undefined ? {} : { prefix: ref(s.prefix) }),
|
|
241
|
+
...(s.body === undefined ? {} : { body: ref(s.body) }),
|
|
242
|
+
...(s.first === undefined ? {} : { first: ref(s.first) }),
|
|
243
|
+
...(s.lazy === undefined ? {} : { lazy: ref(s.lazy) }),
|
|
244
|
+
}))
|
|
245
|
+
const scanSkip = prog.scanSkip?.map(set => set.map(ref))
|
|
246
|
+
|
|
247
|
+
const report = fn((v: unknown, span: Span) => { rec.consume(span.start, span.end); return v })
|
|
248
|
+
const lookIn = fn(() => { rec.enterLookahead(); return true })
|
|
249
|
+
const lookOut = fn((v: unknown) => { rec.exitLookahead(); return v })
|
|
250
|
+
const regexTry = fn(() => { rec.regexAttempt(); return true })
|
|
251
|
+
|
|
252
|
+
// `exit` runs once on each outcome: inside the NOT when the child matched (the
|
|
253
|
+
// NOT then fails), outside it when the NOT holds — including when its lead
|
|
254
|
+
// gate holds without running the child.
|
|
255
|
+
const lookahead = (child: number, fxi: number): number => row(OP_ARMGATE, lookIn,
|
|
256
|
+
row(OP_XFORM, lookOut, row(OP_NOT, row(OP_XFORM, lookOut, replacement(child) ?? child), fxi)), noFx)
|
|
257
|
+
|
|
258
|
+
const replaced = new Map<number, number>()
|
|
259
|
+
function replacement(c: number): number | undefined {
|
|
260
|
+
const hit = replaced.get(c)
|
|
261
|
+
if (hit !== undefined) return hit
|
|
262
|
+
let r: number | undefined
|
|
263
|
+
switch (code[c]) {
|
|
264
|
+
case OP_LIT: case OP_LIT_TRACK: case OP_LIT_CI: case OP_LIT_CI_TRACK:
|
|
265
|
+
case OP_LEX_BODY: case OP_LEX_PROGRAM: case OP_SCAN: case OP_LIVE:
|
|
266
|
+
r = row(OP_XFORM, report, c)
|
|
267
|
+
break
|
|
268
|
+
case OP_RX: case OP_RX_TRACK: {
|
|
269
|
+
const re = prog.k[code[c + 1]!] as RegExp
|
|
270
|
+
r = hasLookaround(re)
|
|
271
|
+
? row(OP_ARMGATE, regexTry,
|
|
272
|
+
row(OP_XFORM, fn((v: unknown, span: Span) => { rec.regexMatch(re, span.start, span.end); return v }), c), noFx)
|
|
273
|
+
: row(OP_XFORM, report, c)
|
|
274
|
+
break
|
|
275
|
+
}
|
|
276
|
+
case OP_NOT:
|
|
277
|
+
r = lookahead(code[c + 1]!, code[c + 2]!)
|
|
278
|
+
break
|
|
279
|
+
case OP_PEEK:
|
|
280
|
+
r = row(OP_NOT, lookahead(code[c + 1]!, code[c + 2]!), code[c + 2]!)
|
|
281
|
+
break
|
|
282
|
+
}
|
|
283
|
+
if (r !== undefined) replaced.set(c, r)
|
|
284
|
+
return r
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const rules: Record<string, number> = {}
|
|
288
|
+
const stack: number[] = []
|
|
289
|
+
for (const [name, ip] of Object.entries(prog.rules)) {
|
|
290
|
+
rules[name] = replacement(ip) ?? ip
|
|
291
|
+
stack.push(ip)
|
|
292
|
+
}
|
|
293
|
+
const seen = new Set<number>()
|
|
294
|
+
while (stack.length > 0) {
|
|
295
|
+
const ip = stack.pop()!
|
|
296
|
+
if (seen.has(ip)) continue
|
|
297
|
+
seen.add(ip)
|
|
298
|
+
slots.length = 0
|
|
299
|
+
if (!childSlotOffsets(code, ip, slots)) throw new Error(`backtrack probe: unknown opcode ${String(code[ip])} at ${ip}`)
|
|
300
|
+
for (let i = 0; i < slots.length; i++) {
|
|
301
|
+
const s = slots[i]!
|
|
302
|
+
const c = code[s]!
|
|
303
|
+
stack.push(c)
|
|
304
|
+
// A keyword classifier re-runs the literal arm its super arm already read;
|
|
305
|
+
// that is classification, not a failed alternative, so only the super arm
|
|
306
|
+
// reports. Its arms are linked, never decoded, so leaving them is exact.
|
|
307
|
+
if (code[ip] === OP_GREEDY && i > 0) continue
|
|
308
|
+
const r = replacement(c)
|
|
309
|
+
if (r !== undefined) code[s] = r
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// A fresh object, not a spread: a spread would carry the source program's
|
|
314
|
+
// compiler-only caches, which describe its rows, not these.
|
|
315
|
+
const out = Object.fromEntries(Object.entries(prog)) as unknown as TableProgram
|
|
316
|
+
return {
|
|
317
|
+
...out, code, fns, fx, rules, asm: [],
|
|
318
|
+
...(scans === undefined ? {} : { scans }),
|
|
319
|
+
...(scanSkip === undefined ? {} : { scanSkip }),
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** The option set a probe runs a program under: a plain parse of its host mode. */
|
|
324
|
+
export function probeCfg(prog: TableProgram): RunCfg {
|
|
325
|
+
return { hostCst: prog.hostMode === 'cst', trackLines: prog.lines === 1, tolerant: false, coverage: false, probe: false }
|
|
326
|
+
}
|
package/src/table/child-slots.ts
CHANGED
|
@@ -33,7 +33,23 @@ import {
|
|
|
33
33
|
* report), `site-labels.ts` ignores it (an unwalked site resolves to `TOP` via
|
|
34
34
|
* `labelAt`, and the enclosing assembly is unemittable anyway).
|
|
35
35
|
*/
|
|
36
|
+
/**
|
|
37
|
+
* The child instructions of the row at `ip`, read through `childSlotOffsets` below:
|
|
38
|
+
* one edge table, two views of it.
|
|
39
|
+
*/
|
|
36
40
|
export function childSlots(code: ArrayLike<number>, ip: number, out: number[]): boolean {
|
|
41
|
+
const from = out.length
|
|
42
|
+
if (!childSlotOffsets(code, ip, out)) return false
|
|
43
|
+
for (let i = from; i < out.length; i++) out[i] = code[out[i]!]!
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The OPERAND OFFSETS that hold child instructions — the edge table itself. A caller
|
|
49
|
+
* that rewrites a program's edges (`backtrack-probe.ts`) needs where an edge is
|
|
50
|
+
* stored, not only where it points.
|
|
51
|
+
*/
|
|
52
|
+
export function childSlotOffsets(code: ArrayLike<number>, ip: number, out: number[]): boolean {
|
|
37
53
|
switch (code[ip]) {
|
|
38
54
|
// ZERO-WIDTH / TERMINAL ROWS — no successor. `OP_GUARD` and `OP_ADJ` are both
|
|
39
55
|
// childless tests. `OP_LIVE` holds a hand-written combinator in `prog.fns`; its
|
|
@@ -48,67 +64,67 @@ export function childSlots(code: ArrayLike<number>, ip: number, out: number[]):
|
|
|
48
64
|
// `opHistogram` throw on every table the coverage build produces.
|
|
49
65
|
case OP_RULE: case OP_OPT: case OP_NOT: case OP_PEEK: case OP_EXPECT:
|
|
50
66
|
case OP_TOKEN: case OP_ATTEMPT: case OP_LABEL: case OP_COV: case OP_REJECT: case OP_GAPGATE:
|
|
51
|
-
out.push(
|
|
67
|
+
out.push(ip + 1)
|
|
52
68
|
return true
|
|
53
69
|
|
|
54
70
|
// Single child at `ip + 2`.
|
|
55
71
|
case OP_GATE: case OP_SCOPE: case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_XFORM:
|
|
56
72
|
case OP_NODE: case OP_NODE_TRACK: case OP_FIELD: case OP_LEAF: case OP_SOURCE_LEAF: case OP_ARMGATE: case OP_RGATE: case OP_RGATE2:
|
|
57
|
-
out.push(
|
|
73
|
+
out.push(ip + 2)
|
|
58
74
|
return true
|
|
59
75
|
|
|
60
76
|
// `STRICT strict tolerant` — both encodings are reachable; an assembly keeps one.
|
|
61
77
|
case OP_STRICT:
|
|
62
|
-
out.push(
|
|
78
|
+
out.push(ip + 1, ip + 2)
|
|
63
79
|
return true
|
|
64
80
|
|
|
65
81
|
// `ROUTED fallback` — the operand is an offset or −1. The routed token itself is
|
|
66
82
|
// data the enclosing `dispatch()` already consumed, so the only edge is the
|
|
67
83
|
// fallback.
|
|
68
84
|
case OP_ROUTED:
|
|
69
|
-
if (code[ip + 1]! >= 0) out.push(
|
|
85
|
+
if (code[ip + 1]! >= 0) out.push(ip + 1)
|
|
70
86
|
return true
|
|
71
87
|
|
|
72
88
|
// `DISPATCH sel d other otherRouted n a1 … an` — the SELECTOR is a child on
|
|
73
89
|
// exactly the same footing as the arms: it runs inside whatever node and scope
|
|
74
90
|
// this site sits in.
|
|
75
91
|
case OP_DISPATCH: {
|
|
76
|
-
out.push(
|
|
77
|
-
if (code[ip + 3]! >= 0) out.push(
|
|
92
|
+
out.push(ip + 1)
|
|
93
|
+
if (code[ip + 3]! >= 0) out.push(ip + 3)
|
|
78
94
|
const n = code[ip + 5]!
|
|
79
|
-
for (let i = 0; i < n; i++) out.push(
|
|
95
|
+
for (let i = 0; i < n; i++) out.push(ip + 6 + i)
|
|
80
96
|
return true
|
|
81
97
|
}
|
|
82
98
|
|
|
83
99
|
case OP_SEQ: case OP_SEQV: {
|
|
84
100
|
const n = code[ip + 1]!
|
|
85
|
-
for (let i = 0; i < n; i++) out.push(
|
|
101
|
+
for (let i = 0; i < n; i++) out.push(ip + 2 + i)
|
|
86
102
|
return true
|
|
87
103
|
}
|
|
88
104
|
|
|
89
105
|
case OP_SEQX: {
|
|
90
106
|
const n = code[ip + 2]!
|
|
91
|
-
for (let i = 0; i < n; i++) out.push(
|
|
107
|
+
for (let i = 0; i < n; i++) out.push(ip + 3 + i)
|
|
92
108
|
return true
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
case OP_CHOICE: {
|
|
96
112
|
const n = code[ip + 2]!
|
|
97
|
-
for (let i = 0; i < n; i++) out.push(
|
|
113
|
+
for (let i = 0; i < n; i++) out.push(ip + 4 + i)
|
|
98
114
|
return true
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
// Arms are stride-2 from `ip + 4` — the gate word sits beside each arm offset.
|
|
102
118
|
case OP_GREEDY: {
|
|
103
|
-
out.push(
|
|
119
|
+
out.push(ip + 1)
|
|
104
120
|
const n = code[ip + 2]!
|
|
105
|
-
for (let i = 0; i < n; i++) out.push(
|
|
121
|
+
for (let i = 0; i < n; i++) out.push(ip + 4 + 2 * i)
|
|
106
122
|
return true
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
case OP_REP: case OP_REPV:
|
|
110
|
-
out.push(
|
|
111
|
-
if (code[ip + 4]! >= 0) out.push(
|
|
126
|
+
out.push(ip + 1)
|
|
127
|
+
if (code[ip + 4]! >= 0) out.push(ip + 4)
|
|
112
128
|
return true
|
|
113
129
|
|
|
114
130
|
default:
|
package/src/version.ts
CHANGED