parseman 0.59.0 → 0.59.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.
Files changed (65) hide show
  1. package/README.md +5 -4
  2. package/dist/analysis/diagnostics.js +19 -45
  3. package/dist/analysis/diagnostics.js.map +1 -1
  4. package/dist/cli/index.js +19 -45
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/combinators/choice.d.ts +0 -5
  7. package/dist/combinators/choice.d.ts.map +1 -1
  8. package/dist/combinators/scanTo.d.ts +1 -16
  9. package/dist/combinators/scanTo.d.ts.map +1 -1
  10. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  11. package/dist/compiler/linker.d.ts +0 -3
  12. package/dist/compiler/linker.d.ts.map +1 -1
  13. package/dist/compiler/token-alphabet.d.ts +5 -25
  14. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  15. package/dist/compiler/token-capability.js.map +1 -1
  16. package/dist/compiler/token-dispatch.d.ts +8 -79
  17. package/dist/compiler/token-dispatch.d.ts.map +1 -1
  18. package/dist/cst/root-trivia-scope.d.ts +0 -3
  19. package/dist/cst/root-trivia-scope.d.ts.map +1 -1
  20. package/dist/cst/trivia-entries.d.ts +0 -2
  21. package/dist/cst/trivia-entries.d.ts.map +1 -1
  22. package/dist/cst/trivia-kinds.d.ts +0 -5
  23. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  24. package/dist/index.js +20 -46
  25. package/dist/index.js.map +1 -1
  26. package/dist/language-service/index.js.map +1 -1
  27. package/dist/plugin/index.js +20 -46
  28. package/dist/plugin/index.js.map +1 -1
  29. package/dist/run/index.js.map +1 -1
  30. package/dist/spec/index.js.map +1 -1
  31. package/dist/spec/model.d.ts +0 -8
  32. package/dist/spec/model.d.ts.map +1 -1
  33. package/dist/table/encode.d.ts.map +1 -1
  34. package/dist/table/index.js +19 -45
  35. package/dist/table/index.js.map +1 -1
  36. package/dist/table/ops.d.ts +6 -6
  37. package/dist/table/ops.d.ts.map +1 -1
  38. package/dist/table/program.d.ts +0 -1
  39. package/dist/table/program.d.ts.map +1 -1
  40. package/dist/table/scalar-terminal.d.ts +0 -7
  41. package/dist/table/scalar-terminal.d.ts.map +1 -1
  42. package/dist/table/scan-shapes.d.ts +0 -8
  43. package/dist/table/scan-shapes.d.ts.map +1 -1
  44. package/dist/table/site-labels.d.ts +0 -2
  45. package/dist/table/site-labels.d.ts.map +1 -1
  46. package/dist/version.d.ts +1 -1
  47. package/package.json +1 -1
  48. package/src/combinators/choice.ts +1 -1
  49. package/src/combinators/scanTo.ts +1 -1
  50. package/src/compiler/ir-serialize.ts +2 -1
  51. package/src/compiler/linker.ts +1 -1
  52. package/src/compiler/token-alphabet.ts +14 -10
  53. package/src/compiler/token-dispatch.ts +10 -313
  54. package/src/compiler/token-scanner.ts +2 -2
  55. package/src/cst/root-trivia-scope.ts +1 -1
  56. package/src/cst/trivia-entries.ts +1 -1
  57. package/src/cst/trivia-kinds.ts +1 -1
  58. package/src/spec/model.ts +1 -1
  59. package/src/table/encode.ts +16 -39
  60. package/src/table/ops.ts +6 -6
  61. package/src/table/program.ts +1 -1
  62. package/src/table/scalar-terminal.ts +1 -1
  63. package/src/table/scan-shapes.ts +15 -46
  64. package/src/table/site-labels.ts +1 -1
  65. package/src/version.ts +1 -1
@@ -1,83 +1,12 @@
1
1
  /**
2
- * Derived tokenization, part 3: token-KEYED dispatch.
2
+ * Packing and case-fold helpers for the dormant token scanner (`token-scanner.ts`).
3
3
  *
4
- * A `dispatch()` used to emit, per case, a chained condition that re-derived the
5
- * key from the selector's STRING one character at a time:
6
- *
7
- * if (_dkey.length === 6 && _dkey.charCodeAt(0) === 64 && (_dkey.charCodeAt(1) | 32) === 108 && ...)
8
- * else if (_dkey.length === 15 && ...)
9
- *
10
- * Measured on the built css artifact: 40,269 B of 3,336,650 B was exactly these
11
- * chains, 10,499 B of it inside `_r_StylesheetAtRule` alone.
12
- *
13
- * Instead the selector's matched span is walked to a small integer case id, and
14
- * the cases select on the integer. Every id strategy below is BUILT, not
15
- * reasoned about — `PARSEMAN_DISPATCH=<id>:<sel>` picks one so they can be
16
- * measured on the shipped artifact rather than in a microbenchmark, which
17
- * already misled us once: a standalone "table" looked slow because that
18
- * implementation sliced and lowercased and hashed, not because table dispatch
19
- * is slow.
20
- *
21
- * Every strategy is "compute a candidate case cheaply, then VERIFY against the
22
- * packed key text", except `trie`, whose walk is already exact.
23
- */
24
-
25
- /** One dispatch case's keys, in emission order. Case ids are 1-based; 0 = no match. */
26
- export type DispatchKeySet = { keys: readonly string[]; caseInsensitive: boolean }
27
-
28
- export type IdStrategy = 'trie' | 'lenswitch' | 'firstchar' | 'phash'
29
- export type ArmSelection = 'ifchain' | 'switch'
30
-
31
- export type DispatchConfig = { id: IdStrategy | 'auto'; sel: ArmSelection }
32
-
33
- /**
34
- * `PARSEMAN_DISPATCH=<id>:<sel>`. The default is the MEASURED winner on the
35
- * shipped css artifact, not a preference. All five configurations were built
36
- * into this emitter and compared on the real artifact with the trees diffed for
37
- * equality, interleaved in one process (medians of 31 rounds):
38
- *
39
- * config ms/parse rel raw B gzip B
40
- * chain 6.092 1.000 3,336,650 426,247
41
- * trie:ifchain 5.967 0.979 3,311,657 424,465 <- default
42
- * trie:switch 5.945 0.976 3,333,421 424,895
43
- * phash:switch 6.011 0.987 3,331,361 424,637
44
- *
45
- * `firstchar` and `lenswitch` BUILD but are inapplicable to css's at-keyword
46
- * sets — every key starts with `@`, and the lengths collide — so they fall back
47
- * and emit the chain. `phash` finds an injective hash (the search works) but
48
- * costs more table bytes than the trie saves. `switch` arm selection is within
49
- * noise of the chain of integer compares at 3 sites x 8 cases; its larger raw
50
- * size is the downstream formatter indenting case bodies, which is why gzip is
51
- * the metric that decides here.
52
- *
53
- * The speed spread across ALL configurations is 2.4% — dispatch keying is not
54
- * where css parse time goes.
4
+ * This module once also emitted token-keyed `dispatch()` id strategies (trie,
5
+ * length switch, first char, perfect hash) for the source lowering deleted in
6
+ * 0.47.0. Those strategies, their `PARSEMAN_DISPATCH` selector and their
7
+ * measurements were removed with nothing left calling them; they last existed at
8
+ * `4a5d0341`. What remains is what `token-scanner.ts` imports.
55
9
  */
56
- export function dispatchConfigFromEnv(env: Record<string, string | undefined>): DispatchConfig {
57
- const raw = env.PARSEMAN_DISPATCH
58
- if (raw === undefined || raw === '') return { id: 'trie', sel: 'ifchain' }
59
- const [idPart, selPart] = raw.split(':')
60
- const id = (['trie', 'lenswitch', 'firstchar', 'phash', 'auto'] as const).find(s => s === idPart) ?? 'auto'
61
- const sel: ArmSelection = selPart === 'ifchain' ? 'ifchain' : 'switch'
62
- return { id, sel }
63
- }
64
-
65
- /** Flattened (key, caseIndex) pairs, folded when the case is case-insensitive. */
66
- type FlatKey = { text: string; caseIndex: number }
67
-
68
- function flatten(cases: ReadonlyArray<DispatchKeySet>): FlatKey[] | null {
69
- const out: FlatKey[] = []
70
- for (const [i, c] of cases.entries()) {
71
- for (const key of c.keys) {
72
- if (key.length === 0) return null
73
- // A case-SENSITIVE key containing an ASCII letter cannot share a folded
74
- // comparison with the rest; refuse rather than accept the wrong case.
75
- if (!c.caseInsensitive && /[a-zA-Z]/.test(key)) return null
76
- out.push({ text: foldText(key), caseIndex: i + 1 })
77
- }
78
- }
79
- return out.length === 0 ? null : out
80
- }
81
10
 
82
11
  /**
83
12
  * ASCII letter-only case fold. Used IDENTICALLY when building the tables and
@@ -88,11 +17,6 @@ function flatten(cases: ReadonlyArray<DispatchKeySet>): FlatKey[] | null {
88
17
  */
89
18
  export const foldExpr = (c: string): string => `(${c} >= 65 && ${c} <= 90 ? ${c} + 32 : ${c})`
90
19
  export function foldCode(c: number): number { return c >= 65 && c <= 90 ? c + 32 : c }
91
- function foldText(t: string): string {
92
- let out = ''
93
- for (let i = 0; i < t.length; i++) out += String.fromCharCode(foldCode(t.charCodeAt(i)))
94
- return out
95
- }
96
20
 
97
21
  /**
98
22
  * Pack a non-negative int array into a two-chars-per-value string literal.
@@ -103,7 +27,7 @@ function foldText(t: string): string {
103
27
  * this is the ONLY implementation: a second copy in token-scanner.ts had the same
104
28
  * encoding and the same missing check, which is how one defect became two.
105
29
  */
106
- export const PACK_MAX = 4095
30
+ const PACK_MAX = 4095
107
31
  export function packInts(values: readonly number[]): string {
108
32
  let s = ''
109
33
  for (const v of values) {
@@ -118,236 +42,9 @@ export function packInts(values: readonly number[]): string {
118
42
  return JSON.stringify(s)
119
43
  }
120
44
 
121
- /* ------------------------------------------------------------------ *
122
- * Shared runtime helpers, emitted ONCE per artifact however many sites
123
- * use them. That is what lets tables shrink the artifact instead of
124
- * growing it: only the small per-site data is duplicated.
125
- * ------------------------------------------------------------------ */
126
-
127
- export type SharedHelper = 'unpack' | 'verify' | 'trie' | 'lenswitch' | 'firstchar' | 'phash'
128
-
129
- export function sharedHelperDecl(kind: SharedHelper, n: (h: SharedHelper) => string): string {
130
- switch (kind) {
131
- case 'unpack':
132
- return `const ${n('unpack')} = s => { const o = new Int32Array(s.length >> 1); for (let i = 0; i < o.length; i++) o[i] = (s.charCodeAt(i * 2) - 35) | ((s.charCodeAt(i * 2 + 1) - 35) << 6); return o }`
133
- case 'verify':
134
- return [
135
- `function ${n('verify')}(input, from, to, keys, ko, kl) {`,
136
- ` if (to - from !== kl) return false`,
137
- ` for (let i = 0; i < kl; i++) { const c = input.charCodeAt(from + i); if (${foldExpr('c')} !== keys.charCodeAt(ko + i)) return false }`,
138
- ` return true`,
139
- `}`,
140
- ].join('\n')
141
- case 'trie':
142
- return [
143
- `function ${n('trie')}(input, from, to, ch, st, nx, ac) {`,
144
- ` let node = 0`,
145
- ` for (let i = from; i < to; i++) {`,
146
- ` const c0 = input.charCodeAt(i), c = ${foldExpr('c0')}`,
147
- ` let t = -1`,
148
- ` for (let e = st[node], z = st[node + 1]; e < z; e++) if (ch.charCodeAt(e) === c) { t = nx[e]; break }`,
149
- ` if (t < 0) return 0`,
150
- ` node = t`,
151
- ` }`,
152
- ` return ac[node]`,
153
- `}`,
154
- ].join('\n')
155
- case 'lenswitch':
156
- return [
157
- `function ${n('lenswitch')}(input, from, to, lens, cix, keys, kos, kls) {`,
158
- ` const len = to - from`,
159
- ` for (let i = 0; i < lens.length; i++) {`,
160
- ` if (lens[i] !== len) continue`,
161
- ` if (${n('verify')}(input, from, to, keys, kos[i], kls[i])) return cix[i]`,
162
- ` }`,
163
- ` return 0`,
164
- `}`,
165
- ].join('\n')
166
- case 'firstchar':
167
- return [
168
- `function ${n('firstchar')}(input, from, to, tab, cix, keys, kos, kls) {`,
169
- ` const c0 = input.charCodeAt(from), c = ${foldExpr('c0')}`,
170
- ` if (c > 127) return 0`,
171
- ` let i = tab[c]`,
172
- ` if (i === 0) return 0`,
173
- ` i--`,
174
- ` return ${n('verify')}(input, from, to, keys, kos[i], kls[i]) ? cix[i] : 0`,
175
- `}`,
176
- ].join('\n')
177
- case 'phash':
178
- return [
179
- `function ${n('phash')}(input, from, to, p1, p2, mul, mod, tab, cix, keys, kos, kls) {`,
180
- ` const len = to - from`,
181
- ` let a = 0, b = 0`,
182
- ` if (p1 < len) { const c = input.charCodeAt(from + p1); a = ${foldExpr('c')} }`,
183
- ` if (p2 < len) { const c = input.charCodeAt(from + p2); b = ${foldExpr('c')} }`,
184
- ` let i = tab[(len * mul + a * 31 + b) % mod]`,
185
- ` if (i === 0) return 0`,
186
- ` i--`,
187
- ` return ${n('verify')}(input, from, to, keys, kos[i], kls[i]) ? cix[i] : 0`,
188
- `}`,
189
- ].join('\n')
190
- }
191
- }
192
-
193
- /** Per-site emission: declarations to hoist plus the call expression. */
194
- export type SiteEmission = {
195
- strategy: IdStrategy
196
- decls: string[]
197
- callExpr: string
198
- helpers: SharedHelper[]
199
- }
200
-
201
- /** Packed key text plus offset/length/case tables, shared by every verify strategy. */
202
- function packKeys(prefix: string, unpack: string, flat: FlatKey[]): {
203
- decls: string[]; kos: string; kls: string; cix: string; keys: string
204
- } {
205
- let text = ''
206
- const kos: number[] = []
207
- const kls: number[] = []
208
- const cix: number[] = []
209
- for (const f of flat) {
210
- kos.push(text.length)
211
- kls.push(f.text.length)
212
- cix.push(f.caseIndex)
213
- text += f.text
214
- }
215
- return {
216
- decls: [
217
- `const ${prefix}k = ${JSON.stringify(text)}`,
218
- `const ${prefix}o = ${unpack}(${packInts(kos)})`,
219
- `const ${prefix}l = ${unpack}(${packInts(kls)})`,
220
- `const ${prefix}x = ${unpack}(${packInts(cix)})`,
221
- ],
222
- keys: `${prefix}k`, kos: `${prefix}o`, kls: `${prefix}l`, cix: `${prefix}x`,
223
- }
224
- }
225
-
226
- function emitTrie(prefix: string, unpack: string, helper: string, flat: FlatKey[], pos: string, end: string): SiteEmission | null {
227
- type Node = { edges: Map<number, number>; accept: number }
228
- const nodes: Node[] = [{ edges: new Map(), accept: 0 }]
229
- for (const f of flat) {
230
- let n = 0
231
- for (let i = 0; i < f.text.length; i++) {
232
- const code = f.text.charCodeAt(i)
233
- let t = nodes[n]!.edges.get(code)
234
- if (t === undefined) { t = nodes.length; nodes.push({ edges: new Map(), accept: 0 }); nodes[n]!.edges.set(code, t) }
235
- n = t
236
- }
237
- if (nodes[n]!.accept !== 0 && nodes[n]!.accept !== f.caseIndex) return null
238
- nodes[n]!.accept = f.caseIndex
239
- }
240
- const start: number[] = [0]; const char: number[] = []; const next: number[] = []; const accept: number[] = []
241
- for (const nd of nodes) {
242
- for (const [c, t] of [...nd.edges.entries()].sort((a, b) => a[0] - b[0])) { char.push(c); next.push(t) }
243
- start.push(char.length); accept.push(nd.accept)
244
- }
245
- return {
246
- strategy: 'trie',
247
- decls: [
248
- `const ${prefix}c = ${JSON.stringify(char.map(c => String.fromCharCode(c)).join(''))}`,
249
- `const ${prefix}s = ${unpack}(${packInts(start)})`,
250
- `const ${prefix}n = ${unpack}(${packInts(next)})`,
251
- `const ${prefix}a = ${unpack}(${packInts(accept)})`,
252
- ],
253
- callExpr: `${helper}(input, ${pos}, ${end}, ${prefix}c, ${prefix}s, ${prefix}n, ${prefix}a)`,
254
- helpers: ['unpack', 'trie'],
255
- }
256
- }
257
-
258
- function emitLenSwitch(prefix: string, unpack: string, helper: string, flat: FlatKey[], pos: string, end: string): SiteEmission | null {
259
- // Only worth it when length is close to decisive: at most one collision bucket.
260
- const byLen = new Map<number, number>()
261
- for (const f of flat) byLen.set(f.text.length, (byLen.get(f.text.length) ?? 0) + 1)
262
- if ([...byLen.values()].filter(v => v > 1).length > 1) return null
263
- const packed = packKeys(prefix, unpack, flat)
264
- return {
265
- strategy: 'lenswitch',
266
- decls: [...packed.decls, `const ${prefix}n = ${unpack}(${packInts(flat.map(f => f.text.length))})`],
267
- callExpr: `${helper}(input, ${pos}, ${end}, ${prefix}n, ${packed.cix}, ${packed.keys}, ${packed.kos}, ${packed.kls})`,
268
- helpers: ['unpack', 'verify', 'lenswitch'],
269
- }
270
- }
271
-
272
- function emitFirstChar(prefix: string, unpack: string, helper: string, flat: FlatKey[], pos: string, end: string): SiteEmission | null {
273
- const tab = new Array<number>(128).fill(0)
274
- for (const [i, f] of flat.entries()) {
275
- const c = f.text.charCodeAt(0)
276
- // Requires the key set to SEPARATE on char 0 — that is the whole premise.
277
- if (c > 127 || tab[c] !== 0) return null
278
- tab[c] = i + 1
279
- }
280
- const packed = packKeys(prefix, unpack, flat)
281
- return {
282
- strategy: 'firstchar',
283
- decls: [...packed.decls, `const ${prefix}t = ${unpack}(${packInts(tab)})`],
284
- callExpr: `${helper}(input, ${pos}, ${end}, ${prefix}t, ${packed.cix}, ${packed.keys}, ${packed.kos}, ${packed.kls})`,
285
- helpers: ['unpack', 'verify', 'firstchar'],
286
- }
287
- }
288
-
289
- /**
290
- * gperf-style SEARCH for an injective hash over the ACTUAL key set. A FIXED
291
- * discriminator provably fails — `(len,c1,c2)` collides on
292
- * `@counter-style`/`@color-profile` and `@font-feature-values`/
293
- * `@font-palette-values` — but that says nothing about a SEARCHED one, which is
294
- * what this does: try every character-position pair against a set of
295
- * multipliers and moduli and keep the first combination with no collisions.
296
- */
297
- function emitPHash(prefix: string, unpack: string, helper: string, flat: FlatKey[], pos: string, end: string): SiteEmission | null {
298
- const maxLen = Math.max(...flat.map(f => f.text.length))
299
- const at = (f: FlatKey, p: number): number => (p < f.text.length ? f.text.charCodeAt(p) : 0)
300
- for (let p1 = 0; p1 < maxLen; p1++) {
301
- for (let p2 = p1; p2 < maxLen; p2++) {
302
- for (const mul of [1, 3, 7, 11, 17, 31, 53]) {
303
- for (let mod = flat.length; mod <= flat.length * 6 + 16; mod++) {
304
- const slots = new Array<number>(mod).fill(0)
305
- let ok = true
306
- for (const [i, f] of flat.entries()) {
307
- const h = (f.text.length * mul + at(f, p1) * 31 + at(f, p2)) % mod
308
- if (slots[h] !== 0) { ok = false; break }
309
- slots[h] = i + 1
310
- }
311
- if (!ok) continue
312
- const packed = packKeys(prefix, unpack, flat)
313
- return {
314
- strategy: 'phash',
315
- decls: [...packed.decls, `const ${prefix}t = ${unpack}(${packInts(slots)})`],
316
- callExpr: `${helper}(input, ${pos}, ${end}, ${p1}, ${p2}, ${mul}, ${mod}, ${prefix}t, ${packed.cix}, ${packed.keys}, ${packed.kos}, ${packed.kls})`,
317
- helpers: ['unpack', 'verify', 'phash'],
318
- }
319
- }
320
- }
321
- }
322
- }
323
- return null
324
- }
325
-
326
45
  /**
327
- * Emit the id computation for one site. `auto` picks by KEY-SET SHAPE measured
328
- * at macro time rather than applying one strategy everywhere: separation on
329
- * char 0 is the cheapest test there is, a searched perfect hash next, then
330
- * length, with the trie as the always-applicable fallback.
46
+ * The decoder for `packInts`, emitted once per artifact under `name`.
331
47
  */
332
- export function emitDispatchId(
333
- cases: ReadonlyArray<DispatchKeySet>,
334
- cfg: DispatchConfig,
335
- prefix: string,
336
- helperName: (h: SharedHelper) => string,
337
- pos: string,
338
- end: string,
339
- ): SiteEmission | null {
340
- const flat = flatten(cases)
341
- if (flat === null) return null
342
- const unpack = helperName('unpack')
343
- const order: IdStrategy[] = cfg.id === 'auto' ? ['firstchar', 'phash', 'lenswitch', 'trie'] : [cfg.id]
344
- for (const s of order) {
345
- const built =
346
- s === 'trie' ? emitTrie(prefix, unpack, helperName('trie'), flat, pos, end)
347
- : s === 'lenswitch' ? emitLenSwitch(prefix, unpack, helperName('lenswitch'), flat, pos, end)
348
- : s === 'firstchar' ? emitFirstChar(prefix, unpack, helperName('firstchar'), flat, pos, end)
349
- : emitPHash(prefix, unpack, helperName('phash'), flat, pos, end)
350
- if (built !== null) return built
351
- }
352
- return null
48
+ export function unpackHelperDecl(name: string): string {
49
+ return `const ${name} = s => { const o = new Int32Array(s.length >> 1); for (let i = 0; i < o.length; i++) o[i] = (s.charCodeAt(i * 2) - 35) | ((s.charCodeAt(i * 2 + 1) - 35) << 6); return o }`
353
50
  }
@@ -35,7 +35,7 @@
35
35
  import type { Alphabet } from './token-alphabet.ts'
36
36
  import { TOK_EOF, TOK_UNKNOWN, TOK_WS } from './token-alphabet.ts'
37
37
  import { firstSetFromRegex } from '../regex/first-set.ts'
38
- import { packInts, sharedHelperDecl } from './token-dispatch.ts'
38
+ import { packInts, unpackHelperDecl } from './token-dispatch.ts'
39
39
  import { foldCode, foldExpr } from './token-dispatch.ts'
40
40
 
41
41
  /**
@@ -193,7 +193,7 @@ export function emitScanner(alphabet: Alphabet, ns: string): ScannerEmission {
193
193
  // two halves of one encoding still lived in two files. They agree today — the
194
194
  // point is that widening the encoding (which the `RangeError` tells you to do)
195
195
  // must not be able to change one half and miss the other.
196
- decls.push(sharedHelperDecl('unpack', () => p('_tkUnpack')))
196
+ decls.push(unpackHelperDecl(p('_tkUnpack')))
197
197
  decls.push(`const ${p('_tkNext_')} = ${p('_tkUnpack')}(${packInts(NEXT)})`)
198
198
  decls.push(`const ${p('_tkStart')} = ${p('_tkUnpack')}(${packInts(START)})`)
199
199
  decls.push(`const ${p('_tkAccept')} = ${p('_tkUnpack')}(${packInts(ACCEPT)})`)
@@ -14,7 +14,7 @@
14
14
 
15
15
  /** The message `parser()` raises for an unclassified local scope. Exported so the
16
16
  * three engines cannot drift a word apart. */
17
- export const ROOT_TRIVIA_SCOPE_REFUSAL
17
+ const ROOT_TRIVIA_SCOPE_REFUSAL
18
18
  = 'parser(): selected root trivia requires classifiedTrivia() for every local trivia scope, or rootCapture: \'opaque\'.'
19
19
 
20
20
  /**
@@ -61,7 +61,7 @@ export type RootTriviaIndex = {
61
61
  const EMPTY_INDICES: readonly number[] = Object.freeze([])
62
62
  const EMPTY_GAPS: readonly RootTriviaGap[] = Object.freeze([])
63
63
  /** Number of packed fields in one selected-root trivia row. */
64
- export const SELECTED_ROOT_STRIDE = 5
64
+ const SELECTED_ROOT_STRIDE = 5
65
65
 
66
66
  type RootTriviaMaps = {
67
67
  before: Map<number, number[]>
@@ -60,7 +60,7 @@ function unwrapTrivia(p: Combinator<unknown>): Combinator<unknown> {
60
60
  }
61
61
 
62
62
  /** Strip `label()` wrapper; returns inner parser for matching. */
63
- export function peelLabel(p: Combinator<unknown>): { label: string; parser: Combinator<unknown> } | null {
63
+ function peelLabel(p: Combinator<unknown>): { label: string; parser: Combinator<unknown> } | null {
64
64
  if (p._def.tag === 'label') return { label: p._def.label, parser: p._def.parser }
65
65
  return null
66
66
  }
package/src/spec/model.ts CHANGED
@@ -247,7 +247,7 @@ function fixedString(alt: string): string | undefined {
247
247
  * boundary (`word()` / `keywords()` and every hand-written copy of them), and a
248
248
  * `\b…\b`-anchored word.
249
249
  */
250
- export function fixedStringsOfRegex(source: string): string[] | undefined {
250
+ function fixedStringsOfRegex(source: string): string[] | undefined {
251
251
  let src = source
252
252
  // `\b` anchors assert a boundary, exactly like the `(?![…])` guard.
253
253
  if (src.startsWith('\\b')) src = src.slice(2)
@@ -517,18 +517,10 @@ class Encoder {
517
517
  /** TOKEN bodies are an all-program decision. A complete occurrence inside an
518
518
  * otherwise incomplete final graph stays on CHARACTER with every sibling. */
519
519
  readonly selectLexicalBodies: boolean
520
- /**
521
- * Decides the extra operands laid down below. ALWAYS TRUE — see
522
- * `TableSettings.recovery`. Kept as a field rather than folded away because it
523
- * is what every emit site reads, and the emit sites are where the shape of the
524
- * row is stated.
525
- */
526
- readonly rec: boolean
527
520
  constructor(settings: TableSettings, selectLexicalBodies: boolean) {
528
521
  this.settings = settings
529
522
  this.track = settings.trackLines === true
530
523
  this.selectLexicalBodies = selectLexicalBodies
531
- this.rec = true
532
524
  this.plan = settings.coverage
533
525
  this.cov = settings.coverage === undefined
534
526
  ? undefined
@@ -1147,16 +1139,12 @@ class Encoder {
1147
1139
  }
1148
1140
  case 'sequence': {
1149
1141
  const kids = d.parsers.map(c => this.node(c).ip)
1150
- // A RECOVERY table appends one follow-set class per term AFTER the child
1151
- // slots. Appending keeps every existing read (`ip + 2 + i`) and the
1152
- // `collapseIndirection` slot map exactly as they were, so a strict table
1153
- // is word-for-word the table it always was.
1154
- const head = this.emitHead(d.valueUnused ? OP_SEQV : OP_SEQ, 1 + kids.length + (this.rec ? kids.length : 0))
1142
+ // One follow-set class per term (recovery's resync sentinel) follows the
1143
+ // child slots, so every child read stays `ip + 2 + i`.
1144
+ const head = this.emitHead(d.valueUnused ? OP_SEQV : OP_SEQ, 1 + 2 * kids.length)
1155
1145
  this.code[head + 1] = kids.length
1156
1146
  for (let i = 0; i < kids.length; i++) this.code[head + 2 + i] = kids[i]!
1157
- if (this.rec) {
1158
- for (let i = 0; i < kids.length; i++) this.code[head + 2 + kids.length + i] = this.followClass(d.parsers, i)
1159
- }
1147
+ for (let i = 0; i < kids.length; i++) this.code[head + 2 + kids.length + i] = this.followClass(d.parsers, i)
1160
1148
  return head
1161
1149
  }
1162
1150
  case 'choice': {
@@ -1369,14 +1357,10 @@ class Encoder {
1369
1357
  : this.charClass(firstSetOf(d.parser, new Set(), rr, this.activeTrivia))
1370
1358
  const op = d.valueUnused ? OP_REPV : OP_REP
1371
1359
  const min = d.tag === 'many' ? 0 : d.min
1372
- // ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
1373
- // class (−1: there is no separator). Both are only laid down for a
1374
- // recovery table, where the resync error's payload has to be the ITEM's
1375
- // set — `deriveExpected(combinator)` in repeat.ts:170, and
1376
- // `deriveExpectedArr([def.parser])` in codegen's emitMany.
1377
- return this.rec
1378
- ? this.emit(op, child, min, d.max ?? -1, -1, 0, this.expected(deriveExpected(d.parser)), itemClass)
1379
- : this.emit(op, child, min, d.max ?? -1, -1, 0)
1360
+ // ip + 6 is the ITEM's expected set and ip + 7 the item's sentinel class:
1361
+ // the resync error's payload is the ITEM's set (`deriveExpected` in
1362
+ // `combinators/repeat.ts`).
1363
+ return this.emit(op, child, min, d.max ?? -1, -1, 0, this.expected(deriveExpected(d.parser)), itemClass)
1380
1364
  }
1381
1365
  case 'sepBy': {
1382
1366
  const child = this.node(d.parser).ip
@@ -1393,20 +1377,15 @@ class Encoder {
1393
1377
  // it means the FIRST item failed, and that failure already set the
1394
1378
  // item's own set. So no committed grammar grows by a word.
1395
1379
  const flags = (d.trailing === 'allow' ? 1 : 0) | (d.keepSeparators === true ? 2 : 0) | (d.min >= 2 ? 4 : 0)
1396
- // A RECOVERY table always carries ip + 6 (the ITEM's expected set, which
1380
+ // Every list row carries ip + 6 (the ITEM's expected set, which
1397
1381
  // the resync error reports) and ip + 7 (the SEPARATOR's sentinel class, so
1398
1382
  // a tolerant list resyncs to its own separator OR the enclosing
1399
1383
  // delimiter — `orSentinel(separator, term)` in repeat.ts:454 and
1400
1384
  // `_rec.or(sepSent, mySync)` in codegen's emitSepBy).
1401
- if (this.rec) {
1402
- return this.emit(
1403
- OP_REP, child, d.min, d.max ?? -1, sep, flags,
1404
- this.expected(deriveExpected(d.parser)), this.charClass(firstSetOf(d.separator, new Set(), this.refResolver(), this.activeTrivia)),
1405
- )
1406
- }
1407
- return d.min >= 2
1408
- ? this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags, this.expected(deriveExpected(d.parser)))
1409
- : this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags)
1385
+ return this.emit(
1386
+ OP_REP, child, d.min, d.max ?? -1, sep, flags,
1387
+ this.expected(deriveExpected(d.parser)), this.charClass(firstSetOf(d.separator, new Set(), this.refResolver(), this.activeTrivia)),
1388
+ )
1410
1389
  }
1411
1390
  case 'optional': {
1412
1391
  const head = this.emit(OP_OPT, this.lookaheadGate(d.parser, this.node(d.parser).ip))
@@ -1424,7 +1403,7 @@ class Encoder {
1424
1403
  const inner = d.parser._def as ParserDef
1425
1404
  if (inner.tag === 'sequence' && !this.memo.has(d.parser) && !this.pending.has(d.parser)) {
1426
1405
  const kids = inner.parsers.map(c => this.node(c).ip)
1427
- const head = this.emitHead(OP_SEQX, 2 + kids.length + (this.rec ? kids.length : 0))
1406
+ const head = this.emitHead(OP_SEQX, 2 + 2 * kids.length)
1428
1407
  const projection = directArrayProjection(d.fn, d.fnSrc, kids.length)
1429
1408
  // A negative operand describes an already-parsed child (`~index`),
1430
1409
  // while every function-pool index remains non-negative. The row is
@@ -1435,9 +1414,7 @@ class Encoder {
1435
1414
  : ~projection
1436
1415
  this.code[head + 2] = kids.length
1437
1416
  for (let i = 0; i < kids.length; i++) this.code[head + 3 + i] = kids[i]!
1438
- if (this.rec) {
1439
- for (let i = 0; i < kids.length; i++) this.code[head + 3 + kids.length + i] = this.followClass(inner.parsers, i)
1440
- }
1417
+ for (let i = 0; i < kids.length; i++) this.code[head + 3 + kids.length + i] = this.followClass(inner.parsers, i)
1441
1418
  return head
1442
1419
  }
1443
1420
  const child = this.node(d.parser).ip
@@ -1938,7 +1915,7 @@ class Encoder {
1938
1915
  ...(this.settings.hostMode === undefined ? {} : { hostMode: this.settings.hostMode }),
1939
1916
  ...(this.runtimeOnly.size === 0 ? {} : { runtimeOnly: [...this.runtimeOnly].sort() }),
1940
1917
  ...(this.triviaSpecs.length === 0 ? {} : { triviaSpecs: this.triviaSpecs }),
1941
- ...(this.rec ? { rec: 1 as const } : {}),
1918
+ rec: 1,
1942
1919
  ...(this.cov === undefined ? {} : { cov: this.cov }),
1943
1920
  lines: this.track ? 1 : 0,
1944
1921
  }, undefined, {
package/src/table/ops.ts CHANGED
@@ -18,9 +18,9 @@
18
18
  * `encode.ts` and `exec.ts`. Nothing else may read the stream.
19
19
  */
20
20
 
21
- /** `LIT k` — `k` indexes a string in the const pool. */
21
+ /** `LIT k fx` — `k` indexes a string in the const pool; `fx` the expected set on failure. */
22
22
  export const OP_LIT = 1
23
- /** `RX k` — `k` indexes a STICKY `RegExp` in the const pool. */
23
+ /** `RX k fx` — `k` indexes a STICKY `RegExp` in the const pool; `fx` as `LIT`. */
24
24
  export const OP_RX = 2
25
25
  /** `SEQ n c1 … cn` — n child offsets; value is the array of their values. */
26
26
  export const OP_SEQ = 3
@@ -80,9 +80,9 @@ export const OP_XFORM = 9
80
80
  export const OP_NODE = 10
81
81
  /** `RULE r` — `r` indexes `prog.rules`. */
82
82
  export const OP_RULE = 11
83
- /** `GATE cc c` — first-char gate: `cc` indexes a char class; then run `c`. */
83
+ /** `GATE cc c fx` — first-char gate: `cc` indexes a char class; then run `c`. `fx` is the set reported when the gate rejects. */
84
84
  export const OP_GATE = 12
85
- /** `NOT c` — zero-width negative lookahead. */
85
+ /** `NOT c fx` — zero-width negative lookahead; `fx` is the assertion's own set, as `PEEK`. */
86
86
  export const OP_NOT = 13
87
87
  /**
88
88
  * `PEEK c fx` — zero-width positive lookahead. `fx` is the ASSERTION's own set,
@@ -201,10 +201,10 @@ export const OP_LIT_CI_TRACK = 28
201
201
  */
202
202
  export const OP_TOKEN = 29
203
203
  /**
204
- * `SCOPE_CAP k c` — a `parser({ trivia, captureTrivia: true })` scope. Identical
204
+ * `SCOPE_CAP k c policy` — a `parser({ trivia, captureTrivia: true })` scope. Identical
205
205
  * operands to `SCOPE`; it additionally sets `ctx.captureTrivia` for the child.
206
206
  *
207
- * A SEPARATE OPCODE rather than a third operand on `SCOPE`, for two reasons.
207
+ * A SEPARATE OPCODE rather than another operand on `SCOPE`, for two reasons.
208
208
  * Widening an instruction means every walker that knows its shape has to learn
209
209
  * the new one, and there is no central arity table here to change in one place.
210
210
  * And the driver should SELECT this piece, not test a flag inside the scope
@@ -213,7 +213,7 @@ export function covKindCode(kind: 'rule' | 'choice-arm' | 'dispatch-arm' | 'labe
213
213
  }
214
214
  }
215
215
 
216
- export function covKindName(code: 0 | 1 | 2 | 3): 'rule' | 'choice-arm' | 'dispatch-arm' | 'label' {
216
+ function covKindName(code: 0 | 1 | 2 | 3): 'rule' | 'choice-arm' | 'dispatch-arm' | 'label' {
217
217
  switch (code) {
218
218
  case 0: return 'rule'
219
219
  case 1: return 'choice-arm'
@@ -209,7 +209,7 @@ export function leadingScalarSequence(
209
209
  * choices, but only when EVERY arm yields literal authority. Longer literals
210
210
  * covered by a shorter prefix are removed (`@{-}` is already covered by `@{`).
211
211
  */
212
- export function leadingLiteralFamily(
212
+ function leadingLiteralFamily(
213
213
  code: ArrayLike<number>, constants: readonly unknown[], ip: number,
214
214
  minDepth = 2, limit = 4,
215
215
  ): readonly number[] | undefined {