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