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.
- package/README.md +5 -4
- package/dist/analysis/diagnostics.js +19 -45
- package/dist/analysis/diagnostics.js.map +1 -1
- package/dist/cli/index.js +19 -45
- package/dist/cli/index.js.map +1 -1
- package/dist/combinators/choice.d.ts +0 -5
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/scanTo.d.ts +1 -16
- package/dist/combinators/scanTo.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +0 -3
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +5 -25
- package/dist/compiler/token-alphabet.d.ts.map +1 -1
- package/dist/compiler/token-capability.js.map +1 -1
- package/dist/compiler/token-dispatch.d.ts +8 -79
- package/dist/compiler/token-dispatch.d.ts.map +1 -1
- package/dist/cst/root-trivia-scope.d.ts +0 -3
- package/dist/cst/root-trivia-scope.d.ts.map +1 -1
- package/dist/cst/trivia-entries.d.ts +0 -2
- package/dist/cst/trivia-entries.d.ts.map +1 -1
- package/dist/cst/trivia-kinds.d.ts +0 -5
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/index.js +20 -46
- package/dist/index.js.map +1 -1
- package/dist/language-service/index.js.map +1 -1
- package/dist/plugin/index.js +20 -46
- package/dist/plugin/index.js.map +1 -1
- package/dist/run/index.js.map +1 -1
- package/dist/spec/index.js.map +1 -1
- package/dist/spec/model.d.ts +0 -8
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/index.js +19 -45
- package/dist/table/index.js.map +1 -1
- package/dist/table/ops.d.ts +6 -6
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +0 -1
- package/dist/table/program.d.ts.map +1 -1
- package/dist/table/scalar-terminal.d.ts +0 -7
- package/dist/table/scalar-terminal.d.ts.map +1 -1
- package/dist/table/scan-shapes.d.ts +0 -8
- package/dist/table/scan-shapes.d.ts.map +1 -1
- package/dist/table/site-labels.d.ts +0 -2
- package/dist/table/site-labels.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/combinators/choice.ts +1 -1
- package/src/combinators/scanTo.ts +1 -1
- package/src/compiler/ir-serialize.ts +2 -1
- package/src/compiler/linker.ts +1 -1
- package/src/compiler/token-alphabet.ts +14 -10
- package/src/compiler/token-dispatch.ts +10 -313
- package/src/compiler/token-scanner.ts +2 -2
- package/src/cst/root-trivia-scope.ts +1 -1
- package/src/cst/trivia-entries.ts +1 -1
- package/src/cst/trivia-kinds.ts +1 -1
- package/src/spec/model.ts +1 -1
- package/src/table/encode.ts +16 -39
- package/src/table/ops.ts +6 -6
- package/src/table/program.ts +1 -1
- package/src/table/scalar-terminal.ts +1 -1
- package/src/table/scan-shapes.ts +15 -46
- package/src/table/site-labels.ts +1 -1
- package/src/version.ts +1 -1
|
@@ -1,83 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Packing and case-fold helpers for the dormant token scanner (`token-scanner.ts`).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
333
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
64
|
+
const SELECTED_ROOT_STRIDE = 5
|
|
65
65
|
|
|
66
66
|
type RootTriviaMaps = {
|
|
67
67
|
before: Map<number, number[]>
|
package/src/cst/trivia-kinds.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|
package/src/table/encode.ts
CHANGED
|
@@ -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
|
-
//
|
|
1151
|
-
// slots
|
|
1152
|
-
|
|
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
|
-
|
|
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
|
|
1373
|
-
//
|
|
1374
|
-
//
|
|
1375
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
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 +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
package/src/table/program.ts
CHANGED
|
@@ -213,7 +213,7 @@ export function covKindCode(kind: 'rule' | 'choice-arm' | 'dispatch-arm' | 'labe
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
|
|
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
|
-
|
|
212
|
+
function leadingLiteralFamily(
|
|
213
213
|
code: ArrayLike<number>, constants: readonly unknown[], ip: number,
|
|
214
214
|
minDepth = 2, limit = 4,
|
|
215
215
|
): readonly number[] | undefined {
|