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