@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.
- package/README.md +23 -0
- package/package.json +1 -1
- package/src/plugins/cn-theme.ts +36 -0
- package/src/plugins/sx-ast/index.ts +157 -14
- package/src/plugins/sx-ast/rewrite.ts +11 -2
- package/src/plugins/tw-compile.ts +152 -38
- package/src/styles/cn-vendor/NOTICE.md +9 -5
- package/src/styles/cn-vendor/compiler.ts +1160 -0
- package/src/styles/cn-vendor/config.ts +136 -0
- package/src/styles/cn-vendor/default-config.generated.ts +26 -0
- package/src/styles/cn-vendor/theme-css.ts +180 -0
- package/src/styles/cn-vendor/validators.ts +122 -0
|
@@ -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
|
+
}
|