@creationix/rex 0.3.1 → 0.6.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/rex.ts CHANGED
@@ -19,6 +19,7 @@ export type IRNode =
19
19
  | { type: "string"; raw: string }
20
20
  | { type: "array"; items: IRNode[] }
21
21
  | { type: "arrayComprehension"; binding: IRBindingOrExpr; body: IRNode }
22
+ | { type: "whileArrayComprehension"; condition: IRNode; body: IRNode }
22
23
  | { type: "object"; entries: { key: IRNode; value: IRNode }[] }
23
24
  | {
24
25
  type: "objectComprehension";
@@ -26,9 +27,15 @@ export type IRNode =
26
27
  key: IRNode;
27
28
  value: IRNode;
28
29
  }
30
+ | {
31
+ type: "whileObjectComprehension";
32
+ condition: IRNode;
33
+ key: IRNode;
34
+ value: IRNode;
35
+ }
29
36
  | { type: "key"; name: string }
30
37
  | { type: "group"; expression: IRNode }
31
- | { type: "unary"; op: "neg" | "not" | "delete"; value: IRNode }
38
+ | { type: "unary"; op: "neg" | "not" | "logicalNot" | "delete"; value: IRNode }
32
39
  | {
33
40
  type: "binary";
34
41
  op:
@@ -42,6 +49,7 @@ export type IRNode =
42
49
  | "bitXor"
43
50
  | "and"
44
51
  | "or"
52
+ | "nor"
45
53
  | "eq"
46
54
  | "neq"
47
55
  | "gt"
@@ -53,7 +61,7 @@ export type IRNode =
53
61
  }
54
62
  | {
55
63
  type: "assign";
56
- op: "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^=";
64
+ op: ":=" | "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^=";
57
65
  place: IRNode;
58
66
  value: IRNode;
59
67
  }
@@ -72,15 +80,18 @@ export type IRNode =
72
80
  }
73
81
  | { type: "for"; binding: IRBindingOrExpr; body: IRNode[] }
74
82
  | { type: "while"; condition: IRNode; body: IRNode[] }
83
+ | { type: "range"; from: IRNode; to: IRNode }
75
84
  | { type: "break" }
76
85
  | { type: "continue" };
77
86
 
78
87
  export type IRBinding =
79
88
  | { type: "binding:keyValueIn"; key: string; value: string; source: IRNode }
80
89
  | { type: "binding:valueIn"; value: string; source: IRNode }
81
- | { type: "binding:keyOf"; key: string; source: IRNode };
90
+ | { type: "binding:keyOf"; key: string; source: IRNode }
91
+ | { type: "binding:bareIn"; source: IRNode }
92
+ | { type: "binding:bareOf"; source: IRNode };
82
93
 
83
- export type IRBindingOrExpr = IRBinding | { type: "binding:expr"; source: IRNode };
94
+ export type IRBindingOrExpr = IRBinding;
84
95
 
85
96
  export type IRConditionalElse =
86
97
  | { type: "else"; block: IRNode[] }
@@ -99,34 +110,39 @@ function byteLength(value: string): number {
99
110
  }
100
111
 
101
112
  const OPCODE_IDS = {
102
- do: 0,
103
- add: 1,
104
- sub: 2,
105
- mul: 3,
106
- div: 4,
107
- eq: 5,
108
- neq: 6,
109
- lt: 7,
110
- lte: 8,
111
- gt: 9,
112
- gte: 10,
113
- and: 11,
114
- or: 12,
115
- xor: 13,
116
- not: 14,
117
- boolean: 15,
118
- number: 16,
119
- string: 17,
120
- array: 18,
121
- object: 19,
122
- mod: 20,
123
- neg: 21,
113
+ do: "",
114
+ add: "ad",
115
+ sub: "sb",
116
+ mul: "ml",
117
+ div: "dv",
118
+ eq: "eq",
119
+ neq: "nq",
120
+ lt: "lt",
121
+ lte: "le",
122
+ gt: "gt",
123
+ gte: "ge",
124
+ and: "an",
125
+ or: "or",
126
+ xor: "xr",
127
+ not: "nt",
128
+ boolean: "bt",
129
+ number: "nm",
130
+ string: "st",
131
+ array: "ar",
132
+ object: "ob",
133
+ mod: "md",
134
+ neg: "ng",
135
+ range: "rn",
124
136
  } as const;
125
137
 
126
138
  type OpcodeName = keyof typeof OPCODE_IDS;
127
139
 
140
+ // Keyword identifiers that are reserved in the grammar and compile to opcodes when called.
141
+ const KEYWORD_OPCODES: ReadonlySet<string> = new Set(["boolean", "number", "string", "array", "object"]);
142
+
128
143
  type EncodeOptions = {
129
- domainRefs?: Record<string, number>;
144
+ domainRefs?: Record<string, string>;
145
+ domainOpcodes?: Record<string, string>;
130
146
  };
131
147
 
132
148
  type CompileOptions = {
@@ -141,10 +157,7 @@ type RexDomainConfigEntry = {
141
157
  names?: unknown;
142
158
  };
143
159
 
144
- const FIRST_NON_RESERVED_REF = 8;
145
- const DOMAIN_DIGIT_INDEX = new Map<string, number>(Array.from(DIGITS).map((char, index) => [char, index]));
146
-
147
- const BINARY_TO_OPCODE: Record<Extract<IRNode, { type: "binary" }> ["op"], OpcodeName> = {
160
+ const BINARY_TO_OPCODE: Record<Exclude<Extract<IRNode, { type: "binary" }> ["op"], "nor">, OpcodeName> = {
148
161
  add: "add",
149
162
  sub: "sub",
150
163
  mul: "mul",
@@ -267,9 +280,9 @@ function encodeDecimal(significand: number, power: number): string {
267
280
 
268
281
  function encodeNumberNode(node: Extract<IRNode, { type: "number" }>): string {
269
282
  const numberValue = node.value;
270
- if (Number.isNaN(numberValue)) return "5'";
271
- if (numberValue === Infinity) return "6'";
272
- if (numberValue === -Infinity) return "7'";
283
+ if (Number.isNaN(numberValue)) return "nan'";
284
+ if (numberValue === Infinity) return "inf'";
285
+ if (numberValue === -Infinity) return "nif'";
273
286
 
274
287
  if (Number.isInteger(numberValue)) {
275
288
  const { base, exp } = splitDecimal(numberValue);
@@ -302,7 +315,7 @@ function encodeNumberNode(node: Extract<IRNode, { type: "number" }>): string {
302
315
  }
303
316
 
304
317
  function encodeOpcode(opcode: OpcodeName): string {
305
- return `${encodeUint(OPCODE_IDS[opcode])}%`;
318
+ return `${OPCODE_IDS[opcode]}%`;
306
319
  }
307
320
 
308
321
  function encodeCallParts(parts: string[]): string {
@@ -321,7 +334,7 @@ function addOptionalPrefix(encoded: string): string {
321
334
  if (encoded.startsWith("?(") || encoded.startsWith("!(") || encoded.startsWith("|(") || encoded.startsWith("&(") || encoded.startsWith(">(") || encoded.startsWith("<(") || encoded.startsWith("#(")) {
322
335
  payload = encoded.slice(2, -1);
323
336
  }
324
- else if (encoded.startsWith(">[") || encoded.startsWith(">{")) {
337
+ else if (encoded.startsWith(">[") || encoded.startsWith(">{") || encoded.startsWith("<[") || encoded.startsWith("<{") || encoded.startsWith("#[") || encoded.startsWith("#{")) {
325
338
  payload = encoded.slice(2, -1);
326
339
  }
327
340
  else if (encoded.startsWith("[") || encoded.startsWith("{") || encoded.startsWith("(")) {
@@ -334,7 +347,7 @@ function addOptionalPrefix(encoded: string): string {
334
347
  }
335
348
 
336
349
  function encodeBlockExpression(block: IRNode[]): string {
337
- if (block.length === 0) return "4'";
350
+ if (block.length === 0) return "un'";
338
351
  if (block.length === 1) return encodeNode(block[0] as IRNode);
339
352
  return encodeCallParts([encodeOpcode("do"), ...block.map((node) => encodeNode(node))]);
340
353
  }
@@ -351,9 +364,14 @@ function encodeConditionalElse(elseBranch: IRConditionalElse): string {
351
364
  return encodeNode(nested);
352
365
  }
353
366
 
367
+ function encodeDomainLookup(shortCode: string, tag: string): string {
368
+ return `${shortCode}${tag}`;
369
+ }
370
+
354
371
  function encodeNavigation(node: Extract<IRNode, { type: "navigation" }>): string {
355
372
  const domainRefs = activeEncodeOptions?.domainRefs;
356
- if (domainRefs && node.target.type === "identifier") {
373
+ const domainOpcodes = activeEncodeOptions?.domainOpcodes;
374
+ if ((domainRefs || domainOpcodes) && node.target.type === "identifier") {
357
375
  const staticPath = [node.target.name];
358
376
  for (const segment of node.segments) {
359
377
  if (segment.type !== "static") break;
@@ -362,15 +380,18 @@ function encodeNavigation(node: Extract<IRNode, { type: "navigation" }>): string
362
380
 
363
381
  for (let pathLength = staticPath.length; pathLength >= 1; pathLength -= 1) {
364
382
  const dottedName = staticPath.slice(0, pathLength).join(".");
365
- const domainRef = domainRefs[dottedName];
366
- if (domainRef === undefined) continue;
383
+ const refCode = domainRefs?.[dottedName];
384
+ const opcodeCode = domainOpcodes?.[dottedName];
385
+ const shortCode = refCode ?? opcodeCode;
386
+ if (shortCode === undefined) continue;
387
+ const tag = refCode !== undefined ? "'" : "%";
367
388
 
368
389
  const consumedStaticSegments = pathLength - 1;
369
390
  if (consumedStaticSegments === node.segments.length) {
370
- return `${encodeUint(domainRef)}'`;
391
+ return encodeDomainLookup(shortCode, tag);
371
392
  }
372
393
 
373
- const parts = [`${encodeUint(domainRef)}'`];
394
+ const parts = [encodeDomainLookup(shortCode, tag)];
374
395
  for (const segment of node.segments.slice(consumedStaticSegments)) {
375
396
  if (segment.type === "static") parts.push(encodeBareOrLengthString(segment.key));
376
397
  else parts.push(encodeNode(segment.key));
@@ -395,9 +416,12 @@ function encodeWhile(node: Extract<IRNode, { type: "while" }>): string {
395
416
 
396
417
  function encodeFor(node: Extract<IRNode, { type: "for" }>): string {
397
418
  const body = addOptionalPrefix(encodeBlockExpression(node.body));
398
- if (node.binding.type === "binding:expr") {
419
+ if (node.binding.type === "binding:bareIn") {
399
420
  return `>(${encodeNode(node.binding.source)}${body})`;
400
421
  }
422
+ if (node.binding.type === "binding:bareOf") {
423
+ return `<(${encodeNode(node.binding.source)}${body})`;
424
+ }
401
425
  if (node.binding.type === "binding:valueIn") {
402
426
  return `>(${encodeNode(node.binding.source)}${node.binding.value}$${body})`;
403
427
  }
@@ -409,31 +433,50 @@ function encodeFor(node: Extract<IRNode, { type: "for" }>): string {
409
433
 
410
434
  function encodeArrayComprehension(node: Extract<IRNode, { type: "arrayComprehension" }>): string {
411
435
  const body = addOptionalPrefix(encodeNode(node.body));
412
- if (node.binding.type === "binding:expr") {
436
+ if (node.binding.type === "binding:bareIn") {
413
437
  return `>[${encodeNode(node.binding.source)}${body}]`;
414
438
  }
439
+ if (node.binding.type === "binding:bareOf") {
440
+ return `<[${encodeNode(node.binding.source)}${body}]`;
441
+ }
415
442
  if (node.binding.type === "binding:valueIn") {
416
443
  return `>[${encodeNode(node.binding.source)}${node.binding.value}$${body}]`;
417
444
  }
418
445
  if (node.binding.type === "binding:keyValueIn") {
419
446
  return `>[${encodeNode(node.binding.source)}${node.binding.key}$${node.binding.value}$${body}]`;
420
447
  }
421
- return `>[${encodeNode(node.binding.source)}${node.binding.key}$${body}]`;
448
+ return `<[${encodeNode(node.binding.source)}${node.binding.key}$${body}]`;
422
449
  }
423
450
 
424
451
  function encodeObjectComprehension(node: Extract<IRNode, { type: "objectComprehension" }>): string {
425
452
  const key = addOptionalPrefix(encodeNode(node.key));
426
453
  const value = addOptionalPrefix(encodeNode(node.value));
427
- if (node.binding.type === "binding:expr") {
454
+ if (node.binding.type === "binding:bareIn") {
428
455
  return `>{${encodeNode(node.binding.source)}${key}${value}}`;
429
456
  }
457
+ if (node.binding.type === "binding:bareOf") {
458
+ return `<{${encodeNode(node.binding.source)}${key}${value}}`;
459
+ }
430
460
  if (node.binding.type === "binding:valueIn") {
431
461
  return `>{${encodeNode(node.binding.source)}${node.binding.value}$${key}${value}}`;
432
462
  }
433
463
  if (node.binding.type === "binding:keyValueIn") {
434
464
  return `>{${encodeNode(node.binding.source)}${node.binding.key}$${node.binding.value}$${key}${value}}`;
435
465
  }
436
- return `>{${encodeNode(node.binding.source)}${node.binding.key}$${key}${value}}`;
466
+ return `<{${encodeNode(node.binding.source)}${node.binding.key}$${key}${value}}`;
467
+ }
468
+
469
+ function encodeWhileArrayComprehension(node: Extract<IRNode, { type: "whileArrayComprehension" }>): string {
470
+ const cond = encodeNode(node.condition);
471
+ const body = addOptionalPrefix(encodeNode(node.body));
472
+ return `#[${cond}${body}]`;
473
+ }
474
+
475
+ function encodeWhileObjectComprehension(node: Extract<IRNode, { type: "whileObjectComprehension" }>): string {
476
+ const cond = encodeNode(node.condition);
477
+ const key = addOptionalPrefix(encodeNode(node.key));
478
+ const value = addOptionalPrefix(encodeNode(node.value));
479
+ return `#{${cond}${key}${value}}`;
437
480
  }
438
481
 
439
482
  let activeEncodeOptions: EncodeOptions | undefined;
@@ -444,7 +487,9 @@ function encodeNode(node: IRNode): string {
444
487
  return encodeBlockExpression(node.body);
445
488
  case "identifier": {
446
489
  const domainRef = activeEncodeOptions?.domainRefs?.[node.name];
447
- if (domainRef !== undefined) return `${encodeUint(domainRef)}'`;
490
+ if (domainRef !== undefined) return `${domainRef}'`;
491
+ const domainOpcode = activeEncodeOptions?.domainOpcodes?.[node.name];
492
+ if (domainOpcode !== undefined) return `${domainOpcode}%`;
448
493
  return `${node.name}$`;
449
494
  }
450
495
  case "self":
@@ -455,11 +500,11 @@ function encodeNode(node: IRNode): string {
455
500
  return `${encodeUint(node.depth - 1)}@`;
456
501
  }
457
502
  case "boolean":
458
- return node.value ? "1'" : "2'";
503
+ return node.value ? "tr'" : "fl'";
459
504
  case "null":
460
- return "3'";
505
+ return "nl'";
461
506
  case "undefined":
462
- return "4'";
507
+ return "un'";
463
508
  case "number":
464
509
  return encodeNumberNode(node);
465
510
  case "string":
@@ -470,6 +515,8 @@ function encodeNode(node: IRNode): string {
470
515
  }
471
516
  case "arrayComprehension":
472
517
  return encodeArrayComprehension(node);
518
+ case "whileArrayComprehension":
519
+ return encodeWhileArrayComprehension(node);
473
520
  case "object": {
474
521
  const body = node.entries
475
522
  .map(({ key, value }) => `${encodeNode(key)}${addOptionalPrefix(encodeNode(value))}`)
@@ -478,6 +525,8 @@ function encodeNode(node: IRNode): string {
478
525
  }
479
526
  case "objectComprehension":
480
527
  return encodeObjectComprehension(node);
528
+ case "whileObjectComprehension":
529
+ return encodeWhileObjectComprehension(node);
481
530
  case "key":
482
531
  return encodeBareOrLengthString(node.name);
483
532
  case "group":
@@ -485,6 +534,10 @@ function encodeNode(node: IRNode): string {
485
534
  case "unary":
486
535
  if (node.op === "delete") return `~${encodeNode(node.value)}`;
487
536
  if (node.op === "neg") return encodeCallParts([encodeOpcode("neg"), encodeNode(node.value)]);
537
+ if (node.op === "logicalNot") {
538
+ const val = encodeNode(node.value);
539
+ return `!(${val}tr')`;
540
+ }
488
541
  return encodeCallParts([encodeOpcode("not"), encodeNode(node.value)]);
489
542
  case "binary":
490
543
  if (node.op === "and") {
@@ -507,12 +560,18 @@ function encodeNode(node: IRNode): string {
507
560
  .join("");
508
561
  return `|(${body})`;
509
562
  }
563
+ if (node.op === "nor") {
564
+ const left = encodeNode(node.left);
565
+ const right = addOptionalPrefix(encodeNode(node.right));
566
+ return `!(${left}${right})`;
567
+ }
510
568
  return encodeCallParts([
511
569
  encodeOpcode(BINARY_TO_OPCODE[node.op]),
512
570
  encodeNode(node.left),
513
571
  encodeNode(node.right),
514
572
  ]);
515
573
  case "assign": {
574
+ if (node.op === ":=") return `/${encodeNode(node.place)}${addOptionalPrefix(encodeNode(node.value))}`;
516
575
  if (node.op === "=") return `=${encodeNode(node.place)}${addOptionalPrefix(encodeNode(node.value))}`;
517
576
  const opcode = ASSIGN_COMPOUND_TO_OPCODE[node.op];
518
577
  if (!opcode) throw new Error(`Unsupported assignment op: ${node.op}`);
@@ -521,8 +580,14 @@ function encodeNode(node: IRNode): string {
521
580
  }
522
581
  case "navigation":
523
582
  return encodeNavigation(node);
524
- case "call":
583
+ case "call": {
584
+ // Keyword identifiers (boolean, number, string, array, object) are parsed
585
+ // as identifier nodes but must be encoded as opcode calls, not variable navigation.
586
+ if (node.callee.type === "identifier" && KEYWORD_OPCODES.has(node.callee.name)) {
587
+ return encodeCallParts([encodeOpcode(node.callee.name as OpcodeName), ...node.args.map((arg) => encodeNode(arg))]);
588
+ }
525
589
  return encodeCallParts([encodeNode(node.callee), ...node.args.map((arg) => encodeNode(arg))]);
590
+ }
526
591
  case "conditional": {
527
592
  const opener = node.head === "when" ? "?(" : "!(";
528
593
  const cond = encodeNode(node.condition);
@@ -530,6 +595,8 @@ function encodeNode(node: IRNode): string {
530
595
  const elseExpr = node.elseBranch ? addOptionalPrefix(encodeConditionalElse(node.elseBranch)) : "";
531
596
  return `${opener}${cond}${thenExpr}${elseExpr})`;
532
597
  }
598
+ case "range":
599
+ return encodeCallParts([encodeOpcode("range"), encodeNode(node.from), encodeNode(node.to)]);
533
600
  case "for":
534
601
  return encodeFor(node);
535
602
  case "while":
@@ -550,11 +617,28 @@ function collectLogicalChain(node: IRNode, op: "and" | "or"): IRNode[] {
550
617
  return [...collectLogicalChain(node.left, op), ...collectLogicalChain(node.right, op)];
551
618
  }
552
619
 
620
+ type ParseFailure = { message?: string; getRightmostFailurePosition?: () => number };
621
+
622
+ export function formatParseError(source: string, match: ParseFailure): string {
623
+ const message = match.message ?? "Parse failed";
624
+ const pos = match.getRightmostFailurePosition?.();
625
+ if (typeof pos !== "number" || !Number.isFinite(pos)) return message;
626
+
627
+ const safePos = Math.max(0, Math.min(source.length, pos));
628
+ const lineStart = source.lastIndexOf("\n", safePos - 1) + 1;
629
+ const lineEndIndex = source.indexOf("\n", safePos);
630
+ const lineEnd = lineEndIndex === -1 ? source.length : lineEndIndex;
631
+ const lineText = source.slice(lineStart, lineEnd);
632
+ const lineNumber = source.slice(0, lineStart).split("\n").length;
633
+ const columnNumber = safePos - lineStart + 1;
634
+ const caret = `${" ".repeat(Math.max(0, columnNumber - 1))}^`;
635
+ return `${message}\n ${lineText}\n ${caret}`;
636
+ }
637
+
553
638
  export function parseToIR(source: string): IRNode {
554
639
  const match = grammar.match(source);
555
640
  if (!match.succeeded()) {
556
- const failure = match as unknown as { message?: string };
557
- throw new Error(failure.message ?? "Parse failed");
641
+ throw new Error(formatParseError(source, match as ParseFailure));
558
642
  }
559
643
  return semantics(match).toIR() as IRNode;
560
644
  }
@@ -690,81 +774,62 @@ export function parse(source: string): unknown {
690
774
  return parseDataNode(parseToIR(source));
691
775
  }
692
776
 
693
- export function domainRefsFromConfig(config: unknown): Record<string, number> {
777
+ type DomainMaps = { domainRefs: Record<string, string>; domainOpcodes: Record<string, string> };
778
+
779
+ export function domainRefsFromConfig(config: unknown): DomainMaps {
694
780
  if (!config || typeof config !== "object" || Array.isArray(config)) {
695
781
  throw new Error("Domain config must be an object");
696
782
  }
697
- const refs: Record<string, number> = {};
698
- for (const section of Object.values(config)) {
699
- if (!section || typeof section !== "object" || Array.isArray(section)) continue;
700
- mapConfigEntries(section as Record<string, unknown>, refs);
701
- }
702
- return refs;
703
- }
783
+ const configObj = config as Record<string, unknown>;
784
+ const domainRefs: Record<string, string> = {};
785
+ const domainOpcodes: Record<string, string> = {};
704
786
 
705
- function decodeDomainRefKey(refText: string): number {
706
- if (!refText) throw new Error("Domain ref key cannot be empty");
707
- if (!/^[0-9A-Za-z_-]+$/.test(refText)) {
708
- throw new Error(`Invalid domain ref key '${refText}' (must use base64 alphabet 0-9a-zA-Z-_)`);
709
- }
710
- if (refText.length > 1 && refText[0] === "0") {
711
- throw new Error(`Invalid domain ref key '${refText}' (leading zeroes are not allowed)`);
712
- }
713
- if (/^[1-9]$/.test(refText)) {
714
- throw new Error(`Invalid domain ref key '${refText}' (reserved by core language)`);
715
- }
716
-
717
- let value = 0;
718
- for (const char of refText) {
719
- const digit = DOMAIN_DIGIT_INDEX.get(char);
720
- if (digit === undefined) throw new Error(`Invalid domain ref key '${refText}'`);
721
- value = value * 64 + digit;
722
- if (value > Number.MAX_SAFE_INTEGER) {
723
- throw new Error(`Invalid domain ref key '${refText}' (must fit in 53 bits)`);
724
- }
787
+ const dataSection = configObj.data;
788
+ if (dataSection && typeof dataSection === "object" && !Array.isArray(dataSection)) {
789
+ mapConfigEntries(dataSection as Record<string, unknown>, domainRefs);
725
790
  }
726
791
 
727
- if (value < FIRST_NON_RESERVED_REF) {
728
- throw new Error(`Invalid domain ref key '${refText}' (maps to reserved id ${value})`);
792
+ const functionsSection = configObj.functions;
793
+ if (functionsSection && typeof functionsSection === "object" && !Array.isArray(functionsSection)) {
794
+ mapConfigEntries(functionsSection as Record<string, unknown>, domainOpcodes);
729
795
  }
730
796
 
731
- return value;
797
+ return { domainRefs, domainOpcodes };
732
798
  }
733
799
 
734
- function mapConfigEntries(entries: Record<string, unknown>, refs: Record<string, number>) {
800
+ function mapConfigEntries(entries: Record<string, unknown>, refs: Record<string, string>) {
735
801
  const sourceKindByRoot = new Map<string, "explicit" | "implicit">();
736
802
  for (const root of Object.keys(refs)) {
737
803
  sourceKindByRoot.set(root, "explicit");
738
804
  }
739
805
 
740
- for (const [refText, rawEntry] of Object.entries(entries)) {
806
+ for (const [shortCode, rawEntry] of Object.entries(entries)) {
741
807
  const entry = rawEntry as RexDomainConfigEntry;
742
808
  if (!entry || typeof entry !== "object") continue;
743
809
  if (!Array.isArray(entry.names)) continue;
744
810
 
745
- const refId = decodeDomainRefKey(refText);
746
811
  for (const rawName of entry.names) {
747
812
  if (typeof rawName !== "string") continue;
748
- const existingNameRef = refs[rawName];
749
- if (existingNameRef !== undefined && existingNameRef !== refId) {
750
- throw new Error(`Conflicting refs for '${rawName}': ${existingNameRef} vs ${refId}`);
813
+ const existingRef = refs[rawName];
814
+ if (existingRef !== undefined && existingRef !== shortCode) {
815
+ throw new Error(`Conflicting refs for '${rawName}': ${existingRef} vs ${shortCode}`);
751
816
  }
752
- refs[rawName] = refId;
817
+ refs[rawName] = shortCode;
753
818
 
754
819
  const root = rawName.split(".")[0];
755
820
  if (!root) continue;
756
821
  const currentKind: "explicit" | "implicit" = rawName.includes(".") ? "implicit" : "explicit";
757
822
  const existing = refs[root];
758
823
  if (existing !== undefined) {
759
- if (existing === refId) continue;
824
+ if (existing === shortCode) continue;
760
825
  const existingKind = sourceKindByRoot.get(root) ?? "explicit";
761
826
  if (currentKind === "explicit") {
762
- throw new Error(`Conflicting refs for '${root}': ${existing} vs ${refId}`);
827
+ throw new Error(`Conflicting refs for '${root}': ${existing} vs ${shortCode}`);
763
828
  }
764
829
  if (existingKind === "explicit") continue;
765
830
  continue;
766
831
  }
767
- refs[root] = refId;
832
+ refs[root] = shortCode;
768
833
  sourceKindByRoot.set(root, currentKind);
769
834
  }
770
835
  }
@@ -910,17 +975,10 @@ function gatherEncodedValueSpans(text: string): EncodedSpan[] {
910
975
  return spans;
911
976
  }
912
977
 
913
- function buildPointerToken(pointerStart: number, targetStart: number): string | undefined {
914
- let offset = targetStart - (pointerStart + 1);
978
+ function buildPointerToken(pointerStart: number, targetStart: number, occurrenceSize: number): string | undefined {
979
+ const offset = targetStart - pointerStart - occurrenceSize;
915
980
  if (offset < 0) return undefined;
916
- for (let guard = 0; guard < 8; guard += 1) {
917
- const prefix = encodeUint(offset);
918
- const recalculated = targetStart - (pointerStart + prefix.length + 1);
919
- if (recalculated === offset) return `${prefix}^`;
920
- offset = recalculated;
921
- if (offset < 0) return undefined;
922
- }
923
- return undefined;
981
+ return `${encodeUint(offset)}^`;
924
982
  }
925
983
 
926
984
  function buildDedupeCandidateTable(encoded: string, minBytes: number): Map<string, DedupeCandidate[]> {
@@ -962,10 +1020,10 @@ function dedupeLargeEncodedValues(encoded: string, minBytes = 4): string {
962
1020
 
963
1021
  if (current.slice(occurrence.span.start, occurrence.span.end) !== value) continue;
964
1022
 
965
- const canonicalCurrentStart = current.length - canonical.offsetFromEnd - canonical.sizeBytes;
966
- const pointerToken = buildPointerToken(occurrence.span.start, canonicalCurrentStart);
967
- if (!pointerToken) continue;
968
- if (pointerToken.length >= occurrence.sizeBytes) continue;
1023
+ const canonicalCurrentStart = current.length - canonical.offsetFromEnd - canonical.sizeBytes;
1024
+ const pointerToken = buildPointerToken(occurrence.span.start, canonicalCurrentStart, occurrence.sizeBytes);
1025
+ if (!pointerToken) continue;
1026
+ if (pointerToken.length >= occurrence.sizeBytes) continue;
969
1027
 
970
1028
  current = `${current.slice(0, occurrence.span.start)}${pointerToken}${current.slice(occurrence.span.end)}`;
971
1029
  replaced = true;
@@ -1053,6 +1111,22 @@ function dropBindingNames(env: OptimizeEnv, binding: IRBindingOrExpr) {
1053
1111
  }
1054
1112
  }
1055
1113
 
1114
+ function optimizeBinding(binding: IRBindingOrExpr, sourceEnv: OptimizeEnv, currentDepth: number): IRBinding {
1115
+ const source = optimizeNode(binding.source, sourceEnv, currentDepth);
1116
+ switch (binding.type) {
1117
+ case "binding:bareIn":
1118
+ return { type: "binding:bareIn", source };
1119
+ case "binding:bareOf":
1120
+ return { type: "binding:bareOf", source };
1121
+ case "binding:valueIn":
1122
+ return { type: "binding:valueIn", value: binding.value, source };
1123
+ case "binding:keyValueIn":
1124
+ return { type: "binding:keyValueIn", key: binding.key, value: binding.value, source };
1125
+ case "binding:keyOf":
1126
+ return { type: "binding:keyOf", key: binding.key, source };
1127
+ }
1128
+ }
1129
+
1056
1130
  function collectReads(node: IRNode, out: Set<string>) {
1057
1131
  switch (node.type) {
1058
1132
  case "identifier":
@@ -1074,11 +1148,20 @@ function collectReads(node: IRNode, out: Set<string>) {
1074
1148
  collectReads(node.binding.source, out);
1075
1149
  collectReads(node.body, out);
1076
1150
  return;
1151
+ case "whileArrayComprehension":
1152
+ collectReads(node.condition, out);
1153
+ collectReads(node.body, out);
1154
+ return;
1077
1155
  case "objectComprehension":
1078
1156
  collectReads(node.binding.source, out);
1079
1157
  collectReads(node.key, out);
1080
1158
  collectReads(node.value, out);
1081
1159
  return;
1160
+ case "whileObjectComprehension":
1161
+ collectReads(node.condition, out);
1162
+ collectReads(node.key, out);
1163
+ collectReads(node.value, out);
1164
+ return;
1082
1165
  case "unary":
1083
1166
  collectReads(node.value, out);
1084
1167
  return;
@@ -1109,6 +1192,10 @@ function collectReads(node: IRNode, out: Set<string>) {
1109
1192
  collectReads(node.binding.source, out);
1110
1193
  for (const part of node.body) collectReads(part, out);
1111
1194
  return;
1195
+ case "range":
1196
+ collectReads(node.from, out);
1197
+ collectReads(node.to, out);
1198
+ return;
1112
1199
  case "program":
1113
1200
  for (const part of node.body) collectReads(part, out);
1114
1201
  return;
@@ -1151,6 +1238,8 @@ function isPureNode(node: IRNode): boolean {
1151
1238
  return node.op !== "delete" && isPureNode(node.value);
1152
1239
  case "binary":
1153
1240
  return isPureNode(node.left) && isPureNode(node.right);
1241
+ case "range":
1242
+ return isPureNode(node.from) && isPureNode(node.to);
1154
1243
  default:
1155
1244
  return false;
1156
1245
  }
@@ -1223,6 +1312,8 @@ function hasIdentifierRead(node: IRNode, name: string, asPlace = false): boolean
1223
1312
  return hasIdentifierRead(node.value, name, node.op === "delete");
1224
1313
  case "binary":
1225
1314
  return hasIdentifierRead(node.left, name) || hasIdentifierRead(node.right, name);
1315
+ case "range":
1316
+ return hasIdentifierRead(node.from, name) || hasIdentifierRead(node.to, name);
1226
1317
  case "assign":
1227
1318
  return hasIdentifierRead(node.place, name, true) || hasIdentifierRead(node.value, name);
1228
1319
  default:
@@ -1245,6 +1336,8 @@ function countIdentifierReads(node: IRNode, name: string, asPlace = false): numb
1245
1336
  return countIdentifierReads(node.value, name, node.op === "delete");
1246
1337
  case "binary":
1247
1338
  return countIdentifierReads(node.left, name) + countIdentifierReads(node.right, name);
1339
+ case "range":
1340
+ return countIdentifierReads(node.from, name) + countIdentifierReads(node.to, name);
1248
1341
  case "assign":
1249
1342
  return countIdentifierReads(node.place, name, true) + countIdentifierReads(node.value, name);
1250
1343
  default:
@@ -1302,6 +1395,12 @@ function replaceIdentifier(node: IRNode, name: string, replacement: IRNode, asPl
1302
1395
  place: replaceIdentifier(node.place, name, replacement, true),
1303
1396
  value: replaceIdentifier(node.value, name, replacement),
1304
1397
  } satisfies IRNode;
1398
+ case "range":
1399
+ return {
1400
+ type: "range",
1401
+ from: replaceIdentifier(node.from, name, replacement),
1402
+ to: replaceIdentifier(node.to, name, replacement),
1403
+ } satisfies IRNode;
1305
1404
  default:
1306
1405
  return node;
1307
1406
  }
@@ -1435,6 +1534,9 @@ function foldUnary(op: Extract<IRNode, { type: "unary" }> ["op"], value: unknown
1435
1534
  if (typeof value === "number") return ~value;
1436
1535
  return undefined;
1437
1536
  }
1537
+ if (op === "logicalNot") {
1538
+ return value === undefined ? true : undefined;
1539
+ }
1438
1540
  return undefined;
1439
1541
  }
1440
1542
 
@@ -1597,6 +1699,12 @@ function optimizeNode(node: IRNode, env: OptimizeEnv, currentDepth: number, asPl
1597
1699
  }
1598
1700
  return { type: "binary", op: node.op, left, right } satisfies IRNode;
1599
1701
  }
1702
+ case "range":
1703
+ return {
1704
+ type: "range",
1705
+ from: optimizeNode(node.from, env, currentDepth),
1706
+ to: optimizeNode(node.to, env, currentDepth),
1707
+ } satisfies IRNode;
1600
1708
  case "navigation": {
1601
1709
  const target = optimizeNode(node.target, env, currentDepth);
1602
1710
  const segments = node.segments.map((segment) => (segment.type === "static"
@@ -1706,31 +1814,7 @@ function optimizeNode(node: IRNode, env: OptimizeEnv, currentDepth: number, asPl
1706
1814
  }
1707
1815
  case "for": {
1708
1816
  const sourceEnv = cloneOptimizeEnv(env);
1709
- const binding = (() => {
1710
- if (node.binding.type === "binding:expr") {
1711
- return { type: "binding:expr", source: optimizeNode(node.binding.source, sourceEnv, currentDepth) } satisfies IRBindingOrExpr;
1712
- }
1713
- if (node.binding.type === "binding:valueIn") {
1714
- return {
1715
- type: "binding:valueIn",
1716
- value: node.binding.value,
1717
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1718
- } satisfies IRBinding;
1719
- }
1720
- if (node.binding.type === "binding:keyValueIn") {
1721
- return {
1722
- type: "binding:keyValueIn",
1723
- key: node.binding.key,
1724
- value: node.binding.value,
1725
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1726
- } satisfies IRBinding;
1727
- }
1728
- return {
1729
- type: "binding:keyOf",
1730
- key: node.binding.key,
1731
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1732
- } satisfies IRBinding;
1733
- })();
1817
+ const binding = optimizeBinding(node.binding, sourceEnv, currentDepth);
1734
1818
  const bodyEnv = cloneOptimizeEnv(env);
1735
1819
  dropBindingNames(bodyEnv, binding);
1736
1820
  return {
@@ -1741,26 +1825,7 @@ function optimizeNode(node: IRNode, env: OptimizeEnv, currentDepth: number, asPl
1741
1825
  }
1742
1826
  case "arrayComprehension": {
1743
1827
  const sourceEnv = cloneOptimizeEnv(env);
1744
- const binding = node.binding.type === "binding:expr"
1745
- ? ({ type: "binding:expr", source: optimizeNode(node.binding.source, sourceEnv, currentDepth) } satisfies IRBindingOrExpr)
1746
- : node.binding.type === "binding:valueIn"
1747
- ? ({
1748
- type: "binding:valueIn",
1749
- value: node.binding.value,
1750
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1751
- } satisfies IRBinding)
1752
- : node.binding.type === "binding:keyValueIn"
1753
- ? ({
1754
- type: "binding:keyValueIn",
1755
- key: node.binding.key,
1756
- value: node.binding.value,
1757
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1758
- } satisfies IRBinding)
1759
- : ({
1760
- type: "binding:keyOf",
1761
- key: node.binding.key,
1762
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1763
- } satisfies IRBinding);
1828
+ const binding = optimizeBinding(node.binding, sourceEnv, currentDepth);
1764
1829
  const bodyEnv = cloneOptimizeEnv(env);
1765
1830
  dropBindingNames(bodyEnv, binding);
1766
1831
  return {
@@ -1769,28 +1834,15 @@ function optimizeNode(node: IRNode, env: OptimizeEnv, currentDepth: number, asPl
1769
1834
  body: optimizeNode(node.body, bodyEnv, currentDepth + 1),
1770
1835
  } satisfies IRNode;
1771
1836
  }
1837
+ case "whileArrayComprehension":
1838
+ return {
1839
+ type: "whileArrayComprehension",
1840
+ condition: optimizeNode(node.condition, env, currentDepth),
1841
+ body: optimizeNode(node.body, env, currentDepth + 1),
1842
+ } satisfies IRNode;
1772
1843
  case "objectComprehension": {
1773
1844
  const sourceEnv = cloneOptimizeEnv(env);
1774
- const binding = node.binding.type === "binding:expr"
1775
- ? ({ type: "binding:expr", source: optimizeNode(node.binding.source, sourceEnv, currentDepth) } satisfies IRBindingOrExpr)
1776
- : node.binding.type === "binding:valueIn"
1777
- ? ({
1778
- type: "binding:valueIn",
1779
- value: node.binding.value,
1780
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1781
- } satisfies IRBinding)
1782
- : node.binding.type === "binding:keyValueIn"
1783
- ? ({
1784
- type: "binding:keyValueIn",
1785
- key: node.binding.key,
1786
- value: node.binding.value,
1787
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1788
- } satisfies IRBinding)
1789
- : ({
1790
- type: "binding:keyOf",
1791
- key: node.binding.key,
1792
- source: optimizeNode(node.binding.source, sourceEnv, currentDepth),
1793
- } satisfies IRBinding);
1845
+ const binding = optimizeBinding(node.binding, sourceEnv, currentDepth);
1794
1846
  const bodyEnv = cloneOptimizeEnv(env);
1795
1847
  dropBindingNames(bodyEnv, binding);
1796
1848
  return {
@@ -1800,13 +1852,21 @@ function optimizeNode(node: IRNode, env: OptimizeEnv, currentDepth: number, asPl
1800
1852
  value: optimizeNode(node.value, bodyEnv, currentDepth + 1),
1801
1853
  } satisfies IRNode;
1802
1854
  }
1855
+ case "whileObjectComprehension":
1856
+ return {
1857
+ type: "whileObjectComprehension",
1858
+ condition: optimizeNode(node.condition, env, currentDepth),
1859
+ key: optimizeNode(node.key, env, currentDepth + 1),
1860
+ value: optimizeNode(node.value, env, currentDepth + 1),
1861
+ } satisfies IRNode;
1803
1862
  default:
1804
1863
  return node;
1805
1864
  }
1806
1865
  }
1807
1866
 
1808
1867
  export function optimizeIR(node: IRNode): IRNode {
1809
- return optimizeNode(node, emptyOptimizeEnv(), 1);
1868
+ // Optimization is temporarily disabled pending correctness fixes.
1869
+ return node;
1810
1870
  }
1811
1871
 
1812
1872
  function collectLocalBindings(node: IRNode, locals: Set<string>) {
@@ -1848,6 +1908,10 @@ function collectLocalBindings(node: IRNode, locals: Set<string>) {
1848
1908
  collectLocalBindings(node.left, locals);
1849
1909
  collectLocalBindings(node.right, locals);
1850
1910
  return;
1911
+ case "range":
1912
+ collectLocalBindings(node.from, locals);
1913
+ collectLocalBindings(node.to, locals);
1914
+ return;
1851
1915
  case "conditional":
1852
1916
  collectLocalBindings(node.condition, locals);
1853
1917
  for (const part of node.thenBlock) collectLocalBindings(part, locals);
@@ -1861,11 +1925,20 @@ function collectLocalBindings(node: IRNode, locals: Set<string>) {
1861
1925
  collectLocalBindingFromBinding(node.binding, locals);
1862
1926
  collectLocalBindings(node.body, locals);
1863
1927
  return;
1928
+ case "whileArrayComprehension":
1929
+ collectLocalBindings(node.condition, locals);
1930
+ collectLocalBindings(node.body, locals);
1931
+ return;
1864
1932
  case "objectComprehension":
1865
1933
  collectLocalBindingFromBinding(node.binding, locals);
1866
1934
  collectLocalBindings(node.key, locals);
1867
1935
  collectLocalBindings(node.value, locals);
1868
1936
  return;
1937
+ case "whileObjectComprehension":
1938
+ collectLocalBindings(node.condition, locals);
1939
+ collectLocalBindings(node.key, locals);
1940
+ collectLocalBindings(node.value, locals);
1941
+ return;
1869
1942
  default:
1870
1943
  return;
1871
1944
  }
@@ -1952,6 +2025,10 @@ function collectNameFrequencies(node: IRNode, locals: Set<string>, frequencies:
1952
2025
  collectNameFrequencies(node.left, locals, frequencies, order, nextOrder);
1953
2026
  collectNameFrequencies(node.right, locals, frequencies, order, nextOrder);
1954
2027
  return;
2028
+ case "range":
2029
+ collectNameFrequencies(node.from, locals, frequencies, order, nextOrder);
2030
+ collectNameFrequencies(node.to, locals, frequencies, order, nextOrder);
2031
+ return;
1955
2032
  case "conditional":
1956
2033
  collectNameFrequencies(node.condition, locals, frequencies, order, nextOrder);
1957
2034
  for (const part of node.thenBlock) collectNameFrequencies(part, locals, frequencies, order, nextOrder);
@@ -1965,11 +2042,20 @@ function collectNameFrequencies(node: IRNode, locals: Set<string>, frequencies:
1965
2042
  collectNameFrequenciesBinding(node.binding, locals, frequencies, order, nextOrder);
1966
2043
  collectNameFrequencies(node.body, locals, frequencies, order, nextOrder);
1967
2044
  return;
2045
+ case "whileArrayComprehension":
2046
+ collectNameFrequencies(node.condition, locals, frequencies, order, nextOrder);
2047
+ collectNameFrequencies(node.body, locals, frequencies, order, nextOrder);
2048
+ return;
1968
2049
  case "objectComprehension":
1969
2050
  collectNameFrequenciesBinding(node.binding, locals, frequencies, order, nextOrder);
1970
2051
  collectNameFrequencies(node.key, locals, frequencies, order, nextOrder);
1971
2052
  collectNameFrequencies(node.value, locals, frequencies, order, nextOrder);
1972
2053
  return;
2054
+ case "whileObjectComprehension":
2055
+ collectNameFrequencies(node.condition, locals, frequencies, order, nextOrder);
2056
+ collectNameFrequencies(node.key, locals, frequencies, order, nextOrder);
2057
+ collectNameFrequencies(node.value, locals, frequencies, order, nextOrder);
2058
+ return;
1973
2059
  default:
1974
2060
  return;
1975
2061
  }
@@ -2046,6 +2132,12 @@ function renameLocalNames(node: IRNode, map: Map<string, string>): IRNode {
2046
2132
  left: renameLocalNames(node.left, map),
2047
2133
  right: renameLocalNames(node.right, map),
2048
2134
  } satisfies IRNode;
2135
+ case "range":
2136
+ return {
2137
+ type: "range",
2138
+ from: renameLocalNames(node.from, map),
2139
+ to: renameLocalNames(node.to, map),
2140
+ } satisfies IRNode;
2049
2141
  case "assign": {
2050
2142
  const place = node.place.type === "identifier" && map.has(node.place.name)
2051
2143
  ? ({ type: "identifier", name: map.get(node.place.name) as string } satisfies IRNode)
@@ -2077,6 +2169,12 @@ function renameLocalNames(node: IRNode, map: Map<string, string>): IRNode {
2077
2169
  binding: renameLocalNamesBinding(node.binding, map),
2078
2170
  body: renameLocalNames(node.body, map),
2079
2171
  } satisfies IRNode;
2172
+ case "whileArrayComprehension":
2173
+ return {
2174
+ type: "whileArrayComprehension",
2175
+ condition: renameLocalNames(node.condition, map),
2176
+ body: renameLocalNames(node.body, map),
2177
+ } satisfies IRNode;
2080
2178
  case "objectComprehension":
2081
2179
  return {
2082
2180
  type: "objectComprehension",
@@ -2084,35 +2182,32 @@ function renameLocalNames(node: IRNode, map: Map<string, string>): IRNode {
2084
2182
  key: renameLocalNames(node.key, map),
2085
2183
  value: renameLocalNames(node.value, map),
2086
2184
  } satisfies IRNode;
2185
+ case "whileObjectComprehension":
2186
+ return {
2187
+ type: "whileObjectComprehension",
2188
+ condition: renameLocalNames(node.condition, map),
2189
+ key: renameLocalNames(node.key, map),
2190
+ value: renameLocalNames(node.value, map),
2191
+ } satisfies IRNode;
2087
2192
  default:
2088
2193
  return node;
2089
2194
  }
2090
2195
  }
2091
2196
 
2092
2197
  function renameLocalNamesBinding(binding: IRBindingOrExpr, map: Map<string, string>): IRBindingOrExpr {
2093
- if (binding.type === "binding:expr") {
2094
- return { type: "binding:expr", source: renameLocalNames(binding.source, map) } satisfies IRBindingOrExpr;
2198
+ const source = renameLocalNames(binding.source, map);
2199
+ switch (binding.type) {
2200
+ case "binding:bareIn":
2201
+ return { type: "binding:bareIn", source };
2202
+ case "binding:bareOf":
2203
+ return { type: "binding:bareOf", source };
2204
+ case "binding:valueIn":
2205
+ return { type: "binding:valueIn", value: map.get(binding.value) ?? binding.value, source };
2206
+ case "binding:keyValueIn":
2207
+ return { type: "binding:keyValueIn", key: map.get(binding.key) ?? binding.key, value: map.get(binding.value) ?? binding.value, source };
2208
+ case "binding:keyOf":
2209
+ return { type: "binding:keyOf", key: map.get(binding.key) ?? binding.key, source };
2095
2210
  }
2096
- if (binding.type === "binding:valueIn") {
2097
- return {
2098
- type: "binding:valueIn",
2099
- value: map.get(binding.value) ?? binding.value,
2100
- source: renameLocalNames(binding.source, map),
2101
- } satisfies IRBinding;
2102
- }
2103
- if (binding.type === "binding:keyValueIn") {
2104
- return {
2105
- type: "binding:keyValueIn",
2106
- key: map.get(binding.key) ?? binding.key,
2107
- value: map.get(binding.value) ?? binding.value,
2108
- source: renameLocalNames(binding.source, map),
2109
- } satisfies IRBinding;
2110
- }
2111
- return {
2112
- type: "binding:keyOf",
2113
- key: map.get(binding.key) ?? binding.key,
2114
- source: renameLocalNames(binding.source, map),
2115
- } satisfies IRBinding;
2116
2211
  }
2117
2212
 
2118
2213
  function renameLocalNamesElse(elseBranch: IRConditionalElse, map: Map<string, string>): IRConditionalElse {
@@ -2163,9 +2258,9 @@ export function compile(source: string, options?: CompileOptions): string {
2163
2258
  const ir = parseToIR(source);
2164
2259
  let lowered = options?.optimize ? optimizeIR(ir) : ir;
2165
2260
  if (options?.minifyNames) lowered = minifyLocalNamesIR(lowered);
2166
- const domainRefs = options?.domainConfig ? domainRefsFromConfig(options.domainConfig) : undefined;
2261
+ const domainMaps = options?.domainConfig ? domainRefsFromConfig(options.domainConfig) : undefined;
2167
2262
  return encodeIR(lowered, {
2168
- domainRefs,
2263
+ ...domainMaps,
2169
2264
  dedupeValues: options?.dedupeValues,
2170
2265
  dedupeMinBytes: options?.dedupeMinBytes,
2171
2266
  });
@@ -2299,6 +2394,9 @@ semantics.addOperation("toIR", {
2299
2394
  ExistenceExpr_or(left, _or, right) {
2300
2395
  return { type: "binary", op: "or", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2301
2396
  },
2397
+ ExistenceExpr_nor(left, _nor, right) {
2398
+ return { type: "binary", op: "nor", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2399
+ },
2302
2400
 
2303
2401
  BitExpr_and(left, _op, right) {
2304
2402
  return { type: "binary", op: "bitAnd", left: left.toIR(), right: right.toIR() } satisfies IRNode;
@@ -2310,6 +2408,10 @@ semantics.addOperation("toIR", {
2310
2408
  return { type: "binary", op: "bitOr", left: left.toIR(), right: right.toIR() } satisfies IRNode;
2311
2409
  },
2312
2410
 
2411
+ RangeExpr_range(left, _op, right) {
2412
+ return { type: "range", from: left.toIR(), to: right.toIR() } satisfies IRNode;
2413
+ },
2414
+
2313
2415
  CompareExpr_binary(left, op, right) {
2314
2416
  const map: Record<string, Extract<IRNode, { type: "binary" }>["op"]> = {
2315
2417
  "==": "eq",
@@ -2352,6 +2454,9 @@ semantics.addOperation("toIR", {
2352
2454
  UnaryExpr_not(_op, value) {
2353
2455
  return { type: "unary", op: "not", value: value.toIR() } satisfies IRNode;
2354
2456
  },
2457
+ UnaryExpr_logicalNot(_not, value) {
2458
+ return { type: "unary", op: "logicalNot", value: value.toIR() } satisfies IRNode;
2459
+ },
2355
2460
  UnaryExpr_delete(_del, place) {
2356
2461
  return { type: "unary", op: "delete", value: place.toIR() } satisfies IRNode;
2357
2462
  },
@@ -2408,13 +2513,6 @@ semantics.addOperation("toIR", {
2408
2513
  return { type: "else", block: block.toIR() as IRNode[] } satisfies IRConditionalElse;
2409
2514
  },
2410
2515
 
2411
- DoExpr(_do, block, _end) {
2412
- const body = block.toIR() as IRNode[];
2413
- if (body.length === 0) return { type: "undefined" } satisfies IRNode;
2414
- if (body.length === 1) return body[0] as IRNode;
2415
- return { type: "program", body } satisfies IRNode;
2416
- },
2417
-
2418
2516
  WhileExpr(_while, condition, _do, block, _end) {
2419
2517
  return {
2420
2518
  type: "while",
@@ -2426,25 +2524,39 @@ semantics.addOperation("toIR", {
2426
2524
  ForExpr(_for, binding, _do, block, _end) {
2427
2525
  return {
2428
2526
  type: "for",
2429
- binding: binding.toIR() as IRBindingOrExpr,
2527
+ binding: binding.toIR() as IRBinding,
2430
2528
  body: block.toIR() as IRNode[],
2431
2529
  } satisfies IRNode;
2432
2530
  },
2433
- BindingExpr(iterOrExpr) {
2434
- const node = iterOrExpr.toIR();
2435
- if (typeof node === "object" && node && "type" in node && String(node.type).startsWith("binding:")) {
2436
- return node as IRBinding;
2437
- }
2438
- return { type: "binding:expr", source: node as IRNode } satisfies IRBindingOrExpr;
2439
- },
2440
2531
 
2441
2532
  Array_empty(_open, _close) {
2442
2533
  return { type: "array", items: [] } satisfies IRNode;
2443
2534
  },
2444
- Array_comprehension(_open, binding, _semi, body, _close) {
2535
+ Array_forComprehension(_open, body, _for, binding, _close) {
2536
+ return {
2537
+ type: "arrayComprehension",
2538
+ binding: binding.toIR() as IRBinding,
2539
+ body: body.toIR(),
2540
+ } satisfies IRNode;
2541
+ },
2542
+ Array_whileComprehension(_open, body, _while, condition, _close) {
2543
+ return {
2544
+ type: "whileArrayComprehension",
2545
+ condition: condition.toIR(),
2546
+ body: body.toIR(),
2547
+ } satisfies IRNode;
2548
+ },
2549
+ Array_inComprehension(_open, body, _in, source, _close) {
2445
2550
  return {
2446
2551
  type: "arrayComprehension",
2447
- binding: binding.toIR() as IRBindingOrExpr,
2552
+ binding: { type: "binding:bareIn", source: source.toIR() } satisfies IRBinding,
2553
+ body: body.toIR(),
2554
+ } satisfies IRNode;
2555
+ },
2556
+ Array_ofComprehension(_open, body, _of, source, _close) {
2557
+ return {
2558
+ type: "arrayComprehension",
2559
+ binding: { type: "binding:bareOf", source: source.toIR() } satisfies IRBinding,
2448
2560
  body: body.toIR(),
2449
2561
  } satisfies IRNode;
2450
2562
  },
@@ -2455,10 +2567,34 @@ semantics.addOperation("toIR", {
2455
2567
  Object_empty(_open, _close) {
2456
2568
  return { type: "object", entries: [] } satisfies IRNode;
2457
2569
  },
2458
- Object_comprehension(_open, binding, _semi, key, _colon, value, _close) {
2570
+ Object_forComprehension(_open, key, _colon, value, _for, binding, _close) {
2459
2571
  return {
2460
2572
  type: "objectComprehension",
2461
- binding: binding.toIR() as IRBindingOrExpr,
2573
+ binding: binding.toIR() as IRBinding,
2574
+ key: key.toIR(),
2575
+ value: value.toIR(),
2576
+ } satisfies IRNode;
2577
+ },
2578
+ Object_whileComprehension(_open, key, _colon, value, _while, condition, _close) {
2579
+ return {
2580
+ type: "whileObjectComprehension",
2581
+ condition: condition.toIR(),
2582
+ key: key.toIR(),
2583
+ value: value.toIR(),
2584
+ } satisfies IRNode;
2585
+ },
2586
+ Object_inComprehension(_open, key, _colon, value, _in, source, _close) {
2587
+ return {
2588
+ type: "objectComprehension",
2589
+ binding: { type: "binding:bareIn", source: source.toIR() } satisfies IRBinding,
2590
+ key: key.toIR(),
2591
+ value: value.toIR(),
2592
+ } satisfies IRNode;
2593
+ },
2594
+ Object_ofComprehension(_open, key, _colon, value, _of, source, _close) {
2595
+ return {
2596
+ type: "objectComprehension",
2597
+ binding: { type: "binding:bareOf", source: source.toIR() } satisfies IRBinding,
2462
2598
  key: key.toIR(),
2463
2599
  value: value.toIR(),
2464
2600
  } satisfies IRNode;
@@ -2492,6 +2628,40 @@ semantics.addOperation("toIR", {
2492
2628
  source: source.toIR(),
2493
2629
  } satisfies IRBinding;
2494
2630
  },
2631
+ IterBinding_bareIn(_in, source) {
2632
+ return {
2633
+ type: "binding:bareIn",
2634
+ source: source.toIR(),
2635
+ } satisfies IRBinding;
2636
+ },
2637
+ IterBinding_bareOf(_of, source) {
2638
+ return {
2639
+ type: "binding:bareOf",
2640
+ source: source.toIR(),
2641
+ } satisfies IRBinding;
2642
+ },
2643
+ IterBindingComprehension_keyValueIn(key, _comma, value, _in, source) {
2644
+ return {
2645
+ type: "binding:keyValueIn",
2646
+ key: key.sourceString,
2647
+ value: value.sourceString,
2648
+ source: source.toIR(),
2649
+ } satisfies IRBinding;
2650
+ },
2651
+ IterBindingComprehension_valueIn(value, _in, source) {
2652
+ return {
2653
+ type: "binding:valueIn",
2654
+ value: value.sourceString,
2655
+ source: source.toIR(),
2656
+ } satisfies IRBinding;
2657
+ },
2658
+ IterBindingComprehension_keyOf(key, _of, source) {
2659
+ return {
2660
+ type: "binding:keyOf",
2661
+ key: key.sourceString,
2662
+ source: source.toIR(),
2663
+ } satisfies IRBinding;
2664
+ },
2495
2665
 
2496
2666
  Pair(key, _colon, value) {
2497
2667
  return { key: key.toIR(), value: value.toIR() };