parseman 0.46.0 → 0.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/analysis/choice-cost-render.d.ts +12 -4
- package/dist/analysis/choice-cost-render.d.ts.map +1 -1
- package/dist/analysis/choice-cost.d.ts +2 -2
- package/dist/analysis/choice-cost.d.ts.map +1 -1
- package/dist/analysis/commitment.d.ts +131 -0
- package/dist/analysis/commitment.d.ts.map +1 -0
- package/dist/analysis/diagnostics.cjs +10339 -7787
- package/dist/analysis/diagnostics.cjs.map +3 -4
- package/dist/analysis/diagnostics.d.ts +2 -2
- package/dist/analysis/diagnostics.js +10339 -7787
- package/dist/analysis/diagnostics.js.map +3 -4
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/fix.d.ts +3 -3
- package/dist/analysis/fix.d.ts.map +1 -1
- package/dist/analysis/format-number.d.ts +23 -0
- package/dist/analysis/format-number.d.ts.map +1 -0
- package/dist/analysis/gating.d.ts +39 -2
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/rebuild.d.ts.map +1 -1
- package/dist/analysis/terminal.d.ts +5 -2
- package/dist/analysis/terminal.d.ts.map +1 -1
- package/dist/cli/index.cjs +10924 -8506
- package/dist/cli/index.cjs.map +3 -4
- package/dist/cli/index.js +10924 -8506
- package/dist/cli/index.js.map +3 -4
- package/dist/combinators/adjacency.d.ts +100 -0
- package/dist/combinators/adjacency.d.ts.map +1 -0
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/completions.d.ts.map +1 -1
- package/dist/combinators/dispatch.d.ts +8 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/gate.d.ts.map +1 -1
- package/dist/combinators/grammar.d.ts +22 -0
- package/dist/combinators/grammar.d.ts.map +1 -1
- package/dist/combinators/literal.d.ts +8 -0
- package/dist/combinators/literal.d.ts.map +1 -1
- package/dist/combinators/map.d.ts +0 -1
- package/dist/combinators/map.d.ts.map +1 -1
- package/dist/combinators/node.d.ts +13 -3
- package/dist/combinators/node.d.ts.map +1 -1
- package/dist/combinators/parser.d.ts +1 -1
- package/dist/combinators/parser.d.ts.map +1 -1
- package/dist/combinators/ref.d.ts.map +1 -1
- package/dist/combinators/regex.d.ts +1 -16
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/repeat.d.ts +30 -2
- package/dist/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/scanTo.d.ts +71 -1
- package/dist/combinators/scanTo.d.ts.map +1 -1
- package/dist/combinators/sequence.d.ts.map +1 -1
- package/dist/combinators/token.d.ts.map +1 -1
- package/dist/combinators/trivia-boundary.d.ts +26 -0
- package/dist/combinators/trivia-boundary.d.ts.map +1 -0
- package/dist/combinators/trivia-skip.d.ts +45 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/combinators/withCtx.d.ts.map +1 -1
- package/dist/compiler/compile-linkable-table.d.ts +104 -0
- package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
- package/dist/compiler/degradation.d.ts +0 -2
- package/dist/compiler/degradation.d.ts.map +1 -1
- package/dist/compiler/fields.d.ts +13 -0
- package/dist/compiler/fields.d.ts.map +1 -1
- package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
- package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +47 -53
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +81 -0
- package/dist/compiler/token-alphabet.d.ts.map +1 -0
- package/dist/compiler/token-dispatch.d.ts +95 -0
- package/dist/compiler/token-dispatch.d.ts.map +1 -0
- package/dist/compiler/token-scanner.d.ts +53 -0
- package/dist/compiler/token-scanner.d.ts.map +1 -0
- package/dist/compiler/value-usage.d.ts.map +1 -1
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/capture-buffer.d.ts +35 -0
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/cst/reflection-attach.d.ts +8 -0
- package/dist/cst/reflection-attach.d.ts.map +1 -0
- package/dist/cst/reflection-symbols.d.ts +10 -0
- package/dist/cst/reflection-symbols.d.ts.map +1 -0
- package/dist/cst/reflection.d.ts +2 -4
- package/dist/cst/reflection.d.ts.map +1 -1
- package/dist/cst/root-trivia-scope.d.ts +26 -0
- package/dist/cst/root-trivia-scope.d.ts.map +1 -0
- package/dist/cst/trivia-charscan.d.ts +18 -0
- package/dist/cst/trivia-charscan.d.ts.map +1 -0
- package/dist/cst/trivia-kinds.d.ts +19 -0
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/functional/doc.d.ts.map +1 -1
- package/dist/functional/run-tabled.d.ts +12 -0
- package/dist/functional/run-tabled.d.ts.map +1 -0
- package/dist/functional/run.d.ts +25 -42
- package/dist/functional/run.d.ts.map +1 -1
- package/dist/grammar-metadata.d.ts +3 -0
- package/dist/grammar-metadata.d.ts.map +1 -0
- package/dist/index.cjs +9290 -7181
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +18 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9300 -7191
- package/dist/index.js.map +3 -4
- package/dist/language-service/index.cjs +313 -121
- package/dist/language-service/index.cjs.map +3 -4
- package/dist/language-service/index.js +313 -121
- package/dist/language-service/index.js.map +3 -4
- package/dist/oracle/index.cjs.map +0 -1
- package/dist/oracle/index.js.map +0 -1
- package/dist/parse-context.d.ts +49 -0
- package/dist/parse-context.d.ts.map +1 -0
- package/dist/plugin/evaluator.d.ts +40 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.cjs +11175 -9534
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11175 -9534
- package/dist/plugin/index.js.map +3 -4
- package/dist/regex/classes.d.ts +28 -0
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/regex/first-set.d.ts +14 -0
- package/dist/regex/first-set.d.ts.map +1 -1
- package/dist/run/index.cjs +136 -51
- package/dist/run/index.cjs.map +2 -3
- package/dist/run/index.js +136 -51
- package/dist/run/index.js.map +2 -3
- package/dist/spec/index.cjs +2934 -1611
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2934 -1611
- package/dist/spec/index.js.map +3 -4
- package/dist/spec/model.d.ts +8 -0
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/table/assemble.d.ts +307 -0
- package/dist/table/assemble.d.ts.map +1 -0
- package/dist/table/cell.d.ts +42 -0
- package/dist/table/cell.d.ts.map +1 -0
- package/dist/table/child-slots.d.ts +30 -0
- package/dist/table/child-slots.d.ts.map +1 -0
- package/dist/table/compile-rule-map.d.ts +136 -0
- package/dist/table/compile-rule-map.d.ts.map +1 -0
- package/dist/table/compile.d.ts +56 -0
- package/dist/table/compile.d.ts.map +1 -0
- package/dist/table/duplication-hook.d.ts +32 -0
- package/dist/table/duplication-hook.d.ts.map +1 -0
- package/dist/table/emit-assembly.d.ts +139 -0
- package/dist/table/emit-assembly.d.ts.map +1 -0
- package/dist/table/emit.d.ts +113 -0
- package/dist/table/emit.d.ts.map +1 -0
- package/dist/table/encode-baseline.d.ts +16 -0
- package/dist/table/encode-baseline.d.ts.map +1 -0
- package/dist/table/encode.d.ts +71 -0
- package/dist/table/encode.d.ts.map +1 -0
- package/dist/table/exec-baseline.d.ts +9 -0
- package/dist/table/exec-baseline.d.ts.map +1 -0
- package/dist/table/exec.d.ts +72 -0
- package/dist/table/exec.d.ts.map +1 -0
- package/dist/table/fold.d.ts +25 -0
- package/dist/table/fold.d.ts.map +1 -0
- package/dist/table/index.cjs +11463 -0
- package/dist/table/index.cjs.map +6 -0
- package/dist/table/index.d.ts +100 -0
- package/dist/table/index.d.ts.map +1 -0
- package/dist/table/index.js +11440 -0
- package/dist/table/index.js.map +6 -0
- package/dist/table/inspect.d.ts +17 -0
- package/dist/table/inspect.d.ts.map +1 -0
- package/dist/table/ops.d.ts +453 -0
- package/dist/table/ops.d.ts.map +1 -0
- package/dist/table/program.d.ts +496 -0
- package/dist/table/program.d.ts.map +1 -0
- package/dist/table/run-support.d.ts +38 -0
- package/dist/table/run-support.d.ts.map +1 -0
- package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
- package/dist/table/scan-shapes.d.ts.map +1 -0
- package/dist/table/site-labels.d.ts +58 -0
- package/dist/table/site-labels.d.ts.map +1 -0
- package/dist/table/stamp.d.ts +70 -0
- package/dist/table/stamp.d.ts.map +1 -0
- package/dist/types.d.ts +80 -24
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +26 -2
- package/src/analysis/choice-cost-gate.ts +359 -0
- package/src/analysis/choice-cost-render.ts +293 -0
- package/src/analysis/choice-cost.ts +1008 -0
- package/src/analysis/commitment.ts +557 -0
- package/src/analysis/corpus.ts +130 -0
- package/src/analysis/diagnose-render.ts +625 -0
- package/src/analysis/diagnose.ts +317 -0
- package/src/analysis/diagnostics.ts +33 -0
- package/src/analysis/duplication.ts +1955 -0
- package/src/analysis/fix-render.ts +217 -0
- package/src/analysis/fix.ts +669 -0
- package/src/analysis/format-number.ts +31 -0
- package/src/analysis/gating.ts +792 -0
- package/src/analysis/grammar.ts +72 -0
- package/src/analysis/rebuild.ts +287 -0
- package/src/analysis/terminal.ts +336 -0
- package/src/cli/index.ts +413 -0
- package/src/combinators/adjacency.ts +171 -0
- package/src/combinators/attempt.ts +41 -0
- package/src/combinators/case-fold.ts +90 -0
- package/src/combinators/choice.ts +466 -0
- package/src/combinators/completions.ts +64 -0
- package/src/combinators/dispatch.ts +358 -0
- package/src/combinators/expect.ts +159 -0
- package/src/combinators/first-set.ts +348 -0
- package/src/combinators/gate.ts +50 -0
- package/src/combinators/grammar.ts +272 -0
- package/src/combinators/keywords.ts +145 -0
- package/src/combinators/literal.ts +102 -0
- package/src/combinators/map.ts +106 -0
- package/src/combinators/node.ts +345 -0
- package/src/combinators/not.ts +55 -0
- package/src/combinators/parser.ts +266 -0
- package/src/combinators/peek.ts +63 -0
- package/src/combinators/probe.ts +25 -0
- package/src/combinators/ref.ts +109 -0
- package/src/combinators/regex.ts +170 -0
- package/src/combinators/repeat.ts +561 -0
- package/src/combinators/scanTo.ts +393 -0
- package/src/combinators/sequence.ts +237 -0
- package/src/combinators/token.ts +120 -0
- package/src/combinators/trivia-boundary.ts +48 -0
- package/src/combinators/trivia-skip.ts +852 -0
- package/src/combinators/withCtx.ts +45 -0
- package/src/compiler/build-arity.ts +330 -0
- package/src/compiler/compile-linkable-table.ts +209 -0
- package/src/compiler/degradation.ts +266 -0
- package/src/compiler/fields.ts +191 -0
- package/src/compiler/grammar-coverage-ids.ts +129 -0
- package/src/compiler/ir-serialize.ts +615 -0
- package/src/compiler/linker.ts +954 -0
- package/src/compiler/token-alphabet.ts +209 -0
- package/src/compiler/token-dispatch.ts +353 -0
- package/src/compiler/token-scanner.ts +318 -0
- package/src/compiler/value-usage.ts +147 -0
- package/src/coverage.ts +383 -0
- package/src/cst/capture-buffer.ts +297 -0
- package/src/cst/host-mode.ts +69 -0
- package/src/cst/reflection-attach.ts +11 -0
- package/src/cst/reflection-symbols.ts +9 -0
- package/src/cst/reflection.ts +149 -0
- package/src/cst/relative-spans.ts +207 -0
- package/src/cst/root-trivia-scope.ts +30 -0
- package/src/cst/trivia-charscan.ts +312 -0
- package/src/cst/trivia-entries.ts +285 -0
- package/src/cst/trivia-index.ts +131 -0
- package/src/cst/trivia-kinds.ts +358 -0
- package/src/cst/types.ts +76 -0
- package/src/cst/walk.ts +101 -0
- package/src/functional/doc.ts +959 -0
- package/src/functional/run-tabled.ts +153 -0
- package/src/functional/run.ts +558 -0
- package/src/grammar-metadata.ts +2 -0
- package/src/index.ts +189 -0
- package/src/language-service/index.ts +221 -0
- package/src/line-index.ts +131 -0
- package/src/oracle/digest.ts +620 -0
- package/src/oracle/index.ts +55 -0
- package/src/parse-context.ts +91 -0
- package/src/plugin/direct-builder-static.ts +102 -0
- package/src/plugin/evaluator.ts +1505 -0
- package/src/plugin/free-identifiers.ts +338 -0
- package/src/plugin/index.ts +2281 -0
- package/src/plugin/reducer-resolver.ts +606 -0
- package/src/recovery/scan.ts +155 -0
- package/src/regex/classes.ts +183 -0
- package/src/regex/first-set.ts +316 -0
- package/src/run/index.ts +31 -0
- package/src/spec/ebnf.ts +128 -0
- package/src/spec/index.ts +54 -0
- package/src/spec/model.ts +636 -0
- package/src/spec/railroad-lib.ts +559 -0
- package/src/spec/railroad.ts +256 -0
- package/src/table/assemble.ts +3006 -0
- package/src/table/cell.ts +42 -0
- package/src/table/child-slots.ts +113 -0
- package/src/table/compile-rule-map.ts +336 -0
- package/src/table/compile.ts +244 -0
- package/src/table/duplication-hook.ts +89 -0
- package/src/table/emit-assembly.ts +1777 -0
- package/src/table/emit.ts +429 -0
- package/src/table/encode-baseline.ts +340 -0
- package/src/table/encode.ts +1569 -0
- package/src/table/exec-baseline.ts +537 -0
- package/src/table/exec.ts +1680 -0
- package/src/table/fold.ts +38 -0
- package/src/table/index.ts +108 -0
- package/src/table/inspect.ts +52 -0
- package/src/table/ops.ts +467 -0
- package/src/table/program.ts +876 -0
- package/src/table/run-support.ts +66 -0
- package/src/table/scan-shapes.ts +1578 -0
- package/src/table/site-labels.ts +228 -0
- package/src/table/stamp.ts +229 -0
- package/src/types.ts +563 -0
- package/src/version.ts +16 -0
- package/dist/compiler/codegen.d.ts +0 -334
- package/dist/compiler/codegen.d.ts.map +0 -1
- package/dist/compiler/inline-build.d.ts +0 -11
- package/dist/compiler/inline-build.d.ts.map +0 -1
- package/dist/compiler/inline-callback.d.ts +0 -21
- package/dist/compiler/inline-callback.d.ts.map +0 -1
- package/dist/compiler/line-index.d.ts +0 -2
- package/dist/compiler/line-index.d.ts.map +0 -1
- package/dist/compiler/module-hoist.d.ts +0 -106
- package/dist/compiler/module-hoist.d.ts.map +0 -1
- package/dist/compiler/scannable-run.d.ts.map +0 -1
- package/dist/compiler/scannable-terminal.d.ts +0 -23
- package/dist/compiler/scannable-terminal.d.ts.map +0 -1
- package/dist/compiler/trivia-fast-path.d.ts +0 -38
- package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived tokenization, part 1: the terminal ALPHABET.
|
|
3
|
+
*
|
|
4
|
+
* Parseman knows every terminal in the composed grammar at macro time — every
|
|
5
|
+
* `literal`, `keywords`, and `regex`. This module walks the combinator graph
|
|
6
|
+
* and assigns each distinct terminal a small integer TOKEN ID.
|
|
7
|
+
*
|
|
8
|
+
* Two things are deliberately separated, because conflating them is what makes
|
|
9
|
+
* a naive derived scanner useless:
|
|
10
|
+
*
|
|
11
|
+
* - The token ID SPACE is GLOBAL. One id per distinct terminal across the whole
|
|
12
|
+
* grammar, so an id can be compared, switched on, and indexed with.
|
|
13
|
+
* - The CANDIDATE SET is LOCAL, per decision point. Global maximal munch over
|
|
14
|
+
* the whole alphabet is wrong: a css alphabet contains construct-local
|
|
15
|
+
* long-run terminals (`[^()]+`, `[^{}/]+`, the `scanTo` raw-prelude runs)
|
|
16
|
+
* that swallow the document. Measured: global munch yields SEVEN tokens for a
|
|
17
|
+
* 123 KB stylesheet. A decision point consults only the terminals that can
|
|
18
|
+
* actually start one of its arms.
|
|
19
|
+
*
|
|
20
|
+
* Nothing here decides HOW a token is recognized (see `token-scanner.ts`); this
|
|
21
|
+
* module only decides WHAT the tokens are and WHICH ones each site may see.
|
|
22
|
+
*/
|
|
23
|
+
import type { Combinator, ParserDef } from '../types.ts'
|
|
24
|
+
|
|
25
|
+
/** A terminal in the derived alphabet, with its globally assigned id. */
|
|
26
|
+
export type TokenTerminal =
|
|
27
|
+
| { kind: 'literal'; id: number; value: string; caseInsensitive: boolean }
|
|
28
|
+
| { kind: 'keywords'; id: number; words: readonly string[]; caseInsensitive: boolean; boundary: string | undefined }
|
|
29
|
+
| { kind: 'regex'; id: number; source: string; flags: string }
|
|
30
|
+
|
|
31
|
+
/** Reserved ids. Real terminals start at `FIRST_TERMINAL_ID`. */
|
|
32
|
+
export const TOK_EOF = 0
|
|
33
|
+
export const TOK_UNKNOWN = 1
|
|
34
|
+
export const TOK_WS = 2
|
|
35
|
+
export const FIRST_TERMINAL_ID = 3
|
|
36
|
+
|
|
37
|
+
export type Alphabet = {
|
|
38
|
+
terminals: TokenTerminal[]
|
|
39
|
+
/** Dedup key → id, so the same terminal spelled twice gets ONE id. */
|
|
40
|
+
byKey: Map<string, number>
|
|
41
|
+
/** The combinator that contributed each id (first one wins), for candidate sets. */
|
|
42
|
+
originOf: Map<number, Combinator<unknown>>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function keyOf(def: ParserDef): string | undefined {
|
|
46
|
+
switch (def.tag) {
|
|
47
|
+
case 'literal': return `L\u0000${def.value}\u0000${def.caseInsensitive ? 'i' : ''}`
|
|
48
|
+
case 'keywords': return `K\u0000${def.words.join('\u0001')}\u0000${def.caseInsensitive ? 'i' : ''}\u0000${def.boundary ?? ''}`
|
|
49
|
+
case 'regex': return `R\u0000${def.source}\u0000${def.flags}`
|
|
50
|
+
default: return undefined
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function terminalOf(def: ParserDef, id: number): TokenTerminal | undefined {
|
|
55
|
+
switch (def.tag) {
|
|
56
|
+
case 'literal': return { kind: 'literal', id, value: def.value, caseInsensitive: def.caseInsensitive }
|
|
57
|
+
case 'keywords': return { kind: 'keywords', id, words: def.words, caseInsensitive: def.caseInsensitive, boundary: def.boundary }
|
|
58
|
+
case 'regex': return { kind: 'regex', id, source: def.source, flags: def.flags }
|
|
59
|
+
default: return undefined
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Direct sub-parsers of a def, resolving a lazy through `resolve` when its own thunk is a hole. */
|
|
64
|
+
export function tokenChildren(
|
|
65
|
+
p: Combinator<unknown>,
|
|
66
|
+
resolve?: (name: string) => Combinator<unknown> | undefined,
|
|
67
|
+
): Combinator<unknown>[] {
|
|
68
|
+
const d = p._def
|
|
69
|
+
const out: Combinator<unknown>[] = []
|
|
70
|
+
const push = (v: Combinator<unknown> | undefined): void => { if (v !== undefined) out.push(v) }
|
|
71
|
+
const rec = d as unknown as Record<string, unknown>
|
|
72
|
+
for (const k of ['parser', 'main', 'skipped', 'separator', 'selector', 'sentinel', 'fallback', 'otherwise', 'triviaParser']) {
|
|
73
|
+
const v = rec[k]
|
|
74
|
+
if (v !== null && typeof v === 'object' && '_def' in (v as object)) push(v as Combinator<unknown>)
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(rec.parsers)) for (const c of rec.parsers as Combinator<unknown>[]) push(c)
|
|
77
|
+
if (Array.isArray(rec.skip)) for (const c of rec.skip as Combinator<unknown>[]) push(c)
|
|
78
|
+
if (Array.isArray(rec.cases)) for (const c of rec.cases as Array<{ parser: Combinator<unknown> }>) push(c.parser)
|
|
79
|
+
if (Array.isArray(rec.matchers)) for (const c of rec.matchers as Array<{ parser: Combinator<unknown> }>) push(c.parser)
|
|
80
|
+
if (d.tag === 'lazy') {
|
|
81
|
+
let target: Combinator<unknown> | undefined
|
|
82
|
+
try { target = d.thunk() } catch { target = undefined }
|
|
83
|
+
if (target === undefined && resolve !== undefined) {
|
|
84
|
+
const name = (p as unknown as { _ruleName?: string })._ruleName
|
|
85
|
+
if (name !== undefined) target = resolve(name)
|
|
86
|
+
}
|
|
87
|
+
push(target)
|
|
88
|
+
}
|
|
89
|
+
return out
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Collect the alphabet over `roots`. Ids are assigned in first-encounter order,
|
|
94
|
+
* which is stable for a given grammar and therefore safe to switch on.
|
|
95
|
+
*/
|
|
96
|
+
export function collectAlphabet(
|
|
97
|
+
roots: ReadonlyArray<Combinator<unknown>>,
|
|
98
|
+
resolve?: (name: string) => Combinator<unknown> | undefined,
|
|
99
|
+
): Alphabet {
|
|
100
|
+
const terminals: TokenTerminal[] = []
|
|
101
|
+
const byKey = new Map<string, number>()
|
|
102
|
+
const originOf = new Map<number, Combinator<unknown>>()
|
|
103
|
+
const seen = new Set<Combinator<unknown>>()
|
|
104
|
+
|
|
105
|
+
const walk = (p: Combinator<unknown>): void => {
|
|
106
|
+
if (seen.has(p)) return
|
|
107
|
+
seen.add(p)
|
|
108
|
+
const key = keyOf(p._def)
|
|
109
|
+
if (key !== undefined && !byKey.has(key)) {
|
|
110
|
+
const id = FIRST_TERMINAL_ID + terminals.length
|
|
111
|
+
const t = terminalOf(p._def, id)
|
|
112
|
+
if (t !== undefined) {
|
|
113
|
+
byKey.set(key, id)
|
|
114
|
+
terminals.push(t)
|
|
115
|
+
originOf.set(id, p)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const c of tokenChildren(p, resolve)) walk(c)
|
|
119
|
+
}
|
|
120
|
+
for (const r of roots) walk(r)
|
|
121
|
+
return { terminals, byKey, originOf }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The id already assigned to this terminal, if it is one. */
|
|
125
|
+
export function terminalId(alphabet: Alphabet, p: Combinator<unknown>): number | undefined {
|
|
126
|
+
const key = keyOf(p._def)
|
|
127
|
+
return key === undefined ? undefined : alphabet.byKey.get(key)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The LEADING terminal of an arm — the one a decision point would consult. Walks
|
|
132
|
+
* through the wrappers that do not consume input, and through a sequence's
|
|
133
|
+
* nullable/zero-width prefix. Returns undefined when the lead is not a single
|
|
134
|
+
* derived terminal, which is the signal to stay scannerless at that site.
|
|
135
|
+
*/
|
|
136
|
+
export function leadTerminal(
|
|
137
|
+
p: Combinator<unknown>,
|
|
138
|
+
alphabet: Alphabet,
|
|
139
|
+
resolve?: (name: string) => Combinator<unknown> | undefined,
|
|
140
|
+
depth = 0,
|
|
141
|
+
): number | undefined {
|
|
142
|
+
if (depth > 24) return undefined
|
|
143
|
+
const direct = terminalId(alphabet, p)
|
|
144
|
+
if (direct !== undefined) return direct
|
|
145
|
+
const d = p._def
|
|
146
|
+
switch (d.tag) {
|
|
147
|
+
case 'sequence': {
|
|
148
|
+
for (const c of d.parsers) {
|
|
149
|
+
// CLASSIFY FIRST. The old order asked for the inner terminal and returned
|
|
150
|
+
// it before checking whether the term could consume anything, so:
|
|
151
|
+
//
|
|
152
|
+
// sequence(optional(X), Y) yielded X's terminal as THE lead -- but the
|
|
153
|
+
// input may legally start with Y's instead
|
|
154
|
+
// sequence(not(X), Y) yielded X's terminal -- from a NEGATIVE
|
|
155
|
+
// lookahead, which never consumes
|
|
156
|
+
//
|
|
157
|
+
// A single lead terminal cannot express "X's first set OR Y's", so a
|
|
158
|
+
// nullable prefix has no direct lead and the site must be REJECTED --
|
|
159
|
+
// scannerless gating handles it correctly. Zero-width prefixes are
|
|
160
|
+
// different: they consume nothing, so the next term genuinely leads.
|
|
161
|
+
const cd = c._def.tag
|
|
162
|
+
if (cd === 'not' || cd === 'peek' || cd === 'guard') continue
|
|
163
|
+
if (cd === 'optional' || cd === 'many') return undefined
|
|
164
|
+
return leadTerminal(c, alphabet, resolve, depth + 1)
|
|
165
|
+
}
|
|
166
|
+
return undefined
|
|
167
|
+
}
|
|
168
|
+
case 'sepBy':
|
|
169
|
+
return leadTerminal(d.parser, alphabet, resolve, depth + 1)
|
|
170
|
+
case 'lazy': {
|
|
171
|
+
let target: Combinator<unknown> | undefined
|
|
172
|
+
try { target = d.thunk() } catch { target = undefined }
|
|
173
|
+
if (target === undefined && resolve !== undefined) {
|
|
174
|
+
const name = (p as unknown as { _ruleName?: string })._ruleName
|
|
175
|
+
if (name !== undefined) target = resolve(name)
|
|
176
|
+
}
|
|
177
|
+
return target === undefined ? undefined : leadTerminal(target, alphabet, resolve, depth + 1)
|
|
178
|
+
}
|
|
179
|
+
case 'node': case 'field': case 'label': case 'transform': case 'leaf':
|
|
180
|
+
case 'token': case 'grammar': case 'attempt': case 'expect': case 'withCtx':
|
|
181
|
+
case 'oneOrMore': case 'trivia': case 'recover':
|
|
182
|
+
return leadTerminal(d.parser, alphabet, resolve, depth + 1)
|
|
183
|
+
default:
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The CANDIDATE SET for one decision point: the leading terminals of its arms.
|
|
190
|
+
* `complete` is false when any arm's lead is not a derived terminal — that site
|
|
191
|
+
* must stay scannerless, per-construct, exactly as `scanSkip` and first-set
|
|
192
|
+
* gating already apply per region rather than globally.
|
|
193
|
+
*/
|
|
194
|
+
export type CandidateSet = { ids: number[]; complete: boolean }
|
|
195
|
+
|
|
196
|
+
export function candidateSet(
|
|
197
|
+
arms: ReadonlyArray<Combinator<unknown>>,
|
|
198
|
+
alphabet: Alphabet,
|
|
199
|
+
resolve?: (name: string) => Combinator<unknown> | undefined,
|
|
200
|
+
): CandidateSet {
|
|
201
|
+
const ids: number[] = []
|
|
202
|
+
let complete = true
|
|
203
|
+
for (const a of arms) {
|
|
204
|
+
const t = leadTerminal(a, alphabet, resolve)
|
|
205
|
+
if (t === undefined) { complete = false; continue }
|
|
206
|
+
if (!ids.includes(t)) ids.push(t)
|
|
207
|
+
}
|
|
208
|
+
return { ids, complete }
|
|
209
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived tokenization, part 3: token-KEYED dispatch.
|
|
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.
|
|
55
|
+
*/
|
|
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
|
+
|
|
82
|
+
/**
|
|
83
|
+
* ASCII letter-only case fold. Used IDENTICALLY when building the tables and
|
|
84
|
+
* when reading input — the two must never disagree. The cheaper `c | 32` is
|
|
85
|
+
* wrong here: it maps '@'(64) to '`'(96), which silently stops every '@'-led
|
|
86
|
+
* key from ever matching, and maps '_'(95) to DEL(127), which would accept a
|
|
87
|
+
* character the key does not contain.
|
|
88
|
+
*/
|
|
89
|
+
export const foldExpr = (c: string): string => `(${c} >= 65 && ${c} <= 90 ? ${c} + 32 : ${c})`
|
|
90
|
+
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
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Pack a non-negative int array into a two-chars-per-value string literal.
|
|
99
|
+
*
|
|
100
|
+
* Two chars at six bits each is TWELVE BITS: the representable range is 0..4095.
|
|
101
|
+
* The mask made anything larger wrap SILENTLY, and `unpack` then decoded a wrong
|
|
102
|
+
* index — a table that looks fine and routes to the wrong arm. Bounded here, and
|
|
103
|
+
* this is the ONLY implementation: a second copy in token-scanner.ts had the same
|
|
104
|
+
* encoding and the same missing check, which is how one defect became two.
|
|
105
|
+
*/
|
|
106
|
+
export const PACK_MAX = 4095
|
|
107
|
+
export function packInts(values: readonly number[]): string {
|
|
108
|
+
let s = ''
|
|
109
|
+
for (const v of values) {
|
|
110
|
+
if (!Number.isInteger(v) || v < 0 || v > PACK_MAX) {
|
|
111
|
+
throw new RangeError(
|
|
112
|
+
`packInts: ${v} is outside the 12-bit range 0..${PACK_MAX} this encoding can represent. ` +
|
|
113
|
+
'Widen the encoding rather than letting the value wrap.',
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
s += String.fromCharCode(35 + (v & 63), 35 + ((v >> 6) & 63))
|
|
117
|
+
}
|
|
118
|
+
return JSON.stringify(s)
|
|
119
|
+
}
|
|
120
|
+
|
|
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
|
+
/**
|
|
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.
|
|
331
|
+
*/
|
|
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
|
|
353
|
+
}
|