@lovrozagar/flare 0.8.0 → 0.9.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.
@@ -0,0 +1,1160 @@
1
+ /**
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2026 shadcn
5
+ *
6
+ * Vendored from https://github.com/shadcn-ui/cn
7
+ * tag cn@0.4.0
8
+ * commit 84db83298f69a229d6f1ffc5d8c8d99ef451fb49
9
+ * Source: packages/cn/src/compiler.ts
10
+ */
11
+
12
+ // The cn compiler: turns a tailwind-merge–shaped config into the flat packed
13
+ // tables the engine consumes. Two outputs from one pipeline:
14
+ //
15
+ // compileToTables(config) → in-memory Tables (runtime custom configs)
16
+ // compileToSource(config) → packed ES module source (CLI / checked-in tables)
17
+ //
18
+ // Config definitions accept three value forms interchangeably:
19
+ // - plain strings / nested objects (as in tailwind-merge configs)
20
+ // - marker objects {$v: 'isNumber'} (validator) and {$t: 'spacing'} (theme
21
+ // scale) — the JSON-safe form used by the vendored default config
22
+ // - functions: tailwind-merge theme getters (fn.isThemeGetter) and validator
23
+ // predicates (any other function; compiled as custom validators)
24
+
25
+ import type { Tables, ValidatorImpls } from "./types.ts"
26
+ import * as refValidators from "./validators.ts"
27
+
28
+ export type ClassGroupDef =
29
+ | string
30
+ | { $v: string }
31
+ | { $t: string }
32
+ | ((value: string) => boolean)
33
+ | { [key: string]: readonly ClassGroupDef[] }
34
+
35
+ export interface CnConfig {
36
+ theme: Record<string, ClassGroupDef[]>
37
+ classGroups: Record<string, ClassGroupDef[]>
38
+ conflictingClassGroups: Record<string, readonly string[]>
39
+ conflictingClassGroupModifiers: Record<string, readonly string[]>
40
+ orderSensitiveModifiers: string[]
41
+ postfixLookupClassGroups?: readonly string[]
42
+ prefix?: string
43
+ }
44
+
45
+ type ConfigExtensionGroups = {
46
+ theme: Record<string, readonly ClassGroupDef[]>
47
+ classGroups: Record<string, readonly ClassGroupDef[]>
48
+ conflictingClassGroups: Record<string, readonly string[]>
49
+ conflictingClassGroupModifiers: Record<string, readonly string[]>
50
+ orderSensitiveModifiers: readonly string[]
51
+ }
52
+
53
+ export interface ConfigExtension {
54
+ prefix?: string
55
+ cacheSize?: number
56
+ override?: Partial<ConfigExtensionGroups>
57
+ extend?: Partial<ConfigExtensionGroups>
58
+ }
59
+
60
+ /**
61
+ * Accepted config input: an `{ extend, override, prefix }` extension, a
62
+ * `(defaultConfig) => config` transform, or a complete config. Lives here
63
+ * (not in config.ts) so the `index` entry can re-export it without pulling
64
+ * config.ts into a shared declaration chunk that collides with the
65
+ * `cn/config` entry's own d.ts filename.
66
+ */
67
+ export type CreateCnInput =
68
+ ConfigExtension | ((config: CnConfig) => CnConfig) | CnConfig
69
+
70
+ const isMarker = (def: object, key: string): boolean => {
71
+ const keys = Object.keys(def)
72
+ return (
73
+ keys.length === 1 &&
74
+ keys[0] === key &&
75
+ typeof (def as never)[key] === "string"
76
+ )
77
+ }
78
+ const isThemeGetterFn = (
79
+ fn: unknown
80
+ ): fn is ((theme: object) => ClassGroupDef[]) & { isThemeGetter: true } =>
81
+ typeof fn === "function" &&
82
+ (fn as { isThemeGetter?: boolean }).isThemeGetter === true
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // mergeConfigs — extend/override semantics matching tailwind-merge
86
+ // ---------------------------------------------------------------------------
87
+
88
+ const cloneConfig = (config: CnConfig): CnConfig => ({
89
+ ...config,
90
+ theme: { ...config.theme },
91
+ classGroups: { ...config.classGroups },
92
+ conflictingClassGroups: { ...config.conflictingClassGroups },
93
+ conflictingClassGroupModifiers: { ...config.conflictingClassGroupModifiers },
94
+ orderSensitiveModifiers: [...config.orderSensitiveModifiers],
95
+ postfixLookupClassGroups: [...(config.postfixLookupClassGroups ?? [])],
96
+ })
97
+
98
+ export const mergeConfigs = (
99
+ base: CnConfig,
100
+ extension: ConfigExtension
101
+ ): CnConfig => {
102
+ const config = cloneConfig(base)
103
+ if (extension.prefix !== undefined) config.prefix = extension.prefix
104
+
105
+ const overrideProps = <V>(
106
+ target: Record<string, V>,
107
+ src?: Record<string, V>
108
+ ) => {
109
+ if (!src) return
110
+ for (const key in src) {
111
+ if (src[key] !== undefined) target[key] = src[key]
112
+ }
113
+ }
114
+ const ov = extension.override
115
+ if (ov) {
116
+ if (ov.orderSensitiveModifiers)
117
+ config.orderSensitiveModifiers = [...ov.orderSensitiveModifiers]
118
+ overrideProps(config.theme, ov.theme)
119
+ overrideProps(config.classGroups, ov.classGroups)
120
+ overrideProps(config.conflictingClassGroups, ov.conflictingClassGroups)
121
+ overrideProps(
122
+ config.conflictingClassGroupModifiers,
123
+ ov.conflictingClassGroupModifiers
124
+ )
125
+ }
126
+
127
+ const extendArrays = <V>(
128
+ target: Record<string, readonly V[]>,
129
+ src?: Record<string, readonly V[]>
130
+ ) => {
131
+ if (!src) return
132
+ for (const key in src) {
133
+ const add = src[key]
134
+ if (add) target[key] = (target[key] ?? []).concat(add)
135
+ }
136
+ }
137
+ const ex = extension.extend
138
+ if (ex) {
139
+ if (ex.orderSensitiveModifiers) {
140
+ config.orderSensitiveModifiers = [
141
+ ...config.orderSensitiveModifiers,
142
+ ...ex.orderSensitiveModifiers,
143
+ ]
144
+ }
145
+ extendArrays(config.theme, ex.theme)
146
+ extendArrays(config.classGroups, ex.classGroups)
147
+ extendArrays(config.conflictingClassGroups, ex.conflictingClassGroups)
148
+ extendArrays(
149
+ config.conflictingClassGroupModifiers,
150
+ ex.conflictingClassGroupModifiers
151
+ )
152
+ }
153
+ return config
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // validator resolution
158
+ // ---------------------------------------------------------------------------
159
+
160
+ // span-opcode ids — must mirror the engine's runValidator switch
161
+ const OPS: Record<string, number> = {
162
+ isAny: 0,
163
+ isAnyNonArbitrary: 1,
164
+ isArbitraryValue: 2,
165
+ isArbitraryVariable: 3,
166
+ isFraction: 4,
167
+ isNumber: 5,
168
+ isInteger: 6,
169
+ isPercent: 7,
170
+ isTshirtSize: 8,
171
+ isNamedContainerQuery: 9,
172
+ isArbitraryLength: 10,
173
+ isArbitraryNumber: 11,
174
+ isArbitraryWeight: 12,
175
+ isArbitraryFamilyName: 13,
176
+ isArbitraryPosition: 14,
177
+ isArbitrarySize: 15,
178
+ isArbitraryImage: 16,
179
+ isArbitraryShadow: 17,
180
+ isArbitraryVariableLength: 18,
181
+ isArbitraryVariableFamilyName: 19,
182
+ isArbitraryVariablePosition: 20,
183
+ isArbitraryVariableSize: 21,
184
+ isArbitraryVariableImage: 22,
185
+ isArbitraryVariableShadow: 23,
186
+ isArbitraryVariableWeight: 24,
187
+ }
188
+ const CUSTOM_OP_BASE = 25
189
+
190
+ interface ValidatorRegistry {
191
+ /** ordered unique validator names (opcode names or generated custom names) */
192
+ names: string[]
193
+ idByName: Map<string, number>
194
+ /** custom validator implementations, keyed by generated name */
195
+ impls: ValidatorImpls
196
+ fnName: Map<(value: string) => boolean, string>
197
+ /** name → plain-string predicate, for the compiler's own classifier */
198
+ classifierFns: Map<string, (value: string) => boolean>
199
+ }
200
+
201
+ const newRegistry = (): ValidatorRegistry => ({
202
+ names: [],
203
+ idByName: new Map(),
204
+ impls: {},
205
+ fnName: new Map(),
206
+ classifierFns: new Map(),
207
+ })
208
+
209
+ const validatorIdFor = (
210
+ reg: ValidatorRegistry,
211
+ name: string,
212
+ impl: (value: string) => boolean
213
+ ): number => {
214
+ let id = reg.idByName.get(name)
215
+ if (id === undefined) {
216
+ id = reg.names.length
217
+ reg.names.push(name)
218
+ reg.idByName.set(name, id)
219
+ reg.classifierFns.set(name, impl)
220
+ }
221
+ return id
222
+ }
223
+
224
+ const resolveValidator = (
225
+ reg: ValidatorRegistry,
226
+ def: { $v: string } | ((value: string) => boolean)
227
+ ): number => {
228
+ if (typeof def === "function") {
229
+ let name = reg.fnName.get(def)
230
+ if (name === undefined) {
231
+ name = "$c" + reg.fnName.size
232
+ reg.fnName.set(def, name)
233
+ reg.impls[name] = def
234
+ }
235
+ return validatorIdFor(reg, name, def)
236
+ }
237
+ const name = def.$v
238
+ const ref = (
239
+ refValidators as unknown as Record<string, (value: string) => boolean>
240
+ )[name]
241
+ if (OPS[name] === undefined || !ref)
242
+ throw new Error(`cn: unknown validator "${name}"`)
243
+ return validatorIdFor(reg, name, ref)
244
+ }
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // part-trie construction (mirrors tailwind-merge's createClassMap)
248
+ // ---------------------------------------------------------------------------
249
+
250
+ interface PartNode {
251
+ nextPart: Map<string, PartNode>
252
+ validators: { validatorId: number; groupId: number }[] | null
253
+ classGroupId: number
254
+ lit: { tail: string; gid: number }[]
255
+ }
256
+ const newPartNode = (): PartNode => ({
257
+ nextPart: new Map(),
258
+ validators: null,
259
+ classGroupId: -1,
260
+ lit: [],
261
+ })
262
+
263
+ const expandTheme = (config: CnConfig, key: string): readonly ClassGroupDef[] =>
264
+ config.theme[key] ?? []
265
+
266
+ const buildPartTrie = (
267
+ config: CnConfig,
268
+ reg: ValidatorRegistry,
269
+ groupId: (name: string) => number
270
+ ): PartNode => {
271
+ const root = newPartNode()
272
+ const getPart = (node: PartNode, path: string): PartNode => {
273
+ for (const part of path.split("-")) {
274
+ let next = node.nextPart.get(part)
275
+ if (!next) {
276
+ next = newPartNode()
277
+ node.nextPart.set(part, next)
278
+ }
279
+ node = next
280
+ }
281
+ return node
282
+ }
283
+ const process = (def: ClassGroupDef, node: PartNode, gid: number): void => {
284
+ if (typeof def === "string") {
285
+ const target = def === "" ? node : getPart(node, def)
286
+ target.classGroupId = gid
287
+ return
288
+ }
289
+ if (typeof def === "function") {
290
+ if (isThemeGetterFn(def)) {
291
+ for (const inner of def(config.theme)) process(inner, node, gid)
292
+ return
293
+ }
294
+ ;(node.validators ??= []).push({
295
+ validatorId: resolveValidator(reg, def),
296
+ groupId: gid,
297
+ })
298
+ return
299
+ }
300
+ if (isMarker(def, "$t")) {
301
+ for (const inner of expandTheme(config, (def as { $t: string }).$t))
302
+ process(inner, node, gid)
303
+ return
304
+ }
305
+ if (isMarker(def, "$v")) {
306
+ ;(node.validators ??= []).push({
307
+ validatorId: resolveValidator(reg, def as { $v: string }),
308
+ groupId: gid,
309
+ })
310
+ return
311
+ }
312
+ for (const [key, value] of Object.entries(
313
+ def as { [key: string]: readonly ClassGroupDef[] }
314
+ )) {
315
+ const child = getPart(node, key)
316
+ for (const inner of value) process(inner, child, gid)
317
+ }
318
+ }
319
+ for (const [name, group] of Object.entries(config.classGroups)) {
320
+ const gid = groupId(name)
321
+ for (const def of group) process(def, root, gid)
322
+ }
323
+ return root
324
+ }
325
+
326
+ // ---------------------------------------------------------------------------
327
+ // subsetting — classify a corpus against the full config, drop unused groups
328
+ // ---------------------------------------------------------------------------
329
+
330
+ export interface SubsetResult {
331
+ config: CnConfig
332
+ usedGroups: number
333
+ totalGroups: number
334
+ }
335
+
336
+ export const subsetConfig = (
337
+ base: CnConfig,
338
+ tokens: Iterable<string>
339
+ ): SubsetResult => {
340
+ const config = cloneConfig(base)
341
+ const reg = newRegistry()
342
+ const groupNames: string[] = []
343
+ const idByName = new Map<string, number>()
344
+ const gidOf = (name: string) => {
345
+ let id = idByName.get(name)
346
+ if (id === undefined) {
347
+ id = groupNames.length
348
+ groupNames.push(name)
349
+ idByName.set(name, id)
350
+ }
351
+ return id
352
+ }
353
+ const root = buildPartTrie(config, reg, gidOf)
354
+
355
+ const walk = (parts: string[], idx: number, node: PartNode): number => {
356
+ if (idx === parts.length) return node.classGroupId
357
+ const next = node.nextPart.get(parts[idx]!)
358
+ if (next) {
359
+ const r = walk(parts, idx + 1, next)
360
+ if (r >= 0) return r
361
+ }
362
+ // lifted-literal check is unnecessary here: subsetting runs before lifting
363
+ if (!node.validators) return -1
364
+ const rest = parts.slice(idx).join("-")
365
+ for (const { validatorId, groupId } of node.validators) {
366
+ const fn = reg.classifierFns.get(reg.names[validatorId]!)!
367
+ if (fn(rest)) return groupId
368
+ }
369
+ return -1
370
+ }
371
+ const classify = (bareBase: string): number => {
372
+ if (bareBase.startsWith("[") && bareBase.endsWith("]")) return -1 // arbitrary property: dynamic group
373
+ const parts = bareBase.split("-")
374
+ return walk(parts, parts[0] === "" && parts.length > 1 ? 1 : 0, root)
375
+ }
376
+
377
+ const prefix = config.prefix ? config.prefix + ":" : null
378
+ const used = new Set<string>()
379
+ for (let token of tokens) {
380
+ if (!token) continue
381
+ if (prefix) {
382
+ if (!token.startsWith(prefix)) continue
383
+ token = token.slice(prefix.length)
384
+ }
385
+ let dB = 0
386
+ let dP = 0
387
+ let lastColon = -1
388
+ let lastSlash = -1
389
+ for (let i = 0; i < token.length; i++) {
390
+ const c = token[i]
391
+ if (dB === 0 && dP === 0) {
392
+ if (c === ":") lastColon = i
393
+ else if (c === "/") lastSlash = i
394
+ }
395
+ if (c === "[") dB++
396
+ else if (c === "]") dB--
397
+ else if (c === "(") dP++
398
+ else if (c === ")") dP--
399
+ }
400
+ let bare = token.slice(lastColon + 1)
401
+ if (bare.endsWith("!")) bare = bare.slice(0, -1)
402
+ else if (bare.startsWith("!")) bare = bare.slice(1)
403
+ const candidates =
404
+ lastSlash > lastColon
405
+ ? [bare, token.slice(lastColon + 1, lastSlash).replace(/^!/, "")]
406
+ : [bare]
407
+ for (const cand of candidates) {
408
+ const g = classify(cand)
409
+ if (g >= 0) used.add(groupNames[g]!)
410
+ }
411
+ }
412
+
413
+ const totalGroups = Object.keys(config.classGroups).length
414
+ for (const key of Object.keys(config.classGroups)) {
415
+ if (!used.has(key)) delete config.classGroups[key]
416
+ }
417
+ for (const key of Object.keys(config.conflictingClassGroups)) {
418
+ if (!used.has(key)) delete config.conflictingClassGroups[key]
419
+ }
420
+ for (const key of Object.keys(config.conflictingClassGroupModifiers)) {
421
+ if (!used.has(key)) delete config.conflictingClassGroupModifiers[key]
422
+ }
423
+ return { config, usedGroups: used.size, totalGroups }
424
+ }
425
+
426
+ // ---------------------------------------------------------------------------
427
+ // the compile pipeline: part-trie → lift → char-trie → radix → flat model
428
+ // ---------------------------------------------------------------------------
429
+
430
+ interface CompiledModel {
431
+ G: number
432
+ customNames: string[]
433
+ impls: ValidatorImpls
434
+ edgeCounts: Int32Array
435
+ edgeLabelLen: Int32Array
436
+ labelText: string
437
+ edgeTargetActual: number[]
438
+ nodeGroup: Int32Array
439
+ nodeVlist: Int32Array
440
+ nodeCount: number
441
+ totalEdges: number
442
+ patCounts: number[]
443
+ patOps: number[]
444
+ listPat: number[]
445
+ vlistGroup: Int32Array
446
+ litEntries: { anchor: number; tail: string; gid: number }[]
447
+ sets: string[][]
448
+ attachments: { anchor: number; gid: number; set: number }[]
449
+ uniqueTailCount: number
450
+ adjGid: number[]
451
+ adjCnt: number[]
452
+ adjTgt: number[]
453
+ patGid: number[]
454
+ patTgt: number[]
455
+ postfixLookup: number[]
456
+ orderSensitiveModifiers: string
457
+ prefix?: string
458
+ }
459
+
460
+ export const compileModel = (config: CnConfig): CompiledModel => {
461
+ const reg = newRegistry()
462
+ const groupNames: string[] = []
463
+ const groupIdByName = new Map<string, number>()
464
+ const groupId = (name: string): number => {
465
+ let id = groupIdByName.get(name)
466
+ if (id === undefined) {
467
+ id = groupNames.length
468
+ groupNames.push(name)
469
+ groupIdByName.set(name, id)
470
+ }
471
+ return id
472
+ }
473
+
474
+ const partRoot = buildPartTrie(config, reg, groupId)
475
+
476
+ // ---- subtree literal lifting ----
477
+ const isLiftable = (node: PartNode): boolean => {
478
+ if (node.validators) return false
479
+ for (const child of node.nextPart.values())
480
+ if (!isLiftable(child)) return false
481
+ return true
482
+ }
483
+ const collectLifted = (
484
+ node: PartNode,
485
+ prefix: string,
486
+ out: { tail: string; gid: number }[]
487
+ ) => {
488
+ if (node.classGroupId >= 0)
489
+ out.push({ tail: prefix, gid: node.classGroupId })
490
+ for (const [part, child] of node.nextPart)
491
+ collectLifted(child, prefix + "-" + part, out)
492
+ }
493
+ const pruneNode = (node: PartNode) => {
494
+ for (const [part, child] of [...node.nextPart]) {
495
+ if (isLiftable(child)) {
496
+ collectLifted(child, part, node.lit)
497
+ node.nextPart.delete(part)
498
+ } else {
499
+ pruneNode(child)
500
+ }
501
+ }
502
+ }
503
+ pruneNode(partRoot)
504
+
505
+ // ---- flatten pruned part-trie → char-level trie ----
506
+ interface CharNode {
507
+ edges: Map<number, number>
508
+ groupId: number
509
+ vlist: number
510
+ }
511
+ const charNodes: CharNode[] = [{ edges: new Map(), groupId: -1, vlist: -1 }]
512
+ const newCharNode = (): number => {
513
+ charNodes.push({ edges: new Map(), groupId: -1, vlist: -1 })
514
+ return charNodes.length - 1
515
+ }
516
+ const vlists: [number, number][][] = []
517
+ const vlistIndex = new Map<string, number>()
518
+ const internVlist = (
519
+ list: { validatorId: number; groupId: number }[]
520
+ ): number => {
521
+ const key = list.map((e) => e.validatorId + ":" + e.groupId).join(",")
522
+ let idx = vlistIndex.get(key)
523
+ if (idx === undefined) {
524
+ idx = vlists.length
525
+ vlists.push(list.map((e) => [e.validatorId, e.groupId]))
526
+ vlistIndex.set(key, idx)
527
+ }
528
+ return idx
529
+ }
530
+ const insertChars = (fromIdx: number, str: string): number => {
531
+ let cur = fromIdx
532
+ for (let i = 0; i < str.length; i++) {
533
+ const c = str.charCodeAt(i)
534
+ let next = charNodes[cur]!.edges.get(c)
535
+ if (next === undefined) {
536
+ next = newCharNode()
537
+ charNodes[cur]!.edges.set(c, next)
538
+ }
539
+ cur = next
540
+ }
541
+ return cur
542
+ }
543
+ const DASH = 45
544
+ const litEntries: { anchor: number; tail: string; gid: number }[] = []
545
+ const flatten = (partNode: PartNode, charIdx: number, isRoot: boolean) => {
546
+ if (partNode.classGroupId >= 0)
547
+ charNodes[charIdx]!.groupId = partNode.classGroupId
548
+ if (partNode.validators)
549
+ charNodes[charIdx]!.vlist = internVlist(partNode.validators)
550
+ for (const { tail, gid } of partNode.lit)
551
+ litEntries.push({ anchor: charIdx, tail, gid })
552
+ for (const [part, child] of partNode.nextPart) {
553
+ let entry = charIdx
554
+ if (!isRoot) {
555
+ let dashNode = charNodes[charIdx]!.edges.get(DASH)
556
+ if (dashNode === undefined) {
557
+ dashNode = newCharNode()
558
+ charNodes[charIdx]!.edges.set(DASH, dashNode)
559
+ }
560
+ entry = dashNode
561
+ }
562
+ const childIdx = insertChars(entry, part)
563
+ flatten(child, childIdx, false)
564
+ }
565
+ }
566
+ flatten(partRoot, 0, true)
567
+
568
+ // ---- radix-collapse chains ----
569
+ const litAnchorSet = new Set(litEntries.map((e) => e.anchor))
570
+ const annotated = (i: number): boolean =>
571
+ charNodes[i]!.groupId >= 0 ||
572
+ charNodes[i]!.vlist >= 0 ||
573
+ litAnchorSet.has(i)
574
+
575
+ interface RadixNode {
576
+ edges: { label: string; oldTarget: number }[]
577
+ groupId: number
578
+ vlist: number
579
+ }
580
+ const oldToNew = new Map<number, number>()
581
+ const radixNodes: RadixNode[] = []
582
+ const buildRadix = (oldId: number): number => {
583
+ const newId = radixNodes.length
584
+ oldToNew.set(oldId, newId)
585
+ const n: RadixNode = {
586
+ edges: [],
587
+ groupId: charNodes[oldId]!.groupId,
588
+ vlist: charNodes[oldId]!.vlist,
589
+ }
590
+ radixNodes.push(n)
591
+ const sorted = [...charNodes[oldId]!.edges.entries()].sort(
592
+ (a, b) => a[0] - b[0]
593
+ )
594
+ for (const [c, t0] of sorted) {
595
+ let label = String.fromCharCode(c)
596
+ let t = t0
597
+ while (charNodes[t]!.edges.size === 1 && !annotated(t)) {
598
+ const [[c2, t2]] = charNodes[t]!.edges.entries() as unknown as [
599
+ [number, number],
600
+ ]
601
+ label += String.fromCharCode(c2)
602
+ t = t2
603
+ }
604
+ n.edges.push({ label, oldTarget: t })
605
+ }
606
+ for (const e of n.edges) buildRadix(e.oldTarget)
607
+ return newId
608
+ }
609
+ buildRadix(0)
610
+ const nodeCount = radixNodes.length
611
+ let totalEdges = 0
612
+ for (const n of radixNodes) totalEdges += n.edges.length
613
+
614
+ const edgeCounts = new Int32Array(nodeCount)
615
+ const edgeLabelLen = new Int32Array(totalEdges)
616
+ let labelText = ""
617
+ const nodeGroup = new Int32Array(nodeCount)
618
+ const nodeVlist = new Int32Array(nodeCount)
619
+ const edgeTargetActual: number[] = []
620
+ {
621
+ let e = 0
622
+ for (let i = 0; i < nodeCount; i++) {
623
+ const n = radixNodes[i]!
624
+ edgeCounts[i] = n.edges.length
625
+ for (const edge of n.edges) {
626
+ edgeLabelLen[e] = edge.label.length
627
+ labelText += edge.label
628
+ edgeTargetActual.push(oldToNew.get(edge.oldTarget)!)
629
+ e++
630
+ }
631
+ nodeGroup[i] = n.groupId
632
+ nodeVlist[i] = n.vlist
633
+ }
634
+ }
635
+ // verify: pre-order tree ⇒ targets derivable from edge counts alone
636
+ {
637
+ const sizes = new Int32Array(nodeCount)
638
+ for (let i = nodeCount - 1; i >= 0; i--) {
639
+ let s = 1
640
+ let c = i + 1
641
+ for (let k = 0; k < edgeCounts[i]!; k++) {
642
+ s += sizes[c]!
643
+ c += sizes[c]!
644
+ }
645
+ sizes[i] = s
646
+ }
647
+ let e = 0
648
+ for (let i = 0; i < nodeCount; i++) {
649
+ let c = i + 1
650
+ for (let k = 0; k < edgeCounts[i]!; k++) {
651
+ if (c !== edgeTargetActual[e])
652
+ throw new Error(`cn compiler: edge target mismatch at ${e}`)
653
+ c += sizes[c]!
654
+ e++
655
+ }
656
+ }
657
+ }
658
+ // remap lit anchors to radix node ids
659
+ for (const en of litEntries) en.anchor = oldToNew.get(en.anchor)!
660
+
661
+ // reorder vlist ids by first use in node order (ascending sparse stream)
662
+ {
663
+ const newId = new Int32Array(vlists.length).fill(-1)
664
+ const order: [number, number][][] = []
665
+ for (let i = 0; i < nodeCount; i++) {
666
+ const v = nodeVlist[i]!
667
+ if (v >= 0) {
668
+ if (newId[v] === -1) {
669
+ newId[v] = order.length
670
+ order.push(vlists[v]!)
671
+ }
672
+ nodeVlist[i] = newId[v]!
673
+ }
674
+ }
675
+ vlists.length = 0
676
+ vlists.push(...order)
677
+ }
678
+
679
+ let totalV = 0
680
+ for (const l of vlists) totalV += l.length
681
+ const vlistValidator = new Int32Array(totalV)
682
+ const vlistGroup = new Int32Array(totalV)
683
+ {
684
+ let v = 0
685
+ for (const l of vlists)
686
+ for (const [vid, gid] of l) {
687
+ vlistValidator[v] = vid
688
+ vlistGroup[v] = gid
689
+ v++
690
+ }
691
+ }
692
+
693
+ // ---- literal entries → tail sets + attachments ----
694
+ litEntries.sort(
695
+ (a, b) => a.anchor - b.anchor || a.gid - b.gid || (a.tail < b.tail ? -1 : 1)
696
+ )
697
+ interface Attachment {
698
+ anchor: number
699
+ gid: number
700
+ tails: string[]
701
+ set: number
702
+ }
703
+ const attachments: Attachment[] = []
704
+ for (const e of litEntries) {
705
+ const last = attachments[attachments.length - 1]
706
+ if (last && last.anchor === e.anchor && last.gid === e.gid)
707
+ last.tails.push(e.tail)
708
+ else
709
+ attachments.push({
710
+ anchor: e.anchor,
711
+ gid: e.gid,
712
+ tails: [e.tail],
713
+ set: -1,
714
+ })
715
+ }
716
+ const setIndex = new Map<string, number>()
717
+ const sets: string[][] = []
718
+ for (const a of attachments) {
719
+ const key = a.tails.join(" ")
720
+ let s = setIndex.get(key)
721
+ if (s === undefined) {
722
+ s = sets.length
723
+ setIndex.set(key, s)
724
+ sets.push(a.tails)
725
+ }
726
+ a.set = s
727
+ }
728
+ for (const s of sets.flat()) {
729
+ if (s.includes("|") || s.includes(" "))
730
+ throw new Error("cn compiler: tail contains delimiter: " + s)
731
+ }
732
+ const uniqueTailCount = new Set(sets.flat()).size
733
+ attachments.sort((x, y) => x.anchor - y.anchor || x.set - y.set)
734
+
735
+ // ---- renumber group ids by first emission ----
736
+ for (const targets of Object.values(config.conflictingClassGroups))
737
+ for (const n of targets) groupId(n)
738
+ for (const targets of Object.values(config.conflictingClassGroupModifiers))
739
+ for (const n of targets) groupId(n)
740
+ for (const n of config.postfixLookupClassGroups ?? []) groupId(n)
741
+ const G = groupNames.length
742
+ const remap = new Int32Array(G).fill(-1)
743
+ let nextNewGid = 0
744
+ const renum = (old: number): number => {
745
+ if (remap[old] === -1) remap[old] = nextNewGid++
746
+ return remap[old]!
747
+ }
748
+ for (let i = 0; i < vlistGroup.length; i++)
749
+ vlistGroup[i] = renum(vlistGroup[i]!)
750
+ for (const a of attachments) a.gid = renum(a.gid)
751
+ for (let i = 0; i < nodeGroup.length; i++)
752
+ if (nodeGroup[i]! >= 0) nodeGroup[i] = renum(nodeGroup[i]!)
753
+ for (let g = 0; g < G; g++) if (remap[g] === -1) remap[g] = nextNewGid++
754
+ const newGroupName: string[] = new Array(G)
755
+ for (let g = 0; g < G; g++) newGroupName[remap[g]!] = groupNames[g]!
756
+ // litEntries carry pre-renumber gids; rebuild them from attachments below
757
+
758
+ // ---- conflict adjacency ----
759
+ const adjGid: number[] = []
760
+ const adjCnt: number[] = []
761
+ const adjTgt: number[] = []
762
+ const patGid: number[] = []
763
+ const patTgt: number[] = []
764
+ for (let ng = 0; ng < G; ng++) {
765
+ const name = newGroupName[ng]!
766
+ const base = (config.conflictingClassGroups[name] ?? []).map(
767
+ (n) => remap[groupIdByName.get(n)!]!
768
+ )
769
+ const mod = (config.conflictingClassGroupModifiers[name] ?? []).map(
770
+ (n) => remap[groupIdByName.get(n)!]!
771
+ )
772
+ if (base.length) {
773
+ adjGid.push(ng)
774
+ adjCnt.push(base.length)
775
+ adjTgt.push(...base)
776
+ }
777
+ for (const m2 of mod) {
778
+ patGid.push(ng)
779
+ patTgt.push(m2)
780
+ }
781
+ }
782
+ const postfixLookup = (config.postfixLookupClassGroups ?? []).map(
783
+ (n) => remap[groupIdByName.get(n)!]!
784
+ )
785
+
786
+ // ---- validator opcodes ----
787
+ const customNames: string[] = []
788
+ const vlistOp = new Int32Array(vlistValidator.length)
789
+ for (let i = 0; i < vlistValidator.length; i++) {
790
+ const name = reg.names[vlistValidator[i]!]!
791
+ const op = OPS[name]
792
+ if (op !== undefined) vlistOp[i] = op
793
+ else {
794
+ let ci = customNames.indexOf(name)
795
+ if (ci === -1) {
796
+ ci = customNames.length
797
+ customNames.push(name)
798
+ }
799
+ vlistOp[i] = CUSTOM_OP_BASE + ci
800
+ }
801
+ }
802
+ // factor op sequences into patterns
803
+ const patIndex = new Map<string, number>()
804
+ const patCounts: number[] = []
805
+ const patOps: number[] = []
806
+ const listPat: number[] = []
807
+ {
808
+ let k = 0
809
+ for (const l of vlists) {
810
+ const ops: number[] = []
811
+ for (let j = 0; j < l.length; j++) ops.push(vlistOp[k++]!)
812
+ const key = ops.join(",")
813
+ let p = patIndex.get(key)
814
+ if (p === undefined) {
815
+ p = patCounts.length
816
+ patIndex.set(key, p)
817
+ patCounts.push(ops.length)
818
+ patOps.push(...ops)
819
+ }
820
+ listPat.push(p)
821
+ }
822
+ }
823
+
824
+ const impls: ValidatorImpls = {}
825
+ for (const name of customNames) impls[name] = reg.classifierFns.get(name)!
826
+
827
+ return {
828
+ G,
829
+ customNames,
830
+ impls,
831
+ edgeCounts,
832
+ edgeLabelLen,
833
+ labelText,
834
+ edgeTargetActual,
835
+ nodeGroup,
836
+ nodeVlist,
837
+ nodeCount,
838
+ totalEdges,
839
+ patCounts,
840
+ patOps,
841
+ listPat,
842
+ vlistGroup,
843
+ litEntries,
844
+ sets,
845
+ attachments,
846
+ uniqueTailCount,
847
+ adjGid,
848
+ adjCnt,
849
+ adjTgt,
850
+ patGid,
851
+ patTgt,
852
+ postfixLookup,
853
+ orderSensitiveModifiers: config.orderSensitiveModifiers.join(" "),
854
+ prefix: config.prefix,
855
+ }
856
+ }
857
+
858
+ // ---------------------------------------------------------------------------
859
+ // output 1: in-memory Tables (runtime custom configs)
860
+ // ---------------------------------------------------------------------------
861
+
862
+ const prefixSums = (counts: ArrayLike<number>): Int32Array => {
863
+ const out = new Int32Array(counts.length + 1)
864
+ for (let i = 0; i < counts.length; i++) out[i + 1] = out[i]! + counts[i]!
865
+ return out
866
+ }
867
+
868
+ export interface CompiledTables {
869
+ tables: Tables
870
+ validatorImpls: ValidatorImpls
871
+ prefix?: string
872
+ }
873
+
874
+ export const compileToTables = (config: CnConfig): CompiledTables => {
875
+ const m = compileModel(config)
876
+ // literal pool: same interning walk the packed module performs at load
877
+ const litCount = m.litEntries.length
878
+ const litAnchor = new Int32Array(litCount)
879
+ const litGroup = new Int32Array(litCount)
880
+ const litPool = new Int32Array(litCount)
881
+ let poolText = ""
882
+ const poolOffsets = new Int32Array(m.uniqueTailCount * 2)
883
+ {
884
+ const tailRef = new Map<string, number>()
885
+ let nextRef = 0
886
+ let e = 0
887
+ for (const a of m.attachments) {
888
+ for (const tail of m.sets[a.set]!) {
889
+ let r = tailRef.get(tail)
890
+ if (r === undefined) {
891
+ r = nextRef++
892
+ tailRef.set(tail, r)
893
+ poolOffsets[r * 2] = poolText.length
894
+ poolOffsets[r * 2 + 1] = tail.length
895
+ poolText += tail
896
+ }
897
+ litAnchor[e] = a.anchor
898
+ litGroup[e] = a.gid
899
+ litPool[e] = r
900
+ e++
901
+ }
902
+ }
903
+ }
904
+ const tables: Tables = {
905
+ GROUP_COUNT: m.G,
906
+ edgeStart: prefixSums(m.edgeCounts),
907
+ labelStart: prefixSums(m.edgeLabelLen),
908
+ labelText: m.labelText,
909
+ edgeTarget: Int32Array.from(m.edgeTargetActual),
910
+ nodeGroup: m.nodeGroup,
911
+ nodeVlist: m.nodeVlist,
912
+ vlistPat: prefixSums(m.patCounts),
913
+ vlistOps: Int32Array.from(m.patOps),
914
+ vlistRef: Int32Array.from(m.listPat),
915
+ vlistGroup: m.vlistGroup,
916
+ litAnchor,
917
+ litGroup,
918
+ litPool,
919
+ poolOffsets,
920
+ poolText,
921
+ adjGid: Int32Array.from(m.adjGid),
922
+ adjStart: prefixSums(m.adjCnt),
923
+ adjTgt: Int32Array.from(m.adjTgt),
924
+ patGid: Int32Array.from(m.patGid),
925
+ patTgt: Int32Array.from(m.patTgt),
926
+ postfixLookupGroups: Int32Array.from(m.postfixLookup),
927
+ customValidatorNames: m.customNames,
928
+ orderSensitiveModifiers: m.orderSensitiveModifiers,
929
+ }
930
+ return { tables, validatorImpls: m.impls, prefix: m.prefix }
931
+ }
932
+
933
+ // ---------------------------------------------------------------------------
934
+ // output 2: packed ES module source (CLI / checked-in default tables)
935
+ // ---------------------------------------------------------------------------
936
+
937
+ const PACK = 0x30
938
+ const packStr = (arr: ArrayLike<number>): string => {
939
+ const a = Array.from(arr as ArrayLike<number>)
940
+ for (const v of a)
941
+ if (v + PACK >= 0xd800 || v < 0)
942
+ throw new Error("cn compiler: unpackable value " + v)
943
+ let s = ""
944
+ for (let i = 0; i < a.length; i += 4096) {
945
+ s += String.fromCharCode(...a.slice(i, i + 4096).map((v) => v + PACK))
946
+ }
947
+ return JSON.stringify(s)
948
+ }
949
+ const plus1 = (arr: ArrayLike<number>) =>
950
+ Array.from(arr as ArrayLike<number>, (v) => v + 1)
951
+ const zig = (arr: ArrayLike<number>) =>
952
+ Array.from(arr as ArrayLike<number>, (v) => (v << 1) ^ (v >> 31))
953
+ const deltas = (arr: ArrayLike<number>) => {
954
+ let prev = 0
955
+ return Array.from(arr as ArrayLike<number>, (v) => {
956
+ const d = v - prev
957
+ prev = v
958
+ return d
959
+ })
960
+ }
961
+
962
+ export interface EmitOptions {
963
+ /** 'ts' annotates decoder helpers; 'js' emits plain JS (default 'js') */
964
+ lang?: "ts" | "js"
965
+ banner?: string
966
+ }
967
+
968
+ export const compileToSource = (
969
+ config: CnConfig,
970
+ options: EmitOptions = {}
971
+ ): string => {
972
+ const m = compileModel(config)
973
+ if (m.customNames.length > 0) {
974
+ throw new Error(
975
+ "cn compiler: configs with custom validator functions cannot be emitted as a module " +
976
+ `(functions are not serializable): ${m.customNames.join(", ")}. ` +
977
+ "Use createCn(config) at runtime instead."
978
+ )
979
+ }
980
+ const ts = options.lang === "ts"
981
+ // Indexed reads are asserted so the .ts output passes
982
+ // noUncheckedIndexedAccess in the project that imports it.
983
+ const b = ts ? "!" : ""
984
+ const sig = {
985
+ u: ts ? "(s: string, o = 0): Int32Array" : "(s, o = 0)",
986
+ ps: ts ? "(counts: Int32Array): Int32Array" : "(counts)",
987
+ dz: ts ? "(s: string): Int32Array" : "(s)",
988
+ }
989
+ // Sorted literal sets store their common prefix once.
990
+ const setsText = m.sets
991
+ .map((tails) => {
992
+ const first = tails[0]!
993
+ const last = tails[tails.length - 1]!
994
+ let length = 0
995
+ while (length < first.length && first[length] === last[length]) {
996
+ length++
997
+ }
998
+ return [
999
+ first.slice(0, length),
1000
+ ...tails.map((tail) => tail.slice(length)),
1001
+ ].join(" ")
1002
+ })
1003
+ .join("|")
1004
+ const attAnchorDelta: number[] = []
1005
+ const attGid: number[] = []
1006
+ const attSet: number[] = []
1007
+ {
1008
+ let prev = 0
1009
+ for (const a of m.attachments) {
1010
+ attAnchorDelta.push(a.anchor - prev)
1011
+ prev = a.anchor
1012
+ attGid.push(a.gid)
1013
+ attSet.push(a.set)
1014
+ }
1015
+ }
1016
+ const nodeVlistAnchors: number[] = []
1017
+ const nodeVlistValues: number[] = []
1018
+ for (let i = 0; i < m.nodeVlist.length; i++) {
1019
+ if (m.nodeVlist[i]! >= 0) {
1020
+ nodeVlistAnchors.push(i)
1021
+ nodeVlistValues.push(m.nodeVlist[i]!)
1022
+ }
1023
+ }
1024
+ const banner =
1025
+ options.banner ?? "// GENERATED by the cn compiler. Do not edit."
1026
+ return `${banner}
1027
+ const P = ${PACK}
1028
+ const U = ${sig.u} => {
1029
+ const out = new Int32Array(s.length)
1030
+ for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) - P - o
1031
+ return out
1032
+ }
1033
+ const PS = ${sig.ps} => {
1034
+ const out = new Int32Array(counts.length + 1)
1035
+ for (let i = 0; i < counts.length; i++) out[i + 1] = out[i]${b} + counts[i]${b}
1036
+ return out
1037
+ }
1038
+ // zigzag-delta stream → running values
1039
+ const DZ = ${sig.dz} => {
1040
+ const out = new Int32Array(s.length)
1041
+ let a = 0
1042
+ for (let i = 0; i < s.length; i++) {
1043
+ const z = s.charCodeAt(i) - P
1044
+ a += (z >>> 1) ^ -(z & 1)
1045
+ out[i] = a
1046
+ }
1047
+ return out
1048
+ }
1049
+ const GROUP_COUNT = ${m.G}
1050
+ const customValidatorNames${ts ? ": string[]" : ""} = ${JSON.stringify(m.customNames)}
1051
+ const edgeStart = PS(U(${packStr(m.edgeCounts)}))
1052
+ const labelStart = PS(U(${packStr(m.edgeLabelLen)}))
1053
+ const labelText = ${JSON.stringify(m.labelText)}
1054
+ // pre-order tree: targets derived from edge counts via subtree sizes
1055
+ const edgeTarget = (() => {
1056
+ const N = edgeStart.length - 1
1057
+ const sizes = new Int32Array(N)
1058
+ for (let i = N - 1; i >= 0; i--) {
1059
+ let s = 1
1060
+ let c = i + 1
1061
+ for (let k = edgeStart[i]${b}; k < edgeStart[i + 1]${b}; k++) { s += sizes[c]${b}; c += sizes[c]${b} }
1062
+ sizes[i] = s
1063
+ }
1064
+ const out = new Int32Array(edgeStart[N]${b})
1065
+ let e = 0
1066
+ for (let i = 0; i < N; i++) {
1067
+ let c = i + 1
1068
+ for (let k = edgeStart[i]${b}; k < edgeStart[i + 1]${b}; k++) { out[e++] = c; c += sizes[c]${b} }
1069
+ }
1070
+ return out
1071
+ })()
1072
+ const nodeGroup = U(${packStr(plus1(m.nodeGroup))}, 1)
1073
+ // vlists = op-pattern pool + per-list refs; the engine indexes these directly
1074
+ const vlistPat = PS(U(${packStr(m.patCounts)}))
1075
+ const vlistOps = U(${packStr(m.patOps)})
1076
+ const vlistRef = U(${packStr(m.listPat)})
1077
+ const vlistGroup = DZ(${packStr(zig(deltas(m.vlistGroup)))})
1078
+ // nodeVlist rebuilt sparse: (anchor deltas, vlist ids)
1079
+ const nodeVlist = (() => {
1080
+ const out = new Int32Array(${m.nodeCount}).fill(-1)
1081
+ const A = DZ(${packStr(zig(deltas(nodeVlistAnchors)))})
1082
+ const V = DZ(${packStr(zig(deltas(nodeVlistValues)))})
1083
+ for (let i = 0; i < A.length; i++) out[A[i]${b}] = V[i]${b}
1084
+ return out
1085
+ })()
1086
+ const SETS = ${JSON.stringify(setsText)}.split('|').map((s) => {
1087
+ const tails = s.split(' ')
1088
+ const prefix = tails.shift()${b}
1089
+ for (let i = 0; i < tails.length; i++) {
1090
+ tails[i] = prefix + tails[i]
1091
+ }
1092
+ return tails
1093
+ })
1094
+ const AA = DZ(${packStr(zig(attAnchorDelta))})
1095
+ const AG = DZ(${packStr(zig(deltas(attGid)))})
1096
+ const AS = DZ(${packStr(zig(deltas(attSet)))})
1097
+ const litAnchor = new Int32Array(${m.litEntries.length})
1098
+ const litGroup = new Int32Array(${m.litEntries.length})
1099
+ const litPool = new Int32Array(${m.litEntries.length})
1100
+ let poolText = ''
1101
+ const poolOffsets = new Int32Array(${m.uniqueTailCount * 2})
1102
+ {
1103
+ const tailRef = new Map()
1104
+ let nextRef = 0
1105
+ let e = 0
1106
+ for (let i = 0; i < AA.length; i++) {
1107
+ for (const tail of SETS[AS[i]${b}]${b}) {
1108
+ let r = tailRef.get(tail)
1109
+ if (r === undefined) {
1110
+ r = nextRef++
1111
+ tailRef.set(tail, r)
1112
+ poolOffsets[r * 2] = poolText.length
1113
+ poolOffsets[r * 2 + 1] = tail.length
1114
+ poolText += tail
1115
+ }
1116
+ litAnchor[e] = AA[i]${b}
1117
+ litGroup[e] = AG[i]${b}
1118
+ litPool[e] = r
1119
+ e++
1120
+ }
1121
+ }
1122
+ }
1123
+ // conflict adjacency (engine builds claim bitmask CSR at init)
1124
+ const adjGid = DZ(${packStr(zig(deltas(m.adjGid)))})
1125
+ const adjStart = PS(U(${packStr(m.adjCnt)}))
1126
+ const adjTgt = DZ(${packStr(zig(deltas(m.adjTgt)))})
1127
+ const patGid = U(${packStr(m.patGid)})
1128
+ const patTgt = U(${packStr(m.patTgt)})
1129
+ const postfixLookupGroups = U(${packStr(m.postfixLookup)})
1130
+ const orderSensitiveModifiers = ${JSON.stringify(m.orderSensitiveModifiers)}
1131
+ export default {
1132
+ GROUP_COUNT, customValidatorNames, edgeStart, labelStart, labelText,
1133
+ edgeTarget, nodeGroup, nodeVlist, vlistPat, vlistOps, vlistRef, vlistGroup,
1134
+ litAnchor, litGroup, litPool, poolOffsets, poolText,
1135
+ adjGid, adjStart, adjTgt, patGid, patTgt, postfixLookupGroups,
1136
+ orderSensitiveModifiers,${m.prefix ? " prefix: " + JSON.stringify(m.prefix) + "," : ""}
1137
+ }
1138
+ `
1139
+ }
1140
+
1141
+ export interface CompileStats {
1142
+ groups: number
1143
+ nodes: number
1144
+ edges: number
1145
+ liftedLiterals: number
1146
+ uniqueTails: number
1147
+ vlists: number
1148
+ }
1149
+
1150
+ export const compileStats = (config: CnConfig): CompileStats => {
1151
+ const m = compileModel(config)
1152
+ return {
1153
+ groups: m.G,
1154
+ nodes: m.nodeCount,
1155
+ edges: m.totalEdges,
1156
+ liftedLiterals: m.litEntries.length,
1157
+ uniqueTails: m.uniqueTailCount,
1158
+ vlists: m.listPat.length,
1159
+ }
1160
+ }