@buckeyestudio/omptype 18.4.13

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