@oh-my-pi/omptype 17.2.6 → 17.2.7

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/src/compile.ts CHANGED
@@ -11,12 +11,12 @@
11
11
  * arrays and messages are inline literals, so cost is one small allocation
12
12
  * - morphing nodes (defaults, `"+": "delete"`, embedded stepped schemas)
13
13
  * produce a fresh output object; pure subtrees below them stay check-only
14
- * - union members that morph are validated by hoisted interpreter closures;
15
- * pure members compile to inline predicates
14
+ * - morphing union members use separately compiled or hoisted runners; pure
15
+ * members compile to inline predicates
16
16
  */
17
17
  import { MISSING, OmpErrors } from "./errors";
18
- import { unionFail, walk } from "./interp";
19
- import { expectedOf, hasMorph, type IR, type PropIR } from "./ir";
18
+ import { canRefineUnionFailure, unionFail, walk } from "./interp";
19
+ import { expectedOf, hasMorph, type IR, type MorphContext, type PropIR, type TupleIR } from "./ir";
20
20
 
21
21
  const own = Object.prototype.hasOwnProperty;
22
22
 
@@ -43,9 +43,57 @@ function litSource(v: unknown): string | undefined {
43
43
 
44
44
  type PathSeg = { s: PropertyKey } | { d: string };
45
45
 
46
+ type LiteralIR = Extract<IR, { k: "lit" }>;
47
+
48
+ function isPrimitiveLiteral(node: IR): node is LiteralIR {
49
+ return node.k === "lit" && (node.v === null || (typeof node.v !== "object" && typeof node.v !== "function"));
50
+ }
51
+
52
+ /** Whether `undefined` necessarily fails, allowing property-presence checks to be elided. */
53
+ function rejectsUndefined(node: IR): boolean {
54
+ switch (node.k) {
55
+ case "unknown":
56
+ case "undefined":
57
+ case "alias":
58
+ case "sub":
59
+ return false;
60
+ case "lit":
61
+ return node.v !== undefined;
62
+ case "union":
63
+ return node.members.every(rejectsUndefined);
64
+ case "intersection":
65
+ return node.members.some(rejectsUndefined);
66
+ case "refine":
67
+ return rejectsUndefined(node.base);
68
+ case "morph":
69
+ return rejectsUndefined(node.input);
70
+ default:
71
+ return true;
72
+ }
73
+ }
74
+
75
+ class CompiledMorphContext implements MorphContext {
76
+ #path: PropertyKey[] | PropertyKey | undefined;
77
+ #data: unknown;
78
+
79
+ constructor(path: PropertyKey[] | PropertyKey | undefined, data: unknown) {
80
+ this.#path = path;
81
+ this.#data = data;
82
+ }
83
+
84
+ error(expectation: string): OmpErrors {
85
+ return new OmpErrors(this.#path, expectation, this.#data);
86
+ }
87
+
88
+ reject(expectation: string): OmpErrors {
89
+ return this.error(expectation);
90
+ }
91
+ }
92
+
46
93
  class Builder {
47
94
  #lines: string[] = [];
48
95
  #refs: unknown[] = [];
96
+ #activeAliases: Set<IR> | undefined;
49
97
  #id = 0;
50
98
 
51
99
  next(prefix: string): string {
@@ -72,8 +120,22 @@ class Builder {
72
120
  return `[${parts.join(",")}]`;
73
121
  }
74
122
 
123
+ storedPathExpr(segs: PathSeg[]): string {
124
+ if (segs.length === 0) return "undefined";
125
+ if (segs.length === 1) {
126
+ const seg = segs[0];
127
+ return "s" in seg ? JSON.stringify(seg.s) : seg.d;
128
+ }
129
+ const staticParts: PropertyKey[] = [];
130
+ for (const seg of segs) {
131
+ if ("d" in seg) return this.pathExpr(segs);
132
+ staticParts.push(seg.s);
133
+ }
134
+ return this.ref(staticParts);
135
+ }
136
+
75
137
  fail(segs: PathSeg[], expected: string, dataExpr: string): string {
76
- return `return E(${this.pathExpr(segs)},${JSON.stringify(expected)},${dataExpr})`;
138
+ return `return new AE(${this.storedPathExpr(segs)},${JSON.stringify(expected)},${dataExpr})`;
77
139
  }
78
140
 
79
141
  /** Pure boolean predicate for a morph-free subtree. */
@@ -89,10 +151,18 @@ class Builder {
89
151
  return `typeof ${v}==="boolean"`;
90
152
  case "bigint":
91
153
  return `typeof ${v}==="bigint"`;
154
+ case "symbol":
155
+ return `typeof ${v}==="symbol"`;
156
+ case "never":
157
+ return "false";
92
158
  case "anyobject":
93
159
  return `(typeof ${v}==="object"&&${v}!==null)`;
94
160
  case "lit":
95
- return `${v}===${this.lit(node.v)}`;
161
+ return node.v instanceof Date
162
+ ? `(${v} instanceof Date&&${v}.valueOf()===${node.v.valueOf()})`
163
+ : `${v}===${this.lit(node.v)}`;
164
+ case "instance":
165
+ return `${v} instanceof ${this.ref(node.ctor)}`;
96
166
  case "string": {
97
167
  let out = `typeof ${v}==="string"`;
98
168
  if (node.min !== undefined) out += `&&${v}.length>=${node.min}`;
@@ -101,29 +171,34 @@ class Builder {
101
171
  return out;
102
172
  }
103
173
  case "number": {
104
- let out = `typeof ${v}==="number"`;
105
- if (node.int) out += `&&Number.isInteger(${v})`;
174
+ let out = node.int ? `Number.isInteger(${v})` : `Number.isFinite(${v})`;
175
+ if (node.divisor !== undefined) out += `&&${v}%${node.divisor}===0`;
106
176
  if (node.min !== undefined) out += `&&${v}${node.xmin ? ">" : ">="}${node.min}`;
107
177
  if (node.max !== undefined) out += `&&${v}${node.xmax ? "<" : "<="}${node.max}`;
108
178
  return out;
109
179
  }
110
180
  case "union": {
111
- const lits = node.members.filter(m => m.k === "lit");
112
- if (lits.length > 4) {
113
- const set = this.ref(new Set(lits.map(m => m.v)));
114
- const rest = node.members.filter(m => m.k !== "lit");
115
- let out = `${set}.has(${v})`;
181
+ const lits = node.members.filter(isPrimitiveLiteral);
182
+ if (lits.length > 8) {
183
+ const values = this.ref(new Set(lits.map(member => member.v)));
184
+ const literalNodes = new Set<IR>(lits);
185
+ const rest = node.members.filter(member => !literalNodes.has(member));
186
+ let out = `${values}.has(${v})`;
116
187
  for (const m of rest) out += `||(${this.predicate(m, v)})`;
117
188
  return `(${out})`;
118
189
  }
119
190
  return `(${node.members.map(m => `(${this.predicate(m, v)})`).join("||")})`;
120
191
  }
192
+ case "intersection":
193
+ return `(${node.members.map(member => `(${this.predicate(member, v)})`).join("&&")})`;
121
194
  case "array": {
122
- const x = this.next("x");
195
+ const array = this.next("a");
196
+ const index = this.next("i");
123
197
  let out = `Array.isArray(${v})`;
124
198
  if (node.min !== undefined) out += `&&${v}.length>=${node.min}`;
125
199
  if (node.max !== undefined) out += `&&${v}.length<=${node.max}`;
126
- out += `&&${v}.every(${x}=>${this.predicate(node.el, x)})`;
200
+ const item = `${array}[${index}]`;
201
+ out += `&&((${array})=>{for(let ${index}=0;${index}<${array}.length;${index}++)if(!(${this.predicate(node.el, item)}))return false;return true})(${v})`;
127
202
  return out;
128
203
  }
129
204
  case "object": {
@@ -131,10 +206,15 @@ class Builder {
131
206
  for (const p of node.props) {
132
207
  const av = access(v, p.key);
133
208
  const present = `${JSON.stringify(p.key)} in ${v}`;
209
+ const predicate = this.predicate(p.val, av);
134
210
  checks.push(
135
211
  p.opt || p.hasDefault
136
- ? `(!(${present})||(${this.predicate(p.val, av)}))`
137
- : `((${present})&&(${this.predicate(p.val, av)}))`,
212
+ ? rejectsUndefined(p.val)
213
+ ? `((${av}!==undefined&&(${predicate}))||!(${present}))`
214
+ : `(!(${present})||(${predicate}))`
215
+ : rejectsUndefined(p.val)
216
+ ? predicate
217
+ : `((${present})&&(${predicate}))`,
138
218
  );
139
219
  }
140
220
  if (node.index) {
@@ -152,6 +232,8 @@ class Builder {
152
232
  }
153
233
  case "sub":
154
234
  return `!(${this.ref(node.schema.run)}(${v}) instanceof AE)`;
235
+ default:
236
+ return `!(${this.ref(boundWalk(node))}(${v}) instanceof AE)`;
155
237
  }
156
238
  }
157
239
 
@@ -164,8 +246,53 @@ class Builder {
164
246
  return `(${props.map(p => `${keyVar}===${JSON.stringify(p.key)}`).join("||")})`;
165
247
  }
166
248
 
249
+ emitDelegate(node: IR, v: string, segs: PathSeg[], out?: string): void {
250
+ const runner = node.k === "sub" ? node.schema.run : boundWalk(node);
251
+ const result = this.next("r");
252
+ this.push(`const ${result}=${this.ref(runner)}(${v});`);
253
+ this.push(
254
+ segs.length === 0
255
+ ? `if(${result} instanceof AE)return ${result};`
256
+ : `if(${result} instanceof AE)return PF(${result},${this.pathExpr(segs)});`,
257
+ );
258
+ if (out !== undefined) this.push(`${out}=${result};`);
259
+ }
260
+
261
+ emitTupleShape(
262
+ node: TupleIR,
263
+ v: string,
264
+ segs: PathSeg[],
265
+ failureData: string,
266
+ ): { postfixStart: string; prefixCount: string; requiredPrefix: number } {
267
+ this.push(`if(!Array.isArray(${v}))${this.fail(segs, "an array", failureData)};`);
268
+ const requiredPrefix = node.prefix.filter(item => !item.opt && !item.hasDefault).length;
269
+ const minimum = requiredPrefix + node.postfix.length;
270
+ if (minimum > 0) {
271
+ this.push(
272
+ `if(${v}.length<${minimum})${this.fail(segs, `an array of at least length ${minimum}`, failureData)};`,
273
+ );
274
+ }
275
+ if (node.variadic === undefined) {
276
+ const maximum = node.prefix.length + node.postfix.length;
277
+ this.push(
278
+ `if(${v}.length>${maximum})${this.fail(segs, `an array of at most length ${maximum}`, failureData)};`,
279
+ );
280
+ }
281
+ let postfixStart = `${v}.length`;
282
+ if (node.postfix.length > 0) {
283
+ postfixStart = this.next("p");
284
+ this.push(`const ${postfixStart}=${v}.length-${node.postfix.length};`);
285
+ }
286
+ let prefixCount = String(node.prefix.length);
287
+ if (requiredPrefix !== node.prefix.length) {
288
+ prefixCount = this.next("n");
289
+ this.push(`const ${prefixCount}=Math.min(${node.prefix.length},${postfixStart});`);
290
+ }
291
+ return { postfixStart, prefixCount, requiredPrefix };
292
+ }
293
+
167
294
  /** Statement-form check for a morph-free subtree with precise error paths. */
168
- emitCheck(node: IR, v: string, segs: PathSeg[]): void {
295
+ emitCheck(node: IR, v: string, segs: PathSeg[], failureData = v): void {
169
296
  switch (node.k) {
170
297
  case "unknown":
171
298
  return;
@@ -173,7 +300,7 @@ class Builder {
173
300
  let head = `Array.isArray(${v})`;
174
301
  if (node.min !== undefined) head += `&&${v}.length>=${node.min}`;
175
302
  if (node.max !== undefined) head += `&&${v}.length<=${node.max}`;
176
- this.push(`if(!(${head}))${this.fail(segs, expectedOf(node), v)};`);
303
+ this.push(`if(!(${head}))${this.fail(segs, expectedOf(node), failureData)};`);
177
304
  const i = this.next("i");
178
305
  const x = this.next("x");
179
306
  this.push(`for(let ${i}=0;${i}<${v}.length;${i}++){const ${x}=${v}[${i}];`);
@@ -181,9 +308,28 @@ class Builder {
181
308
  this.push("}");
182
309
  return;
183
310
  }
311
+ case "tuple": {
312
+ const { postfixStart, prefixCount, requiredPrefix } = this.emitTupleShape(node, v, segs, failureData);
313
+ for (let index = 0; index < node.prefix.length; index++) {
314
+ if (index >= requiredPrefix) this.push(`if(${index}<${prefixCount}){`);
315
+ this.emitCheck(node.prefix[index].val, `${v}[${index}]`, [...segs, { d: String(index) }]);
316
+ if (index >= requiredPrefix) this.push("}");
317
+ }
318
+ if (node.variadic !== undefined) {
319
+ const index = this.next("i");
320
+ this.push(`for(let ${index}=${prefixCount};${index}<${postfixStart};${index}++){`);
321
+ this.emitCheck(node.variadic, `${v}[${index}]`, [...segs, { d: index }]);
322
+ this.push("}");
323
+ }
324
+ for (let index = 0; index < node.postfix.length; index++) {
325
+ const inputIndex = index === 0 ? postfixStart : `${postfixStart}+${index}`;
326
+ this.emitCheck(node.postfix[index], `${v}[${inputIndex}]`, [...segs, { d: inputIndex }]);
327
+ }
328
+ return;
329
+ }
184
330
  case "object": {
185
331
  this.push(
186
- `if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object", v)};`,
332
+ `if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object", failureData)};`,
187
333
  );
188
334
  for (const p of node.props) {
189
335
  const present = `${JSON.stringify(p.key)} in ${v}`;
@@ -214,17 +360,46 @@ class Builder {
214
360
  }
215
361
  return;
216
362
  }
217
- case "sub": {
218
- const r = this.next("r");
219
- this.push(`const ${r}=${this.ref(node.schema.run)}(${v});`);
220
- this.push(`if(${r} instanceof AE)return PF(${r},${this.pathExpr(segs)});`);
363
+ case "sub":
364
+ this.emitDelegate(node, v, segs);
365
+ return;
366
+ case "union": {
367
+ const failure = node.members.some(canRefineUnionFailure)
368
+ ? `return UF(${this.ref(node)},${failureData},${this.pathExpr(segs)},${JSON.stringify(expectedOf(node))})`
369
+ : this.fail(segs, expectedOf(node), failureData);
370
+ const literals = node.members.filter(isPrimitiveLiteral);
371
+ if (literals.length === node.members.length && literals.length >= 4) {
372
+ const cases = literals.map(member => `case ${this.lit(member.v)}:`).join("");
373
+ this.push(`switch(${v}){${cases}break;default:${failure};}`);
374
+ } else {
375
+ this.push(`if(!(${this.predicate(node, v)}))${failure};`);
376
+ }
221
377
  return;
222
378
  }
223
- case "union":
224
- this.push(`if(!(${this.predicate(node, v)}))return UF(${this.ref(node)},${v},${this.pathExpr(segs)});`);
379
+ case "null":
380
+ case "undefined":
381
+ case "boolean":
382
+ case "bigint":
383
+ case "symbol":
384
+ case "never":
385
+ case "anyobject":
386
+ case "lit":
387
+ case "string":
388
+ case "number":
389
+ case "instance":
390
+ this.push(`if(!(${this.predicate(node, v)}))${this.fail(segs, expectedOf(node), failureData)};`);
391
+ return;
392
+ case "refine": {
393
+ this.emitCheck(node.base, v, segs, failureData);
394
+ const failure = this.fail(segs, node.expected, v);
395
+ this.push(`try{if(!${this.ref(node.pred)}(${v}))${failure};}catch{${failure};}`);
396
+ return;
397
+ }
398
+ case "intersection":
399
+ for (const member of node.members) this.emitCheck(member, v, segs, failureData);
225
400
  return;
226
401
  default:
227
- this.push(`if(!(${this.predicate(node, v)}))${this.fail(segs, expectedOf(node), v)};`);
402
+ this.emitDelegate(node, v, segs);
228
403
  }
229
404
  }
230
405
 
@@ -232,18 +407,45 @@ class Builder {
232
407
  * Validate `v` against a morphing subtree and assign the produced output
233
408
  * to `out` (an already-declared `let`).
234
409
  */
235
- emitProduce(node: IR, v: string, segs: PathSeg[], out: string): void {
410
+ emitProduce(node: IR, v: string, segs: PathSeg[], out: string, failureData = v): void {
236
411
  if (!hasMorph(node)) {
237
- this.emitCheck(node, v, segs);
412
+ this.emitCheck(node, v, segs, failureData);
238
413
  this.push(`${out}=${v};`);
239
414
  return;
240
415
  }
241
416
  switch (node.k) {
242
- case "sub": {
243
- const r = this.next("r");
244
- this.push(`const ${r}=${this.ref(node.schema.run)}(${v});`);
245
- this.push(`if(${r} instanceof AE)return PF(${r},${this.pathExpr(segs)});`);
246
- this.push(`${out}=${r};`);
417
+ case "sub":
418
+ this.emitDelegate(node, v, segs, out);
419
+ return;
420
+ case "morph": {
421
+ const input = this.next("t");
422
+ this.push(`let ${input};`);
423
+ this.emitProduce(node.input, v, segs, input, failureData);
424
+ const context = this.next("c");
425
+ const result = this.next("r");
426
+ this.push(`const ${context}=new MC(${this.storedPathExpr(segs)},${input});`);
427
+ this.push(`const ${result}=${this.ref(node.fn)}(${input},${context});`);
428
+ this.push(`if(${result} instanceof AE)return ${result};`);
429
+ if (node.out === undefined) this.push(`${out}=${result};`);
430
+ else this.emitProduce(node.out, result, segs, out);
431
+ return;
432
+ }
433
+ case "alias": {
434
+ let active = this.#activeAliases;
435
+ if (active === undefined) {
436
+ active = new Set();
437
+ this.#activeAliases = active;
438
+ }
439
+ if (active.has(node)) {
440
+ this.emitDelegate(node, v, segs, out);
441
+ return;
442
+ }
443
+ active.add(node);
444
+ try {
445
+ this.emitProduce(node.resolve(), v, segs, out, failureData);
446
+ } finally {
447
+ active.delete(node);
448
+ }
247
449
  return;
248
450
  }
249
451
  case "union": {
@@ -258,21 +460,23 @@ class Builder {
258
460
  }
259
461
  for (const m of node.members) {
260
462
  if (m.k === "sub" || hasMorph(m)) {
261
- const runner = m.k === "sub" ? m.schema.run : boundWalk(m);
463
+ const runner = m.k === "sub" ? m.schema.run : m.k === "alias" ? boundWalk(m) : compile(m);
262
464
  const r = this.next("r");
263
465
  this.push(`const ${r}=${this.ref(runner)}(${v});`);
264
466
  this.push(`if(!(${r} instanceof AE)){${out}=${r};${ok}=true;break ${label};}`);
265
467
  }
266
468
  }
267
469
  this.push("}");
268
- this.push(`if(!${ok})return UF(${this.ref(node)},${v},${this.pathExpr(segs)});`);
470
+ this.push(
471
+ `if(!${ok})return UF(${this.ref(node)},${failureData},${this.pathExpr(segs)},${JSON.stringify(expectedOf(node))});`,
472
+ );
269
473
  return;
270
474
  }
271
475
  case "array": {
272
476
  let head = `Array.isArray(${v})`;
273
477
  if (node.min !== undefined) head += `&&${v}.length>=${node.min}`;
274
478
  if (node.max !== undefined) head += `&&${v}.length<=${node.max}`;
275
- this.push(`if(!(${head}))${this.fail(segs, expectedOf(node), v)};`);
479
+ this.push(`if(!(${head}))${this.fail(segs, expectedOf(node), failureData)};`);
276
480
  const arr = this.next("a");
277
481
  const i = this.next("i");
278
482
  const x = this.next("x");
@@ -284,12 +488,63 @@ class Builder {
284
488
  this.push(`${out}=${arr};`);
285
489
  return;
286
490
  }
491
+ case "tuple": {
492
+ const { postfixStart, prefixCount, requiredPrefix } = this.emitTupleShape(node, v, segs, failureData);
493
+ const tuple = this.next("a");
494
+ this.push(`const ${tuple}=[...${v}];`);
495
+ for (let index = 0; index < node.prefix.length; index++) {
496
+ const item = node.prefix[index];
497
+ const input = `${v}[${index}]`;
498
+ const output = `${tuple}[${index}]`;
499
+ if (index >= requiredPrefix) this.push(`if(${index}<${prefixCount}){`);
500
+ if (hasMorph(item.val)) this.emitProduce(item.val, input, [...segs, { d: String(index) }], output);
501
+ else {
502
+ this.emitCheck(item.val, input, [...segs, { d: String(index) }]);
503
+ this.push(`${output}=${input};`);
504
+ }
505
+ if (index >= requiredPrefix) {
506
+ if (item.hasDefault) {
507
+ const defaultValue = item.defFactory ? `${this.ref(item.def)}()` : this.lit(item.def);
508
+ this.push(`}else{${output}=${defaultValue};}`);
509
+ } else {
510
+ this.push("}");
511
+ }
512
+ }
513
+ }
514
+ if (node.variadic !== undefined) {
515
+ const index = this.next("i");
516
+ const input = this.next("x");
517
+ this.push(
518
+ `for(let ${index}=${prefixCount};${index}<${postfixStart};${index}++){const ${input}=${v}[${index}];`,
519
+ );
520
+ if (hasMorph(node.variadic)) {
521
+ this.emitProduce(node.variadic, input, [...segs, { d: index }], `${tuple}[${index}]`);
522
+ } else {
523
+ this.emitCheck(node.variadic, input, [...segs, { d: index }]);
524
+ this.push(`${tuple}[${index}]=${input};`);
525
+ }
526
+ this.push("}");
527
+ }
528
+ for (let index = 0; index < node.postfix.length; index++) {
529
+ const inputIndex = index === 0 ? postfixStart : `${postfixStart}+${index}`;
530
+ const input = `${v}[${inputIndex}]`;
531
+ const output = `${tuple}[${inputIndex}]`;
532
+ const item = node.postfix[index];
533
+ if (hasMorph(item)) this.emitProduce(item, input, [...segs, { d: inputIndex }], output);
534
+ else {
535
+ this.emitCheck(item, input, [...segs, { d: inputIndex }]);
536
+ this.push(`${output}=${input};`);
537
+ }
538
+ }
539
+ this.push(`${out}=${tuple};`);
540
+ return;
541
+ }
287
542
  case "object": {
288
543
  this.push(
289
- `if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object", v)};`,
544
+ `if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object", failureData)};`,
290
545
  );
291
546
  const o = this.next("o");
292
- const fresh = node.extras === "delete" && !node.index;
547
+ const fresh = node.extras !== "keep" && !node.index;
293
548
  this.push(fresh ? `const ${o}={};` : `const ${o}={...${v}};`);
294
549
  for (const p of node.props) {
295
550
  const present = `${JSON.stringify(p.key)} in ${v}`;
@@ -303,8 +558,6 @@ class Builder {
303
558
  missing.push(`${ao}=${dflt};`);
304
559
  } else if (!p.opt) {
305
560
  missing.push(`${this.fail(propSegs, expectedOf(p.val), "M")};`);
306
- } else if (fresh) {
307
- // optional + absent under "delete": nothing to copy
308
561
  }
309
562
  this.push(`if(!(${present})){${missing.join("")}}else{`);
310
563
  if (morphChild) {
@@ -343,10 +596,27 @@ class Builder {
343
596
  this.push(`${out}=${o};`);
344
597
  return;
345
598
  }
599
+ case "refine": {
600
+ const refined = this.next("t");
601
+ this.push(`let ${refined};`);
602
+ this.emitProduce(node.base, v, segs, refined, failureData);
603
+ const failure = this.fail(segs, node.expected, refined);
604
+ this.push(`try{if(!${this.ref(node.pred)}(${refined}))${failure};}catch{${failure};}`);
605
+ this.push(`${out}=${refined};`);
606
+ return;
607
+ }
608
+ case "intersection": {
609
+ const current = this.next("t");
610
+ this.push(`let ${current}=${v};`);
611
+ for (const member of node.members) {
612
+ if (hasMorph(member)) this.emitProduce(member, current, segs, current);
613
+ else this.emitCheck(member, current, segs);
614
+ }
615
+ this.push(`${out}=${current};`);
616
+ return;
617
+ }
346
618
  default:
347
- // morph-free kinds are handled by the hasMorph guard above
348
- this.emitCheck(node, v, segs);
349
- this.push(`${out}=${v};`);
619
+ this.emitDelegate(node, v, segs, out);
350
620
  }
351
621
  }
352
622
 
@@ -362,16 +632,99 @@ class Builder {
362
632
  ret = "v";
363
633
  }
364
634
  const src = `return function(v){${this.#lines.join("")}return ${ret}}`;
365
- const make = new Function("R", "E", "AE", "M", "PF", "UF", "own", src) as (
635
+ const make = new Function("R", "AE", "M", "PF", "UF", "MC", "own", src) as (
366
636
  refs: unknown[],
367
- e: typeof OmpErrors.single,
368
637
  ae: typeof OmpErrors,
369
638
  m: typeof MISSING,
370
639
  pf: typeof prefixErrors,
371
640
  uf: typeof unionFail,
641
+ mc: typeof CompiledMorphContext,
372
642
  ownFn: typeof own,
373
643
  ) => (value: unknown) => unknown;
374
- return make(this.#refs, OmpErrors.single, OmpErrors, MISSING, prefixErrors, unionFail, own);
644
+ return make(this.#refs, OmpErrors, MISSING, prefixErrors, unionFail, CompiledMorphContext, own);
645
+ }
646
+
647
+ emitAllows(node: IR, v: string): void {
648
+ switch (node.k) {
649
+ case "array": {
650
+ const array = this.next("a");
651
+ const index = this.next("i");
652
+ this.push(`const ${array}=${v};if(!Array.isArray(${array}))return false;`);
653
+ if (node.min !== undefined) this.push(`if(${array}.length<${node.min})return false;`);
654
+ if (node.max !== undefined) this.push(`if(${array}.length>${node.max})return false;`);
655
+ this.push(`for(let ${index}=0;${index}<${array}.length;${index}++){`);
656
+ this.emitAllows(node.el, `${array}[${index}]`);
657
+ this.push("}");
658
+ return;
659
+ }
660
+ case "object": {
661
+ const object = this.next("o");
662
+ this.push(
663
+ `const ${object}=${v};if(typeof ${object}!=="object"||${object}===null||Array.isArray(${object}))return false;`,
664
+ );
665
+ for (const prop of node.props) {
666
+ const value = this.next("p");
667
+ const present = `${JSON.stringify(prop.key)} in ${object}`;
668
+ this.push(`const ${value}=${access(object, prop.key)};`);
669
+ if (prop.opt || prop.hasDefault) {
670
+ if (rejectsUndefined(prop.val)) {
671
+ this.push(`if(${value}!==undefined){`);
672
+ this.emitAllows(prop.val, value);
673
+ this.push(`}else if(${present})return false;`);
674
+ } else {
675
+ this.push(`if(${present}){`);
676
+ this.emitAllows(prop.val, value);
677
+ this.push("}");
678
+ }
679
+ } else {
680
+ if (!rejectsUndefined(prop.val)) this.push(`if(!(${present}))return false;`);
681
+ this.emitAllows(prop.val, value);
682
+ }
683
+ }
684
+ if (node.index) {
685
+ const key = this.next("k");
686
+ this.push(`for(const ${key} in ${object}){if(!own.call(${object},${key}))continue;`);
687
+ this.emitAllows(node.index, `${object}[${key}]`);
688
+ this.push("}");
689
+ } else if (node.extras === "reject") {
690
+ const key = this.next("k");
691
+ this.push(
692
+ `for(const ${key} in ${object})if(own.call(${object},${key})&&!(${this.declaredCheck(node.props, key)}))return false;`,
693
+ );
694
+ }
695
+ return;
696
+ }
697
+ case "union": {
698
+ const sources: string[] = [];
699
+ for (const member of node.members) {
700
+ if (!isPrimitiveLiteral(member)) break;
701
+ const source = litSource(member.v);
702
+ if (source === undefined) break;
703
+ sources.push(source);
704
+ }
705
+ if (sources.length === node.members.length && sources.length >= 4) {
706
+ this.push(
707
+ `switch(${v}){${sources.map(source => `case ${source}:`).join("")}break;default:return false;}`,
708
+ );
709
+ return;
710
+ }
711
+ this.push(`if(!(${this.predicate(node, v)}))return false;`);
712
+ return;
713
+ }
714
+ default:
715
+ this.push(`if(!(${this.predicate(node, v)}))return false;`);
716
+ }
717
+ }
718
+
719
+ buildAllows(ir: IR): (value: unknown) => boolean {
720
+ this.emitAllows(ir, "v");
721
+ const src = `return function(v){${this.#lines.join("")}return true}`;
722
+ const make = new Function("R", "AE", "own", src) as (
723
+ refs: unknown[],
724
+ ae: typeof OmpErrors,
725
+ ownFn: typeof own,
726
+ ) => (value: unknown) => boolean;
727
+ return make(this.#refs, OmpErrors, own);
375
728
  }
376
729
  }
377
730
 
@@ -386,7 +739,7 @@ interface WalkTagged {
386
739
  [kWalk]?: (value: unknown) => unknown;
387
740
  }
388
741
 
389
- /** Interpreter closure for a morphing union member (rare; hoisted into refs). */
742
+ /** Cached interpreter closure for recursive aliases and predicate-only fallbacks. */
390
743
  function boundWalk(node: IR): (value: unknown) => unknown {
391
744
  const tagged = node as IR & WalkTagged;
392
745
  let fn = tagged[kWalk];
@@ -397,19 +750,44 @@ function boundWalk(node: IR): (value: unknown) => unknown {
397
750
  return fn;
398
751
  }
399
752
 
753
+ function resolvedRoot(ir: IR): IR {
754
+ return ir.k === "alias" ? ir.resolve() : ir;
755
+ }
756
+
757
+ const compiledCache = new WeakMap<IR, (value: unknown) => unknown>();
758
+ const allowsCache = new WeakMap<IR, (value: unknown) => boolean>();
759
+
400
760
  /** Compile `ir` into a specialized validator. */
401
761
  export function compile(ir: IR): (value: unknown) => unknown {
402
- return new Builder().build(ir);
762
+ const root = resolvedRoot(ir);
763
+ let validator = compiledCache.get(root);
764
+ if (validator === undefined) {
765
+ validator = new Builder().build(root);
766
+ compiledCache.set(root, validator);
767
+ }
768
+ return validator;
769
+ }
770
+
771
+ /** Compile `ir` into an allocation-free boolean validator. */
772
+ export function compileAllows(ir: IR): (value: unknown) => boolean {
773
+ const root = resolvedRoot(ir);
774
+ let validator = allowsCache.get(root);
775
+ if (validator === undefined) {
776
+ validator = new Builder().buildAllows(root);
777
+ allowsCache.set(root, validator);
778
+ }
779
+ return validator;
403
780
  }
404
781
 
405
782
  /** Generated source for inspection/debugging. */
406
783
  export function compileToSource(ir: IR): string {
407
- const b = new Builder();
408
- if (hasMorph(ir)) {
409
- b.push("let o;");
410
- b.emitProduce(ir, "v", [], "o");
784
+ const root = resolvedRoot(ir);
785
+ const builder = new Builder();
786
+ if (hasMorph(root)) {
787
+ builder.push("let o;");
788
+ builder.emitProduce(root, "v", [], "o");
411
789
  return `function(v){/* refs elided */return o}`;
412
790
  }
413
- b.emitCheck(ir, "v", []);
791
+ builder.emitCheck(root, "v", []);
414
792
  return `function(v){/* refs elided */return v}`;
415
793
  }