@oh-my-pi/omptype 17.2.7 → 17.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/js/compile.js +414 -219
- package/dist/js/errors.js +236 -41
- package/dist/js/from-json-schema.js +234 -0
- package/dist/js/index.js +1 -0
- package/dist/js/interp.js +514 -132
- package/dist/js/ir.js +972 -202
- package/dist/js/json-schema.js +73 -34
- package/dist/js/keywords.js +108 -1
- package/dist/js/type.js +2156 -172
- package/dist/js/typebox.js +41 -32
- package/dist/js/zod.js +2 -2
- package/dist/types/compile.d.ts +1 -1
- package/dist/types/errors.d.ts +19 -18
- package/dist/types/from-json-schema.d.ts +9 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/infer.d.ts +45 -6
- package/dist/types/interp.d.ts +9 -2
- package/dist/types/ir.d.ts +57 -6
- package/dist/types/json-schema.d.ts +10 -1
- package/dist/types/type.d.ts +324 -48
- package/dist/types/typebox.d.ts +78 -50
- package/package.json +5 -1
- package/src/compile.ts +598 -243
- package/src/errors.ts +247 -49
- package/src/from-json-schema.ts +231 -0
- package/src/index.ts +1 -0
- package/src/infer.ts +107 -32
- package/src/interp.ts +457 -118
- package/src/ir.ts +981 -212
- package/src/json-schema.ts +82 -34
- package/src/keywords.ts +111 -1
- package/src/type.ts +2760 -271
- package/src/typebox.ts +141 -98
- package/src/zod.ts +1 -1
package/src/ir.ts
CHANGED
|
@@ -10,16 +10,30 @@
|
|
|
10
10
|
* and the JSON Schema emitter (`json-schema.ts`).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { OmpErrors, OmpTypeError } from "./errors";
|
|
13
|
+
import { type ErrorConfig, OmpErrors, OmpTypeError } from "./errors";
|
|
14
14
|
import { keywordIR, patternIR, templateIR } from "./keywords";
|
|
15
15
|
|
|
16
16
|
/** Brand carried by `Type` instances so the parser can embed them in defs. */
|
|
17
17
|
export const IR_BRAND: unique symbol = Symbol("omptype.schema");
|
|
18
18
|
|
|
19
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");
|
|
20
25
|
|
|
21
26
|
interface IRAnalysis {
|
|
22
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;
|
|
23
37
|
}
|
|
24
38
|
|
|
25
39
|
/**
|
|
@@ -32,13 +46,20 @@ export interface EmbeddableSchema {
|
|
|
32
46
|
ir: IR;
|
|
33
47
|
/** True when the schema carries `.pipe()`/`.narrow()` steps. */
|
|
34
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;
|
|
35
53
|
/** `.default()` payload; a function is a factory invoked per fill. */
|
|
36
54
|
defaultValue?: unknown;
|
|
37
55
|
hasDefault: boolean;
|
|
56
|
+
/** Precomputed output for a non-factory default after validation and morphs. */
|
|
57
|
+
defaultOutput?: unknown;
|
|
58
|
+
hasDefaultOutput?: boolean;
|
|
38
59
|
/** `.describe()` annotation, emitted into JSON Schema. */
|
|
39
60
|
description?: string;
|
|
40
61
|
/** Full validate+morph pipeline (identical to calling the schema). */
|
|
41
|
-
run(value: unknown): unknown;
|
|
62
|
+
run(value: unknown, path?: readonly PropertyKey[]): unknown;
|
|
42
63
|
}
|
|
43
64
|
|
|
44
65
|
/** Policy for undeclared object keys. */
|
|
@@ -62,6 +83,8 @@ export interface TupleItemIR {
|
|
|
62
83
|
def?: unknown;
|
|
63
84
|
defFactory?: boolean;
|
|
64
85
|
hasDefault?: boolean;
|
|
86
|
+
/** True once the default has been validated and static morph output precomputed. */
|
|
87
|
+
defValidated?: boolean;
|
|
65
88
|
}
|
|
66
89
|
|
|
67
90
|
/** Fixed, optional, variadic, and postfix tuple sequence. */
|
|
@@ -100,11 +123,19 @@ export type IR = IRAnalysis &
|
|
|
100
123
|
| { k: "intersection"; members: IR[]; desc?: string }
|
|
101
124
|
| { k: "array"; el: IR; min?: number; max?: number; desc?: string }
|
|
102
125
|
| TupleIR
|
|
103
|
-
| {
|
|
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
|
+
}
|
|
104
135
|
| {
|
|
105
136
|
k: "refine";
|
|
106
137
|
base: IR;
|
|
107
|
-
pred: (value: unknown) => boolean;
|
|
138
|
+
pred: (value: unknown) => boolean | OmpErrors;
|
|
108
139
|
expected: string;
|
|
109
140
|
json?: Record<string, unknown>;
|
|
110
141
|
desc?: string;
|
|
@@ -123,13 +154,15 @@ export type IR = IRAnalysis &
|
|
|
123
154
|
);
|
|
124
155
|
|
|
125
156
|
export interface PropIR {
|
|
126
|
-
key:
|
|
157
|
+
key: PropertyKey;
|
|
127
158
|
opt: boolean;
|
|
128
159
|
val: IR;
|
|
129
160
|
/** Default payload (value, or factory when `defFactory`); missing key is filled. */
|
|
130
161
|
def?: unknown;
|
|
131
162
|
defFactory?: boolean;
|
|
132
163
|
hasDefault?: boolean;
|
|
164
|
+
/** True once the default has been validated and static morph output precomputed. */
|
|
165
|
+
defValidated?: boolean;
|
|
133
166
|
}
|
|
134
167
|
|
|
135
168
|
/** Definition input accepted by `type()` and object property values. */
|
|
@@ -140,11 +173,13 @@ export type Def = string | RegExp | Date | EmbeddableSchema | readonly unknown[]
|
|
|
140
173
|
type Tok =
|
|
141
174
|
| { t: "id"; v: string }
|
|
142
175
|
| { t: "num"; v: number }
|
|
176
|
+
| { t: "bigint"; v: bigint }
|
|
143
177
|
| { t: "date"; v: Date }
|
|
178
|
+
| { t: "regex"; v: RegExp }
|
|
144
179
|
| { t: "str"; v: string }
|
|
145
180
|
| { t: "op"; v: string };
|
|
146
181
|
|
|
147
|
-
const SIMPLE_OPS = "
|
|
182
|
+
const SIMPLE_OPS = "|&()[]=?%,#";
|
|
148
183
|
|
|
149
184
|
function tokenize(src: string): Tok[] {
|
|
150
185
|
const toks: Tok[] = [];
|
|
@@ -156,28 +191,63 @@ function tokenize(src: string): Tok[] {
|
|
|
156
191
|
i++;
|
|
157
192
|
continue;
|
|
158
193
|
}
|
|
159
|
-
if (c === "'" || c === '"') {
|
|
160
|
-
let j = i + 1;
|
|
161
|
-
while (j < n && src[j] !== c) j++;
|
|
162
|
-
if (j >= n) throw new OmpTypeError(`unterminated string literal in "${src}"`);
|
|
163
|
-
toks.push({ t: "str", v: src.slice(i + 1, j) });
|
|
164
|
-
i = j + 1;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
194
|
if (c === "d" && (src[i + 1] === "'" || src[i + 1] === '"')) {
|
|
168
195
|
const quote = src[i + 1];
|
|
169
196
|
const end = src.indexOf(quote, i + 2);
|
|
170
197
|
if (end < 0) throw new OmpTypeError(`unterminated date literal in "${src}"`);
|
|
171
|
-
const
|
|
198
|
+
const source = src.slice(i + 2, end).trim();
|
|
199
|
+
const value = /^\d+$/.test(source) ? new Date(Number(source)) : new Date(source);
|
|
172
200
|
if (Number.isNaN(value.valueOf())) throw new OmpTypeError(`invalid date literal in "${src}"`);
|
|
173
201
|
toks.push({ t: "date", v: value });
|
|
174
202
|
i = end + 1;
|
|
175
203
|
continue;
|
|
176
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
|
+
}
|
|
177
239
|
if ((c >= "0" && c <= "9") || (c === "-" && i + 1 < n && src[i + 1] >= "0" && src[i + 1] <= "9")) {
|
|
178
240
|
let j = i + 1;
|
|
179
|
-
while (j < n &&
|
|
180
|
-
|
|
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
|
+
}
|
|
181
251
|
i = j;
|
|
182
252
|
continue;
|
|
183
253
|
}
|
|
@@ -198,6 +268,11 @@ function tokenize(src: string): Tok[] {
|
|
|
198
268
|
}
|
|
199
269
|
continue;
|
|
200
270
|
}
|
|
271
|
+
if (c === "=" && src[i + 1] === "=") {
|
|
272
|
+
toks.push({ t: "op", v: "==" });
|
|
273
|
+
i += 2;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
201
276
|
if (SIMPLE_OPS.includes(c)) {
|
|
202
277
|
toks.push({ t: "op", v: c });
|
|
203
278
|
i++;
|
|
@@ -229,8 +304,24 @@ const KEYWORDS: Record<string, () => IR> = {
|
|
|
229
304
|
false: () => ({ k: "lit", v: false }),
|
|
230
305
|
};
|
|
231
306
|
|
|
232
|
-
/** Resolve
|
|
233
|
-
export
|
|
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
|
+
}
|
|
234
325
|
|
|
235
326
|
interface ParsedTop {
|
|
236
327
|
ir: IR;
|
|
@@ -278,9 +369,10 @@ class StrParser {
|
|
|
278
369
|
let hasDefault = false;
|
|
279
370
|
if (this.#eatOp("=")) {
|
|
280
371
|
const t = this.#next();
|
|
281
|
-
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;
|
|
282
373
|
else if (t.t === "id" && (t.v === "true" || t.v === "false")) def = t.v === "true";
|
|
283
374
|
else if (t.t === "id" && t.v === "null") def = null;
|
|
375
|
+
else if (t.t === "id" && t.v === "undefined") def = undefined;
|
|
284
376
|
else throw new OmpTypeError(`unsupported default literal in "${this.#src}"`);
|
|
285
377
|
hasDefault = true;
|
|
286
378
|
}
|
|
@@ -296,13 +388,36 @@ class StrParser {
|
|
|
296
388
|
}
|
|
297
389
|
|
|
298
390
|
parseUnion(): IR {
|
|
299
|
-
const first = this.
|
|
391
|
+
const first = this.parseIntersection();
|
|
300
392
|
if (!this.#eatOp("|")) return first;
|
|
301
|
-
const members = [first, this.
|
|
302
|
-
while (this.#eatOp("|")) members.push(this.
|
|
393
|
+
const members = [first, this.parseIntersection()];
|
|
394
|
+
while (this.#eatOp("|")) members.push(this.parseIntersection());
|
|
303
395
|
return { k: "union", members };
|
|
304
396
|
}
|
|
305
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
|
+
|
|
306
421
|
/**
|
|
307
422
|
* `NUM CMP base (CMP NUM)?` or `base (CMP NUM)?`, with `[]*` postfix on the
|
|
308
423
|
* base AND after a trailing bound — `string>0[]` is an array of bounded
|
|
@@ -311,30 +426,38 @@ class StrParser {
|
|
|
311
426
|
parseBounded(): IR {
|
|
312
427
|
const t = this.#peek();
|
|
313
428
|
const t1 = this.#peek(1);
|
|
314
|
-
if ((t?.t === "num" || t?.t === "date") && t1?.t === "op" &&
|
|
429
|
+
if ((t?.t === "num" || t?.t === "date") && t1?.t === "op" && (t1.v === "<" || t1.v === "<=")) {
|
|
315
430
|
const lo = t.v;
|
|
316
431
|
this.#pos += 2;
|
|
317
432
|
let node = this.#eatDivisor(this.parsePostfix());
|
|
318
433
|
node = applyBound(node, flip(t1.v), lo, this.#src);
|
|
319
434
|
const t2 = this.#peek();
|
|
320
|
-
if (t2?.t === "op" && CMP[t2.v]) {
|
|
321
|
-
this.#
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
node = applyBound(node, t2.v, hi.v, this.#src);
|
|
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}"`);
|
|
327
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);
|
|
328
447
|
return this.#eatArraySuffixes(node);
|
|
329
448
|
}
|
|
330
449
|
let node = this.#eatDivisor(this.parsePostfix());
|
|
331
450
|
const t2 = this.#peek();
|
|
332
|
-
if (t2?.t === "op" &&
|
|
451
|
+
if (t2?.t === "op" && t2.v === "==") {
|
|
333
452
|
this.#pos++;
|
|
334
453
|
const limit = this.#next();
|
|
335
|
-
if (limit.t !== "num" && limit.t !== "date") {
|
|
336
|
-
throw new OmpTypeError(`expected
|
|
454
|
+
if (limit.t !== "num" && limit.t !== "bigint" && limit.t !== "date") {
|
|
455
|
+
throw new OmpTypeError(`expected literal after == in "${this.#src}"`);
|
|
337
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" }>;
|
|
338
461
|
node = applyBound(node, t2.v, limit.v, this.#src);
|
|
339
462
|
node = this.#eatArraySuffixes(node);
|
|
340
463
|
}
|
|
@@ -346,10 +469,11 @@ class StrParser {
|
|
|
346
469
|
const divisor = this.#next();
|
|
347
470
|
if (divisor.t !== "num") throw new OmpTypeError(`expected number after % in "${this.#src}"`);
|
|
348
471
|
if (node.k !== "number") throw new OmpTypeError(`% requires number in "${this.#src}"`);
|
|
349
|
-
if (!Number.isFinite(divisor.v) || divisor.v === 0)
|
|
350
|
-
throw new OmpTypeError(`divisor must be non-zero in "${this.#src}"`);
|
|
351
|
-
node
|
|
352
|
-
|
|
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) };
|
|
353
477
|
}
|
|
354
478
|
|
|
355
479
|
/** Wrap `node` in array IR for each `[]` pair at the cursor. */
|
|
@@ -367,14 +491,38 @@ class StrParser {
|
|
|
367
491
|
let node = this.parsePrimary();
|
|
368
492
|
for (;;) {
|
|
369
493
|
const t = this.#peek();
|
|
370
|
-
if (
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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;
|
|
374
507
|
}
|
|
375
508
|
return node;
|
|
376
509
|
}
|
|
377
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
|
+
|
|
378
526
|
parsePrimary(): IR {
|
|
379
527
|
const t = this.#next();
|
|
380
528
|
if (t.t === "op" && t.v === "(") {
|
|
@@ -382,10 +530,43 @@ class StrParser {
|
|
|
382
530
|
if (!this.#eatOp(")")) throw new OmpTypeError(`expected ')' in "${this.#src}"`);
|
|
383
531
|
return inner;
|
|
384
532
|
}
|
|
385
|
-
if (t.t === "str" || t.t === "num" || t.t === "date") return { k: "lit", v: t.v };
|
|
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);
|
|
386
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);
|
|
387
568
|
const make = KEYWORDS[t.v];
|
|
388
|
-
const keyword = make?.() ?? keywordIR(t.v)
|
|
569
|
+
const keyword = scoped ?? make?.() ?? keywordIR(t.v);
|
|
389
570
|
if (!keyword) throw new OmpTypeError(`unknown keyword "${t.v}" in "${this.#src}"`);
|
|
390
571
|
return keyword;
|
|
391
572
|
}
|
|
@@ -403,7 +584,7 @@ function isWhitespaceAt(src: string, index: number): boolean {
|
|
|
403
584
|
|
|
404
585
|
/** Fast path for the literal unions pervasive in command schemas. */
|
|
405
586
|
function parseLiteralUnion(src: string): IR | undefined {
|
|
406
|
-
const members: IR[] = [];
|
|
587
|
+
const members: Extract<IR, { k: "lit" }>[] = [];
|
|
407
588
|
let index = 0;
|
|
408
589
|
while (index < src.length && isWhitespaceAt(src, index)) index++;
|
|
409
590
|
for (;;) {
|
|
@@ -415,7 +596,19 @@ function parseLiteralUnion(src: string): IR | undefined {
|
|
|
415
596
|
index = end + 1;
|
|
416
597
|
while (index < src.length && isWhitespaceAt(src, index)) index++;
|
|
417
598
|
if (index === src.length) {
|
|
418
|
-
|
|
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;
|
|
419
612
|
}
|
|
420
613
|
if (src[index] !== "|") return undefined;
|
|
421
614
|
index++;
|
|
@@ -453,8 +646,8 @@ function genericArguments(src: string): { name: string; args: string[] } | undef
|
|
|
453
646
|
return { name, args };
|
|
454
647
|
}
|
|
455
648
|
|
|
456
|
-
function genericKeys(ir: IR): Set<
|
|
457
|
-
const keys = new Set<
|
|
649
|
+
function genericKeys(ir: IR): Set<PropertyKey> {
|
|
650
|
+
const keys = new Set<PropertyKey>();
|
|
458
651
|
const visit = (node: IR): void => {
|
|
459
652
|
if (node.k === "lit" && typeof node.v === "string") keys.add(node.v);
|
|
460
653
|
else if (node.k === "union") for (const member of node.members) visit(member);
|
|
@@ -489,6 +682,11 @@ function mergeObjectIR(left: IR, right: IR): IR {
|
|
|
489
682
|
k: "object",
|
|
490
683
|
props,
|
|
491
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 ?? [])],
|
|
492
690
|
extras: right.extras === "keep" ? left.extras : right.extras,
|
|
493
691
|
};
|
|
494
692
|
}
|
|
@@ -496,9 +694,26 @@ function mergeObjectIR(left: IR, right: IR): IR {
|
|
|
496
694
|
function parseGeneric(src: string, resolve?: AliasResolver): IR | undefined {
|
|
497
695
|
const generic = genericArguments(src);
|
|
498
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
|
+
}
|
|
499
707
|
if (generic.name === "Record" && generic.args.length === 2) {
|
|
500
708
|
return { k: "object", props: [], index: parseDef(generic.args[1], resolve), extras: "keep" };
|
|
501
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
|
+
}
|
|
502
717
|
if ((generic.name === "Partial" || generic.name === "Required") && generic.args.length === 1) {
|
|
503
718
|
const object = resolveStructuralIR(parseDef(generic.args[0], resolve));
|
|
504
719
|
if (object.k !== "object") throw new OmpTypeError(`${generic.name} requires an object`);
|
|
@@ -518,78 +733,88 @@ function parseGeneric(src: string, resolve?: AliasResolver): IR | undefined {
|
|
|
518
733
|
return undefined;
|
|
519
734
|
}
|
|
520
735
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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 };
|
|
525
757
|
}
|
|
526
758
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
if (
|
|
530
|
-
const
|
|
531
|
-
if (
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
: reverse?.[3] === "<="
|
|
539
|
-
? ">="
|
|
540
|
-
: reverse?.[3] === "<"
|
|
541
|
-
? ">"
|
|
542
|
-
: reverse?.[3] === ">="
|
|
543
|
-
? "<="
|
|
544
|
-
: "<";
|
|
545
|
-
const timestamp = bound.valueOf();
|
|
546
|
-
const relation =
|
|
547
|
-
operator === ">="
|
|
548
|
-
? "at or after"
|
|
549
|
-
: operator === ">"
|
|
550
|
-
? "later than"
|
|
551
|
-
: operator === "<="
|
|
552
|
-
? "at or before"
|
|
553
|
-
: "earlier than";
|
|
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
|
+
}
|
|
554
770
|
return {
|
|
555
|
-
k: "
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
return operator === ">="
|
|
561
|
-
? actual >= timestamp
|
|
562
|
-
: operator === ">"
|
|
563
|
-
? actual > timestamp
|
|
564
|
-
: operator === "<="
|
|
565
|
-
? actual <= timestamp
|
|
566
|
-
: actual < timestamp;
|
|
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}`);
|
|
567
776
|
},
|
|
568
|
-
expected: `a Date ${relation} ${bound.toISOString()}`,
|
|
569
|
-
json: operator === ">=" || operator === ">" ? { minimum: bound.toISOString() } : { maximum: bound.toISOString() },
|
|
570
777
|
};
|
|
571
778
|
}
|
|
572
779
|
|
|
573
|
-
/** Parse recurring global DSL fragments once; scoped aliases bypass the cache. */
|
|
574
780
|
function parseStringDef(src: string, resolve?: AliasResolver): ParsedTop {
|
|
575
|
-
|
|
781
|
+
const cacheable = resolve === undefined || (!src.includes("this") && THIS_ONLY_RESOLVERS.has(resolve));
|
|
782
|
+
if (cacheable) {
|
|
576
783
|
const cached = stringDefCache.get(src);
|
|
577
784
|
if (cached) return cached;
|
|
578
785
|
}
|
|
579
|
-
|
|
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);
|
|
580
811
|
if (ir === undefined && src.startsWith("`") && src.endsWith("`")) {
|
|
581
812
|
ir = templateIR(src.slice(1, -1));
|
|
582
|
-
} else if (ir === undefined && src.startsWith("/") && src.lastIndexOf("/") > 0) {
|
|
583
|
-
const end = src.lastIndexOf("/");
|
|
584
|
-
try {
|
|
585
|
-
ir = patternIR(new RegExp(src.slice(1, end), src.slice(end + 1)));
|
|
586
|
-
} catch {
|
|
587
|
-
throw new OmpTypeError(`invalid regular expression "${src}"`);
|
|
588
|
-
}
|
|
589
813
|
}
|
|
814
|
+
|
|
590
815
|
const parsed: ParsedTop =
|
|
591
816
|
ir === undefined ? new StrParser(src, resolve).parseTop() : { ir, hasDefault: false, optional: false };
|
|
592
|
-
if (
|
|
817
|
+
if (cacheable && stringDefCache.size < STRING_DEF_CACHE_MAX) stringDefCache.set(src, parsed);
|
|
593
818
|
return parsed;
|
|
594
819
|
}
|
|
595
820
|
|
|
@@ -606,8 +831,43 @@ function flip(op: string): string {
|
|
|
606
831
|
}
|
|
607
832
|
}
|
|
608
833
|
|
|
609
|
-
|
|
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
|
+
*/
|
|
610
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
|
+
}
|
|
611
871
|
if (value instanceof Date) {
|
|
612
872
|
if (!acceptsDate(node)) throw new OmpTypeError(`date bound requires Date in "${src}"`);
|
|
613
873
|
const limit = value.valueOf();
|
|
@@ -625,42 +885,60 @@ function applyBound(node: IR, op: string, value: number | Date, src: string): IR
|
|
|
625
885
|
};
|
|
626
886
|
}
|
|
627
887
|
if (node.k === "number") {
|
|
888
|
+
const bounded = { ...node };
|
|
628
889
|
switch (op) {
|
|
629
890
|
case ">=":
|
|
630
|
-
|
|
631
|
-
|
|
891
|
+
bounded.min = value;
|
|
892
|
+
bounded.xmin = false;
|
|
632
893
|
break;
|
|
633
894
|
case ">":
|
|
634
|
-
|
|
635
|
-
|
|
895
|
+
bounded.min = value;
|
|
896
|
+
bounded.xmin = true;
|
|
636
897
|
break;
|
|
637
898
|
case "<=":
|
|
638
|
-
|
|
639
|
-
|
|
899
|
+
bounded.max = value;
|
|
900
|
+
bounded.xmax = false;
|
|
640
901
|
break;
|
|
641
902
|
case "<":
|
|
642
|
-
|
|
643
|
-
|
|
903
|
+
bounded.max = value;
|
|
904
|
+
bounded.xmax = true;
|
|
644
905
|
break;
|
|
645
906
|
}
|
|
646
|
-
|
|
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;
|
|
647
915
|
}
|
|
648
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 };
|
|
649
921
|
switch (op) {
|
|
650
922
|
case ">=":
|
|
651
|
-
|
|
923
|
+
bounded.min = value;
|
|
652
924
|
break;
|
|
653
925
|
case ">":
|
|
654
|
-
|
|
926
|
+
bounded.min = value + 1;
|
|
655
927
|
break;
|
|
656
928
|
case "<=":
|
|
657
|
-
|
|
929
|
+
bounded.max = value;
|
|
658
930
|
break;
|
|
659
931
|
case "<":
|
|
660
|
-
|
|
932
|
+
bounded.max = value - 1;
|
|
661
933
|
break;
|
|
662
934
|
}
|
|
663
|
-
|
|
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;
|
|
664
942
|
}
|
|
665
943
|
throw new OmpTypeError(`cannot bound ${node.k} in "${src}"`);
|
|
666
944
|
}
|
|
@@ -677,12 +955,18 @@ function isEmbedded(def: unknown): def is EmbeddableSchema {
|
|
|
677
955
|
|
|
678
956
|
/** Embed a schema value: inline pure structure, keep `sub` nodes for stepped schemas. */
|
|
679
957
|
export function embed(schema: EmbeddableSchema): IR {
|
|
680
|
-
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 };
|
|
681
959
|
if (schema.description !== undefined && schema.ir.desc === undefined) {
|
|
682
|
-
return { ...schema.ir, desc: schema.description };
|
|
960
|
+
return { ...schema.ir, desc: schema.description, descAuto: true };
|
|
683
961
|
}
|
|
684
962
|
return schema.ir;
|
|
685
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
|
+
}
|
|
686
970
|
|
|
687
971
|
function isCallback(value: unknown): value is (input: unknown, context: MorphContext) => unknown {
|
|
688
972
|
return typeof value === "function";
|
|
@@ -705,52 +989,137 @@ function parseTupleItem(def: unknown, resolve?: AliasResolver): TupleItemIR {
|
|
|
705
989
|
hasDefault: true,
|
|
706
990
|
};
|
|
707
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
|
+
}
|
|
708
1001
|
return { val: parseDef(def, resolve), opt: false };
|
|
709
1002
|
}
|
|
710
1003
|
|
|
711
|
-
function
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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: [] }];
|
|
716
1058
|
for (let index = 0; index < def.length; index++) {
|
|
717
1059
|
if (def[index] === "...") {
|
|
718
|
-
if (
|
|
1060
|
+
if (index + 1 >= def.length) {
|
|
719
1061
|
throw new OmpTypeError("a tuple may have one spread followed by an array definition");
|
|
720
1062
|
}
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
throw new OmpTypeError("optional tuple elements cannot follow a variadic element");
|
|
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
|
+
}
|
|
730
1071
|
}
|
|
731
|
-
|
|
1072
|
+
branches = distributed;
|
|
732
1073
|
continue;
|
|
733
1074
|
}
|
|
734
1075
|
const item = parseTupleItem(def[index], resolve);
|
|
735
|
-
|
|
736
|
-
throw new OmpTypeError("required tuple elements cannot follow optional elements");
|
|
737
|
-
}
|
|
738
|
-
optionalSeen ||= item.opt || item.hasDefault === true;
|
|
739
|
-
prefix.push(item);
|
|
1076
|
+
for (const branch of branches) appendTupleItem(branch, { ...item });
|
|
740
1077
|
}
|
|
741
|
-
return { k: "
|
|
1078
|
+
return branches.length === 1 ? branches[0] : { k: "union", members: branches };
|
|
742
1079
|
}
|
|
743
1080
|
|
|
744
1081
|
/** Build the runtime schema for an object's or tuple's keys. */
|
|
745
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
|
+
}
|
|
746
1093
|
if (node.k === "object") {
|
|
747
1094
|
const members: IR[] = node.props.map(prop => ({ k: "lit", v: prop.key }));
|
|
748
|
-
if (node.index !== undefined) members.push({ k: "string" });
|
|
1095
|
+
if (node.index !== undefined || (node.patternIndexes?.length ?? 0) > 0) members.push({ k: "string" });
|
|
1096
|
+
if (node.symbolIndex !== undefined) members.push({ k: "symbol" });
|
|
749
1097
|
if (members.length === 0) return { k: "never" };
|
|
750
1098
|
return members.length === 1 ? members[0] : { k: "union", members };
|
|
751
1099
|
}
|
|
752
1100
|
if (node.k === "tuple") return { k: "number", int: true, min: 0 };
|
|
753
|
-
|
|
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");
|
|
754
1123
|
}
|
|
755
1124
|
|
|
756
1125
|
function parseArrayExpression(def: readonly unknown[], resolve?: AliasResolver): IR {
|
|
@@ -761,7 +1130,11 @@ function parseArrayExpression(def: readonly unknown[], resolve?: AliasResolver):
|
|
|
761
1130
|
for (let index = 1; index < def.length; index++) {
|
|
762
1131
|
const ctor = def[index];
|
|
763
1132
|
if (!isConstructor(ctor)) throw new OmpTypeError("instanceof operands must be constructors");
|
|
764
|
-
members.push({
|
|
1133
|
+
members.push({
|
|
1134
|
+
k: "instance",
|
|
1135
|
+
ctor,
|
|
1136
|
+
expected: ctor === Error ? "an Error" : `an instance of ${ctor.name || "the constructor"}`,
|
|
1137
|
+
});
|
|
765
1138
|
}
|
|
766
1139
|
return members.length === 1 ? members[0] : { k: "union", members };
|
|
767
1140
|
}
|
|
@@ -786,30 +1159,59 @@ function parseArrayExpression(def: readonly unknown[], resolve?: AliasResolver):
|
|
|
786
1159
|
if (def.length === 3 && def[1] === ":") {
|
|
787
1160
|
if (!isCallback(def[2])) throw new OmpTypeError("narrow operator requires a predicate");
|
|
788
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}`;
|
|
789
1164
|
return {
|
|
790
1165
|
k: "refine",
|
|
791
1166
|
base: parseDef(def[0], resolve),
|
|
792
|
-
pred: value =>
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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,
|
|
798
1192
|
};
|
|
799
1193
|
}
|
|
800
1194
|
if (def.length >= 3 && def[1] === "@") {
|
|
801
1195
|
const base = parseDef(def[0], resolve);
|
|
802
1196
|
const meta = def[2];
|
|
803
|
-
if (typeof meta === "string") return { ...base, desc: meta };
|
|
804
|
-
if (typeof meta === "object" && meta !== null
|
|
805
|
-
|
|
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
|
+
};
|
|
806
1208
|
}
|
|
807
1209
|
return base;
|
|
808
1210
|
}
|
|
809
1211
|
return parseTuple(def, resolve);
|
|
810
1212
|
}
|
|
811
1213
|
|
|
812
|
-
function isObjectDefinition(def: unknown): def is Record<
|
|
1214
|
+
function isObjectDefinition(def: unknown): def is Record<PropertyKey, unknown> {
|
|
813
1215
|
return (
|
|
814
1216
|
typeof def === "object" &&
|
|
815
1217
|
def !== null &&
|
|
@@ -819,141 +1221,508 @@ function isObjectDefinition(def: unknown): def is Record<string, unknown> {
|
|
|
819
1221
|
);
|
|
820
1222
|
}
|
|
821
1223
|
|
|
822
|
-
function
|
|
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 {
|
|
823
1289
|
const props: PropIR[] = [];
|
|
824
|
-
let
|
|
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;
|
|
825
1300
|
let extras: Extras = "keep";
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
if (
|
|
829
|
-
|
|
830
|
-
|
|
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";
|
|
831
1310
|
else throw new OmpTypeError(`bad "+" value ${String(val)}`);
|
|
832
1311
|
continue;
|
|
833
1312
|
}
|
|
834
|
-
if (
|
|
835
|
-
const
|
|
836
|
-
|
|
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();
|
|
837
1321
|
for (const prop of spread.props) {
|
|
838
1322
|
const previous = props.findIndex(candidate => candidate.key === prop.key);
|
|
839
1323
|
if (previous < 0) props.push(prop);
|
|
840
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);
|
|
841
1333
|
}
|
|
842
|
-
index ??= spread.index;
|
|
843
1334
|
if (spread.extras !== "keep") extras = spread.extras;
|
|
844
1335
|
continue;
|
|
845
1336
|
}
|
|
846
|
-
if (
|
|
847
|
-
|
|
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;
|
|
848
1368
|
continue;
|
|
849
1369
|
}
|
|
850
|
-
const
|
|
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("?");
|
|
851
1380
|
const key = opt ? rawKey.slice(0, -1) : rawKey;
|
|
1381
|
+
let prop: PropIR;
|
|
852
1382
|
if (typeof val === "string") {
|
|
853
1383
|
const parsed = parseStringDef(val, resolve);
|
|
854
|
-
|
|
1384
|
+
prop = { key, opt: opt || parsed.optional, val: parsed.ir };
|
|
855
1385
|
if (parsed.hasDefault) {
|
|
856
1386
|
prop.def = parsed.def;
|
|
857
1387
|
prop.hasDefault = true;
|
|
858
1388
|
}
|
|
859
|
-
props.push(prop);
|
|
860
1389
|
} else if (Array.isArray(val) && val.length === 2 && val[1] === "?") {
|
|
861
|
-
|
|
1390
|
+
prop = { key, opt: true, val: parseDef(val[0], resolve) };
|
|
862
1391
|
} else if (Array.isArray(val) && val.length === 3 && val[1] === "=") {
|
|
863
|
-
|
|
1392
|
+
prop = {
|
|
864
1393
|
key,
|
|
865
1394
|
opt,
|
|
866
1395
|
val: parseDef(val[0], resolve),
|
|
867
1396
|
def: val[2],
|
|
868
1397
|
defFactory: typeof val[2] === "function",
|
|
869
1398
|
hasDefault: true,
|
|
870
|
-
}
|
|
1399
|
+
};
|
|
871
1400
|
} else if (isEmbedded(val)) {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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);
|
|
883
1427
|
}
|
|
884
|
-
} else if (
|
|
885
|
-
|
|
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) };
|
|
886
1469
|
} else {
|
|
887
|
-
|
|
1470
|
+
prop = {
|
|
1471
|
+
key,
|
|
1472
|
+
opt: false,
|
|
1473
|
+
val: isObjectDefinition(val) ? parseObjectDefinition(val, resolve) : parseDef(val, resolve),
|
|
1474
|
+
};
|
|
888
1475
|
}
|
|
1476
|
+
if (simple && (prop.hasDefault || !isSimpleIR(prop.val))) simple = false;
|
|
1477
|
+
addObjectProp(props, spreadKeys, prop);
|
|
889
1478
|
}
|
|
890
|
-
|
|
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;
|
|
891
1490
|
}
|
|
892
1491
|
|
|
893
1492
|
/** Parse a definition, optionally resolving names from an enclosing scope. */
|
|
894
1493
|
export function parseDef(def: unknown, resolve?: AliasResolver): IR {
|
|
895
1494
|
if (typeof def === "string") {
|
|
896
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
|
+
}
|
|
897
1499
|
if (parsed.optional) {
|
|
898
1500
|
throw new OmpTypeError(`optional "?" marker is only valid on object property values`);
|
|
899
1501
|
}
|
|
900
1502
|
return parsed.ir;
|
|
901
1503
|
}
|
|
902
|
-
if (Array.isArray(def))
|
|
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
|
+
}
|
|
903
1510
|
if (def instanceof RegExp) return patternIR(def);
|
|
904
1511
|
if (def instanceof Date) return { k: "lit", v: def };
|
|
905
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
|
+
}
|
|
906
1520
|
if (isObjectDefinition(def)) return parseObjectDefinition(def, resolve);
|
|
907
|
-
throw new OmpTypeError(`unsupported definition ${String(def)}`);
|
|
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
|
+
}
|
|
908
1590
|
}
|
|
909
1591
|
|
|
910
1592
|
/** True when validating `ir` can produce an output different from its input. */
|
|
911
1593
|
export function hasMorph(ir: IR): boolean {
|
|
912
|
-
const cached = ir[kMorph];
|
|
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;
|
|
913
1604
|
if (cached !== undefined) return cached;
|
|
914
1605
|
|
|
915
1606
|
let result = false;
|
|
916
1607
|
switch (ir.k) {
|
|
917
1608
|
case "sub":
|
|
918
|
-
result = true;
|
|
919
|
-
break;
|
|
920
1609
|
case "morph":
|
|
921
|
-
case "alias":
|
|
922
1610
|
result = true;
|
|
923
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
|
+
}
|
|
924
1620
|
case "object":
|
|
925
1621
|
result = ir.extras === "delete";
|
|
926
|
-
for (let
|
|
927
|
-
const prop = ir.props[
|
|
928
|
-
result = prop.hasDefault === true ||
|
|
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
|
+
}
|
|
929
1635
|
}
|
|
930
|
-
if (!result && ir.index !== undefined) result = hasMorph(ir.index);
|
|
931
1636
|
break;
|
|
932
1637
|
case "array":
|
|
933
|
-
result =
|
|
1638
|
+
result = scanMorph(ir.el, activeAliases);
|
|
934
1639
|
break;
|
|
935
1640
|
case "union":
|
|
936
|
-
result = ir.members.some(hasMorph);
|
|
937
|
-
break;
|
|
938
1641
|
case "intersection":
|
|
939
|
-
|
|
1642
|
+
for (const member of ir.members) {
|
|
1643
|
+
if (scanMorph(member, activeAliases)) {
|
|
1644
|
+
result = true;
|
|
1645
|
+
break;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
940
1648
|
break;
|
|
941
1649
|
case "refine":
|
|
942
|
-
result =
|
|
1650
|
+
result = scanMorph(ir.base, activeAliases);
|
|
943
1651
|
break;
|
|
944
1652
|
case "tuple":
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
+
}
|
|
949
1668
|
break;
|
|
950
1669
|
}
|
|
951
|
-
ir[kMorph] = result;
|
|
952
1670
|
return result;
|
|
953
1671
|
}
|
|
954
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
|
+
|
|
955
1723
|
/** Human-readable expectation for error messages, e.g. `"a string"`. */
|
|
956
1724
|
export function expectedOf(ir: IR): string {
|
|
1725
|
+
if (ir.desc !== undefined) return ir.desc;
|
|
957
1726
|
switch (ir.k) {
|
|
958
1727
|
case "unknown":
|
|
959
1728
|
return "unknown";
|
|
@@ -1020,8 +1789,8 @@ export function expectedOf(ir: IR): string {
|
|
|
1020
1789
|
case "morph":
|
|
1021
1790
|
return expectedOf(ir.input);
|
|
1022
1791
|
case "alias":
|
|
1023
|
-
return ir.name;
|
|
1792
|
+
return ir.name === "this" ? expectedOf(ir.resolve()) : ir.name;
|
|
1024
1793
|
case "sub":
|
|
1025
|
-
return expectedOf(ir.schema.ir);
|
|
1794
|
+
return ir.desc ?? ir.schema.description ?? expectedOf(ir.schema.ir);
|
|
1026
1795
|
}
|
|
1027
1796
|
}
|