parseman 0.58.3 → 0.59.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 (101) hide show
  1. package/dist/analysis/diagnostics.js +159 -137
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +161 -138
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/choice.d.ts +0 -5
  6. package/dist/combinators/choice.d.ts.map +1 -1
  7. package/dist/combinators/dispatch.d.ts.map +1 -1
  8. package/dist/combinators/field-trivia.d.ts.map +1 -1
  9. package/dist/combinators/keywords.d.ts.map +1 -1
  10. package/dist/combinators/literal.d.ts.map +1 -1
  11. package/dist/combinators/node.d.ts.map +1 -1
  12. package/dist/combinators/regex.d.ts.map +1 -1
  13. package/dist/combinators/scanTo.d.ts +1 -16
  14. package/dist/combinators/scanTo.d.ts.map +1 -1
  15. package/dist/combinators/token.d.ts.map +1 -1
  16. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  17. package/dist/compiler/linker.d.ts +0 -3
  18. package/dist/compiler/linker.d.ts.map +1 -1
  19. package/dist/compiler/token-alphabet.d.ts +5 -25
  20. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  21. package/dist/compiler/token-capability.js.map +1 -1
  22. package/dist/compiler/token-dispatch.d.ts +8 -79
  23. package/dist/compiler/token-dispatch.d.ts.map +1 -1
  24. package/dist/cst/capture-buffer.d.ts.map +1 -1
  25. package/dist/cst/plain.js +69 -0
  26. package/dist/cst/plain.js.map +6 -0
  27. package/dist/cst/root-trivia-scope.d.ts +0 -3
  28. package/dist/cst/root-trivia-scope.d.ts.map +1 -1
  29. package/dist/cst/trivia-entries.d.ts +0 -2
  30. package/dist/cst/trivia-entries.d.ts.map +1 -1
  31. package/dist/cst/trivia-kinds.d.ts +0 -5
  32. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  33. package/dist/index.js +163 -143
  34. package/dist/index.js.map +3 -3
  35. package/dist/language-service/index.js +19 -2
  36. package/dist/language-service/index.js.map +2 -2
  37. package/dist/line-index.d.ts +13 -0
  38. package/dist/line-index.d.ts.map +1 -1
  39. package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
  40. package/dist/plugin/index.js +165 -145
  41. package/dist/plugin/index.js.map +3 -3
  42. package/dist/recovery/scan.d.ts.map +1 -1
  43. package/dist/run/index.js +4 -1
  44. package/dist/run/index.js.map +2 -2
  45. package/dist/spec/index.js +18 -1
  46. package/dist/spec/index.js.map +2 -2
  47. package/dist/spec/model.d.ts +0 -8
  48. package/dist/spec/model.d.ts.map +1 -1
  49. package/dist/table/assemble.d.ts.map +1 -1
  50. package/dist/table/emit-assembly.d.ts +1 -1
  51. package/dist/table/emit-assembly.d.ts.map +1 -1
  52. package/dist/table/emit.d.ts +6 -0
  53. package/dist/table/emit.d.ts.map +1 -1
  54. package/dist/table/encode.d.ts.map +1 -1
  55. package/dist/table/index.js +142 -133
  56. package/dist/table/index.js.map +3 -3
  57. package/dist/table/program.d.ts +0 -1
  58. package/dist/table/program.d.ts.map +1 -1
  59. package/dist/table/run-support.d.ts.map +1 -1
  60. package/dist/table/scalar-terminal.d.ts +0 -7
  61. package/dist/table/scalar-terminal.d.ts.map +1 -1
  62. package/dist/table/scan-shapes.d.ts +0 -8
  63. package/dist/table/scan-shapes.d.ts.map +1 -1
  64. package/dist/table/site-labels.d.ts +0 -2
  65. package/dist/table/site-labels.d.ts.map +1 -1
  66. package/dist/version.d.ts +1 -1
  67. package/package.json +2 -1
  68. package/src/combinators/choice.ts +1 -1
  69. package/src/combinators/dispatch.ts +2 -1
  70. package/src/combinators/field-trivia.ts +6 -5
  71. package/src/combinators/keywords.ts +3 -2
  72. package/src/combinators/literal.ts +5 -4
  73. package/src/combinators/node.ts +3 -1
  74. package/src/combinators/regex.ts +5 -4
  75. package/src/combinators/scanTo.ts +3 -2
  76. package/src/combinators/token.ts +3 -6
  77. package/src/compiler/ir-serialize.ts +2 -1
  78. package/src/compiler/linker.ts +5 -3
  79. package/src/compiler/token-alphabet.ts +14 -10
  80. package/src/compiler/token-dispatch.ts +10 -313
  81. package/src/compiler/token-scanner.ts +2 -2
  82. package/src/cst/capture-buffer.ts +13 -5
  83. package/src/cst/plain.d.ts +35 -0
  84. package/src/cst/plain.js +74 -0
  85. package/src/cst/root-trivia-scope.ts +1 -1
  86. package/src/cst/trivia-entries.ts +1 -1
  87. package/src/cst/trivia-kinds.ts +1 -1
  88. package/src/line-index.ts +37 -0
  89. package/src/plugin/entry-supercompiler.ts +3 -2
  90. package/src/recovery/scan.ts +2 -1
  91. package/src/spec/model.ts +1 -1
  92. package/src/table/assemble.ts +33 -35
  93. package/src/table/emit-assembly.ts +27 -24
  94. package/src/table/emit.ts +9 -1
  95. package/src/table/encode.ts +16 -39
  96. package/src/table/program.ts +1 -1
  97. package/src/table/run-support.ts +4 -7
  98. package/src/table/scalar-terminal.ts +1 -1
  99. package/src/table/scan-shapes.ts +15 -46
  100. package/src/table/site-labels.ts +1 -1
  101. 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)})`)
@@ -1,5 +1,6 @@
1
1
  import type { ParseContext, Span } from '../types.ts'
2
- import { annotateSpanFromLineContext } from '../line-index.ts'
2
+ import { annotateSpanFromLineContext, lineSpanFromContext } from '../line-index.ts'
3
+ import { PlainBuf, PlainLeaf } from './plain.js'
3
4
 
4
5
  /** Lazy per-node capture state — arrays materialized on first push. */
5
6
  export type CstCaptureBuf = {
@@ -42,7 +43,7 @@ export function beginCstNodeCapture(ctx: ParseContext): CstNodeCaptureSaved {
42
43
  cap: ctx.captureTrivia,
43
44
  buf: ctx._cstBuf,
44
45
  }
45
- ctx._cstBuf = {}
46
+ ctx._cstBuf = new PlainBuf(undefined, undefined, 0)
46
47
  ctx._cstChildren = undefined
47
48
  ctx._cstLeaves = undefined
48
49
  ctx._cstRawChildren = undefined
@@ -79,14 +80,21 @@ export function endCstNodeCapture(
79
80
  return materialized
80
81
  }
81
82
 
83
+ /** Every caller passes a leaf `{ _tag, value, span }` (parseman's own terminals; this is not public API). */
84
+ function isLeaf(v: unknown): v is { _tag: 'leaf'; value: unknown; span: Span } {
85
+ return typeof v === 'object' && v !== null && (v as { _tag?: unknown })._tag === 'leaf'
86
+ }
87
+
82
88
  export function pushCstLeaf(ctx: ParseContext, leaf: unknown): void {
83
89
  if (!ctx.trackLines) {
84
90
  pushCstChild(ctx, leaf, leaf)
85
91
  return
86
92
  }
87
- const annotated = leaf && typeof leaf === 'object' && (leaf as { span?: unknown }).span
88
- ? { ...(leaf as Record<string, unknown>), span: annotateSpanFromLineContext((leaf as { span: Span }).span, ctx) }
89
- : leaf
93
+ const annotated = isLeaf(leaf)
94
+ ? new PlainLeaf(leaf.value, lineSpanFromContext(leaf.span, ctx))
95
+ : leaf && typeof leaf === 'object' && (leaf as { span?: unknown }).span
96
+ ? { ...(leaf as Record<string, unknown>), span: annotateSpanFromLineContext((leaf as { span: Span }).span, ctx) }
97
+ : leaf
90
98
  pushCstChild(ctx, annotated, annotated)
91
99
  }
92
100
 
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The objects a parse keeps until it ends — spans, leaves, default nodes, field
3
+ * captures, capture buffers — built by `new` calls instead of object literals.
4
+ *
5
+ * WHY. V8 keeps an allocation site per object literal and decides, per site, whether to
6
+ * allocate straight into old space (pretenuring). The decision is taken during warm-up
7
+ * from where scavenges happen to fall, so identical processes split into a fast and a
8
+ * slow mode, and any change in bundle layout re-rolls it: on Node 26 a pretenured
9
+ * process ran 34–61 mark-compacts per run where a fast one ran 1. A `new` call has no
10
+ * allocation site, so these objects are never pretenured. Each constructor's
11
+ * `prototype` is `Object.prototype`, so what it builds is a plain object: same
12
+ * prototype, same `constructor`, same keys in the same order as the literal it replaces.
13
+ *
14
+ * ONE MODULE. Every constructor defines its own hidden class, so two copies of this
15
+ * module build two maps for the same shape, and every site that reads both turns
16
+ * polymorphic. The build therefore emits it once (`dist/cst/plain.js`) and every public
17
+ * entry imports that file (`scripts/build.mjs`). The emitted engine receives the same
18
+ * functions as factory parameters (`EMITTED_PARAMS`).
19
+ */
20
+ import type { Span } from '../types.ts'
21
+ import type { CstCaptureBuf } from './capture-buffer.ts'
22
+
23
+ export type PlainLeafObject = { _tag: 'leaf'; value: unknown; span: Span }
24
+ export type PlainNodeObject = { _tag: 'node'; type: string; span: Span; state: unknown; children: unknown[] }
25
+ export type PlainFieldObject = { name: string; value: unknown; span: Span }
26
+
27
+ export declare const PlainSpan: new (start: number, end: number) => Span
28
+ export declare const PlainLeaf: new (value: unknown, span: Span) => PlainLeafObject
29
+ export type PlainLineSpanObject = { start: number; end: number; startLine: number; startColumn: number; endLine: number; endColumn: number }
30
+ export declare const PlainLineSpan: new (start: number, end: number, startLine: number, startColumn: number, endLine: number, endColumn: number) => PlainLineSpanObject
31
+ export declare const PlainTaggedNode: new (type: string, tags: readonly string[], span: Span, state: unknown, children: unknown[]) => { _tag: 'node'; type: string; tags: readonly string[]; span: Span; state: unknown; children: unknown[] }
32
+ export declare const PlainTriviaField: new (name: string, value: unknown, span: Span, leading: string, trailing: string) => { name: string; value: unknown; span: Span; leading: string; trailing: string }
33
+ export declare const PlainNode: new (type: string, span: Span, state: unknown, children: unknown[]) => PlainNodeObject
34
+ export declare const PlainField: new (name: string, value: unknown, span: Span) => PlainFieldObject
35
+ export declare const PlainBuf: new (rawOnly: true | undefined, noRaw: true | undefined, rawLen: number) => CstCaptureBuf
@@ -0,0 +1,74 @@
1
+ // Constructors for the objects a parse keeps until it ends. Plain JavaScript on
2
+ // purpose: a function constructor whose `prototype` is `Object.prototype` is the only
3
+ // spelling that builds a plain object with a `new` call, and TypeScript cannot declare
4
+ // one in a `.ts` file without a cast. `plain.d.ts` types them. Why constructors at all,
5
+ // and why this is ONE module shared by every bundle: see `plain.d.ts`.
6
+
7
+ export function PlainSpan(start, end) {
8
+ this.start = start
9
+ this.end = end
10
+ }
11
+
12
+ export function PlainLeaf(value, span) {
13
+ this._tag = 'leaf'
14
+ this.value = value
15
+ this.span = span
16
+ }
17
+
18
+ export function PlainLineSpan(start, end, startLine, startColumn, endLine, endColumn) {
19
+ this.start = start
20
+ this.end = end
21
+ this.startLine = startLine
22
+ this.startColumn = startColumn
23
+ this.endLine = endLine
24
+ this.endColumn = endColumn
25
+ }
26
+
27
+ export function PlainNode(type, span, state, children) {
28
+ this._tag = 'node'
29
+ this.type = type
30
+ this.span = span
31
+ this.state = state
32
+ this.children = children
33
+ }
34
+
35
+ export function PlainTaggedNode(type, tags, span, state, children) {
36
+ this._tag = 'node'
37
+ this.type = type
38
+ this.tags = tags
39
+ this.span = span
40
+ this.state = state
41
+ this.children = children
42
+ }
43
+
44
+ export function PlainField(name, value, span) {
45
+ this.name = name
46
+ this.value = value
47
+ this.span = span
48
+ }
49
+
50
+ export function PlainTriviaField(name, value, span, leading, trailing) {
51
+ this.name = name
52
+ this.value = value
53
+ this.span = span
54
+ this.leading = leading
55
+ this.trailing = trailing
56
+ }
57
+
58
+ // Every field assigned, so the three buffer kinds share one shape. `rawLen` is always a
59
+ // count: a field that held `undefined` in most buffers and a number in some would
60
+ // generalize and deoptimize the pushes that read it.
61
+ export function PlainBuf(rawOnly, noRaw, rawLen) {
62
+ this.rawOnly = rawOnly
63
+ this.noRaw = noRaw
64
+ this.rawLen = rawLen
65
+ this.single = undefined
66
+ this.ch = undefined
67
+ this.rawSingle = undefined
68
+ this.raw = undefined
69
+ this.tl = undefined
70
+ }
71
+
72
+ for (const f of [PlainSpan, PlainLineSpan, PlainLeaf, PlainNode, PlainTaggedNode, PlainField, PlainTriviaField, PlainBuf]) {
73
+ f.prototype = Object.prototype
74
+ }
@@ -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/line-index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Span } from './types.ts'
2
+ import { PlainLineSpan, PlainSpan } from './cst/plain.js'
2
3
 
3
4
  /**
4
5
  * Precomputed index of newline positions for O(log n) offset->line/col lookup.
@@ -98,6 +99,42 @@ export function annotateSpan(span: Span, index: LineIndex): Span {
98
99
  }
99
100
  }
100
101
 
102
+ const SPAN_KEYS = ['start', 'end', 'startLine', 'startColumn', 'endLine', 'endColumn'] as const
103
+
104
+ /**
105
+ * A copy of a span, exactly as given: the same keys in the same order. The two shapes
106
+ * parseman builds itself, `{ start, end }` and the six-key line span, are rebuilt by their
107
+ * constructors (`src/cst/plain.d.ts`); any other shape a caller passes (an extra key, a
108
+ * different order, a partial line span) is spread, as before.
109
+ */
110
+ export function copySpan(span: Span): Span {
111
+ // `for…in` walks the keys in order without allocating a key list.
112
+ let n = 0
113
+ for (const key in span) {
114
+ if (key !== SPAN_KEYS[n]) return { ...span }
115
+ n++
116
+ }
117
+ if (n === 2) return new PlainSpan(span.start, span.end)
118
+ if (n === 6 && span.startLine !== undefined && span.startColumn !== undefined && span.endLine !== undefined && span.endColumn !== undefined) {
119
+ return new PlainLineSpan(span.start, span.end, span.startLine, span.startColumn, span.endLine, span.endColumn)
120
+ }
121
+ return { ...span }
122
+ }
123
+
124
+ /**
125
+ * `annotateSpanFromLineContext` for a span parseman built itself (`{ start, end }` only),
126
+ * returning a constructor-built line span (`src/cst/plain.d.ts`). The public form keeps
127
+ * any extra keys a caller's span carries, so it spreads.
128
+ */
129
+ export function lineSpanFromContext(span: Span, ctx: LineTrackingContext): Span {
130
+ if (!ctx.trackLines) return span
131
+ const index = ctx._lineIndex ?? (ctx._lineStarts ? { lineStarts: ctx._lineStarts } : undefined)
132
+ if (!index) return span
133
+ const s = offsetToLineCol(index, span.start)
134
+ const e = offsetToLineCol(index, span.end)
135
+ return new PlainLineSpan(span.start, span.end, s.line, s.col, e.line, e.col)
136
+ }
137
+
101
138
  export function annotateSpanFromLineContext(
102
139
  span: Span,
103
140
  ctx: LineTrackingContext,
@@ -12,6 +12,7 @@ import { OP_ATTEMPT, OP_NODE, OP_NODE_TRACK, OP_REPV, OP_SCAN } from '../table/o
12
12
  import { emitAssemblySource, type EmitResult } from '../table/emit-assembly.ts'
13
13
  import { defaultAssemblyCfgs } from '../table/emit.ts'
14
14
  import { assemblyExtraIps, resolveTable, type TableProgram } from '../table/program.ts'
15
+ import { FACTORY_ARITY_GUARD } from '../table/emit.ts'
15
16
 
16
17
  type Ast = { type: string; start?: number; end?: number; [key: string]: unknown }
17
18
 
@@ -271,8 +272,8 @@ export function supercompileRuleMapReplacement(
271
272
  if (typeof body.start !== 'number' || typeof body.end !== 'number') return replacement
272
273
  const start = body.start - prefix.length + 1
273
274
  const end = body.end - prefix.length - 1
274
- if (replacement.slice(start, end) !== canonical.source + '\n') return replacement
275
- const transformed = replacement.slice(0, start) + candidate.source + '\n' + replacement.slice(end)
275
+ if (replacement.slice(start, end) !== FACTORY_ARITY_GUARD + canonical.source + '\n') return replacement
276
+ const transformed = replacement.slice(0, start) + FACTORY_ARITY_GUARD + candidate.source + '\n' + replacement.slice(end)
276
277
  const checked = parseSync('parseman-entry-supercompile-replacement-result.js', prefix + transformed)
277
278
  return checked.errors.length === 0 ? transformed : replacement
278
279
  }
@@ -2,6 +2,7 @@ import type { Combinator, FirstSet, ParseContext, ParseError, ParseResult, Recov
2
2
  import { cstCaptureActive, pushCstChild } from '../cst/capture-buffer.ts'
3
3
  import { rangesHaveLead } from '../lead-ranges.ts'
4
4
  import { annotateSpanFromLineContext, recordLineRangeFromContext } from '../line-index.ts'
5
+ import { PlainSpan } from '../cst/plain.js'
5
6
 
6
7
  /**
7
8
  * Recovery MECHANISM — sync-source-agnostic. These primitives implement "scan
@@ -112,7 +113,7 @@ export function recoverScan(
112
113
  ): { error: ParseError; end: number } {
113
114
  let scanPos = from
114
115
  while (scanPos < input.length && !probeAt(sync, input, scanPos, ctx)) scanPos++
115
- const span = { start: from, end: scanPos }
116
+ const span = new PlainSpan(from, scanPos)
116
117
  if (ctx.trackLines) recordLineRangeFromContext(ctx, input, from, scanPos)
117
118
  const error: ParseError = { _tag: 'parseError', span: ctx.trackLines ? annotateSpanFromLineContext(span, ctx) : span, expected }
118
119
  ctx._errors?.push(error)
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)