@creationix/rex 0.1.4 → 0.3.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/rex.ts CHANGED
@@ -1,2422 +1,2579 @@
1
- import { createRequire } from "node:module";
2
-
3
- const require = createRequire(import.meta.url);
4
- const rexGrammarModule = require("./rex.ohm-bundle.cjs");
5
- const rexGrammar = rexGrammarModule?.default ?? rexGrammarModule;
6
-
7
- export const grammar = rexGrammar;
8
- export const semantics = rexGrammar.createSemantics();
9
-
10
- export type IRNode =
11
- | { type: "program"; body: IRNode[] }
12
- | { type: "identifier"; name: string }
13
- | { type: "self" }
14
- | { type: "selfDepth"; depth: number }
15
- | { type: "boolean"; value: boolean }
16
- | { type: "null" }
17
- | { type: "undefined" }
18
- | { type: "number"; raw: string; value: number }
19
- | { type: "string"; raw: string }
20
- | { type: "array"; items: IRNode[] }
21
- | { type: "arrayComprehension"; binding: IRBindingOrExpr; body: IRNode }
22
- | { type: "object"; entries: { key: IRNode; value: IRNode }[] }
23
- | {
24
- type: "objectComprehension";
25
- binding: IRBindingOrExpr;
26
- key: IRNode;
27
- value: IRNode;
28
- }
29
- | { type: "key"; name: string }
30
- | { type: "group"; expression: IRNode }
31
- | { type: "unary"; op: "neg" | "not" | "delete"; value: IRNode }
32
- | {
33
- type: "binary";
34
- op:
35
- | "add"
36
- | "sub"
37
- | "mul"
38
- | "div"
39
- | "mod"
40
- | "bitAnd"
41
- | "bitOr"
42
- | "bitXor"
43
- | "and"
44
- | "or"
45
- | "eq"
46
- | "neq"
47
- | "gt"
48
- | "gte"
49
- | "lt"
50
- | "lte";
51
- left: IRNode;
52
- right: IRNode;
53
- }
54
- | {
55
- type: "assign";
56
- op: "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^=";
57
- place: IRNode;
58
- value: IRNode;
59
- }
60
- | {
61
- type: "navigation";
62
- target: IRNode;
63
- segments: ({ type: "static"; key: string } | { type: "dynamic"; key: IRNode })[];
64
- }
65
- | { type: "call"; callee: IRNode; args: IRNode[] }
66
- | {
67
- type: "conditional";
68
- head: "when" | "unless";
69
- condition: IRNode;
70
- thenBlock: IRNode[];
71
- elseBranch?: IRConditionalElse;
72
- }
73
- | { type: "for"; binding: IRBindingOrExpr; body: IRNode[] }
74
- | { type: "break" }
75
- | { type: "continue" };
76
-
77
- export type IRBinding =
78
- | { type: "binding:keyValueIn"; key: string; value: string; source: IRNode }
79
- | { type: "binding:valueIn"; value: string; source: IRNode }
80
- | { type: "binding:keyOf"; key: string; source: IRNode };
81
-
82
- export type IRBindingOrExpr = IRBinding | { type: "binding:expr"; source: IRNode };
83
-
84
- export type IRConditionalElse =
85
- | { type: "else"; block: IRNode[] }
86
- | {
87
- type: "elseChain";
88
- head: "when" | "unless";
89
- condition: IRNode;
90
- thenBlock: IRNode[];
91
- elseBranch?: IRConditionalElse;
92
- };
93
-
94
- const DIGITS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_";
95
-
96
- function byteLength(value: string): number {
97
- return Buffer.byteLength(value, "utf8");
98
- }
99
-
100
- const OPCODE_IDS = {
101
- do: 0,
102
- add: 1,
103
- sub: 2,
104
- mul: 3,
105
- div: 4,
106
- eq: 5,
107
- neq: 6,
108
- lt: 7,
109
- lte: 8,
110
- gt: 9,
111
- gte: 10,
112
- and: 11,
113
- or: 12,
114
- xor: 13,
115
- not: 14,
116
- boolean: 15,
117
- number: 16,
118
- string: 17,
119
- array: 18,
120
- object: 19,
121
- mod: 20,
122
- neg: 21,
123
- } as const;
124
-
125
- type OpcodeName = keyof typeof OPCODE_IDS;
126
-
127
- type EncodeOptions = {
128
- domainRefs?: Record<string, number>;
129
- };
130
-
131
- type CompileOptions = {
132
- optimize?: boolean;
133
- minifyNames?: boolean;
134
- domainRefs?: Record<string, number>;
135
- dedupeValues?: boolean;
136
- dedupeMinBytes?: number;
137
- };
138
-
139
- const registeredDomainRefs: Record<string, number> = {};
140
-
141
- export function registerDomainExtensionRef(name: string, refId = 0) {
142
- if (!name) throw new Error("Domain extension name cannot be empty");
143
- if (!Number.isInteger(refId) || refId < 0) throw new Error(`Invalid domain extension ref id for '${name}': ${refId}`);
144
- registeredDomainRefs[name] = refId;
145
- }
146
-
147
- export function registerDomainExtensionRefs(refs: Record<string, number>) {
148
- for (const [name, refId] of Object.entries(refs)) {
149
- registerDomainExtensionRef(name, refId);
150
- }
151
- }
152
-
153
- export function clearDomainExtensionRefs() {
154
- for (const name of Object.keys(registeredDomainRefs)) delete registeredDomainRefs[name];
155
- }
156
-
157
- export function getRegisteredDomainExtensionRefs(): Record<string, number> {
158
- return { ...registeredDomainRefs };
159
- }
160
-
161
- function resolveDomainRefMap(overrides?: Record<string, number>): Record<string, number> | undefined {
162
- if (!overrides || Object.keys(overrides).length === 0) {
163
- return Object.keys(registeredDomainRefs).length > 0 ? { ...registeredDomainRefs } : undefined;
164
- }
165
- const merged = { ...registeredDomainRefs };
166
- for (const [name, refId] of Object.entries(overrides)) {
167
- if (!Number.isInteger(refId) || refId < 0) throw new Error(`Invalid domain extension ref id for '${name}': ${refId}`);
168
- merged[name] = refId;
169
- }
170
- return merged;
171
- }
172
-
173
- const BINARY_TO_OPCODE: Record<Extract<IRNode, { type: "binary" }> ["op"], OpcodeName> = {
174
- add: "add",
175
- sub: "sub",
176
- mul: "mul",
177
- div: "div",
178
- mod: "mod",
179
- bitAnd: "and",
180
- bitOr: "or",
181
- bitXor: "xor",
182
- and: "and",
183
- or: "or",
184
- eq: "eq",
185
- neq: "neq",
186
- gt: "gt",
187
- gte: "gte",
188
- lt: "lt",
189
- lte: "lte",
190
- };
191
-
192
- const ASSIGN_COMPOUND_TO_OPCODE: Partial<Record<Extract<IRNode, { type: "assign" }> ["op"], OpcodeName>> = {
193
- "+=": "add",
194
- "-=": "sub",
195
- "*=": "mul",
196
- "/=": "div",
197
- "%=": "mod",
198
- "&=": "and",
199
- "|=": "or",
200
- "^=": "xor",
201
- };
202
-
203
- function encodeUint(value: number): string {
204
- if (!Number.isInteger(value) || value < 0) throw new Error(`Cannot encode non-uint value: ${value}`);
205
- if (value === 0) return "";
206
- let current = value;
207
- let out = "";
208
- while (current > 0) {
209
- const digit = current % 64;
210
- out = `${DIGITS[digit]}${out}`;
211
- current = Math.floor(current / 64);
212
- }
213
- return out;
214
- }
215
-
216
- function encodeZigzag(value: number): string {
217
- if (!Number.isInteger(value)) throw new Error(`Cannot zigzag non-integer: ${value}`);
218
- const encoded = value >= 0 ? value * 2 : -value * 2 - 1;
219
- return encodeUint(encoded);
220
- }
221
-
222
- function encodeInt(value: number): string {
223
- return `${encodeZigzag(value)}+`;
224
- }
225
-
226
- function canUseBareString(value: string): boolean {
227
- for (const char of value) {
228
- if (!DIGITS.includes(char)) return false;
229
- }
230
- return true;
231
- }
232
-
233
- function decodeStringLiteral(raw: string): string {
234
- const quote = raw[0];
235
- if ((quote !== '"' && quote !== "'") || raw[raw.length - 1] !== quote) {
236
- throw new Error(`Invalid string literal: ${raw}`);
237
- }
238
- let out = "";
239
- for (let index = 1; index < raw.length - 1; index += 1) {
240
- const char = raw[index];
241
- if (char !== "\\") {
242
- out += char;
243
- continue;
244
- }
245
- index += 1;
246
- const esc = raw[index];
247
- if (esc === undefined) throw new Error(`Invalid escape sequence in ${raw}`);
248
- if (esc === "n") out += "\n";
249
- else if (esc === "r") out += "\r";
250
- else if (esc === "t") out += "\t";
251
- else if (esc === "b") out += "\b";
252
- else if (esc === "f") out += "\f";
253
- else if (esc === "v") out += "\v";
254
- else if (esc === "0") out += "\0";
255
- else if (esc === "x") {
256
- const hex = raw.slice(index + 1, index + 3);
257
- if (!/^[0-9a-fA-F]{2}$/.test(hex)) throw new Error(`Invalid hex escape in ${raw}`);
258
- out += String.fromCodePoint(parseInt(hex, 16));
259
- index += 2;
260
- }
261
- else if (esc === "u") {
262
- const hex = raw.slice(index + 1, index + 5);
263
- if (!/^[0-9a-fA-F]{4}$/.test(hex)) throw new Error(`Invalid unicode escape in ${raw}`);
264
- out += String.fromCodePoint(parseInt(hex, 16));
265
- index += 4;
266
- }
267
- else {
268
- out += esc;
269
- }
270
- }
271
- return out;
272
- }
273
-
274
- function encodeBareOrLengthString(value: string): string {
275
- if (canUseBareString(value)) return `${value}:`;
276
- return `${encodeUint(byteLength(value))},${value}`;
277
- }
278
-
279
- function encodeNumberNode(node: Extract<IRNode, { type: "number" }>): string {
280
- const numberValue = node.value;
281
- if (!Number.isFinite(numberValue)) throw new Error(`Cannot encode non-finite number: ${node.raw}`);
282
- if (Number.isInteger(numberValue)) return encodeInt(numberValue);
283
-
284
- const raw = node.raw.toLowerCase();
285
- const sign = raw.startsWith("-") ? -1 : 1;
286
- const unsigned = sign < 0 ? raw.slice(1) : raw;
287
- const splitExp = unsigned.split("e");
288
- const mantissaText = splitExp[0];
289
- const exponentText = splitExp[1] ?? "0";
290
- if (!mantissaText) throw new Error(`Invalid decimal literal: ${node.raw}`);
291
- const exponent = Number(exponentText);
292
- if (!Number.isInteger(exponent)) throw new Error(`Invalid decimal exponent: ${node.raw}`);
293
-
294
- const dotIndex = mantissaText.indexOf(".");
295
- const decimals = dotIndex === -1 ? 0 : mantissaText.length - dotIndex - 1;
296
- const digits = mantissaText.replace(".", "");
297
- if (!/^\d+$/.test(digits)) throw new Error(`Invalid decimal digits: ${node.raw}`);
298
-
299
- let significand = Number(digits) * sign;
300
- let power = exponent - decimals;
301
- while (significand !== 0 && significand % 10 === 0) {
302
- significand /= 10;
303
- power += 1;
304
- }
305
- return `${encodeZigzag(power)}*${encodeInt(significand)}`;
306
- }
307
-
308
- function encodeOpcode(opcode: OpcodeName): string {
309
- return `${encodeUint(OPCODE_IDS[opcode])}%`;
310
- }
311
-
312
- function encodeCallParts(parts: string[]): string {
313
- return `(${parts.join("")})`;
314
- }
315
-
316
- function needsOptionalPrefix(encoded: string): boolean {
317
- const first = encoded[0];
318
- if (!first) return false;
319
- return first === "[" || first === "{" || first === "(" || first === "=" || first === "~" || first === "?" || first === "!" || first === "|" || first === "&" || first === ">" || first === "<";
320
- }
321
-
322
- function addOptionalPrefix(encoded: string): string {
323
- if (!needsOptionalPrefix(encoded)) return encoded;
324
- let payload = encoded;
325
- if (encoded.startsWith("?(") || encoded.startsWith("!(") || encoded.startsWith("|(") || encoded.startsWith("&(") || encoded.startsWith(">(") || encoded.startsWith("<(")) {
326
- payload = encoded.slice(2, -1);
327
- }
328
- else if (encoded.startsWith(">[") || encoded.startsWith(">{")) {
329
- payload = encoded.slice(2, -1);
330
- }
331
- else if (encoded.startsWith("[") || encoded.startsWith("{") || encoded.startsWith("(")) {
332
- payload = encoded.slice(1, -1);
333
- }
334
- else if (encoded.startsWith("=") || encoded.startsWith("~")) {
335
- payload = encoded.slice(1);
336
- }
337
- return `${encodeUint(byteLength(payload))}${encoded}`;
338
- }
339
-
340
- function encodeBlockExpression(block: IRNode[]): string {
341
- if (block.length === 0) return "4'";
342
- if (block.length === 1) return encodeNode(block[0] as IRNode);
343
- return encodeCallParts([encodeOpcode("do"), ...block.map((node) => encodeNode(node))]);
344
- }
345
-
346
- function encodeConditionalElse(elseBranch: IRConditionalElse): string {
347
- if (elseBranch.type === "else") return encodeBlockExpression(elseBranch.block);
348
- const nested = {
349
- type: "conditional",
350
- head: elseBranch.head,
351
- condition: elseBranch.condition,
352
- thenBlock: elseBranch.thenBlock,
353
- elseBranch: elseBranch.elseBranch,
354
- } satisfies IRNode;
355
- return encodeNode(nested);
356
- }
357
-
358
- function encodeNavigation(node: Extract<IRNode, { type: "navigation" }>): string {
359
- const parts = [encodeNode(node.target)];
360
- for (const segment of node.segments) {
361
- if (segment.type === "static") parts.push(encodeBareOrLengthString(segment.key));
362
- else parts.push(encodeNode(segment.key));
363
- }
364
- return encodeCallParts(parts);
365
- }
366
-
367
- function encodeFor(node: Extract<IRNode, { type: "for" }>): string {
368
- const body = addOptionalPrefix(encodeBlockExpression(node.body));
369
- if (node.binding.type === "binding:expr") {
370
- return `>(${encodeNode(node.binding.source)}${body})`;
371
- }
372
- if (node.binding.type === "binding:valueIn") {
373
- return `>(${encodeNode(node.binding.source)}${node.binding.value}$${body})`;
374
- }
375
- if (node.binding.type === "binding:keyValueIn") {
376
- return `>(${encodeNode(node.binding.source)}${node.binding.key}$${node.binding.value}$${body})`;
377
- }
378
- return `<(${encodeNode(node.binding.source)}${node.binding.key}$${body})`;
379
- }
380
-
381
- function encodeArrayComprehension(node: Extract<IRNode, { type: "arrayComprehension" }>): string {
382
- const body = addOptionalPrefix(encodeNode(node.body));
383
- if (node.binding.type === "binding:expr") {
384
- return `>[${encodeNode(node.binding.source)}${body}]`;
385
- }
386
- if (node.binding.type === "binding:valueIn") {
387
- return `>[${encodeNode(node.binding.source)}${node.binding.value}$${body}]`;
388
- }
389
- if (node.binding.type === "binding:keyValueIn") {
390
- return `>[${encodeNode(node.binding.source)}${node.binding.key}$${node.binding.value}$${body}]`;
391
- }
392
- return `>[${encodeNode(node.binding.source)}${node.binding.key}$${body}]`;
393
- }
394
-
395
- function encodeObjectComprehension(node: Extract<IRNode, { type: "objectComprehension" }>): string {
396
- const key = addOptionalPrefix(encodeNode(node.key));
397
- const value = addOptionalPrefix(encodeNode(node.value));
398
- if (node.binding.type === "binding:expr") {
399
- return `>{${encodeNode(node.binding.source)}${key}${value}}`;
400
- }
401
- if (node.binding.type === "binding:valueIn") {
402
- return `>{${encodeNode(node.binding.source)}${node.binding.value}$${key}${value}}`;
403
- }
404
- if (node.binding.type === "binding:keyValueIn") {
405
- return `>{${encodeNode(node.binding.source)}${node.binding.key}$${node.binding.value}$${key}${value}}`;
406
- }
407
- return `>{${encodeNode(node.binding.source)}${node.binding.key}$${key}${value}}`;
408
- }
409
-
410
- let activeEncodeOptions: EncodeOptions | undefined;
411
-
412
- function encodeNode(node: IRNode): string {
413
- switch (node.type) {
414
- case "program":
415
- return encodeBlockExpression(node.body);
416
- case "identifier": {
417
- const domainRef = activeEncodeOptions?.domainRefs?.[node.name];
418
- if (domainRef !== undefined) return `${encodeUint(domainRef)}'`;
419
- return `${node.name}$`;
420
- }
421
- case "self":
422
- return "@";
423
- case "selfDepth": {
424
- if (!Number.isInteger(node.depth) || node.depth < 1) throw new Error(`Invalid self depth: ${node.depth}`);
425
- if (node.depth === 1) return "@";
426
- return `${encodeUint(node.depth - 1)}@`;
427
- }
428
- case "boolean":
429
- return node.value ? "1'" : "2'";
430
- case "null":
431
- return "3'";
432
- case "undefined":
433
- return "4'";
434
- case "number":
435
- return encodeNumberNode(node);
436
- case "string":
437
- return encodeBareOrLengthString(decodeStringLiteral(node.raw));
438
- case "array": {
439
- const body = node.items.map((item) => addOptionalPrefix(encodeNode(item))).join("");
440
- return `[${body}]`;
441
- }
442
- case "arrayComprehension":
443
- return encodeArrayComprehension(node);
444
- case "object": {
445
- const body = node.entries
446
- .map(({ key, value }) => `${encodeNode(key)}${addOptionalPrefix(encodeNode(value))}`)
447
- .join("");
448
- return `{${body}}`;
449
- }
450
- case "objectComprehension":
451
- return encodeObjectComprehension(node);
452
- case "key":
453
- return encodeBareOrLengthString(node.name);
454
- case "group":
455
- return encodeNode(node.expression);
456
- case "unary":
457
- if (node.op === "delete") return `~${encodeNode(node.value)}`;
458
- if (node.op === "neg") return encodeCallParts([encodeOpcode("neg"), encodeNode(node.value)]);
459
- return encodeCallParts([encodeOpcode("not"), encodeNode(node.value)]);
460
- case "binary":
461
- if (node.op === "and") {
462
- const operands = collectLogicalChain(node, "and");
463
- const body = operands
464
- .map((operand, index) => {
465
- const encoded = encodeNode(operand);
466
- return index === 0 ? encoded : addOptionalPrefix(encoded);
467
- })
468
- .join("");
469
- return `&(${body})`;
470
- }
471
- if (node.op === "or") {
472
- const operands = collectLogicalChain(node, "or");
473
- const body = operands
474
- .map((operand, index) => {
475
- const encoded = encodeNode(operand);
476
- return index === 0 ? encoded : addOptionalPrefix(encoded);
477
- })
478
- .join("");
479
- return `|(${body})`;
480
- }
481
- return encodeCallParts([
482
- encodeOpcode(BINARY_TO_OPCODE[node.op]),
483
- encodeNode(node.left),
484
- encodeNode(node.right),
485
- ]);
486
- case "assign": {
487
- if (node.op === "=") return `=${encodeNode(node.place)}${addOptionalPrefix(encodeNode(node.value))}`;
488
- const opcode = ASSIGN_COMPOUND_TO_OPCODE[node.op];
489
- if (!opcode) throw new Error(`Unsupported assignment op: ${node.op}`);
490
- const computedValue = encodeCallParts([encodeOpcode(opcode), encodeNode(node.place), encodeNode(node.value)]);
491
- return `=${encodeNode(node.place)}${addOptionalPrefix(computedValue)}`;
492
- }
493
- case "navigation":
494
- return encodeNavigation(node);
495
- case "call":
496
- return encodeCallParts([encodeNode(node.callee), ...node.args.map((arg) => encodeNode(arg))]);
497
- case "conditional": {
498
- const opener = node.head === "when" ? "?(" : "!(";
499
- const cond = encodeNode(node.condition);
500
- const thenExpr = addOptionalPrefix(encodeBlockExpression(node.thenBlock));
501
- const elseExpr = node.elseBranch ? addOptionalPrefix(encodeConditionalElse(node.elseBranch)) : "";
502
- return `${opener}${cond}${thenExpr}${elseExpr})`;
503
- }
504
- case "for":
505
- return encodeFor(node);
506
- case "break":
507
- return ";";
508
- case "continue":
509
- return "1;";
510
- default: {
511
- const exhaustive: never = node;
512
- throw new Error(`Unsupported IR node ${(exhaustive as { type?: string }).type ?? "unknown"}`);
513
- }
514
- }
515
- }
516
-
517
- function collectLogicalChain(node: IRNode, op: "and" | "or"): IRNode[] {
518
- if (node.type !== "binary" || node.op !== op) return [node];
519
- return [...collectLogicalChain(node.left, op), ...collectLogicalChain(node.right, op)];
520
- }
521
-
522
- export function parseToIR(source: string): IRNode {
523
- const match = grammar.match(source);
524
- if (!match.succeeded()) {
525
- const failure = match as unknown as { message?: string };
526
- throw new Error(failure.message ?? "Parse failed");
527
- }
528
- return semantics(match).toIR() as IRNode;
529
- }
530
-
531
- function parseDataNode(node: IRNode): unknown {
532
- switch (node.type) {
533
- case "group":
534
- return parseDataNode(node.expression);
535
- case "program": {
536
- if (node.body.length === 1) return parseDataNode(node.body[0]!);
537
- if (node.body.length === 0) return undefined;
538
- throw new Error("Rex parse() expects a single data expression");
539
- }
540
- case "undefined":
541
- return undefined;
542
- case "null":
543
- return null;
544
- case "boolean":
545
- return node.value;
546
- case "number":
547
- return node.value;
548
- case "string":
549
- return decodeStringLiteral(node.raw);
550
- case "array":
551
- return node.items.map((item) => parseDataNode(item));
552
- case "object": {
553
- const out: Record<string, unknown> = {};
554
- for (const entry of node.entries) {
555
- const keyNode = entry.key;
556
- let key: string;
557
- if (keyNode.type === "key") key = keyNode.name;
558
- else {
559
- const keyValue = parseDataNode(keyNode);
560
- key = String(keyValue);
561
- }
562
- out[key] = parseDataNode(entry.value);
563
- }
564
- return out;
565
- }
566
- default:
567
- throw new Error(`Rex parse() only supports data expressions. Found: ${node.type}`);
568
- }
569
- }
570
-
571
- function isPlainObject(value: unknown): value is Record<string, unknown> {
572
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
573
- const proto = Object.getPrototypeOf(value);
574
- return proto === Object.prototype || proto === null;
575
- }
576
-
577
- function isBareKeyName(key: string): boolean {
578
- return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(key);
579
- }
580
-
581
- function stringifyString(value: string): string {
582
- return JSON.stringify(value);
583
- }
584
-
585
- function stringifyInline(value: unknown): string {
586
- if (value === undefined) return "undefined";
587
- if (value === null) return "null";
588
- if (typeof value === "boolean") return value ? "true" : "false";
589
- if (typeof value === "number") {
590
- if (!Number.isFinite(value)) throw new Error("Rex stringify() cannot encode non-finite numbers");
591
- return String(value);
592
- }
593
- if (typeof value === "string") return stringifyString(value);
594
- if (Array.isArray(value)) {
595
- if (value.length === 0) return "[]";
596
- return `[${value.map((item) => stringifyInline(item)).join(" ")}]`;
597
- }
598
- if (isPlainObject(value)) {
599
- const entries = Object.entries(value);
600
- if (entries.length === 0) return "{}";
601
- const body = entries
602
- .map(([key, item]) => `${isBareKeyName(key) ? key : stringifyString(key)}: ${stringifyInline(item)}`)
603
- .join(" ");
604
- return `{${body}}`;
605
- }
606
- throw new Error(`Rex stringify() cannot encode value of type ${typeof value}`);
607
- }
608
-
609
- function fitsInline(rendered: string, depth: number, indentSize: number, maxWidth: number): boolean {
610
- if (rendered.includes("\n")) return false;
611
- return depth * indentSize + rendered.length <= maxWidth;
612
- }
613
-
614
- function stringifyPretty(value: unknown, depth: number, indentSize: number, maxWidth: number): string {
615
- const inline = stringifyInline(value);
616
- if (fitsInline(inline, depth, indentSize, maxWidth)) return inline;
617
-
618
- const indent = " ".repeat(depth * indentSize);
619
- const childIndent = " ".repeat((depth + 1) * indentSize);
620
-
621
- if (Array.isArray(value)) {
622
- if (value.length === 0) return "[]";
623
- const lines = value.map((item) => {
624
- const rendered = stringifyPretty(item, depth + 1, indentSize, maxWidth);
625
- if (!rendered.includes("\n")) return `${childIndent}${rendered}`;
626
- return `${childIndent}${rendered}`;
627
- });
628
- return `[\n${lines.join("\n")}\n${indent}]`;
629
- }
630
-
631
- if (isPlainObject(value)) {
632
- const entries = Object.entries(value);
633
- if (entries.length === 0) return "{}";
634
- const lines = entries.map(([key, item]) => {
635
- const keyText = isBareKeyName(key) ? key : stringifyString(key);
636
- const rendered = stringifyPretty(item, depth + 1, indentSize, maxWidth);
637
- return `${childIndent}${keyText}: ${rendered}`;
638
- });
639
- return `{\n${lines.join("\n")}\n${indent}}`;
640
- }
641
-
642
- return inline;
643
- }
644
-
645
- export function parse(source: string): unknown {
646
- return parseDataNode(parseToIR(source));
647
- }
648
-
649
- export function stringify(value: unknown, options?: { indent?: number; maxWidth?: number }): string {
650
- const indent = options?.indent ?? 2;
651
- const maxWidth = options?.maxWidth ?? 80;
652
- if (!Number.isInteger(indent) || indent < 0) throw new Error("Rex stringify() indent must be a non-negative integer");
653
- if (!Number.isInteger(maxWidth) || maxWidth < 20) throw new Error("Rex stringify() maxWidth must be an integer >= 20");
654
- return stringifyPretty(value, 0, indent, maxWidth);
655
- }
656
-
657
- const DIGIT_SET = new Set(DIGITS.split(""));
658
- const DIGIT_INDEX = new Map<string, number>(Array.from(DIGITS).map((char, index) => [char, index]));
659
-
660
- type EncodedSpan = { start: number; end: number; raw: string };
661
- type DedupeCandidate = {
662
- span: EncodedSpan;
663
- sizeBytes: number;
664
- offsetFromEnd: number;
665
- };
666
-
667
- function readPrefixAt(text: string, start: number): { end: number; raw: string; value: number } {
668
- let index = start;
669
- while (index < text.length && DIGIT_SET.has(text[index] as string)) index += 1;
670
- const raw = text.slice(start, index);
671
- let value = 0;
672
- for (const char of raw) {
673
- const digit = DIGIT_INDEX.get(char);
674
- if (digit === undefined) throw new Error(`Invalid prefix in encoded stream at ${start}`);
675
- value = value * 64 + digit;
676
- }
677
- return { end: index, raw, value };
678
- }
679
-
680
- function parsePlaceEnd(text: string, start: number, out?: EncodedSpan[]): number {
681
- if (text[start] === "(") {
682
- let index = start + 1;
683
- while (index < text.length && text[index] !== ")") {
684
- index = parseValueEnd(text, index, out).end;
685
- }
686
- if (text[index] !== ")") throw new Error(`Unterminated place at ${start}`);
687
- return index + 1;
688
- }
689
-
690
- const prefix = readPrefixAt(text, start);
691
- const tag = text[prefix.end];
692
- if (tag !== "$" && tag !== "'") throw new Error(`Invalid place at ${start}`);
693
- let index = prefix.end + 1;
694
- if (text[index] !== "(") return index;
695
- index += 1;
696
- while (index < text.length && text[index] !== ")") {
697
- index = parseValueEnd(text, index, out).end;
698
- }
699
- if (text[index] !== ")") throw new Error(`Unterminated place at ${start}`);
700
- return index + 1;
701
- }
702
-
703
- function parseValueEnd(text: string, start: number, out?: EncodedSpan[]): EncodedSpan {
704
- const prefix = readPrefixAt(text, start);
705
- const tag = text[prefix.end];
706
- if (!tag) throw new Error(`Unexpected end of encoded stream at ${start}`);
707
-
708
- if (tag === ",") {
709
- const strStart = prefix.end + 1;
710
- const strEnd = strStart + prefix.value;
711
- if (strEnd > text.length) throw new Error(`String overflows encoded stream at ${start}`);
712
- const raw = text.slice(start, strEnd);
713
- if (Buffer.byteLength(text.slice(strStart, strEnd), "utf8") !== prefix.value) {
714
- throw new Error(`Non-ASCII length-string not currently dedupe-safe at ${start}`);
715
- }
716
- const span = { start, end: strEnd, raw };
717
- if (out) out.push(span);
718
- return span;
719
- }
720
-
721
- if (tag === "=") {
722
- const placeEnd = parsePlaceEnd(text, prefix.end + 1, out);
723
- const valueEnd = parseValueEnd(text, placeEnd, out).end;
724
- const span = { start, end: valueEnd, raw: text.slice(start, valueEnd) };
725
- if (out) out.push(span);
726
- return span;
727
- }
728
-
729
- if (tag === "~") {
730
- const placeEnd = parsePlaceEnd(text, prefix.end + 1, out);
731
- const span = { start, end: placeEnd, raw: text.slice(start, placeEnd) };
732
- if (out) out.push(span);
733
- return span;
734
- }
735
-
736
- if (tag === "(" || tag === "[" || tag === "{") {
737
- const close = tag === "(" ? ")" : tag === "[" ? "]" : "}";
738
- let index = prefix.end + 1;
739
- while (index < text.length && text[index] !== close) {
740
- index = parseValueEnd(text, index, out).end;
741
- }
742
- if (text[index] !== close) throw new Error(`Unterminated container at ${start}`);
743
- const span = { start, end: index + 1, raw: text.slice(start, index + 1) };
744
- if (out) out.push(span);
745
- return span;
746
- }
747
-
748
- if (tag === "?" || tag === "!" || tag === "|" || tag === "&") {
749
- if (text[prefix.end + 1] !== "(") throw new Error(`Expected '(' after '${tag}' at ${start}`);
750
- let index = prefix.end + 2;
751
- while (index < text.length && text[index] !== ")") {
752
- index = parseValueEnd(text, index, out).end;
753
- }
754
- if (text[index] !== ")") throw new Error(`Unterminated flow container at ${start}`);
755
- const span = { start, end: index + 1, raw: text.slice(start, index + 1) };
756
- if (out) out.push(span);
757
- return span;
758
- }
759
-
760
- if (tag === ">" || tag === "<") {
761
- const open = text[prefix.end + 1];
762
- if (open !== "(" && open !== "[" && open !== "{") throw new Error(`Invalid loop opener at ${start}`);
763
- const close = open === "(" ? ")" : open === "[" ? "]" : "}";
764
- let index = prefix.end + 2;
765
- while (index < text.length && text[index] !== close) {
766
- index = parseValueEnd(text, index, out).end;
767
- }
768
- if (text[index] !== close) throw new Error(`Unterminated loop container at ${start}`);
769
- const span = { start, end: index + 1, raw: text.slice(start, index + 1) };
770
- if (out) out.push(span);
771
- return span;
772
- }
773
-
774
- const span = { start, end: prefix.end + 1, raw: text.slice(start, prefix.end + 1) };
775
- if (out) out.push(span);
776
- return span;
777
- }
778
-
779
- function gatherEncodedValueSpans(text: string): EncodedSpan[] {
780
- const spans: EncodedSpan[] = [];
781
- let index = 0;
782
- while (index < text.length) {
783
- const span = parseValueEnd(text, index, spans);
784
- index = span.end;
785
- }
786
- return spans;
787
- }
788
-
789
- function buildPointerToken(pointerStart: number, targetStart: number): string | undefined {
790
- let offset = targetStart - (pointerStart + 1);
791
- if (offset < 0) return undefined;
792
- for (let guard = 0; guard < 8; guard += 1) {
793
- const prefix = encodeUint(offset);
794
- const recalculated = targetStart - (pointerStart + prefix.length + 1);
795
- if (recalculated === offset) return `${prefix}^`;
796
- offset = recalculated;
797
- if (offset < 0) return undefined;
798
- }
799
- return undefined;
800
- }
801
-
802
- function buildDedupeCandidateTable(encoded: string, minBytes: number): Map<string, DedupeCandidate[]> {
803
- const spans = gatherEncodedValueSpans(encoded);
804
- const table = new Map<string, DedupeCandidate[]>();
805
- for (const span of spans) {
806
- const sizeBytes = span.raw.length;
807
- if (sizeBytes < minBytes) continue;
808
- const prefix = readPrefixAt(encoded, span.start);
809
- const tag = encoded[prefix.end];
810
- if (tag !== "{" && tag !== "[" && tag !== "," && tag !== ":") continue;
811
-
812
- const offsetFromEnd = encoded.length - span.end;
813
- const entry: DedupeCandidate = {
814
- span,
815
- sizeBytes,
816
- offsetFromEnd,
817
- };
818
-
819
- if (!table.has(span.raw)) table.set(span.raw, []);
820
- (table.get(span.raw) as DedupeCandidate[]).push(entry);
821
- }
822
- return table;
823
- }
824
-
825
- function dedupeLargeEncodedValues(encoded: string, minBytes = 4): string {
826
- const effectiveMinBytes = Math.max(1, minBytes);
827
- let current = encoded;
828
- while (true) {
829
- const groups = buildDedupeCandidateTable(current, effectiveMinBytes);
830
-
831
- let replaced = false;
832
- for (const [value, occurrences] of groups.entries()) {
833
- if (occurrences.length < 2) continue;
834
- const canonical = occurrences[occurrences.length - 1] as DedupeCandidate;
835
- for (let index = occurrences.length - 2; index >= 0; index -= 1) {
836
- const occurrence = occurrences[index] as DedupeCandidate;
837
- if (occurrence.span.end > canonical.span.start) continue;
838
-
839
- if (current.slice(occurrence.span.start, occurrence.span.end) !== value) continue;
840
-
841
- const canonicalCurrentStart = current.length - canonical.offsetFromEnd - canonical.sizeBytes;
842
- const pointerToken = buildPointerToken(occurrence.span.start, canonicalCurrentStart);
843
- if (!pointerToken) continue;
844
- if (pointerToken.length >= occurrence.sizeBytes) continue;
845
-
846
- current = `${current.slice(0, occurrence.span.start)}${pointerToken}${current.slice(occurrence.span.end)}`;
847
- replaced = true;
848
- break;
849
- }
850
- if (replaced) break;
851
- }
852
-
853
- if (!replaced) return current;
854
- }
855
- }
856
-
857
- export function encodeIR(node: IRNode, options?: EncodeOptions & { dedupeValues?: boolean; dedupeMinBytes?: number }): string {
858
- const previous = activeEncodeOptions;
859
- activeEncodeOptions = options;
860
- try {
861
- const encoded = encodeNode(node);
862
- if (options?.dedupeValues) {
863
- return dedupeLargeEncodedValues(encoded, options.dedupeMinBytes ?? 4);
864
- }
865
- return encoded;
866
- } finally {
867
- activeEncodeOptions = previous;
868
- }
869
- }
870
-
871
- type OptimizeEnv = {
872
- constants: Record<string, IRNode>;
873
- selfCaptures: Record<string, number>;
874
- };
875
-
876
- function cloneNode<T extends IRNode>(node: T): T {
877
- return structuredClone(node);
878
- }
879
-
880
- function emptyOptimizeEnv(): OptimizeEnv {
881
- return { constants: {}, selfCaptures: {} };
882
- }
883
-
884
- function cloneOptimizeEnv(env: OptimizeEnv): OptimizeEnv {
885
- return {
886
- constants: { ...env.constants },
887
- selfCaptures: { ...env.selfCaptures },
888
- };
889
- }
890
-
891
- function clearOptimizeEnv(env: OptimizeEnv) {
892
- for (const key of Object.keys(env.constants)) delete env.constants[key];
893
- for (const key of Object.keys(env.selfCaptures)) delete env.selfCaptures[key];
894
- }
895
-
896
- function clearBinding(env: OptimizeEnv, name: string) {
897
- delete env.constants[name];
898
- delete env.selfCaptures[name];
899
- }
900
-
901
- function selfTargetFromNode(node: IRNode, currentDepth: number): number | undefined {
902
- if (node.type === "self") return currentDepth;
903
- if (node.type === "selfDepth") {
904
- const target = currentDepth - (node.depth - 1);
905
- if (target >= 1) return target;
906
- }
907
- return undefined;
908
- }
909
-
910
- function selfNodeFromTarget(targetDepth: number, currentDepth: number): IRNode | undefined {
911
- const relDepth = currentDepth - targetDepth + 1;
912
- if (!Number.isInteger(relDepth) || relDepth < 1) return undefined;
913
- if (relDepth === 1) return { type: "self" } satisfies IRNode;
914
- return { type: "selfDepth", depth: relDepth } satisfies IRNode;
915
- }
916
-
917
- function dropBindingNames(env: OptimizeEnv, binding: IRBindingOrExpr) {
918
- if (binding.type === "binding:valueIn") {
919
- clearBinding(env, binding.value);
920
- return;
921
- }
922
- if (binding.type === "binding:keyValueIn") {
923
- clearBinding(env, binding.key);
924
- clearBinding(env, binding.value);
925
- return;
926
- }
927
- if (binding.type === "binding:keyOf") {
928
- clearBinding(env, binding.key);
929
- }
930
- }
931
-
932
- function collectReads(node: IRNode, out: Set<string>) {
933
- switch (node.type) {
934
- case "identifier":
935
- out.add(node.name);
936
- return;
937
- case "group":
938
- collectReads(node.expression, out);
939
- return;
940
- case "array":
941
- for (const item of node.items) collectReads(item, out);
942
- return;
943
- case "object":
944
- for (const entry of node.entries) {
945
- collectReads(entry.key, out);
946
- collectReads(entry.value, out);
947
- }
948
- return;
949
- case "arrayComprehension":
950
- collectReads(node.binding.source, out);
951
- collectReads(node.body, out);
952
- return;
953
- case "objectComprehension":
954
- collectReads(node.binding.source, out);
955
- collectReads(node.key, out);
956
- collectReads(node.value, out);
957
- return;
958
- case "unary":
959
- collectReads(node.value, out);
960
- return;
961
- case "binary":
962
- collectReads(node.left, out);
963
- collectReads(node.right, out);
964
- return;
965
- case "assign":
966
- if (!(node.op === "=" && node.place.type === "identifier")) collectReads(node.place, out);
967
- collectReads(node.value, out);
968
- return;
969
- case "navigation":
970
- collectReads(node.target, out);
971
- for (const segment of node.segments) {
972
- if (segment.type === "dynamic") collectReads(segment.key, out);
973
- }
974
- return;
975
- case "call":
976
- collectReads(node.callee, out);
977
- for (const arg of node.args) collectReads(arg, out);
978
- return;
979
- case "conditional":
980
- collectReads(node.condition, out);
981
- for (const part of node.thenBlock) collectReads(part, out);
982
- if (node.elseBranch) collectReadsElse(node.elseBranch, out);
983
- return;
984
- case "for":
985
- collectReads(node.binding.source, out);
986
- for (const part of node.body) collectReads(part, out);
987
- return;
988
- case "program":
989
- for (const part of node.body) collectReads(part, out);
990
- return;
991
- default:
992
- return;
993
- }
994
- }
995
-
996
- function collectReadsElse(elseBranch: IRConditionalElse, out: Set<string>) {
997
- if (elseBranch.type === "else") {
998
- for (const part of elseBranch.block) collectReads(part, out);
999
- return;
1000
- }
1001
- collectReads(elseBranch.condition, out);
1002
- for (const part of elseBranch.thenBlock) collectReads(part, out);
1003
- if (elseBranch.elseBranch) collectReadsElse(elseBranch.elseBranch, out);
1004
- }
1005
-
1006
- function isPureNode(node: IRNode): boolean {
1007
- switch (node.type) {
1008
- case "identifier":
1009
- case "self":
1010
- case "selfDepth":
1011
- case "boolean":
1012
- case "null":
1013
- case "undefined":
1014
- case "number":
1015
- case "string":
1016
- case "key":
1017
- return true;
1018
- case "group":
1019
- return isPureNode(node.expression);
1020
- case "array":
1021
- return node.items.every((item) => isPureNode(item));
1022
- case "object":
1023
- return node.entries.every((entry) => isPureNode(entry.key) && isPureNode(entry.value));
1024
- case "navigation":
1025
- return isPureNode(node.target) && node.segments.every((segment) => segment.type === "static" || isPureNode(segment.key));
1026
- case "unary":
1027
- return node.op !== "delete" && isPureNode(node.value);
1028
- case "binary":
1029
- return isPureNode(node.left) && isPureNode(node.right);
1030
- default:
1031
- return false;
1032
- }
1033
- }
1034
-
1035
- function eliminateDeadAssignments(block: IRNode[]): IRNode[] {
1036
- const needed = new Set<string>();
1037
- const out: IRNode[] = [];
1038
-
1039
- for (let index = block.length - 1; index >= 0; index -= 1) {
1040
- const node = block[index] as IRNode;
1041
-
1042
- if (node.type === "conditional") {
1043
- let rewritten = node;
1044
- if (
1045
- node.condition.type === "assign"
1046
- && node.condition.op === "="
1047
- && node.condition.place.type === "identifier"
1048
- ) {
1049
- const name = node.condition.place.name;
1050
- const branchReads = new Set<string>();
1051
- for (const part of node.thenBlock) collectReads(part, branchReads);
1052
- if (node.elseBranch) collectReadsElse(node.elseBranch, branchReads);
1053
- if (!needed.has(name) && !branchReads.has(name)) {
1054
- rewritten = {
1055
- type: "conditional",
1056
- head: node.head,
1057
- condition: node.condition.value,
1058
- thenBlock: node.thenBlock,
1059
- elseBranch: node.elseBranch,
1060
- } satisfies IRNode;
1061
- }
1062
- }
1063
-
1064
- collectReads(rewritten, needed);
1065
- out.push(rewritten);
1066
- continue;
1067
- }
1068
-
1069
- if (node.type === "assign" && node.op === "=" && node.place.type === "identifier") {
1070
- collectReads(node.value, needed);
1071
- const name = node.place.name;
1072
- const canDrop = !needed.has(name) && isPureNode(node.value);
1073
- needed.delete(name);
1074
- if (canDrop) continue;
1075
- out.push(node);
1076
- continue;
1077
- }
1078
-
1079
- collectReads(node, needed);
1080
- out.push(node);
1081
- }
1082
-
1083
- out.reverse();
1084
- return out;
1085
- }
1086
-
1087
- function hasIdentifierRead(node: IRNode, name: string, asPlace = false): boolean {
1088
- if (node.type === "identifier") return !asPlace && node.name === name;
1089
- switch (node.type) {
1090
- case "group":
1091
- return hasIdentifierRead(node.expression, name);
1092
- case "array":
1093
- return node.items.some((item) => hasIdentifierRead(item, name));
1094
- case "object":
1095
- return node.entries.some((entry) => hasIdentifierRead(entry.key, name) || hasIdentifierRead(entry.value, name));
1096
- case "navigation":
1097
- return hasIdentifierRead(node.target, name) || node.segments.some((segment) => segment.type === "dynamic" && hasIdentifierRead(segment.key, name));
1098
- case "unary":
1099
- return hasIdentifierRead(node.value, name, node.op === "delete");
1100
- case "binary":
1101
- return hasIdentifierRead(node.left, name) || hasIdentifierRead(node.right, name);
1102
- case "assign":
1103
- return hasIdentifierRead(node.place, name, true) || hasIdentifierRead(node.value, name);
1104
- default:
1105
- return false;
1106
- }
1107
- }
1108
-
1109
- function countIdentifierReads(node: IRNode, name: string, asPlace = false): number {
1110
- if (node.type === "identifier") return !asPlace && node.name === name ? 1 : 0;
1111
- switch (node.type) {
1112
- case "group":
1113
- return countIdentifierReads(node.expression, name);
1114
- case "array":
1115
- return node.items.reduce((sum, item) => sum + countIdentifierReads(item, name), 0);
1116
- case "object":
1117
- return node.entries.reduce((sum, entry) => sum + countIdentifierReads(entry.key, name) + countIdentifierReads(entry.value, name), 0);
1118
- case "navigation":
1119
- return countIdentifierReads(node.target, name) + node.segments.reduce((sum, segment) => sum + (segment.type === "dynamic" ? countIdentifierReads(segment.key, name) : 0), 0);
1120
- case "unary":
1121
- return countIdentifierReads(node.value, name, node.op === "delete");
1122
- case "binary":
1123
- return countIdentifierReads(node.left, name) + countIdentifierReads(node.right, name);
1124
- case "assign":
1125
- return countIdentifierReads(node.place, name, true) + countIdentifierReads(node.value, name);
1126
- default:
1127
- return 0;
1128
- }
1129
- }
1130
-
1131
- function replaceIdentifier(node: IRNode, name: string, replacement: IRNode, asPlace = false): IRNode {
1132
- if (node.type === "identifier") {
1133
- if (!asPlace && node.name === name) return cloneNode(replacement);
1134
- return node;
1135
- }
1136
-
1137
- switch (node.type) {
1138
- case "group":
1139
- return {
1140
- type: "group",
1141
- expression: replaceIdentifier(node.expression, name, replacement),
1142
- } satisfies IRNode;
1143
- case "array":
1144
- return { type: "array", items: node.items.map((item) => replaceIdentifier(item, name, replacement)) } satisfies IRNode;
1145
- case "object":
1146
- return {
1147
- type: "object",
1148
- entries: node.entries.map((entry) => ({
1149
- key: replaceIdentifier(entry.key, name, replacement),
1150
- value: replaceIdentifier(entry.value, name, replacement),
1151
- })),
1152
- } satisfies IRNode;
1153
- case "navigation":
1154
- return {
1155
- type: "navigation",
1156
- target: replaceIdentifier(node.target, name, replacement),
1157
- segments: node.segments.map((segment) => segment.type === "static"
1158
- ? segment
1159
- : { type: "dynamic", key: replaceIdentifier(segment.key, name, replacement) }),
1160
- } satisfies IRNode;
1161
- case "unary":
1162
- return {
1163
- type: "unary",
1164
- op: node.op,
1165
- value: replaceIdentifier(node.value, name, replacement, node.op === "delete"),
1166
- } satisfies IRNode;
1167
- case "binary":
1168
- return {
1169
- type: "binary",
1170
- op: node.op,
1171
- left: replaceIdentifier(node.left, name, replacement),
1172
- right: replaceIdentifier(node.right, name, replacement),
1173
- } satisfies IRNode;
1174
- case "assign":
1175
- return {
1176
- type: "assign",
1177
- op: node.op,
1178
- place: replaceIdentifier(node.place, name, replacement, true),
1179
- value: replaceIdentifier(node.value, name, replacement),
1180
- } satisfies IRNode;
1181
- default:
1182
- return node;
1183
- }
1184
- }
1185
-
1186
- function isSafeInlineTargetNode(node: IRNode): boolean {
1187
- if (isPureNode(node)) return true;
1188
- if (node.type === "assign" && node.op === "=") {
1189
- return isPureNode(node.place) && isPureNode(node.value);
1190
- }
1191
- return false;
1192
- }
1193
-
1194
- function inlineAdjacentPureAssignments(block: IRNode[]): IRNode[] {
1195
- const out = [...block];
1196
- let changed = true;
1197
-
1198
- while (changed) {
1199
- changed = false;
1200
- for (let index = 0; index < out.length - 1; index += 1) {
1201
- const current = out[index] as IRNode;
1202
- if (current.type !== "assign" || current.op !== "=" || current.place.type !== "identifier") continue;
1203
- if (!isPureNode(current.value)) continue;
1204
- const name = current.place.name;
1205
- if (hasIdentifierRead(current.value, name)) continue;
1206
-
1207
- const next = out[index + 1] as IRNode;
1208
- if (!isSafeInlineTargetNode(next)) continue;
1209
- if (countIdentifierReads(next, name) !== 1) continue;
1210
-
1211
- out[index + 1] = replaceIdentifier(next, name, current.value);
1212
- out.splice(index, 1);
1213
- changed = true;
1214
- break;
1215
- }
1216
- }
1217
-
1218
- return out;
1219
- }
1220
-
1221
- function toNumberNode(value: number): IRNode {
1222
- return { type: "number", raw: String(value), value } satisfies IRNode;
1223
- }
1224
-
1225
- function toStringNode(value: string): IRNode {
1226
- return { type: "string", raw: JSON.stringify(value) } satisfies IRNode;
1227
- }
1228
-
1229
- function toLiteralNode(value: unknown): IRNode | undefined {
1230
- if (value === undefined) return { type: "undefined" } satisfies IRNode;
1231
- if (value === null) return { type: "null" } satisfies IRNode;
1232
- if (typeof value === "boolean") return { type: "boolean", value } satisfies IRNode;
1233
- if (typeof value === "number" && Number.isFinite(value)) return toNumberNode(value);
1234
- if (typeof value === "string") return toStringNode(value);
1235
- if (Array.isArray(value)) {
1236
- const items: IRNode[] = [];
1237
- for (const item of value) {
1238
- const lowered = toLiteralNode(item);
1239
- if (!lowered) return undefined;
1240
- items.push(lowered);
1241
- }
1242
- return { type: "array", items } satisfies IRNode;
1243
- }
1244
- if (value && typeof value === "object") {
1245
- const entries: Array<{ key: IRNode; value: IRNode }> = [];
1246
- for (const [key, entryValue] of Object.entries(value as Record<string, unknown>)) {
1247
- const loweredValue = toLiteralNode(entryValue);
1248
- if (!loweredValue) return undefined;
1249
- entries.push({ key: { type: "key", name: key }, value: loweredValue });
1250
- }
1251
- return { type: "object", entries } satisfies IRNode;
1252
- }
1253
- return undefined;
1254
- }
1255
-
1256
- function constValue(node: IRNode): unknown | undefined {
1257
- switch (node.type) {
1258
- case "undefined":
1259
- return undefined;
1260
- case "null":
1261
- return null;
1262
- case "boolean":
1263
- return node.value;
1264
- case "number":
1265
- return node.value;
1266
- case "string":
1267
- return decodeStringLiteral(node.raw);
1268
- case "key":
1269
- return node.name;
1270
- case "array": {
1271
- const out: unknown[] = [];
1272
- for (const item of node.items) {
1273
- const value = constValue(item);
1274
- if (value === undefined && item.type !== "undefined") return undefined;
1275
- out.push(value);
1276
- }
1277
- return out;
1278
- }
1279
- case "object": {
1280
- const out: Record<string, unknown> = {};
1281
- for (const entry of node.entries) {
1282
- const key = constValue(entry.key);
1283
- if (key === undefined && entry.key.type !== "undefined") return undefined;
1284
- const value = constValue(entry.value);
1285
- if (value === undefined && entry.value.type !== "undefined") return undefined;
1286
- out[String(key)] = value;
1287
- }
1288
- return out;
1289
- }
1290
- default:
1291
- return undefined;
1292
- }
1293
- }
1294
-
1295
- function isDefinedValue(value: unknown): boolean {
1296
- return value !== undefined;
1297
- }
1298
-
1299
- function foldUnary(op: Extract<IRNode, { type: "unary" }> ["op"], value: unknown): unknown | undefined {
1300
- if (op === "neg") {
1301
- if (typeof value !== "number") return undefined;
1302
- return -value;
1303
- }
1304
- if (op === "not") {
1305
- if (typeof value === "boolean") return !value;
1306
- if (typeof value === "number") return ~value;
1307
- return undefined;
1308
- }
1309
- return undefined;
1310
- }
1311
-
1312
- function foldBinary(op: Extract<IRNode, { type: "binary" }> ["op"], left: unknown, right: unknown): unknown | undefined {
1313
- if (op === "add" || op === "sub" || op === "mul" || op === "div" || op === "mod") {
1314
- if (typeof left !== "number" || typeof right !== "number") return undefined;
1315
- if (op === "add") return left + right;
1316
- if (op === "sub") return left - right;
1317
- if (op === "mul") return left * right;
1318
- if (op === "div") return left / right;
1319
- return left % right;
1320
- }
1321
-
1322
- if (op === "bitAnd" || op === "bitOr" || op === "bitXor") {
1323
- if (typeof left !== "number" || typeof right !== "number") return undefined;
1324
- if (op === "bitAnd") return left & right;
1325
- if (op === "bitOr") return left | right;
1326
- return left ^ right;
1327
- }
1328
-
1329
- if (op === "eq") return left === right ? left : undefined;
1330
- if (op === "neq") return left !== right ? left : undefined;
1331
-
1332
- if (op === "gt" || op === "gte" || op === "lt" || op === "lte") {
1333
- if (typeof left !== "number" || typeof right !== "number") return undefined;
1334
- if (op === "gt") return left > right ? left : undefined;
1335
- if (op === "gte") return left >= right ? left : undefined;
1336
- if (op === "lt") return left < right ? left : undefined;
1337
- return left <= right ? left : undefined;
1338
- }
1339
-
1340
- if (op === "and") return isDefinedValue(left) ? right : undefined;
1341
- if (op === "or") return isDefinedValue(left) ? left : right;
1342
- return undefined;
1343
- }
1344
-
1345
- function optimizeElse(elseBranch: IRConditionalElse | undefined, env: OptimizeEnv, currentDepth: number): IRConditionalElse | undefined {
1346
- if (!elseBranch) return undefined;
1347
- if (elseBranch.type === "else") {
1348
- return { type: "else", block: optimizeBlock(elseBranch.block, cloneOptimizeEnv(env), currentDepth) } satisfies IRConditionalElse;
1349
- }
1350
-
1351
- const optimizedCondition = optimizeNode(elseBranch.condition, env, currentDepth);
1352
- const foldedCondition = constValue(optimizedCondition);
1353
- if (foldedCondition !== undefined || optimizedCondition.type === "undefined") {
1354
- const passes = elseBranch.head === "when" ? isDefinedValue(foldedCondition) : !isDefinedValue(foldedCondition);
1355
- if (passes) {
1356
- return {
1357
- type: "else",
1358
- block: optimizeBlock(elseBranch.thenBlock, cloneOptimizeEnv(env), currentDepth),
1359
- } satisfies IRConditionalElse;
1360
- }
1361
- return optimizeElse(elseBranch.elseBranch, env, currentDepth);
1362
- }
1363
-
1364
- return {
1365
- type: "elseChain",
1366
- head: elseBranch.head,
1367
- condition: optimizedCondition,
1368
- thenBlock: optimizeBlock(elseBranch.thenBlock, cloneOptimizeEnv(env), currentDepth),
1369
- elseBranch: optimizeElse(elseBranch.elseBranch, cloneOptimizeEnv(env), currentDepth),
1370
- } satisfies IRConditionalElse;
1371
- }
1372
-
1373
- function optimizeBlock(block: IRNode[], env: OptimizeEnv, currentDepth: number): IRNode[] {
1374
- const out: IRNode[] = [];
1375
- for (const node of block) {
1376
- const optimized = optimizeNode(node, env, currentDepth);
1377
- out.push(optimized);
1378
- if (optimized.type === "break" || optimized.type === "continue") break;
1379
-
1380
- if (optimized.type === "assign" && optimized.op === "=" && optimized.place.type === "identifier") {
1381
- const selfTarget = selfTargetFromNode(optimized.value, currentDepth);
1382
- if (selfTarget !== undefined) {
1383
- env.selfCaptures[optimized.place.name] = selfTarget;
1384
- delete env.constants[optimized.place.name];
1385
- continue;
1386
- }
1387
-
1388
- const folded = constValue(optimized.value);
1389
- if (folded !== undefined || optimized.value.type === "undefined") {
1390
- env.constants[optimized.place.name] = cloneNode(optimized.value);
1391
- delete env.selfCaptures[optimized.place.name];
1392
- }
1393
- else {
1394
- clearBinding(env, optimized.place.name);
1395
- }
1396
- continue;
1397
- }
1398
-
1399
- if (optimized.type === "unary" && optimized.op === "delete" && optimized.value.type === "identifier") {
1400
- clearBinding(env, optimized.value.name);
1401
- continue;
1402
- }
1403
-
1404
- if (optimized.type === "assign" && optimized.place.type === "identifier") {
1405
- clearBinding(env, optimized.place.name);
1406
- continue;
1407
- }
1408
-
1409
- if (optimized.type === "assign" || optimized.type === "for" || optimized.type === "call") {
1410
- clearOptimizeEnv(env);
1411
- }
1412
- }
1413
- return inlineAdjacentPureAssignments(eliminateDeadAssignments(out));
1414
- }
1415
-
1416
- function optimizeNode(node: IRNode, env: OptimizeEnv, currentDepth: number, asPlace = false): IRNode {
1417
- switch (node.type) {
1418
- case "program": {
1419
- const body = optimizeBlock(node.body, cloneOptimizeEnv(env), currentDepth);
1420
- if (body.length === 0) return { type: "undefined" } satisfies IRNode;
1421
- if (body.length === 1) return body[0] as IRNode;
1422
- return { type: "program", body } satisfies IRNode;
1423
- }
1424
- case "identifier": {
1425
- if (asPlace) return node;
1426
- const selfTarget = env.selfCaptures[node.name];
1427
- if (selfTarget !== undefined) {
1428
- const rewritten = selfNodeFromTarget(selfTarget, currentDepth);
1429
- if (rewritten) return rewritten;
1430
- }
1431
- const replacement = env.constants[node.name];
1432
- return replacement ? cloneNode(replacement) : node;
1433
- }
1434
- case "group": {
1435
- return optimizeNode(node.expression, env, currentDepth);
1436
- }
1437
- case "array": {
1438
- return { type: "array", items: node.items.map((item) => optimizeNode(item, env, currentDepth)) } satisfies IRNode;
1439
- }
1440
- case "object": {
1441
- return {
1442
- type: "object",
1443
- entries: node.entries.map((entry) => ({
1444
- key: optimizeNode(entry.key, env, currentDepth),
1445
- value: optimizeNode(entry.value, env, currentDepth),
1446
- })),
1447
- } satisfies IRNode;
1448
- }
1449
- case "unary": {
1450
- const value = optimizeNode(node.value, env, currentDepth, node.op === "delete");
1451
- const foldedValue = constValue(value);
1452
- if (foldedValue !== undefined || value.type === "undefined") {
1453
- const folded = foldUnary(node.op, foldedValue);
1454
- const literal = folded === undefined ? undefined : toLiteralNode(folded);
1455
- if (literal) return literal;
1456
- }
1457
- return { type: "unary", op: node.op, value } satisfies IRNode;
1458
- }
1459
- case "binary": {
1460
- const left = optimizeNode(node.left, env, currentDepth);
1461
- const right = optimizeNode(node.right, env, currentDepth);
1462
- const leftValue = constValue(left);
1463
- const rightValue = constValue(right);
1464
- if ((leftValue !== undefined || left.type === "undefined") && (rightValue !== undefined || right.type === "undefined")) {
1465
- const folded = foldBinary(node.op, leftValue, rightValue);
1466
- const literal = folded === undefined ? undefined : toLiteralNode(folded);
1467
- if (literal) return literal;
1468
- }
1469
- return { type: "binary", op: node.op, left, right } satisfies IRNode;
1470
- }
1471
- case "navigation": {
1472
- const target = optimizeNode(node.target, env, currentDepth);
1473
- const segments = node.segments.map((segment) => (segment.type === "static"
1474
- ? segment
1475
- : { type: "dynamic", key: optimizeNode(segment.key, env, currentDepth) }));
1476
-
1477
- const targetValue = constValue(target);
1478
- if (targetValue !== undefined || target.type === "undefined") {
1479
- let current: unknown = targetValue;
1480
- let foldable = true;
1481
- for (const segment of segments) {
1482
- if (!foldable) break;
1483
- const key = segment.type === "static" ? segment.key : constValue(segment.key);
1484
- if (segment.type === "dynamic" && key === undefined && segment.key.type !== "undefined") {
1485
- foldable = false;
1486
- break;
1487
- }
1488
- if (current === null || current === undefined) {
1489
- current = undefined;
1490
- continue;
1491
- }
1492
- current = (current as Record<string, unknown>)[String(key)];
1493
- }
1494
- if (foldable) {
1495
- const literal = toLiteralNode(current);
1496
- if (literal) return literal;
1497
- }
1498
- }
1499
-
1500
- return {
1501
- type: "navigation",
1502
- target,
1503
- segments: segments as Extract<IRNode, { type: "navigation" }> ["segments"],
1504
- } satisfies IRNode;
1505
- }
1506
- case "call": {
1507
- return {
1508
- type: "call",
1509
- callee: optimizeNode(node.callee, env, currentDepth),
1510
- args: node.args.map((arg) => optimizeNode(arg, env, currentDepth)),
1511
- } satisfies IRNode;
1512
- }
1513
- case "assign": {
1514
- return {
1515
- type: "assign",
1516
- op: node.op,
1517
- place: optimizeNode(node.place, env, currentDepth, true),
1518
- value: optimizeNode(node.value, env, currentDepth),
1519
- } satisfies IRNode;
1520
- }
1521
- case "conditional": {
1522
- const condition = optimizeNode(node.condition, env, currentDepth);
1523
- const thenEnv = cloneOptimizeEnv(env);
1524
- if (condition.type === "assign" && condition.op === "=" && condition.place.type === "identifier") {
1525
- thenEnv.selfCaptures[condition.place.name] = currentDepth;
1526
- delete thenEnv.constants[condition.place.name];
1527
- }
1528
- const conditionValue = constValue(condition);
1529
- if (conditionValue !== undefined || condition.type === "undefined") {
1530
- const passes = node.head === "when" ? isDefinedValue(conditionValue) : !isDefinedValue(conditionValue);
1531
- if (passes) {
1532
- const thenBlock = optimizeBlock(node.thenBlock, thenEnv, currentDepth);
1533
- if (thenBlock.length === 0) return { type: "undefined" } satisfies IRNode;
1534
- if (thenBlock.length === 1) return thenBlock[0] as IRNode;
1535
- return { type: "program", body: thenBlock } satisfies IRNode;
1536
- }
1537
- if (!node.elseBranch) return { type: "undefined" } satisfies IRNode;
1538
- const loweredElse = optimizeElse(node.elseBranch, cloneOptimizeEnv(env), currentDepth);
1539
- if (!loweredElse) return { type: "undefined" } satisfies IRNode;
1540
- if (loweredElse.type === "else") {
1541
- if (loweredElse.block.length === 0) return { type: "undefined" } satisfies IRNode;
1542
- if (loweredElse.block.length === 1) return loweredElse.block[0] as IRNode;
1543
- return { type: "program", body: loweredElse.block } satisfies IRNode;
1544
- }
1545
- return {
1546
- type: "conditional",
1547
- head: loweredElse.head,
1548
- condition: loweredElse.condition,
1549
- thenBlock: loweredElse.thenBlock,
1550
- elseBranch: loweredElse.elseBranch,
1551
- } satisfies IRNode;
1552
- }
1553
-
1554
- return {
1555
- type: "conditional",
1556
- head: node.head,
1557
- condition,
1558
- thenBlock: optimizeBlock(node.thenBlock, thenEnv, currentDepth),
1559
- elseBranch: optimizeElse(node.elseBranch, cloneOptimizeEnv(env), currentDepth),
1560
- } satisfies IRNode;
1561
- }
1562
- case "for": {
1563
- const sourceEnv = cloneOptimizeEnv(env);
1564
- const binding = (() => {
1565
- if (node.binding.type === "binding:expr") {
1566
- return { type: "binding:expr", source: optimizeNode(node.binding.source, sourceEnv, currentDepth) } satisfies IRBindingOrExpr;
1567
- }
1568
- if (node.binding.type === "binding:valueIn") {
1569
- return {
1570
- type: "binding:valueIn",
1571
- value: node.binding.value,
1572
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1573
- } satisfies IRBinding;
1574
- }
1575
- if (node.binding.type === "binding:keyValueIn") {
1576
- return {
1577
- type: "binding:keyValueIn",
1578
- key: node.binding.key,
1579
- value: node.binding.value,
1580
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1581
- } satisfies IRBinding;
1582
- }
1583
- return {
1584
- type: "binding:keyOf",
1585
- key: node.binding.key,
1586
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1587
- } satisfies IRBinding;
1588
- })();
1589
- const bodyEnv = cloneOptimizeEnv(env);
1590
- dropBindingNames(bodyEnv, binding);
1591
- return {
1592
- type: "for",
1593
- binding,
1594
- body: optimizeBlock(node.body, bodyEnv, currentDepth + 1),
1595
- } satisfies IRNode;
1596
- }
1597
- case "arrayComprehension": {
1598
- const sourceEnv = cloneOptimizeEnv(env);
1599
- const binding = node.binding.type === "binding:expr"
1600
- ? ({ type: "binding:expr", source: optimizeNode(node.binding.source, sourceEnv, currentDepth) } satisfies IRBindingOrExpr)
1601
- : node.binding.type === "binding:valueIn"
1602
- ? ({
1603
- type: "binding:valueIn",
1604
- value: node.binding.value,
1605
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1606
- } satisfies IRBinding)
1607
- : node.binding.type === "binding:keyValueIn"
1608
- ? ({
1609
- type: "binding:keyValueIn",
1610
- key: node.binding.key,
1611
- value: node.binding.value,
1612
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1613
- } satisfies IRBinding)
1614
- : ({
1615
- type: "binding:keyOf",
1616
- key: node.binding.key,
1617
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1618
- } satisfies IRBinding);
1619
- const bodyEnv = cloneOptimizeEnv(env);
1620
- dropBindingNames(bodyEnv, binding);
1621
- return {
1622
- type: "arrayComprehension",
1623
- binding,
1624
- body: optimizeNode(node.body, bodyEnv, currentDepth + 1),
1625
- } satisfies IRNode;
1626
- }
1627
- case "objectComprehension": {
1628
- const sourceEnv = cloneOptimizeEnv(env);
1629
- const binding = node.binding.type === "binding:expr"
1630
- ? ({ type: "binding:expr", source: optimizeNode(node.binding.source, sourceEnv, currentDepth) } satisfies IRBindingOrExpr)
1631
- : node.binding.type === "binding:valueIn"
1632
- ? ({
1633
- type: "binding:valueIn",
1634
- value: node.binding.value,
1635
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1636
- } satisfies IRBinding)
1637
- : node.binding.type === "binding:keyValueIn"
1638
- ? ({
1639
- type: "binding:keyValueIn",
1640
- key: node.binding.key,
1641
- value: node.binding.value,
1642
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1643
- } satisfies IRBinding)
1644
- : ({
1645
- type: "binding:keyOf",
1646
- key: node.binding.key,
1647
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1648
- } satisfies IRBinding);
1649
- const bodyEnv = cloneOptimizeEnv(env);
1650
- dropBindingNames(bodyEnv, binding);
1651
- return {
1652
- type: "objectComprehension",
1653
- binding,
1654
- key: optimizeNode(node.key, bodyEnv, currentDepth + 1),
1655
- value: optimizeNode(node.value, bodyEnv, currentDepth + 1),
1656
- } satisfies IRNode;
1657
- }
1658
- default:
1659
- return node;
1660
- }
1661
- }
1662
-
1663
- export function optimizeIR(node: IRNode): IRNode {
1664
- return optimizeNode(node, emptyOptimizeEnv(), 1);
1665
- }
1666
-
1667
- function collectLocalBindings(node: IRNode, locals: Set<string>) {
1668
- switch (node.type) {
1669
- case "assign":
1670
- if (node.place.type === "identifier") locals.add(node.place.name);
1671
- collectLocalBindings(node.place, locals);
1672
- collectLocalBindings(node.value, locals);
1673
- return;
1674
- case "program":
1675
- for (const part of node.body) collectLocalBindings(part, locals);
1676
- return;
1677
- case "group":
1678
- collectLocalBindings(node.expression, locals);
1679
- return;
1680
- case "array":
1681
- for (const item of node.items) collectLocalBindings(item, locals);
1682
- return;
1683
- case "object":
1684
- for (const entry of node.entries) {
1685
- collectLocalBindings(entry.key, locals);
1686
- collectLocalBindings(entry.value, locals);
1687
- }
1688
- return;
1689
- case "navigation":
1690
- collectLocalBindings(node.target, locals);
1691
- for (const segment of node.segments) {
1692
- if (segment.type === "dynamic") collectLocalBindings(segment.key, locals);
1693
- }
1694
- return;
1695
- case "call":
1696
- collectLocalBindings(node.callee, locals);
1697
- for (const arg of node.args) collectLocalBindings(arg, locals);
1698
- return;
1699
- case "unary":
1700
- collectLocalBindings(node.value, locals);
1701
- return;
1702
- case "binary":
1703
- collectLocalBindings(node.left, locals);
1704
- collectLocalBindings(node.right, locals);
1705
- return;
1706
- case "conditional":
1707
- collectLocalBindings(node.condition, locals);
1708
- for (const part of node.thenBlock) collectLocalBindings(part, locals);
1709
- if (node.elseBranch) collectLocalBindingsElse(node.elseBranch, locals);
1710
- return;
1711
- case "for":
1712
- collectLocalBindingFromBinding(node.binding, locals);
1713
- for (const part of node.body) collectLocalBindings(part, locals);
1714
- return;
1715
- case "arrayComprehension":
1716
- collectLocalBindingFromBinding(node.binding, locals);
1717
- collectLocalBindings(node.body, locals);
1718
- return;
1719
- case "objectComprehension":
1720
- collectLocalBindingFromBinding(node.binding, locals);
1721
- collectLocalBindings(node.key, locals);
1722
- collectLocalBindings(node.value, locals);
1723
- return;
1724
- default:
1725
- return;
1726
- }
1727
- }
1728
-
1729
- function collectLocalBindingFromBinding(binding: IRBindingOrExpr, locals: Set<string>) {
1730
- if (binding.type === "binding:valueIn") {
1731
- locals.add(binding.value);
1732
- collectLocalBindings(binding.source, locals);
1733
- return;
1734
- }
1735
- if (binding.type === "binding:keyValueIn") {
1736
- locals.add(binding.key);
1737
- locals.add(binding.value);
1738
- collectLocalBindings(binding.source, locals);
1739
- return;
1740
- }
1741
- if (binding.type === "binding:keyOf") {
1742
- locals.add(binding.key);
1743
- collectLocalBindings(binding.source, locals);
1744
- return;
1745
- }
1746
- collectLocalBindings(binding.source, locals);
1747
- }
1748
-
1749
- function collectLocalBindingsElse(elseBranch: IRConditionalElse, locals: Set<string>) {
1750
- if (elseBranch.type === "else") {
1751
- for (const part of elseBranch.block) collectLocalBindings(part, locals);
1752
- return;
1753
- }
1754
- collectLocalBindings(elseBranch.condition, locals);
1755
- for (const part of elseBranch.thenBlock) collectLocalBindings(part, locals);
1756
- if (elseBranch.elseBranch) collectLocalBindingsElse(elseBranch.elseBranch, locals);
1757
- }
1758
-
1759
- function bumpNameFrequency(name: string, locals: Set<string>, frequencies: Map<string, number>, order: Map<string, number>, nextOrder: { value: number }) {
1760
- if (!locals.has(name)) return;
1761
- if (!order.has(name)) {
1762
- order.set(name, nextOrder.value);
1763
- nextOrder.value += 1;
1764
- }
1765
- frequencies.set(name, (frequencies.get(name) ?? 0) + 1);
1766
- }
1767
-
1768
- function collectNameFrequencies(node: IRNode, locals: Set<string>, frequencies: Map<string, number>, order: Map<string, number>, nextOrder: { value: number }) {
1769
- switch (node.type) {
1770
- case "identifier":
1771
- bumpNameFrequency(node.name, locals, frequencies, order, nextOrder);
1772
- return;
1773
- case "assign":
1774
- if (node.place.type === "identifier") bumpNameFrequency(node.place.name, locals, frequencies, order, nextOrder);
1775
- collectNameFrequencies(node.place, locals, frequencies, order, nextOrder);
1776
- collectNameFrequencies(node.value, locals, frequencies, order, nextOrder);
1777
- return;
1778
- case "program":
1779
- for (const part of node.body) collectNameFrequencies(part, locals, frequencies, order, nextOrder);
1780
- return;
1781
- case "group":
1782
- collectNameFrequencies(node.expression, locals, frequencies, order, nextOrder);
1783
- return;
1784
- case "array":
1785
- for (const item of node.items) collectNameFrequencies(item, locals, frequencies, order, nextOrder);
1786
- return;
1787
- case "object":
1788
- for (const entry of node.entries) {
1789
- collectNameFrequencies(entry.key, locals, frequencies, order, nextOrder);
1790
- collectNameFrequencies(entry.value, locals, frequencies, order, nextOrder);
1791
- }
1792
- return;
1793
- case "navigation":
1794
- collectNameFrequencies(node.target, locals, frequencies, order, nextOrder);
1795
- for (const segment of node.segments) {
1796
- if (segment.type === "dynamic") collectNameFrequencies(segment.key, locals, frequencies, order, nextOrder);
1797
- }
1798
- return;
1799
- case "call":
1800
- collectNameFrequencies(node.callee, locals, frequencies, order, nextOrder);
1801
- for (const arg of node.args) collectNameFrequencies(arg, locals, frequencies, order, nextOrder);
1802
- return;
1803
- case "unary":
1804
- collectNameFrequencies(node.value, locals, frequencies, order, nextOrder);
1805
- return;
1806
- case "binary":
1807
- collectNameFrequencies(node.left, locals, frequencies, order, nextOrder);
1808
- collectNameFrequencies(node.right, locals, frequencies, order, nextOrder);
1809
- return;
1810
- case "conditional":
1811
- collectNameFrequencies(node.condition, locals, frequencies, order, nextOrder);
1812
- for (const part of node.thenBlock) collectNameFrequencies(part, locals, frequencies, order, nextOrder);
1813
- if (node.elseBranch) collectNameFrequenciesElse(node.elseBranch, locals, frequencies, order, nextOrder);
1814
- return;
1815
- case "for":
1816
- collectNameFrequenciesBinding(node.binding, locals, frequencies, order, nextOrder);
1817
- for (const part of node.body) collectNameFrequencies(part, locals, frequencies, order, nextOrder);
1818
- return;
1819
- case "arrayComprehension":
1820
- collectNameFrequenciesBinding(node.binding, locals, frequencies, order, nextOrder);
1821
- collectNameFrequencies(node.body, locals, frequencies, order, nextOrder);
1822
- return;
1823
- case "objectComprehension":
1824
- collectNameFrequenciesBinding(node.binding, locals, frequencies, order, nextOrder);
1825
- collectNameFrequencies(node.key, locals, frequencies, order, nextOrder);
1826
- collectNameFrequencies(node.value, locals, frequencies, order, nextOrder);
1827
- return;
1828
- default:
1829
- return;
1830
- }
1831
- }
1832
-
1833
- function collectNameFrequenciesBinding(binding: IRBindingOrExpr, locals: Set<string>, frequencies: Map<string, number>, order: Map<string, number>, nextOrder: { value: number }) {
1834
- if (binding.type === "binding:valueIn") {
1835
- bumpNameFrequency(binding.value, locals, frequencies, order, nextOrder);
1836
- collectNameFrequencies(binding.source, locals, frequencies, order, nextOrder);
1837
- return;
1838
- }
1839
- if (binding.type === "binding:keyValueIn") {
1840
- bumpNameFrequency(binding.key, locals, frequencies, order, nextOrder);
1841
- bumpNameFrequency(binding.value, locals, frequencies, order, nextOrder);
1842
- collectNameFrequencies(binding.source, locals, frequencies, order, nextOrder);
1843
- return;
1844
- }
1845
- if (binding.type === "binding:keyOf") {
1846
- bumpNameFrequency(binding.key, locals, frequencies, order, nextOrder);
1847
- collectNameFrequencies(binding.source, locals, frequencies, order, nextOrder);
1848
- return;
1849
- }
1850
- collectNameFrequencies(binding.source, locals, frequencies, order, nextOrder);
1851
- }
1852
-
1853
- function collectNameFrequenciesElse(elseBranch: IRConditionalElse, locals: Set<string>, frequencies: Map<string, number>, order: Map<string, number>, nextOrder: { value: number }) {
1854
- if (elseBranch.type === "else") {
1855
- for (const part of elseBranch.block) collectNameFrequencies(part, locals, frequencies, order, nextOrder);
1856
- return;
1857
- }
1858
- collectNameFrequencies(elseBranch.condition, locals, frequencies, order, nextOrder);
1859
- for (const part of elseBranch.thenBlock) collectNameFrequencies(part, locals, frequencies, order, nextOrder);
1860
- if (elseBranch.elseBranch) collectNameFrequenciesElse(elseBranch.elseBranch, locals, frequencies, order, nextOrder);
1861
- }
1862
-
1863
- function renameLocalNames(node: IRNode, map: Map<string, string>): IRNode {
1864
- switch (node.type) {
1865
- case "identifier":
1866
- return map.has(node.name) ? ({ type: "identifier", name: map.get(node.name) as string } satisfies IRNode) : node;
1867
- case "program":
1868
- return { type: "program", body: node.body.map((part) => renameLocalNames(part, map)) } satisfies IRNode;
1869
- case "group":
1870
- return { type: "group", expression: renameLocalNames(node.expression, map) } satisfies IRNode;
1871
- case "array":
1872
- return { type: "array", items: node.items.map((item) => renameLocalNames(item, map)) } satisfies IRNode;
1873
- case "object":
1874
- return {
1875
- type: "object",
1876
- entries: node.entries.map((entry) => ({
1877
- key: renameLocalNames(entry.key, map),
1878
- value: renameLocalNames(entry.value, map),
1879
- })),
1880
- } satisfies IRNode;
1881
- case "navigation":
1882
- return {
1883
- type: "navigation",
1884
- target: renameLocalNames(node.target, map),
1885
- segments: node.segments.map((segment) => segment.type === "static"
1886
- ? segment
1887
- : { type: "dynamic", key: renameLocalNames(segment.key, map) }),
1888
- } satisfies IRNode;
1889
- case "call":
1890
- return {
1891
- type: "call",
1892
- callee: renameLocalNames(node.callee, map),
1893
- args: node.args.map((arg) => renameLocalNames(arg, map)),
1894
- } satisfies IRNode;
1895
- case "unary":
1896
- return { type: "unary", op: node.op, value: renameLocalNames(node.value, map) } satisfies IRNode;
1897
- case "binary":
1898
- return {
1899
- type: "binary",
1900
- op: node.op,
1901
- left: renameLocalNames(node.left, map),
1902
- right: renameLocalNames(node.right, map),
1903
- } satisfies IRNode;
1904
- case "assign": {
1905
- const place = node.place.type === "identifier" && map.has(node.place.name)
1906
- ? ({ type: "identifier", name: map.get(node.place.name) as string } satisfies IRNode)
1907
- : renameLocalNames(node.place, map);
1908
- return {
1909
- type: "assign",
1910
- op: node.op,
1911
- place,
1912
- value: renameLocalNames(node.value, map),
1913
- } satisfies IRNode;
1914
- }
1915
- case "conditional":
1916
- return {
1917
- type: "conditional",
1918
- head: node.head,
1919
- condition: renameLocalNames(node.condition, map),
1920
- thenBlock: node.thenBlock.map((part) => renameLocalNames(part, map)),
1921
- elseBranch: node.elseBranch ? renameLocalNamesElse(node.elseBranch, map) : undefined,
1922
- } satisfies IRNode;
1923
- case "for":
1924
- return {
1925
- type: "for",
1926
- binding: renameLocalNamesBinding(node.binding, map),
1927
- body: node.body.map((part) => renameLocalNames(part, map)),
1928
- } satisfies IRNode;
1929
- case "arrayComprehension":
1930
- return {
1931
- type: "arrayComprehension",
1932
- binding: renameLocalNamesBinding(node.binding, map),
1933
- body: renameLocalNames(node.body, map),
1934
- } satisfies IRNode;
1935
- case "objectComprehension":
1936
- return {
1937
- type: "objectComprehension",
1938
- binding: renameLocalNamesBinding(node.binding, map),
1939
- key: renameLocalNames(node.key, map),
1940
- value: renameLocalNames(node.value, map),
1941
- } satisfies IRNode;
1942
- default:
1943
- return node;
1944
- }
1945
- }
1946
-
1947
- function renameLocalNamesBinding(binding: IRBindingOrExpr, map: Map<string, string>): IRBindingOrExpr {
1948
- if (binding.type === "binding:expr") {
1949
- return { type: "binding:expr", source: renameLocalNames(binding.source, map) } satisfies IRBindingOrExpr;
1950
- }
1951
- if (binding.type === "binding:valueIn") {
1952
- return {
1953
- type: "binding:valueIn",
1954
- value: map.get(binding.value) ?? binding.value,
1955
- source: renameLocalNames(binding.source, map),
1956
- } satisfies IRBinding;
1957
- }
1958
- if (binding.type === "binding:keyValueIn") {
1959
- return {
1960
- type: "binding:keyValueIn",
1961
- key: map.get(binding.key) ?? binding.key,
1962
- value: map.get(binding.value) ?? binding.value,
1963
- source: renameLocalNames(binding.source, map),
1964
- } satisfies IRBinding;
1965
- }
1966
- return {
1967
- type: "binding:keyOf",
1968
- key: map.get(binding.key) ?? binding.key,
1969
- source: renameLocalNames(binding.source, map),
1970
- } satisfies IRBinding;
1971
- }
1972
-
1973
- function renameLocalNamesElse(elseBranch: IRConditionalElse, map: Map<string, string>): IRConditionalElse {
1974
- if (elseBranch.type === "else") {
1975
- return {
1976
- type: "else",
1977
- block: elseBranch.block.map((part) => renameLocalNames(part, map)),
1978
- } satisfies IRConditionalElse;
1979
- }
1980
- return {
1981
- type: "elseChain",
1982
- head: elseBranch.head,
1983
- condition: renameLocalNames(elseBranch.condition, map),
1984
- thenBlock: elseBranch.thenBlock.map((part) => renameLocalNames(part, map)),
1985
- elseBranch: elseBranch.elseBranch ? renameLocalNamesElse(elseBranch.elseBranch, map) : undefined,
1986
- } satisfies IRConditionalElse;
1987
- }
1988
-
1989
- export function minifyLocalNamesIR(node: IRNode): IRNode {
1990
- const locals = new Set<string>();
1991
- collectLocalBindings(node, locals);
1992
- if (locals.size === 0) return node;
1993
-
1994
- const frequencies = new Map<string, number>();
1995
- const order = new Map<string, number>();
1996
- collectNameFrequencies(node, locals, frequencies, order, { value: 0 });
1997
-
1998
- const ranked = Array.from(locals)
1999
- .sort((a, b) => {
2000
- const freqA = frequencies.get(a) ?? 0;
2001
- const freqB = frequencies.get(b) ?? 0;
2002
- if (freqA !== freqB) return freqB - freqA;
2003
- const orderA = order.get(a) ?? Number.MAX_SAFE_INTEGER;
2004
- const orderB = order.get(b) ?? Number.MAX_SAFE_INTEGER;
2005
- if (orderA !== orderB) return orderA - orderB;
2006
- return a.localeCompare(b);
2007
- });
2008
-
2009
- const renameMap = new Map<string, string>();
2010
- ranked.forEach((name, index) => {
2011
- renameMap.set(name, encodeUint(index));
2012
- });
2013
-
2014
- return renameLocalNames(node, renameMap);
2015
- }
2016
-
2017
- export function compile(source: string, options?: CompileOptions): string {
2018
- const ir = parseToIR(source);
2019
- let lowered = options?.optimize ? optimizeIR(ir) : ir;
2020
- if (options?.minifyNames) lowered = minifyLocalNamesIR(lowered);
2021
- return encodeIR(lowered, {
2022
- domainRefs: resolveDomainRefMap(options?.domainRefs),
2023
- dedupeValues: options?.dedupeValues,
2024
- dedupeMinBytes: options?.dedupeMinBytes,
2025
- });
2026
- }
2027
-
2028
- type IRPostfixStep =
2029
- | { kind: "navStatic"; key: string }
2030
- | { kind: "navDynamic"; key: IRNode }
2031
- | { kind: "call"; args: IRNode[] };
2032
-
2033
- function parseNumber(raw: string) {
2034
- if (/^-?0x/i.test(raw)) return parseInt(raw, 16);
2035
- if (/^-?0b/i.test(raw)) {
2036
- const isNegative = raw.startsWith("-");
2037
- const digits = raw.replace(/^-?0b/i, "");
2038
- const value = parseInt(digits, 2);
2039
- return isNegative ? -value : value;
2040
- }
2041
- return Number(raw);
2042
- }
2043
-
2044
- function collectStructured(value: unknown, out: Array<IRNode | { key: IRNode; value: IRNode }>) {
2045
- if (Array.isArray(value)) {
2046
- for (const part of value) collectStructured(part, out);
2047
- return;
2048
- }
2049
- if (!value || typeof value !== "object") return;
2050
- if ("type" in value || ("key" in value && "value" in value)) {
2051
- out.push(value as IRNode | { key: IRNode; value: IRNode });
2052
- }
2053
- }
2054
-
2055
- function normalizeList(value: unknown): Array<IRNode | { key: IRNode; value: IRNode }> {
2056
- const out: Array<IRNode | { key: IRNode; value: IRNode }> = [];
2057
- collectStructured(value, out);
2058
- return out;
2059
- }
2060
-
2061
- function collectPostfixSteps(value: unknown, out: IRPostfixStep[]) {
2062
- if (Array.isArray(value)) {
2063
- for (const part of value) collectPostfixSteps(part, out);
2064
- return;
2065
- }
2066
- if (!value || typeof value !== "object") return;
2067
- if ("kind" in value) out.push(value as IRPostfixStep);
2068
- }
2069
-
2070
- function normalizePostfixSteps(value: unknown): IRPostfixStep[] {
2071
- const out: IRPostfixStep[] = [];
2072
- collectPostfixSteps(value, out);
2073
- return out;
2074
- }
2075
-
2076
- function buildPostfix(base: IRNode, steps: IRPostfixStep[]) {
2077
- let current = base;
2078
- let pendingSegments: Extract<IRNode, { type: "navigation" }>["segments"] = [];
2079
-
2080
- const flushSegments = () => {
2081
- if (pendingSegments.length === 0) return;
2082
- current = {
2083
- type: "navigation",
2084
- target: current,
2085
- segments: pendingSegments,
2086
- } satisfies IRNode;
2087
- pendingSegments = [];
2088
- };
2089
-
2090
- for (const step of steps) {
2091
- if (step.kind === "navStatic") {
2092
- pendingSegments.push({ type: "static", key: step.key });
2093
- continue;
2094
- }
2095
- if (step.kind === "navDynamic") {
2096
- pendingSegments.push({ type: "dynamic", key: step.key });
2097
- continue;
2098
- }
2099
- flushSegments();
2100
- current = { type: "call", callee: current, args: step.args } satisfies IRNode;
2101
- }
2102
-
2103
- flushSegments();
2104
- return current;
2105
- }
2106
-
2107
- semantics.addOperation("toIR", {
2108
- _iter(...children) {
2109
- return children.map((child) => child.toIR());
2110
- },
2111
- _terminal() {
2112
- return this.sourceString;
2113
- },
2114
- _nonterminal(...children) {
2115
- if (children.length === 1 && children[0]) return children[0].toIR();
2116
- return children.map((child) => child.toIR());
2117
- },
2118
-
2119
- Program(expressions) {
2120
- const body = normalizeList(expressions.toIR()) as IRNode[];
2121
- if (body.length === 1) return body[0];
2122
- return { type: "program", body } satisfies IRNode;
2123
- },
2124
-
2125
- Block(expressions) {
2126
- return normalizeList(expressions.toIR()) as IRNode[];
2127
- },
2128
-
2129
- Elements(first, separatorsAndItems, maybeTrailingComma, maybeEmpty) {
2130
- return normalizeList([
2131
- first.toIR(),
2132
- separatorsAndItems.toIR(),
2133
- maybeTrailingComma.toIR(),
2134
- maybeEmpty.toIR(),
2135
- ]);
2136
- },
2137
-
2138
- AssignExpr_assign(place, op, value) {
2139
- return {
2140
- type: "assign",
2141
- op: op.sourceString as Extract<IRNode, { type: "assign" }>["op"],
2142
- place: place.toIR(),
2143
- value: value.toIR(),
2144
- } satisfies IRNode;
2145
- },
2146
-
2147
- ExistenceExpr_and(left, _and, right) {
2148
- return { type: "binary", op: "and", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2149
- },
2150
- ExistenceExpr_or(left, _or, right) {
2151
- return { type: "binary", op: "or", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2152
- },
2153
-
2154
- BitExpr_and(left, _op, right) {
2155
- return { type: "binary", op: "bitAnd", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2156
- },
2157
- BitExpr_xor(left, _op, right) {
2158
- return { type: "binary", op: "bitXor", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2159
- },
2160
- BitExpr_or(left, _op, right) {
2161
- return { type: "binary", op: "bitOr", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2162
- },
2163
-
2164
- CompareExpr_binary(left, op, right) {
2165
- const map: Record<string, Extract<IRNode, { type: "binary" }>["op"]> = {
2166
- "==": "eq",
2167
- "!=": "neq",
2168
- ">": "gt",
2169
- ">=": "gte",
2170
- "<": "lt",
2171
- "<=": "lte",
2172
- };
2173
- const mapped = map[op.sourceString];
2174
- if (!mapped) throw new Error(`Unsupported compare op: ${op.sourceString}`);
2175
- return { type: "binary", op: mapped, left: left.toIR(), right: right.toIR() } satisfies IRNode;
2176
- },
2177
-
2178
- AddExpr_add(left, _op, right) {
2179
- return { type: "binary", op: "add", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2180
- },
2181
- AddExpr_sub(left, _op, right) {
2182
- return { type: "binary", op: "sub", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2183
- },
2184
-
2185
- MulExpr_mul(left, _op, right) {
2186
- return { type: "binary", op: "mul", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2187
- },
2188
- MulExpr_div(left, _op, right) {
2189
- return { type: "binary", op: "div", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2190
- },
2191
- MulExpr_mod(left, _op, right) {
2192
- return { type: "binary", op: "mod", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2193
- },
2194
-
2195
- UnaryExpr_neg(_op, value) {
2196
- const lowered = value.toIR() as IRNode;
2197
- if (lowered.type === "number") {
2198
- const raw = lowered.raw.startsWith("-") ? lowered.raw.slice(1) : `-${lowered.raw}`;
2199
- return { type: "number", raw, value: -lowered.value } satisfies IRNode;
2200
- }
2201
- return { type: "unary", op: "neg", value: lowered } satisfies IRNode;
2202
- },
2203
- UnaryExpr_not(_op, value) {
2204
- return { type: "unary", op: "not", value: value.toIR() } satisfies IRNode;
2205
- },
2206
- UnaryExpr_delete(_del, place) {
2207
- return { type: "unary", op: "delete", value: place.toIR() } satisfies IRNode;
2208
- },
2209
-
2210
- PostfixExpr_chain(base, tails) {
2211
- return buildPostfix(base.toIR(), normalizePostfixSteps(tails.toIR()));
2212
- },
2213
- Place(base, tails) {
2214
- return buildPostfix(base.toIR(), normalizePostfixSteps(tails.toIR()));
2215
- },
2216
- PlaceTail_navStatic(_dot, key) {
2217
- return { kind: "navStatic", key: key.sourceString } satisfies IRPostfixStep;
2218
- },
2219
- PlaceTail_navDynamic(_dotOpen, key, _close) {
2220
- return { kind: "navDynamic", key: key.toIR() } satisfies IRPostfixStep;
2221
- },
2222
- PostfixTail_navStatic(_dot, key) {
2223
- return { kind: "navStatic", key: key.sourceString } satisfies IRPostfixStep;
2224
- },
2225
- PostfixTail_navDynamic(_dotOpen, key, _close) {
2226
- return { kind: "navDynamic", key: key.toIR() } satisfies IRPostfixStep;
2227
- },
2228
- PostfixTail_callEmpty(_open, _close) {
2229
- return { kind: "call", args: [] } satisfies IRPostfixStep;
2230
- },
2231
- PostfixTail_call(_open, args, _close) {
2232
- return { kind: "call", args: normalizeList(args.toIR()) as IRNode[] } satisfies IRPostfixStep;
2233
- },
2234
-
2235
- ConditionalExpr(head, condition, _do, thenBlock, elseBranch, _end) {
2236
- const nextElse = elseBranch.children[0];
2237
- return {
2238
- type: "conditional",
2239
- head: head.toIR() as "when" | "unless",
2240
- condition: condition.toIR(),
2241
- thenBlock: thenBlock.toIR() as IRNode[],
2242
- elseBranch: nextElse ? (nextElse.toIR() as IRConditionalElse) : undefined,
2243
- } satisfies IRNode;
2244
- },
2245
- ConditionalHead(_kw) {
2246
- return this.sourceString as "when" | "unless";
2247
- },
2248
- ConditionalElse_elseChain(_else, head, condition, _do, thenBlock, elseBranch) {
2249
- const nextElse = elseBranch.children[0];
2250
- return {
2251
- type: "elseChain",
2252
- head: head.toIR() as "when" | "unless",
2253
- condition: condition.toIR(),
2254
- thenBlock: thenBlock.toIR() as IRNode[],
2255
- elseBranch: nextElse ? (nextElse.toIR() as IRConditionalElse) : undefined,
2256
- } satisfies IRConditionalElse;
2257
- },
2258
- ConditionalElse_else(_else, block) {
2259
- return { type: "else", block: block.toIR() as IRNode[] } satisfies IRConditionalElse;
2260
- },
2261
-
2262
- DoExpr(_do, block, _end) {
2263
- const body = block.toIR() as IRNode[];
2264
- if (body.length === 0) return { type: "undefined" } satisfies IRNode;
2265
- if (body.length === 1) return body[0] as IRNode;
2266
- return { type: "program", body } satisfies IRNode;
2267
- },
2268
-
2269
- ForExpr(_for, binding, _do, block, _end) {
2270
- return {
2271
- type: "for",
2272
- binding: binding.toIR() as IRBindingOrExpr,
2273
- body: block.toIR() as IRNode[],
2274
- } satisfies IRNode;
2275
- },
2276
- BindingExpr(iterOrExpr) {
2277
- const node = iterOrExpr.toIR();
2278
- if (typeof node === "object" && node && "type" in node && String(node.type).startsWith("binding:")) {
2279
- return node as IRBinding;
2280
- }
2281
- return { type: "binding:expr", source: node as IRNode } satisfies IRBindingOrExpr;
2282
- },
2283
-
2284
- Array_empty(_open, _close) {
2285
- return { type: "array", items: [] } satisfies IRNode;
2286
- },
2287
- Array_comprehension(_open, binding, _semi, body, _close) {
2288
- return {
2289
- type: "arrayComprehension",
2290
- binding: binding.toIR() as IRBindingOrExpr,
2291
- body: body.toIR(),
2292
- } satisfies IRNode;
2293
- },
2294
- Array_values(_open, items, _close) {
2295
- return { type: "array", items: normalizeList(items.toIR()) as IRNode[] } satisfies IRNode;
2296
- },
2297
-
2298
- Object_empty(_open, _close) {
2299
- return { type: "object", entries: [] } satisfies IRNode;
2300
- },
2301
- Object_comprehension(_open, binding, _semi, key, _colon, value, _close) {
2302
- return {
2303
- type: "objectComprehension",
2304
- binding: binding.toIR() as IRBindingOrExpr,
2305
- key: key.toIR(),
2306
- value: value.toIR(),
2307
- } satisfies IRNode;
2308
- },
2309
- Object_pairs(_open, pairs, _close) {
2310
- return {
2311
- type: "object",
2312
- entries: normalizeList(pairs.toIR()) as Array<{ key: IRNode; value: IRNode }>,
2313
- } satisfies IRNode;
2314
- },
2315
-
2316
- IterBinding_keyValueIn(key, _comma, value, _in, source) {
2317
- return {
2318
- type: "binding:keyValueIn",
2319
- key: key.sourceString,
2320
- value: value.sourceString,
2321
- source: source.toIR(),
2322
- } satisfies IRBinding;
2323
- },
2324
- IterBinding_valueIn(value, _in, source) {
2325
- return {
2326
- type: "binding:valueIn",
2327
- value: value.sourceString,
2328
- source: source.toIR(),
2329
- } satisfies IRBinding;
2330
- },
2331
- IterBinding_keyOf(key, _of, source) {
2332
- return {
2333
- type: "binding:keyOf",
2334
- key: key.sourceString,
2335
- source: source.toIR(),
2336
- } satisfies IRBinding;
2337
- },
2338
-
2339
- Pair(key, _colon, value) {
2340
- return { key: key.toIR(), value: value.toIR() };
2341
- },
2342
- ObjKey_bare(key) {
2343
- return { type: "key", name: key.sourceString } satisfies IRNode;
2344
- },
2345
- ObjKey_number(num) {
2346
- return num.toIR();
2347
- },
2348
- ObjKey_string(str) {
2349
- return str.toIR();
2350
- },
2351
- ObjKey_computed(_open, expr, _close) {
2352
- return expr.toIR();
2353
- },
2354
-
2355
- BreakKw(_kw) {
2356
- return { type: "break" } satisfies IRNode;
2357
- },
2358
- ContinueKw(_kw) {
2359
- return { type: "continue" } satisfies IRNode;
2360
- },
2361
- SelfExpr_depth(_self, _at, depth) {
2362
- const value = depth.toIR() as IRNode;
2363
- if (value.type !== "number" || !Number.isInteger(value.value) || value.value < 1) {
2364
- throw new Error("self depth must be a positive integer literal");
2365
- }
2366
- if (value.value === 1) return { type: "self" } satisfies IRNode;
2367
- return { type: "selfDepth", depth: value.value } satisfies IRNode;
2368
- },
2369
- SelfExpr_plain(selfKw) {
2370
- return selfKw.toIR();
2371
- },
2372
- SelfKw(_kw) {
2373
- return { type: "self" } satisfies IRNode;
2374
- },
2375
- TrueKw(_kw) {
2376
- return { type: "boolean", value: true } satisfies IRNode;
2377
- },
2378
- FalseKw(_kw) {
2379
- return { type: "boolean", value: false } satisfies IRNode;
2380
- },
2381
- NullKw(_kw) {
2382
- return { type: "null" } satisfies IRNode;
2383
- },
2384
- UndefinedKw(_kw) {
2385
- return { type: "undefined" } satisfies IRNode;
2386
- },
2387
-
2388
- StringKw(_kw) {
2389
- return { type: "identifier", name: "string" } satisfies IRNode;
2390
- },
2391
- NumberKw(_kw) {
2392
- return { type: "identifier", name: "number" } satisfies IRNode;
2393
- },
2394
- ObjectKw(_kw) {
2395
- return { type: "identifier", name: "object" } satisfies IRNode;
2396
- },
2397
- ArrayKw(_kw) {
2398
- return { type: "identifier", name: "array" } satisfies IRNode;
2399
- },
2400
- BooleanKw(_kw) {
2401
- return { type: "identifier", name: "boolean" } satisfies IRNode;
2402
- },
2403
-
2404
- identifier(_a, _b) {
2405
- return { type: "identifier", name: this.sourceString } satisfies IRNode;
2406
- },
2407
-
2408
- String(_value) {
2409
- return { type: "string", raw: this.sourceString } satisfies IRNode;
2410
- },
2411
- Number(_value) {
2412
- return { type: "number", raw: this.sourceString, value: parseNumber(this.sourceString) } satisfies IRNode;
2413
- },
2414
-
2415
- PrimaryExpr_group(_open, expr, _close) {
2416
- return { type: "group", expression: expr.toIR() } satisfies IRNode;
2417
- },
2418
- });
2419
-
2420
- export default semantics;
2421
-
2422
- export type { RexActionDict, RexSemantics } from "./rex.ohm-bundle.js";
1
+ import { createRequire } from "node:module";
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const rexGrammarModule = require("./rex.ohm-bundle.cjs");
5
+ const rexGrammar = rexGrammarModule?.default ?? rexGrammarModule;
6
+
7
+ export const grammar = rexGrammar;
8
+ export const semantics = rexGrammar.createSemantics();
9
+
10
+ export type IRNode =
11
+ | { type: "program"; body: IRNode[] }
12
+ | { type: "identifier"; name: string }
13
+ | { type: "self" }
14
+ | { type: "selfDepth"; depth: number }
15
+ | { type: "boolean"; value: boolean }
16
+ | { type: "null" }
17
+ | { type: "undefined" }
18
+ | { type: "number"; raw: string; value: number }
19
+ | { type: "string"; raw: string }
20
+ | { type: "array"; items: IRNode[] }
21
+ | { type: "arrayComprehension"; binding: IRBindingOrExpr; body: IRNode }
22
+ | { type: "object"; entries: { key: IRNode; value: IRNode }[] }
23
+ | {
24
+ type: "objectComprehension";
25
+ binding: IRBindingOrExpr;
26
+ key: IRNode;
27
+ value: IRNode;
28
+ }
29
+ | { type: "key"; name: string }
30
+ | { type: "group"; expression: IRNode }
31
+ | { type: "unary"; op: "neg" | "not" | "delete"; value: IRNode }
32
+ | {
33
+ type: "binary";
34
+ op:
35
+ | "add"
36
+ | "sub"
37
+ | "mul"
38
+ | "div"
39
+ | "mod"
40
+ | "bitAnd"
41
+ | "bitOr"
42
+ | "bitXor"
43
+ | "and"
44
+ | "or"
45
+ | "eq"
46
+ | "neq"
47
+ | "gt"
48
+ | "gte"
49
+ | "lt"
50
+ | "lte";
51
+ left: IRNode;
52
+ right: IRNode;
53
+ }
54
+ | {
55
+ type: "assign";
56
+ op: "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^=";
57
+ place: IRNode;
58
+ value: IRNode;
59
+ }
60
+ | {
61
+ type: "navigation";
62
+ target: IRNode;
63
+ segments: ({ type: "static"; key: string } | { type: "dynamic"; key: IRNode })[];
64
+ }
65
+ | { type: "call"; callee: IRNode; args: IRNode[] }
66
+ | {
67
+ type: "conditional";
68
+ head: "when" | "unless";
69
+ condition: IRNode;
70
+ thenBlock: IRNode[];
71
+ elseBranch?: IRConditionalElse;
72
+ }
73
+ | { type: "for"; binding: IRBindingOrExpr; body: IRNode[] }
74
+ | { type: "while"; condition: IRNode; body: IRNode[] }
75
+ | { type: "break" }
76
+ | { type: "continue" };
77
+
78
+ export type IRBinding =
79
+ | { type: "binding:keyValueIn"; key: string; value: string; source: IRNode }
80
+ | { type: "binding:valueIn"; value: string; source: IRNode }
81
+ | { type: "binding:keyOf"; key: string; source: IRNode };
82
+
83
+ export type IRBindingOrExpr = IRBinding | { type: "binding:expr"; source: IRNode };
84
+
85
+ export type IRConditionalElse =
86
+ | { type: "else"; block: IRNode[] }
87
+ | {
88
+ type: "elseChain";
89
+ head: "when" | "unless";
90
+ condition: IRNode;
91
+ thenBlock: IRNode[];
92
+ elseBranch?: IRConditionalElse;
93
+ };
94
+
95
+ const DIGITS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_";
96
+
97
+ function byteLength(value: string): number {
98
+ return Buffer.byteLength(value, "utf8");
99
+ }
100
+
101
+ const OPCODE_IDS = {
102
+ do: 0,
103
+ add: 1,
104
+ sub: 2,
105
+ mul: 3,
106
+ div: 4,
107
+ eq: 5,
108
+ neq: 6,
109
+ lt: 7,
110
+ lte: 8,
111
+ gt: 9,
112
+ gte: 10,
113
+ and: 11,
114
+ or: 12,
115
+ xor: 13,
116
+ not: 14,
117
+ boolean: 15,
118
+ number: 16,
119
+ string: 17,
120
+ array: 18,
121
+ object: 19,
122
+ mod: 20,
123
+ neg: 21,
124
+ } as const;
125
+
126
+ type OpcodeName = keyof typeof OPCODE_IDS;
127
+
128
+ type EncodeOptions = {
129
+ domainRefs?: Record<string, number>;
130
+ };
131
+
132
+ type CompileOptions = {
133
+ optimize?: boolean;
134
+ minifyNames?: boolean;
135
+ dedupeValues?: boolean;
136
+ dedupeMinBytes?: number;
137
+ domainConfig?: unknown;
138
+ };
139
+
140
+ type RexDomainConfigEntry = {
141
+ names?: unknown;
142
+ };
143
+
144
+ const FIRST_NON_RESERVED_REF = 8;
145
+ const DOMAIN_DIGIT_INDEX = new Map<string, number>(Array.from(DIGITS).map((char, index) => [char, index]));
146
+
147
+ const BINARY_TO_OPCODE: Record<Extract<IRNode, { type: "binary" }> ["op"], OpcodeName> = {
148
+ add: "add",
149
+ sub: "sub",
150
+ mul: "mul",
151
+ div: "div",
152
+ mod: "mod",
153
+ bitAnd: "and",
154
+ bitOr: "or",
155
+ bitXor: "xor",
156
+ and: "and",
157
+ or: "or",
158
+ eq: "eq",
159
+ neq: "neq",
160
+ gt: "gt",
161
+ gte: "gte",
162
+ lt: "lt",
163
+ lte: "lte",
164
+ };
165
+
166
+ const ASSIGN_COMPOUND_TO_OPCODE: Partial<Record<Extract<IRNode, { type: "assign" }> ["op"], OpcodeName>> = {
167
+ "+=": "add",
168
+ "-=": "sub",
169
+ "*=": "mul",
170
+ "/=": "div",
171
+ "%=": "mod",
172
+ "&=": "and",
173
+ "|=": "or",
174
+ "^=": "xor",
175
+ };
176
+
177
+ function encodeUint(value: number): string {
178
+ if (!Number.isInteger(value) || value < 0) throw new Error(`Cannot encode non-uint value: ${value}`);
179
+ if (value === 0) return "";
180
+ let current = value;
181
+ let out = "";
182
+ while (current > 0) {
183
+ const digit = current % 64;
184
+ out = `${DIGITS[digit]}${out}`;
185
+ current = Math.floor(current / 64);
186
+ }
187
+ return out;
188
+ }
189
+
190
+ function encodeZigzag(value: number): string {
191
+ if (!Number.isInteger(value)) throw new Error(`Cannot zigzag non-integer: ${value}`);
192
+ const encoded = value >= 0 ? value * 2 : -value * 2 - 1;
193
+ return encodeUint(encoded);
194
+ }
195
+
196
+ function encodeInt(value: number): string {
197
+ return `${encodeZigzag(value)}+`;
198
+ }
199
+
200
+ function canUseBareString(value: string): boolean {
201
+ for (const char of value) {
202
+ if (!DIGITS.includes(char)) return false;
203
+ }
204
+ return true;
205
+ }
206
+
207
+ function decodeStringLiteral(raw: string): string {
208
+ const quote = raw[0];
209
+ if ((quote !== '"' && quote !== "'") || raw[raw.length - 1] !== quote) {
210
+ throw new Error(`Invalid string literal: ${raw}`);
211
+ }
212
+ let out = "";
213
+ for (let index = 1; index < raw.length - 1; index += 1) {
214
+ const char = raw[index];
215
+ if (char !== "\\") {
216
+ out += char;
217
+ continue;
218
+ }
219
+ index += 1;
220
+ const esc = raw[index];
221
+ if (esc === undefined) throw new Error(`Invalid escape sequence in ${raw}`);
222
+ if (esc === "n") out += "\n";
223
+ else if (esc === "r") out += "\r";
224
+ else if (esc === "t") out += "\t";
225
+ else if (esc === "b") out += "\b";
226
+ else if (esc === "f") out += "\f";
227
+ else if (esc === "v") out += "\v";
228
+ else if (esc === "0") out += "\0";
229
+ else if (esc === "x") {
230
+ const hex = raw.slice(index + 1, index + 3);
231
+ if (!/^[0-9a-fA-F]{2}$/.test(hex)) throw new Error(`Invalid hex escape in ${raw}`);
232
+ out += String.fromCodePoint(parseInt(hex, 16));
233
+ index += 2;
234
+ }
235
+ else if (esc === "u") {
236
+ const hex = raw.slice(index + 1, index + 5);
237
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) throw new Error(`Invalid unicode escape in ${raw}`);
238
+ out += String.fromCodePoint(parseInt(hex, 16));
239
+ index += 4;
240
+ }
241
+ else {
242
+ out += esc;
243
+ }
244
+ }
245
+ return out;
246
+ }
247
+
248
+ function encodeBareOrLengthString(value: string): string {
249
+ if (canUseBareString(value)) return `${value}:`;
250
+ return `${encodeUint(byteLength(value))},${value}`;
251
+ }
252
+
253
+ const DEC_PARTS = /^(-?\d)(?:\.(\d+))?e([+-]\d+)$/;
254
+
255
+ function splitDecimal(num: number): { base: number; exp: number } {
256
+ const match = num.toExponential().match(DEC_PARTS);
257
+ if (!match) throw new Error(`Failed to split decimal for ${num}`);
258
+ const [, b1, b2 = "", e1] = match as RegExpMatchArray;
259
+ const base = Number.parseInt(b1 + b2, 10);
260
+ const exp = Number.parseInt(e1!, 10) - b2.length;
261
+ return { base, exp };
262
+ }
263
+
264
+ function encodeDecimal(significand: number, power: number): string {
265
+ return `${encodeZigzag(power)}*${encodeInt(significand)}`;
266
+ }
267
+
268
+ function encodeNumberNode(node: Extract<IRNode, { type: "number" }>): string {
269
+ const numberValue = node.value;
270
+ if (Number.isNaN(numberValue)) return "5'";
271
+ if (numberValue === Infinity) return "6'";
272
+ if (numberValue === -Infinity) return "7'";
273
+
274
+ if (Number.isInteger(numberValue)) {
275
+ const { base, exp } = splitDecimal(numberValue);
276
+ if (exp >= 0 && exp <= 4) return encodeInt(numberValue);
277
+ return encodeDecimal(base, exp);
278
+ }
279
+
280
+ const raw = node.raw.toLowerCase();
281
+ const sign = raw.startsWith("-") ? -1 : 1;
282
+ const unsigned = sign < 0 ? raw.slice(1) : raw;
283
+ const splitExp = unsigned.split("e");
284
+ const mantissaText = splitExp[0];
285
+ const exponentText = splitExp[1] ?? "0";
286
+ if (!mantissaText) throw new Error(`Invalid decimal literal: ${node.raw}`);
287
+ const exponent = Number(exponentText);
288
+ if (!Number.isInteger(exponent)) throw new Error(`Invalid decimal exponent: ${node.raw}`);
289
+
290
+ const dotIndex = mantissaText.indexOf(".");
291
+ const decimals = dotIndex === -1 ? 0 : mantissaText.length - dotIndex - 1;
292
+ const digits = mantissaText.replace(".", "");
293
+ if (!/^\d+$/.test(digits)) throw new Error(`Invalid decimal digits: ${node.raw}`);
294
+
295
+ let significand = Number(digits) * sign;
296
+ let power = exponent - decimals;
297
+ while (significand !== 0 && significand % 10 === 0) {
298
+ significand /= 10;
299
+ power += 1;
300
+ }
301
+ return encodeDecimal(significand, power);
302
+ }
303
+
304
+ function encodeOpcode(opcode: OpcodeName): string {
305
+ return `${encodeUint(OPCODE_IDS[opcode])}%`;
306
+ }
307
+
308
+ function encodeCallParts(parts: string[]): string {
309
+ return `(${parts.join("")})`;
310
+ }
311
+
312
+ function needsOptionalPrefix(encoded: string): boolean {
313
+ const first = encoded[0];
314
+ if (!first) return false;
315
+ return first === "[" || first === "{" || first === "(" || first === "=" || first === "~" || first === "?" || first === "!" || first === "|" || first === "&" || first === ">" || first === "<" || first === "#";
316
+ }
317
+
318
+ function addOptionalPrefix(encoded: string): string {
319
+ if (!needsOptionalPrefix(encoded)) return encoded;
320
+ let payload = encoded;
321
+ if (encoded.startsWith("?(") || encoded.startsWith("!(") || encoded.startsWith("|(") || encoded.startsWith("&(") || encoded.startsWith(">(") || encoded.startsWith("<(") || encoded.startsWith("#(")) {
322
+ payload = encoded.slice(2, -1);
323
+ }
324
+ else if (encoded.startsWith(">[") || encoded.startsWith(">{")) {
325
+ payload = encoded.slice(2, -1);
326
+ }
327
+ else if (encoded.startsWith("[") || encoded.startsWith("{") || encoded.startsWith("(")) {
328
+ payload = encoded.slice(1, -1);
329
+ }
330
+ else if (encoded.startsWith("=") || encoded.startsWith("~")) {
331
+ payload = encoded.slice(1);
332
+ }
333
+ return `${encodeUint(byteLength(payload))}${encoded}`;
334
+ }
335
+
336
+ function encodeBlockExpression(block: IRNode[]): string {
337
+ if (block.length === 0) return "4'";
338
+ if (block.length === 1) return encodeNode(block[0] as IRNode);
339
+ return encodeCallParts([encodeOpcode("do"), ...block.map((node) => encodeNode(node))]);
340
+ }
341
+
342
+ function encodeConditionalElse(elseBranch: IRConditionalElse): string {
343
+ if (elseBranch.type === "else") return encodeBlockExpression(elseBranch.block);
344
+ const nested = {
345
+ type: "conditional",
346
+ head: elseBranch.head,
347
+ condition: elseBranch.condition,
348
+ thenBlock: elseBranch.thenBlock,
349
+ elseBranch: elseBranch.elseBranch,
350
+ } satisfies IRNode;
351
+ return encodeNode(nested);
352
+ }
353
+
354
+ function encodeNavigation(node: Extract<IRNode, { type: "navigation" }>): string {
355
+ const domainRefs = activeEncodeOptions?.domainRefs;
356
+ if (domainRefs && node.target.type === "identifier") {
357
+ const staticPath = [node.target.name];
358
+ for (const segment of node.segments) {
359
+ if (segment.type !== "static") break;
360
+ staticPath.push(segment.key);
361
+ }
362
+
363
+ for (let pathLength = staticPath.length; pathLength >= 1; pathLength -= 1) {
364
+ const dottedName = staticPath.slice(0, pathLength).join(".");
365
+ const domainRef = domainRefs[dottedName];
366
+ if (domainRef === undefined) continue;
367
+
368
+ const consumedStaticSegments = pathLength - 1;
369
+ if (consumedStaticSegments === node.segments.length) {
370
+ return `${encodeUint(domainRef)}'`;
371
+ }
372
+
373
+ const parts = [`${encodeUint(domainRef)}'`];
374
+ for (const segment of node.segments.slice(consumedStaticSegments)) {
375
+ if (segment.type === "static") parts.push(encodeBareOrLengthString(segment.key));
376
+ else parts.push(encodeNode(segment.key));
377
+ }
378
+ return encodeCallParts(parts);
379
+ }
380
+ }
381
+
382
+ const parts = [encodeNode(node.target)];
383
+ for (const segment of node.segments) {
384
+ if (segment.type === "static") parts.push(encodeBareOrLengthString(segment.key));
385
+ else parts.push(encodeNode(segment.key));
386
+ }
387
+ return encodeCallParts(parts);
388
+ }
389
+
390
+ function encodeWhile(node: Extract<IRNode, { type: "while" }>): string {
391
+ const cond = encodeNode(node.condition);
392
+ const body = addOptionalPrefix(encodeBlockExpression(node.body));
393
+ return `#(${cond}${body})`;
394
+ }
395
+
396
+ function encodeFor(node: Extract<IRNode, { type: "for" }>): string {
397
+ const body = addOptionalPrefix(encodeBlockExpression(node.body));
398
+ if (node.binding.type === "binding:expr") {
399
+ return `>(${encodeNode(node.binding.source)}${body})`;
400
+ }
401
+ if (node.binding.type === "binding:valueIn") {
402
+ return `>(${encodeNode(node.binding.source)}${node.binding.value}$${body})`;
403
+ }
404
+ if (node.binding.type === "binding:keyValueIn") {
405
+ return `>(${encodeNode(node.binding.source)}${node.binding.key}$${node.binding.value}$${body})`;
406
+ }
407
+ return `<(${encodeNode(node.binding.source)}${node.binding.key}$${body})`;
408
+ }
409
+
410
+ function encodeArrayComprehension(node: Extract<IRNode, { type: "arrayComprehension" }>): string {
411
+ const body = addOptionalPrefix(encodeNode(node.body));
412
+ if (node.binding.type === "binding:expr") {
413
+ return `>[${encodeNode(node.binding.source)}${body}]`;
414
+ }
415
+ if (node.binding.type === "binding:valueIn") {
416
+ return `>[${encodeNode(node.binding.source)}${node.binding.value}$${body}]`;
417
+ }
418
+ if (node.binding.type === "binding:keyValueIn") {
419
+ return `>[${encodeNode(node.binding.source)}${node.binding.key}$${node.binding.value}$${body}]`;
420
+ }
421
+ return `>[${encodeNode(node.binding.source)}${node.binding.key}$${body}]`;
422
+ }
423
+
424
+ function encodeObjectComprehension(node: Extract<IRNode, { type: "objectComprehension" }>): string {
425
+ const key = addOptionalPrefix(encodeNode(node.key));
426
+ const value = addOptionalPrefix(encodeNode(node.value));
427
+ if (node.binding.type === "binding:expr") {
428
+ return `>{${encodeNode(node.binding.source)}${key}${value}}`;
429
+ }
430
+ if (node.binding.type === "binding:valueIn") {
431
+ return `>{${encodeNode(node.binding.source)}${node.binding.value}$${key}${value}}`;
432
+ }
433
+ if (node.binding.type === "binding:keyValueIn") {
434
+ return `>{${encodeNode(node.binding.source)}${node.binding.key}$${node.binding.value}$${key}${value}}`;
435
+ }
436
+ return `>{${encodeNode(node.binding.source)}${node.binding.key}$${key}${value}}`;
437
+ }
438
+
439
+ let activeEncodeOptions: EncodeOptions | undefined;
440
+
441
+ function encodeNode(node: IRNode): string {
442
+ switch (node.type) {
443
+ case "program":
444
+ return encodeBlockExpression(node.body);
445
+ case "identifier": {
446
+ const domainRef = activeEncodeOptions?.domainRefs?.[node.name];
447
+ if (domainRef !== undefined) return `${encodeUint(domainRef)}'`;
448
+ return `${node.name}$`;
449
+ }
450
+ case "self":
451
+ return "@";
452
+ case "selfDepth": {
453
+ if (!Number.isInteger(node.depth) || node.depth < 1) throw new Error(`Invalid self depth: ${node.depth}`);
454
+ if (node.depth === 1) return "@";
455
+ return `${encodeUint(node.depth - 1)}@`;
456
+ }
457
+ case "boolean":
458
+ return node.value ? "1'" : "2'";
459
+ case "null":
460
+ return "3'";
461
+ case "undefined":
462
+ return "4'";
463
+ case "number":
464
+ return encodeNumberNode(node);
465
+ case "string":
466
+ return encodeBareOrLengthString(decodeStringLiteral(node.raw));
467
+ case "array": {
468
+ const body = node.items.map((item) => addOptionalPrefix(encodeNode(item))).join("");
469
+ return `[${body}]`;
470
+ }
471
+ case "arrayComprehension":
472
+ return encodeArrayComprehension(node);
473
+ case "object": {
474
+ const body = node.entries
475
+ .map(({ key, value }) => `${encodeNode(key)}${addOptionalPrefix(encodeNode(value))}`)
476
+ .join("");
477
+ return `{${body}}`;
478
+ }
479
+ case "objectComprehension":
480
+ return encodeObjectComprehension(node);
481
+ case "key":
482
+ return encodeBareOrLengthString(node.name);
483
+ case "group":
484
+ return encodeNode(node.expression);
485
+ case "unary":
486
+ if (node.op === "delete") return `~${encodeNode(node.value)}`;
487
+ if (node.op === "neg") return encodeCallParts([encodeOpcode("neg"), encodeNode(node.value)]);
488
+ return encodeCallParts([encodeOpcode("not"), encodeNode(node.value)]);
489
+ case "binary":
490
+ if (node.op === "and") {
491
+ const operands = collectLogicalChain(node, "and");
492
+ const body = operands
493
+ .map((operand, index) => {
494
+ const encoded = encodeNode(operand);
495
+ return index === 0 ? encoded : addOptionalPrefix(encoded);
496
+ })
497
+ .join("");
498
+ return `&(${body})`;
499
+ }
500
+ if (node.op === "or") {
501
+ const operands = collectLogicalChain(node, "or");
502
+ const body = operands
503
+ .map((operand, index) => {
504
+ const encoded = encodeNode(operand);
505
+ return index === 0 ? encoded : addOptionalPrefix(encoded);
506
+ })
507
+ .join("");
508
+ return `|(${body})`;
509
+ }
510
+ return encodeCallParts([
511
+ encodeOpcode(BINARY_TO_OPCODE[node.op]),
512
+ encodeNode(node.left),
513
+ encodeNode(node.right),
514
+ ]);
515
+ case "assign": {
516
+ if (node.op === "=") return `=${encodeNode(node.place)}${addOptionalPrefix(encodeNode(node.value))}`;
517
+ const opcode = ASSIGN_COMPOUND_TO_OPCODE[node.op];
518
+ if (!opcode) throw new Error(`Unsupported assignment op: ${node.op}`);
519
+ const computedValue = encodeCallParts([encodeOpcode(opcode), encodeNode(node.place), encodeNode(node.value)]);
520
+ return `=${encodeNode(node.place)}${addOptionalPrefix(computedValue)}`;
521
+ }
522
+ case "navigation":
523
+ return encodeNavigation(node);
524
+ case "call":
525
+ return encodeCallParts([encodeNode(node.callee), ...node.args.map((arg) => encodeNode(arg))]);
526
+ case "conditional": {
527
+ const opener = node.head === "when" ? "?(" : "!(";
528
+ const cond = encodeNode(node.condition);
529
+ const thenExpr = addOptionalPrefix(encodeBlockExpression(node.thenBlock));
530
+ const elseExpr = node.elseBranch ? addOptionalPrefix(encodeConditionalElse(node.elseBranch)) : "";
531
+ return `${opener}${cond}${thenExpr}${elseExpr})`;
532
+ }
533
+ case "for":
534
+ return encodeFor(node);
535
+ case "while":
536
+ return encodeWhile(node);
537
+ case "break":
538
+ return ";";
539
+ case "continue":
540
+ return "1;";
541
+ default: {
542
+ const exhaustive: never = node;
543
+ throw new Error(`Unsupported IR node ${(exhaustive as { type?: string }).type ?? "unknown"}`);
544
+ }
545
+ }
546
+ }
547
+
548
+ function collectLogicalChain(node: IRNode, op: "and" | "or"): IRNode[] {
549
+ if (node.type !== "binary" || node.op !== op) return [node];
550
+ return [...collectLogicalChain(node.left, op), ...collectLogicalChain(node.right, op)];
551
+ }
552
+
553
+ export function parseToIR(source: string): IRNode {
554
+ const match = grammar.match(source);
555
+ if (!match.succeeded()) {
556
+ const failure = match as unknown as { message?: string };
557
+ throw new Error(failure.message ?? "Parse failed");
558
+ }
559
+ return semantics(match).toIR() as IRNode;
560
+ }
561
+
562
+ function parseDataNode(node: IRNode): unknown {
563
+ switch (node.type) {
564
+ case "group":
565
+ return parseDataNode(node.expression);
566
+ case "program": {
567
+ if (node.body.length === 1) return parseDataNode(node.body[0]!);
568
+ if (node.body.length === 0) return undefined;
569
+ throw new Error("Rex parse() expects a single data expression");
570
+ }
571
+ case "undefined":
572
+ return undefined;
573
+ case "null":
574
+ return null;
575
+ case "boolean":
576
+ return node.value;
577
+ case "number":
578
+ return node.value;
579
+ case "string":
580
+ return decodeStringLiteral(node.raw);
581
+ case "array":
582
+ return node.items.map((item) => parseDataNode(item));
583
+ case "object": {
584
+ const out: Record<string, unknown> = {};
585
+ for (const entry of node.entries) {
586
+ const keyNode = entry.key;
587
+ let key: string;
588
+ if (keyNode.type === "key") key = keyNode.name;
589
+ else {
590
+ const keyValue = parseDataNode(keyNode);
591
+ key = String(keyValue);
592
+ }
593
+ out[key] = parseDataNode(entry.value);
594
+ }
595
+ return out;
596
+ }
597
+ default:
598
+ throw new Error(`Rex parse() only supports data expressions. Found: ${node.type}`);
599
+ }
600
+ }
601
+
602
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
603
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
604
+ const proto = Object.getPrototypeOf(value);
605
+ return proto === Object.prototype || proto === null;
606
+ }
607
+
608
+ function isBareKeyName(key: string): boolean {
609
+ return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(key);
610
+ }
611
+
612
+ function isNumericKey(key: string): boolean {
613
+ if (key === "") return false;
614
+ return String(Number(key)) === key && Number.isFinite(Number(key));
615
+ }
616
+
617
+ function stringifyKey(key: string): string {
618
+ if (isBareKeyName(key)) return key;
619
+ if (isNumericKey(key)) return key;
620
+ return stringifyString(key);
621
+ }
622
+
623
+ function stringifyString(value: string): string {
624
+ return JSON.stringify(value);
625
+ }
626
+
627
+ function stringifyInline(value: unknown): string {
628
+ if (value === undefined) return "undefined";
629
+ if (value === null) return "null";
630
+ if (typeof value === "boolean") return value ? "true" : "false";
631
+ if (typeof value === "number") {
632
+ if (Number.isNaN(value)) return "nan";
633
+ if (value === Infinity) return "inf";
634
+ if (value === -Infinity) return "-inf";
635
+ return String(value);
636
+ }
637
+ if (typeof value === "string") return stringifyString(value);
638
+ if (Array.isArray(value)) {
639
+ if (value.length === 0) return "[]";
640
+ return `[${value.map((item) => stringifyInline(item)).join(" ")}]`;
641
+ }
642
+ if (isPlainObject(value)) {
643
+ const entries = Object.entries(value);
644
+ if (entries.length === 0) return "{}";
645
+ const body = entries
646
+ .map(([key, item]) => `${stringifyKey(key)}: ${stringifyInline(item)}`)
647
+ .join(" ");
648
+ return `{${body}}`;
649
+ }
650
+ throw new Error(`Rex stringify() cannot encode value of type ${typeof value}`);
651
+ }
652
+
653
+ function fitsInline(rendered: string, depth: number, indentSize: number, maxWidth: number): boolean {
654
+ if (rendered.includes("\n")) return false;
655
+ return depth * indentSize + rendered.length <= maxWidth;
656
+ }
657
+
658
+ function stringifyPretty(value: unknown, depth: number, indentSize: number, maxWidth: number): string {
659
+ const inline = stringifyInline(value);
660
+ if (fitsInline(inline, depth, indentSize, maxWidth)) return inline;
661
+
662
+ const indent = " ".repeat(depth * indentSize);
663
+ const childIndent = " ".repeat((depth + 1) * indentSize);
664
+
665
+ if (Array.isArray(value)) {
666
+ if (value.length === 0) return "[]";
667
+ const lines = value.map((item) => {
668
+ const rendered = stringifyPretty(item, depth + 1, indentSize, maxWidth);
669
+ if (!rendered.includes("\n")) return `${childIndent}${rendered}`;
670
+ return `${childIndent}${rendered}`;
671
+ });
672
+ return `[\n${lines.join("\n")}\n${indent}]`;
673
+ }
674
+
675
+ if (isPlainObject(value)) {
676
+ const entries = Object.entries(value);
677
+ if (entries.length === 0) return "{}";
678
+ const lines = entries.map(([key, item]) => {
679
+ const keyText = stringifyKey(key);
680
+ const rendered = stringifyPretty(item, depth + 1, indentSize, maxWidth);
681
+ return `${childIndent}${keyText}: ${rendered}`;
682
+ });
683
+ return `{\n${lines.join("\n")}\n${indent}}`;
684
+ }
685
+
686
+ return inline;
687
+ }
688
+
689
+ export function parse(source: string): unknown {
690
+ return parseDataNode(parseToIR(source));
691
+ }
692
+
693
+ export function domainRefsFromConfig(config: unknown): Record<string, number> {
694
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
695
+ throw new Error("Domain config must be an object");
696
+ }
697
+ const refs: Record<string, number> = {};
698
+ for (const section of Object.values(config)) {
699
+ if (!section || typeof section !== "object" || Array.isArray(section)) continue;
700
+ mapConfigEntries(section as Record<string, unknown>, refs);
701
+ }
702
+ return refs;
703
+ }
704
+
705
+ function decodeDomainRefKey(refText: string): number {
706
+ if (!refText) throw new Error("Domain ref key cannot be empty");
707
+ if (!/^[0-9A-Za-z_-]+$/.test(refText)) {
708
+ throw new Error(`Invalid domain ref key '${refText}' (must use base64 alphabet 0-9a-zA-Z-_)`);
709
+ }
710
+ if (refText.length > 1 && refText[0] === "0") {
711
+ throw new Error(`Invalid domain ref key '${refText}' (leading zeroes are not allowed)`);
712
+ }
713
+ if (/^[1-9]$/.test(refText)) {
714
+ throw new Error(`Invalid domain ref key '${refText}' (reserved by core language)`);
715
+ }
716
+
717
+ let value = 0;
718
+ for (const char of refText) {
719
+ const digit = DOMAIN_DIGIT_INDEX.get(char);
720
+ if (digit === undefined) throw new Error(`Invalid domain ref key '${refText}'`);
721
+ value = value * 64 + digit;
722
+ if (value > Number.MAX_SAFE_INTEGER) {
723
+ throw new Error(`Invalid domain ref key '${refText}' (must fit in 53 bits)`);
724
+ }
725
+ }
726
+
727
+ if (value < FIRST_NON_RESERVED_REF) {
728
+ throw new Error(`Invalid domain ref key '${refText}' (maps to reserved id ${value})`);
729
+ }
730
+
731
+ return value;
732
+ }
733
+
734
+ function mapConfigEntries(entries: Record<string, unknown>, refs: Record<string, number>) {
735
+ const sourceKindByRoot = new Map<string, "explicit" | "implicit">();
736
+ for (const root of Object.keys(refs)) {
737
+ sourceKindByRoot.set(root, "explicit");
738
+ }
739
+
740
+ for (const [refText, rawEntry] of Object.entries(entries)) {
741
+ const entry = rawEntry as RexDomainConfigEntry;
742
+ if (!entry || typeof entry !== "object") continue;
743
+ if (!Array.isArray(entry.names)) continue;
744
+
745
+ const refId = decodeDomainRefKey(refText);
746
+ for (const rawName of entry.names) {
747
+ if (typeof rawName !== "string") continue;
748
+ const existingNameRef = refs[rawName];
749
+ if (existingNameRef !== undefined && existingNameRef !== refId) {
750
+ throw new Error(`Conflicting refs for '${rawName}': ${existingNameRef} vs ${refId}`);
751
+ }
752
+ refs[rawName] = refId;
753
+
754
+ const root = rawName.split(".")[0];
755
+ if (!root) continue;
756
+ const currentKind: "explicit" | "implicit" = rawName.includes(".") ? "implicit" : "explicit";
757
+ const existing = refs[root];
758
+ if (existing !== undefined) {
759
+ if (existing === refId) continue;
760
+ const existingKind = sourceKindByRoot.get(root) ?? "explicit";
761
+ if (currentKind === "explicit") {
762
+ throw new Error(`Conflicting refs for '${root}': ${existing} vs ${refId}`);
763
+ }
764
+ if (existingKind === "explicit") continue;
765
+ continue;
766
+ }
767
+ refs[root] = refId;
768
+ sourceKindByRoot.set(root, currentKind);
769
+ }
770
+ }
771
+ }
772
+
773
+ export function stringify(value: unknown, options?: { indent?: number; maxWidth?: number }): string {
774
+ const indent = options?.indent ?? 2;
775
+ const maxWidth = options?.maxWidth ?? 80;
776
+ if (!Number.isInteger(indent) || indent < 0) throw new Error("Rex stringify() indent must be a non-negative integer");
777
+ if (!Number.isInteger(maxWidth) || maxWidth < 20) throw new Error("Rex stringify() maxWidth must be an integer >= 20");
778
+ return stringifyPretty(value, 0, indent, maxWidth);
779
+ }
780
+
781
+ const DIGIT_SET = new Set(DIGITS.split(""));
782
+ const DIGIT_INDEX = new Map<string, number>(Array.from(DIGITS).map((char, index) => [char, index]));
783
+
784
+ type EncodedSpan = { start: number; end: number; raw: string };
785
+ type DedupeCandidate = {
786
+ span: EncodedSpan;
787
+ sizeBytes: number;
788
+ offsetFromEnd: number;
789
+ };
790
+
791
+ function readPrefixAt(text: string, start: number): { end: number; raw: string; value: number } {
792
+ let index = start;
793
+ while (index < text.length && DIGIT_SET.has(text[index] as string)) index += 1;
794
+ const raw = text.slice(start, index);
795
+ let value = 0;
796
+ for (const char of raw) {
797
+ const digit = DIGIT_INDEX.get(char);
798
+ if (digit === undefined) throw new Error(`Invalid prefix in encoded stream at ${start}`);
799
+ value = value * 64 + digit;
800
+ }
801
+ return { end: index, raw, value };
802
+ }
803
+
804
+ function parsePlaceEnd(text: string, start: number, out?: EncodedSpan[]): number {
805
+ if (text[start] === "(") {
806
+ let index = start + 1;
807
+ while (index < text.length && text[index] !== ")") {
808
+ index = parseValueEnd(text, index, out).end;
809
+ }
810
+ if (text[index] !== ")") throw new Error(`Unterminated place at ${start}`);
811
+ return index + 1;
812
+ }
813
+
814
+ const prefix = readPrefixAt(text, start);
815
+ const tag = text[prefix.end];
816
+ if (tag !== "$" && tag !== "'") throw new Error(`Invalid place at ${start}`);
817
+ let index = prefix.end + 1;
818
+ if (text[index] !== "(") return index;
819
+ index += 1;
820
+ while (index < text.length && text[index] !== ")") {
821
+ index = parseValueEnd(text, index, out).end;
822
+ }
823
+ if (text[index] !== ")") throw new Error(`Unterminated place at ${start}`);
824
+ return index + 1;
825
+ }
826
+
827
+ function parseValueEnd(text: string, start: number, out?: EncodedSpan[]): EncodedSpan {
828
+ const prefix = readPrefixAt(text, start);
829
+ const tag = text[prefix.end];
830
+ if (!tag) throw new Error(`Unexpected end of encoded stream at ${start}`);
831
+
832
+ if (tag === ",") {
833
+ const strStart = prefix.end + 1;
834
+ const strEnd = strStart + prefix.value;
835
+ if (strEnd > text.length) throw new Error(`String overflows encoded stream at ${start}`);
836
+ const raw = text.slice(start, strEnd);
837
+ if (Buffer.byteLength(text.slice(strStart, strEnd), "utf8") !== prefix.value) {
838
+ throw new Error(`Non-ASCII length-string not currently dedupe-safe at ${start}`);
839
+ }
840
+ const span = { start, end: strEnd, raw };
841
+ if (out) out.push(span);
842
+ return span;
843
+ }
844
+
845
+ if (tag === "=") {
846
+ const placeEnd = parsePlaceEnd(text, prefix.end + 1, out);
847
+ const valueEnd = parseValueEnd(text, placeEnd, out).end;
848
+ const span = { start, end: valueEnd, raw: text.slice(start, valueEnd) };
849
+ if (out) out.push(span);
850
+ return span;
851
+ }
852
+
853
+ if (tag === "~") {
854
+ const placeEnd = parsePlaceEnd(text, prefix.end + 1, out);
855
+ const span = { start, end: placeEnd, raw: text.slice(start, placeEnd) };
856
+ if (out) out.push(span);
857
+ return span;
858
+ }
859
+
860
+ if (tag === "(" || tag === "[" || tag === "{") {
861
+ const close = tag === "(" ? ")" : tag === "[" ? "]" : "}";
862
+ let index = prefix.end + 1;
863
+ while (index < text.length && text[index] !== close) {
864
+ index = parseValueEnd(text, index, out).end;
865
+ }
866
+ if (text[index] !== close) throw new Error(`Unterminated container at ${start}`);
867
+ const span = { start, end: index + 1, raw: text.slice(start, index + 1) };
868
+ if (out) out.push(span);
869
+ return span;
870
+ }
871
+
872
+ if (tag === "?" || tag === "!" || tag === "|" || tag === "&") {
873
+ if (text[prefix.end + 1] !== "(") throw new Error(`Expected '(' after '${tag}' at ${start}`);
874
+ let index = prefix.end + 2;
875
+ while (index < text.length && text[index] !== ")") {
876
+ index = parseValueEnd(text, index, out).end;
877
+ }
878
+ if (text[index] !== ")") throw new Error(`Unterminated flow container at ${start}`);
879
+ const span = { start, end: index + 1, raw: text.slice(start, index + 1) };
880
+ if (out) out.push(span);
881
+ return span;
882
+ }
883
+
884
+ if (tag === ">" || tag === "<") {
885
+ const open = text[prefix.end + 1];
886
+ if (open !== "(" && open !== "[" && open !== "{") throw new Error(`Invalid loop opener at ${start}`);
887
+ const close = open === "(" ? ")" : open === "[" ? "]" : "}";
888
+ let index = prefix.end + 2;
889
+ while (index < text.length && text[index] !== close) {
890
+ index = parseValueEnd(text, index, out).end;
891
+ }
892
+ if (text[index] !== close) throw new Error(`Unterminated loop container at ${start}`);
893
+ const span = { start, end: index + 1, raw: text.slice(start, index + 1) };
894
+ if (out) out.push(span);
895
+ return span;
896
+ }
897
+
898
+ const span = { start, end: prefix.end + 1, raw: text.slice(start, prefix.end + 1) };
899
+ if (out) out.push(span);
900
+ return span;
901
+ }
902
+
903
+ function gatherEncodedValueSpans(text: string): EncodedSpan[] {
904
+ const spans: EncodedSpan[] = [];
905
+ let index = 0;
906
+ while (index < text.length) {
907
+ const span = parseValueEnd(text, index, spans);
908
+ index = span.end;
909
+ }
910
+ return spans;
911
+ }
912
+
913
+ function buildPointerToken(pointerStart: number, targetStart: number): string | undefined {
914
+ let offset = targetStart - (pointerStart + 1);
915
+ if (offset < 0) return undefined;
916
+ for (let guard = 0; guard < 8; guard += 1) {
917
+ const prefix = encodeUint(offset);
918
+ const recalculated = targetStart - (pointerStart + prefix.length + 1);
919
+ if (recalculated === offset) return `${prefix}^`;
920
+ offset = recalculated;
921
+ if (offset < 0) return undefined;
922
+ }
923
+ return undefined;
924
+ }
925
+
926
+ function buildDedupeCandidateTable(encoded: string, minBytes: number): Map<string, DedupeCandidate[]> {
927
+ const spans = gatherEncodedValueSpans(encoded);
928
+ const table = new Map<string, DedupeCandidate[]>();
929
+ for (const span of spans) {
930
+ const sizeBytes = span.raw.length;
931
+ if (sizeBytes < minBytes) continue;
932
+ const prefix = readPrefixAt(encoded, span.start);
933
+ const tag = encoded[prefix.end];
934
+ if (tag !== "{" && tag !== "[" && tag !== "," && tag !== ":") continue;
935
+
936
+ const offsetFromEnd = encoded.length - span.end;
937
+ const entry: DedupeCandidate = {
938
+ span,
939
+ sizeBytes,
940
+ offsetFromEnd,
941
+ };
942
+
943
+ if (!table.has(span.raw)) table.set(span.raw, []);
944
+ (table.get(span.raw) as DedupeCandidate[]).push(entry);
945
+ }
946
+ return table;
947
+ }
948
+
949
+ function dedupeLargeEncodedValues(encoded: string, minBytes = 4): string {
950
+ const effectiveMinBytes = Math.max(1, minBytes);
951
+ let current = encoded;
952
+ while (true) {
953
+ const groups = buildDedupeCandidateTable(current, effectiveMinBytes);
954
+
955
+ let replaced = false;
956
+ for (const [value, occurrences] of groups.entries()) {
957
+ if (occurrences.length < 2) continue;
958
+ const canonical = occurrences[occurrences.length - 1] as DedupeCandidate;
959
+ for (let index = occurrences.length - 2; index >= 0; index -= 1) {
960
+ const occurrence = occurrences[index] as DedupeCandidate;
961
+ if (occurrence.span.end > canonical.span.start) continue;
962
+
963
+ if (current.slice(occurrence.span.start, occurrence.span.end) !== value) continue;
964
+
965
+ const canonicalCurrentStart = current.length - canonical.offsetFromEnd - canonical.sizeBytes;
966
+ const pointerToken = buildPointerToken(occurrence.span.start, canonicalCurrentStart);
967
+ if (!pointerToken) continue;
968
+ if (pointerToken.length >= occurrence.sizeBytes) continue;
969
+
970
+ current = `${current.slice(0, occurrence.span.start)}${pointerToken}${current.slice(occurrence.span.end)}`;
971
+ replaced = true;
972
+ break;
973
+ }
974
+ if (replaced) break;
975
+ }
976
+
977
+ if (!replaced) return current;
978
+ }
979
+ }
980
+
981
+ export function encodeIR(node: IRNode, options?: EncodeOptions & { dedupeValues?: boolean; dedupeMinBytes?: number }): string {
982
+ const previous = activeEncodeOptions;
983
+ activeEncodeOptions = options;
984
+ try {
985
+ const encoded = encodeNode(node);
986
+ if (options?.dedupeValues) {
987
+ return dedupeLargeEncodedValues(encoded, options.dedupeMinBytes ?? 4);
988
+ }
989
+ return encoded;
990
+ } finally {
991
+ activeEncodeOptions = previous;
992
+ }
993
+ }
994
+
995
+ type OptimizeEnv = {
996
+ constants: Record<string, IRNode>;
997
+ selfCaptures: Record<string, number>;
998
+ };
999
+
1000
+ function cloneNode<T extends IRNode>(node: T): T {
1001
+ return structuredClone(node);
1002
+ }
1003
+
1004
+ function emptyOptimizeEnv(): OptimizeEnv {
1005
+ return { constants: {}, selfCaptures: {} };
1006
+ }
1007
+
1008
+ function cloneOptimizeEnv(env: OptimizeEnv): OptimizeEnv {
1009
+ return {
1010
+ constants: { ...env.constants },
1011
+ selfCaptures: { ...env.selfCaptures },
1012
+ };
1013
+ }
1014
+
1015
+ function clearOptimizeEnv(env: OptimizeEnv) {
1016
+ for (const key of Object.keys(env.constants)) delete env.constants[key];
1017
+ for (const key of Object.keys(env.selfCaptures)) delete env.selfCaptures[key];
1018
+ }
1019
+
1020
+ function clearBinding(env: OptimizeEnv, name: string) {
1021
+ delete env.constants[name];
1022
+ delete env.selfCaptures[name];
1023
+ }
1024
+
1025
+ function selfTargetFromNode(node: IRNode, currentDepth: number): number | undefined {
1026
+ if (node.type === "self") return currentDepth;
1027
+ if (node.type === "selfDepth") {
1028
+ const target = currentDepth - (node.depth - 1);
1029
+ if (target >= 1) return target;
1030
+ }
1031
+ return undefined;
1032
+ }
1033
+
1034
+ function selfNodeFromTarget(targetDepth: number, currentDepth: number): IRNode | undefined {
1035
+ const relDepth = currentDepth - targetDepth + 1;
1036
+ if (!Number.isInteger(relDepth) || relDepth < 1) return undefined;
1037
+ if (relDepth === 1) return { type: "self" } satisfies IRNode;
1038
+ return { type: "selfDepth", depth: relDepth } satisfies IRNode;
1039
+ }
1040
+
1041
+ function dropBindingNames(env: OptimizeEnv, binding: IRBindingOrExpr) {
1042
+ if (binding.type === "binding:valueIn") {
1043
+ clearBinding(env, binding.value);
1044
+ return;
1045
+ }
1046
+ if (binding.type === "binding:keyValueIn") {
1047
+ clearBinding(env, binding.key);
1048
+ clearBinding(env, binding.value);
1049
+ return;
1050
+ }
1051
+ if (binding.type === "binding:keyOf") {
1052
+ clearBinding(env, binding.key);
1053
+ }
1054
+ }
1055
+
1056
+ function collectReads(node: IRNode, out: Set<string>) {
1057
+ switch (node.type) {
1058
+ case "identifier":
1059
+ out.add(node.name);
1060
+ return;
1061
+ case "group":
1062
+ collectReads(node.expression, out);
1063
+ return;
1064
+ case "array":
1065
+ for (const item of node.items) collectReads(item, out);
1066
+ return;
1067
+ case "object":
1068
+ for (const entry of node.entries) {
1069
+ collectReads(entry.key, out);
1070
+ collectReads(entry.value, out);
1071
+ }
1072
+ return;
1073
+ case "arrayComprehension":
1074
+ collectReads(node.binding.source, out);
1075
+ collectReads(node.body, out);
1076
+ return;
1077
+ case "objectComprehension":
1078
+ collectReads(node.binding.source, out);
1079
+ collectReads(node.key, out);
1080
+ collectReads(node.value, out);
1081
+ return;
1082
+ case "unary":
1083
+ collectReads(node.value, out);
1084
+ return;
1085
+ case "binary":
1086
+ collectReads(node.left, out);
1087
+ collectReads(node.right, out);
1088
+ return;
1089
+ case "assign":
1090
+ if (!(node.op === "=" && node.place.type === "identifier")) collectReads(node.place, out);
1091
+ collectReads(node.value, out);
1092
+ return;
1093
+ case "navigation":
1094
+ collectReads(node.target, out);
1095
+ for (const segment of node.segments) {
1096
+ if (segment.type === "dynamic") collectReads(segment.key, out);
1097
+ }
1098
+ return;
1099
+ case "call":
1100
+ collectReads(node.callee, out);
1101
+ for (const arg of node.args) collectReads(arg, out);
1102
+ return;
1103
+ case "conditional":
1104
+ collectReads(node.condition, out);
1105
+ for (const part of node.thenBlock) collectReads(part, out);
1106
+ if (node.elseBranch) collectReadsElse(node.elseBranch, out);
1107
+ return;
1108
+ case "for":
1109
+ collectReads(node.binding.source, out);
1110
+ for (const part of node.body) collectReads(part, out);
1111
+ return;
1112
+ case "program":
1113
+ for (const part of node.body) collectReads(part, out);
1114
+ return;
1115
+ default:
1116
+ return;
1117
+ }
1118
+ }
1119
+
1120
+ function collectReadsElse(elseBranch: IRConditionalElse, out: Set<string>) {
1121
+ if (elseBranch.type === "else") {
1122
+ for (const part of elseBranch.block) collectReads(part, out);
1123
+ return;
1124
+ }
1125
+ collectReads(elseBranch.condition, out);
1126
+ for (const part of elseBranch.thenBlock) collectReads(part, out);
1127
+ if (elseBranch.elseBranch) collectReadsElse(elseBranch.elseBranch, out);
1128
+ }
1129
+
1130
+ function isPureNode(node: IRNode): boolean {
1131
+ switch (node.type) {
1132
+ case "identifier":
1133
+ case "self":
1134
+ case "selfDepth":
1135
+ case "boolean":
1136
+ case "null":
1137
+ case "undefined":
1138
+ case "number":
1139
+ case "string":
1140
+ case "key":
1141
+ return true;
1142
+ case "group":
1143
+ return isPureNode(node.expression);
1144
+ case "array":
1145
+ return node.items.every((item) => isPureNode(item));
1146
+ case "object":
1147
+ return node.entries.every((entry) => isPureNode(entry.key) && isPureNode(entry.value));
1148
+ case "navigation":
1149
+ return isPureNode(node.target) && node.segments.every((segment) => segment.type === "static" || isPureNode(segment.key));
1150
+ case "unary":
1151
+ return node.op !== "delete" && isPureNode(node.value);
1152
+ case "binary":
1153
+ return isPureNode(node.left) && isPureNode(node.right);
1154
+ default:
1155
+ return false;
1156
+ }
1157
+ }
1158
+
1159
+ function eliminateDeadAssignments(block: IRNode[]): IRNode[] {
1160
+ const needed = new Set<string>();
1161
+ const out: IRNode[] = [];
1162
+
1163
+ for (let index = block.length - 1; index >= 0; index -= 1) {
1164
+ const node = block[index] as IRNode;
1165
+
1166
+ if (node.type === "conditional") {
1167
+ let rewritten = node;
1168
+ if (
1169
+ node.condition.type === "assign"
1170
+ && node.condition.op === "="
1171
+ && node.condition.place.type === "identifier"
1172
+ ) {
1173
+ const name = node.condition.place.name;
1174
+ const branchReads = new Set<string>();
1175
+ for (const part of node.thenBlock) collectReads(part, branchReads);
1176
+ if (node.elseBranch) collectReadsElse(node.elseBranch, branchReads);
1177
+ if (!needed.has(name) && !branchReads.has(name)) {
1178
+ rewritten = {
1179
+ type: "conditional",
1180
+ head: node.head,
1181
+ condition: node.condition.value,
1182
+ thenBlock: node.thenBlock,
1183
+ elseBranch: node.elseBranch,
1184
+ } satisfies IRNode;
1185
+ }
1186
+ }
1187
+
1188
+ collectReads(rewritten, needed);
1189
+ out.push(rewritten);
1190
+ continue;
1191
+ }
1192
+
1193
+ if (node.type === "assign" && node.op === "=" && node.place.type === "identifier") {
1194
+ collectReads(node.value, needed);
1195
+ const name = node.place.name;
1196
+ const canDrop = !needed.has(name) && isPureNode(node.value);
1197
+ needed.delete(name);
1198
+ if (canDrop) continue;
1199
+ out.push(node);
1200
+ continue;
1201
+ }
1202
+
1203
+ collectReads(node, needed);
1204
+ out.push(node);
1205
+ }
1206
+
1207
+ out.reverse();
1208
+ return out;
1209
+ }
1210
+
1211
+ function hasIdentifierRead(node: IRNode, name: string, asPlace = false): boolean {
1212
+ if (node.type === "identifier") return !asPlace && node.name === name;
1213
+ switch (node.type) {
1214
+ case "group":
1215
+ return hasIdentifierRead(node.expression, name);
1216
+ case "array":
1217
+ return node.items.some((item) => hasIdentifierRead(item, name));
1218
+ case "object":
1219
+ return node.entries.some((entry) => hasIdentifierRead(entry.key, name) || hasIdentifierRead(entry.value, name));
1220
+ case "navigation":
1221
+ return hasIdentifierRead(node.target, name) || node.segments.some((segment) => segment.type === "dynamic" && hasIdentifierRead(segment.key, name));
1222
+ case "unary":
1223
+ return hasIdentifierRead(node.value, name, node.op === "delete");
1224
+ case "binary":
1225
+ return hasIdentifierRead(node.left, name) || hasIdentifierRead(node.right, name);
1226
+ case "assign":
1227
+ return hasIdentifierRead(node.place, name, true) || hasIdentifierRead(node.value, name);
1228
+ default:
1229
+ return false;
1230
+ }
1231
+ }
1232
+
1233
+ function countIdentifierReads(node: IRNode, name: string, asPlace = false): number {
1234
+ if (node.type === "identifier") return !asPlace && node.name === name ? 1 : 0;
1235
+ switch (node.type) {
1236
+ case "group":
1237
+ return countIdentifierReads(node.expression, name);
1238
+ case "array":
1239
+ return node.items.reduce((sum, item) => sum + countIdentifierReads(item, name), 0);
1240
+ case "object":
1241
+ return node.entries.reduce((sum, entry) => sum + countIdentifierReads(entry.key, name) + countIdentifierReads(entry.value, name), 0);
1242
+ case "navigation":
1243
+ return countIdentifierReads(node.target, name) + node.segments.reduce((sum, segment) => sum + (segment.type === "dynamic" ? countIdentifierReads(segment.key, name) : 0), 0);
1244
+ case "unary":
1245
+ return countIdentifierReads(node.value, name, node.op === "delete");
1246
+ case "binary":
1247
+ return countIdentifierReads(node.left, name) + countIdentifierReads(node.right, name);
1248
+ case "assign":
1249
+ return countIdentifierReads(node.place, name, true) + countIdentifierReads(node.value, name);
1250
+ default:
1251
+ return 0;
1252
+ }
1253
+ }
1254
+
1255
+ function replaceIdentifier(node: IRNode, name: string, replacement: IRNode, asPlace = false): IRNode {
1256
+ if (node.type === "identifier") {
1257
+ if (!asPlace && node.name === name) return cloneNode(replacement);
1258
+ return node;
1259
+ }
1260
+
1261
+ switch (node.type) {
1262
+ case "group":
1263
+ return {
1264
+ type: "group",
1265
+ expression: replaceIdentifier(node.expression, name, replacement),
1266
+ } satisfies IRNode;
1267
+ case "array":
1268
+ return { type: "array", items: node.items.map((item) => replaceIdentifier(item, name, replacement)) } satisfies IRNode;
1269
+ case "object":
1270
+ return {
1271
+ type: "object",
1272
+ entries: node.entries.map((entry) => ({
1273
+ key: replaceIdentifier(entry.key, name, replacement),
1274
+ value: replaceIdentifier(entry.value, name, replacement),
1275
+ })),
1276
+ } satisfies IRNode;
1277
+ case "navigation":
1278
+ return {
1279
+ type: "navigation",
1280
+ target: replaceIdentifier(node.target, name, replacement),
1281
+ segments: node.segments.map((segment) => segment.type === "static"
1282
+ ? segment
1283
+ : { type: "dynamic", key: replaceIdentifier(segment.key, name, replacement) }),
1284
+ } satisfies IRNode;
1285
+ case "unary":
1286
+ return {
1287
+ type: "unary",
1288
+ op: node.op,
1289
+ value: replaceIdentifier(node.value, name, replacement, node.op === "delete"),
1290
+ } satisfies IRNode;
1291
+ case "binary":
1292
+ return {
1293
+ type: "binary",
1294
+ op: node.op,
1295
+ left: replaceIdentifier(node.left, name, replacement),
1296
+ right: replaceIdentifier(node.right, name, replacement),
1297
+ } satisfies IRNode;
1298
+ case "assign":
1299
+ return {
1300
+ type: "assign",
1301
+ op: node.op,
1302
+ place: replaceIdentifier(node.place, name, replacement, true),
1303
+ value: replaceIdentifier(node.value, name, replacement),
1304
+ } satisfies IRNode;
1305
+ default:
1306
+ return node;
1307
+ }
1308
+ }
1309
+
1310
+ function isSafeInlineTargetNode(node: IRNode): boolean {
1311
+ if (isPureNode(node)) return true;
1312
+ if (node.type === "assign" && node.op === "=") {
1313
+ return isPureNode(node.place) && isPureNode(node.value);
1314
+ }
1315
+ return false;
1316
+ }
1317
+
1318
+ function inlineAdjacentPureAssignments(block: IRNode[]): IRNode[] {
1319
+ const out = [...block];
1320
+ let changed = true;
1321
+
1322
+ while (changed) {
1323
+ changed = false;
1324
+ for (let index = 0; index < out.length - 1; index += 1) {
1325
+ const current = out[index] as IRNode;
1326
+ if (current.type !== "assign" || current.op !== "=" || current.place.type !== "identifier") continue;
1327
+ if (!isPureNode(current.value)) continue;
1328
+ const name = current.place.name;
1329
+ if (hasIdentifierRead(current.value, name)) continue;
1330
+
1331
+ const next = out[index + 1] as IRNode;
1332
+ if (!isSafeInlineTargetNode(next)) continue;
1333
+ if (countIdentifierReads(next, name) !== 1) continue;
1334
+
1335
+ out[index + 1] = replaceIdentifier(next, name, current.value);
1336
+ out.splice(index, 1);
1337
+ changed = true;
1338
+ break;
1339
+ }
1340
+ }
1341
+
1342
+ return out;
1343
+ }
1344
+
1345
+ function toNumberNode(value: number): IRNode {
1346
+ let raw: string;
1347
+ if (Number.isNaN(value)) raw = "nan";
1348
+ else if (value === Infinity) raw = "inf";
1349
+ else if (value === -Infinity) raw = "-inf";
1350
+ else raw = String(value);
1351
+ return { type: "number", raw, value } satisfies IRNode;
1352
+ }
1353
+
1354
+ function toStringNode(value: string): IRNode {
1355
+ return { type: "string", raw: JSON.stringify(value) } satisfies IRNode;
1356
+ }
1357
+
1358
+ function toLiteralNode(value: unknown): IRNode | undefined {
1359
+ if (value === undefined) return { type: "undefined" } satisfies IRNode;
1360
+ if (value === null) return { type: "null" } satisfies IRNode;
1361
+ if (typeof value === "boolean") return { type: "boolean", value } satisfies IRNode;
1362
+ if (typeof value === "number") return toNumberNode(value);
1363
+ if (typeof value === "string") return toStringNode(value);
1364
+ if (Array.isArray(value)) {
1365
+ const items: IRNode[] = [];
1366
+ for (const item of value) {
1367
+ const lowered = toLiteralNode(item);
1368
+ if (!lowered) return undefined;
1369
+ items.push(lowered);
1370
+ }
1371
+ return { type: "array", items } satisfies IRNode;
1372
+ }
1373
+ if (value && typeof value === "object") {
1374
+ const entries: Array<{ key: IRNode; value: IRNode }> = [];
1375
+ for (const [key, entryValue] of Object.entries(value as Record<string, unknown>)) {
1376
+ const loweredValue = toLiteralNode(entryValue);
1377
+ if (!loweredValue) return undefined;
1378
+ entries.push({ key: { type: "key", name: key }, value: loweredValue });
1379
+ }
1380
+ return { type: "object", entries } satisfies IRNode;
1381
+ }
1382
+ return undefined;
1383
+ }
1384
+
1385
+ function constValue(node: IRNode): unknown | undefined {
1386
+ switch (node.type) {
1387
+ case "undefined":
1388
+ return undefined;
1389
+ case "null":
1390
+ return null;
1391
+ case "boolean":
1392
+ return node.value;
1393
+ case "number":
1394
+ return node.value;
1395
+ case "string":
1396
+ return decodeStringLiteral(node.raw);
1397
+ case "key":
1398
+ return node.name;
1399
+ case "array": {
1400
+ const out: unknown[] = [];
1401
+ for (const item of node.items) {
1402
+ const value = constValue(item);
1403
+ if (value === undefined && item.type !== "undefined") return undefined;
1404
+ out.push(value);
1405
+ }
1406
+ return out;
1407
+ }
1408
+ case "object": {
1409
+ const out: Record<string, unknown> = {};
1410
+ for (const entry of node.entries) {
1411
+ const key = constValue(entry.key);
1412
+ if (key === undefined && entry.key.type !== "undefined") return undefined;
1413
+ const value = constValue(entry.value);
1414
+ if (value === undefined && entry.value.type !== "undefined") return undefined;
1415
+ out[String(key)] = value;
1416
+ }
1417
+ return out;
1418
+ }
1419
+ default:
1420
+ return undefined;
1421
+ }
1422
+ }
1423
+
1424
+ function isDefinedValue(value: unknown): boolean {
1425
+ return value !== undefined;
1426
+ }
1427
+
1428
+ function foldUnary(op: Extract<IRNode, { type: "unary" }> ["op"], value: unknown): unknown | undefined {
1429
+ if (op === "neg") {
1430
+ if (typeof value !== "number") return undefined;
1431
+ return -value;
1432
+ }
1433
+ if (op === "not") {
1434
+ if (typeof value === "boolean") return !value;
1435
+ if (typeof value === "number") return ~value;
1436
+ return undefined;
1437
+ }
1438
+ return undefined;
1439
+ }
1440
+
1441
+ function foldBinary(op: Extract<IRNode, { type: "binary" }> ["op"], left: unknown, right: unknown): unknown | undefined {
1442
+ if (op === "add" || op === "sub" || op === "mul" || op === "div" || op === "mod") {
1443
+ if (typeof left !== "number" || typeof right !== "number") return undefined;
1444
+ if (op === "add") return left + right;
1445
+ if (op === "sub") return left - right;
1446
+ if (op === "mul") return left * right;
1447
+ if (op === "div") return left / right;
1448
+ return left % right;
1449
+ }
1450
+
1451
+ if (op === "bitAnd" || op === "bitOr" || op === "bitXor") {
1452
+ if (typeof left !== "number" || typeof right !== "number") return undefined;
1453
+ if (op === "bitAnd") return left & right;
1454
+ if (op === "bitOr") return left | right;
1455
+ return left ^ right;
1456
+ }
1457
+
1458
+ if (op === "eq") return left === right ? left : undefined;
1459
+ if (op === "neq") return left !== right ? left : undefined;
1460
+
1461
+ if (op === "gt" || op === "gte" || op === "lt" || op === "lte") {
1462
+ if (typeof left !== "number" || typeof right !== "number") return undefined;
1463
+ if (op === "gt") return left > right ? left : undefined;
1464
+ if (op === "gte") return left >= right ? left : undefined;
1465
+ if (op === "lt") return left < right ? left : undefined;
1466
+ return left <= right ? left : undefined;
1467
+ }
1468
+
1469
+ if (op === "and") return isDefinedValue(left) ? right : undefined;
1470
+ if (op === "or") return isDefinedValue(left) ? left : right;
1471
+ return undefined;
1472
+ }
1473
+
1474
+ function optimizeElse(elseBranch: IRConditionalElse | undefined, env: OptimizeEnv, currentDepth: number): IRConditionalElse | undefined {
1475
+ if (!elseBranch) return undefined;
1476
+ if (elseBranch.type === "else") {
1477
+ return { type: "else", block: optimizeBlock(elseBranch.block, cloneOptimizeEnv(env), currentDepth) } satisfies IRConditionalElse;
1478
+ }
1479
+
1480
+ const optimizedCondition = optimizeNode(elseBranch.condition, env, currentDepth);
1481
+ const foldedCondition = constValue(optimizedCondition);
1482
+ if (foldedCondition !== undefined || optimizedCondition.type === "undefined") {
1483
+ const passes = elseBranch.head === "when" ? isDefinedValue(foldedCondition) : !isDefinedValue(foldedCondition);
1484
+ if (passes) {
1485
+ return {
1486
+ type: "else",
1487
+ block: optimizeBlock(elseBranch.thenBlock, cloneOptimizeEnv(env), currentDepth),
1488
+ } satisfies IRConditionalElse;
1489
+ }
1490
+ return optimizeElse(elseBranch.elseBranch, env, currentDepth);
1491
+ }
1492
+
1493
+ return {
1494
+ type: "elseChain",
1495
+ head: elseBranch.head,
1496
+ condition: optimizedCondition,
1497
+ thenBlock: optimizeBlock(elseBranch.thenBlock, cloneOptimizeEnv(env), currentDepth),
1498
+ elseBranch: optimizeElse(elseBranch.elseBranch, cloneOptimizeEnv(env), currentDepth),
1499
+ } satisfies IRConditionalElse;
1500
+ }
1501
+
1502
+ function optimizeBlock(block: IRNode[], env: OptimizeEnv, currentDepth: number): IRNode[] {
1503
+ const out: IRNode[] = [];
1504
+ for (const node of block) {
1505
+ const optimized = optimizeNode(node, env, currentDepth);
1506
+ out.push(optimized);
1507
+ if (optimized.type === "break" || optimized.type === "continue") break;
1508
+
1509
+ if (optimized.type === "assign" && optimized.op === "=" && optimized.place.type === "identifier") {
1510
+ const selfTarget = selfTargetFromNode(optimized.value, currentDepth);
1511
+ if (selfTarget !== undefined) {
1512
+ env.selfCaptures[optimized.place.name] = selfTarget;
1513
+ delete env.constants[optimized.place.name];
1514
+ continue;
1515
+ }
1516
+
1517
+ const folded = constValue(optimized.value);
1518
+ if (folded !== undefined || optimized.value.type === "undefined") {
1519
+ env.constants[optimized.place.name] = cloneNode(optimized.value);
1520
+ delete env.selfCaptures[optimized.place.name];
1521
+ }
1522
+ else {
1523
+ clearBinding(env, optimized.place.name);
1524
+ }
1525
+ continue;
1526
+ }
1527
+
1528
+ if (optimized.type === "unary" && optimized.op === "delete" && optimized.value.type === "identifier") {
1529
+ clearBinding(env, optimized.value.name);
1530
+ continue;
1531
+ }
1532
+
1533
+ if (optimized.type === "assign" && optimized.place.type === "identifier") {
1534
+ clearBinding(env, optimized.place.name);
1535
+ continue;
1536
+ }
1537
+
1538
+ if (optimized.type === "assign" || optimized.type === "for" || optimized.type === "call") {
1539
+ clearOptimizeEnv(env);
1540
+ }
1541
+ }
1542
+ return inlineAdjacentPureAssignments(eliminateDeadAssignments(out));
1543
+ }
1544
+
1545
+ function optimizeNode(node: IRNode, env: OptimizeEnv, currentDepth: number, asPlace = false): IRNode {
1546
+ switch (node.type) {
1547
+ case "program": {
1548
+ const body = optimizeBlock(node.body, cloneOptimizeEnv(env), currentDepth);
1549
+ if (body.length === 0) return { type: "undefined" } satisfies IRNode;
1550
+ if (body.length === 1) return body[0] as IRNode;
1551
+ return { type: "program", body } satisfies IRNode;
1552
+ }
1553
+ case "identifier": {
1554
+ if (asPlace) return node;
1555
+ const selfTarget = env.selfCaptures[node.name];
1556
+ if (selfTarget !== undefined) {
1557
+ const rewritten = selfNodeFromTarget(selfTarget, currentDepth);
1558
+ if (rewritten) return rewritten;
1559
+ }
1560
+ const replacement = env.constants[node.name];
1561
+ return replacement ? cloneNode(replacement) : node;
1562
+ }
1563
+ case "group": {
1564
+ return optimizeNode(node.expression, env, currentDepth);
1565
+ }
1566
+ case "array": {
1567
+ return { type: "array", items: node.items.map((item) => optimizeNode(item, env, currentDepth)) } satisfies IRNode;
1568
+ }
1569
+ case "object": {
1570
+ return {
1571
+ type: "object",
1572
+ entries: node.entries.map((entry) => ({
1573
+ key: optimizeNode(entry.key, env, currentDepth),
1574
+ value: optimizeNode(entry.value, env, currentDepth),
1575
+ })),
1576
+ } satisfies IRNode;
1577
+ }
1578
+ case "unary": {
1579
+ const value = optimizeNode(node.value, env, currentDepth, node.op === "delete");
1580
+ const foldedValue = constValue(value);
1581
+ if (foldedValue !== undefined || value.type === "undefined") {
1582
+ const folded = foldUnary(node.op, foldedValue);
1583
+ const literal = folded === undefined ? undefined : toLiteralNode(folded);
1584
+ if (literal) return literal;
1585
+ }
1586
+ return { type: "unary", op: node.op, value } satisfies IRNode;
1587
+ }
1588
+ case "binary": {
1589
+ const left = optimizeNode(node.left, env, currentDepth);
1590
+ const right = optimizeNode(node.right, env, currentDepth);
1591
+ const leftValue = constValue(left);
1592
+ const rightValue = constValue(right);
1593
+ if ((leftValue !== undefined || left.type === "undefined") && (rightValue !== undefined || right.type === "undefined")) {
1594
+ const folded = foldBinary(node.op, leftValue, rightValue);
1595
+ const literal = folded === undefined ? undefined : toLiteralNode(folded);
1596
+ if (literal) return literal;
1597
+ }
1598
+ return { type: "binary", op: node.op, left, right } satisfies IRNode;
1599
+ }
1600
+ case "navigation": {
1601
+ const target = optimizeNode(node.target, env, currentDepth);
1602
+ const segments = node.segments.map((segment) => (segment.type === "static"
1603
+ ? segment
1604
+ : { type: "dynamic", key: optimizeNode(segment.key, env, currentDepth) }));
1605
+
1606
+ const targetValue = constValue(target);
1607
+ if (targetValue !== undefined || target.type === "undefined") {
1608
+ let current: unknown = targetValue;
1609
+ let foldable = true;
1610
+ for (const segment of segments) {
1611
+ if (!foldable) break;
1612
+ const key = segment.type === "static" ? segment.key : constValue(segment.key);
1613
+ if (segment.type === "dynamic" && key === undefined && segment.key.type !== "undefined") {
1614
+ foldable = false;
1615
+ break;
1616
+ }
1617
+ if (current === null || current === undefined) {
1618
+ current = undefined;
1619
+ continue;
1620
+ }
1621
+ current = (current as Record<string, unknown>)[String(key)];
1622
+ }
1623
+ if (foldable) {
1624
+ const literal = toLiteralNode(current);
1625
+ if (literal) return literal;
1626
+ }
1627
+ }
1628
+
1629
+ return {
1630
+ type: "navigation",
1631
+ target,
1632
+ segments: segments as Extract<IRNode, { type: "navigation" }> ["segments"],
1633
+ } satisfies IRNode;
1634
+ }
1635
+ case "call": {
1636
+ return {
1637
+ type: "call",
1638
+ callee: optimizeNode(node.callee, env, currentDepth),
1639
+ args: node.args.map((arg) => optimizeNode(arg, env, currentDepth)),
1640
+ } satisfies IRNode;
1641
+ }
1642
+ case "assign": {
1643
+ return {
1644
+ type: "assign",
1645
+ op: node.op,
1646
+ place: optimizeNode(node.place, env, currentDepth, true),
1647
+ value: optimizeNode(node.value, env, currentDepth),
1648
+ } satisfies IRNode;
1649
+ }
1650
+ case "conditional": {
1651
+ const condition = optimizeNode(node.condition, env, currentDepth);
1652
+ const thenEnv = cloneOptimizeEnv(env);
1653
+ if (condition.type === "assign" && condition.op === "=" && condition.place.type === "identifier") {
1654
+ thenEnv.selfCaptures[condition.place.name] = currentDepth;
1655
+ delete thenEnv.constants[condition.place.name];
1656
+ }
1657
+ const conditionValue = constValue(condition);
1658
+ if (conditionValue !== undefined || condition.type === "undefined") {
1659
+ const passes = node.head === "when" ? isDefinedValue(conditionValue) : !isDefinedValue(conditionValue);
1660
+ if (passes) {
1661
+ const thenBlock = optimizeBlock(node.thenBlock, thenEnv, currentDepth);
1662
+ if (thenBlock.length === 0) return { type: "undefined" } satisfies IRNode;
1663
+ if (thenBlock.length === 1) return thenBlock[0] as IRNode;
1664
+ return { type: "program", body: thenBlock } satisfies IRNode;
1665
+ }
1666
+ if (!node.elseBranch) return { type: "undefined" } satisfies IRNode;
1667
+ const loweredElse = optimizeElse(node.elseBranch, cloneOptimizeEnv(env), currentDepth);
1668
+ if (!loweredElse) return { type: "undefined" } satisfies IRNode;
1669
+ if (loweredElse.type === "else") {
1670
+ if (loweredElse.block.length === 0) return { type: "undefined" } satisfies IRNode;
1671
+ if (loweredElse.block.length === 1) return loweredElse.block[0] as IRNode;
1672
+ return { type: "program", body: loweredElse.block } satisfies IRNode;
1673
+ }
1674
+ return {
1675
+ type: "conditional",
1676
+ head: loweredElse.head,
1677
+ condition: loweredElse.condition,
1678
+ thenBlock: loweredElse.thenBlock,
1679
+ elseBranch: loweredElse.elseBranch,
1680
+ } satisfies IRNode;
1681
+ }
1682
+
1683
+ const thenBlock = optimizeBlock(node.thenBlock, thenEnv, currentDepth);
1684
+ const elseBranch = optimizeElse(node.elseBranch, cloneOptimizeEnv(env), currentDepth);
1685
+
1686
+ // Strip dead assignment from condition: when x=expr do ... end
1687
+ // If x is no longer read in the optimized body/else, unwrap to just the value.
1688
+ let finalCondition = condition;
1689
+ if (condition.type === "assign" && condition.op === "=" && condition.place.type === "identifier") {
1690
+ const name = condition.place.name;
1691
+ const reads = new Set<string>();
1692
+ for (const part of thenBlock) collectReads(part, reads);
1693
+ if (elseBranch) collectReadsElse(elseBranch, reads);
1694
+ if (!reads.has(name)) {
1695
+ finalCondition = condition.value;
1696
+ }
1697
+ }
1698
+
1699
+ return {
1700
+ type: "conditional",
1701
+ head: node.head,
1702
+ condition: finalCondition,
1703
+ thenBlock,
1704
+ elseBranch,
1705
+ } satisfies IRNode;
1706
+ }
1707
+ case "for": {
1708
+ const sourceEnv = cloneOptimizeEnv(env);
1709
+ const binding = (() => {
1710
+ if (node.binding.type === "binding:expr") {
1711
+ return { type: "binding:expr", source: optimizeNode(node.binding.source, sourceEnv, currentDepth) } satisfies IRBindingOrExpr;
1712
+ }
1713
+ if (node.binding.type === "binding:valueIn") {
1714
+ return {
1715
+ type: "binding:valueIn",
1716
+ value: node.binding.value,
1717
+ source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1718
+ } satisfies IRBinding;
1719
+ }
1720
+ if (node.binding.type === "binding:keyValueIn") {
1721
+ return {
1722
+ type: "binding:keyValueIn",
1723
+ key: node.binding.key,
1724
+ value: node.binding.value,
1725
+ source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1726
+ } satisfies IRBinding;
1727
+ }
1728
+ return {
1729
+ type: "binding:keyOf",
1730
+ key: node.binding.key,
1731
+ source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1732
+ } satisfies IRBinding;
1733
+ })();
1734
+ const bodyEnv = cloneOptimizeEnv(env);
1735
+ dropBindingNames(bodyEnv, binding);
1736
+ return {
1737
+ type: "for",
1738
+ binding,
1739
+ body: optimizeBlock(node.body, bodyEnv, currentDepth + 1),
1740
+ } satisfies IRNode;
1741
+ }
1742
+ case "arrayComprehension": {
1743
+ const sourceEnv = cloneOptimizeEnv(env);
1744
+ const binding = node.binding.type === "binding:expr"
1745
+ ? ({ type: "binding:expr", source: optimizeNode(node.binding.source, sourceEnv, currentDepth) } satisfies IRBindingOrExpr)
1746
+ : node.binding.type === "binding:valueIn"
1747
+ ? ({
1748
+ type: "binding:valueIn",
1749
+ value: node.binding.value,
1750
+ source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1751
+ } satisfies IRBinding)
1752
+ : node.binding.type === "binding:keyValueIn"
1753
+ ? ({
1754
+ type: "binding:keyValueIn",
1755
+ key: node.binding.key,
1756
+ value: node.binding.value,
1757
+ source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1758
+ } satisfies IRBinding)
1759
+ : ({
1760
+ type: "binding:keyOf",
1761
+ key: node.binding.key,
1762
+ source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1763
+ } satisfies IRBinding);
1764
+ const bodyEnv = cloneOptimizeEnv(env);
1765
+ dropBindingNames(bodyEnv, binding);
1766
+ return {
1767
+ type: "arrayComprehension",
1768
+ binding,
1769
+ body: optimizeNode(node.body, bodyEnv, currentDepth + 1),
1770
+ } satisfies IRNode;
1771
+ }
1772
+ case "objectComprehension": {
1773
+ const sourceEnv = cloneOptimizeEnv(env);
1774
+ const binding = node.binding.type === "binding:expr"
1775
+ ? ({ type: "binding:expr", source: optimizeNode(node.binding.source, sourceEnv, currentDepth) } satisfies IRBindingOrExpr)
1776
+ : node.binding.type === "binding:valueIn"
1777
+ ? ({
1778
+ type: "binding:valueIn",
1779
+ value: node.binding.value,
1780
+ source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1781
+ } satisfies IRBinding)
1782
+ : node.binding.type === "binding:keyValueIn"
1783
+ ? ({
1784
+ type: "binding:keyValueIn",
1785
+ key: node.binding.key,
1786
+ value: node.binding.value,
1787
+ source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1788
+ } satisfies IRBinding)
1789
+ : ({
1790
+ type: "binding:keyOf",
1791
+ key: node.binding.key,
1792
+ source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1793
+ } satisfies IRBinding);
1794
+ const bodyEnv = cloneOptimizeEnv(env);
1795
+ dropBindingNames(bodyEnv, binding);
1796
+ return {
1797
+ type: "objectComprehension",
1798
+ binding,
1799
+ key: optimizeNode(node.key, bodyEnv, currentDepth + 1),
1800
+ value: optimizeNode(node.value, bodyEnv, currentDepth + 1),
1801
+ } satisfies IRNode;
1802
+ }
1803
+ default:
1804
+ return node;
1805
+ }
1806
+ }
1807
+
1808
+ export function optimizeIR(node: IRNode): IRNode {
1809
+ return optimizeNode(node, emptyOptimizeEnv(), 1);
1810
+ }
1811
+
1812
+ function collectLocalBindings(node: IRNode, locals: Set<string>) {
1813
+ switch (node.type) {
1814
+ case "assign":
1815
+ if (node.place.type === "identifier") locals.add(node.place.name);
1816
+ collectLocalBindings(node.place, locals);
1817
+ collectLocalBindings(node.value, locals);
1818
+ return;
1819
+ case "program":
1820
+ for (const part of node.body) collectLocalBindings(part, locals);
1821
+ return;
1822
+ case "group":
1823
+ collectLocalBindings(node.expression, locals);
1824
+ return;
1825
+ case "array":
1826
+ for (const item of node.items) collectLocalBindings(item, locals);
1827
+ return;
1828
+ case "object":
1829
+ for (const entry of node.entries) {
1830
+ collectLocalBindings(entry.key, locals);
1831
+ collectLocalBindings(entry.value, locals);
1832
+ }
1833
+ return;
1834
+ case "navigation":
1835
+ collectLocalBindings(node.target, locals);
1836
+ for (const segment of node.segments) {
1837
+ if (segment.type === "dynamic") collectLocalBindings(segment.key, locals);
1838
+ }
1839
+ return;
1840
+ case "call":
1841
+ collectLocalBindings(node.callee, locals);
1842
+ for (const arg of node.args) collectLocalBindings(arg, locals);
1843
+ return;
1844
+ case "unary":
1845
+ collectLocalBindings(node.value, locals);
1846
+ return;
1847
+ case "binary":
1848
+ collectLocalBindings(node.left, locals);
1849
+ collectLocalBindings(node.right, locals);
1850
+ return;
1851
+ case "conditional":
1852
+ collectLocalBindings(node.condition, locals);
1853
+ for (const part of node.thenBlock) collectLocalBindings(part, locals);
1854
+ if (node.elseBranch) collectLocalBindingsElse(node.elseBranch, locals);
1855
+ return;
1856
+ case "for":
1857
+ collectLocalBindingFromBinding(node.binding, locals);
1858
+ for (const part of node.body) collectLocalBindings(part, locals);
1859
+ return;
1860
+ case "arrayComprehension":
1861
+ collectLocalBindingFromBinding(node.binding, locals);
1862
+ collectLocalBindings(node.body, locals);
1863
+ return;
1864
+ case "objectComprehension":
1865
+ collectLocalBindingFromBinding(node.binding, locals);
1866
+ collectLocalBindings(node.key, locals);
1867
+ collectLocalBindings(node.value, locals);
1868
+ return;
1869
+ default:
1870
+ return;
1871
+ }
1872
+ }
1873
+
1874
+ function collectLocalBindingFromBinding(binding: IRBindingOrExpr, locals: Set<string>) {
1875
+ if (binding.type === "binding:valueIn") {
1876
+ locals.add(binding.value);
1877
+ collectLocalBindings(binding.source, locals);
1878
+ return;
1879
+ }
1880
+ if (binding.type === "binding:keyValueIn") {
1881
+ locals.add(binding.key);
1882
+ locals.add(binding.value);
1883
+ collectLocalBindings(binding.source, locals);
1884
+ return;
1885
+ }
1886
+ if (binding.type === "binding:keyOf") {
1887
+ locals.add(binding.key);
1888
+ collectLocalBindings(binding.source, locals);
1889
+ return;
1890
+ }
1891
+ collectLocalBindings(binding.source, locals);
1892
+ }
1893
+
1894
+ function collectLocalBindingsElse(elseBranch: IRConditionalElse, locals: Set<string>) {
1895
+ if (elseBranch.type === "else") {
1896
+ for (const part of elseBranch.block) collectLocalBindings(part, locals);
1897
+ return;
1898
+ }
1899
+ collectLocalBindings(elseBranch.condition, locals);
1900
+ for (const part of elseBranch.thenBlock) collectLocalBindings(part, locals);
1901
+ if (elseBranch.elseBranch) collectLocalBindingsElse(elseBranch.elseBranch, locals);
1902
+ }
1903
+
1904
+ function bumpNameFrequency(name: string, locals: Set<string>, frequencies: Map<string, number>, order: Map<string, number>, nextOrder: { value: number }) {
1905
+ if (!locals.has(name)) return;
1906
+ if (!order.has(name)) {
1907
+ order.set(name, nextOrder.value);
1908
+ nextOrder.value += 1;
1909
+ }
1910
+ frequencies.set(name, (frequencies.get(name) ?? 0) + 1);
1911
+ }
1912
+
1913
+ function collectNameFrequencies(node: IRNode, locals: Set<string>, frequencies: Map<string, number>, order: Map<string, number>, nextOrder: { value: number }) {
1914
+ switch (node.type) {
1915
+ case "identifier":
1916
+ bumpNameFrequency(node.name, locals, frequencies, order, nextOrder);
1917
+ return;
1918
+ case "assign":
1919
+ if (node.place.type === "identifier") bumpNameFrequency(node.place.name, locals, frequencies, order, nextOrder);
1920
+ collectNameFrequencies(node.place, locals, frequencies, order, nextOrder);
1921
+ collectNameFrequencies(node.value, locals, frequencies, order, nextOrder);
1922
+ return;
1923
+ case "program":
1924
+ for (const part of node.body) collectNameFrequencies(part, locals, frequencies, order, nextOrder);
1925
+ return;
1926
+ case "group":
1927
+ collectNameFrequencies(node.expression, locals, frequencies, order, nextOrder);
1928
+ return;
1929
+ case "array":
1930
+ for (const item of node.items) collectNameFrequencies(item, locals, frequencies, order, nextOrder);
1931
+ return;
1932
+ case "object":
1933
+ for (const entry of node.entries) {
1934
+ collectNameFrequencies(entry.key, locals, frequencies, order, nextOrder);
1935
+ collectNameFrequencies(entry.value, locals, frequencies, order, nextOrder);
1936
+ }
1937
+ return;
1938
+ case "navigation":
1939
+ collectNameFrequencies(node.target, locals, frequencies, order, nextOrder);
1940
+ for (const segment of node.segments) {
1941
+ if (segment.type === "dynamic") collectNameFrequencies(segment.key, locals, frequencies, order, nextOrder);
1942
+ }
1943
+ return;
1944
+ case "call":
1945
+ collectNameFrequencies(node.callee, locals, frequencies, order, nextOrder);
1946
+ for (const arg of node.args) collectNameFrequencies(arg, locals, frequencies, order, nextOrder);
1947
+ return;
1948
+ case "unary":
1949
+ collectNameFrequencies(node.value, locals, frequencies, order, nextOrder);
1950
+ return;
1951
+ case "binary":
1952
+ collectNameFrequencies(node.left, locals, frequencies, order, nextOrder);
1953
+ collectNameFrequencies(node.right, locals, frequencies, order, nextOrder);
1954
+ return;
1955
+ case "conditional":
1956
+ collectNameFrequencies(node.condition, locals, frequencies, order, nextOrder);
1957
+ for (const part of node.thenBlock) collectNameFrequencies(part, locals, frequencies, order, nextOrder);
1958
+ if (node.elseBranch) collectNameFrequenciesElse(node.elseBranch, locals, frequencies, order, nextOrder);
1959
+ return;
1960
+ case "for":
1961
+ collectNameFrequenciesBinding(node.binding, locals, frequencies, order, nextOrder);
1962
+ for (const part of node.body) collectNameFrequencies(part, locals, frequencies, order, nextOrder);
1963
+ return;
1964
+ case "arrayComprehension":
1965
+ collectNameFrequenciesBinding(node.binding, locals, frequencies, order, nextOrder);
1966
+ collectNameFrequencies(node.body, locals, frequencies, order, nextOrder);
1967
+ return;
1968
+ case "objectComprehension":
1969
+ collectNameFrequenciesBinding(node.binding, locals, frequencies, order, nextOrder);
1970
+ collectNameFrequencies(node.key, locals, frequencies, order, nextOrder);
1971
+ collectNameFrequencies(node.value, locals, frequencies, order, nextOrder);
1972
+ return;
1973
+ default:
1974
+ return;
1975
+ }
1976
+ }
1977
+
1978
+ function collectNameFrequenciesBinding(binding: IRBindingOrExpr, locals: Set<string>, frequencies: Map<string, number>, order: Map<string, number>, nextOrder: { value: number }) {
1979
+ if (binding.type === "binding:valueIn") {
1980
+ bumpNameFrequency(binding.value, locals, frequencies, order, nextOrder);
1981
+ collectNameFrequencies(binding.source, locals, frequencies, order, nextOrder);
1982
+ return;
1983
+ }
1984
+ if (binding.type === "binding:keyValueIn") {
1985
+ bumpNameFrequency(binding.key, locals, frequencies, order, nextOrder);
1986
+ bumpNameFrequency(binding.value, locals, frequencies, order, nextOrder);
1987
+ collectNameFrequencies(binding.source, locals, frequencies, order, nextOrder);
1988
+ return;
1989
+ }
1990
+ if (binding.type === "binding:keyOf") {
1991
+ bumpNameFrequency(binding.key, locals, frequencies, order, nextOrder);
1992
+ collectNameFrequencies(binding.source, locals, frequencies, order, nextOrder);
1993
+ return;
1994
+ }
1995
+ collectNameFrequencies(binding.source, locals, frequencies, order, nextOrder);
1996
+ }
1997
+
1998
+ function collectNameFrequenciesElse(elseBranch: IRConditionalElse, locals: Set<string>, frequencies: Map<string, number>, order: Map<string, number>, nextOrder: { value: number }) {
1999
+ if (elseBranch.type === "else") {
2000
+ for (const part of elseBranch.block) collectNameFrequencies(part, locals, frequencies, order, nextOrder);
2001
+ return;
2002
+ }
2003
+ collectNameFrequencies(elseBranch.condition, locals, frequencies, order, nextOrder);
2004
+ for (const part of elseBranch.thenBlock) collectNameFrequencies(part, locals, frequencies, order, nextOrder);
2005
+ if (elseBranch.elseBranch) collectNameFrequenciesElse(elseBranch.elseBranch, locals, frequencies, order, nextOrder);
2006
+ }
2007
+
2008
+ function renameLocalNames(node: IRNode, map: Map<string, string>): IRNode {
2009
+ switch (node.type) {
2010
+ case "identifier":
2011
+ return map.has(node.name) ? ({ type: "identifier", name: map.get(node.name) as string } satisfies IRNode) : node;
2012
+ case "program":
2013
+ return { type: "program", body: node.body.map((part) => renameLocalNames(part, map)) } satisfies IRNode;
2014
+ case "group":
2015
+ return { type: "group", expression: renameLocalNames(node.expression, map) } satisfies IRNode;
2016
+ case "array":
2017
+ return { type: "array", items: node.items.map((item) => renameLocalNames(item, map)) } satisfies IRNode;
2018
+ case "object":
2019
+ return {
2020
+ type: "object",
2021
+ entries: node.entries.map((entry) => ({
2022
+ key: renameLocalNames(entry.key, map),
2023
+ value: renameLocalNames(entry.value, map),
2024
+ })),
2025
+ } satisfies IRNode;
2026
+ case "navigation":
2027
+ return {
2028
+ type: "navigation",
2029
+ target: renameLocalNames(node.target, map),
2030
+ segments: node.segments.map((segment) => segment.type === "static"
2031
+ ? segment
2032
+ : { type: "dynamic", key: renameLocalNames(segment.key, map) }),
2033
+ } satisfies IRNode;
2034
+ case "call":
2035
+ return {
2036
+ type: "call",
2037
+ callee: renameLocalNames(node.callee, map),
2038
+ args: node.args.map((arg) => renameLocalNames(arg, map)),
2039
+ } satisfies IRNode;
2040
+ case "unary":
2041
+ return { type: "unary", op: node.op, value: renameLocalNames(node.value, map) } satisfies IRNode;
2042
+ case "binary":
2043
+ return {
2044
+ type: "binary",
2045
+ op: node.op,
2046
+ left: renameLocalNames(node.left, map),
2047
+ right: renameLocalNames(node.right, map),
2048
+ } satisfies IRNode;
2049
+ case "assign": {
2050
+ const place = node.place.type === "identifier" && map.has(node.place.name)
2051
+ ? ({ type: "identifier", name: map.get(node.place.name) as string } satisfies IRNode)
2052
+ : renameLocalNames(node.place, map);
2053
+ return {
2054
+ type: "assign",
2055
+ op: node.op,
2056
+ place,
2057
+ value: renameLocalNames(node.value, map),
2058
+ } satisfies IRNode;
2059
+ }
2060
+ case "conditional":
2061
+ return {
2062
+ type: "conditional",
2063
+ head: node.head,
2064
+ condition: renameLocalNames(node.condition, map),
2065
+ thenBlock: node.thenBlock.map((part) => renameLocalNames(part, map)),
2066
+ elseBranch: node.elseBranch ? renameLocalNamesElse(node.elseBranch, map) : undefined,
2067
+ } satisfies IRNode;
2068
+ case "for":
2069
+ return {
2070
+ type: "for",
2071
+ binding: renameLocalNamesBinding(node.binding, map),
2072
+ body: node.body.map((part) => renameLocalNames(part, map)),
2073
+ } satisfies IRNode;
2074
+ case "arrayComprehension":
2075
+ return {
2076
+ type: "arrayComprehension",
2077
+ binding: renameLocalNamesBinding(node.binding, map),
2078
+ body: renameLocalNames(node.body, map),
2079
+ } satisfies IRNode;
2080
+ case "objectComprehension":
2081
+ return {
2082
+ type: "objectComprehension",
2083
+ binding: renameLocalNamesBinding(node.binding, map),
2084
+ key: renameLocalNames(node.key, map),
2085
+ value: renameLocalNames(node.value, map),
2086
+ } satisfies IRNode;
2087
+ default:
2088
+ return node;
2089
+ }
2090
+ }
2091
+
2092
+ function renameLocalNamesBinding(binding: IRBindingOrExpr, map: Map<string, string>): IRBindingOrExpr {
2093
+ if (binding.type === "binding:expr") {
2094
+ return { type: "binding:expr", source: renameLocalNames(binding.source, map) } satisfies IRBindingOrExpr;
2095
+ }
2096
+ if (binding.type === "binding:valueIn") {
2097
+ return {
2098
+ type: "binding:valueIn",
2099
+ value: map.get(binding.value) ?? binding.value,
2100
+ source: renameLocalNames(binding.source, map),
2101
+ } satisfies IRBinding;
2102
+ }
2103
+ if (binding.type === "binding:keyValueIn") {
2104
+ return {
2105
+ type: "binding:keyValueIn",
2106
+ key: map.get(binding.key) ?? binding.key,
2107
+ value: map.get(binding.value) ?? binding.value,
2108
+ source: renameLocalNames(binding.source, map),
2109
+ } satisfies IRBinding;
2110
+ }
2111
+ return {
2112
+ type: "binding:keyOf",
2113
+ key: map.get(binding.key) ?? binding.key,
2114
+ source: renameLocalNames(binding.source, map),
2115
+ } satisfies IRBinding;
2116
+ }
2117
+
2118
+ function renameLocalNamesElse(elseBranch: IRConditionalElse, map: Map<string, string>): IRConditionalElse {
2119
+ if (elseBranch.type === "else") {
2120
+ return {
2121
+ type: "else",
2122
+ block: elseBranch.block.map((part) => renameLocalNames(part, map)),
2123
+ } satisfies IRConditionalElse;
2124
+ }
2125
+ return {
2126
+ type: "elseChain",
2127
+ head: elseBranch.head,
2128
+ condition: renameLocalNames(elseBranch.condition, map),
2129
+ thenBlock: elseBranch.thenBlock.map((part) => renameLocalNames(part, map)),
2130
+ elseBranch: elseBranch.elseBranch ? renameLocalNamesElse(elseBranch.elseBranch, map) : undefined,
2131
+ } satisfies IRConditionalElse;
2132
+ }
2133
+
2134
+ export function minifyLocalNamesIR(node: IRNode): IRNode {
2135
+ const locals = new Set<string>();
2136
+ collectLocalBindings(node, locals);
2137
+ if (locals.size === 0) return node;
2138
+
2139
+ const frequencies = new Map<string, number>();
2140
+ const order = new Map<string, number>();
2141
+ collectNameFrequencies(node, locals, frequencies, order, { value: 0 });
2142
+
2143
+ const ranked = Array.from(locals)
2144
+ .sort((a, b) => {
2145
+ const freqA = frequencies.get(a) ?? 0;
2146
+ const freqB = frequencies.get(b) ?? 0;
2147
+ if (freqA !== freqB) return freqB - freqA;
2148
+ const orderA = order.get(a) ?? Number.MAX_SAFE_INTEGER;
2149
+ const orderB = order.get(b) ?? Number.MAX_SAFE_INTEGER;
2150
+ if (orderA !== orderB) return orderA - orderB;
2151
+ return a.localeCompare(b);
2152
+ });
2153
+
2154
+ const renameMap = new Map<string, string>();
2155
+ ranked.forEach((name, index) => {
2156
+ renameMap.set(name, encodeUint(index));
2157
+ });
2158
+
2159
+ return renameLocalNames(node, renameMap);
2160
+ }
2161
+
2162
+ export function compile(source: string, options?: CompileOptions): string {
2163
+ const ir = parseToIR(source);
2164
+ let lowered = options?.optimize ? optimizeIR(ir) : ir;
2165
+ if (options?.minifyNames) lowered = minifyLocalNamesIR(lowered);
2166
+ const domainRefs = options?.domainConfig ? domainRefsFromConfig(options.domainConfig) : undefined;
2167
+ return encodeIR(lowered, {
2168
+ domainRefs,
2169
+ dedupeValues: options?.dedupeValues,
2170
+ dedupeMinBytes: options?.dedupeMinBytes,
2171
+ });
2172
+ }
2173
+
2174
+ type IRPostfixStep =
2175
+ | { kind: "navStatic"; key: string }
2176
+ | { kind: "navDynamic"; key: IRNode }
2177
+ | { kind: "call"; args: IRNode[] };
2178
+
2179
+ function parseNumber(raw: string) {
2180
+ if (raw === "nan") return NaN;
2181
+ if (raw === "inf") return Infinity;
2182
+ if (raw === "-inf") return -Infinity;
2183
+ if (/^-?0x/i.test(raw)) return parseInt(raw, 16);
2184
+ if (/^-?0b/i.test(raw)) {
2185
+ const isNegative = raw.startsWith("-");
2186
+ const digits = raw.replace(/^-?0b/i, "");
2187
+ const value = parseInt(digits, 2);
2188
+ return isNegative ? -value : value;
2189
+ }
2190
+ return Number(raw);
2191
+ }
2192
+
2193
+ function collectStructured(value: unknown, out: Array<IRNode | { key: IRNode; value: IRNode }>) {
2194
+ if (Array.isArray(value)) {
2195
+ for (const part of value) collectStructured(part, out);
2196
+ return;
2197
+ }
2198
+ if (!value || typeof value !== "object") return;
2199
+ if ("type" in value || ("key" in value && "value" in value)) {
2200
+ out.push(value as IRNode | { key: IRNode; value: IRNode });
2201
+ }
2202
+ }
2203
+
2204
+ function normalizeList(value: unknown): Array<IRNode | { key: IRNode; value: IRNode }> {
2205
+ const out: Array<IRNode | { key: IRNode; value: IRNode }> = [];
2206
+ collectStructured(value, out);
2207
+ return out;
2208
+ }
2209
+
2210
+ function collectPostfixSteps(value: unknown, out: IRPostfixStep[]) {
2211
+ if (Array.isArray(value)) {
2212
+ for (const part of value) collectPostfixSteps(part, out);
2213
+ return;
2214
+ }
2215
+ if (!value || typeof value !== "object") return;
2216
+ if ("kind" in value) out.push(value as IRPostfixStep);
2217
+ }
2218
+
2219
+ function normalizePostfixSteps(value: unknown): IRPostfixStep[] {
2220
+ const out: IRPostfixStep[] = [];
2221
+ collectPostfixSteps(value, out);
2222
+ return out;
2223
+ }
2224
+
2225
+ function buildPostfix(base: IRNode, steps: IRPostfixStep[]) {
2226
+ let current = base;
2227
+ let pendingSegments: Extract<IRNode, { type: "navigation" }>["segments"] = [];
2228
+
2229
+ const flushSegments = () => {
2230
+ if (pendingSegments.length === 0) return;
2231
+ current = {
2232
+ type: "navigation",
2233
+ target: current,
2234
+ segments: pendingSegments,
2235
+ } satisfies IRNode;
2236
+ pendingSegments = [];
2237
+ };
2238
+
2239
+ for (const step of steps) {
2240
+ if (step.kind === "navStatic") {
2241
+ pendingSegments.push({ type: "static", key: step.key });
2242
+ continue;
2243
+ }
2244
+ if (step.kind === "navDynamic") {
2245
+ pendingSegments.push({ type: "dynamic", key: step.key });
2246
+ continue;
2247
+ }
2248
+ flushSegments();
2249
+ current = { type: "call", callee: current, args: step.args } satisfies IRNode;
2250
+ }
2251
+
2252
+ flushSegments();
2253
+ return current;
2254
+ }
2255
+
2256
+ semantics.addOperation("toIR", {
2257
+ _iter(...children) {
2258
+ return children.map((child) => child.toIR());
2259
+ },
2260
+ _terminal() {
2261
+ return this.sourceString;
2262
+ },
2263
+ _nonterminal(...children) {
2264
+ if (children.length === 1 && children[0]) return children[0].toIR();
2265
+ return children.map((child) => child.toIR());
2266
+ },
2267
+
2268
+ Program(expressions) {
2269
+ const body = normalizeList(expressions.toIR()) as IRNode[];
2270
+ if (body.length === 1) return body[0];
2271
+ return { type: "program", body } satisfies IRNode;
2272
+ },
2273
+
2274
+ Block(expressions) {
2275
+ return normalizeList(expressions.toIR()) as IRNode[];
2276
+ },
2277
+
2278
+ Elements(first, separatorsAndItems, maybeTrailingComma, maybeEmpty) {
2279
+ return normalizeList([
2280
+ first.toIR(),
2281
+ separatorsAndItems.toIR(),
2282
+ maybeTrailingComma.toIR(),
2283
+ maybeEmpty.toIR(),
2284
+ ]);
2285
+ },
2286
+
2287
+ AssignExpr_assign(place, op, value) {
2288
+ return {
2289
+ type: "assign",
2290
+ op: op.sourceString as Extract<IRNode, { type: "assign" }>["op"],
2291
+ place: place.toIR(),
2292
+ value: value.toIR(),
2293
+ } satisfies IRNode;
2294
+ },
2295
+
2296
+ ExistenceExpr_and(left, _and, right) {
2297
+ return { type: "binary", op: "and", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2298
+ },
2299
+ ExistenceExpr_or(left, _or, right) {
2300
+ return { type: "binary", op: "or", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2301
+ },
2302
+
2303
+ BitExpr_and(left, _op, right) {
2304
+ return { type: "binary", op: "bitAnd", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2305
+ },
2306
+ BitExpr_xor(left, _op, right) {
2307
+ return { type: "binary", op: "bitXor", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2308
+ },
2309
+ BitExpr_or(left, _op, right) {
2310
+ return { type: "binary", op: "bitOr", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2311
+ },
2312
+
2313
+ CompareExpr_binary(left, op, right) {
2314
+ const map: Record<string, Extract<IRNode, { type: "binary" }>["op"]> = {
2315
+ "==": "eq",
2316
+ "!=": "neq",
2317
+ ">": "gt",
2318
+ ">=": "gte",
2319
+ "<": "lt",
2320
+ "<=": "lte",
2321
+ };
2322
+ const mapped = map[op.sourceString];
2323
+ if (!mapped) throw new Error(`Unsupported compare op: ${op.sourceString}`);
2324
+ return { type: "binary", op: mapped, left: left.toIR(), right: right.toIR() } satisfies IRNode;
2325
+ },
2326
+
2327
+ AddExpr_add(left, _op, right) {
2328
+ return { type: "binary", op: "add", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2329
+ },
2330
+ AddExpr_sub(left, _op, right) {
2331
+ return { type: "binary", op: "sub", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2332
+ },
2333
+
2334
+ MulExpr_mul(left, _op, right) {
2335
+ return { type: "binary", op: "mul", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2336
+ },
2337
+ MulExpr_div(left, _op, right) {
2338
+ return { type: "binary", op: "div", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2339
+ },
2340
+ MulExpr_mod(left, _op, right) {
2341
+ return { type: "binary", op: "mod", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2342
+ },
2343
+
2344
+ UnaryExpr_neg(_op, value) {
2345
+ const lowered = value.toIR() as IRNode;
2346
+ if (lowered.type === "number") {
2347
+ const raw = lowered.raw.startsWith("-") ? lowered.raw.slice(1) : `-${lowered.raw}`;
2348
+ return { type: "number", raw, value: -lowered.value } satisfies IRNode;
2349
+ }
2350
+ return { type: "unary", op: "neg", value: lowered } satisfies IRNode;
2351
+ },
2352
+ UnaryExpr_not(_op, value) {
2353
+ return { type: "unary", op: "not", value: value.toIR() } satisfies IRNode;
2354
+ },
2355
+ UnaryExpr_delete(_del, place) {
2356
+ return { type: "unary", op: "delete", value: place.toIR() } satisfies IRNode;
2357
+ },
2358
+
2359
+ PostfixExpr_chain(base, tails) {
2360
+ return buildPostfix(base.toIR(), normalizePostfixSteps(tails.toIR()));
2361
+ },
2362
+ Place(base, tails) {
2363
+ return buildPostfix(base.toIR(), normalizePostfixSteps(tails.toIR()));
2364
+ },
2365
+ PlaceTail_navStatic(_dot, key) {
2366
+ return { kind: "navStatic", key: key.sourceString } satisfies IRPostfixStep;
2367
+ },
2368
+ PlaceTail_navDynamic(_dotOpen, key, _close) {
2369
+ return { kind: "navDynamic", key: key.toIR() } satisfies IRPostfixStep;
2370
+ },
2371
+ PostfixTail_navStatic(_dot, key) {
2372
+ return { kind: "navStatic", key: key.sourceString } satisfies IRPostfixStep;
2373
+ },
2374
+ PostfixTail_navDynamic(_dotOpen, key, _close) {
2375
+ return { kind: "navDynamic", key: key.toIR() } satisfies IRPostfixStep;
2376
+ },
2377
+ PostfixTail_callEmpty(_open, _close) {
2378
+ return { kind: "call", args: [] } satisfies IRPostfixStep;
2379
+ },
2380
+ PostfixTail_call(_open, args, _close) {
2381
+ return { kind: "call", args: normalizeList(args.toIR()) as IRNode[] } satisfies IRPostfixStep;
2382
+ },
2383
+
2384
+ ConditionalExpr(head, condition, _do, thenBlock, elseBranch, _end) {
2385
+ const nextElse = elseBranch.children[0];
2386
+ return {
2387
+ type: "conditional",
2388
+ head: head.toIR() as "when" | "unless",
2389
+ condition: condition.toIR(),
2390
+ thenBlock: thenBlock.toIR() as IRNode[],
2391
+ elseBranch: nextElse ? (nextElse.toIR() as IRConditionalElse) : undefined,
2392
+ } satisfies IRNode;
2393
+ },
2394
+ ConditionalHead(_kw) {
2395
+ return this.sourceString as "when" | "unless";
2396
+ },
2397
+ ConditionalElse_elseChain(_else, head, condition, _do, thenBlock, elseBranch) {
2398
+ const nextElse = elseBranch.children[0];
2399
+ return {
2400
+ type: "elseChain",
2401
+ head: head.toIR() as "when" | "unless",
2402
+ condition: condition.toIR(),
2403
+ thenBlock: thenBlock.toIR() as IRNode[],
2404
+ elseBranch: nextElse ? (nextElse.toIR() as IRConditionalElse) : undefined,
2405
+ } satisfies IRConditionalElse;
2406
+ },
2407
+ ConditionalElse_else(_else, block) {
2408
+ return { type: "else", block: block.toIR() as IRNode[] } satisfies IRConditionalElse;
2409
+ },
2410
+
2411
+ DoExpr(_do, block, _end) {
2412
+ const body = block.toIR() as IRNode[];
2413
+ if (body.length === 0) return { type: "undefined" } satisfies IRNode;
2414
+ if (body.length === 1) return body[0] as IRNode;
2415
+ return { type: "program", body } satisfies IRNode;
2416
+ },
2417
+
2418
+ WhileExpr(_while, condition, _do, block, _end) {
2419
+ return {
2420
+ type: "while",
2421
+ condition: condition.toIR(),
2422
+ body: block.toIR() as IRNode[],
2423
+ } satisfies IRNode;
2424
+ },
2425
+
2426
+ ForExpr(_for, binding, _do, block, _end) {
2427
+ return {
2428
+ type: "for",
2429
+ binding: binding.toIR() as IRBindingOrExpr,
2430
+ body: block.toIR() as IRNode[],
2431
+ } satisfies IRNode;
2432
+ },
2433
+ BindingExpr(iterOrExpr) {
2434
+ const node = iterOrExpr.toIR();
2435
+ if (typeof node === "object" && node && "type" in node && String(node.type).startsWith("binding:")) {
2436
+ return node as IRBinding;
2437
+ }
2438
+ return { type: "binding:expr", source: node as IRNode } satisfies IRBindingOrExpr;
2439
+ },
2440
+
2441
+ Array_empty(_open, _close) {
2442
+ return { type: "array", items: [] } satisfies IRNode;
2443
+ },
2444
+ Array_comprehension(_open, binding, _semi, body, _close) {
2445
+ return {
2446
+ type: "arrayComprehension",
2447
+ binding: binding.toIR() as IRBindingOrExpr,
2448
+ body: body.toIR(),
2449
+ } satisfies IRNode;
2450
+ },
2451
+ Array_values(_open, items, _close) {
2452
+ return { type: "array", items: normalizeList(items.toIR()) as IRNode[] } satisfies IRNode;
2453
+ },
2454
+
2455
+ Object_empty(_open, _close) {
2456
+ return { type: "object", entries: [] } satisfies IRNode;
2457
+ },
2458
+ Object_comprehension(_open, binding, _semi, key, _colon, value, _close) {
2459
+ return {
2460
+ type: "objectComprehension",
2461
+ binding: binding.toIR() as IRBindingOrExpr,
2462
+ key: key.toIR(),
2463
+ value: value.toIR(),
2464
+ } satisfies IRNode;
2465
+ },
2466
+ Object_pairs(_open, pairs, _close) {
2467
+ return {
2468
+ type: "object",
2469
+ entries: normalizeList(pairs.toIR()) as Array<{ key: IRNode; value: IRNode }>,
2470
+ } satisfies IRNode;
2471
+ },
2472
+
2473
+ IterBinding_keyValueIn(key, _comma, value, _in, source) {
2474
+ return {
2475
+ type: "binding:keyValueIn",
2476
+ key: key.sourceString,
2477
+ value: value.sourceString,
2478
+ source: source.toIR(),
2479
+ } satisfies IRBinding;
2480
+ },
2481
+ IterBinding_valueIn(value, _in, source) {
2482
+ return {
2483
+ type: "binding:valueIn",
2484
+ value: value.sourceString,
2485
+ source: source.toIR(),
2486
+ } satisfies IRBinding;
2487
+ },
2488
+ IterBinding_keyOf(key, _of, source) {
2489
+ return {
2490
+ type: "binding:keyOf",
2491
+ key: key.sourceString,
2492
+ source: source.toIR(),
2493
+ } satisfies IRBinding;
2494
+ },
2495
+
2496
+ Pair(key, _colon, value) {
2497
+ return { key: key.toIR(), value: value.toIR() };
2498
+ },
2499
+ ObjKey_bare(key) {
2500
+ return { type: "key", name: key.sourceString } satisfies IRNode;
2501
+ },
2502
+ ObjKey_number(num) {
2503
+ return num.toIR();
2504
+ },
2505
+ ObjKey_string(str) {
2506
+ return str.toIR();
2507
+ },
2508
+ ObjKey_computed(_open, expr, _close) {
2509
+ return expr.toIR();
2510
+ },
2511
+
2512
+ BreakKw(_kw) {
2513
+ return { type: "break" } satisfies IRNode;
2514
+ },
2515
+ ContinueKw(_kw) {
2516
+ return { type: "continue" } satisfies IRNode;
2517
+ },
2518
+ SelfExpr_depth(_self, _at, depth) {
2519
+ const value = depth.toIR() as IRNode;
2520
+ if (value.type !== "number" || !Number.isInteger(value.value) || value.value < 1) {
2521
+ throw new Error("self depth must be a positive integer literal");
2522
+ }
2523
+ if (value.value === 1) return { type: "self" } satisfies IRNode;
2524
+ return { type: "selfDepth", depth: value.value } satisfies IRNode;
2525
+ },
2526
+ SelfExpr_plain(selfKw) {
2527
+ return selfKw.toIR();
2528
+ },
2529
+ SelfKw(_kw) {
2530
+ return { type: "self" } satisfies IRNode;
2531
+ },
2532
+ TrueKw(_kw) {
2533
+ return { type: "boolean", value: true } satisfies IRNode;
2534
+ },
2535
+ FalseKw(_kw) {
2536
+ return { type: "boolean", value: false } satisfies IRNode;
2537
+ },
2538
+ NullKw(_kw) {
2539
+ return { type: "null" } satisfies IRNode;
2540
+ },
2541
+ UndefinedKw(_kw) {
2542
+ return { type: "undefined" } satisfies IRNode;
2543
+ },
2544
+
2545
+ StringKw(_kw) {
2546
+ return { type: "identifier", name: "string" } satisfies IRNode;
2547
+ },
2548
+ NumberKw(_kw) {
2549
+ return { type: "identifier", name: "number" } satisfies IRNode;
2550
+ },
2551
+ ObjectKw(_kw) {
2552
+ return { type: "identifier", name: "object" } satisfies IRNode;
2553
+ },
2554
+ ArrayKw(_kw) {
2555
+ return { type: "identifier", name: "array" } satisfies IRNode;
2556
+ },
2557
+ BooleanKw(_kw) {
2558
+ return { type: "identifier", name: "boolean" } satisfies IRNode;
2559
+ },
2560
+
2561
+ identifier(_a, _b) {
2562
+ return { type: "identifier", name: this.sourceString } satisfies IRNode;
2563
+ },
2564
+
2565
+ String(_value) {
2566
+ return { type: "string", raw: this.sourceString } satisfies IRNode;
2567
+ },
2568
+ Number(_value) {
2569
+ return { type: "number", raw: this.sourceString, value: parseNumber(this.sourceString) } satisfies IRNode;
2570
+ },
2571
+
2572
+ PrimaryExpr_group(_open, expr, _close) {
2573
+ return { type: "group", expression: expr.toIR() } satisfies IRNode;
2574
+ },
2575
+ });
2576
+
2577
+ export default semantics;
2578
+
2579
+ export type { RexActionDict, RexSemantics } from "./rex.ohm-bundle.js";