@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/ir.js CHANGED
@@ -14,7 +14,12 @@ import { keywordIR, patternIR, templateIR } from "./keywords.js";
14
14
  /** Brand carried by `Type` instances so the parser can embed them in defs. */
15
15
  export const IR_BRAND = Symbol("omptype.schema");
16
16
  const kMorph = Symbol("omptype.hasMorph");
17
- const SIMPLE_OPS = "|()[]=?%";
17
+ const kMorphOwner = Symbol("omptype.hasMorphOwner");
18
+ const kAlias = Symbol("omptype.hasAlias");
19
+ const kAliasOwner = Symbol("omptype.hasAliasOwner");
20
+ const kSimple = Symbol("omptype.simple");
21
+ const kSimpleOwner = Symbol("omptype.simpleOwner");
22
+ const SIMPLE_OPS = "|&()[]=?%,#";
18
23
  function tokenize(src) {
19
24
  const toks = [];
20
25
  let i = 0;
@@ -25,33 +30,74 @@ function tokenize(src) {
25
30
  i++;
26
31
  continue;
27
32
  }
28
- if (c === "'" || c === '"') {
29
- let j = i + 1;
30
- while (j < n && src[j] !== c)
31
- j++;
32
- if (j >= n)
33
- throw new OmpTypeError(`unterminated string literal in "${src}"`);
34
- toks.push({ t: "str", v: src.slice(i + 1, j) });
35
- i = j + 1;
36
- continue;
37
- }
38
33
  if (c === "d" && (src[i + 1] === "'" || src[i + 1] === '"')) {
39
34
  const quote = src[i + 1];
40
35
  const end = src.indexOf(quote, i + 2);
41
36
  if (end < 0)
42
37
  throw new OmpTypeError(`unterminated date literal in "${src}"`);
43
- const value = new Date(src.slice(i + 2, end));
38
+ const source = src.slice(i + 2, end).trim();
39
+ const value = /^\d+$/.test(source) ? new Date(Number(source)) : new Date(source);
44
40
  if (Number.isNaN(value.valueOf()))
45
41
  throw new OmpTypeError(`invalid date literal in "${src}"`);
46
42
  toks.push({ t: "date", v: value });
47
43
  i = end + 1;
48
44
  continue;
49
45
  }
46
+ if (c === "'" || c === '"') {
47
+ let j = i + 1;
48
+ let value = "";
49
+ for (; j < n && src[j] !== c; j++) {
50
+ if (src[j] === "\\") {
51
+ j++;
52
+ if (j >= n)
53
+ break;
54
+ }
55
+ value += src[j];
56
+ }
57
+ if (j >= n)
58
+ throw new OmpTypeError(`unterminated string literal in "${src}"`);
59
+ toks.push({ t: "str", v: value });
60
+ i = j + 1;
61
+ continue;
62
+ }
63
+ if (c === "/") {
64
+ let j = i + 1;
65
+ for (; j < n; j++) {
66
+ if (src[j] === "\\")
67
+ j++;
68
+ else if (src[j] === "/")
69
+ break;
70
+ }
71
+ if (j >= n)
72
+ throw new OmpTypeError(`unterminated regular expression in "${src}"`);
73
+ let end = j + 1;
74
+ while (end < n && /[dgimsuvy]/.test(src[end]))
75
+ end++;
76
+ const source = src.slice(i + 1, j);
77
+ const flags = src.slice(j + 1, end);
78
+ try {
79
+ toks.push({ t: "regex", v: new RegExp(source, flags) });
80
+ }
81
+ catch {
82
+ throw new OmpTypeError(`invalid regular expression "${src.slice(i, end)}"`);
83
+ }
84
+ i = end;
85
+ continue;
86
+ }
50
87
  if ((c >= "0" && c <= "9") || (c === "-" && i + 1 < n && src[i + 1] >= "0" && src[i + 1] <= "9")) {
51
88
  let j = i + 1;
52
- while (j < n && ((src[j] >= "0" && src[j] <= "9") || src[j] === "." || src[j] === "e" || src[j] === "+"))
89
+ while (j < n && /[\w.+-]/.test(src[j]))
53
90
  j++;
54
- toks.push({ t: "num", v: Number(src.slice(i, j)) });
91
+ const raw = src.slice(i, j);
92
+ if (/^-?(?:0|[1-9]\d*)n$/.test(raw) && raw !== "-0n") {
93
+ toks.push({ t: "bigint", v: BigInt(raw.slice(0, -1)) });
94
+ }
95
+ else {
96
+ const valid = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(raw) && !Object.is(Number(raw), -0) && String(Number(raw)) === raw;
97
+ if (!valid)
98
+ throw new OmpTypeError(`Malformed number literal '${raw}'`);
99
+ toks.push({ t: "num", v: Number(raw) });
100
+ }
55
101
  i = j;
56
102
  continue;
57
103
  }
@@ -74,6 +120,11 @@ function tokenize(src) {
74
120
  }
75
121
  continue;
76
122
  }
123
+ if (c === "=" && src[i + 1] === "=") {
124
+ toks.push({ t: "op", v: "==" });
125
+ i += 2;
126
+ continue;
127
+ }
77
128
  if (SIMPLE_OPS.includes(c)) {
78
129
  toks.push({ t: "op", v: c });
79
130
  i++;
@@ -101,6 +152,16 @@ const KEYWORDS = {
101
152
  true: () => ({ k: "lit", v: true }),
102
153
  false: () => ({ k: "lit", v: false }),
103
154
  };
155
+ /**
156
+ * Resolvers that only intercept the `this` self-reference. A parse under such
157
+ * a resolver of a source with no `this` token is identical to a resolver-free
158
+ * parse, so it may read and populate the string-definition cache.
159
+ */
160
+ const THIS_ONLY_RESOLVERS = new WeakSet();
161
+ /** Declare that `resolve` only intercepts `this` (see THIS_ONLY_RESOLVERS). */
162
+ export function markThisOnlyResolver(resolve) {
163
+ THIS_ONLY_RESOLVERS.add(resolve);
164
+ }
104
165
  class StrParser {
105
166
  #toks;
106
167
  #pos = 0;
@@ -135,12 +196,14 @@ class StrParser {
135
196
  let hasDefault = false;
136
197
  if (this.#eatOp("=")) {
137
198
  const t = this.#next();
138
- if (t.t === "num" || t.t === "str")
199
+ if (t.t === "num" || t.t === "bigint" || t.t === "date" || t.t === "str")
139
200
  def = t.v;
140
201
  else if (t.t === "id" && (t.v === "true" || t.v === "false"))
141
202
  def = t.v === "true";
142
203
  else if (t.t === "id" && t.v === "null")
143
204
  def = null;
205
+ else if (t.t === "id" && t.v === "undefined")
206
+ def = undefined;
144
207
  else
145
208
  throw new OmpTypeError(`unsupported default literal in "${this.#src}"`);
146
209
  hasDefault = true;
@@ -155,14 +218,36 @@ class StrParser {
155
218
  }
156
219
  }
157
220
  parseUnion() {
158
- const first = this.parseBounded();
221
+ const first = this.parseIntersection();
159
222
  if (!this.#eatOp("|"))
160
223
  return first;
161
- const members = [first, this.parseBounded()];
224
+ const members = [first, this.parseIntersection()];
162
225
  while (this.#eatOp("|"))
163
- members.push(this.parseBounded());
226
+ members.push(this.parseIntersection());
164
227
  return { k: "union", members };
165
228
  }
229
+ parseIntersection() {
230
+ const first = this.parseBounded();
231
+ if (!this.#eatOp("&"))
232
+ return first;
233
+ const members = [first, this.parseBounded()];
234
+ while (this.#eatOp("&"))
235
+ members.push(this.parseBounded());
236
+ const literal = members.find((member) => member.k === "lit");
237
+ if (literal && typeof literal.v === "number") {
238
+ for (const member of members) {
239
+ if (member.k === "number" &&
240
+ ((member.int && !Number.isInteger(literal.v)) ||
241
+ (member.divisor !== undefined && literal.v % member.divisor !== 0) ||
242
+ (member.min !== undefined && (member.xmin ? literal.v <= member.min : literal.v < member.min)) ||
243
+ (member.max !== undefined && (member.xmax ? literal.v >= member.max : literal.v > member.max)))) {
244
+ throw new OmpTypeError("literal is excluded by intersection");
245
+ }
246
+ }
247
+ return literal;
248
+ }
249
+ return { k: "intersection", members };
250
+ }
166
251
  /**
167
252
  * `NUM CMP base (CMP NUM)?` or `base (CMP NUM)?`, with `[]*` postfix on the
168
253
  * base AND after a trailing bound — `string>0[]` is an array of bounded
@@ -171,30 +256,39 @@ class StrParser {
171
256
  parseBounded() {
172
257
  const t = this.#peek();
173
258
  const t1 = this.#peek(1);
174
- if ((t?.t === "num" || t?.t === "date") && t1?.t === "op" && CMP[t1.v]) {
259
+ if ((t?.t === "num" || t?.t === "date") && t1?.t === "op" && (t1.v === "<" || t1.v === "<=")) {
175
260
  const lo = t.v;
176
261
  this.#pos += 2;
177
262
  let node = this.#eatDivisor(this.parsePostfix());
178
263
  node = applyBound(node, flip(t1.v), lo, this.#src);
179
264
  const t2 = this.#peek();
180
- if (t2?.t === "op" && CMP[t2.v]) {
181
- this.#pos++;
182
- const hi = this.#next();
183
- if (hi.t !== "num" && hi.t !== "date") {
184
- throw new OmpTypeError(`expected bound after comparator in "${this.#src}"`);
185
- }
186
- node = applyBound(node, t2.v, hi.v, this.#src);
265
+ if (!(t2?.t === "op" && CMP[t2.v])) {
266
+ throw new OmpTypeError(`left bound requires a corresponding right bound in "${this.#src}"`);
267
+ }
268
+ if (t2.v === ">" || t2.v === ">=") {
269
+ throw new OmpTypeError(`right bound must use < or <= in "${this.#src}"`);
187
270
  }
271
+ this.#pos++;
272
+ const hi = this.#next();
273
+ if (hi.t !== "num" && hi.t !== "date") {
274
+ throw new OmpTypeError(`expected bound after comparator in "${this.#src}"`);
275
+ }
276
+ node = applyBound(node, t2.v, hi.v, this.#src);
188
277
  return this.#eatArraySuffixes(node);
189
278
  }
190
279
  let node = this.#eatDivisor(this.parsePostfix());
191
280
  const t2 = this.#peek();
192
- if (t2?.t === "op" && CMP[t2.v]) {
281
+ if (t2?.t === "op" && t2.v === "==") {
193
282
  this.#pos++;
194
283
  const limit = this.#next();
195
- if (limit.t !== "num" && limit.t !== "date") {
196
- throw new OmpTypeError(`expected bound after comparator in "${this.#src}"`);
284
+ if (limit.t !== "num" && limit.t !== "bigint" && limit.t !== "date") {
285
+ throw new OmpTypeError(`expected literal after == in "${this.#src}"`);
197
286
  }
287
+ node = applyEquality(node, limit.v, this.#src);
288
+ }
289
+ else if (t2?.t === "op" && CMP[t2.v] && (this.#peek(1)?.t === "num" || this.#peek(1)?.t === "date")) {
290
+ this.#pos++;
291
+ const limit = this.#next();
198
292
  node = applyBound(node, t2.v, limit.v, this.#src);
199
293
  node = this.#eatArraySuffixes(node);
200
294
  }
@@ -208,10 +302,11 @@ class StrParser {
208
302
  throw new OmpTypeError(`expected number after % in "${this.#src}"`);
209
303
  if (node.k !== "number")
210
304
  throw new OmpTypeError(`% requires number in "${this.#src}"`);
211
- if (!Number.isFinite(divisor.v) || divisor.v === 0)
212
- throw new OmpTypeError(`divisor must be non-zero in "${this.#src}"`);
213
- node.divisor = divisor.v;
214
- return node;
305
+ if (!Number.isFinite(divisor.v) || !Number.isInteger(divisor.v) || divisor.v === 0)
306
+ throw new OmpTypeError(`divisor must be a non-zero integer in "${this.#src}"`);
307
+ // Copy-on-write: the primary may be a shared node (string-def cache,
308
+ // generic arguments); stamping it in place would leak into other schemas.
309
+ return { ...node, divisor: Math.abs(divisor.v) };
215
310
  }
216
311
  /** Wrap `node` in array IR for each `[]` pair at the cursor. */
217
312
  #eatArraySuffixes(node) {
@@ -229,15 +324,41 @@ class StrParser {
229
324
  let node = this.parsePrimary();
230
325
  for (;;) {
231
326
  const t = this.#peek();
232
- if (!(t?.t === "op" && t.v === "["))
233
- break;
234
- this.#pos++;
235
- if (!this.#eatOp("]"))
236
- throw new OmpTypeError(`expected ']' in "${this.#src}"`);
237
- node = { k: "array", el: node };
327
+ if (t?.t === "op" && t.v === "[") {
328
+ this.#pos++;
329
+ if (!this.#eatOp("]"))
330
+ throw new OmpTypeError(`expected ']' in "${this.#src}"`);
331
+ node = { k: "array", el: node };
332
+ continue;
333
+ }
334
+ if (t?.t === "op" && t.v === "#") {
335
+ this.#pos++;
336
+ const name = this.#next();
337
+ if (name.t !== "id")
338
+ throw new OmpTypeError(`expected brand name after # in "${this.#src}"`);
339
+ continue;
340
+ }
341
+ break;
238
342
  }
239
343
  return node;
240
344
  }
345
+ #parseGenericArguments() {
346
+ this.#eatOp("<");
347
+ const arguments_ = [];
348
+ if (this.#eatOp(">"))
349
+ return arguments_;
350
+ for (;;) {
351
+ arguments_.push(this.parseUnion());
352
+ if (this.#eatOp(">"))
353
+ return arguments_;
354
+ if (!this.#eatOp(","))
355
+ throw new OmpTypeError(`expected ',' or '>' in "${this.#src}"`);
356
+ const next = this.#peek();
357
+ if (next?.t === "op" && (next.v === "," || next.v === ">")) {
358
+ throw new OmpTypeError(`generic arguments cannot be empty in "${this.#src}"`);
359
+ }
360
+ }
361
+ }
241
362
  parsePrimary() {
242
363
  const t = this.#next();
243
364
  if (t.t === "op" && t.v === "(") {
@@ -246,11 +367,50 @@ class StrParser {
246
367
  throw new OmpTypeError(`expected ')' in "${this.#src}"`);
247
368
  return inner;
248
369
  }
249
- if (t.t === "str" || t.t === "num" || t.t === "date")
370
+ if (t.t === "str" || t.t === "num" || t.t === "bigint" || t.t === "date")
250
371
  return { k: "lit", v: t.v };
372
+ if (t.t === "regex")
373
+ return patternIR(t.v);
251
374
  if (t.t === "id") {
375
+ if (t.v === "keyof") {
376
+ try {
377
+ return keyOf(this.parsePostfix());
378
+ }
379
+ catch (error) {
380
+ if (error instanceof OmpTypeError)
381
+ throw new OmpTypeError("keyof operand must be an object");
382
+ throw error;
383
+ }
384
+ }
385
+ if (t.v === "Array.liftFrom" && this.#peek()?.t === "op" && this.#peek()?.v === "<") {
386
+ this.#pos++;
387
+ const element = this.parsePrimary();
388
+ if (!this.#eatOp(">"))
389
+ throw new OmpTypeError(`expected '>' in "${this.#src}"`);
390
+ const array = { k: "array", el: element, desc: "an object" };
391
+ return {
392
+ k: "morph",
393
+ input: { k: "union", members: [element, array] },
394
+ fn: value => (Array.isArray(value) ? value : [value]),
395
+ out: array,
396
+ };
397
+ }
398
+ if (t.v === "Record" && this.#peek()?.t === "op" && this.#peek()?.v === "<") {
399
+ const arguments_ = this.#parseGenericArguments();
400
+ if (arguments_.length !== 2)
401
+ throw new OmpTypeError("Record requires two arguments");
402
+ return { k: "object", props: [], index: arguments_[1], extras: "keep" };
403
+ }
404
+ if (this.#peek()?.t === "op" && this.#peek()?.v === "<" && this.#resolve?.hasGeneric?.(t.v)) {
405
+ const arguments_ = this.#parseGenericArguments();
406
+ const instantiated = this.#resolve.generic?.(t.v, arguments_);
407
+ if (!instantiated)
408
+ throw new OmpTypeError(`unknown generic "${t.v}" in "${this.#src}"`);
409
+ return instantiated;
410
+ }
411
+ const scoped = this.#resolve?.(t.v);
252
412
  const make = KEYWORDS[t.v];
253
- const keyword = make?.() ?? keywordIR(t.v) ?? this.#resolve?.(t.v);
413
+ const keyword = scoped ?? make?.() ?? keywordIR(t.v);
254
414
  if (!keyword)
255
415
  throw new OmpTypeError(`unknown keyword "${t.v}" in "${this.#src}"`);
256
416
  return keyword;
@@ -282,7 +442,19 @@ function parseLiteralUnion(src) {
282
442
  while (index < src.length && isWhitespaceAt(src, index))
283
443
  index++;
284
444
  if (index === src.length) {
285
- return members.length === 1 ? members[0] : { k: "union", members };
445
+ const ir = members.length === 1 ? members[0] : { k: "union", members };
446
+ let simple = true;
447
+ for (let member = 1; simple && member < members.length; member++) {
448
+ for (let previous = 0; previous < member; previous++) {
449
+ if (members[previous].k === "lit" && members[previous].v === members[member].v) {
450
+ simple = false;
451
+ break;
452
+ }
453
+ }
454
+ }
455
+ ir[kSimple] = simple;
456
+ ir[kSimpleOwner] = ir;
457
+ return ir;
286
458
  }
287
459
  if (src[index] !== "|")
288
460
  return undefined;
@@ -365,6 +537,10 @@ function mergeObjectIR(left, right) {
365
537
  k: "object",
366
538
  props,
367
539
  index: right.index ?? left.index,
540
+ symbolIndex: right.symbolIndex ?? left.symbolIndex,
541
+ patternIndexes: left.patternIndexes === undefined && right.patternIndexes === undefined
542
+ ? undefined
543
+ : [...(left.patternIndexes ?? []), ...(right.patternIndexes ?? [])],
368
544
  extras: right.extras === "keep" ? left.extras : right.extras,
369
545
  };
370
546
  }
@@ -372,9 +548,22 @@ function parseGeneric(src, resolve) {
372
548
  const generic = genericArguments(src);
373
549
  if (generic === undefined)
374
550
  return undefined;
551
+ if (generic.name === "Array.liftFrom" && generic.args.length === 1) {
552
+ const element = parseDef(generic.args[0], resolve);
553
+ const array = { k: "array", el: element, desc: "an object" };
554
+ return {
555
+ k: "morph",
556
+ input: { k: "union", members: [element, array] },
557
+ fn: value => (Array.isArray(value) ? value : [value]),
558
+ out: array,
559
+ };
560
+ }
375
561
  if (generic.name === "Record" && generic.args.length === 2) {
376
562
  return { k: "object", props: [], index: parseDef(generic.args[1], resolve), extras: "keep" };
377
563
  }
564
+ if ((generic.name === "Extract" || generic.name === "Exclude") && generic.args.length === 2) {
565
+ return distributeFilter(parseDef(generic.args[0], resolve), parseDef(generic.args[1], resolve), generic.name === "Extract");
566
+ }
378
567
  if ((generic.name === "Partial" || generic.name === "Required") && generic.args.length === 1) {
379
568
  const object = resolveStructuralIR(parseDef(generic.args[0], resolve));
380
569
  if (object.k !== "object")
@@ -395,82 +584,88 @@ function parseGeneric(src, resolve) {
395
584
  }
396
585
  return undefined;
397
586
  }
398
- function dateLiteral(source) {
399
- const value = new Date(source);
400
- if (!Number.isFinite(value.valueOf()))
401
- throw new OmpTypeError(`invalid Date literal "${source}"`);
402
- return value;
587
+ /**
588
+ * Subtype comparison lives in `type.ts` (it needs full traversal), so it is
589
+ * installed here at module load for the parser's `Extract`/`Exclude` support.
590
+ */
591
+ let isAssignable = () => false;
592
+ /** Install the assignability comparator used by `Extract`/`Exclude`. */
593
+ export function useAssignability(compare) {
594
+ isAssignable = compare;
403
595
  }
404
- function parseDateExpression(src) {
405
- const first = src.charCodeAt(0);
406
- if (first !== 68 && first !== 100)
407
- return undefined;
408
- const literal = src.match(/^d(['"])(.*)\1$/);
409
- if (literal)
410
- return { k: "lit", v: dateLiteral(literal[2]) };
411
- const forward = src.match(/^Date\s*(<=|<|>=|>)\s*d(['"])(.*)\2$/);
412
- const reverse = src.match(/^d(['"])(.*)\1\s*(<=|<|>=|>)\s*Date$/);
413
- if (!forward && !reverse)
596
+ /**
597
+ * Distribute `base` over its union members, keeping those assignable to
598
+ * `target` (`keepAssignable`) or those that are not (`Exclude`).
599
+ */
600
+ export function distributeFilter(base, target, keepAssignable) {
601
+ const resolved = base.k === "alias" ? base.resolve() : base;
602
+ const members = resolved.k === "union" ? resolved.members : [resolved];
603
+ const retained = members.filter(member => isAssignable(member, target) === keepAssignable);
604
+ if (retained.length === 0)
605
+ return { k: "never" };
606
+ return retained.length === 1 ? retained[0] : { k: "union", members: retained };
607
+ }
608
+ /** Parse recurring global DSL fragments once; scoped aliases bypass the cache. */
609
+ function parseRegexExec(src) {
610
+ if (!src.startsWith("x/"))
414
611
  return undefined;
415
- const bound = dateLiteral(forward ? forward[3] : (reverse?.[2] ?? ""));
416
- const operator = forward
417
- ? forward[1]
418
- : reverse?.[3] === "<="
419
- ? ">="
420
- : reverse?.[3] === "<"
421
- ? ">"
422
- : reverse?.[3] === ">="
423
- ? "<="
424
- : "<";
425
- const timestamp = bound.valueOf();
426
- const relation = operator === ">="
427
- ? "at or after"
428
- : operator === ">"
429
- ? "later than"
430
- : operator === "<="
431
- ? "at or before"
432
- : "earlier than";
612
+ const end = src.lastIndexOf("/");
613
+ if (end < 2)
614
+ throw new OmpTypeError(`unterminated regular expression in "${src}"`);
615
+ let regex;
616
+ try {
617
+ regex = new RegExp(src.slice(2, end), src.slice(end + 1));
618
+ }
619
+ catch {
620
+ throw new OmpTypeError(`invalid regular expression "${src.slice(1)}"`);
621
+ }
433
622
  return {
434
- k: "refine",
435
- base: { k: "instance", ctor: Date, expected: "a Date" },
436
- pred: value => {
437
- if (!(value instanceof Date))
438
- return false;
439
- const actual = value.valueOf();
440
- return operator === ">="
441
- ? actual >= timestamp
442
- : operator === ">"
443
- ? actual > timestamp
444
- : operator === "<="
445
- ? actual <= timestamp
446
- : actual < timestamp;
623
+ k: "morph",
624
+ input: patternIR(regex),
625
+ fn: (value, context) => {
626
+ regex.lastIndex = 0;
627
+ return regex.exec(value) ?? context.error(`a string matching ${regex}`);
447
628
  },
448
- expected: `a Date ${relation} ${bound.toISOString()}`,
449
- json: operator === ">=" || operator === ">" ? { minimum: bound.toISOString() } : { maximum: bound.toISOString() },
450
629
  };
451
630
  }
452
- /** Parse recurring global DSL fragments once; scoped aliases bypass the cache. */
453
631
  function parseStringDef(src, resolve) {
454
- if (resolve === undefined) {
632
+ const cacheable = resolve === undefined || (!src.includes("this") && THIS_ONLY_RESOLVERS.has(resolve));
633
+ if (cacheable) {
455
634
  const cached = stringDefCache.get(src);
456
635
  if (cached)
457
636
  return cached;
458
637
  }
459
- let ir = parseDateExpression(src) ?? parseLiteralUnion(src) ?? parseGeneric(src, resolve);
460
- if (ir === undefined && src.startsWith("`") && src.endsWith("`")) {
461
- ir = templateIR(src.slice(1, -1));
462
- }
463
- else if (ir === undefined && src.startsWith("/") && src.lastIndexOf("/") > 0) {
464
- const end = src.lastIndexOf("/");
465
- try {
466
- ir = patternIR(new RegExp(src.slice(1, end), src.slice(end + 1)));
638
+ const pipeIndex = src.indexOf("|>");
639
+ if (pipeIndex >= 0) {
640
+ const input = src.slice(0, pipeIndex).trim();
641
+ const output = src.slice(pipeIndex + 2).trim();
642
+ if (input.length === 0 || output.length === 0) {
643
+ throw new OmpTypeError(`pipe expression requires operands in "${src}"`);
467
644
  }
468
- catch {
469
- throw new OmpTypeError(`invalid regular expression "${src}"`);
645
+ const parsedInput = parseStringDef(input, resolve);
646
+ if (parsedInput.hasDefault) {
647
+ throw new OmpTypeError(`unexpected pipe expression after default in "${src}"`);
470
648
  }
649
+ const parsed = {
650
+ ir: {
651
+ k: "morph",
652
+ input: parsedInput.ir,
653
+ fn: value => value,
654
+ out: parseStringDef(output, resolve).ir,
655
+ },
656
+ hasDefault: false,
657
+ optional: false,
658
+ };
659
+ if (cacheable && stringDefCache.size < STRING_DEF_CACHE_MAX)
660
+ stringDefCache.set(src, parsed);
661
+ return parsed;
662
+ }
663
+ let ir = parseLiteralUnion(src) ?? parseRegexExec(src) ?? parseGeneric(src, resolve);
664
+ if (ir === undefined && src.startsWith("`") && src.endsWith("`")) {
665
+ ir = templateIR(src.slice(1, -1));
471
666
  }
472
667
  const parsed = ir === undefined ? new StrParser(src, resolve).parseTop() : { ir, hasDefault: false, optional: false };
473
- if (resolve === undefined && stringDefCache.size < STRING_DEF_CACHE_MAX)
668
+ if (cacheable && stringDefCache.size < STRING_DEF_CACHE_MAX)
474
669
  stringDefCache.set(src, parsed);
475
670
  return parsed;
476
671
  }
@@ -486,8 +681,47 @@ function flip(op) {
486
681
  return "<=";
487
682
  }
488
683
  }
489
- /** Apply `node CMP value` — numeric/string/array ranges or Date bounds. */
684
+ function applyEquality(node, value, src) {
685
+ if (node.k === "union") {
686
+ return { k: "union", members: node.members.map(member => applyEquality(member, value, src)) };
687
+ }
688
+ if (value instanceof Date) {
689
+ if (!acceptsDate(node))
690
+ throw new OmpTypeError(`Date equality requires Date in "${src}"`);
691
+ return { k: "lit", v: value };
692
+ }
693
+ if (node.k === "number" && typeof value === "number")
694
+ return { k: "lit", v: value };
695
+ if (node.k === "bigint" && typeof value === "bigint")
696
+ return { k: "lit", v: value };
697
+ if ((node.k === "string" || node.k === "array") && typeof value === "number") {
698
+ if (!Number.isInteger(value) || value < 0) {
699
+ throw new OmpTypeError(`exact length must be a non-negative integer in "${src}"`);
700
+ }
701
+ return { ...node, min: value, max: value };
702
+ }
703
+ throw new OmpTypeError(`equality literal is incompatible with ${node.k} in "${src}"`);
704
+ }
705
+ /**
706
+ * Apply `node CMP value` — numeric/string/array ranges or Date bounds.
707
+ * Copy-on-write: `node` may be shared (string-def cache, generic arguments,
708
+ * resolved aliases), so bounds land on a fresh node, never in place.
709
+ */
490
710
  function applyBound(node, op, value, src) {
711
+ if (node.k === "alias")
712
+ return applyBound(node.resolve(), op, value, src);
713
+ if (node.k === "refine" && !(value instanceof Date)) {
714
+ return { ...node, base: applyBound(node.base, op, value, src) };
715
+ }
716
+ if (node.k === "union") {
717
+ const kinds = new Set(node.members.map(boundKind));
718
+ if (kinds.size !== 1)
719
+ throw new OmpTypeError(`cannot apply one bound to multiple bound kinds in "${src}"`);
720
+ return { k: "union", members: node.members.map(member => applyBound(member, op, value, src)) };
721
+ }
722
+ if (!(value instanceof Date) && acceptsDate(node)) {
723
+ return applyBound(node, op, new Date(value), src);
724
+ }
491
725
  if (value instanceof Date) {
492
726
  if (!acceptsDate(node))
493
727
  throw new OmpTypeError(`date bound requires Date in "${src}"`);
@@ -506,42 +740,56 @@ function applyBound(node, op, value, src) {
506
740
  };
507
741
  }
508
742
  if (node.k === "number") {
743
+ const bounded = { ...node };
509
744
  switch (op) {
510
745
  case ">=":
511
- node.min = value;
512
- node.xmin = false;
746
+ bounded.min = value;
747
+ bounded.xmin = false;
513
748
  break;
514
749
  case ">":
515
- node.min = value;
516
- node.xmin = true;
750
+ bounded.min = value;
751
+ bounded.xmin = true;
517
752
  break;
518
753
  case "<=":
519
- node.max = value;
520
- node.xmax = false;
754
+ bounded.max = value;
755
+ bounded.xmax = false;
521
756
  break;
522
757
  case "<":
523
- node.max = value;
524
- node.xmax = true;
758
+ bounded.max = value;
759
+ bounded.xmax = true;
525
760
  break;
526
761
  }
527
- return node;
762
+ if (bounded.min !== undefined &&
763
+ bounded.max !== undefined &&
764
+ (bounded.min > bounded.max || (bounded.min === bounded.max && (bounded.xmin || bounded.xmax)))) {
765
+ throw new OmpTypeError(`numeric range is unsatisfiable in "${src}"`);
766
+ }
767
+ return bounded;
528
768
  }
529
769
  if (node.k === "string" || node.k === "array") {
770
+ if (!Number.isInteger(value) || value < 0) {
771
+ throw new OmpTypeError(`length bound must be a non-negative integer in "${src}"`);
772
+ }
773
+ const bounded = { ...node };
530
774
  switch (op) {
531
775
  case ">=":
532
- node.min = value;
776
+ bounded.min = value;
533
777
  break;
534
778
  case ">":
535
- node.min = value + 1;
779
+ bounded.min = value + 1;
536
780
  break;
537
781
  case "<=":
538
- node.max = value;
782
+ bounded.max = value;
539
783
  break;
540
784
  case "<":
541
- node.max = value - 1;
785
+ bounded.max = value - 1;
542
786
  break;
543
787
  }
544
- return node;
788
+ if ((bounded.min !== undefined && bounded.max !== undefined && bounded.min > bounded.max) ||
789
+ (bounded.max ?? 0) < 0) {
790
+ throw new OmpTypeError(`length range is unsatisfiable in "${src}"`);
791
+ }
792
+ return bounded;
545
793
  }
546
794
  throw new OmpTypeError(`cannot bound ${node.k} in "${src}"`);
547
795
  }
@@ -555,12 +803,21 @@ function isEmbedded(def) {
555
803
  /** Embed a schema value: inline pure structure, keep `sub` nodes for stepped schemas. */
556
804
  export function embed(schema) {
557
805
  if (schema.hasSteps)
558
- return { k: "sub", schema, desc: schema.description };
806
+ return { k: "sub", schema, desc: schema.description, descAuto: schema.ir.desc === undefined };
559
807
  if (schema.description !== undefined && schema.ir.desc === undefined) {
560
- return { ...schema.ir, desc: schema.description };
808
+ return { ...schema.ir, desc: schema.description, descAuto: true };
561
809
  }
562
810
  return schema.ir;
563
811
  }
812
+ function boundKind(node) {
813
+ if (node.k === "number")
814
+ return "number";
815
+ if (node.k === "string" || node.k === "array")
816
+ return "length";
817
+ if (acceptsDate(node))
818
+ return "date";
819
+ return undefined;
820
+ }
564
821
  function isCallback(value) {
565
822
  return typeof value === "function";
566
823
  }
@@ -580,54 +837,145 @@ function parseTupleItem(def, resolve) {
580
837
  hasDefault: true,
581
838
  };
582
839
  }
840
+ if (typeof def === "string") {
841
+ const parsed = parseStringDef(def, resolve);
842
+ return {
843
+ val: parsed.ir,
844
+ opt: parsed.optional || parsed.hasDefault,
845
+ def: parsed.def,
846
+ hasDefault: parsed.hasDefault,
847
+ };
848
+ }
583
849
  return { val: parseDef(def, resolve), opt: false };
584
850
  }
851
+ function cloneTuple(tuple) {
852
+ return {
853
+ ...tuple,
854
+ prefix: tuple.prefix.map(item => ({ ...item })),
855
+ postfix: [...tuple.postfix],
856
+ };
857
+ }
858
+ function hasOptionalPrefix(tuple) {
859
+ return tuple.prefix.some(item => item.opt || item.hasDefault === true);
860
+ }
861
+ function appendTupleItem(tuple, item) {
862
+ if (tuple.variadic !== undefined) {
863
+ if (item.opt || item.hasDefault) {
864
+ throw new OmpTypeError("An optional element may not follow a variadic element");
865
+ }
866
+ if (hasOptionalPrefix(tuple)) {
867
+ throw new OmpTypeError("A postfix required element cannot follow an optional or defaultable element");
868
+ }
869
+ tuple.postfix.push(item.val);
870
+ return;
871
+ }
872
+ if (item.hasDefault && tuple.prefix.some(prefixItem => prefixItem.opt && !prefixItem.hasDefault)) {
873
+ throw new OmpTypeError("A defaultable element may not follow an optional element without a default");
874
+ }
875
+ if (hasOptionalPrefix(tuple) && !item.opt) {
876
+ throw new OmpTypeError("required tuple elements cannot follow optional elements");
877
+ }
878
+ tuple.prefix.push(item);
879
+ }
880
+ function appendTuple(target, spread) {
881
+ if (target.variadic !== undefined && spread.variadic !== undefined) {
882
+ throw new OmpTypeError("a tuple may have one spread followed by an array definition");
883
+ }
884
+ for (const item of spread.prefix)
885
+ appendTupleItem(target, { ...item });
886
+ if (spread.variadic !== undefined) {
887
+ target.variadic = spread.variadic;
888
+ }
889
+ for (const item of spread.postfix)
890
+ appendTupleItem(target, { val: item, opt: false });
891
+ }
892
+ function spreadAlternatives(spread) {
893
+ if (spread.k === "alias")
894
+ return spreadAlternatives(spread.resolve());
895
+ if (spread.k === "sub")
896
+ return spreadAlternatives(spread.schema.ir);
897
+ if (spread.k === "union")
898
+ return spread.members.flatMap(spreadAlternatives);
899
+ if (spread.k === "array")
900
+ return [{ k: "tuple", prefix: [], variadic: spread.el, postfix: [] }];
901
+ if (spread.k === "tuple")
902
+ return [spread];
903
+ throw new OmpTypeError("tuple spread element must be an array");
904
+ }
585
905
  function parseTuple(def, resolve) {
586
- const prefix = [];
587
- const postfix = [];
588
- let variadic;
589
- let optionalSeen = false;
906
+ let branches = [{ k: "tuple", prefix: [], postfix: [] }];
590
907
  for (let index = 0; index < def.length; index++) {
591
908
  if (def[index] === "...") {
592
- if (variadic !== undefined || index + 1 >= def.length) {
909
+ if (index + 1 >= def.length) {
593
910
  throw new OmpTypeError("a tuple may have one spread followed by an array definition");
594
911
  }
595
- const spread = parseDef(def[++index], resolve);
596
- if (spread.k !== "array")
597
- throw new OmpTypeError("tuple spread element must be an array");
598
- variadic = spread.el;
599
- continue;
600
- }
601
- if (variadic !== undefined) {
602
- const item = parseTupleItem(def[index], resolve);
603
- if (item.opt || item.hasDefault) {
604
- throw new OmpTypeError("optional tuple elements cannot follow a variadic element");
912
+ const alternatives = spreadAlternatives(parseDef(def[++index], resolve));
913
+ const distributed = [];
914
+ for (const branch of branches) {
915
+ for (const alternative of alternatives) {
916
+ const next = cloneTuple(branch);
917
+ appendTuple(next, alternative);
918
+ distributed.push(next);
919
+ }
605
920
  }
606
- postfix.push(item.val);
921
+ branches = distributed;
607
922
  continue;
608
923
  }
609
924
  const item = parseTupleItem(def[index], resolve);
610
- if (optionalSeen && !item.opt && !item.hasDefault) {
611
- throw new OmpTypeError("required tuple elements cannot follow optional elements");
612
- }
613
- optionalSeen ||= item.opt || item.hasDefault === true;
614
- prefix.push(item);
925
+ for (const branch of branches)
926
+ appendTupleItem(branch, { ...item });
615
927
  }
616
- return { k: "tuple", prefix, variadic, postfix };
928
+ return branches.length === 1 ? branches[0] : { k: "union", members: branches };
617
929
  }
618
930
  /** Build the runtime schema for an object's or tuple's keys. */
619
931
  export function keyOf(node) {
932
+ if (node.k === "alias")
933
+ return keyOf(node.resolve());
934
+ if (node.k === "sub")
935
+ return keyOf(node.schema.ir);
936
+ if (node.k === "refine")
937
+ return keyOf(node.base);
938
+ if (node.k === "intersection") {
939
+ const members = node.members.flatMap(member => {
940
+ const keys = keyOf(member);
941
+ return keys.k === "union" ? keys.members : [keys];
942
+ });
943
+ return members.length === 1 ? members[0] : { k: "union", members };
944
+ }
620
945
  if (node.k === "object") {
621
946
  const members = node.props.map(prop => ({ k: "lit", v: prop.key }));
622
- if (node.index !== undefined)
947
+ if (node.index !== undefined || (node.patternIndexes?.length ?? 0) > 0)
623
948
  members.push({ k: "string" });
949
+ if (node.symbolIndex !== undefined)
950
+ members.push({ k: "symbol" });
624
951
  if (members.length === 0)
625
952
  return { k: "never" };
626
953
  return members.length === 1 ? members[0] : { k: "union", members };
627
954
  }
628
955
  if (node.k === "tuple")
629
956
  return { k: "number", int: true, min: 0 };
630
- throw new OmpTypeError(`keyof requires an object or tuple (was ${node.k})`);
957
+ if (node.k === "union") {
958
+ if (node.members.length === 0)
959
+ return { k: "never" };
960
+ const literalSets = node.members.map(member => {
961
+ const keyed = keyOf(member);
962
+ const literals = keyed.k === "union" ? keyed.members : [keyed];
963
+ const keys = new Set();
964
+ for (const literal of literals) {
965
+ if (literal.k === "lit" &&
966
+ (typeof literal.v === "string" || typeof literal.v === "number" || typeof literal.v === "symbol")) {
967
+ keys.add(literal.v);
968
+ }
969
+ }
970
+ return keys;
971
+ });
972
+ const common = [...literalSets[0]].filter(key => literalSets.slice(1).every(keys => keys.has(key)));
973
+ if (common.length === 0)
974
+ throw new OmpTypeError("keyof operand must be an object");
975
+ const members = common.map(value => ({ k: "lit", v: value }));
976
+ return members.length === 1 ? members[0] : { k: "union", members };
977
+ }
978
+ throw new OmpTypeError("keyof operand must be an object");
631
979
  }
632
980
  function parseArrayExpression(def, resolve) {
633
981
  if (def.length === 2 && def[1] === "[]")
@@ -640,7 +988,11 @@ function parseArrayExpression(def, resolve) {
640
988
  const ctor = def[index];
641
989
  if (!isConstructor(ctor))
642
990
  throw new OmpTypeError("instanceof operands must be constructors");
643
- members.push({ k: "instance", ctor, expected: `an instance of ${ctor.name || "the constructor"}` });
991
+ members.push({
992
+ k: "instance",
993
+ ctor,
994
+ expected: ctor === Error ? "an Error" : `an instance of ${ctor.name || "the constructor"}`,
995
+ });
644
996
  }
645
997
  return members.length === 1 ? members[0] : { k: "union", members };
646
998
  }
@@ -668,23 +1020,46 @@ function parseArrayExpression(def, resolve) {
668
1020
  if (!isCallback(def[2]))
669
1021
  throw new OmpTypeError("narrow operator requires a predicate");
670
1022
  const predicate = def[2];
1023
+ const name = predicate.name;
1024
+ const expected = name.length === 0 ? "valid according to an anonymous predicate" : `valid according to ${name}`;
671
1025
  return {
672
1026
  k: "refine",
673
1027
  base: parseDef(def[0], resolve),
674
- pred: value => predicate(value, {
675
- error: () => OmpErrors.single([], "the predicate", value),
676
- reject: () => OmpErrors.single([], "the predicate", value),
677
- }) === true,
678
- expected: "a value satisfying the predicate",
1028
+ pred: value => {
1029
+ let errors;
1030
+ const error = (input) => {
1031
+ const detail = typeof input === "string" ? { expected: input } : input;
1032
+ const next = OmpErrors.single([...(detail.path ?? detail.relativePath ?? [])], detail.expected, value, {
1033
+ preserveActual: true,
1034
+ ...(Object.hasOwn(detail, "actual") ? { actual: String(detail.actual) } : {}),
1035
+ });
1036
+ if (errors)
1037
+ errors.append(next);
1038
+ else
1039
+ errors = next;
1040
+ return next;
1041
+ };
1042
+ const result = predicate(value, { error, reject: error });
1043
+ return errors ?? (result instanceof OmpErrors ? result : result === true);
1044
+ },
1045
+ expected,
679
1046
  };
680
1047
  }
681
1048
  if (def.length >= 3 && def[1] === "@") {
682
1049
  const base = parseDef(def[0], resolve);
683
1050
  const meta = def[2];
684
1051
  if (typeof meta === "string")
685
- return { ...base, desc: meta };
686
- if (typeof meta === "object" && meta !== null && "description" in meta && typeof meta.description === "string") {
687
- return { ...base, desc: meta.description };
1052
+ return { ...base, desc: meta, cfg: { ...base.cfg, expected: meta } };
1053
+ if (typeof meta === "object" && meta !== null) {
1054
+ const config = meta;
1055
+ return {
1056
+ ...base,
1057
+ cfg: {
1058
+ ...(typeof config.description === "string" ? { expected: config.description } : {}),
1059
+ ...config,
1060
+ },
1061
+ ...(typeof config.description === "string" ? { desc: config.description } : {}),
1062
+ };
688
1063
  }
689
1064
  return base;
690
1065
  }
@@ -697,157 +1072,552 @@ function isObjectDefinition(def) {
697
1072
  !(def instanceof RegExp) &&
698
1073
  !(def instanceof Date));
699
1074
  }
1075
+ function spreadObjectOf(ir) {
1076
+ if (ir.k === "alias")
1077
+ return spreadObjectOf(ir.resolve());
1078
+ if (ir.k === "sub")
1079
+ return spreadObjectOf(ir.schema.ir);
1080
+ if (ir.k === "refine")
1081
+ return spreadObjectOf(ir.base);
1082
+ if (ir.k === "anyobject")
1083
+ return { k: "object", props: [], extras: "keep" };
1084
+ if (ir.k === "object")
1085
+ return ir;
1086
+ if (ir.k !== "intersection")
1087
+ return undefined;
1088
+ let result = { k: "object", props: [], extras: "keep" };
1089
+ for (const member of ir.members) {
1090
+ const object = spreadObjectOf(member);
1091
+ if (object === undefined)
1092
+ return undefined;
1093
+ result = mergeObjectIR(result, object);
1094
+ }
1095
+ return result;
1096
+ }
1097
+ function indexKeyKind(key, value, props, indexes) {
1098
+ if (key.k === "alias")
1099
+ return indexKeyKind(key.resolve(), value, props, indexes);
1100
+ if (key.k === "union") {
1101
+ for (const member of key.members)
1102
+ indexKeyKind(member, value, props, indexes);
1103
+ return;
1104
+ }
1105
+ if (key.k === "lit" && (typeof key.v === "string" || typeof key.v === "symbol")) {
1106
+ props.push({ key: key.v, opt: false, val: value });
1107
+ return;
1108
+ }
1109
+ if (key.k === "string") {
1110
+ indexes.string = value;
1111
+ return;
1112
+ }
1113
+ if (key.k === "symbol") {
1114
+ indexes.symbol = value;
1115
+ return;
1116
+ }
1117
+ if (key.k === "refine" && key.base.k === "string") {
1118
+ indexes.patterns.push({ key, val: value });
1119
+ return;
1120
+ }
1121
+ throw new OmpTypeError(`indexed key definition must resolve to a string or symbol (was ${expectedOf(key)})`);
1122
+ }
1123
+ function addObjectProp(props, spreadKeys, prop) {
1124
+ if (spreadKeys === undefined) {
1125
+ props.push(prop);
1126
+ return;
1127
+ }
1128
+ const previous = props.findIndex(candidate => candidate.key === prop.key);
1129
+ if (previous < 0) {
1130
+ props.push(prop);
1131
+ return;
1132
+ }
1133
+ if (!spreadKeys.delete(prop.key))
1134
+ throw new OmpTypeError(`duplicate object key ${String(prop.key)}`);
1135
+ props[previous] = prop;
1136
+ }
700
1137
  function parseObjectDefinition(def, resolve) {
701
1138
  const props = [];
702
- let index;
1139
+ let spreadKeys;
1140
+ let normalizedKey;
1141
+ let normalizedKeys;
1142
+ let indexes;
703
1143
  let extras = "keep";
704
- for (const rawKey in def) {
705
- const val = def[rawKey];
706
- if (rawKey === "+") {
707
- if (val === "reject" || val === "delete")
1144
+ let simple = true;
1145
+ for (const originalKey in def) {
1146
+ if (!Object.hasOwn(def, originalKey))
1147
+ continue;
1148
+ const val = def[originalKey];
1149
+ if (originalKey === "+") {
1150
+ if (val === "reject" || val === "delete") {
708
1151
  extras = val;
1152
+ if (val === "delete")
1153
+ simple = false;
1154
+ }
709
1155
  else if (val === "ignore")
710
1156
  extras = "keep";
711
1157
  else
712
1158
  throw new OmpTypeError(`bad "+" value ${String(val)}`);
713
1159
  continue;
714
1160
  }
715
- if (rawKey === "...") {
716
- const spread = parseDef(val, resolve);
717
- if (spread.k !== "object")
718
- throw new OmpTypeError("object spread must resolve to an object");
1161
+ if (originalKey === "...") {
1162
+ const parsed = parseDef(val, resolve);
1163
+ const spread = spreadObjectOf(parsed);
1164
+ if (spread === undefined) {
1165
+ throw new OmpTypeError(`object spread must resolve to an object literal (was ${expectedOf(parsed)})`);
1166
+ }
1167
+ if (simple && !isSimpleIR(spread))
1168
+ simple = false;
1169
+ spreadKeys ??= new Set();
719
1170
  for (const prop of spread.props) {
720
1171
  const previous = props.findIndex(candidate => candidate.key === prop.key);
721
1172
  if (previous < 0)
722
1173
  props.push(prop);
723
1174
  else
724
1175
  props[previous] = prop;
1176
+ spreadKeys.add(prop.key);
1177
+ }
1178
+ if (spread.index !== undefined || spread.symbolIndex !== undefined || spread.patternIndexes !== undefined) {
1179
+ const objectIndexes = indexes ?? { patterns: [] };
1180
+ indexes = objectIndexes;
1181
+ objectIndexes.string ??= spread.index;
1182
+ objectIndexes.symbol ??= spread.symbolIndex;
1183
+ if (spread.patternIndexes !== undefined)
1184
+ objectIndexes.patterns.push(...spread.patternIndexes);
725
1185
  }
726
- index ??= spread.index;
727
1186
  if (spread.extras !== "keep")
728
1187
  extras = spread.extras;
729
1188
  continue;
730
1189
  }
731
- if (rawKey === "[string]") {
732
- index = parseDef(val, resolve);
1190
+ if (typeof originalKey === "string" && originalKey.startsWith("[") && originalKey.endsWith("]")) {
1191
+ let value;
1192
+ if (typeof val === "string") {
1193
+ const parsed = parseStringDef(val, resolve);
1194
+ if (parsed.hasDefault)
1195
+ throw new OmpTypeError("index signatures cannot specify a default");
1196
+ value = parsed.ir;
1197
+ }
1198
+ else {
1199
+ if (Array.isArray(val) && val.length === 3 && val[1] === "=") {
1200
+ throw new OmpTypeError("index signatures cannot specify a default");
1201
+ }
1202
+ value = parseDef(val, resolve);
1203
+ if (isEmbedded(val) && val.hasDefault) {
1204
+ throw new OmpTypeError("index signatures cannot specify a default");
1205
+ }
1206
+ }
1207
+ const keyDefinition = originalKey.slice(1, -1);
1208
+ const regex = /^\/((?:\\.|[^\\/])*)\/([dgimsuvy]*)$/.exec(keyDefinition);
1209
+ let key;
1210
+ if (regex === null) {
1211
+ key = parseDef(keyDefinition, resolve);
1212
+ }
1213
+ else {
1214
+ try {
1215
+ key = patternIR(new RegExp(regex[1], regex[2]));
1216
+ }
1217
+ catch {
1218
+ throw new OmpTypeError(`invalid index signature pattern ${keyDefinition}`);
1219
+ }
1220
+ }
1221
+ const objectIndexes = indexes ?? { patterns: [] };
1222
+ indexes = objectIndexes;
1223
+ indexKeyKind(key, value, props, objectIndexes);
1224
+ if (simple && (!isSimpleIR(key) || !isSimpleIR(value)))
1225
+ simple = false;
733
1226
  continue;
734
1227
  }
735
- const opt = rawKey.charCodeAt(rawKey.length - 1) === 63;
1228
+ const escapedOptional = typeof originalKey === "string" && originalKey.endsWith("\\?");
1229
+ const escapedMeta = typeof originalKey === "string" &&
1230
+ (originalKey === "\\+" || originalKey === "\\..." || originalKey.startsWith("\\["));
1231
+ const rawKey = escapedOptional
1232
+ ? `${originalKey.slice(0, -2)}?`
1233
+ : escapedMeta
1234
+ ? originalKey.slice(1)
1235
+ : originalKey;
1236
+ const opt = typeof rawKey === "string" && !escapedOptional && !escapedMeta && rawKey.endsWith("?");
736
1237
  const key = opt ? rawKey.slice(0, -1) : rawKey;
1238
+ let prop;
737
1239
  if (typeof val === "string") {
738
1240
  const parsed = parseStringDef(val, resolve);
739
- const prop = { key, opt: opt || parsed.optional, val: parsed.ir };
1241
+ prop = { key, opt: opt || parsed.optional, val: parsed.ir };
740
1242
  if (parsed.hasDefault) {
741
1243
  prop.def = parsed.def;
742
1244
  prop.hasDefault = true;
743
1245
  }
744
- props.push(prop);
745
1246
  }
746
1247
  else if (Array.isArray(val) && val.length === 2 && val[1] === "?") {
747
- props.push({ key, opt: true, val: parseDef(val[0], resolve) });
1248
+ prop = { key, opt: true, val: parseDef(val[0], resolve) };
748
1249
  }
749
1250
  else if (Array.isArray(val) && val.length === 3 && val[1] === "=") {
750
- props.push({
1251
+ prop = {
751
1252
  key,
752
1253
  opt,
753
1254
  val: parseDef(val[0], resolve),
754
1255
  def: val[2],
755
1256
  defFactory: typeof val[2] === "function",
756
1257
  hasDefault: true,
757
- });
1258
+ };
758
1259
  }
759
1260
  else if (isEmbedded(val)) {
760
- if (val.hasDefault) {
761
- props.push({
1261
+ prop = val.hasDefault
1262
+ ? {
762
1263
  key,
763
1264
  opt,
764
1265
  val: embed(val),
765
- def: val.defaultValue,
1266
+ def: val.hasDefaultOutput ? val.defaultOutput : val.defaultValue,
766
1267
  defFactory: typeof val.defaultValue === "function",
767
1268
  hasDefault: true,
768
- });
1269
+ defValidated: val.hasDefaultOutput,
1270
+ }
1271
+ : { key, opt, val: embed(val) };
1272
+ }
1273
+ else {
1274
+ prop = {
1275
+ key,
1276
+ opt,
1277
+ val: isObjectDefinition(val) ? parseObjectDefinition(val, resolve) : parseDef(val, resolve),
1278
+ };
1279
+ }
1280
+ if (key !== originalKey) {
1281
+ if (!spreadKeys?.has(key) && props.some(candidate => candidate.key === key)) {
1282
+ throw new OmpTypeError(`duplicate object key ${String(key)}`);
769
1283
  }
1284
+ if (normalizedKey === undefined)
1285
+ normalizedKey = key;
770
1286
  else {
771
- props.push({ key, opt, val: embed(val) });
1287
+ normalizedKeys ??= [normalizedKey];
1288
+ normalizedKeys.push(key);
1289
+ }
1290
+ }
1291
+ else if (!spreadKeys?.has(key) && (key === normalizedKey || normalizedKeys?.includes(key))) {
1292
+ throw new OmpTypeError(`duplicate object key ${String(key)}`);
1293
+ }
1294
+ if (opt && prop.hasDefault)
1295
+ throw new OmpTypeError(`optional key ${String(key)} cannot specify a default`);
1296
+ if (simple && (prop.hasDefault || !isSimpleIR(prop.val)))
1297
+ simple = false;
1298
+ addObjectProp(props, spreadKeys, prop);
1299
+ }
1300
+ for (const key of Object.getOwnPropertySymbols(def)) {
1301
+ if (!Object.prototype.propertyIsEnumerable.call(def, key))
1302
+ continue;
1303
+ const val = def[key];
1304
+ let prop;
1305
+ if (typeof val === "string") {
1306
+ const parsed = parseStringDef(val, resolve);
1307
+ prop = { key, opt: parsed.optional, val: parsed.ir };
1308
+ if (parsed.hasDefault) {
1309
+ prop.def = parsed.def;
1310
+ prop.hasDefault = true;
772
1311
  }
773
1312
  }
774
- else if (isObjectDefinition(val)) {
775
- props.push({ key, opt, val: parseObjectDefinition(val, resolve) });
1313
+ else if (Array.isArray(val) && val.length === 2 && val[1] === "?") {
1314
+ prop = { key, opt: true, val: parseDef(val[0], resolve) };
1315
+ }
1316
+ else if (Array.isArray(val) && val.length === 3 && val[1] === "=") {
1317
+ prop = {
1318
+ key,
1319
+ opt: false,
1320
+ val: parseDef(val[0], resolve),
1321
+ def: val[2],
1322
+ defFactory: typeof val[2] === "function",
1323
+ hasDefault: true,
1324
+ };
1325
+ }
1326
+ else if (isEmbedded(val)) {
1327
+ prop = val.hasDefault
1328
+ ? {
1329
+ key,
1330
+ opt: false,
1331
+ val: embed(val),
1332
+ def: val.hasDefaultOutput ? val.defaultOutput : val.defaultValue,
1333
+ defFactory: typeof val.defaultValue === "function",
1334
+ hasDefault: true,
1335
+ defValidated: val.hasDefaultOutput,
1336
+ }
1337
+ : { key, opt: false, val: embed(val) };
776
1338
  }
777
1339
  else {
778
- props.push({ key, opt, val: parseDef(val, resolve) });
1340
+ prop = {
1341
+ key,
1342
+ opt: false,
1343
+ val: isObjectDefinition(val) ? parseObjectDefinition(val, resolve) : parseDef(val, resolve),
1344
+ };
779
1345
  }
1346
+ if (simple && (prop.hasDefault || !isSimpleIR(prop.val)))
1347
+ simple = false;
1348
+ addObjectProp(props, spreadKeys, prop);
780
1349
  }
781
- return { k: "object", props, index, extras };
1350
+ const object = {
1351
+ k: "object",
1352
+ props,
1353
+ index: indexes?.string,
1354
+ symbolIndex: indexes?.symbol,
1355
+ patternIndexes: indexes === undefined || indexes.patterns.length === 0 ? undefined : indexes.patterns,
1356
+ extras,
1357
+ };
1358
+ object[kSimple] = simple;
1359
+ object[kSimpleOwner] = object;
1360
+ return object;
782
1361
  }
783
1362
  /** Parse a definition, optionally resolving names from an enclosing scope. */
784
1363
  export function parseDef(def, resolve) {
785
1364
  if (typeof def === "string") {
786
1365
  const parsed = parseStringDef(def, resolve);
1366
+ if (parsed.hasDefault) {
1367
+ throw new OmpTypeError("A default may only be specified for an object property or tuple element");
1368
+ }
787
1369
  if (parsed.optional) {
788
1370
  throw new OmpTypeError(`optional "?" marker is only valid on object property values`);
789
1371
  }
790
1372
  return parsed.ir;
791
1373
  }
792
- if (Array.isArray(def))
1374
+ if (Array.isArray(def)) {
1375
+ if (def.length === 3 && def[1] === "=") {
1376
+ throw new OmpTypeError("A default may only be specified for an object property or tuple element");
1377
+ }
793
1378
  return parseArrayExpression(def, resolve);
1379
+ }
794
1380
  if (def instanceof RegExp)
795
1381
  return patternIR(def);
796
1382
  if (def instanceof Date)
797
1383
  return { k: "lit", v: def };
798
1384
  if (isEmbedded(def))
799
1385
  return embed(def);
1386
+ if (typeof def === "function") {
1387
+ const resolved = Reflect.apply(def, undefined, []);
1388
+ if (!isEmbedded(resolved)) {
1389
+ throw new OmpTypeError(`thunk must return a Type (was ${typeof resolved})`);
1390
+ }
1391
+ return embed(resolved);
1392
+ }
800
1393
  if (isObjectDefinition(def))
801
1394
  return parseObjectDefinition(def, resolve);
802
- throw new OmpTypeError(`unsupported definition ${String(def)}`);
1395
+ throw new OmpTypeError(`unsupported definition ${String(def)} (was ${typeof def})`);
1396
+ }
1397
+ /** Whether `ir` needs no construction-time normalization or morph analysis. */
1398
+ export function isSimpleIR(ir) {
1399
+ const cached = ir[kSimpleOwner] === ir ? ir[kSimple] : undefined;
1400
+ if (cached !== undefined)
1401
+ return cached;
1402
+ const simple = scanSimpleIR(ir);
1403
+ ir[kSimple] = simple;
1404
+ ir[kSimpleOwner] = ir;
1405
+ return simple;
1406
+ }
1407
+ function scanSimpleIR(ir) {
1408
+ switch (ir.k) {
1409
+ case "intersection":
1410
+ case "morph":
1411
+ case "sub":
1412
+ case "alias":
1413
+ return false;
1414
+ case "refine":
1415
+ return scanSimpleIR(ir.base);
1416
+ case "union":
1417
+ if (ir.members.length < 2)
1418
+ return false;
1419
+ if (ir.members.length === 2 &&
1420
+ ir.members.every(member => member.k === "lit" && typeof member.v === "boolean")) {
1421
+ return false;
1422
+ }
1423
+ for (let index = 0; index < ir.members.length; index++) {
1424
+ const member = ir.members[index];
1425
+ if (member.k !== "lit" ||
1426
+ (member.v !== null && (typeof member.v === "object" || typeof member.v === "function"))) {
1427
+ return false;
1428
+ }
1429
+ for (let previous = 0; previous < index; previous++) {
1430
+ const candidate = ir.members[previous];
1431
+ if (candidate.k === "lit" && candidate.v === member.v)
1432
+ return false;
1433
+ }
1434
+ }
1435
+ return true;
1436
+ case "array":
1437
+ return scanSimpleIR(ir.el);
1438
+ case "tuple":
1439
+ for (const item of ir.prefix) {
1440
+ if (item.hasDefault || !scanSimpleIR(item.val))
1441
+ return false;
1442
+ }
1443
+ if (ir.variadic !== undefined && !scanSimpleIR(ir.variadic))
1444
+ return false;
1445
+ for (const item of ir.postfix)
1446
+ if (!scanSimpleIR(item))
1447
+ return false;
1448
+ return true;
1449
+ case "object":
1450
+ if (ir.extras === "delete")
1451
+ return false;
1452
+ for (const prop of ir.props) {
1453
+ if (prop.hasDefault || !scanSimpleIR(prop.val))
1454
+ return false;
1455
+ }
1456
+ if (ir.index !== undefined && !scanSimpleIR(ir.index))
1457
+ return false;
1458
+ if (ir.symbolIndex !== undefined && !scanSimpleIR(ir.symbolIndex))
1459
+ return false;
1460
+ if (ir.patternIndexes !== undefined) {
1461
+ for (const pattern of ir.patternIndexes) {
1462
+ if (!scanSimpleIR(pattern.key) || !scanSimpleIR(pattern.val))
1463
+ return false;
1464
+ }
1465
+ }
1466
+ return true;
1467
+ default:
1468
+ return true;
1469
+ }
803
1470
  }
804
1471
  /** True when validating `ir` can produce an output different from its input. */
805
1472
  export function hasMorph(ir) {
806
- const cached = ir[kMorph];
1473
+ const cached = ir[kMorphOwner] === ir ? ir[kMorph] : undefined;
1474
+ if (cached !== undefined)
1475
+ return cached;
1476
+ const result = scanMorph(ir);
1477
+ ir[kMorph] = result;
1478
+ ir[kMorphOwner] = ir;
1479
+ return result;
1480
+ }
1481
+ function scanMorph(ir, activeAliases) {
1482
+ const cached = ir[kMorphOwner] === ir ? ir[kMorph] : undefined;
807
1483
  if (cached !== undefined)
808
1484
  return cached;
809
1485
  let result = false;
810
1486
  switch (ir.k) {
811
1487
  case "sub":
812
- result = true;
813
- break;
814
1488
  case "morph":
815
- case "alias":
816
1489
  result = true;
817
1490
  break;
1491
+ case "alias": {
1492
+ if (activeAliases?.has(ir))
1493
+ return false;
1494
+ const aliases = activeAliases ?? new Set();
1495
+ aliases.add(ir);
1496
+ result = scanMorph(ir.resolve(), aliases);
1497
+ aliases.delete(ir);
1498
+ return result;
1499
+ }
818
1500
  case "object":
819
1501
  result = ir.extras === "delete";
820
- for (let i = 0; !result && i < ir.props.length; i++) {
821
- const prop = ir.props[i];
822
- result = prop.hasDefault === true || hasMorph(prop.val);
1502
+ for (let index = 0; !result && index < ir.props.length; index++) {
1503
+ const prop = ir.props[index];
1504
+ result = prop.hasDefault === true || scanMorph(prop.val, activeAliases);
823
1505
  }
824
1506
  if (!result && ir.index !== undefined)
825
- result = hasMorph(ir.index);
1507
+ result = scanMorph(ir.index, activeAliases);
1508
+ if (!result && ir.symbolIndex !== undefined)
1509
+ result = scanMorph(ir.symbolIndex, activeAliases);
1510
+ if (!result && ir.patternIndexes !== undefined) {
1511
+ for (const pattern of ir.patternIndexes) {
1512
+ if (scanMorph(pattern.val, activeAliases)) {
1513
+ result = true;
1514
+ break;
1515
+ }
1516
+ }
1517
+ }
826
1518
  break;
827
1519
  case "array":
828
- result = hasMorph(ir.el);
1520
+ result = scanMorph(ir.el, activeAliases);
829
1521
  break;
830
1522
  case "union":
831
- result = ir.members.some(hasMorph);
832
- break;
833
1523
  case "intersection":
834
- result = ir.members.some(hasMorph);
1524
+ for (const member of ir.members) {
1525
+ if (scanMorph(member, activeAliases)) {
1526
+ result = true;
1527
+ break;
1528
+ }
1529
+ }
835
1530
  break;
836
1531
  case "refine":
837
- result = hasMorph(ir.base);
1532
+ result = scanMorph(ir.base, activeAliases);
838
1533
  break;
839
1534
  case "tuple":
840
- result =
841
- ir.prefix.some(item => item.hasDefault === true || hasMorph(item.val)) ||
842
- (ir.variadic !== undefined && hasMorph(ir.variadic)) ||
843
- ir.postfix.some(hasMorph);
1535
+ for (const item of ir.prefix) {
1536
+ if (item.hasDefault === true || scanMorph(item.val, activeAliases)) {
1537
+ result = true;
1538
+ break;
1539
+ }
1540
+ }
1541
+ if (!result && ir.variadic !== undefined)
1542
+ result = scanMorph(ir.variadic, activeAliases);
1543
+ if (!result) {
1544
+ for (const item of ir.postfix) {
1545
+ if (scanMorph(item, activeAliases)) {
1546
+ result = true;
1547
+ break;
1548
+ }
1549
+ }
1550
+ }
844
1551
  break;
845
1552
  }
846
- ir[kMorph] = result;
847
1553
  return result;
848
1554
  }
1555
+ /**
1556
+ * True when a traversal of `ir` can revisit nodes through recursive aliases,
1557
+ * requiring cycle guards in the interpreter. Embedded sub-schemas run their
1558
+ * own guarded traversal and are intentionally not inspected.
1559
+ */
1560
+ export function hasAlias(ir) {
1561
+ const cached = ir[kAliasOwner] === ir ? ir[kAlias] : undefined;
1562
+ if (cached !== undefined)
1563
+ return cached;
1564
+ const result = scanAlias(ir);
1565
+ ir[kAlias] = result;
1566
+ ir[kAliasOwner] = ir;
1567
+ return result;
1568
+ }
1569
+ function scanAlias(ir) {
1570
+ const cached = ir[kAliasOwner] === ir ? ir[kAlias] : undefined;
1571
+ if (cached !== undefined)
1572
+ return cached;
1573
+ switch (ir.k) {
1574
+ case "alias":
1575
+ return true;
1576
+ case "object":
1577
+ for (const prop of ir.props)
1578
+ if (scanAlias(prop.val))
1579
+ return true;
1580
+ if (ir.index !== undefined && scanAlias(ir.index))
1581
+ return true;
1582
+ if (ir.symbolIndex !== undefined && scanAlias(ir.symbolIndex))
1583
+ return true;
1584
+ if (ir.patternIndexes !== undefined) {
1585
+ for (const pattern of ir.patternIndexes) {
1586
+ if (scanAlias(pattern.key) || scanAlias(pattern.val))
1587
+ return true;
1588
+ }
1589
+ }
1590
+ return false;
1591
+ case "array":
1592
+ return scanAlias(ir.el);
1593
+ case "tuple":
1594
+ for (const item of ir.prefix)
1595
+ if (scanAlias(item.val))
1596
+ return true;
1597
+ if (ir.variadic !== undefined && scanAlias(ir.variadic))
1598
+ return true;
1599
+ for (const item of ir.postfix)
1600
+ if (scanAlias(item))
1601
+ return true;
1602
+ return false;
1603
+ case "union":
1604
+ case "intersection":
1605
+ for (const member of ir.members)
1606
+ if (scanAlias(member))
1607
+ return true;
1608
+ return false;
1609
+ case "refine":
1610
+ return scanAlias(ir.base);
1611
+ case "morph":
1612
+ return scanAlias(ir.input) || (ir.out !== undefined && scanAlias(ir.out));
1613
+ default:
1614
+ return false;
1615
+ }
1616
+ }
849
1617
  /** Human-readable expectation for error messages, e.g. `"a string"`. */
850
1618
  export function expectedOf(ir) {
1619
+ if (ir.desc !== undefined)
1620
+ return ir.desc;
851
1621
  switch (ir.k) {
852
1622
  case "unknown":
853
1623
  return "unknown";
@@ -923,8 +1693,8 @@ export function expectedOf(ir) {
923
1693
  case "morph":
924
1694
  return expectedOf(ir.input);
925
1695
  case "alias":
926
- return ir.name;
1696
+ return ir.name === "this" ? expectedOf(ir.resolve()) : ir.name;
927
1697
  case "sub":
928
- return expectedOf(ir.schema.ir);
1698
+ return ir.desc ?? ir.schema.description ?? expectedOf(ir.schema.ir);
929
1699
  }
930
1700
  }