@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/interp.ts CHANGED
@@ -9,15 +9,36 @@
9
9
  * - failure returns an `OmpErrors` with a single fast-fail entry
10
10
  */
11
11
  import { MISSING, OmpErrors } from "./errors";
12
- import { expectedOf, hasMorph, type IR } from "./ir";
12
+ import { expectedOf, hasAlias, hasMorph, type IR } from "./ir";
13
13
 
14
14
  const own = Object.prototype.hasOwnProperty;
15
+ /** Return an independent runtime value for a prevalidated static default. */
16
+ export function materializeDefault(payload: unknown): unknown {
17
+ if (payload === null || typeof payload !== "object") return payload;
18
+ if (payload instanceof Date) return new Date(payload);
19
+ return structuredClone(payload);
20
+ }
21
+ let activeVisits: WeakMap<object, Set<IR>> | undefined;
22
+ let activeChecks: WeakMap<object, Set<IR>> | undefined;
15
23
 
16
- /** Validate `value` against `ir`; returns output value or `OmpErrors`. */
17
- export function walk(ir: IR, value: unknown): unknown {
18
- const path: PropertyKey[] = [];
19
- const out = visit(ir, value, path);
20
- return out;
24
+ /**
25
+ * Validate `value` against `ir`; returns output value or `OmpErrors`.
26
+ * `path` seeds the traversal location so nested step callbacks observe
27
+ * absolute ctx.path values when a compiled parent delegates a subtree;
28
+ * resulting error paths are then already absolute.
29
+ */
30
+ export function walk(ir: IR, value: unknown, path: PropertyKey[] = []): unknown {
31
+ const previousVisits = activeVisits;
32
+ const previousChecks = activeChecks;
33
+ // Created lazily by visit/checks only when a recursive-alias node is reached.
34
+ activeVisits = undefined;
35
+ activeChecks = undefined;
36
+ try {
37
+ return visit(ir, value, path);
38
+ } finally {
39
+ activeVisits = previousVisits;
40
+ activeChecks = previousChecks;
41
+ }
21
42
  }
22
43
 
23
44
  function fail(path: PropertyKey[], expected: string, data: unknown): OmpErrors {
@@ -27,6 +48,26 @@ function fail(path: PropertyKey[], expected: string, data: unknown): OmpErrors {
27
48
 
28
49
  /** Pure predicate used for union-member scanning (no morphs, no errors). */
29
50
  function checks(ir: IR, v: unknown): boolean {
51
+ // Cycle guards are only needed when the subtree can revisit nodes through
52
+ // recursive aliases; plain schemas skip the WeakMap bookkeeping entirely.
53
+ if (typeof v !== "object" || v === null || !hasAlias(ir)) return checkNode(ir, v);
54
+ activeChecks ??= new WeakMap();
55
+ const visits = activeChecks;
56
+ let visited = visits.get(v);
57
+ if (visited?.has(ir)) return true;
58
+ if (visited === undefined) {
59
+ visited = new Set();
60
+ visits.set(v, visited);
61
+ }
62
+ visited.add(ir);
63
+ try {
64
+ return checkNode(ir, v);
65
+ } finally {
66
+ visited.delete(ir);
67
+ }
68
+ }
69
+
70
+ function checkNode(ir: IR, v: unknown): boolean {
30
71
  switch (ir.k) {
31
72
  case "unknown":
32
73
  return true;
@@ -69,7 +110,7 @@ function checks(ir: IR, v: unknown): boolean {
69
110
  if (!Array.isArray(v)) return false;
70
111
  if (ir.min !== undefined && v.length < ir.min) return false;
71
112
  if (ir.max !== undefined && v.length > ir.max) return false;
72
- for (const el of v) if (!checks(ir.el, el)) return false;
113
+ for (const element of v) if (!checks(ir.el, element)) return false;
73
114
  return true;
74
115
  }
75
116
  case "tuple": {
@@ -98,8 +139,8 @@ function checks(ir: IR, v: unknown): boolean {
98
139
  return true;
99
140
  }
100
141
  case "object": {
101
- if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
102
- const rec = v as Record<string, unknown>;
142
+ if (typeof v !== "object" || v === null) return false;
143
+ const rec = v as Record<PropertyKey, unknown>;
103
144
  for (const p of ir.props) {
104
145
  const present = p.key in rec;
105
146
  if (!present) {
@@ -108,21 +149,31 @@ function checks(ir: IR, v: unknown): boolean {
108
149
  }
109
150
  if (!checks(p.val, rec[p.key])) return false;
110
151
  }
111
- if (ir.index) {
112
- for (const key in rec) {
113
- if (own.call(rec, key) && !checks(ir.index, rec[key])) return false;
114
- }
115
- } else if (ir.extras === "reject") {
116
- for (const key in rec) {
117
- if (!own.call(rec, key)) continue;
118
- let declared = false;
119
- for (const p of ir.props) {
120
- if (p.key === key) {
121
- declared = true;
122
- break;
123
- }
152
+ for (const key in rec) {
153
+ if (!own.call(rec, key)) continue;
154
+ if (ir.index !== undefined && !checks(ir.index, rec[key])) return false;
155
+ let patternMatched = false;
156
+ if (ir.patternIndexes !== undefined) {
157
+ for (const pattern of ir.patternIndexes) {
158
+ if (!checks(pattern.key, key)) continue;
159
+ patternMatched = true;
160
+ if (!checks(pattern.val, rec[key])) return false;
124
161
  }
125
- if (!declared) return false;
162
+ }
163
+ if (
164
+ ir.extras === "reject" &&
165
+ ir.index === undefined &&
166
+ !patternMatched &&
167
+ !ir.props.some(prop => prop.key === key)
168
+ ) {
169
+ return false;
170
+ }
171
+ }
172
+ for (const key of Object.getOwnPropertySymbols(rec)) {
173
+ if (!Object.prototype.propertyIsEnumerable.call(rec, key)) continue;
174
+ if (ir.symbolIndex !== undefined && !checks(ir.symbolIndex, rec[key])) return false;
175
+ if (ir.extras === "reject" && ir.symbolIndex === undefined && !ir.props.some(prop => prop.key === key)) {
176
+ return false;
126
177
  }
127
178
  }
128
179
  return true;
@@ -132,7 +183,7 @@ function checks(ir: IR, v: unknown): boolean {
132
183
  case "refine":
133
184
  if (!checks(ir.base, v)) return false;
134
185
  try {
135
- return ir.pred(v);
186
+ return ir.pred(v) === true;
136
187
  } catch {
137
188
  return false;
138
189
  }
@@ -146,6 +197,34 @@ function checks(ir: IR, v: unknown): boolean {
146
197
  }
147
198
 
148
199
  function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
200
+ if (typeof v !== "object" || v === null || !hasAlias(ir)) return visitFinish(ir, v, path);
201
+ activeVisits ??= new WeakMap();
202
+ const visits = activeVisits;
203
+ let visited = visits.get(v);
204
+ if (visited?.has(ir)) return v;
205
+ if (visited === undefined) {
206
+ visited = new Set();
207
+ visits.set(v, visited);
208
+ }
209
+ visited.add(ir);
210
+ try {
211
+ return visitFinish(ir, v, path);
212
+ } finally {
213
+ visited.delete(ir);
214
+ }
215
+ }
216
+
217
+ /** Run the node visitor, then apply node-local error configuration. */
218
+ function visitFinish(ir: IR, v: unknown, path: PropertyKey[]): unknown {
219
+ const out = visitNode(ir, v, path);
220
+ if (!(out instanceof OmpErrors) || ir.cfg === undefined) return out;
221
+ for (const error of out) {
222
+ if (error.path.length !== path.length) return out;
223
+ }
224
+ return out.configure(ir.cfg);
225
+ }
226
+
227
+ function visitNode(ir: IR, v: unknown, path: PropertyKey[]): unknown {
149
228
  switch (ir.k) {
150
229
  case "alias":
151
230
  return visit(ir.resolve(), v, path);
@@ -153,7 +232,9 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
153
232
  const base = visit(ir.base, v, path);
154
233
  if (base instanceof OmpErrors) return base;
155
234
  try {
156
- return ir.pred(base) ? base : fail(path, ir.expected, base);
235
+ const result = ir.pred(base);
236
+ if (result instanceof OmpErrors) return path.length === 0 ? result : prefixAll(result, path);
237
+ return result ? base : fail(path, ir.expected, base);
157
238
  } catch {
158
239
  return fail(path, ir.expected, base);
159
240
  }
@@ -178,7 +259,7 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
178
259
  return output;
179
260
  }
180
261
  case "sub": {
181
- const out = ir.schema.run(v);
262
+ const out = ir.schema.run(v, path);
182
263
  if (out instanceof OmpErrors) {
183
264
  return path.length === 0 ? out : prefixAll(out, path);
184
265
  }
@@ -192,38 +273,40 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
192
273
  return v;
193
274
  }
194
275
  }
195
- for (const m of ir.members) {
196
- if (m.k === "sub" || hasMorph(m)) {
197
- const out = visit(m, v, path);
276
+ let targeted: OmpErrors | undefined;
277
+ let targetCount = 0;
278
+ for (const member of ir.members) {
279
+ if (member.k === "sub" || hasMorph(member)) {
280
+ const out = visit(member, v, path);
198
281
  if (!(out instanceof OmpErrors)) return out;
282
+ if (kindMatches(unwrapBase(member), v)) {
283
+ targetCount++;
284
+ targeted ??= out;
285
+ }
199
286
  }
200
287
  }
288
+ if (targetCount === 1 && targeted !== undefined) return targeted;
201
289
  return ir.members.some(canRefineUnionFailure) ? unionFail(ir, v, path) : fail(path, expectedOf(ir), v);
202
290
  }
203
291
  case "array": {
204
292
  if (!Array.isArray(v)) return fail(path, "an array", v);
205
- if (ir.min !== undefined && v.length < ir.min) return fail(path, `at least length ${ir.min}`, v);
206
- if (ir.max !== undefined && v.length > ir.max) return fail(path, `at most length ${ir.max}`, v);
207
- if (!hasMorph(ir.el)) {
208
- for (let i = 0; i < v.length; i++) {
209
- if (!checks(ir.el, v[i])) {
210
- path.push(i);
211
- const err = visit(ir.el, v[i], path);
212
- path.pop();
213
- return err instanceof OmpErrors ? err : fail([...path, i], expectedOf(ir.el), v[i]);
214
- }
215
- }
216
- return v;
217
- }
218
- const out = new Array<unknown>(v.length);
219
- for (let i = 0; i < v.length; i++) {
220
- path.push(i);
221
- const el = visit(ir.el, v[i], path);
293
+ if (ir.min !== undefined && v.length < ir.min) return fail(path, `at least length ${ir.min}`, v.length);
294
+ if (ir.max !== undefined && v.length > ir.max) return fail(path, `at most length ${ir.max}`, v.length);
295
+ const morph = hasMorph(ir.el);
296
+ const out = morph ? new Array<unknown>(v.length) : v;
297
+ let errors: OmpErrors | undefined;
298
+ for (let index = 0; index < v.length; index++) {
299
+ path.push(index);
300
+ const element = visit(ir.el, v[index], path);
222
301
  path.pop();
223
- if (el instanceof OmpErrors) return el;
224
- out[i] = el;
302
+ if (element instanceof OmpErrors) {
303
+ if (errors) errors.append(element);
304
+ else errors = element;
305
+ } else if (morph) {
306
+ out[index] = element;
307
+ }
225
308
  }
226
- return out;
309
+ return errors ?? out;
227
310
  }
228
311
  case "tuple": {
229
312
  if (!Array.isArray(v)) return fail(path, "an array", v);
@@ -238,23 +321,41 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
238
321
  const prefixCount = Math.min(ir.prefix.length, postfixStart);
239
322
  const morph = hasMorph(ir);
240
323
  const output = morph ? [...v] : v;
324
+ let errors: OmpErrors | undefined;
241
325
  for (let index = 0; index < prefixCount; index++) {
242
326
  path.push(index);
243
327
  const item = visit(ir.prefix[index].val, v[index], path);
244
328
  path.pop();
245
- if (item instanceof OmpErrors) return item;
246
- if (morph) output[index] = item;
329
+ if (item instanceof OmpErrors) {
330
+ if (errors) errors.append(item);
331
+ else errors = item;
332
+ } else if (morph) {
333
+ output[index] = item;
334
+ }
247
335
  }
248
336
  for (let index = prefixCount; index < ir.prefix.length; index++) {
249
337
  const item = ir.prefix[index];
250
338
  if (item.hasDefault && morph) {
251
339
  const payload = item.def;
252
- output[index] = item.defFactory && typeof payload === "function" ? payload() : payload;
340
+ if (item.defFactory && typeof payload === "function") {
341
+ path.push(index);
342
+ const resolved = visit(item.val, payload(), path);
343
+ path.pop();
344
+ if (resolved instanceof OmpErrors) {
345
+ if (errors) errors.append(resolved);
346
+ else errors = resolved;
347
+ } else {
348
+ output[index] = resolved;
349
+ }
350
+ } else {
351
+ output[index] = materializeDefault(payload);
352
+ }
253
353
  } else if (!item.opt) {
254
354
  path.push(index);
255
355
  const error = fail(path, expectedOf(item.val), MISSING);
256
356
  path.pop();
257
- return error;
357
+ if (errors) errors.append(error);
358
+ else errors = error;
258
359
  }
259
360
  }
260
361
  if (ir.variadic !== undefined) {
@@ -262,8 +363,12 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
262
363
  path.push(index);
263
364
  const item = visit(ir.variadic, v[index], path);
264
365
  path.pop();
265
- if (item instanceof OmpErrors) return item;
266
- if (morph) output[index] = item;
366
+ if (item instanceof OmpErrors) {
367
+ if (errors) errors.append(item);
368
+ else errors = item;
369
+ } else if (morph) {
370
+ output[index] = item;
371
+ }
267
372
  }
268
373
  }
269
374
  for (let index = 0; index < ir.postfix.length; index++) {
@@ -271,18 +376,28 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
271
376
  path.push(inputIndex);
272
377
  const item = visit(ir.postfix[index], v[inputIndex], path);
273
378
  path.pop();
274
- if (item instanceof OmpErrors) return item;
275
- if (morph) output[inputIndex] = item;
379
+ if (item instanceof OmpErrors) {
380
+ if (errors) errors.append(item);
381
+ else errors = item;
382
+ } else if (morph) {
383
+ output[inputIndex] = item;
384
+ }
276
385
  }
277
- return output;
386
+ return errors ?? output;
278
387
  }
279
388
  case "object": {
280
- if (typeof v !== "object" || v === null || Array.isArray(v)) return fail(path, "an object", v);
281
- const rec = v as Record<string, unknown>;
389
+ if (typeof v !== "object" || v === null) return fail(path, "an object", v);
390
+ const rec = v as Record<PropertyKey, unknown>;
282
391
  const morph = hasMorph(ir);
283
- let out: Record<string, unknown> | undefined;
392
+ let out: Record<PropertyKey, unknown> | undefined;
393
+ let errors: OmpErrors | undefined;
284
394
  if (morph) {
285
- if (ir.extras === "delete" && !ir.index) {
395
+ if (
396
+ ir.extras === "delete" &&
397
+ ir.index === undefined &&
398
+ ir.symbolIndex === undefined &&
399
+ ir.patternIndexes === undefined
400
+ ) {
286
401
  out = {};
287
402
  } else {
288
403
  out = { ...rec };
@@ -291,51 +406,155 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
291
406
  for (const p of ir.props) {
292
407
  if (!(p.key in rec)) {
293
408
  if (p.hasDefault && out) {
294
- // defFactory guarantees a callable default payload
295
409
  const payload = p.def;
296
- out[p.key] = p.defFactory && typeof payload === "function" ? payload() : payload;
410
+ if (p.defFactory && typeof payload === "function") {
411
+ path.push(p.key);
412
+ const resolved = visit(p.val, payload(), path);
413
+ path.pop();
414
+ if (resolved instanceof OmpErrors) {
415
+ if (errors) errors.append(resolved);
416
+ else errors = resolved;
417
+ } else {
418
+ out[p.key] = resolved;
419
+ }
420
+ } else {
421
+ out[p.key] = materializeDefault(payload);
422
+ }
297
423
  continue;
298
424
  }
299
425
  if (p.opt || p.hasDefault) continue;
300
426
  path.push(p.key);
301
- const err = fail(path, expectedOf(p.val), MISSING);
427
+ const error = fail(path, expectedOf(p.val), MISSING);
302
428
  path.pop();
303
- return err;
429
+ if (errors) errors.append(error);
430
+ else errors = error;
431
+ continue;
304
432
  }
305
433
  path.push(p.key);
306
- const res = visit(p.val, rec[p.key], path);
434
+ const result = visit(p.val, rec[p.key], path);
307
435
  path.pop();
308
- if (res instanceof OmpErrors) return res;
309
- if (out) out[p.key] = res;
436
+ if (result instanceof OmpErrors) {
437
+ if (errors) errors.append(result);
438
+ else errors = result;
439
+ } else if (out) {
440
+ out[p.key] = result;
441
+ }
310
442
  }
311
- if (ir.index) {
312
- for (const key in rec) {
313
- if (!own.call(rec, key)) continue;
443
+ for (const key in rec) {
444
+ if (!own.call(rec, key)) continue;
445
+ let indexed = false;
446
+ if (ir.index !== undefined) {
447
+ indexed = true;
314
448
  path.push(key);
315
- const res = visit(ir.index, rec[key], path);
449
+ const result = visit(ir.index, rec[key], path);
316
450
  path.pop();
317
- if (res instanceof OmpErrors) return res;
318
- if (out) out[key] = res;
319
- }
320
- } else if (ir.extras === "reject") {
321
- for (const key in rec) {
322
- if (!own.call(rec, key)) continue;
323
- let declared = false;
324
- for (const p of ir.props) {
325
- if (p.key === key) {
326
- declared = true;
327
- break;
328
- }
451
+ if (result instanceof OmpErrors) {
452
+ if (errors) errors.append(result);
453
+ else errors = result;
454
+ } else if (out) {
455
+ out[key] = result;
329
456
  }
330
- if (!declared) {
457
+ }
458
+ if (ir.patternIndexes !== undefined) {
459
+ for (const pattern of ir.patternIndexes) {
460
+ if (!checks(pattern.key, key)) continue;
461
+ indexed = true;
331
462
  path.push(key);
332
- const err = fail(path, "removed (undeclared key)", rec[key]);
463
+ const result = visit(pattern.val, rec[key], path);
333
464
  path.pop();
334
- return err;
465
+ if (result instanceof OmpErrors) {
466
+ if (errors) errors.append(result);
467
+ else errors = result;
468
+ } else if (out) {
469
+ out[key] = result;
470
+ }
335
471
  }
336
472
  }
473
+ if (ir.extras === "reject" && !indexed && !ir.props.some(prop => prop.key === key)) {
474
+ path.push(key);
475
+ const error = fail(path, "removed", rec[key]);
476
+ path.pop();
477
+ if (errors) errors.append(error);
478
+ else errors = error;
479
+ }
337
480
  }
338
- return out ?? v;
481
+ for (const key of Object.getOwnPropertySymbols(rec)) {
482
+ if (!Object.prototype.propertyIsEnumerable.call(rec, key)) continue;
483
+ if (ir.symbolIndex !== undefined) {
484
+ path.push(key);
485
+ const result = visit(ir.symbolIndex, rec[key], path);
486
+ path.pop();
487
+ if (result instanceof OmpErrors) {
488
+ if (errors) errors.append(result);
489
+ else errors = result;
490
+ } else if (out) {
491
+ out[key] = result;
492
+ }
493
+ } else if (ir.extras === "reject" && !ir.props.some(prop => prop.key === key)) {
494
+ path.push(key);
495
+ const error = fail(path, "removed", rec[key]);
496
+ path.pop();
497
+ if (errors) errors.append(error);
498
+ else errors = error;
499
+ }
500
+ }
501
+ return errors ?? out ?? v;
502
+ }
503
+ case "string": {
504
+ if (typeof v !== "string") return fail(path, "a string", v);
505
+ if (ir.min !== undefined && v.length < ir.min) return fail(path, `at least length ${ir.min}`, v.length);
506
+ if (ir.max !== undefined && v.length > ir.max) return fail(path, `at most length ${ir.max}`, v.length);
507
+ if (ir.url && !URL.canParse(v)) return fail(path, "a URL string", v);
508
+ return v;
509
+ }
510
+ case "number": {
511
+ if (typeof v !== "number" || !Number.isFinite(v)) return fail(path, ir.int ? "an integer" : "a number", v);
512
+ let errors: OmpErrors | undefined;
513
+ const add = (expected: string): void => {
514
+ const error = fail(path, expected, v);
515
+ if (errors) errors.append(error);
516
+ else errors = error;
517
+ };
518
+ if (ir.int && !Number.isInteger(v)) add("an integer");
519
+ if (ir.divisor !== undefined && v % ir.divisor !== 0) add(`a number divisible by ${ir.divisor}`);
520
+ if (ir.min !== undefined && (ir.xmin ? v <= ir.min : v < ir.min)) {
521
+ add(
522
+ ir.min === 0
523
+ ? ir.xmin
524
+ ? "positive"
525
+ : "non-negative"
526
+ : `a number ${ir.xmin ? "more than" : "at least"} ${ir.min}`,
527
+ );
528
+ }
529
+ if (ir.max !== undefined && (ir.xmax ? v >= ir.max : v > ir.max)) {
530
+ add(
531
+ ir.max === 0
532
+ ? ir.xmax
533
+ ? "negative"
534
+ : "non-positive"
535
+ : `a number ${ir.xmax ? "less than" : "at most"} ${ir.max}`,
536
+ );
537
+ }
538
+ return errors ?? v;
539
+ }
540
+ case "lit": {
541
+ if (checks(ir, v)) return v;
542
+ if ((typeof ir.v === "object" && ir.v !== null) || typeof ir.v === "function") {
543
+ let expected = "the specified reference";
544
+ try {
545
+ const serialized = JSON.stringify(ir.v);
546
+ if (serialized !== undefined) {
547
+ expected = `reference equal to ${serialized}`;
548
+ if (typeof v === "object" && v !== null && JSON.stringify(v) === serialized) {
549
+ expected += " (serialized to the same value)";
550
+ }
551
+ }
552
+ } catch {
553
+ // Cyclic values still get a useful reference-identity expectation.
554
+ }
555
+ return fail(path, expected, v);
556
+ }
557
+ return fail(path, expectedOf(ir), v);
339
558
  }
340
559
  default:
341
560
  return checks(ir, v) ? v : fail(path, expectedOf(ir), v);
@@ -349,16 +568,7 @@ function prefixAll(errs: OmpErrors, path: PropertyKey[]): OmpErrors {
349
568
 
350
569
  /** True when a union failure can be replaced with a more specific nested error. */
351
570
  export function canRefineUnionFailure(member: IR): boolean {
352
- const base = member.k === "sub" ? member.schema.ir : member;
353
- if (member.k === "sub") {
354
- return (
355
- base.k === "array" ||
356
- base.k === "object" ||
357
- base.k === "anyobject" ||
358
- base.k === "string" ||
359
- base.k === "number"
360
- );
361
- }
571
+ const base = unwrapBase(member);
362
572
  if (base.k === "array" || base.k === "object") return true;
363
573
  if (base.k === "string") return base.min !== undefined || base.max !== undefined || base.url === true;
364
574
  return base.k === "number" && (base.int === true || base.min !== undefined || base.max !== undefined);
@@ -373,39 +583,168 @@ export function canRefineUnionFailure(member: IR): boolean {
373
583
  */
374
584
  export function unionFail(ir: IR & { k: "union" }, v: unknown, path: PropertyKey[], expected?: string): OmpErrors {
375
585
  let best: IR | undefined;
376
- for (const m of ir.members) {
377
- const base = m.k === "sub" ? m.schema.ir : m;
586
+ for (const member of ir.members) {
587
+ const base = unwrapBase(member);
378
588
  if (!kindMatches(base, v)) continue;
379
589
  if (best !== undefined) {
380
590
  best = undefined;
381
591
  break;
382
592
  }
383
- best = m;
593
+ best = member;
594
+ }
595
+ if (best === undefined) {
596
+ const discriminated = discriminateFailure(ir.members, v, path);
597
+ if (discriminated !== undefined) return discriminated;
384
598
  }
385
- if (best === undefined) best = discriminate(ir.members, v);
386
- if (best) {
599
+ if (best !== undefined) {
387
600
  const out = visit(best, v, path);
388
601
  if (out instanceof OmpErrors) return out;
389
602
  }
603
+ if (ir.members.every(member => unwrapBase(member).k === "object")) {
604
+ const branches = ir.members.flatMap(member => {
605
+ const result = visit(member, v, path);
606
+ return result instanceof OmpErrors ? [[...result]] : [];
607
+ });
608
+ if (branches.length !== 0) {
609
+ const common = branches[0].filter(
610
+ (entry, index, first) =>
611
+ first.findIndex(candidate => pathsEqual(candidate.path, entry.path)) === index &&
612
+ branches.every(branch => branch.some(candidate => pathsEqual(candidate.path, entry.path))),
613
+ );
614
+ const alternatives: OmpErrors[] = [];
615
+ if (common.length !== 0) {
616
+ for (const entry of common) {
617
+ const expectations = new Set<string>();
618
+ for (const branch of branches) {
619
+ for (const candidate of branch) {
620
+ if (pathsEqual(candidate.path, entry.path)) {
621
+ expectations.add(candidate.expected.endsWith(" instance") ? "an object" : candidate.expected);
622
+ }
623
+ }
624
+ }
625
+ alternatives.push(
626
+ new OmpErrors(entry.path, [...expectations].join(" or "), entry.data, { preserveActual: true }),
627
+ );
628
+ }
629
+ } else {
630
+ for (const branch of branches) {
631
+ for (const entry of branch) {
632
+ alternatives.push(
633
+ new OmpErrors(
634
+ entry.path,
635
+ entry.expected.endsWith(" instance") ? "an object" : entry.expected,
636
+ entry.data,
637
+ { preserveActual: true },
638
+ ),
639
+ );
640
+ }
641
+ }
642
+ }
643
+ const combined = alternatives[0];
644
+ for (let index = 1; index < alternatives.length; index++) combined.append(alternatives[index]);
645
+ return alternatives.length === 1 ? combined : combined.asAlternatives();
646
+ }
647
+ }
390
648
  return fail(path, expected ?? expectedOf(ir), v);
391
649
  }
392
650
 
393
- /** Pick the sole object member whose literal-typed property matches the value's. */
394
- function discriminate(members: IR[], v: unknown): IR | undefined {
395
- if (typeof v !== "object" || v === null || Array.isArray(v)) return undefined;
396
- const rec = v as Record<string, unknown>;
397
- let match: IR | undefined;
398
- for (const m of members) {
399
- const base = m.k === "sub" ? m.schema.ir : m;
400
- if (base.k !== "object") continue;
401
- for (const p of base.props) {
402
- if (p.val.k !== "lit" || rec[p.key] !== p.val.v) continue;
403
- if (match !== undefined) return undefined; // ambiguous
404
- match = m;
405
- break;
651
+ interface LiteralDiscriminant {
652
+ path: PropertyKey[];
653
+ value: unknown;
654
+ }
655
+
656
+ function unwrapBase(member: IR, seen = new Set<IR>()): IR {
657
+ if (seen.has(member)) return member;
658
+ seen.add(member);
659
+ if (member.k === "sub") return unwrapBase(member.schema.ir, seen);
660
+ if (member.k === "alias") return unwrapBase(member.resolve(), seen);
661
+ if (member.k === "refine") return unwrapBase(member.base, seen);
662
+ return member;
663
+ }
664
+
665
+ function collectDiscriminants(member: IR, prefix: PropertyKey[] = [], seen = new Set<IR>()): LiteralDiscriminant[] {
666
+ if (seen.has(member)) return [];
667
+ seen.add(member);
668
+ if (member.k === "alias") return collectDiscriminants(member.resolve(), prefix, seen);
669
+ if (member.k === "sub") return collectDiscriminants(member.schema.ir, prefix, seen);
670
+ if (member.k === "refine") return collectDiscriminants(member.base, prefix, seen);
671
+ if (member.k !== "object") return [];
672
+ const result: LiteralDiscriminant[] = [];
673
+ for (const property of member.props) {
674
+ const propertyPath = [...prefix, property.key];
675
+ const value = unwrapBase(property.val);
676
+ if (value.k === "lit") result.push({ path: propertyPath, value: value.v });
677
+ else result.push(...collectDiscriminants(property.val, propertyPath, new Set(seen)));
678
+ }
679
+ return result;
680
+ }
681
+
682
+ function pathsEqual(left: readonly PropertyKey[], right: readonly PropertyKey[]): boolean {
683
+ return left.length === right.length && left.every((key, index) => key === right[index]);
684
+ }
685
+
686
+ function valueAtPath(value: unknown, path: readonly PropertyKey[]): { present: boolean; value?: unknown } {
687
+ let cursor = value;
688
+ for (const key of path) {
689
+ if ((typeof cursor !== "object" && typeof cursor !== "function") || cursor === null || !(key in cursor)) {
690
+ return { present: false };
691
+ }
692
+ cursor = (cursor as Record<PropertyKey, unknown>)[key];
693
+ }
694
+ return { present: true, value: cursor };
695
+ }
696
+
697
+ function discriminateFailure(members: IR[], value: unknown, path: PropertyKey[]): OmpErrors | undefined {
698
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
699
+ const byMember = members.map(member => collectDiscriminants(member));
700
+ const candidates: { path: PropertyKey[]; distinct: number; declared: number }[] = [];
701
+ for (const discriminants of byMember) {
702
+ for (const discriminant of discriminants) {
703
+ if (candidates.some(candidate => pathsEqual(candidate.path, discriminant.path))) continue;
704
+ const values: unknown[] = [];
705
+ let declared = 0;
706
+ for (const branch of byMember) {
707
+ const match = branch.find(candidate => pathsEqual(candidate.path, discriminant.path));
708
+ if (match === undefined) continue;
709
+ declared++;
710
+ if (!values.some(candidate => Object.is(candidate, match.value))) values.push(match.value);
711
+ }
712
+ if (values.length > 1) candidates.push({ path: discriminant.path, distinct: values.length, declared });
713
+ }
714
+ }
715
+ candidates.sort((left, right) => right.distinct - left.distinct || right.declared - left.declared);
716
+ for (const candidate of candidates) {
717
+ const actual = valueAtPath(value, candidate.path);
718
+ const exact: IR[] = [];
719
+ const defaults: IR[] = [];
720
+ const expectedMembers: IR[] = [];
721
+ for (let index = 0; index < members.length; index++) {
722
+ const discriminant = byMember[index].find(item => pathsEqual(item.path, candidate.path));
723
+ if (discriminant === undefined) {
724
+ defaults.push(members[index]);
725
+ continue;
726
+ }
727
+ expectedMembers.push({ k: "lit", v: discriminant.value });
728
+ if (actual.present && Object.is(actual.value, discriminant.value)) exact.push(members[index]);
729
+ }
730
+ if (!actual.present) {
731
+ if (defaults.length !== 0 && defaults.length < members.length) {
732
+ return discriminateFailure(defaults, value, path);
733
+ }
734
+ return fail([...path, ...candidate.path], expectedOf({ k: "union", members: expectedMembers }), undefined);
735
+ }
736
+ if (exact.length === 0) {
737
+ if (defaults.length !== 0) return discriminateFailure(defaults, value, path);
738
+ return fail([...path, ...candidate.path], expectedOf({ k: "union", members: expectedMembers }), actual.value);
739
+ }
740
+ if (exact.length === 1) {
741
+ const result = visit(exact[0], value, path);
742
+ return result instanceof OmpErrors ? result : undefined;
406
743
  }
744
+ const nested = discriminateFailure(exact, value, path);
745
+ if (nested !== undefined) return nested;
407
746
  }
408
- return match;
747
+ return undefined;
409
748
  }
410
749
 
411
750
  /** True when a value's runtime shape could only be aimed at this member. */