@oh-my-pi/omptype 17.2.6 → 17.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -2
- package/README.md +83 -14
- package/dist/js/ark.js +16 -0
- package/dist/js/compile.js +942 -0
- package/dist/js/errors.js +394 -0
- package/dist/js/from-json-schema.js +234 -0
- package/dist/js/index.js +13 -0
- package/dist/js/infer.js +2 -0
- package/dist/js/interp.js +858 -0
- package/dist/js/ir.js +1700 -0
- package/dist/js/json-schema.js +312 -0
- package/dist/js/keywords.js +340 -0
- package/dist/js/type.js +3062 -0
- package/dist/js/typebox.js +355 -0
- package/dist/js/zod.js +269 -0
- package/dist/types/ark.d.ts +3 -11
- package/dist/types/compile.d.ts +2 -0
- package/dist/types/errors.d.ts +53 -16
- package/dist/types/from-json-schema.d.ts +9 -0
- package/dist/types/index.d.ts +4 -4
- package/dist/types/infer.d.ts +147 -31
- package/dist/types/interp.d.ts +14 -5
- package/dist/types/ir.d.ts +129 -9
- package/dist/types/json-schema.d.ts +15 -1
- package/dist/types/keywords.d.ts +7 -0
- package/dist/types/type.d.ts +525 -60
- package/dist/types/typebox.d.ts +78 -50
- package/dist/types/zod.d.ts +1 -1
- package/package.json +24 -8
- package/src/ark.ts +4 -14
- package/src/compile.ts +887 -154
- package/src/errors.ts +347 -37
- package/src/from-json-schema.ts +231 -0
- package/src/index.ts +4 -4
- package/src/infer.ts +363 -81
- package/src/interp.ts +585 -102
- package/src/ir.ts +1444 -128
- package/src/json-schema.ts +162 -19
- package/src/keywords.ts +380 -0
- package/src/type.ts +3842 -198
- package/src/typebox.ts +142 -99
- package/src/zod.ts +41 -28
package/src/ir.ts
CHANGED
|
@@ -10,11 +10,32 @@
|
|
|
10
10
|
* and the JSON Schema emitter (`json-schema.ts`).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { OmpTypeError } from "./errors";
|
|
13
|
+
import { type ErrorConfig, OmpErrors, OmpTypeError } from "./errors";
|
|
14
|
+
import { keywordIR, patternIR, templateIR } from "./keywords";
|
|
14
15
|
|
|
15
16
|
/** Brand carried by `Type` instances so the parser can embed them in defs. */
|
|
16
17
|
export const IR_BRAND: unique symbol = Symbol("omptype.schema");
|
|
17
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
|
+
|
|
18
39
|
/**
|
|
19
40
|
* The parser-facing surface of an embedded `Type` instance.
|
|
20
41
|
* `type.ts` implements this on every schema it creates.
|
|
@@ -25,53 +46,140 @@ export interface EmbeddableSchema {
|
|
|
25
46
|
ir: IR;
|
|
26
47
|
/** True when the schema carries `.pipe()`/`.narrow()` steps. */
|
|
27
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;
|
|
28
53
|
/** `.default()` payload; a function is a factory invoked per fill. */
|
|
29
54
|
defaultValue?: unknown;
|
|
30
55
|
hasDefault: boolean;
|
|
56
|
+
/** Precomputed output for a non-factory default after validation and morphs. */
|
|
57
|
+
defaultOutput?: unknown;
|
|
58
|
+
hasDefaultOutput?: boolean;
|
|
31
59
|
/** `.describe()` annotation, emitted into JSON Schema. */
|
|
32
60
|
description?: string;
|
|
33
61
|
/** Full validate+morph pipeline (identical to calling the schema). */
|
|
34
|
-
run(value: unknown): unknown;
|
|
62
|
+
run(value: unknown, path?: readonly PropertyKey[]): unknown;
|
|
35
63
|
}
|
|
36
64
|
|
|
37
65
|
/** Policy for undeclared object keys. */
|
|
38
66
|
export type Extras = "keep" | "reject" | "delete";
|
|
39
67
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
);
|
|
56
155
|
|
|
57
156
|
export interface PropIR {
|
|
58
|
-
key:
|
|
157
|
+
key: PropertyKey;
|
|
59
158
|
opt: boolean;
|
|
60
159
|
val: IR;
|
|
61
160
|
/** Default payload (value, or factory when `defFactory`); missing key is filled. */
|
|
62
161
|
def?: unknown;
|
|
63
162
|
defFactory?: boolean;
|
|
64
163
|
hasDefault?: boolean;
|
|
164
|
+
/** True once the default has been validated and static morph output precomputed. */
|
|
165
|
+
defValidated?: boolean;
|
|
65
166
|
}
|
|
66
167
|
|
|
67
168
|
/** Definition input accepted by `type()` and object property values. */
|
|
68
|
-
export type Def = string | EmbeddableSchema | readonly
|
|
169
|
+
export type Def = string | RegExp | Date | EmbeddableSchema | readonly unknown[] | { readonly [k: string]: unknown };
|
|
69
170
|
|
|
70
171
|
// ── tokenizer ────────────────────────────────────────────────────────────────
|
|
71
172
|
|
|
72
|
-
type Tok =
|
|
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 };
|
|
73
181
|
|
|
74
|
-
const SIMPLE_OPS = "
|
|
182
|
+
const SIMPLE_OPS = "|&()[]=?%,#";
|
|
75
183
|
|
|
76
184
|
function tokenize(src: string): Tok[] {
|
|
77
185
|
const toks: Tok[] = [];
|
|
@@ -83,18 +191,63 @@ function tokenize(src: string): Tok[] {
|
|
|
83
191
|
i++;
|
|
84
192
|
continue;
|
|
85
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
|
+
}
|
|
86
205
|
if (c === "'" || c === '"') {
|
|
87
206
|
let j = i + 1;
|
|
88
|
-
|
|
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
|
+
}
|
|
89
215
|
if (j >= n) throw new OmpTypeError(`unterminated string literal in "${src}"`);
|
|
90
|
-
toks.push({ t: "str", v:
|
|
216
|
+
toks.push({ t: "str", v: value });
|
|
91
217
|
i = j + 1;
|
|
92
218
|
continue;
|
|
93
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
|
+
}
|
|
94
239
|
if ((c >= "0" && c <= "9") || (c === "-" && i + 1 < n && src[i + 1] >= "0" && src[i + 1] <= "9")) {
|
|
95
240
|
let j = i + 1;
|
|
96
|
-
while (j < n &&
|
|
97
|
-
|
|
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
|
+
}
|
|
98
251
|
i = j;
|
|
99
252
|
continue;
|
|
100
253
|
}
|
|
@@ -115,6 +268,11 @@ function tokenize(src: string): Tok[] {
|
|
|
115
268
|
}
|
|
116
269
|
continue;
|
|
117
270
|
}
|
|
271
|
+
if (c === "=" && src[i + 1] === "=") {
|
|
272
|
+
toks.push({ t: "op", v: "==" });
|
|
273
|
+
i += 2;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
118
276
|
if (SIMPLE_OPS.includes(c)) {
|
|
119
277
|
toks.push({ t: "op", v: c });
|
|
120
278
|
i++;
|
|
@@ -130,20 +288,41 @@ function tokenize(src: string): Tok[] {
|
|
|
130
288
|
const CMP: Record<string, true> = { "<": true, "<=": true, ">": true, ">=": true };
|
|
131
289
|
|
|
132
290
|
const KEYWORDS: Record<string, () => IR> = {
|
|
133
|
-
string: () => ({ k: "string" }),
|
|
134
|
-
"string.url": () => ({ k: "string", url: true }),
|
|
135
291
|
number: () => ({ k: "number" }),
|
|
136
292
|
"number.integer": () => ({ k: "number", int: true }),
|
|
137
293
|
boolean: () => ({ k: "boolean" }),
|
|
138
294
|
bigint: () => ({ k: "bigint" }),
|
|
295
|
+
symbol: () => ({ k: "symbol" }),
|
|
296
|
+
never: () => ({ k: "never" }),
|
|
139
297
|
null: () => ({ k: "null" }),
|
|
140
298
|
undefined: () => ({ k: "undefined" }),
|
|
141
299
|
unknown: () => ({ k: "unknown" }),
|
|
300
|
+
any: () => ({ k: "unknown" }),
|
|
142
301
|
object: () => ({ k: "anyobject" }),
|
|
302
|
+
Date: () => ({ k: "instance", ctor: Date, expected: "a Date" }),
|
|
143
303
|
true: () => ({ k: "lit", v: true }),
|
|
144
304
|
false: () => ({ k: "lit", v: false }),
|
|
145
305
|
};
|
|
146
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
|
+
|
|
147
326
|
interface ParsedTop {
|
|
148
327
|
ir: IR;
|
|
149
328
|
def?: unknown;
|
|
@@ -156,9 +335,11 @@ class StrParser {
|
|
|
156
335
|
#toks: Tok[];
|
|
157
336
|
#pos = 0;
|
|
158
337
|
#src: string;
|
|
338
|
+
#resolve: AliasResolver | undefined;
|
|
159
339
|
|
|
160
|
-
constructor(src: string) {
|
|
340
|
+
constructor(src: string, resolve?: AliasResolver) {
|
|
161
341
|
this.#src = src;
|
|
342
|
+
this.#resolve = resolve;
|
|
162
343
|
this.#toks = tokenize(src);
|
|
163
344
|
}
|
|
164
345
|
|
|
@@ -188,9 +369,10 @@ class StrParser {
|
|
|
188
369
|
let hasDefault = false;
|
|
189
370
|
if (this.#eatOp("=")) {
|
|
190
371
|
const t = this.#next();
|
|
191
|
-
if (t.t === "num" || t.t === "str") def = t.v;
|
|
372
|
+
if (t.t === "num" || t.t === "bigint" || t.t === "date" || t.t === "str") def = t.v;
|
|
192
373
|
else if (t.t === "id" && (t.v === "true" || t.v === "false")) def = t.v === "true";
|
|
193
374
|
else if (t.t === "id" && t.v === "null") def = null;
|
|
375
|
+
else if (t.t === "id" && t.v === "undefined") def = undefined;
|
|
194
376
|
else throw new OmpTypeError(`unsupported default literal in "${this.#src}"`);
|
|
195
377
|
hasDefault = true;
|
|
196
378
|
}
|
|
@@ -206,13 +388,36 @@ class StrParser {
|
|
|
206
388
|
}
|
|
207
389
|
|
|
208
390
|
parseUnion(): IR {
|
|
209
|
-
const first = this.
|
|
391
|
+
const first = this.parseIntersection();
|
|
210
392
|
if (!this.#eatOp("|")) return first;
|
|
211
|
-
const members = [first, this.
|
|
212
|
-
while (this.#eatOp("|")) members.push(this.
|
|
393
|
+
const members = [first, this.parseIntersection()];
|
|
394
|
+
while (this.#eatOp("|")) members.push(this.parseIntersection());
|
|
213
395
|
return { k: "union", members };
|
|
214
396
|
}
|
|
215
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
|
+
|
|
216
421
|
/**
|
|
217
422
|
* `NUM CMP base (CMP NUM)?` or `base (CMP NUM)?`, with `[]*` postfix on the
|
|
218
423
|
* base AND after a trailing bound — `string>0[]` is an array of bounded
|
|
@@ -221,33 +426,56 @@ class StrParser {
|
|
|
221
426
|
parseBounded(): IR {
|
|
222
427
|
const t = this.#peek();
|
|
223
428
|
const t1 = this.#peek(1);
|
|
224
|
-
if (t?.t === "num" && t1?.t === "op" &&
|
|
225
|
-
// leading bound: `1 <= string <= 10`, `0 < number`
|
|
429
|
+
if ((t?.t === "num" || t?.t === "date") && t1?.t === "op" && (t1.v === "<" || t1.v === "<=")) {
|
|
226
430
|
const lo = t.v;
|
|
227
431
|
this.#pos += 2;
|
|
228
|
-
let node = this.parsePostfix();
|
|
432
|
+
let node = this.#eatDivisor(this.parsePostfix());
|
|
229
433
|
node = applyBound(node, flip(t1.v), lo, this.#src);
|
|
230
434
|
const t2 = this.#peek();
|
|
231
|
-
if (t2?.t === "op" && CMP[t2.v]) {
|
|
232
|
-
this.#
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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}"`);
|
|
236
445
|
}
|
|
446
|
+
node = applyBound(node, t2.v, hi.v, this.#src);
|
|
237
447
|
return this.#eatArraySuffixes(node);
|
|
238
448
|
}
|
|
239
|
-
let node = this.parsePostfix();
|
|
449
|
+
let node = this.#eatDivisor(this.parsePostfix());
|
|
240
450
|
const t2 = this.#peek();
|
|
241
|
-
if (t2?.t === "op" &&
|
|
451
|
+
if (t2?.t === "op" && t2.v === "==") {
|
|
242
452
|
this.#pos++;
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
245
|
-
|
|
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);
|
|
246
462
|
node = this.#eatArraySuffixes(node);
|
|
247
463
|
}
|
|
248
464
|
return node;
|
|
249
465
|
}
|
|
250
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
|
+
|
|
251
479
|
/** Wrap `node` in array IR for each `[]` pair at the cursor. */
|
|
252
480
|
#eatArraySuffixes(node: IR): IR {
|
|
253
481
|
for (;;) {
|
|
@@ -263,14 +491,38 @@ class StrParser {
|
|
|
263
491
|
let node = this.parsePrimary();
|
|
264
492
|
for (;;) {
|
|
265
493
|
const t = this.#peek();
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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;
|
|
270
507
|
}
|
|
271
508
|
return node;
|
|
272
509
|
}
|
|
273
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
|
+
|
|
274
526
|
parsePrimary(): IR {
|
|
275
527
|
const t = this.#next();
|
|
276
528
|
if (t.t === "op" && t.v === "(") {
|
|
@@ -278,17 +530,294 @@ class StrParser {
|
|
|
278
530
|
if (!this.#eatOp(")")) throw new OmpTypeError(`expected ')' in "${this.#src}"`);
|
|
279
531
|
return inner;
|
|
280
532
|
}
|
|
281
|
-
if (t.t === "str") return { k: "lit", v: t.v };
|
|
282
|
-
if (t.t === "
|
|
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);
|
|
283
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);
|
|
284
568
|
const make = KEYWORDS[t.v];
|
|
285
|
-
|
|
286
|
-
|
|
569
|
+
const keyword = scoped ?? make?.() ?? keywordIR(t.v);
|
|
570
|
+
if (!keyword) throw new OmpTypeError(`unknown keyword "${t.v}" in "${this.#src}"`);
|
|
571
|
+
return keyword;
|
|
287
572
|
}
|
|
288
573
|
throw new OmpTypeError(`unexpected token in "${this.#src}"`);
|
|
289
574
|
}
|
|
290
575
|
}
|
|
291
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
|
+
|
|
292
821
|
function flip(op: string): string {
|
|
293
822
|
switch (op) {
|
|
294
823
|
case "<":
|
|
@@ -302,136 +831,890 @@ function flip(op: string): string {
|
|
|
302
831
|
}
|
|
303
832
|
}
|
|
304
833
|
|
|
305
|
-
|
|
306
|
-
|
|
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
|
+
}
|
|
307
887
|
if (node.k === "number") {
|
|
888
|
+
const bounded = { ...node };
|
|
308
889
|
switch (op) {
|
|
309
890
|
case ">=":
|
|
310
|
-
|
|
311
|
-
|
|
891
|
+
bounded.min = value;
|
|
892
|
+
bounded.xmin = false;
|
|
312
893
|
break;
|
|
313
894
|
case ">":
|
|
314
|
-
|
|
315
|
-
|
|
895
|
+
bounded.min = value;
|
|
896
|
+
bounded.xmin = true;
|
|
316
897
|
break;
|
|
317
898
|
case "<=":
|
|
318
|
-
|
|
319
|
-
|
|
899
|
+
bounded.max = value;
|
|
900
|
+
bounded.xmax = false;
|
|
320
901
|
break;
|
|
321
902
|
case "<":
|
|
322
|
-
|
|
323
|
-
|
|
903
|
+
bounded.max = value;
|
|
904
|
+
bounded.xmax = true;
|
|
324
905
|
break;
|
|
325
906
|
}
|
|
326
|
-
|
|
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;
|
|
327
915
|
}
|
|
328
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 };
|
|
329
921
|
switch (op) {
|
|
330
922
|
case ">=":
|
|
331
|
-
|
|
923
|
+
bounded.min = value;
|
|
332
924
|
break;
|
|
333
925
|
case ">":
|
|
334
|
-
|
|
926
|
+
bounded.min = value + 1;
|
|
335
927
|
break;
|
|
336
928
|
case "<=":
|
|
337
|
-
|
|
929
|
+
bounded.max = value;
|
|
338
930
|
break;
|
|
339
931
|
case "<":
|
|
340
|
-
|
|
932
|
+
bounded.max = value - 1;
|
|
341
933
|
break;
|
|
342
934
|
}
|
|
343
|
-
|
|
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;
|
|
344
942
|
}
|
|
345
943
|
throw new OmpTypeError(`cannot bound ${node.k} in "${src}"`);
|
|
346
944
|
}
|
|
347
945
|
|
|
946
|
+
function acceptsDate(node: IR): boolean {
|
|
947
|
+
return (node.k === "instance" && node.ctor === Date) || (node.k === "refine" && acceptsDate(node.base));
|
|
948
|
+
}
|
|
949
|
+
|
|
348
950
|
// ── definition parser ────────────────────────────────────────────────────────
|
|
349
951
|
|
|
350
952
|
function isEmbedded(def: unknown): def is EmbeddableSchema {
|
|
351
|
-
return (typeof def === "function" || (typeof def === "object" && def !== null)) && IR_BRAND in
|
|
953
|
+
return (typeof def === "function" || (typeof def === "object" && def !== null)) && IR_BRAND in def;
|
|
352
954
|
}
|
|
353
955
|
|
|
354
956
|
/** Embed a schema value: inline pure structure, keep `sub` nodes for stepped schemas. */
|
|
355
957
|
export function embed(schema: EmbeddableSchema): IR {
|
|
356
|
-
if (schema.hasSteps) return { k: "sub", schema, desc: schema.description };
|
|
958
|
+
if (schema.hasSteps) return { k: "sub", schema, desc: schema.description, descAuto: schema.ir.desc === undefined };
|
|
357
959
|
if (schema.description !== undefined && schema.ir.desc === undefined) {
|
|
358
|
-
return { ...schema.ir, desc: schema.description };
|
|
960
|
+
return { ...schema.ir, desc: schema.description, descAuto: true };
|
|
359
961
|
}
|
|
360
962
|
return schema.ir;
|
|
361
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
|
+
}
|
|
362
978
|
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
}
|
|
365
992
|
if (typeof def === "string") {
|
|
366
|
-
const parsed =
|
|
367
|
-
|
|
368
|
-
|
|
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
|
+
};
|
|
369
1000
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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");
|
|
387
1062
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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);
|
|
391
1326
|
}
|
|
392
|
-
|
|
393
|
-
|
|
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;
|
|
394
1339
|
if (typeof val === "string") {
|
|
395
|
-
const parsed =
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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");
|
|
400
1350
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
def: val.defaultValue,
|
|
408
|
-
defFactory: typeof val.defaultValue === "function",
|
|
409
|
-
hasDefault: true,
|
|
410
|
-
});
|
|
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);
|
|
411
1357
|
} else {
|
|
412
|
-
|
|
1358
|
+
try {
|
|
1359
|
+
key = patternIR(new RegExp(regex[1], regex[2]));
|
|
1360
|
+
} catch {
|
|
1361
|
+
throw new OmpTypeError(`invalid index signature pattern ${keyDefinition}`);
|
|
1362
|
+
}
|
|
413
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;
|
|
414
1369
|
}
|
|
415
|
-
|
|
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;
|
|
416
1589
|
}
|
|
417
|
-
throw new OmpTypeError(`unsupported definition ${String(def)}`);
|
|
418
1590
|
}
|
|
419
1591
|
|
|
420
1592
|
/** True when validating `ir` can produce an output different from its input. */
|
|
421
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;
|
|
422
1607
|
switch (ir.k) {
|
|
423
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":
|
|
424
1692
|
return true;
|
|
425
1693
|
case "object":
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
+
}
|
|
429
1701
|
}
|
|
430
|
-
return
|
|
1702
|
+
return false;
|
|
431
1703
|
case "array":
|
|
432
|
-
return
|
|
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;
|
|
433
1710
|
case "union":
|
|
434
|
-
|
|
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));
|
|
435
1718
|
default:
|
|
436
1719
|
return false;
|
|
437
1720
|
}
|
|
@@ -439,6 +1722,7 @@ export function hasMorph(ir: IR): boolean {
|
|
|
439
1722
|
|
|
440
1723
|
/** Human-readable expectation for error messages, e.g. `"a string"`. */
|
|
441
1724
|
export function expectedOf(ir: IR): string {
|
|
1725
|
+
if (ir.desc !== undefined) return ir.desc;
|
|
442
1726
|
switch (ir.k) {
|
|
443
1727
|
case "unknown":
|
|
444
1728
|
return "unknown";
|
|
@@ -450,6 +1734,10 @@ export function expectedOf(ir: IR): string {
|
|
|
450
1734
|
return "boolean";
|
|
451
1735
|
case "bigint":
|
|
452
1736
|
return "a bigint";
|
|
1737
|
+
case "symbol":
|
|
1738
|
+
return "a symbol";
|
|
1739
|
+
case "never":
|
|
1740
|
+
return "never";
|
|
453
1741
|
case "anyobject":
|
|
454
1742
|
return "an object";
|
|
455
1743
|
case "string": {
|
|
@@ -464,17 +1752,45 @@ export function expectedOf(ir: IR): string {
|
|
|
464
1752
|
if (ir.min !== undefined) out += ` ${ir.xmin ? "more than" : "at least"} ${ir.min}`;
|
|
465
1753
|
if (ir.max !== undefined)
|
|
466
1754
|
out += `${ir.min !== undefined ? " and" : ""} ${ir.xmax ? "less than" : "at most"} ${ir.max}`;
|
|
1755
|
+
if (ir.divisor !== undefined) out += ` divisible by ${ir.divisor}`;
|
|
467
1756
|
return out;
|
|
468
1757
|
}
|
|
469
1758
|
case "lit":
|
|
470
|
-
return
|
|
471
|
-
|
|
472
|
-
|
|
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 ");
|
|
473
1779
|
case "array":
|
|
474
1780
|
return "an array";
|
|
1781
|
+
case "tuple":
|
|
1782
|
+
return "a tuple";
|
|
475
1783
|
case "object":
|
|
476
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;
|
|
477
1793
|
case "sub":
|
|
478
|
-
return expectedOf(ir.schema.ir);
|
|
1794
|
+
return ir.desc ?? ir.schema.description ?? expectedOf(ir.schema.ir);
|
|
479
1795
|
}
|
|
480
1796
|
}
|