@oh-my-pi/omptype 17.2.7 → 17.2.8

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
@@ -15,17 +15,12 @@
15
15
  * members compile to inline predicates
16
16
  */
17
17
  import { MISSING, OmpErrors } from "./errors";
18
- import { canRefineUnionFailure, unionFail, walk } from "./interp";
18
+ import { canRefineUnionFailure, materializeDefault, unionFail, walk } from "./interp";
19
19
  import { expectedOf, hasMorph, type IR, type MorphContext, type PropIR, type TupleIR } from "./ir";
20
20
 
21
21
  const own = Object.prototype.hasOwnProperty;
22
-
23
22
  const IDENT = /^[A-Za-z_$][\w$]*$/;
24
23
 
25
- function access(base: string, key: string): string {
26
- return IDENT.test(key) ? `${base}.${key}` : `${base}[${JSON.stringify(key)}]`;
27
- }
28
-
29
24
  /** Inline-able literal, else undefined (caller hoists into the refs pool). */
30
25
  function litSource(v: unknown): string | undefined {
31
26
  if (v === null) return "null";
@@ -115,8 +110,12 @@ class Builder {
115
110
  return litSource(v) ?? this.ref(v);
116
111
  }
117
112
 
113
+ access(base: string, key: PropertyKey): string {
114
+ return typeof key === "string" && IDENT.test(key) ? `${base}.${key}` : `${base}[${this.lit(key)}]`;
115
+ }
116
+
118
117
  pathExpr(segs: PathSeg[]): string {
119
- const parts = segs.map(seg => ("s" in seg ? JSON.stringify(seg.s) : seg.d));
118
+ const parts = segs.map(seg => ("s" in seg ? this.lit(seg.s) : seg.d));
120
119
  return `[${parts.join(",")}]`;
121
120
  }
122
121
 
@@ -124,7 +123,7 @@ class Builder {
124
123
  if (segs.length === 0) return "undefined";
125
124
  if (segs.length === 1) {
126
125
  const seg = segs[0];
127
- return "s" in seg ? JSON.stringify(seg.s) : seg.d;
126
+ return "s" in seg ? this.lit(seg.s) : seg.d;
128
127
  }
129
128
  const staticParts: PropertyKey[] = [];
130
129
  for (const seg of segs) {
@@ -134,8 +133,16 @@ class Builder {
134
133
  return this.ref(staticParts);
135
134
  }
136
135
 
136
+ error(segs: PathSeg[], expected: string, dataExpr: string): string {
137
+ return `new AE(${this.storedPathExpr(segs)},${JSON.stringify(expected)},${dataExpr})`;
138
+ }
139
+
137
140
  fail(segs: PathSeg[], expected: string, dataExpr: string): string {
138
- return `return new AE(${this.storedPathExpr(segs)},${JSON.stringify(expected)},${dataExpr})`;
141
+ return `return ${this.error(segs, expected, dataExpr)}`;
142
+ }
143
+
144
+ appendError(errors: string, error: string): string {
145
+ return `if(${errors}===undefined)${errors}=${error};else ${errors}.append(${error});`;
139
146
  }
140
147
 
141
148
  /** Pure boolean predicate for a morph-free subtree. */
@@ -202,10 +209,10 @@ class Builder {
202
209
  return out;
203
210
  }
204
211
  case "object": {
205
- const checks = [`typeof ${v}==="object"`, `${v}!==null`, `!Array.isArray(${v})`];
212
+ const checks = [`typeof ${v}==="object"`, `${v}!==null`];
206
213
  for (const p of node.props) {
207
- const av = access(v, p.key);
208
- const present = `${JSON.stringify(p.key)} in ${v}`;
214
+ const av = this.access(v, p.key);
215
+ const present = `${this.lit(p.key)} in ${v}`;
209
216
  const predicate = this.predicate(p.val, av);
210
217
  checks.push(
211
218
  p.opt || p.hasDefault
@@ -217,17 +224,41 @@ class Builder {
217
224
  : `((${present})&&(${predicate}))`,
218
225
  );
219
226
  }
220
- if (node.index) {
221
- const k = this.next("k");
227
+ const stringKey = this.next("k");
228
+ if (node.index !== undefined) {
222
229
  checks.push(
223
- `(()=>{for(const ${k} in ${v})if(own.call(${v},${k})&&!(${this.predicate(node.index, `${v}[${k}]`)}))return false;return true})()`,
230
+ `(()=>{for(const ${stringKey} in ${v})if(own.call(${v},${stringKey})&&!(${this.predicate(node.index, `${v}[${stringKey}]`)}))return false;return true})()`,
224
231
  );
225
- } else if (node.extras === "reject") {
226
- const k = this.next("k");
232
+ }
233
+ if (node.patternIndexes !== undefined) {
234
+ for (const pattern of node.patternIndexes) {
235
+ checks.push(
236
+ `(()=>{for(const ${stringKey} in ${v})if(own.call(${v},${stringKey})&&(${this.predicate(pattern.key, stringKey)})&&!(${this.predicate(pattern.val, `${v}[${stringKey}]`)}))return false;return true})()`,
237
+ );
238
+ }
239
+ }
240
+ if (node.symbolIndex !== undefined) {
241
+ const symbol = this.next("s");
227
242
  checks.push(
228
- `(()=>{for(const ${k} in ${v})if(own.call(${v},${k})&&!(${this.declaredCheck(node.props, k)}))return false;return true})()`,
243
+ `(()=>{for(const ${symbol} of Object.getOwnPropertySymbols(${v}))if(Object.prototype.propertyIsEnumerable.call(${v},${symbol})&&!(${this.predicate(node.symbolIndex, `${v}[${symbol}]`)}))return false;return true})()`,
229
244
  );
230
245
  }
246
+ if (node.extras === "reject") {
247
+ const patternMatch =
248
+ node.patternIndexes?.map(pattern => `(${this.predicate(pattern.key, stringKey)})`).join("||") ??
249
+ "false";
250
+ if (node.index === undefined) {
251
+ checks.push(
252
+ `(()=>{for(const ${stringKey} in ${v})if(own.call(${v},${stringKey})&&!(${this.declaredCheck(node.props, stringKey)})&&!(${patternMatch}))return false;return true})()`,
253
+ );
254
+ }
255
+ if (node.symbolIndex === undefined) {
256
+ const symbol = this.next("s");
257
+ checks.push(
258
+ `(()=>{for(const ${symbol} of Object.getOwnPropertySymbols(${v}))if(Object.prototype.propertyIsEnumerable.call(${v},${symbol})&&!(${this.declaredCheck(node.props, symbol)}))return false;return true})()`,
259
+ );
260
+ }
261
+ }
231
262
  return `(${checks.join("&&")})`;
232
263
  }
233
264
  case "sub":
@@ -243,139 +274,293 @@ class Builder {
243
274
  const set = this.ref(new Set(props.map(p => p.key)));
244
275
  return `${set}.has(${keyVar})`;
245
276
  }
246
- return `(${props.map(p => `${keyVar}===${JSON.stringify(p.key)}`).join("||")})`;
277
+ return `(${props.map(p => `${keyVar}===${this.lit(p.key)}`).join("||")})`;
247
278
  }
248
279
 
249
- emitDelegate(node: IR, v: string, segs: PathSeg[], out?: string): void {
250
- const runner = node.k === "sub" ? node.schema.run : boundWalk(node);
280
+ /**
281
+ * Run a node through its interpreter/sub-schema runner, appending any
282
+ * failure to `errors`. `brk` (when given) exits the enclosing block on
283
+ * failure so dependent statements (output assignment, morph fns) are
284
+ * skipped. Both runner kinds receive the absolute path so nested step
285
+ * callbacks observe ctx.path; walk-produced errors are already absolute,
286
+ * while sub runners return schema-relative errors that need prefixing.
287
+ */
288
+ emitCollectDelegate(node: IR, v: string, segs: PathSeg[], errors: string, brk?: string, out?: string): void {
289
+ const sub = node.k === "sub";
290
+ const runner = sub ? node.schema.run : boundWalk(node);
251
291
  const result = this.next("r");
252
- this.push(`const ${result}=${this.ref(runner)}(${v});`);
292
+ const args = segs.length > 0 ? `${v},${this.pathExpr(segs)}` : v;
293
+ this.push(`const ${result}=${this.ref(runner)}(${args});`);
294
+ const failure = sub && segs.length > 0 ? `PF(${result},${this.pathExpr(segs)})` : result;
253
295
  this.push(
254
- segs.length === 0
255
- ? `if(${result} instanceof AE)return ${result};`
256
- : `if(${result} instanceof AE)return PF(${result},${this.pathExpr(segs)});`,
296
+ `if(${result} instanceof AE){${this.appendError(errors, failure)}${brk === undefined ? "" : `break ${brk};`}}`,
257
297
  );
258
298
  if (out !== undefined) this.push(`${out}=${result};`);
259
299
  }
260
300
 
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 };
301
+ /** Snapshot the error count so sequencing sites can detect soft failures. */
302
+ markErrors(errors: string): string {
303
+ const mark = this.next("n");
304
+ this.push(`const ${mark}=${errors}===void 0?0:${errors}.length;`);
305
+ return mark;
292
306
  }
293
307
 
294
- /** Statement-form check for a morph-free subtree with precise error paths. */
295
- emitCheck(node: IR, v: string, segs: PathSeg[], failureData = v): void {
308
+ /** Exit `brk` when errors were appended since `mark` (interp's return-on-error). */
309
+ guardGrowth(errors: string, mark: string, brk: string): void {
310
+ this.push(`if((${errors}===void 0?0:${errors}.length)!==${mark})break ${brk};`);
311
+ }
312
+
313
+ /** Aggregate every independent failure in a morph-free subtree. */
314
+ emitCollectCheck(node: IR, v: string, segs: PathSeg[], errors: string, failureData = v): void {
315
+ if (node.cfg !== undefined || node.k === "refine") {
316
+ this.emitCollectDelegate(node, v, segs, errors);
317
+ return;
318
+ }
296
319
  switch (node.k) {
297
320
  case "unknown":
298
321
  return;
299
322
  case "array": {
300
- let head = `Array.isArray(${v})`;
301
- if (node.min !== undefined) head += `&&${v}.length>=${node.min}`;
302
- if (node.max !== undefined) head += `&&${v}.length<=${node.max}`;
303
- this.push(`if(!(${head}))${this.fail(segs, expectedOf(node), failureData)};`);
304
- const i = this.next("i");
305
- const x = this.next("x");
306
- this.push(`for(let ${i}=0;${i}<${v}.length;${i}++){const ${x}=${v}[${i}];`);
307
- this.emitCheck(node.el, x, [...segs, { d: i }]);
308
- this.push("}");
323
+ this.push(
324
+ `if(!Array.isArray(${v})){${this.appendError(errors, this.error(segs, "an array", failureData))}}`,
325
+ );
326
+ if (node.min !== undefined) {
327
+ this.push(
328
+ `else if(${v}.length<${node.min}){${this.appendError(
329
+ errors,
330
+ this.error(segs, `at least length ${node.min}`, `${v}.length`),
331
+ )}}`,
332
+ );
333
+ }
334
+ if (node.max !== undefined) {
335
+ this.push(
336
+ `else if(${v}.length>${node.max}){${this.appendError(
337
+ errors,
338
+ this.error(segs, `at most length ${node.max}`, `${v}.length`),
339
+ )}}`,
340
+ );
341
+ }
342
+ this.push("else{");
343
+ const index = this.next("i");
344
+ this.push(`for(let ${index}=0;${index}<${v}.length;${index}++){`);
345
+ this.emitCollectCheck(node.el, `${v}[${index}]`, [...segs, { d: index }], errors);
346
+ this.push("}}");
309
347
  return;
310
348
  }
311
349
  case "tuple": {
312
- const { postfixStart, prefixCount, requiredPrefix } = this.emitTupleShape(node, v, segs, failureData);
350
+ this.push(
351
+ `if(!Array.isArray(${v})){${this.appendError(errors, this.error(segs, "an array", failureData))}}else{`,
352
+ );
353
+ const requiredPrefix = node.prefix.filter(item => !item.opt && !item.hasDefault).length;
354
+ const minimum = requiredPrefix + node.postfix.length;
355
+ const maximum = node.prefix.length + node.postfix.length;
356
+ if (minimum > 0) {
357
+ this.push(
358
+ `if(${v}.length<${minimum}){${this.appendError(
359
+ errors,
360
+ this.error(segs, `an array of at least length ${minimum}`, failureData),
361
+ )}}else{`,
362
+ );
363
+ }
364
+ if (node.variadic === undefined) {
365
+ this.push(
366
+ `if(${v}.length>${maximum}){${this.appendError(
367
+ errors,
368
+ this.error(segs, `an array of at most length ${maximum}`, failureData),
369
+ )}}else{`,
370
+ );
371
+ }
372
+ let postfixStart = `${v}.length`;
373
+ if (node.postfix.length > 0) {
374
+ postfixStart = this.next("p");
375
+ this.push(`const ${postfixStart}=${v}.length-${node.postfix.length};`);
376
+ }
377
+ let prefixCount = String(node.prefix.length);
378
+ if (requiredPrefix !== node.prefix.length) {
379
+ prefixCount = this.next("n");
380
+ this.push(`const ${prefixCount}=Math.min(${node.prefix.length},${postfixStart});`);
381
+ }
313
382
  for (let index = 0; index < node.prefix.length; index++) {
314
383
  if (index >= requiredPrefix) this.push(`if(${index}<${prefixCount}){`);
315
- this.emitCheck(node.prefix[index].val, `${v}[${index}]`, [...segs, { d: String(index) }]);
384
+ this.emitCollectCheck(node.prefix[index].val, `${v}[${index}]`, [...segs, { d: String(index) }], errors);
316
385
  if (index >= requiredPrefix) this.push("}");
317
386
  }
318
387
  if (node.variadic !== undefined) {
319
388
  const index = this.next("i");
320
389
  this.push(`for(let ${index}=${prefixCount};${index}<${postfixStart};${index}++){`);
321
- this.emitCheck(node.variadic, `${v}[${index}]`, [...segs, { d: index }]);
390
+ this.emitCollectCheck(node.variadic, `${v}[${index}]`, [...segs, { d: index }], errors);
322
391
  this.push("}");
323
392
  }
324
393
  for (let index = 0; index < node.postfix.length; index++) {
325
394
  const inputIndex = index === 0 ? postfixStart : `${postfixStart}+${index}`;
326
- this.emitCheck(node.postfix[index], `${v}[${inputIndex}]`, [...segs, { d: inputIndex }]);
395
+ this.emitCollectCheck(node.postfix[index], `${v}[${inputIndex}]`, [...segs, { d: inputIndex }], errors);
327
396
  }
397
+ if (node.variadic === undefined) this.push("}");
398
+ if (minimum > 0) this.push("}");
399
+ this.push("}");
328
400
  return;
329
401
  }
330
402
  case "object": {
403
+ if (
404
+ node.patternIndexes !== undefined ||
405
+ node.symbolIndex !== undefined ||
406
+ node.props.some(prop => typeof prop.key === "symbol")
407
+ ) {
408
+ this.emitCollectDelegate(node, v, segs, errors);
409
+ return;
410
+ }
331
411
  this.push(
332
- `if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object", failureData)};`,
412
+ `if(typeof ${v}!=="object"||${v}===null){${this.appendError(
413
+ errors,
414
+ this.error(segs, "an object", failureData),
415
+ )}}else{`,
333
416
  );
334
- for (const p of node.props) {
335
- const present = `${JSON.stringify(p.key)} in ${v}`;
336
- const propSegs: PathSeg[] = [...segs, { s: p.key }];
337
- if (p.opt || p.hasDefault) {
417
+ for (const prop of node.props) {
418
+ const present = `${this.lit(prop.key)} in ${v}`;
419
+ const propSegs: PathSeg[] = [...segs, { s: prop.key }];
420
+ if (prop.opt || prop.hasDefault) {
338
421
  this.push(`if(${present}){`);
339
- this.emitCheck(p.val, access(v, p.key), propSegs);
422
+ this.emitCollectCheck(prop.val, this.access(v, prop.key), propSegs, errors);
340
423
  this.push("}");
341
424
  } else {
342
- this.push(`if(!(${present}))${this.fail(propSegs, expectedOf(p.val), "M")};`);
343
- this.emitCheck(p.val, access(v, p.key), propSegs);
425
+ this.push(
426
+ `if(!(${present})){${this.appendError(
427
+ errors,
428
+ this.error(propSegs, expectedOf(prop.val), "M"),
429
+ )}}else{`,
430
+ );
431
+ this.emitCollectCheck(prop.val, this.access(v, prop.key), propSegs, errors);
432
+ this.push("}");
344
433
  }
345
434
  }
346
- if (node.index) {
347
- const k = this.next("k");
348
- this.push(`for(const ${k} in ${v})if(own.call(${v},${k})){`);
349
- this.emitCheck(node.index, `${v}[${k}]`, [...segs, { d: k }]);
435
+ if (node.index !== undefined) {
436
+ const key = this.next("k");
437
+ this.push(`for(const ${key} in ${v})if(own.call(${v},${key})){`);
438
+ this.emitCollectCheck(node.index, `${v}[${key}]`, [...segs, { d: key }], errors);
350
439
  this.push("}");
351
440
  } else if (node.extras === "reject") {
352
- const k = this.next("k");
441
+ const key = this.next("k");
353
442
  this.push(
354
- `for(const ${k} in ${v})if(own.call(${v},${k})&&!(${this.declaredCheck(node.props, k)}))${this.fail(
355
- [...segs, { d: k }],
356
- "removed (undeclared key)",
357
- `${v}[${k}]`,
358
- )};`,
443
+ `for(const ${key} in ${v})if(own.call(${v},${key})&&!(${this.declaredCheck(node.props, key)})){`,
359
444
  );
445
+ this.push(this.appendError(errors, this.error([...segs, { d: key }], "removed", `${v}[${key}]`)));
446
+ this.push("}");
360
447
  }
448
+ if (node.extras === "reject") {
449
+ const symbol = this.next("s");
450
+ this.push(
451
+ `for(const ${symbol} of Object.getOwnPropertySymbols(${v}))if(Object.prototype.propertyIsEnumerable.call(${v},${symbol})&&!(${this.declaredCheck(node.props, symbol)})){${this.appendError(errors, this.error([...segs, { d: symbol }], "removed", `${v}[${symbol}]`))}}`,
452
+ );
453
+ }
454
+ this.push("}");
361
455
  return;
362
456
  }
363
- case "sub":
364
- this.emitDelegate(node, v, segs);
365
- return;
366
457
  case "union": {
367
458
  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};`);
459
+ ? `UF(${this.ref(node)},${failureData},${this.pathExpr(segs)},${JSON.stringify(expectedOf(node))})`
460
+ : this.error(segs, expectedOf(node), failureData);
461
+ this.push(`if(!(${this.predicate(node, v)})){${this.appendError(errors, failure)}}`);
462
+ return;
463
+ }
464
+ case "string": {
465
+ this.push(
466
+ `if(typeof ${v}!=="string"){${this.appendError(errors, this.error(segs, "a string", failureData))}}`,
467
+ );
468
+ if (node.min !== undefined) {
469
+ this.push(
470
+ `else if(${v}.length<${node.min}){${this.appendError(
471
+ errors,
472
+ this.error(segs, `at least length ${node.min}`, `${v}.length`),
473
+ )}}`,
474
+ );
475
+ }
476
+ if (node.max !== undefined) {
477
+ this.push(
478
+ `else if(${v}.length>${node.max}){${this.appendError(
479
+ errors,
480
+ this.error(segs, `at most length ${node.max}`, `${v}.length`),
481
+ )}}`,
482
+ );
483
+ }
484
+ if (node.url) {
485
+ this.push(
486
+ `else if(!URL.canParse(${v})){${this.appendError(
487
+ errors,
488
+ this.error(segs, "a URL string", failureData),
489
+ )}}`,
490
+ );
376
491
  }
377
492
  return;
378
493
  }
494
+ case "number": {
495
+ this.push(
496
+ `if(typeof ${v}!=="number"||!Number.isFinite(${v})){${this.appendError(
497
+ errors,
498
+ this.error(segs, node.int ? "an integer" : "a number", failureData),
499
+ )}}else{`,
500
+ );
501
+ if (node.int) {
502
+ this.push(
503
+ `if(!Number.isInteger(${v})){${this.appendError(errors, this.error(segs, "an integer", failureData))}}`,
504
+ );
505
+ }
506
+ if (node.divisor !== undefined) {
507
+ this.push(
508
+ `if(${v}%${node.divisor}!==0){${this.appendError(
509
+ errors,
510
+ this.error(segs, `a number divisible by ${node.divisor}`, failureData),
511
+ )}}`,
512
+ );
513
+ }
514
+ if (node.min !== undefined) {
515
+ const expected =
516
+ node.min === 0
517
+ ? node.xmin
518
+ ? "positive"
519
+ : "non-negative"
520
+ : `a number ${node.xmin ? "more than" : "at least"} ${node.min}`;
521
+ this.push(
522
+ `if(${v}${node.xmin ? "<=" : "<"}${node.min}){${this.appendError(
523
+ errors,
524
+ this.error(segs, expected, failureData),
525
+ )}}`,
526
+ );
527
+ }
528
+ if (node.max !== undefined) {
529
+ const expected =
530
+ node.max === 0
531
+ ? node.xmax
532
+ ? "negative"
533
+ : "non-positive"
534
+ : `a number ${node.xmax ? "less than" : "at most"} ${node.max}`;
535
+ this.push(
536
+ `if(${v}${node.xmax ? ">=" : ">"}${node.max}){${this.appendError(
537
+ errors,
538
+ this.error(segs, expected, failureData),
539
+ )}}`,
540
+ );
541
+ }
542
+ this.push("}");
543
+ return;
544
+ }
545
+ case "lit":
546
+ if (
547
+ (node.v !== null && typeof node.v === "object" && !(node.v instanceof Date)) ||
548
+ typeof node.v === "function"
549
+ ) {
550
+ this.emitCollectDelegate(node, v, segs, errors);
551
+ } else {
552
+ this.push(
553
+ `if(!(${this.predicate(node, v)})){${this.appendError(
554
+ errors,
555
+ this.error(segs, expectedOf(node), failureData),
556
+ )}}`,
557
+ );
558
+ }
559
+ return;
560
+ case "intersection":
561
+ case "sub":
562
+ this.emitCollectDelegate(node, v, segs, errors);
563
+ return;
379
564
  case "null":
380
565
  case "undefined":
381
566
  case "boolean":
@@ -383,51 +568,119 @@ class Builder {
383
568
  case "symbol":
384
569
  case "never":
385
570
  case "anyobject":
386
- case "lit":
387
- case "string":
388
- case "number":
389
571
  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);
572
+ this.push(
573
+ `if(!(${this.predicate(node, v)})){${this.appendError(
574
+ errors,
575
+ this.error(segs, expectedOf(node), failureData),
576
+ )}}`,
577
+ );
400
578
  return;
401
579
  default:
402
- this.emitDelegate(node, v, segs);
580
+ this.emitCollectDelegate(node, v, segs, errors);
581
+ }
582
+ }
583
+
584
+ emitTupleShape(
585
+ node: TupleIR,
586
+ v: string,
587
+ segs: PathSeg[],
588
+ errors: string,
589
+ brk: string,
590
+ failureData: string,
591
+ ): { postfixStart: string; prefixCount: string; requiredPrefix: number } {
592
+ this.push(
593
+ `if(!Array.isArray(${v})){${this.appendError(errors, this.error(segs, "an array", failureData))}break ${brk};}`,
594
+ );
595
+ const requiredPrefix = node.prefix.filter(item => !item.opt && !item.hasDefault).length;
596
+ const minimum = requiredPrefix + node.postfix.length;
597
+ if (minimum > 0) {
598
+ this.push(
599
+ `if(${v}.length<${minimum}){${this.appendError(
600
+ errors,
601
+ this.error(segs, `an array of at least length ${minimum}`, failureData),
602
+ )}break ${brk};}`,
603
+ );
604
+ }
605
+ if (node.variadic === undefined) {
606
+ const maximum = node.prefix.length + node.postfix.length;
607
+ this.push(
608
+ `if(${v}.length>${maximum}){${this.appendError(
609
+ errors,
610
+ this.error(segs, `an array of at most length ${maximum}`, failureData),
611
+ )}break ${brk};}`,
612
+ );
613
+ }
614
+ let postfixStart = `${v}.length`;
615
+ if (node.postfix.length > 0) {
616
+ postfixStart = this.next("p");
617
+ this.push(`const ${postfixStart}=${v}.length-${node.postfix.length};`);
618
+ }
619
+ let prefixCount = String(node.prefix.length);
620
+ if (requiredPrefix !== node.prefix.length) {
621
+ prefixCount = this.next("n");
622
+ this.push(`const ${prefixCount}=Math.min(${node.prefix.length},${postfixStart});`);
623
+ }
624
+ return { postfixStart, prefixCount, requiredPrefix };
625
+ }
626
+
627
+ /** Fill `target` with a validated default (factory output revalidated per call). */
628
+ emitDefaultFill(val: IR, def: unknown, isFactory: boolean, target: string, segs: PathSeg[], errors: string): void {
629
+ if (isFactory && typeof def === "function") {
630
+ const candidate = this.next("d");
631
+ const resolved = this.next("t");
632
+ const label = this.next("L");
633
+ this.push(`const ${candidate}=${this.ref(def)}();let ${resolved};${label}:{`);
634
+ this.emitCollectProduce(val, candidate, segs, resolved, errors, label);
635
+ this.push(`${target}=${resolved};}`);
636
+ } else {
637
+ // Static defaults were prevalidated at construction; MD clones
638
+ // mutable payloads so callers cannot alias the schema's copy.
639
+ this.push(`${target}=${litSource(def) ?? `MD(${this.ref(def)})`};`);
403
640
  }
404
641
  }
405
642
 
406
643
  /**
407
644
  * Validate `v` against a morphing subtree and assign the produced output
408
- * to `out` (an already-declared `let`).
645
+ * to `out` (an already-declared `let`). Failures append to `errors` and
646
+ * `break ${brk}` (skipping the output assignment), mirroring interp: an
647
+ * error in one child never suppresses sibling validation or morphs.
409
648
  */
410
- emitProduce(node: IR, v: string, segs: PathSeg[], out: string, failureData = v): void {
649
+ emitCollectProduce(
650
+ node: IR,
651
+ v: string,
652
+ segs: PathSeg[],
653
+ out: string,
654
+ errors: string,
655
+ brk: string,
656
+ failureData = v,
657
+ ): void {
658
+ if (node.cfg !== undefined || node.k === "refine") {
659
+ this.emitCollectDelegate(node, v, segs, errors, brk, out);
660
+ return;
661
+ }
411
662
  if (!hasMorph(node)) {
412
- this.emitCheck(node, v, segs, failureData);
663
+ this.emitCollectCheck(node, v, segs, errors, failureData);
413
664
  this.push(`${out}=${v};`);
414
665
  return;
415
666
  }
416
667
  switch (node.k) {
417
668
  case "sub":
418
- this.emitDelegate(node, v, segs, out);
669
+ this.emitCollectDelegate(node, v, segs, errors, brk, out);
419
670
  return;
420
671
  case "morph": {
421
672
  const input = this.next("t");
422
673
  this.push(`let ${input};`);
423
- this.emitProduce(node.input, v, segs, input, failureData);
674
+ const mark = this.markErrors(errors);
675
+ this.emitCollectProduce(node.input, v, segs, input, errors, brk, failureData);
676
+ this.guardGrowth(errors, mark, brk);
424
677
  const context = this.next("c");
425
678
  const result = this.next("r");
426
679
  this.push(`const ${context}=new MC(${this.storedPathExpr(segs)},${input});`);
427
680
  this.push(`const ${result}=${this.ref(node.fn)}(${input},${context});`);
428
- this.push(`if(${result} instanceof AE)return ${result};`);
681
+ this.push(`if(${result} instanceof AE){${this.appendError(errors, result)}break ${brk};}`);
429
682
  if (node.out === undefined) this.push(`${out}=${result};`);
430
- else this.emitProduce(node.out, result, segs, out);
683
+ else this.emitCollectProduce(node.out, result, segs, out, errors, brk);
431
684
  return;
432
685
  }
433
686
  case "alias": {
@@ -437,12 +690,12 @@ class Builder {
437
690
  this.#activeAliases = active;
438
691
  }
439
692
  if (active.has(node)) {
440
- this.emitDelegate(node, v, segs, out);
693
+ this.emitCollectDelegate(node, v, segs, errors, brk, out);
441
694
  return;
442
695
  }
443
696
  active.add(node);
444
697
  try {
445
- this.emitProduce(node.resolve(), v, segs, out, failureData);
698
+ this.emitCollectProduce(node.resolve(), v, segs, out, errors, brk, failureData);
446
699
  } finally {
447
700
  active.delete(node);
448
701
  }
@@ -467,45 +720,77 @@ class Builder {
467
720
  }
468
721
  }
469
722
  this.push("}");
470
- this.push(
471
- `if(!${ok})return UF(${this.ref(node)},${failureData},${this.pathExpr(segs)},${JSON.stringify(expectedOf(node))});`,
472
- );
723
+ const failure = `UF(${this.ref(node)},${failureData},${this.pathExpr(segs)},${JSON.stringify(expectedOf(node))})`;
724
+ this.push(`if(!${ok}){${this.appendError(errors, failure)}break ${brk};}`);
473
725
  return;
474
726
  }
475
727
  case "array": {
476
- let head = `Array.isArray(${v})`;
477
- if (node.min !== undefined) head += `&&${v}.length>=${node.min}`;
478
- if (node.max !== undefined) head += `&&${v}.length<=${node.max}`;
479
- this.push(`if(!(${head}))${this.fail(segs, expectedOf(node), failureData)};`);
480
- const arr = this.next("a");
481
- const i = this.next("i");
482
- const x = this.next("x");
483
- const el = this.next("t");
484
- this.push(`const ${arr}=new Array(${v}.length);`);
485
- this.push(`for(let ${i}=0;${i}<${v}.length;${i}++){const ${x}=${v}[${i}];let ${el};`);
486
- this.emitProduce(node.el, x, [...segs, { d: i }], el);
487
- this.push(`${arr}[${i}]=${el};}`);
488
- this.push(`${out}=${arr};`);
728
+ this.push(
729
+ `if(!Array.isArray(${v})){${this.appendError(errors, this.error(segs, "an array", failureData))}break ${brk};}`,
730
+ );
731
+ if (node.min !== undefined) {
732
+ this.push(
733
+ `if(${v}.length<${node.min}){${this.appendError(
734
+ errors,
735
+ this.error(segs, `at least length ${node.min}`, `${v}.length`),
736
+ )}break ${brk};}`,
737
+ );
738
+ }
739
+ if (node.max !== undefined) {
740
+ this.push(
741
+ `if(${v}.length>${node.max}){${this.appendError(
742
+ errors,
743
+ this.error(segs, `at most length ${node.max}`, `${v}.length`),
744
+ )}break ${brk};}`,
745
+ );
746
+ }
747
+ const array = this.next("a");
748
+ const index = this.next("i");
749
+ const input = this.next("x");
750
+ const element = this.next("t");
751
+ const label = this.next("L");
752
+ this.push(`const ${array}=new Array(${v}.length);`);
753
+ this.push(
754
+ `for(let ${index}=0;${index}<${v}.length;${index}++){const ${input}=${v}[${index}];let ${element};${label}:{`,
755
+ );
756
+ this.emitCollectProduce(node.el, input, [...segs, { d: index }], element, errors, label);
757
+ this.push(`${array}[${index}]=${element};}}`);
758
+ this.push(`${out}=${array};`);
489
759
  return;
490
760
  }
491
761
  case "tuple": {
492
- const { postfixStart, prefixCount, requiredPrefix } = this.emitTupleShape(node, v, segs, failureData);
762
+ const { postfixStart, prefixCount, requiredPrefix } = this.emitTupleShape(
763
+ node,
764
+ v,
765
+ segs,
766
+ errors,
767
+ brk,
768
+ failureData,
769
+ );
493
770
  const tuple = this.next("a");
494
771
  this.push(`const ${tuple}=[...${v}];`);
495
772
  for (let index = 0; index < node.prefix.length; index++) {
496
773
  const item = node.prefix[index];
774
+ const itemSegs: PathSeg[] = [...segs, { s: index }];
497
775
  const input = `${v}[${index}]`;
498
776
  const output = `${tuple}[${index}]`;
777
+ const label = this.next("L");
499
778
  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};`);
779
+ this.push(`${label}:{`);
780
+ if (hasMorph(item.val)) {
781
+ const temporary = this.next("t");
782
+ this.push(`let ${temporary};`);
783
+ this.emitCollectProduce(item.val, input, itemSegs, temporary, errors, label);
784
+ this.push(`${output}=${temporary};`);
785
+ } else {
786
+ this.emitCollectCheck(item.val, input, itemSegs, errors);
504
787
  }
788
+ this.push("}");
505
789
  if (index >= requiredPrefix) {
506
790
  if (item.hasDefault) {
507
- const defaultValue = item.defFactory ? `${this.ref(item.def)}()` : this.lit(item.def);
508
- this.push(`}else{${output}=${defaultValue};}`);
791
+ this.push("}else{");
792
+ this.emitDefaultFill(item.val, item.def, item.defFactory === true, output, itemSegs, errors);
793
+ this.push("}");
509
794
  } else {
510
795
  this.push("}");
511
796
  }
@@ -514,125 +799,147 @@ class Builder {
514
799
  if (node.variadic !== undefined) {
515
800
  const index = this.next("i");
516
801
  const input = this.next("x");
802
+ const label = this.next("L");
517
803
  this.push(
518
- `for(let ${index}=${prefixCount};${index}<${postfixStart};${index}++){const ${input}=${v}[${index}];`,
804
+ `for(let ${index}=${prefixCount};${index}<${postfixStart};${index}++){const ${input}=${v}[${index}];${label}:{`,
519
805
  );
520
806
  if (hasMorph(node.variadic)) {
521
- this.emitProduce(node.variadic, input, [...segs, { d: index }], `${tuple}[${index}]`);
807
+ const temporary = this.next("t");
808
+ this.push(`let ${temporary};`);
809
+ this.emitCollectProduce(node.variadic, input, [...segs, { d: index }], temporary, errors, label);
810
+ this.push(`${tuple}[${index}]=${temporary};`);
522
811
  } else {
523
- this.emitCheck(node.variadic, input, [...segs, { d: index }]);
524
- this.push(`${tuple}[${index}]=${input};`);
812
+ this.emitCollectCheck(node.variadic, input, [...segs, { d: index }], errors);
525
813
  }
526
- this.push("}");
814
+ this.push("}}");
527
815
  }
528
816
  for (let index = 0; index < node.postfix.length; index++) {
529
817
  const inputIndex = index === 0 ? postfixStart : `${postfixStart}+${index}`;
530
818
  const input = `${v}[${inputIndex}]`;
531
- const output = `${tuple}[${inputIndex}]`;
532
819
  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};`);
820
+ const label = this.next("L");
821
+ this.push(`${label}:{`);
822
+ if (hasMorph(item)) {
823
+ const temporary = this.next("t");
824
+ this.push(`let ${temporary};`);
825
+ this.emitCollectProduce(item, input, [...segs, { d: inputIndex }], temporary, errors, label);
826
+ this.push(`${tuple}[${inputIndex}]=${temporary};`);
827
+ } else {
828
+ this.emitCollectCheck(item, input, [...segs, { d: inputIndex }], errors);
537
829
  }
830
+ this.push("}");
538
831
  }
539
832
  this.push(`${out}=${tuple};`);
540
833
  return;
541
834
  }
542
835
  case "object": {
836
+ if (
837
+ node.patternIndexes !== undefined ||
838
+ node.symbolIndex !== undefined ||
839
+ node.props.some(prop => typeof prop.key === "symbol")
840
+ ) {
841
+ this.emitCollectDelegate(node, v, segs, errors, brk, out);
842
+ return;
843
+ }
543
844
  this.push(
544
- `if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object", failureData)};`,
845
+ `if(typeof ${v}!=="object"||${v}===null){${this.appendError(
846
+ errors,
847
+ this.error(segs, "an object", failureData),
848
+ )}break ${brk};}`,
545
849
  );
546
- const o = this.next("o");
547
- const fresh = node.extras !== "keep" && !node.index;
548
- this.push(fresh ? `const ${o}={};` : `const ${o}={...${v}};`);
549
- for (const p of node.props) {
550
- const present = `${JSON.stringify(p.key)} in ${v}`;
551
- const propSegs: PathSeg[] = [...segs, { s: p.key }];
552
- const av = access(v, p.key);
553
- const ao = access(o, p.key);
554
- const morphChild = hasMorph(p.val);
555
- const missing: string[] = [];
556
- if (p.hasDefault) {
557
- const dflt = p.defFactory ? `${this.ref(p.def)}()` : this.lit(p.def);
558
- missing.push(`${ao}=${dflt};`);
559
- } else if (!p.opt) {
560
- missing.push(`${this.fail(propSegs, expectedOf(p.val), "M")};`);
850
+ const object = this.next("o");
851
+ const fresh = node.extras === "delete" && node.index === undefined;
852
+ this.push(fresh ? `const ${object}={};` : `const ${object}={...${v}};`);
853
+ for (const prop of node.props) {
854
+ const present = `${this.lit(prop.key)} in ${v}`;
855
+ const propSegs: PathSeg[] = [...segs, { s: prop.key }];
856
+ const input = this.access(v, prop.key);
857
+ const output = this.access(object, prop.key);
858
+ const label = this.next("L");
859
+ this.push(`if(!(${present})){`);
860
+ if (prop.hasDefault) {
861
+ this.emitDefaultFill(prop.val, prop.def, prop.defFactory === true, output, propSegs, errors);
862
+ } else if (!prop.opt) {
863
+ this.push(this.appendError(errors, this.error(propSegs, expectedOf(prop.val), "M")));
561
864
  }
562
- this.push(`if(!(${present})){${missing.join("")}}else{`);
563
- if (morphChild) {
564
- const t = this.next("t");
565
- this.push(`let ${t};`);
566
- this.emitProduce(p.val, av, propSegs, t);
567
- this.push(`${ao}=${t};`);
865
+ this.push(`}else{${label}:{`);
866
+ if (hasMorph(prop.val)) {
867
+ const temporary = this.next("t");
868
+ this.push(`let ${temporary};`);
869
+ this.emitCollectProduce(prop.val, input, propSegs, temporary, errors, label);
870
+ this.push(`${output}=${temporary};`);
568
871
  } else {
569
- this.emitCheck(p.val, av, propSegs);
570
- if (fresh) this.push(`${ao}=${av};`);
872
+ this.emitCollectCheck(prop.val, input, propSegs, errors);
873
+ if (fresh) this.push(`${output}=${input};`);
571
874
  }
572
- this.push("}");
875
+ this.push("}}");
573
876
  }
574
- if (node.index) {
575
- const k = this.next("k");
576
- this.push(`for(const ${k} in ${v})if(own.call(${v},${k})){`);
877
+ if (node.index !== undefined) {
878
+ const key = this.next("k");
879
+ const label = this.next("L");
880
+ this.push(`for(const ${key} in ${v})if(own.call(${v},${key})){${label}:{`);
577
881
  if (hasMorph(node.index)) {
578
- const t = this.next("t");
579
- this.push(`let ${t};`);
580
- this.emitProduce(node.index, `${v}[${k}]`, [...segs, { d: k }], t);
581
- this.push(`${o}[${k}]=${t};`);
882
+ const temporary = this.next("t");
883
+ this.push(`let ${temporary};`);
884
+ this.emitCollectProduce(node.index, `${v}[${key}]`, [...segs, { d: key }], temporary, errors, label);
885
+ this.push(`${object}[${key}]=${temporary};`);
582
886
  } else {
583
- this.emitCheck(node.index, `${v}[${k}]`, [...segs, { d: k }]);
887
+ this.emitCollectCheck(node.index, `${v}[${key}]`, [...segs, { d: key }], errors);
584
888
  }
585
- this.push("}");
889
+ this.push("}}");
586
890
  } else if (node.extras === "reject") {
587
- const k = this.next("k");
891
+ const key = this.next("k");
588
892
  this.push(
589
- `for(const ${k} in ${v})if(own.call(${v},${k})&&!(${this.declaredCheck(node.props, k)}))${this.fail(
590
- [...segs, { d: k }],
591
- "removed (undeclared key)",
592
- `${v}[${k}]`,
593
- )};`,
893
+ `for(const ${key} in ${v})if(own.call(${v},${key})&&!(${this.declaredCheck(node.props, key)})){${this.appendError(
894
+ errors,
895
+ this.error([...segs, { d: key }], "removed", `${v}[${key}]`),
896
+ )}}`,
594
897
  );
595
898
  }
596
- this.push(`${out}=${o};`);
597
- return;
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};`);
899
+ if (node.extras === "reject") {
900
+ const symbol = this.next("s");
901
+ this.push(
902
+ `for(const ${symbol} of Object.getOwnPropertySymbols(${v}))if(Object.prototype.propertyIsEnumerable.call(${v},${symbol})&&!(${this.declaredCheck(node.props, symbol)})){${this.appendError(errors, this.error([...segs, { d: symbol }], "removed", `${v}[${symbol}]`))}}`,
903
+ );
904
+ }
905
+ this.push(`${out}=${object};`);
606
906
  return;
607
907
  }
608
908
  case "intersection": {
609
909
  const current = this.next("t");
610
910
  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);
911
+ const mark = this.markErrors(errors);
912
+ for (let index = 0; index < node.members.length; index++) {
913
+ if (index > 0) this.guardGrowth(errors, mark, brk);
914
+ const member = node.members[index];
915
+ if (hasMorph(member)) this.emitCollectProduce(member, current, segs, current, errors, brk);
916
+ else this.emitCollectCheck(member, current, segs, errors);
614
917
  }
615
918
  this.push(`${out}=${current};`);
616
919
  return;
617
920
  }
618
921
  default:
619
- this.emitDelegate(node, v, segs, out);
922
+ this.emitCollectDelegate(node, v, segs, errors, brk, out);
620
923
  }
621
924
  }
622
925
 
623
926
  build(ir: IR): (value: unknown) => unknown {
927
+ const errors = this.next("e");
624
928
  let ret: string;
625
929
  if (hasMorph(ir)) {
626
- this.push("let o;");
627
- // body emitted below needs `o` declared first, so splice ordering:
628
- this.emitProduce(ir, "v", [], "o");
930
+ const label = this.next("L");
931
+ this.push(`let ${errors};let o;${label}:{`);
932
+ this.emitCollectProduce(ir, "v", [], "o", errors, label);
933
+ this.push("}");
629
934
  ret = "o";
630
935
  } else {
631
- this.emitCheck(ir, "v", []);
936
+ this.push(`let ${errors};`);
937
+ this.emitCollectCheck(ir, "v", [], errors);
632
938
  ret = "v";
633
939
  }
940
+ this.push(`if(${errors}!==undefined)return ${errors};`);
634
941
  const src = `return function(v){${this.#lines.join("")}return ${ret}}`;
635
- const make = new Function("R", "AE", "M", "PF", "UF", "MC", "own", src) as (
942
+ const make = new Function("R", "AE", "M", "PF", "UF", "MC", "own", "MD", src) as (
636
943
  refs: unknown[],
637
944
  ae: typeof OmpErrors,
638
945
  m: typeof MISSING,
@@ -640,8 +947,18 @@ class Builder {
640
947
  uf: typeof unionFail,
641
948
  mc: typeof CompiledMorphContext,
642
949
  ownFn: typeof own,
950
+ md: typeof materializeDefault,
643
951
  ) => (value: unknown) => unknown;
644
- return make(this.#refs, OmpErrors, MISSING, prefixErrors, unionFail, CompiledMorphContext, own);
952
+ return make(
953
+ this.#refs,
954
+ OmpErrors,
955
+ MISSING,
956
+ prefixErrors,
957
+ unionFail,
958
+ CompiledMorphContext,
959
+ own,
960
+ materializeDefault,
961
+ );
645
962
  }
646
963
 
647
964
  emitAllows(node: IR, v: string): void {
@@ -659,13 +976,11 @@ class Builder {
659
976
  }
660
977
  case "object": {
661
978
  const object = this.next("o");
662
- this.push(
663
- `const ${object}=${v};if(typeof ${object}!=="object"||${object}===null||Array.isArray(${object}))return false;`,
664
- );
979
+ this.push(`const ${object}=${v};if(typeof ${object}!=="object"||${object}===null)return false;`);
665
980
  for (const prop of node.props) {
666
981
  const value = this.next("p");
667
- const present = `${JSON.stringify(prop.key)} in ${object}`;
668
- this.push(`const ${value}=${access(object, prop.key)};`);
982
+ const present = `${this.lit(prop.key)} in ${object}`;
983
+ this.push(`const ${value}=${this.access(object, prop.key)};`);
669
984
  if (prop.opt || prop.hasDefault) {
670
985
  if (rejectsUndefined(prop.val)) {
671
986
  this.push(`if(${value}!==undefined){`);
@@ -681,16 +996,42 @@ class Builder {
681
996
  this.emitAllows(prop.val, value);
682
997
  }
683
998
  }
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}]`);
999
+ const stringKey = this.next("k");
1000
+ if (node.index !== undefined) {
1001
+ this.push(`for(const ${stringKey} in ${object}){if(!own.call(${object},${stringKey}))continue;`);
1002
+ this.emitAllows(node.index, `${object}[${stringKey}]`);
688
1003
  this.push("}");
689
- } else if (node.extras === "reject") {
690
- const key = this.next("k");
1004
+ }
1005
+ if (node.patternIndexes !== undefined) {
1006
+ for (const pattern of node.patternIndexes) {
1007
+ this.push(
1008
+ `for(const ${stringKey} in ${object})if(own.call(${object},${stringKey})&&(${this.predicate(pattern.key, stringKey)})&&!(${this.predicate(pattern.val, `${object}[${stringKey}]`)}))return false;`,
1009
+ );
1010
+ }
1011
+ }
1012
+ if (node.symbolIndex !== undefined) {
1013
+ const symbol = this.next("s");
691
1014
  this.push(
692
- `for(const ${key} in ${object})if(own.call(${object},${key})&&!(${this.declaredCheck(node.props, key)}))return false;`,
1015
+ `for(const ${symbol} of Object.getOwnPropertySymbols(${object})){if(!Object.prototype.propertyIsEnumerable.call(${object},${symbol}))continue;`,
693
1016
  );
1017
+ this.emitAllows(node.symbolIndex, `${object}[${symbol}]`);
1018
+ this.push("}");
1019
+ }
1020
+ if (node.extras === "reject") {
1021
+ const patternMatch =
1022
+ node.patternIndexes?.map(pattern => `(${this.predicate(pattern.key, stringKey)})`).join("||") ??
1023
+ "false";
1024
+ if (node.index === undefined) {
1025
+ this.push(
1026
+ `for(const ${stringKey} in ${object})if(own.call(${object},${stringKey})&&!(${this.declaredCheck(node.props, stringKey)})&&!(${patternMatch}))return false;`,
1027
+ );
1028
+ }
1029
+ if (node.symbolIndex === undefined) {
1030
+ const symbol = this.next("s");
1031
+ this.push(
1032
+ `for(const ${symbol} of Object.getOwnPropertySymbols(${object}))if(Object.prototype.propertyIsEnumerable.call(${object},${symbol})&&!(${this.declaredCheck(node.props, symbol)}))return false;`,
1033
+ );
1034
+ }
694
1035
  }
695
1036
  return;
696
1037
  }
@@ -716,14 +1057,14 @@ class Builder {
716
1057
  }
717
1058
  }
718
1059
 
719
- buildAllows(ir: IR): (value: unknown) => boolean {
1060
+ buildAllows(ir: IR): (value: unknown) => value is unknown {
720
1061
  this.emitAllows(ir, "v");
721
1062
  const src = `return function(v){${this.#lines.join("")}return true}`;
722
1063
  const make = new Function("R", "AE", "own", src) as (
723
1064
  refs: unknown[],
724
1065
  ae: typeof OmpErrors,
725
1066
  ownFn: typeof own,
726
- ) => (value: unknown) => boolean;
1067
+ ) => (value: unknown) => value is unknown;
727
1068
  return make(this.#refs, OmpErrors, own);
728
1069
  }
729
1070
  }
@@ -736,15 +1077,15 @@ function prefixErrors(errs: OmpErrors, parts: PropertyKey[]): OmpErrors {
736
1077
  const kWalk = Symbol("omptype.boundWalk");
737
1078
 
738
1079
  interface WalkTagged {
739
- [kWalk]?: (value: unknown) => unknown;
1080
+ [kWalk]?: (value: unknown, path?: PropertyKey[]) => unknown;
740
1081
  }
741
1082
 
742
1083
  /** Cached interpreter closure for recursive aliases and predicate-only fallbacks. */
743
- function boundWalk(node: IR): (value: unknown) => unknown {
1084
+ function boundWalk(node: IR): (value: unknown, path?: PropertyKey[]) => unknown {
744
1085
  const tagged = node as IR & WalkTagged;
745
1086
  let fn = tagged[kWalk];
746
1087
  if (!fn) {
747
- fn = (value: unknown) => walk(node, value);
1088
+ fn = (value: unknown, path?: PropertyKey[]) => walk(node, value, path);
748
1089
  tagged[kWalk] = fn;
749
1090
  }
750
1091
  return fn;
@@ -755,26 +1096,40 @@ function resolvedRoot(ir: IR): IR {
755
1096
  }
756
1097
 
757
1098
  const compiledCache = new WeakMap<IR, (value: unknown) => unknown>();
758
- const allowsCache = new WeakMap<IR, (value: unknown) => boolean>();
1099
+ const allowsCache = new WeakMap<IR, (value: unknown) => value is unknown>();
759
1100
 
760
1101
  /** Compile `ir` into a specialized validator. */
761
1102
  export function compile(ir: IR): (value: unknown) => unknown {
762
1103
  const root = resolvedRoot(ir);
763
- let validator = compiledCache.get(root);
1104
+ const validator = compiledCache.get(root);
764
1105
  if (validator === undefined) {
765
- validator = new Builder().build(root);
766
- compiledCache.set(root, validator);
1106
+ // Publish a deferred wrapper before building: recursive schemas re-enter
1107
+ // compile() for the same root mid-build (e.g. an alias element inside an
1108
+ // array), and each re-entry must reuse this build instead of starting a
1109
+ // fresh one forever. The wrapper resolves to the built validator by call
1110
+ // time; the interpreter is a safety net that never triggers post-build.
1111
+ let built: ((value: unknown) => unknown) | undefined;
1112
+ compiledCache.set(root, value => (built === undefined ? walk(root, value) : built(value)));
1113
+ built = new Builder().build(root);
1114
+ compiledCache.set(root, built);
1115
+ return built;
767
1116
  }
768
1117
  return validator;
769
1118
  }
770
1119
 
771
1120
  /** Compile `ir` into an allocation-free boolean validator. */
772
- export function compileAllows(ir: IR): (value: unknown) => boolean {
1121
+ export function compileAllows(ir: IR): (value: unknown) => value is unknown {
773
1122
  const root = resolvedRoot(ir);
774
- let validator = allowsCache.get(root);
1123
+ const validator = allowsCache.get(root);
775
1124
  if (validator === undefined) {
776
- validator = new Builder().buildAllows(root);
777
- allowsCache.set(root, validator);
1125
+ let built: ((value: unknown) => value is unknown) | undefined;
1126
+ allowsCache.set(root, ((value: unknown) =>
1127
+ built === undefined ? !(walk(root, value) instanceof OmpErrors) : built(value)) as (
1128
+ value: unknown,
1129
+ ) => value is unknown);
1130
+ built = new Builder().buildAllows(root);
1131
+ allowsCache.set(root, built);
1132
+ return built;
778
1133
  }
779
1134
  return validator;
780
1135
  }
@@ -785,9 +1140,9 @@ export function compileToSource(ir: IR): string {
785
1140
  const builder = new Builder();
786
1141
  if (hasMorph(root)) {
787
1142
  builder.push("let o;");
788
- builder.emitProduce(root, "v", [], "o");
1143
+ builder.emitCollectProduce(root, "v", [], "o", "e", "L0");
789
1144
  return `function(v){/* refs elided */return o}`;
790
1145
  }
791
- builder.emitCheck(root, "v", []);
1146
+ builder.emitCollectCheck(root, "v", [], "e");
792
1147
  return `function(v){/* refs elided */return v}`;
793
1148
  }