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