@oh-my-pi/omptype 17.2.6 → 17.2.7
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 +17 -2
- package/README.md +83 -14
- package/dist/js/ark.js +16 -0
- package/dist/js/compile.js +747 -0
- package/dist/js/errors.js +199 -0
- package/dist/js/index.js +12 -0
- package/dist/js/infer.js +2 -0
- package/dist/js/interp.js +476 -0
- package/dist/js/ir.js +930 -0
- package/dist/js/json-schema.js +273 -0
- package/dist/js/keywords.js +233 -0
- package/dist/js/type.js +1078 -0
- package/dist/js/typebox.js +346 -0
- package/dist/js/zod.js +269 -0
- package/dist/types/ark.d.ts +3 -11
- package/dist/types/compile.d.ts +2 -0
- package/dist/types/errors.d.ts +51 -15
- package/dist/types/index.d.ts +3 -4
- package/dist/types/infer.d.ts +107 -30
- package/dist/types/interp.d.ts +5 -3
- package/dist/types/ir.d.ts +76 -7
- package/dist/types/json-schema.d.ts +6 -1
- package/dist/types/keywords.d.ts +7 -0
- package/dist/types/type.d.ts +238 -49
- package/dist/types/zod.d.ts +1 -1
- package/package.json +20 -8
- package/src/ark.ts +4 -14
- package/src/compile.ts +432 -54
- package/src/errors.ts +146 -34
- package/src/index.ts +3 -4
- package/src/infer.ts +284 -77
- package/src/interp.ts +153 -9
- package/src/ir.ts +636 -89
- package/src/json-schema.ts +109 -14
- package/src/keywords.ts +270 -0
- package/src/type.ts +1302 -147
- package/src/typebox.ts +1 -1
- package/src/zod.ts +40 -27
package/src/compile.ts
CHANGED
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
* arrays and messages are inline literals, so cost is one small allocation
|
|
12
12
|
* - morphing nodes (defaults, `"+": "delete"`, embedded stepped schemas)
|
|
13
13
|
* produce a fresh output object; pure subtrees below them stay check-only
|
|
14
|
-
* - union members
|
|
15
|
-
*
|
|
14
|
+
* - morphing union members use separately compiled or hoisted runners; pure
|
|
15
|
+
* members compile to inline predicates
|
|
16
16
|
*/
|
|
17
17
|
import { MISSING, OmpErrors } from "./errors";
|
|
18
|
-
import { unionFail, walk } from "./interp";
|
|
19
|
-
import { expectedOf, hasMorph, type IR, type PropIR } from "./ir";
|
|
18
|
+
import { canRefineUnionFailure, unionFail, walk } from "./interp";
|
|
19
|
+
import { expectedOf, hasMorph, type IR, type MorphContext, type PropIR, type TupleIR } from "./ir";
|
|
20
20
|
|
|
21
21
|
const own = Object.prototype.hasOwnProperty;
|
|
22
22
|
|
|
@@ -43,9 +43,57 @@ function litSource(v: unknown): string | undefined {
|
|
|
43
43
|
|
|
44
44
|
type PathSeg = { s: PropertyKey } | { d: string };
|
|
45
45
|
|
|
46
|
+
type LiteralIR = Extract<IR, { k: "lit" }>;
|
|
47
|
+
|
|
48
|
+
function isPrimitiveLiteral(node: IR): node is LiteralIR {
|
|
49
|
+
return node.k === "lit" && (node.v === null || (typeof node.v !== "object" && typeof node.v !== "function"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Whether `undefined` necessarily fails, allowing property-presence checks to be elided. */
|
|
53
|
+
function rejectsUndefined(node: IR): boolean {
|
|
54
|
+
switch (node.k) {
|
|
55
|
+
case "unknown":
|
|
56
|
+
case "undefined":
|
|
57
|
+
case "alias":
|
|
58
|
+
case "sub":
|
|
59
|
+
return false;
|
|
60
|
+
case "lit":
|
|
61
|
+
return node.v !== undefined;
|
|
62
|
+
case "union":
|
|
63
|
+
return node.members.every(rejectsUndefined);
|
|
64
|
+
case "intersection":
|
|
65
|
+
return node.members.some(rejectsUndefined);
|
|
66
|
+
case "refine":
|
|
67
|
+
return rejectsUndefined(node.base);
|
|
68
|
+
case "morph":
|
|
69
|
+
return rejectsUndefined(node.input);
|
|
70
|
+
default:
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class CompiledMorphContext implements MorphContext {
|
|
76
|
+
#path: PropertyKey[] | PropertyKey | undefined;
|
|
77
|
+
#data: unknown;
|
|
78
|
+
|
|
79
|
+
constructor(path: PropertyKey[] | PropertyKey | undefined, data: unknown) {
|
|
80
|
+
this.#path = path;
|
|
81
|
+
this.#data = data;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
error(expectation: string): OmpErrors {
|
|
85
|
+
return new OmpErrors(this.#path, expectation, this.#data);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
reject(expectation: string): OmpErrors {
|
|
89
|
+
return this.error(expectation);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
46
93
|
class Builder {
|
|
47
94
|
#lines: string[] = [];
|
|
48
95
|
#refs: unknown[] = [];
|
|
96
|
+
#activeAliases: Set<IR> | undefined;
|
|
49
97
|
#id = 0;
|
|
50
98
|
|
|
51
99
|
next(prefix: string): string {
|
|
@@ -72,8 +120,22 @@ class Builder {
|
|
|
72
120
|
return `[${parts.join(",")}]`;
|
|
73
121
|
}
|
|
74
122
|
|
|
123
|
+
storedPathExpr(segs: PathSeg[]): string {
|
|
124
|
+
if (segs.length === 0) return "undefined";
|
|
125
|
+
if (segs.length === 1) {
|
|
126
|
+
const seg = segs[0];
|
|
127
|
+
return "s" in seg ? JSON.stringify(seg.s) : seg.d;
|
|
128
|
+
}
|
|
129
|
+
const staticParts: PropertyKey[] = [];
|
|
130
|
+
for (const seg of segs) {
|
|
131
|
+
if ("d" in seg) return this.pathExpr(segs);
|
|
132
|
+
staticParts.push(seg.s);
|
|
133
|
+
}
|
|
134
|
+
return this.ref(staticParts);
|
|
135
|
+
}
|
|
136
|
+
|
|
75
137
|
fail(segs: PathSeg[], expected: string, dataExpr: string): string {
|
|
76
|
-
return `return
|
|
138
|
+
return `return new AE(${this.storedPathExpr(segs)},${JSON.stringify(expected)},${dataExpr})`;
|
|
77
139
|
}
|
|
78
140
|
|
|
79
141
|
/** Pure boolean predicate for a morph-free subtree. */
|
|
@@ -89,10 +151,18 @@ class Builder {
|
|
|
89
151
|
return `typeof ${v}==="boolean"`;
|
|
90
152
|
case "bigint":
|
|
91
153
|
return `typeof ${v}==="bigint"`;
|
|
154
|
+
case "symbol":
|
|
155
|
+
return `typeof ${v}==="symbol"`;
|
|
156
|
+
case "never":
|
|
157
|
+
return "false";
|
|
92
158
|
case "anyobject":
|
|
93
159
|
return `(typeof ${v}==="object"&&${v}!==null)`;
|
|
94
160
|
case "lit":
|
|
95
|
-
return
|
|
161
|
+
return node.v instanceof Date
|
|
162
|
+
? `(${v} instanceof Date&&${v}.valueOf()===${node.v.valueOf()})`
|
|
163
|
+
: `${v}===${this.lit(node.v)}`;
|
|
164
|
+
case "instance":
|
|
165
|
+
return `${v} instanceof ${this.ref(node.ctor)}`;
|
|
96
166
|
case "string": {
|
|
97
167
|
let out = `typeof ${v}==="string"`;
|
|
98
168
|
if (node.min !== undefined) out += `&&${v}.length>=${node.min}`;
|
|
@@ -101,29 +171,34 @@ class Builder {
|
|
|
101
171
|
return out;
|
|
102
172
|
}
|
|
103
173
|
case "number": {
|
|
104
|
-
let out = `
|
|
105
|
-
if (node.
|
|
174
|
+
let out = node.int ? `Number.isInteger(${v})` : `Number.isFinite(${v})`;
|
|
175
|
+
if (node.divisor !== undefined) out += `&&${v}%${node.divisor}===0`;
|
|
106
176
|
if (node.min !== undefined) out += `&&${v}${node.xmin ? ">" : ">="}${node.min}`;
|
|
107
177
|
if (node.max !== undefined) out += `&&${v}${node.xmax ? "<" : "<="}${node.max}`;
|
|
108
178
|
return out;
|
|
109
179
|
}
|
|
110
180
|
case "union": {
|
|
111
|
-
const lits = node.members.filter(
|
|
112
|
-
if (lits.length >
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
181
|
+
const lits = node.members.filter(isPrimitiveLiteral);
|
|
182
|
+
if (lits.length > 8) {
|
|
183
|
+
const values = this.ref(new Set(lits.map(member => member.v)));
|
|
184
|
+
const literalNodes = new Set<IR>(lits);
|
|
185
|
+
const rest = node.members.filter(member => !literalNodes.has(member));
|
|
186
|
+
let out = `${values}.has(${v})`;
|
|
116
187
|
for (const m of rest) out += `||(${this.predicate(m, v)})`;
|
|
117
188
|
return `(${out})`;
|
|
118
189
|
}
|
|
119
190
|
return `(${node.members.map(m => `(${this.predicate(m, v)})`).join("||")})`;
|
|
120
191
|
}
|
|
192
|
+
case "intersection":
|
|
193
|
+
return `(${node.members.map(member => `(${this.predicate(member, v)})`).join("&&")})`;
|
|
121
194
|
case "array": {
|
|
122
|
-
const
|
|
195
|
+
const array = this.next("a");
|
|
196
|
+
const index = this.next("i");
|
|
123
197
|
let out = `Array.isArray(${v})`;
|
|
124
198
|
if (node.min !== undefined) out += `&&${v}.length>=${node.min}`;
|
|
125
199
|
if (node.max !== undefined) out += `&&${v}.length<=${node.max}`;
|
|
126
|
-
|
|
200
|
+
const item = `${array}[${index}]`;
|
|
201
|
+
out += `&&((${array})=>{for(let ${index}=0;${index}<${array}.length;${index}++)if(!(${this.predicate(node.el, item)}))return false;return true})(${v})`;
|
|
127
202
|
return out;
|
|
128
203
|
}
|
|
129
204
|
case "object": {
|
|
@@ -131,10 +206,15 @@ class Builder {
|
|
|
131
206
|
for (const p of node.props) {
|
|
132
207
|
const av = access(v, p.key);
|
|
133
208
|
const present = `${JSON.stringify(p.key)} in ${v}`;
|
|
209
|
+
const predicate = this.predicate(p.val, av);
|
|
134
210
|
checks.push(
|
|
135
211
|
p.opt || p.hasDefault
|
|
136
|
-
?
|
|
137
|
-
|
|
212
|
+
? rejectsUndefined(p.val)
|
|
213
|
+
? `((${av}!==undefined&&(${predicate}))||!(${present}))`
|
|
214
|
+
: `(!(${present})||(${predicate}))`
|
|
215
|
+
: rejectsUndefined(p.val)
|
|
216
|
+
? predicate
|
|
217
|
+
: `((${present})&&(${predicate}))`,
|
|
138
218
|
);
|
|
139
219
|
}
|
|
140
220
|
if (node.index) {
|
|
@@ -152,6 +232,8 @@ class Builder {
|
|
|
152
232
|
}
|
|
153
233
|
case "sub":
|
|
154
234
|
return `!(${this.ref(node.schema.run)}(${v}) instanceof AE)`;
|
|
235
|
+
default:
|
|
236
|
+
return `!(${this.ref(boundWalk(node))}(${v}) instanceof AE)`;
|
|
155
237
|
}
|
|
156
238
|
}
|
|
157
239
|
|
|
@@ -164,8 +246,53 @@ class Builder {
|
|
|
164
246
|
return `(${props.map(p => `${keyVar}===${JSON.stringify(p.key)}`).join("||")})`;
|
|
165
247
|
}
|
|
166
248
|
|
|
249
|
+
emitDelegate(node: IR, v: string, segs: PathSeg[], out?: string): void {
|
|
250
|
+
const runner = node.k === "sub" ? node.schema.run : boundWalk(node);
|
|
251
|
+
const result = this.next("r");
|
|
252
|
+
this.push(`const ${result}=${this.ref(runner)}(${v});`);
|
|
253
|
+
this.push(
|
|
254
|
+
segs.length === 0
|
|
255
|
+
? `if(${result} instanceof AE)return ${result};`
|
|
256
|
+
: `if(${result} instanceof AE)return PF(${result},${this.pathExpr(segs)});`,
|
|
257
|
+
);
|
|
258
|
+
if (out !== undefined) this.push(`${out}=${result};`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
emitTupleShape(
|
|
262
|
+
node: TupleIR,
|
|
263
|
+
v: string,
|
|
264
|
+
segs: PathSeg[],
|
|
265
|
+
failureData: string,
|
|
266
|
+
): { postfixStart: string; prefixCount: string; requiredPrefix: number } {
|
|
267
|
+
this.push(`if(!Array.isArray(${v}))${this.fail(segs, "an array", failureData)};`);
|
|
268
|
+
const requiredPrefix = node.prefix.filter(item => !item.opt && !item.hasDefault).length;
|
|
269
|
+
const minimum = requiredPrefix + node.postfix.length;
|
|
270
|
+
if (minimum > 0) {
|
|
271
|
+
this.push(
|
|
272
|
+
`if(${v}.length<${minimum})${this.fail(segs, `an array of at least length ${minimum}`, failureData)};`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (node.variadic === undefined) {
|
|
276
|
+
const maximum = node.prefix.length + node.postfix.length;
|
|
277
|
+
this.push(
|
|
278
|
+
`if(${v}.length>${maximum})${this.fail(segs, `an array of at most length ${maximum}`, failureData)};`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
let postfixStart = `${v}.length`;
|
|
282
|
+
if (node.postfix.length > 0) {
|
|
283
|
+
postfixStart = this.next("p");
|
|
284
|
+
this.push(`const ${postfixStart}=${v}.length-${node.postfix.length};`);
|
|
285
|
+
}
|
|
286
|
+
let prefixCount = String(node.prefix.length);
|
|
287
|
+
if (requiredPrefix !== node.prefix.length) {
|
|
288
|
+
prefixCount = this.next("n");
|
|
289
|
+
this.push(`const ${prefixCount}=Math.min(${node.prefix.length},${postfixStart});`);
|
|
290
|
+
}
|
|
291
|
+
return { postfixStart, prefixCount, requiredPrefix };
|
|
292
|
+
}
|
|
293
|
+
|
|
167
294
|
/** Statement-form check for a morph-free subtree with precise error paths. */
|
|
168
|
-
emitCheck(node: IR, v: string, segs: PathSeg[]): void {
|
|
295
|
+
emitCheck(node: IR, v: string, segs: PathSeg[], failureData = v): void {
|
|
169
296
|
switch (node.k) {
|
|
170
297
|
case "unknown":
|
|
171
298
|
return;
|
|
@@ -173,7 +300,7 @@ class Builder {
|
|
|
173
300
|
let head = `Array.isArray(${v})`;
|
|
174
301
|
if (node.min !== undefined) head += `&&${v}.length>=${node.min}`;
|
|
175
302
|
if (node.max !== undefined) head += `&&${v}.length<=${node.max}`;
|
|
176
|
-
this.push(`if(!(${head}))${this.fail(segs, expectedOf(node),
|
|
303
|
+
this.push(`if(!(${head}))${this.fail(segs, expectedOf(node), failureData)};`);
|
|
177
304
|
const i = this.next("i");
|
|
178
305
|
const x = this.next("x");
|
|
179
306
|
this.push(`for(let ${i}=0;${i}<${v}.length;${i}++){const ${x}=${v}[${i}];`);
|
|
@@ -181,9 +308,28 @@ class Builder {
|
|
|
181
308
|
this.push("}");
|
|
182
309
|
return;
|
|
183
310
|
}
|
|
311
|
+
case "tuple": {
|
|
312
|
+
const { postfixStart, prefixCount, requiredPrefix } = this.emitTupleShape(node, v, segs, failureData);
|
|
313
|
+
for (let index = 0; index < node.prefix.length; index++) {
|
|
314
|
+
if (index >= requiredPrefix) this.push(`if(${index}<${prefixCount}){`);
|
|
315
|
+
this.emitCheck(node.prefix[index].val, `${v}[${index}]`, [...segs, { d: String(index) }]);
|
|
316
|
+
if (index >= requiredPrefix) this.push("}");
|
|
317
|
+
}
|
|
318
|
+
if (node.variadic !== undefined) {
|
|
319
|
+
const index = this.next("i");
|
|
320
|
+
this.push(`for(let ${index}=${prefixCount};${index}<${postfixStart};${index}++){`);
|
|
321
|
+
this.emitCheck(node.variadic, `${v}[${index}]`, [...segs, { d: index }]);
|
|
322
|
+
this.push("}");
|
|
323
|
+
}
|
|
324
|
+
for (let index = 0; index < node.postfix.length; index++) {
|
|
325
|
+
const inputIndex = index === 0 ? postfixStart : `${postfixStart}+${index}`;
|
|
326
|
+
this.emitCheck(node.postfix[index], `${v}[${inputIndex}]`, [...segs, { d: inputIndex }]);
|
|
327
|
+
}
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
184
330
|
case "object": {
|
|
185
331
|
this.push(
|
|
186
|
-
`if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object",
|
|
332
|
+
`if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object", failureData)};`,
|
|
187
333
|
);
|
|
188
334
|
for (const p of node.props) {
|
|
189
335
|
const present = `${JSON.stringify(p.key)} in ${v}`;
|
|
@@ -214,17 +360,46 @@ class Builder {
|
|
|
214
360
|
}
|
|
215
361
|
return;
|
|
216
362
|
}
|
|
217
|
-
case "sub":
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
363
|
+
case "sub":
|
|
364
|
+
this.emitDelegate(node, v, segs);
|
|
365
|
+
return;
|
|
366
|
+
case "union": {
|
|
367
|
+
const failure = node.members.some(canRefineUnionFailure)
|
|
368
|
+
? `return UF(${this.ref(node)},${failureData},${this.pathExpr(segs)},${JSON.stringify(expectedOf(node))})`
|
|
369
|
+
: this.fail(segs, expectedOf(node), failureData);
|
|
370
|
+
const literals = node.members.filter(isPrimitiveLiteral);
|
|
371
|
+
if (literals.length === node.members.length && literals.length >= 4) {
|
|
372
|
+
const cases = literals.map(member => `case ${this.lit(member.v)}:`).join("");
|
|
373
|
+
this.push(`switch(${v}){${cases}break;default:${failure};}`);
|
|
374
|
+
} else {
|
|
375
|
+
this.push(`if(!(${this.predicate(node, v)}))${failure};`);
|
|
376
|
+
}
|
|
221
377
|
return;
|
|
222
378
|
}
|
|
223
|
-
case "
|
|
224
|
-
|
|
379
|
+
case "null":
|
|
380
|
+
case "undefined":
|
|
381
|
+
case "boolean":
|
|
382
|
+
case "bigint":
|
|
383
|
+
case "symbol":
|
|
384
|
+
case "never":
|
|
385
|
+
case "anyobject":
|
|
386
|
+
case "lit":
|
|
387
|
+
case "string":
|
|
388
|
+
case "number":
|
|
389
|
+
case "instance":
|
|
390
|
+
this.push(`if(!(${this.predicate(node, v)}))${this.fail(segs, expectedOf(node), failureData)};`);
|
|
391
|
+
return;
|
|
392
|
+
case "refine": {
|
|
393
|
+
this.emitCheck(node.base, v, segs, failureData);
|
|
394
|
+
const failure = this.fail(segs, node.expected, v);
|
|
395
|
+
this.push(`try{if(!${this.ref(node.pred)}(${v}))${failure};}catch{${failure};}`);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
case "intersection":
|
|
399
|
+
for (const member of node.members) this.emitCheck(member, v, segs, failureData);
|
|
225
400
|
return;
|
|
226
401
|
default:
|
|
227
|
-
this.
|
|
402
|
+
this.emitDelegate(node, v, segs);
|
|
228
403
|
}
|
|
229
404
|
}
|
|
230
405
|
|
|
@@ -232,18 +407,45 @@ class Builder {
|
|
|
232
407
|
* Validate `v` against a morphing subtree and assign the produced output
|
|
233
408
|
* to `out` (an already-declared `let`).
|
|
234
409
|
*/
|
|
235
|
-
emitProduce(node: IR, v: string, segs: PathSeg[], out: string): void {
|
|
410
|
+
emitProduce(node: IR, v: string, segs: PathSeg[], out: string, failureData = v): void {
|
|
236
411
|
if (!hasMorph(node)) {
|
|
237
|
-
this.emitCheck(node, v, segs);
|
|
412
|
+
this.emitCheck(node, v, segs, failureData);
|
|
238
413
|
this.push(`${out}=${v};`);
|
|
239
414
|
return;
|
|
240
415
|
}
|
|
241
416
|
switch (node.k) {
|
|
242
|
-
case "sub":
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
this.
|
|
417
|
+
case "sub":
|
|
418
|
+
this.emitDelegate(node, v, segs, out);
|
|
419
|
+
return;
|
|
420
|
+
case "morph": {
|
|
421
|
+
const input = this.next("t");
|
|
422
|
+
this.push(`let ${input};`);
|
|
423
|
+
this.emitProduce(node.input, v, segs, input, failureData);
|
|
424
|
+
const context = this.next("c");
|
|
425
|
+
const result = this.next("r");
|
|
426
|
+
this.push(`const ${context}=new MC(${this.storedPathExpr(segs)},${input});`);
|
|
427
|
+
this.push(`const ${result}=${this.ref(node.fn)}(${input},${context});`);
|
|
428
|
+
this.push(`if(${result} instanceof AE)return ${result};`);
|
|
429
|
+
if (node.out === undefined) this.push(`${out}=${result};`);
|
|
430
|
+
else this.emitProduce(node.out, result, segs, out);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
case "alias": {
|
|
434
|
+
let active = this.#activeAliases;
|
|
435
|
+
if (active === undefined) {
|
|
436
|
+
active = new Set();
|
|
437
|
+
this.#activeAliases = active;
|
|
438
|
+
}
|
|
439
|
+
if (active.has(node)) {
|
|
440
|
+
this.emitDelegate(node, v, segs, out);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
active.add(node);
|
|
444
|
+
try {
|
|
445
|
+
this.emitProduce(node.resolve(), v, segs, out, failureData);
|
|
446
|
+
} finally {
|
|
447
|
+
active.delete(node);
|
|
448
|
+
}
|
|
247
449
|
return;
|
|
248
450
|
}
|
|
249
451
|
case "union": {
|
|
@@ -258,21 +460,23 @@ class Builder {
|
|
|
258
460
|
}
|
|
259
461
|
for (const m of node.members) {
|
|
260
462
|
if (m.k === "sub" || hasMorph(m)) {
|
|
261
|
-
const runner = m.k === "sub" ? m.schema.run : boundWalk(m);
|
|
463
|
+
const runner = m.k === "sub" ? m.schema.run : m.k === "alias" ? boundWalk(m) : compile(m);
|
|
262
464
|
const r = this.next("r");
|
|
263
465
|
this.push(`const ${r}=${this.ref(runner)}(${v});`);
|
|
264
466
|
this.push(`if(!(${r} instanceof AE)){${out}=${r};${ok}=true;break ${label};}`);
|
|
265
467
|
}
|
|
266
468
|
}
|
|
267
469
|
this.push("}");
|
|
268
|
-
this.push(
|
|
470
|
+
this.push(
|
|
471
|
+
`if(!${ok})return UF(${this.ref(node)},${failureData},${this.pathExpr(segs)},${JSON.stringify(expectedOf(node))});`,
|
|
472
|
+
);
|
|
269
473
|
return;
|
|
270
474
|
}
|
|
271
475
|
case "array": {
|
|
272
476
|
let head = `Array.isArray(${v})`;
|
|
273
477
|
if (node.min !== undefined) head += `&&${v}.length>=${node.min}`;
|
|
274
478
|
if (node.max !== undefined) head += `&&${v}.length<=${node.max}`;
|
|
275
|
-
this.push(`if(!(${head}))${this.fail(segs, expectedOf(node),
|
|
479
|
+
this.push(`if(!(${head}))${this.fail(segs, expectedOf(node), failureData)};`);
|
|
276
480
|
const arr = this.next("a");
|
|
277
481
|
const i = this.next("i");
|
|
278
482
|
const x = this.next("x");
|
|
@@ -284,12 +488,63 @@ class Builder {
|
|
|
284
488
|
this.push(`${out}=${arr};`);
|
|
285
489
|
return;
|
|
286
490
|
}
|
|
491
|
+
case "tuple": {
|
|
492
|
+
const { postfixStart, prefixCount, requiredPrefix } = this.emitTupleShape(node, v, segs, failureData);
|
|
493
|
+
const tuple = this.next("a");
|
|
494
|
+
this.push(`const ${tuple}=[...${v}];`);
|
|
495
|
+
for (let index = 0; index < node.prefix.length; index++) {
|
|
496
|
+
const item = node.prefix[index];
|
|
497
|
+
const input = `${v}[${index}]`;
|
|
498
|
+
const output = `${tuple}[${index}]`;
|
|
499
|
+
if (index >= requiredPrefix) this.push(`if(${index}<${prefixCount}){`);
|
|
500
|
+
if (hasMorph(item.val)) this.emitProduce(item.val, input, [...segs, { d: String(index) }], output);
|
|
501
|
+
else {
|
|
502
|
+
this.emitCheck(item.val, input, [...segs, { d: String(index) }]);
|
|
503
|
+
this.push(`${output}=${input};`);
|
|
504
|
+
}
|
|
505
|
+
if (index >= requiredPrefix) {
|
|
506
|
+
if (item.hasDefault) {
|
|
507
|
+
const defaultValue = item.defFactory ? `${this.ref(item.def)}()` : this.lit(item.def);
|
|
508
|
+
this.push(`}else{${output}=${defaultValue};}`);
|
|
509
|
+
} else {
|
|
510
|
+
this.push("}");
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (node.variadic !== undefined) {
|
|
515
|
+
const index = this.next("i");
|
|
516
|
+
const input = this.next("x");
|
|
517
|
+
this.push(
|
|
518
|
+
`for(let ${index}=${prefixCount};${index}<${postfixStart};${index}++){const ${input}=${v}[${index}];`,
|
|
519
|
+
);
|
|
520
|
+
if (hasMorph(node.variadic)) {
|
|
521
|
+
this.emitProduce(node.variadic, input, [...segs, { d: index }], `${tuple}[${index}]`);
|
|
522
|
+
} else {
|
|
523
|
+
this.emitCheck(node.variadic, input, [...segs, { d: index }]);
|
|
524
|
+
this.push(`${tuple}[${index}]=${input};`);
|
|
525
|
+
}
|
|
526
|
+
this.push("}");
|
|
527
|
+
}
|
|
528
|
+
for (let index = 0; index < node.postfix.length; index++) {
|
|
529
|
+
const inputIndex = index === 0 ? postfixStart : `${postfixStart}+${index}`;
|
|
530
|
+
const input = `${v}[${inputIndex}]`;
|
|
531
|
+
const output = `${tuple}[${inputIndex}]`;
|
|
532
|
+
const item = node.postfix[index];
|
|
533
|
+
if (hasMorph(item)) this.emitProduce(item, input, [...segs, { d: inputIndex }], output);
|
|
534
|
+
else {
|
|
535
|
+
this.emitCheck(item, input, [...segs, { d: inputIndex }]);
|
|
536
|
+
this.push(`${output}=${input};`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
this.push(`${out}=${tuple};`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
287
542
|
case "object": {
|
|
288
543
|
this.push(
|
|
289
|
-
`if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object",
|
|
544
|
+
`if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object", failureData)};`,
|
|
290
545
|
);
|
|
291
546
|
const o = this.next("o");
|
|
292
|
-
const fresh = node.extras
|
|
547
|
+
const fresh = node.extras !== "keep" && !node.index;
|
|
293
548
|
this.push(fresh ? `const ${o}={};` : `const ${o}={...${v}};`);
|
|
294
549
|
for (const p of node.props) {
|
|
295
550
|
const present = `${JSON.stringify(p.key)} in ${v}`;
|
|
@@ -303,8 +558,6 @@ class Builder {
|
|
|
303
558
|
missing.push(`${ao}=${dflt};`);
|
|
304
559
|
} else if (!p.opt) {
|
|
305
560
|
missing.push(`${this.fail(propSegs, expectedOf(p.val), "M")};`);
|
|
306
|
-
} else if (fresh) {
|
|
307
|
-
// optional + absent under "delete": nothing to copy
|
|
308
561
|
}
|
|
309
562
|
this.push(`if(!(${present})){${missing.join("")}}else{`);
|
|
310
563
|
if (morphChild) {
|
|
@@ -343,10 +596,27 @@ class Builder {
|
|
|
343
596
|
this.push(`${out}=${o};`);
|
|
344
597
|
return;
|
|
345
598
|
}
|
|
599
|
+
case "refine": {
|
|
600
|
+
const refined = this.next("t");
|
|
601
|
+
this.push(`let ${refined};`);
|
|
602
|
+
this.emitProduce(node.base, v, segs, refined, failureData);
|
|
603
|
+
const failure = this.fail(segs, node.expected, refined);
|
|
604
|
+
this.push(`try{if(!${this.ref(node.pred)}(${refined}))${failure};}catch{${failure};}`);
|
|
605
|
+
this.push(`${out}=${refined};`);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
case "intersection": {
|
|
609
|
+
const current = this.next("t");
|
|
610
|
+
this.push(`let ${current}=${v};`);
|
|
611
|
+
for (const member of node.members) {
|
|
612
|
+
if (hasMorph(member)) this.emitProduce(member, current, segs, current);
|
|
613
|
+
else this.emitCheck(member, current, segs);
|
|
614
|
+
}
|
|
615
|
+
this.push(`${out}=${current};`);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
346
618
|
default:
|
|
347
|
-
|
|
348
|
-
this.emitCheck(node, v, segs);
|
|
349
|
-
this.push(`${out}=${v};`);
|
|
619
|
+
this.emitDelegate(node, v, segs, out);
|
|
350
620
|
}
|
|
351
621
|
}
|
|
352
622
|
|
|
@@ -362,16 +632,99 @@ class Builder {
|
|
|
362
632
|
ret = "v";
|
|
363
633
|
}
|
|
364
634
|
const src = `return function(v){${this.#lines.join("")}return ${ret}}`;
|
|
365
|
-
const make = new Function("R", "
|
|
635
|
+
const make = new Function("R", "AE", "M", "PF", "UF", "MC", "own", src) as (
|
|
366
636
|
refs: unknown[],
|
|
367
|
-
e: typeof OmpErrors.single,
|
|
368
637
|
ae: typeof OmpErrors,
|
|
369
638
|
m: typeof MISSING,
|
|
370
639
|
pf: typeof prefixErrors,
|
|
371
640
|
uf: typeof unionFail,
|
|
641
|
+
mc: typeof CompiledMorphContext,
|
|
372
642
|
ownFn: typeof own,
|
|
373
643
|
) => (value: unknown) => unknown;
|
|
374
|
-
return make(this.#refs, OmpErrors
|
|
644
|
+
return make(this.#refs, OmpErrors, MISSING, prefixErrors, unionFail, CompiledMorphContext, own);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
emitAllows(node: IR, v: string): void {
|
|
648
|
+
switch (node.k) {
|
|
649
|
+
case "array": {
|
|
650
|
+
const array = this.next("a");
|
|
651
|
+
const index = this.next("i");
|
|
652
|
+
this.push(`const ${array}=${v};if(!Array.isArray(${array}))return false;`);
|
|
653
|
+
if (node.min !== undefined) this.push(`if(${array}.length<${node.min})return false;`);
|
|
654
|
+
if (node.max !== undefined) this.push(`if(${array}.length>${node.max})return false;`);
|
|
655
|
+
this.push(`for(let ${index}=0;${index}<${array}.length;${index}++){`);
|
|
656
|
+
this.emitAllows(node.el, `${array}[${index}]`);
|
|
657
|
+
this.push("}");
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
case "object": {
|
|
661
|
+
const object = this.next("o");
|
|
662
|
+
this.push(
|
|
663
|
+
`const ${object}=${v};if(typeof ${object}!=="object"||${object}===null||Array.isArray(${object}))return false;`,
|
|
664
|
+
);
|
|
665
|
+
for (const prop of node.props) {
|
|
666
|
+
const value = this.next("p");
|
|
667
|
+
const present = `${JSON.stringify(prop.key)} in ${object}`;
|
|
668
|
+
this.push(`const ${value}=${access(object, prop.key)};`);
|
|
669
|
+
if (prop.opt || prop.hasDefault) {
|
|
670
|
+
if (rejectsUndefined(prop.val)) {
|
|
671
|
+
this.push(`if(${value}!==undefined){`);
|
|
672
|
+
this.emitAllows(prop.val, value);
|
|
673
|
+
this.push(`}else if(${present})return false;`);
|
|
674
|
+
} else {
|
|
675
|
+
this.push(`if(${present}){`);
|
|
676
|
+
this.emitAllows(prop.val, value);
|
|
677
|
+
this.push("}");
|
|
678
|
+
}
|
|
679
|
+
} else {
|
|
680
|
+
if (!rejectsUndefined(prop.val)) this.push(`if(!(${present}))return false;`);
|
|
681
|
+
this.emitAllows(prop.val, value);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (node.index) {
|
|
685
|
+
const key = this.next("k");
|
|
686
|
+
this.push(`for(const ${key} in ${object}){if(!own.call(${object},${key}))continue;`);
|
|
687
|
+
this.emitAllows(node.index, `${object}[${key}]`);
|
|
688
|
+
this.push("}");
|
|
689
|
+
} else if (node.extras === "reject") {
|
|
690
|
+
const key = this.next("k");
|
|
691
|
+
this.push(
|
|
692
|
+
`for(const ${key} in ${object})if(own.call(${object},${key})&&!(${this.declaredCheck(node.props, key)}))return false;`,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
case "union": {
|
|
698
|
+
const sources: string[] = [];
|
|
699
|
+
for (const member of node.members) {
|
|
700
|
+
if (!isPrimitiveLiteral(member)) break;
|
|
701
|
+
const source = litSource(member.v);
|
|
702
|
+
if (source === undefined) break;
|
|
703
|
+
sources.push(source);
|
|
704
|
+
}
|
|
705
|
+
if (sources.length === node.members.length && sources.length >= 4) {
|
|
706
|
+
this.push(
|
|
707
|
+
`switch(${v}){${sources.map(source => `case ${source}:`).join("")}break;default:return false;}`,
|
|
708
|
+
);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
this.push(`if(!(${this.predicate(node, v)}))return false;`);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
default:
|
|
715
|
+
this.push(`if(!(${this.predicate(node, v)}))return false;`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
buildAllows(ir: IR): (value: unknown) => boolean {
|
|
720
|
+
this.emitAllows(ir, "v");
|
|
721
|
+
const src = `return function(v){${this.#lines.join("")}return true}`;
|
|
722
|
+
const make = new Function("R", "AE", "own", src) as (
|
|
723
|
+
refs: unknown[],
|
|
724
|
+
ae: typeof OmpErrors,
|
|
725
|
+
ownFn: typeof own,
|
|
726
|
+
) => (value: unknown) => boolean;
|
|
727
|
+
return make(this.#refs, OmpErrors, own);
|
|
375
728
|
}
|
|
376
729
|
}
|
|
377
730
|
|
|
@@ -386,7 +739,7 @@ interface WalkTagged {
|
|
|
386
739
|
[kWalk]?: (value: unknown) => unknown;
|
|
387
740
|
}
|
|
388
741
|
|
|
389
|
-
/**
|
|
742
|
+
/** Cached interpreter closure for recursive aliases and predicate-only fallbacks. */
|
|
390
743
|
function boundWalk(node: IR): (value: unknown) => unknown {
|
|
391
744
|
const tagged = node as IR & WalkTagged;
|
|
392
745
|
let fn = tagged[kWalk];
|
|
@@ -397,19 +750,44 @@ function boundWalk(node: IR): (value: unknown) => unknown {
|
|
|
397
750
|
return fn;
|
|
398
751
|
}
|
|
399
752
|
|
|
753
|
+
function resolvedRoot(ir: IR): IR {
|
|
754
|
+
return ir.k === "alias" ? ir.resolve() : ir;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const compiledCache = new WeakMap<IR, (value: unknown) => unknown>();
|
|
758
|
+
const allowsCache = new WeakMap<IR, (value: unknown) => boolean>();
|
|
759
|
+
|
|
400
760
|
/** Compile `ir` into a specialized validator. */
|
|
401
761
|
export function compile(ir: IR): (value: unknown) => unknown {
|
|
402
|
-
|
|
762
|
+
const root = resolvedRoot(ir);
|
|
763
|
+
let validator = compiledCache.get(root);
|
|
764
|
+
if (validator === undefined) {
|
|
765
|
+
validator = new Builder().build(root);
|
|
766
|
+
compiledCache.set(root, validator);
|
|
767
|
+
}
|
|
768
|
+
return validator;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** Compile `ir` into an allocation-free boolean validator. */
|
|
772
|
+
export function compileAllows(ir: IR): (value: unknown) => boolean {
|
|
773
|
+
const root = resolvedRoot(ir);
|
|
774
|
+
let validator = allowsCache.get(root);
|
|
775
|
+
if (validator === undefined) {
|
|
776
|
+
validator = new Builder().buildAllows(root);
|
|
777
|
+
allowsCache.set(root, validator);
|
|
778
|
+
}
|
|
779
|
+
return validator;
|
|
403
780
|
}
|
|
404
781
|
|
|
405
782
|
/** Generated source for inspection/debugging. */
|
|
406
783
|
export function compileToSource(ir: IR): string {
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
784
|
+
const root = resolvedRoot(ir);
|
|
785
|
+
const builder = new Builder();
|
|
786
|
+
if (hasMorph(root)) {
|
|
787
|
+
builder.push("let o;");
|
|
788
|
+
builder.emitProduce(root, "v", [], "o");
|
|
411
789
|
return `function(v){/* refs elided */return o}`;
|
|
412
790
|
}
|
|
413
|
-
|
|
791
|
+
builder.emitCheck(root, "v", []);
|
|
414
792
|
return `function(v){/* refs elided */return v}`;
|
|
415
793
|
}
|