@oh-my-pi/omptype 17.2.6 → 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
@@ -11,21 +11,16 @@
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, materializeDefault, 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
-
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";
@@ -43,9 +38,57 @@ function litSource(v: unknown): string | undefined {
43
38
 
44
39
  type PathSeg = { s: PropertyKey } | { d: string };
45
40
 
41
+ type LiteralIR = Extract<IR, { k: "lit" }>;
42
+
43
+ function isPrimitiveLiteral(node: IR): node is LiteralIR {
44
+ return node.k === "lit" && (node.v === null || (typeof node.v !== "object" && typeof node.v !== "function"));
45
+ }
46
+
47
+ /** Whether `undefined` necessarily fails, allowing property-presence checks to be elided. */
48
+ function rejectsUndefined(node: IR): boolean {
49
+ switch (node.k) {
50
+ case "unknown":
51
+ case "undefined":
52
+ case "alias":
53
+ case "sub":
54
+ return false;
55
+ case "lit":
56
+ return node.v !== undefined;
57
+ case "union":
58
+ return node.members.every(rejectsUndefined);
59
+ case "intersection":
60
+ return node.members.some(rejectsUndefined);
61
+ case "refine":
62
+ return rejectsUndefined(node.base);
63
+ case "morph":
64
+ return rejectsUndefined(node.input);
65
+ default:
66
+ return true;
67
+ }
68
+ }
69
+
70
+ class CompiledMorphContext implements MorphContext {
71
+ #path: PropertyKey[] | PropertyKey | undefined;
72
+ #data: unknown;
73
+
74
+ constructor(path: PropertyKey[] | PropertyKey | undefined, data: unknown) {
75
+ this.#path = path;
76
+ this.#data = data;
77
+ }
78
+
79
+ error(expectation: string): OmpErrors {
80
+ return new OmpErrors(this.#path, expectation, this.#data);
81
+ }
82
+
83
+ reject(expectation: string): OmpErrors {
84
+ return this.error(expectation);
85
+ }
86
+ }
87
+
46
88
  class Builder {
47
89
  #lines: string[] = [];
48
90
  #refs: unknown[] = [];
91
+ #activeAliases: Set<IR> | undefined;
49
92
  #id = 0;
50
93
 
51
94
  next(prefix: string): string {
@@ -67,13 +110,39 @@ class Builder {
67
110
  return litSource(v) ?? this.ref(v);
68
111
  }
69
112
 
113
+ access(base: string, key: PropertyKey): string {
114
+ return typeof key === "string" && IDENT.test(key) ? `${base}.${key}` : `${base}[${this.lit(key)}]`;
115
+ }
116
+
70
117
  pathExpr(segs: PathSeg[]): string {
71
- 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));
72
119
  return `[${parts.join(",")}]`;
73
120
  }
74
121
 
122
+ storedPathExpr(segs: PathSeg[]): string {
123
+ if (segs.length === 0) return "undefined";
124
+ if (segs.length === 1) {
125
+ const seg = segs[0];
126
+ return "s" in seg ? this.lit(seg.s) : seg.d;
127
+ }
128
+ const staticParts: PropertyKey[] = [];
129
+ for (const seg of segs) {
130
+ if ("d" in seg) return this.pathExpr(segs);
131
+ staticParts.push(seg.s);
132
+ }
133
+ return this.ref(staticParts);
134
+ }
135
+
136
+ error(segs: PathSeg[], expected: string, dataExpr: string): string {
137
+ return `new AE(${this.storedPathExpr(segs)},${JSON.stringify(expected)},${dataExpr})`;
138
+ }
139
+
75
140
  fail(segs: PathSeg[], expected: string, dataExpr: string): string {
76
- return `return E(${this.pathExpr(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});`;
77
146
  }
78
147
 
79
148
  /** Pure boolean predicate for a morph-free subtree. */
@@ -89,10 +158,18 @@ class Builder {
89
158
  return `typeof ${v}==="boolean"`;
90
159
  case "bigint":
91
160
  return `typeof ${v}==="bigint"`;
161
+ case "symbol":
162
+ return `typeof ${v}==="symbol"`;
163
+ case "never":
164
+ return "false";
92
165
  case "anyobject":
93
166
  return `(typeof ${v}==="object"&&${v}!==null)`;
94
167
  case "lit":
95
- return `${v}===${this.lit(node.v)}`;
168
+ return node.v instanceof Date
169
+ ? `(${v} instanceof Date&&${v}.valueOf()===${node.v.valueOf()})`
170
+ : `${v}===${this.lit(node.v)}`;
171
+ case "instance":
172
+ return `${v} instanceof ${this.ref(node.ctor)}`;
96
173
  case "string": {
97
174
  let out = `typeof ${v}==="string"`;
98
175
  if (node.min !== undefined) out += `&&${v}.length>=${node.min}`;
@@ -101,57 +178,93 @@ class Builder {
101
178
  return out;
102
179
  }
103
180
  case "number": {
104
- let out = `typeof ${v}==="number"`;
105
- if (node.int) out += `&&Number.isInteger(${v})`;
181
+ let out = node.int ? `Number.isInteger(${v})` : `Number.isFinite(${v})`;
182
+ if (node.divisor !== undefined) out += `&&${v}%${node.divisor}===0`;
106
183
  if (node.min !== undefined) out += `&&${v}${node.xmin ? ">" : ">="}${node.min}`;
107
184
  if (node.max !== undefined) out += `&&${v}${node.xmax ? "<" : "<="}${node.max}`;
108
185
  return out;
109
186
  }
110
187
  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})`;
188
+ const lits = node.members.filter(isPrimitiveLiteral);
189
+ if (lits.length > 8) {
190
+ const values = this.ref(new Set(lits.map(member => member.v)));
191
+ const literalNodes = new Set<IR>(lits);
192
+ const rest = node.members.filter(member => !literalNodes.has(member));
193
+ let out = `${values}.has(${v})`;
116
194
  for (const m of rest) out += `||(${this.predicate(m, v)})`;
117
195
  return `(${out})`;
118
196
  }
119
197
  return `(${node.members.map(m => `(${this.predicate(m, v)})`).join("||")})`;
120
198
  }
199
+ case "intersection":
200
+ return `(${node.members.map(member => `(${this.predicate(member, v)})`).join("&&")})`;
121
201
  case "array": {
122
- const x = this.next("x");
202
+ const array = this.next("a");
203
+ const index = this.next("i");
123
204
  let out = `Array.isArray(${v})`;
124
205
  if (node.min !== undefined) out += `&&${v}.length>=${node.min}`;
125
206
  if (node.max !== undefined) out += `&&${v}.length<=${node.max}`;
126
- out += `&&${v}.every(${x}=>${this.predicate(node.el, x)})`;
207
+ const item = `${array}[${index}]`;
208
+ out += `&&((${array})=>{for(let ${index}=0;${index}<${array}.length;${index}++)if(!(${this.predicate(node.el, item)}))return false;return true})(${v})`;
127
209
  return out;
128
210
  }
129
211
  case "object": {
130
- const checks = [`typeof ${v}==="object"`, `${v}!==null`, `!Array.isArray(${v})`];
212
+ const checks = [`typeof ${v}==="object"`, `${v}!==null`];
131
213
  for (const p of node.props) {
132
- const av = access(v, p.key);
133
- 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}`;
216
+ const predicate = this.predicate(p.val, av);
134
217
  checks.push(
135
218
  p.opt || p.hasDefault
136
- ? `(!(${present})||(${this.predicate(p.val, av)}))`
137
- : `((${present})&&(${this.predicate(p.val, av)}))`,
219
+ ? rejectsUndefined(p.val)
220
+ ? `((${av}!==undefined&&(${predicate}))||!(${present}))`
221
+ : `(!(${present})||(${predicate}))`
222
+ : rejectsUndefined(p.val)
223
+ ? predicate
224
+ : `((${present})&&(${predicate}))`,
138
225
  );
139
226
  }
140
- if (node.index) {
141
- const k = this.next("k");
227
+ const stringKey = this.next("k");
228
+ if (node.index !== undefined) {
142
229
  checks.push(
143
- `(()=>{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})()`,
144
231
  );
145
- } else if (node.extras === "reject") {
146
- 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");
147
242
  checks.push(
148
- `(()=>{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})()`,
149
244
  );
150
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
+ }
151
262
  return `(${checks.join("&&")})`;
152
263
  }
153
264
  case "sub":
154
265
  return `!(${this.ref(node.schema.run)}(${v}) instanceof AE)`;
266
+ default:
267
+ return `!(${this.ref(boundWalk(node))}(${v}) instanceof AE)`;
155
268
  }
156
269
  }
157
270
 
@@ -161,89 +274,431 @@ class Builder {
161
274
  const set = this.ref(new Set(props.map(p => p.key)));
162
275
  return `${set}.has(${keyVar})`;
163
276
  }
164
- return `(${props.map(p => `${keyVar}===${JSON.stringify(p.key)}`).join("||")})`;
277
+ return `(${props.map(p => `${keyVar}===${this.lit(p.key)}`).join("||")})`;
278
+ }
279
+
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);
291
+ const result = this.next("r");
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;
295
+ this.push(
296
+ `if(${result} instanceof AE){${this.appendError(errors, failure)}${brk === undefined ? "" : `break ${brk};`}}`,
297
+ );
298
+ if (out !== undefined) this.push(`${out}=${result};`);
299
+ }
300
+
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;
306
+ }
307
+
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};`);
165
311
  }
166
312
 
167
- /** Statement-form check for a morph-free subtree with precise error paths. */
168
- emitCheck(node: IR, v: string, segs: PathSeg[]): void {
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
+ }
169
319
  switch (node.k) {
170
320
  case "unknown":
171
321
  return;
172
322
  case "array": {
173
- let head = `Array.isArray(${v})`;
174
- if (node.min !== undefined) head += `&&${v}.length>=${node.min}`;
175
- if (node.max !== undefined) head += `&&${v}.length<=${node.max}`;
176
- this.push(`if(!(${head}))${this.fail(segs, expectedOf(node), v)};`);
177
- const i = this.next("i");
178
- const x = this.next("x");
179
- this.push(`for(let ${i}=0;${i}<${v}.length;${i}++){const ${x}=${v}[${i}];`);
180
- this.emitCheck(node.el, x, [...segs, { d: i }]);
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("}}");
347
+ return;
348
+ }
349
+ case "tuple": {
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
+ }
382
+ for (let index = 0; index < node.prefix.length; index++) {
383
+ if (index >= requiredPrefix) this.push(`if(${index}<${prefixCount}){`);
384
+ this.emitCollectCheck(node.prefix[index].val, `${v}[${index}]`, [...segs, { d: String(index) }], errors);
385
+ if (index >= requiredPrefix) this.push("}");
386
+ }
387
+ if (node.variadic !== undefined) {
388
+ const index = this.next("i");
389
+ this.push(`for(let ${index}=${prefixCount};${index}<${postfixStart};${index}++){`);
390
+ this.emitCollectCheck(node.variadic, `${v}[${index}]`, [...segs, { d: index }], errors);
391
+ this.push("}");
392
+ }
393
+ for (let index = 0; index < node.postfix.length; index++) {
394
+ const inputIndex = index === 0 ? postfixStart : `${postfixStart}+${index}`;
395
+ this.emitCollectCheck(node.postfix[index], `${v}[${inputIndex}]`, [...segs, { d: inputIndex }], errors);
396
+ }
397
+ if (node.variadic === undefined) this.push("}");
398
+ if (minimum > 0) this.push("}");
181
399
  this.push("}");
182
400
  return;
183
401
  }
184
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
+ }
185
411
  this.push(
186
- `if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object", v)};`,
412
+ `if(typeof ${v}!=="object"||${v}===null){${this.appendError(
413
+ errors,
414
+ this.error(segs, "an object", failureData),
415
+ )}}else{`,
187
416
  );
188
- for (const p of node.props) {
189
- const present = `${JSON.stringify(p.key)} in ${v}`;
190
- const propSegs: PathSeg[] = [...segs, { s: p.key }];
191
- 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) {
192
421
  this.push(`if(${present}){`);
193
- this.emitCheck(p.val, access(v, p.key), propSegs);
422
+ this.emitCollectCheck(prop.val, this.access(v, prop.key), propSegs, errors);
194
423
  this.push("}");
195
424
  } else {
196
- this.push(`if(!(${present}))${this.fail(propSegs, expectedOf(p.val), "M")};`);
197
- 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("}");
198
433
  }
199
434
  }
200
- if (node.index) {
201
- const k = this.next("k");
202
- this.push(`for(const ${k} in ${v})if(own.call(${v},${k})){`);
203
- 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);
204
439
  this.push("}");
205
440
  } else if (node.extras === "reject") {
206
- const k = this.next("k");
441
+ const key = this.next("k");
442
+ this.push(
443
+ `for(const ${key} in ${v})if(own.call(${v},${key})&&!(${this.declaredCheck(node.props, key)})){`,
444
+ );
445
+ this.push(this.appendError(errors, this.error([...segs, { d: key }], "removed", `${v}[${key}]`)));
446
+ this.push("}");
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("}");
455
+ return;
456
+ }
457
+ case "union": {
458
+ const failure = node.members.some(canRefineUnionFailure)
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) {
207
477
  this.push(
208
- `for(const ${k} in ${v})if(own.call(${v},${k})&&!(${this.declaredCheck(node.props, k)}))${this.fail(
209
- [...segs, { d: k }],
210
- "removed (undeclared key)",
211
- `${v}[${k}]`,
212
- )};`,
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
+ )}}`,
213
490
  );
214
491
  }
215
492
  return;
216
493
  }
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)});`);
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("}");
221
543
  return;
222
544
  }
223
- case "union":
224
- this.push(`if(!(${this.predicate(node, v)}))return UF(${this.ref(node)},${v},${this.pathExpr(segs)});`);
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;
564
+ case "null":
565
+ case "undefined":
566
+ case "boolean":
567
+ case "bigint":
568
+ case "symbol":
569
+ case "never":
570
+ case "anyobject":
571
+ case "instance":
572
+ this.push(
573
+ `if(!(${this.predicate(node, v)})){${this.appendError(
574
+ errors,
575
+ this.error(segs, expectedOf(node), failureData),
576
+ )}}`,
577
+ );
225
578
  return;
226
579
  default:
227
- this.push(`if(!(${this.predicate(node, v)}))${this.fail(segs, expectedOf(node), v)};`);
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)})`};`);
228
640
  }
229
641
  }
230
642
 
231
643
  /**
232
644
  * Validate `v` against a morphing subtree and assign the produced output
233
- * 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.
234
648
  */
235
- emitProduce(node: IR, v: string, segs: PathSeg[], out: string): 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
+ }
236
662
  if (!hasMorph(node)) {
237
- this.emitCheck(node, v, segs);
663
+ this.emitCollectCheck(node, v, segs, errors, failureData);
238
664
  this.push(`${out}=${v};`);
239
665
  return;
240
666
  }
241
667
  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};`);
668
+ case "sub":
669
+ this.emitCollectDelegate(node, v, segs, errors, brk, out);
670
+ return;
671
+ case "morph": {
672
+ const input = this.next("t");
673
+ this.push(`let ${input};`);
674
+ const mark = this.markErrors(errors);
675
+ this.emitCollectProduce(node.input, v, segs, input, errors, brk, failureData);
676
+ this.guardGrowth(errors, mark, brk);
677
+ const context = this.next("c");
678
+ const result = this.next("r");
679
+ this.push(`const ${context}=new MC(${this.storedPathExpr(segs)},${input});`);
680
+ this.push(`const ${result}=${this.ref(node.fn)}(${input},${context});`);
681
+ this.push(`if(${result} instanceof AE){${this.appendError(errors, result)}break ${brk};}`);
682
+ if (node.out === undefined) this.push(`${out}=${result};`);
683
+ else this.emitCollectProduce(node.out, result, segs, out, errors, brk);
684
+ return;
685
+ }
686
+ case "alias": {
687
+ let active = this.#activeAliases;
688
+ if (active === undefined) {
689
+ active = new Set();
690
+ this.#activeAliases = active;
691
+ }
692
+ if (active.has(node)) {
693
+ this.emitCollectDelegate(node, v, segs, errors, brk, out);
694
+ return;
695
+ }
696
+ active.add(node);
697
+ try {
698
+ this.emitCollectProduce(node.resolve(), v, segs, out, errors, brk, failureData);
699
+ } finally {
700
+ active.delete(node);
701
+ }
247
702
  return;
248
703
  }
249
704
  case "union": {
@@ -258,120 +713,359 @@ class Builder {
258
713
  }
259
714
  for (const m of node.members) {
260
715
  if (m.k === "sub" || hasMorph(m)) {
261
- const runner = m.k === "sub" ? m.schema.run : boundWalk(m);
716
+ const runner = m.k === "sub" ? m.schema.run : m.k === "alias" ? boundWalk(m) : compile(m);
262
717
  const r = this.next("r");
263
718
  this.push(`const ${r}=${this.ref(runner)}(${v});`);
264
719
  this.push(`if(!(${r} instanceof AE)){${out}=${r};${ok}=true;break ${label};}`);
265
720
  }
266
721
  }
267
722
  this.push("}");
268
- this.push(`if(!${ok})return UF(${this.ref(node)},${v},${this.pathExpr(segs)});`);
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};}`);
269
725
  return;
270
726
  }
271
727
  case "array": {
272
- let head = `Array.isArray(${v})`;
273
- if (node.min !== undefined) head += `&&${v}.length>=${node.min}`;
274
- if (node.max !== undefined) head += `&&${v}.length<=${node.max}`;
275
- this.push(`if(!(${head}))${this.fail(segs, expectedOf(node), v)};`);
276
- const arr = this.next("a");
277
- const i = this.next("i");
278
- const x = this.next("x");
279
- const el = this.next("t");
280
- this.push(`const ${arr}=new Array(${v}.length);`);
281
- this.push(`for(let ${i}=0;${i}<${v}.length;${i}++){const ${x}=${v}[${i}];let ${el};`);
282
- this.emitProduce(node.el, x, [...segs, { d: i }], el);
283
- this.push(`${arr}[${i}]=${el};}`);
284
- 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};`);
759
+ return;
760
+ }
761
+ case "tuple": {
762
+ const { postfixStart, prefixCount, requiredPrefix } = this.emitTupleShape(
763
+ node,
764
+ v,
765
+ segs,
766
+ errors,
767
+ brk,
768
+ failureData,
769
+ );
770
+ const tuple = this.next("a");
771
+ this.push(`const ${tuple}=[...${v}];`);
772
+ for (let index = 0; index < node.prefix.length; index++) {
773
+ const item = node.prefix[index];
774
+ const itemSegs: PathSeg[] = [...segs, { s: index }];
775
+ const input = `${v}[${index}]`;
776
+ const output = `${tuple}[${index}]`;
777
+ const label = this.next("L");
778
+ if (index >= requiredPrefix) this.push(`if(${index}<${prefixCount}){`);
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);
787
+ }
788
+ this.push("}");
789
+ if (index >= requiredPrefix) {
790
+ if (item.hasDefault) {
791
+ this.push("}else{");
792
+ this.emitDefaultFill(item.val, item.def, item.defFactory === true, output, itemSegs, errors);
793
+ this.push("}");
794
+ } else {
795
+ this.push("}");
796
+ }
797
+ }
798
+ }
799
+ if (node.variadic !== undefined) {
800
+ const index = this.next("i");
801
+ const input = this.next("x");
802
+ const label = this.next("L");
803
+ this.push(
804
+ `for(let ${index}=${prefixCount};${index}<${postfixStart};${index}++){const ${input}=${v}[${index}];${label}:{`,
805
+ );
806
+ if (hasMorph(node.variadic)) {
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};`);
811
+ } else {
812
+ this.emitCollectCheck(node.variadic, input, [...segs, { d: index }], errors);
813
+ }
814
+ this.push("}}");
815
+ }
816
+ for (let index = 0; index < node.postfix.length; index++) {
817
+ const inputIndex = index === 0 ? postfixStart : `${postfixStart}+${index}`;
818
+ const input = `${v}[${inputIndex}]`;
819
+ const item = node.postfix[index];
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);
829
+ }
830
+ this.push("}");
831
+ }
832
+ this.push(`${out}=${tuple};`);
285
833
  return;
286
834
  }
287
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
+ }
288
844
  this.push(
289
- `if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object", v)};`,
845
+ `if(typeof ${v}!=="object"||${v}===null){${this.appendError(
846
+ errors,
847
+ this.error(segs, "an object", failureData),
848
+ )}break ${brk};}`,
290
849
  );
291
- const o = this.next("o");
292
- const fresh = node.extras === "delete" && !node.index;
293
- this.push(fresh ? `const ${o}={};` : `const ${o}={...${v}};`);
294
- for (const p of node.props) {
295
- const present = `${JSON.stringify(p.key)} in ${v}`;
296
- const propSegs: PathSeg[] = [...segs, { s: p.key }];
297
- const av = access(v, p.key);
298
- const ao = access(o, p.key);
299
- const morphChild = hasMorph(p.val);
300
- const missing: string[] = [];
301
- if (p.hasDefault) {
302
- const dflt = p.defFactory ? `${this.ref(p.def)}()` : this.lit(p.def);
303
- missing.push(`${ao}=${dflt};`);
304
- } else if (!p.opt) {
305
- missing.push(`${this.fail(propSegs, expectedOf(p.val), "M")};`);
306
- } else if (fresh) {
307
- // optional + absent under "delete": nothing to copy
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")));
308
864
  }
309
- this.push(`if(!(${present})){${missing.join("")}}else{`);
310
- if (morphChild) {
311
- const t = this.next("t");
312
- this.push(`let ${t};`);
313
- this.emitProduce(p.val, av, propSegs, t);
314
- 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};`);
315
871
  } else {
316
- this.emitCheck(p.val, av, propSegs);
317
- if (fresh) this.push(`${ao}=${av};`);
872
+ this.emitCollectCheck(prop.val, input, propSegs, errors);
873
+ if (fresh) this.push(`${output}=${input};`);
318
874
  }
319
- this.push("}");
875
+ this.push("}}");
320
876
  }
321
- if (node.index) {
322
- const k = this.next("k");
323
- 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}:{`);
324
881
  if (hasMorph(node.index)) {
325
- const t = this.next("t");
326
- this.push(`let ${t};`);
327
- this.emitProduce(node.index, `${v}[${k}]`, [...segs, { d: k }], t);
328
- 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};`);
329
886
  } else {
330
- this.emitCheck(node.index, `${v}[${k}]`, [...segs, { d: k }]);
887
+ this.emitCollectCheck(node.index, `${v}[${key}]`, [...segs, { d: key }], errors);
331
888
  }
332
- this.push("}");
889
+ this.push("}}");
333
890
  } else if (node.extras === "reject") {
334
- const k = this.next("k");
891
+ const key = this.next("k");
892
+ this.push(
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
+ )}}`,
897
+ );
898
+ }
899
+ if (node.extras === "reject") {
900
+ const symbol = this.next("s");
335
901
  this.push(
336
- `for(const ${k} in ${v})if(own.call(${v},${k})&&!(${this.declaredCheck(node.props, k)}))${this.fail(
337
- [...segs, { d: k }],
338
- "removed (undeclared key)",
339
- `${v}[${k}]`,
340
- )};`,
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}]`))}}`,
341
903
  );
342
904
  }
343
- this.push(`${out}=${o};`);
905
+ this.push(`${out}=${object};`);
906
+ return;
907
+ }
908
+ case "intersection": {
909
+ const current = this.next("t");
910
+ this.push(`let ${current}=${v};`);
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);
917
+ }
918
+ this.push(`${out}=${current};`);
344
919
  return;
345
920
  }
346
921
  default:
347
- // morph-free kinds are handled by the hasMorph guard above
348
- this.emitCheck(node, v, segs);
349
- this.push(`${out}=${v};`);
922
+ this.emitCollectDelegate(node, v, segs, errors, brk, out);
350
923
  }
351
924
  }
352
925
 
353
926
  build(ir: IR): (value: unknown) => unknown {
927
+ const errors = this.next("e");
354
928
  let ret: string;
355
929
  if (hasMorph(ir)) {
356
- this.push("let o;");
357
- // body emitted below needs `o` declared first, so splice ordering:
358
- 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("}");
359
934
  ret = "o";
360
935
  } else {
361
- this.emitCheck(ir, "v", []);
936
+ this.push(`let ${errors};`);
937
+ this.emitCollectCheck(ir, "v", [], errors);
362
938
  ret = "v";
363
939
  }
940
+ this.push(`if(${errors}!==undefined)return ${errors};`);
364
941
  const src = `return function(v){${this.#lines.join("")}return ${ret}}`;
365
- const make = new Function("R", "E", "AE", "M", "PF", "UF", "own", src) as (
942
+ const make = new Function("R", "AE", "M", "PF", "UF", "MC", "own", "MD", src) as (
366
943
  refs: unknown[],
367
- e: typeof OmpErrors.single,
368
944
  ae: typeof OmpErrors,
369
945
  m: typeof MISSING,
370
946
  pf: typeof prefixErrors,
371
947
  uf: typeof unionFail,
948
+ mc: typeof CompiledMorphContext,
372
949
  ownFn: typeof own,
950
+ md: typeof materializeDefault,
373
951
  ) => (value: unknown) => unknown;
374
- return make(this.#refs, OmpErrors.single, OmpErrors, MISSING, prefixErrors, unionFail, own);
952
+ return make(
953
+ this.#refs,
954
+ OmpErrors,
955
+ MISSING,
956
+ prefixErrors,
957
+ unionFail,
958
+ CompiledMorphContext,
959
+ own,
960
+ materializeDefault,
961
+ );
962
+ }
963
+
964
+ emitAllows(node: IR, v: string): void {
965
+ switch (node.k) {
966
+ case "array": {
967
+ const array = this.next("a");
968
+ const index = this.next("i");
969
+ this.push(`const ${array}=${v};if(!Array.isArray(${array}))return false;`);
970
+ if (node.min !== undefined) this.push(`if(${array}.length<${node.min})return false;`);
971
+ if (node.max !== undefined) this.push(`if(${array}.length>${node.max})return false;`);
972
+ this.push(`for(let ${index}=0;${index}<${array}.length;${index}++){`);
973
+ this.emitAllows(node.el, `${array}[${index}]`);
974
+ this.push("}");
975
+ return;
976
+ }
977
+ case "object": {
978
+ const object = this.next("o");
979
+ this.push(`const ${object}=${v};if(typeof ${object}!=="object"||${object}===null)return false;`);
980
+ for (const prop of node.props) {
981
+ const value = this.next("p");
982
+ const present = `${this.lit(prop.key)} in ${object}`;
983
+ this.push(`const ${value}=${this.access(object, prop.key)};`);
984
+ if (prop.opt || prop.hasDefault) {
985
+ if (rejectsUndefined(prop.val)) {
986
+ this.push(`if(${value}!==undefined){`);
987
+ this.emitAllows(prop.val, value);
988
+ this.push(`}else if(${present})return false;`);
989
+ } else {
990
+ this.push(`if(${present}){`);
991
+ this.emitAllows(prop.val, value);
992
+ this.push("}");
993
+ }
994
+ } else {
995
+ if (!rejectsUndefined(prop.val)) this.push(`if(!(${present}))return false;`);
996
+ this.emitAllows(prop.val, value);
997
+ }
998
+ }
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}]`);
1003
+ this.push("}");
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");
1014
+ this.push(
1015
+ `for(const ${symbol} of Object.getOwnPropertySymbols(${object})){if(!Object.prototype.propertyIsEnumerable.call(${object},${symbol}))continue;`,
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
+ }
1035
+ }
1036
+ return;
1037
+ }
1038
+ case "union": {
1039
+ const sources: string[] = [];
1040
+ for (const member of node.members) {
1041
+ if (!isPrimitiveLiteral(member)) break;
1042
+ const source = litSource(member.v);
1043
+ if (source === undefined) break;
1044
+ sources.push(source);
1045
+ }
1046
+ if (sources.length === node.members.length && sources.length >= 4) {
1047
+ this.push(
1048
+ `switch(${v}){${sources.map(source => `case ${source}:`).join("")}break;default:return false;}`,
1049
+ );
1050
+ return;
1051
+ }
1052
+ this.push(`if(!(${this.predicate(node, v)}))return false;`);
1053
+ return;
1054
+ }
1055
+ default:
1056
+ this.push(`if(!(${this.predicate(node, v)}))return false;`);
1057
+ }
1058
+ }
1059
+
1060
+ buildAllows(ir: IR): (value: unknown) => value is unknown {
1061
+ this.emitAllows(ir, "v");
1062
+ const src = `return function(v){${this.#lines.join("")}return true}`;
1063
+ const make = new Function("R", "AE", "own", src) as (
1064
+ refs: unknown[],
1065
+ ae: typeof OmpErrors,
1066
+ ownFn: typeof own,
1067
+ ) => (value: unknown) => value is unknown;
1068
+ return make(this.#refs, OmpErrors, own);
375
1069
  }
376
1070
  }
377
1071
 
@@ -383,33 +1077,72 @@ function prefixErrors(errs: OmpErrors, parts: PropertyKey[]): OmpErrors {
383
1077
  const kWalk = Symbol("omptype.boundWalk");
384
1078
 
385
1079
  interface WalkTagged {
386
- [kWalk]?: (value: unknown) => unknown;
1080
+ [kWalk]?: (value: unknown, path?: PropertyKey[]) => unknown;
387
1081
  }
388
1082
 
389
- /** Interpreter closure for a morphing union member (rare; hoisted into refs). */
390
- function boundWalk(node: IR): (value: unknown) => unknown {
1083
+ /** Cached interpreter closure for recursive aliases and predicate-only fallbacks. */
1084
+ function boundWalk(node: IR): (value: unknown, path?: PropertyKey[]) => unknown {
391
1085
  const tagged = node as IR & WalkTagged;
392
1086
  let fn = tagged[kWalk];
393
1087
  if (!fn) {
394
- fn = (value: unknown) => walk(node, value);
1088
+ fn = (value: unknown, path?: PropertyKey[]) => walk(node, value, path);
395
1089
  tagged[kWalk] = fn;
396
1090
  }
397
1091
  return fn;
398
1092
  }
399
1093
 
1094
+ function resolvedRoot(ir: IR): IR {
1095
+ return ir.k === "alias" ? ir.resolve() : ir;
1096
+ }
1097
+
1098
+ const compiledCache = new WeakMap<IR, (value: unknown) => unknown>();
1099
+ const allowsCache = new WeakMap<IR, (value: unknown) => value is unknown>();
1100
+
400
1101
  /** Compile `ir` into a specialized validator. */
401
1102
  export function compile(ir: IR): (value: unknown) => unknown {
402
- return new Builder().build(ir);
1103
+ const root = resolvedRoot(ir);
1104
+ const validator = compiledCache.get(root);
1105
+ if (validator === undefined) {
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;
1116
+ }
1117
+ return validator;
1118
+ }
1119
+
1120
+ /** Compile `ir` into an allocation-free boolean validator. */
1121
+ export function compileAllows(ir: IR): (value: unknown) => value is unknown {
1122
+ const root = resolvedRoot(ir);
1123
+ const validator = allowsCache.get(root);
1124
+ if (validator === undefined) {
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;
1133
+ }
1134
+ return validator;
403
1135
  }
404
1136
 
405
1137
  /** Generated source for inspection/debugging. */
406
1138
  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");
1139
+ const root = resolvedRoot(ir);
1140
+ const builder = new Builder();
1141
+ if (hasMorph(root)) {
1142
+ builder.push("let o;");
1143
+ builder.emitCollectProduce(root, "v", [], "o", "e", "L0");
411
1144
  return `function(v){/* refs elided */return o}`;
412
1145
  }
413
- b.emitCheck(ir, "v", []);
1146
+ builder.emitCollectCheck(root, "v", [], "e");
414
1147
  return `function(v){/* refs elided */return v}`;
415
1148
  }