@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
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JIT compiler: lowers schema IR into a specialized validator via
|
|
3
|
+
* `new Function`. Invoked by `type.ts` after a schema's third call; the
|
|
4
|
+
* interpreter (`interp.ts`) covers earlier calls so rarely-used schemas never
|
|
5
|
+
* pay codegen cost.
|
|
6
|
+
*
|
|
7
|
+
* Generated code philosophy:
|
|
8
|
+
* - success path is straight-line monomorphic JS with zero allocation; when
|
|
9
|
+
* the schema has no morphs the input value itself is returned
|
|
10
|
+
* - failure allocates a single `OmpErrors` (`E(path, expected, data)`); path
|
|
11
|
+
* arrays and messages are inline literals, so cost is one small allocation
|
|
12
|
+
* - morphing nodes (defaults, `"+": "delete"`, embedded stepped schemas)
|
|
13
|
+
* produce a fresh output object; pure subtrees below them stay check-only
|
|
14
|
+
* - morphing union members use separately compiled or hoisted runners; pure
|
|
15
|
+
* members compile to inline predicates
|
|
16
|
+
*/
|
|
17
|
+
import { MISSING, OmpErrors } from "./errors.js";
|
|
18
|
+
import { canRefineUnionFailure, unionFail, walk } from "./interp.js";
|
|
19
|
+
import { expectedOf, hasMorph } from "./ir.js";
|
|
20
|
+
const own = Object.prototype.hasOwnProperty;
|
|
21
|
+
const IDENT = /^[A-Za-z_$][\w$]*$/;
|
|
22
|
+
function access(base, key) {
|
|
23
|
+
return IDENT.test(key) ? `${base}.${key}` : `${base}[${JSON.stringify(key)}]`;
|
|
24
|
+
}
|
|
25
|
+
/** Inline-able literal, else undefined (caller hoists into the refs pool). */
|
|
26
|
+
function litSource(v) {
|
|
27
|
+
if (v === null)
|
|
28
|
+
return "null";
|
|
29
|
+
if (v === undefined)
|
|
30
|
+
return "undefined";
|
|
31
|
+
switch (typeof v) {
|
|
32
|
+
case "string":
|
|
33
|
+
case "boolean":
|
|
34
|
+
return JSON.stringify(v);
|
|
35
|
+
case "number":
|
|
36
|
+
return Number.isFinite(v) ? String(v) : undefined;
|
|
37
|
+
default:
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isPrimitiveLiteral(node) {
|
|
42
|
+
return node.k === "lit" && (node.v === null || (typeof node.v !== "object" && typeof node.v !== "function"));
|
|
43
|
+
}
|
|
44
|
+
/** Whether `undefined` necessarily fails, allowing property-presence checks to be elided. */
|
|
45
|
+
function rejectsUndefined(node) {
|
|
46
|
+
switch (node.k) {
|
|
47
|
+
case "unknown":
|
|
48
|
+
case "undefined":
|
|
49
|
+
case "alias":
|
|
50
|
+
case "sub":
|
|
51
|
+
return false;
|
|
52
|
+
case "lit":
|
|
53
|
+
return node.v !== undefined;
|
|
54
|
+
case "union":
|
|
55
|
+
return node.members.every(rejectsUndefined);
|
|
56
|
+
case "intersection":
|
|
57
|
+
return node.members.some(rejectsUndefined);
|
|
58
|
+
case "refine":
|
|
59
|
+
return rejectsUndefined(node.base);
|
|
60
|
+
case "morph":
|
|
61
|
+
return rejectsUndefined(node.input);
|
|
62
|
+
default:
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
class CompiledMorphContext {
|
|
67
|
+
#path;
|
|
68
|
+
#data;
|
|
69
|
+
constructor(path, data) {
|
|
70
|
+
this.#path = path;
|
|
71
|
+
this.#data = data;
|
|
72
|
+
}
|
|
73
|
+
error(expectation) {
|
|
74
|
+
return new OmpErrors(this.#path, expectation, this.#data);
|
|
75
|
+
}
|
|
76
|
+
reject(expectation) {
|
|
77
|
+
return this.error(expectation);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
class Builder {
|
|
81
|
+
#lines = [];
|
|
82
|
+
#refs = [];
|
|
83
|
+
#activeAliases;
|
|
84
|
+
#id = 0;
|
|
85
|
+
next(prefix) {
|
|
86
|
+
return `${prefix}${this.#id++}`;
|
|
87
|
+
}
|
|
88
|
+
push(line) {
|
|
89
|
+
this.#lines.push(line);
|
|
90
|
+
}
|
|
91
|
+
ref(value) {
|
|
92
|
+
const idx = this.#refs.indexOf(value);
|
|
93
|
+
if (idx >= 0)
|
|
94
|
+
return `R[${idx}]`;
|
|
95
|
+
this.#refs.push(value);
|
|
96
|
+
return `R[${this.#refs.length - 1}]`;
|
|
97
|
+
}
|
|
98
|
+
lit(v) {
|
|
99
|
+
return litSource(v) ?? this.ref(v);
|
|
100
|
+
}
|
|
101
|
+
pathExpr(segs) {
|
|
102
|
+
const parts = segs.map(seg => ("s" in seg ? JSON.stringify(seg.s) : seg.d));
|
|
103
|
+
return `[${parts.join(",")}]`;
|
|
104
|
+
}
|
|
105
|
+
storedPathExpr(segs) {
|
|
106
|
+
if (segs.length === 0)
|
|
107
|
+
return "undefined";
|
|
108
|
+
if (segs.length === 1) {
|
|
109
|
+
const seg = segs[0];
|
|
110
|
+
return "s" in seg ? JSON.stringify(seg.s) : seg.d;
|
|
111
|
+
}
|
|
112
|
+
const staticParts = [];
|
|
113
|
+
for (const seg of segs) {
|
|
114
|
+
if ("d" in seg)
|
|
115
|
+
return this.pathExpr(segs);
|
|
116
|
+
staticParts.push(seg.s);
|
|
117
|
+
}
|
|
118
|
+
return this.ref(staticParts);
|
|
119
|
+
}
|
|
120
|
+
fail(segs, expected, dataExpr) {
|
|
121
|
+
return `return new AE(${this.storedPathExpr(segs)},${JSON.stringify(expected)},${dataExpr})`;
|
|
122
|
+
}
|
|
123
|
+
/** Pure boolean predicate for a morph-free subtree. */
|
|
124
|
+
predicate(node, v) {
|
|
125
|
+
switch (node.k) {
|
|
126
|
+
case "unknown":
|
|
127
|
+
return "true";
|
|
128
|
+
case "null":
|
|
129
|
+
return `${v}===null`;
|
|
130
|
+
case "undefined":
|
|
131
|
+
return `${v}===undefined`;
|
|
132
|
+
case "boolean":
|
|
133
|
+
return `typeof ${v}==="boolean"`;
|
|
134
|
+
case "bigint":
|
|
135
|
+
return `typeof ${v}==="bigint"`;
|
|
136
|
+
case "symbol":
|
|
137
|
+
return `typeof ${v}==="symbol"`;
|
|
138
|
+
case "never":
|
|
139
|
+
return "false";
|
|
140
|
+
case "anyobject":
|
|
141
|
+
return `(typeof ${v}==="object"&&${v}!==null)`;
|
|
142
|
+
case "lit":
|
|
143
|
+
return node.v instanceof Date
|
|
144
|
+
? `(${v} instanceof Date&&${v}.valueOf()===${node.v.valueOf()})`
|
|
145
|
+
: `${v}===${this.lit(node.v)}`;
|
|
146
|
+
case "instance":
|
|
147
|
+
return `${v} instanceof ${this.ref(node.ctor)}`;
|
|
148
|
+
case "string": {
|
|
149
|
+
let out = `typeof ${v}==="string"`;
|
|
150
|
+
if (node.min !== undefined)
|
|
151
|
+
out += `&&${v}.length>=${node.min}`;
|
|
152
|
+
if (node.max !== undefined)
|
|
153
|
+
out += `&&${v}.length<=${node.max}`;
|
|
154
|
+
if (node.url)
|
|
155
|
+
out += `&&URL.canParse(${v})`;
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
case "number": {
|
|
159
|
+
let out = node.int ? `Number.isInteger(${v})` : `Number.isFinite(${v})`;
|
|
160
|
+
if (node.divisor !== undefined)
|
|
161
|
+
out += `&&${v}%${node.divisor}===0`;
|
|
162
|
+
if (node.min !== undefined)
|
|
163
|
+
out += `&&${v}${node.xmin ? ">" : ">="}${node.min}`;
|
|
164
|
+
if (node.max !== undefined)
|
|
165
|
+
out += `&&${v}${node.xmax ? "<" : "<="}${node.max}`;
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
case "union": {
|
|
169
|
+
const lits = node.members.filter(isPrimitiveLiteral);
|
|
170
|
+
if (lits.length > 8) {
|
|
171
|
+
const values = this.ref(new Set(lits.map(member => member.v)));
|
|
172
|
+
const literalNodes = new Set(lits);
|
|
173
|
+
const rest = node.members.filter(member => !literalNodes.has(member));
|
|
174
|
+
let out = `${values}.has(${v})`;
|
|
175
|
+
for (const m of rest)
|
|
176
|
+
out += `||(${this.predicate(m, v)})`;
|
|
177
|
+
return `(${out})`;
|
|
178
|
+
}
|
|
179
|
+
return `(${node.members.map(m => `(${this.predicate(m, v)})`).join("||")})`;
|
|
180
|
+
}
|
|
181
|
+
case "intersection":
|
|
182
|
+
return `(${node.members.map(member => `(${this.predicate(member, v)})`).join("&&")})`;
|
|
183
|
+
case "array": {
|
|
184
|
+
const array = this.next("a");
|
|
185
|
+
const index = this.next("i");
|
|
186
|
+
let out = `Array.isArray(${v})`;
|
|
187
|
+
if (node.min !== undefined)
|
|
188
|
+
out += `&&${v}.length>=${node.min}`;
|
|
189
|
+
if (node.max !== undefined)
|
|
190
|
+
out += `&&${v}.length<=${node.max}`;
|
|
191
|
+
const item = `${array}[${index}]`;
|
|
192
|
+
out += `&&((${array})=>{for(let ${index}=0;${index}<${array}.length;${index}++)if(!(${this.predicate(node.el, item)}))return false;return true})(${v})`;
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
case "object": {
|
|
196
|
+
const checks = [`typeof ${v}==="object"`, `${v}!==null`, `!Array.isArray(${v})`];
|
|
197
|
+
for (const p of node.props) {
|
|
198
|
+
const av = access(v, p.key);
|
|
199
|
+
const present = `${JSON.stringify(p.key)} in ${v}`;
|
|
200
|
+
const predicate = this.predicate(p.val, av);
|
|
201
|
+
checks.push(p.opt || p.hasDefault
|
|
202
|
+
? rejectsUndefined(p.val)
|
|
203
|
+
? `((${av}!==undefined&&(${predicate}))||!(${present}))`
|
|
204
|
+
: `(!(${present})||(${predicate}))`
|
|
205
|
+
: rejectsUndefined(p.val)
|
|
206
|
+
? predicate
|
|
207
|
+
: `((${present})&&(${predicate}))`);
|
|
208
|
+
}
|
|
209
|
+
if (node.index) {
|
|
210
|
+
const k = this.next("k");
|
|
211
|
+
checks.push(`(()=>{for(const ${k} in ${v})if(own.call(${v},${k})&&!(${this.predicate(node.index, `${v}[${k}]`)}))return false;return true})()`);
|
|
212
|
+
}
|
|
213
|
+
else if (node.extras === "reject") {
|
|
214
|
+
const k = this.next("k");
|
|
215
|
+
checks.push(`(()=>{for(const ${k} in ${v})if(own.call(${v},${k})&&!(${this.declaredCheck(node.props, k)}))return false;return true})()`);
|
|
216
|
+
}
|
|
217
|
+
return `(${checks.join("&&")})`;
|
|
218
|
+
}
|
|
219
|
+
case "sub":
|
|
220
|
+
return `!(${this.ref(node.schema.run)}(${v}) instanceof AE)`;
|
|
221
|
+
default:
|
|
222
|
+
return `!(${this.ref(boundWalk(node))}(${v}) instanceof AE)`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
declaredCheck(props, keyVar) {
|
|
226
|
+
if (props.length === 0)
|
|
227
|
+
return "false";
|
|
228
|
+
if (props.length > 6) {
|
|
229
|
+
const set = this.ref(new Set(props.map(p => p.key)));
|
|
230
|
+
return `${set}.has(${keyVar})`;
|
|
231
|
+
}
|
|
232
|
+
return `(${props.map(p => `${keyVar}===${JSON.stringify(p.key)}`).join("||")})`;
|
|
233
|
+
}
|
|
234
|
+
emitDelegate(node, v, segs, out) {
|
|
235
|
+
const runner = node.k === "sub" ? node.schema.run : boundWalk(node);
|
|
236
|
+
const result = this.next("r");
|
|
237
|
+
this.push(`const ${result}=${this.ref(runner)}(${v});`);
|
|
238
|
+
this.push(segs.length === 0
|
|
239
|
+
? `if(${result} instanceof AE)return ${result};`
|
|
240
|
+
: `if(${result} instanceof AE)return PF(${result},${this.pathExpr(segs)});`);
|
|
241
|
+
if (out !== undefined)
|
|
242
|
+
this.push(`${out}=${result};`);
|
|
243
|
+
}
|
|
244
|
+
emitTupleShape(node, v, segs, failureData) {
|
|
245
|
+
this.push(`if(!Array.isArray(${v}))${this.fail(segs, "an array", failureData)};`);
|
|
246
|
+
const requiredPrefix = node.prefix.filter(item => !item.opt && !item.hasDefault).length;
|
|
247
|
+
const minimum = requiredPrefix + node.postfix.length;
|
|
248
|
+
if (minimum > 0) {
|
|
249
|
+
this.push(`if(${v}.length<${minimum})${this.fail(segs, `an array of at least length ${minimum}`, failureData)};`);
|
|
250
|
+
}
|
|
251
|
+
if (node.variadic === undefined) {
|
|
252
|
+
const maximum = node.prefix.length + node.postfix.length;
|
|
253
|
+
this.push(`if(${v}.length>${maximum})${this.fail(segs, `an array of at most length ${maximum}`, failureData)};`);
|
|
254
|
+
}
|
|
255
|
+
let postfixStart = `${v}.length`;
|
|
256
|
+
if (node.postfix.length > 0) {
|
|
257
|
+
postfixStart = this.next("p");
|
|
258
|
+
this.push(`const ${postfixStart}=${v}.length-${node.postfix.length};`);
|
|
259
|
+
}
|
|
260
|
+
let prefixCount = String(node.prefix.length);
|
|
261
|
+
if (requiredPrefix !== node.prefix.length) {
|
|
262
|
+
prefixCount = this.next("n");
|
|
263
|
+
this.push(`const ${prefixCount}=Math.min(${node.prefix.length},${postfixStart});`);
|
|
264
|
+
}
|
|
265
|
+
return { postfixStart, prefixCount, requiredPrefix };
|
|
266
|
+
}
|
|
267
|
+
/** Statement-form check for a morph-free subtree with precise error paths. */
|
|
268
|
+
emitCheck(node, v, segs, failureData = v) {
|
|
269
|
+
switch (node.k) {
|
|
270
|
+
case "unknown":
|
|
271
|
+
return;
|
|
272
|
+
case "array": {
|
|
273
|
+
let head = `Array.isArray(${v})`;
|
|
274
|
+
if (node.min !== undefined)
|
|
275
|
+
head += `&&${v}.length>=${node.min}`;
|
|
276
|
+
if (node.max !== undefined)
|
|
277
|
+
head += `&&${v}.length<=${node.max}`;
|
|
278
|
+
this.push(`if(!(${head}))${this.fail(segs, expectedOf(node), failureData)};`);
|
|
279
|
+
const i = this.next("i");
|
|
280
|
+
const x = this.next("x");
|
|
281
|
+
this.push(`for(let ${i}=0;${i}<${v}.length;${i}++){const ${x}=${v}[${i}];`);
|
|
282
|
+
this.emitCheck(node.el, x, [...segs, { d: i }]);
|
|
283
|
+
this.push("}");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
case "tuple": {
|
|
287
|
+
const { postfixStart, prefixCount, requiredPrefix } = this.emitTupleShape(node, v, segs, failureData);
|
|
288
|
+
for (let index = 0; index < node.prefix.length; index++) {
|
|
289
|
+
if (index >= requiredPrefix)
|
|
290
|
+
this.push(`if(${index}<${prefixCount}){`);
|
|
291
|
+
this.emitCheck(node.prefix[index].val, `${v}[${index}]`, [...segs, { d: String(index) }]);
|
|
292
|
+
if (index >= requiredPrefix)
|
|
293
|
+
this.push("}");
|
|
294
|
+
}
|
|
295
|
+
if (node.variadic !== undefined) {
|
|
296
|
+
const index = this.next("i");
|
|
297
|
+
this.push(`for(let ${index}=${prefixCount};${index}<${postfixStart};${index}++){`);
|
|
298
|
+
this.emitCheck(node.variadic, `${v}[${index}]`, [...segs, { d: index }]);
|
|
299
|
+
this.push("}");
|
|
300
|
+
}
|
|
301
|
+
for (let index = 0; index < node.postfix.length; index++) {
|
|
302
|
+
const inputIndex = index === 0 ? postfixStart : `${postfixStart}+${index}`;
|
|
303
|
+
this.emitCheck(node.postfix[index], `${v}[${inputIndex}]`, [...segs, { d: inputIndex }]);
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
case "object": {
|
|
308
|
+
this.push(`if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object", failureData)};`);
|
|
309
|
+
for (const p of node.props) {
|
|
310
|
+
const present = `${JSON.stringify(p.key)} in ${v}`;
|
|
311
|
+
const propSegs = [...segs, { s: p.key }];
|
|
312
|
+
if (p.opt || p.hasDefault) {
|
|
313
|
+
this.push(`if(${present}){`);
|
|
314
|
+
this.emitCheck(p.val, access(v, p.key), propSegs);
|
|
315
|
+
this.push("}");
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
this.push(`if(!(${present}))${this.fail(propSegs, expectedOf(p.val), "M")};`);
|
|
319
|
+
this.emitCheck(p.val, access(v, p.key), propSegs);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (node.index) {
|
|
323
|
+
const k = this.next("k");
|
|
324
|
+
this.push(`for(const ${k} in ${v})if(own.call(${v},${k})){`);
|
|
325
|
+
this.emitCheck(node.index, `${v}[${k}]`, [...segs, { d: k }]);
|
|
326
|
+
this.push("}");
|
|
327
|
+
}
|
|
328
|
+
else if (node.extras === "reject") {
|
|
329
|
+
const k = this.next("k");
|
|
330
|
+
this.push(`for(const ${k} in ${v})if(own.call(${v},${k})&&!(${this.declaredCheck(node.props, k)}))${this.fail([...segs, { d: k }], "removed (undeclared key)", `${v}[${k}]`)};`);
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
case "sub":
|
|
335
|
+
this.emitDelegate(node, v, segs);
|
|
336
|
+
return;
|
|
337
|
+
case "union": {
|
|
338
|
+
const failure = node.members.some(canRefineUnionFailure)
|
|
339
|
+
? `return UF(${this.ref(node)},${failureData},${this.pathExpr(segs)},${JSON.stringify(expectedOf(node))})`
|
|
340
|
+
: this.fail(segs, expectedOf(node), failureData);
|
|
341
|
+
const literals = node.members.filter(isPrimitiveLiteral);
|
|
342
|
+
if (literals.length === node.members.length && literals.length >= 4) {
|
|
343
|
+
const cases = literals.map(member => `case ${this.lit(member.v)}:`).join("");
|
|
344
|
+
this.push(`switch(${v}){${cases}break;default:${failure};}`);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
this.push(`if(!(${this.predicate(node, v)}))${failure};`);
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
case "null":
|
|
352
|
+
case "undefined":
|
|
353
|
+
case "boolean":
|
|
354
|
+
case "bigint":
|
|
355
|
+
case "symbol":
|
|
356
|
+
case "never":
|
|
357
|
+
case "anyobject":
|
|
358
|
+
case "lit":
|
|
359
|
+
case "string":
|
|
360
|
+
case "number":
|
|
361
|
+
case "instance":
|
|
362
|
+
this.push(`if(!(${this.predicate(node, v)}))${this.fail(segs, expectedOf(node), failureData)};`);
|
|
363
|
+
return;
|
|
364
|
+
case "refine": {
|
|
365
|
+
this.emitCheck(node.base, v, segs, failureData);
|
|
366
|
+
const failure = this.fail(segs, node.expected, v);
|
|
367
|
+
this.push(`try{if(!${this.ref(node.pred)}(${v}))${failure};}catch{${failure};}`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
case "intersection":
|
|
371
|
+
for (const member of node.members)
|
|
372
|
+
this.emitCheck(member, v, segs, failureData);
|
|
373
|
+
return;
|
|
374
|
+
default:
|
|
375
|
+
this.emitDelegate(node, v, segs);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Validate `v` against a morphing subtree and assign the produced output
|
|
380
|
+
* to `out` (an already-declared `let`).
|
|
381
|
+
*/
|
|
382
|
+
emitProduce(node, v, segs, out, failureData = v) {
|
|
383
|
+
if (!hasMorph(node)) {
|
|
384
|
+
this.emitCheck(node, v, segs, failureData);
|
|
385
|
+
this.push(`${out}=${v};`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
switch (node.k) {
|
|
389
|
+
case "sub":
|
|
390
|
+
this.emitDelegate(node, v, segs, out);
|
|
391
|
+
return;
|
|
392
|
+
case "morph": {
|
|
393
|
+
const input = this.next("t");
|
|
394
|
+
this.push(`let ${input};`);
|
|
395
|
+
this.emitProduce(node.input, v, segs, input, failureData);
|
|
396
|
+
const context = this.next("c");
|
|
397
|
+
const result = this.next("r");
|
|
398
|
+
this.push(`const ${context}=new MC(${this.storedPathExpr(segs)},${input});`);
|
|
399
|
+
this.push(`const ${result}=${this.ref(node.fn)}(${input},${context});`);
|
|
400
|
+
this.push(`if(${result} instanceof AE)return ${result};`);
|
|
401
|
+
if (node.out === undefined)
|
|
402
|
+
this.push(`${out}=${result};`);
|
|
403
|
+
else
|
|
404
|
+
this.emitProduce(node.out, result, segs, out);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
case "alias": {
|
|
408
|
+
let active = this.#activeAliases;
|
|
409
|
+
if (active === undefined) {
|
|
410
|
+
active = new Set();
|
|
411
|
+
this.#activeAliases = active;
|
|
412
|
+
}
|
|
413
|
+
if (active.has(node)) {
|
|
414
|
+
this.emitDelegate(node, v, segs, out);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
active.add(node);
|
|
418
|
+
try {
|
|
419
|
+
this.emitProduce(node.resolve(), v, segs, out, failureData);
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
active.delete(node);
|
|
423
|
+
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
case "union": {
|
|
427
|
+
const ok = this.next("u");
|
|
428
|
+
this.push(`let ${ok}=false;`);
|
|
429
|
+
const label = this.next("b");
|
|
430
|
+
this.push(`${label}:{`);
|
|
431
|
+
for (const m of node.members) {
|
|
432
|
+
if (m.k !== "sub" && !hasMorph(m)) {
|
|
433
|
+
this.push(`if(${this.predicate(m, v)}){${out}=${v};${ok}=true;break ${label};}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
for (const m of node.members) {
|
|
437
|
+
if (m.k === "sub" || hasMorph(m)) {
|
|
438
|
+
const runner = m.k === "sub" ? m.schema.run : m.k === "alias" ? boundWalk(m) : compile(m);
|
|
439
|
+
const r = this.next("r");
|
|
440
|
+
this.push(`const ${r}=${this.ref(runner)}(${v});`);
|
|
441
|
+
this.push(`if(!(${r} instanceof AE)){${out}=${r};${ok}=true;break ${label};}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
this.push("}");
|
|
445
|
+
this.push(`if(!${ok})return UF(${this.ref(node)},${failureData},${this.pathExpr(segs)},${JSON.stringify(expectedOf(node))});`);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
case "array": {
|
|
449
|
+
let head = `Array.isArray(${v})`;
|
|
450
|
+
if (node.min !== undefined)
|
|
451
|
+
head += `&&${v}.length>=${node.min}`;
|
|
452
|
+
if (node.max !== undefined)
|
|
453
|
+
head += `&&${v}.length<=${node.max}`;
|
|
454
|
+
this.push(`if(!(${head}))${this.fail(segs, expectedOf(node), failureData)};`);
|
|
455
|
+
const arr = this.next("a");
|
|
456
|
+
const i = this.next("i");
|
|
457
|
+
const x = this.next("x");
|
|
458
|
+
const el = this.next("t");
|
|
459
|
+
this.push(`const ${arr}=new Array(${v}.length);`);
|
|
460
|
+
this.push(`for(let ${i}=0;${i}<${v}.length;${i}++){const ${x}=${v}[${i}];let ${el};`);
|
|
461
|
+
this.emitProduce(node.el, x, [...segs, { d: i }], el);
|
|
462
|
+
this.push(`${arr}[${i}]=${el};}`);
|
|
463
|
+
this.push(`${out}=${arr};`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
case "tuple": {
|
|
467
|
+
const { postfixStart, prefixCount, requiredPrefix } = this.emitTupleShape(node, v, segs, failureData);
|
|
468
|
+
const tuple = this.next("a");
|
|
469
|
+
this.push(`const ${tuple}=[...${v}];`);
|
|
470
|
+
for (let index = 0; index < node.prefix.length; index++) {
|
|
471
|
+
const item = node.prefix[index];
|
|
472
|
+
const input = `${v}[${index}]`;
|
|
473
|
+
const output = `${tuple}[${index}]`;
|
|
474
|
+
if (index >= requiredPrefix)
|
|
475
|
+
this.push(`if(${index}<${prefixCount}){`);
|
|
476
|
+
if (hasMorph(item.val))
|
|
477
|
+
this.emitProduce(item.val, input, [...segs, { d: String(index) }], output);
|
|
478
|
+
else {
|
|
479
|
+
this.emitCheck(item.val, input, [...segs, { d: String(index) }]);
|
|
480
|
+
this.push(`${output}=${input};`);
|
|
481
|
+
}
|
|
482
|
+
if (index >= requiredPrefix) {
|
|
483
|
+
if (item.hasDefault) {
|
|
484
|
+
const defaultValue = item.defFactory ? `${this.ref(item.def)}()` : this.lit(item.def);
|
|
485
|
+
this.push(`}else{${output}=${defaultValue};}`);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
this.push("}");
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (node.variadic !== undefined) {
|
|
493
|
+
const index = this.next("i");
|
|
494
|
+
const input = this.next("x");
|
|
495
|
+
this.push(`for(let ${index}=${prefixCount};${index}<${postfixStart};${index}++){const ${input}=${v}[${index}];`);
|
|
496
|
+
if (hasMorph(node.variadic)) {
|
|
497
|
+
this.emitProduce(node.variadic, input, [...segs, { d: index }], `${tuple}[${index}]`);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
this.emitCheck(node.variadic, input, [...segs, { d: index }]);
|
|
501
|
+
this.push(`${tuple}[${index}]=${input};`);
|
|
502
|
+
}
|
|
503
|
+
this.push("}");
|
|
504
|
+
}
|
|
505
|
+
for (let index = 0; index < node.postfix.length; index++) {
|
|
506
|
+
const inputIndex = index === 0 ? postfixStart : `${postfixStart}+${index}`;
|
|
507
|
+
const input = `${v}[${inputIndex}]`;
|
|
508
|
+
const output = `${tuple}[${inputIndex}]`;
|
|
509
|
+
const item = node.postfix[index];
|
|
510
|
+
if (hasMorph(item))
|
|
511
|
+
this.emitProduce(item, input, [...segs, { d: inputIndex }], output);
|
|
512
|
+
else {
|
|
513
|
+
this.emitCheck(item, input, [...segs, { d: inputIndex }]);
|
|
514
|
+
this.push(`${output}=${input};`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
this.push(`${out}=${tuple};`);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
case "object": {
|
|
521
|
+
this.push(`if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object", failureData)};`);
|
|
522
|
+
const o = this.next("o");
|
|
523
|
+
const fresh = node.extras !== "keep" && !node.index;
|
|
524
|
+
this.push(fresh ? `const ${o}={};` : `const ${o}={...${v}};`);
|
|
525
|
+
for (const p of node.props) {
|
|
526
|
+
const present = `${JSON.stringify(p.key)} in ${v}`;
|
|
527
|
+
const propSegs = [...segs, { s: p.key }];
|
|
528
|
+
const av = access(v, p.key);
|
|
529
|
+
const ao = access(o, p.key);
|
|
530
|
+
const morphChild = hasMorph(p.val);
|
|
531
|
+
const missing = [];
|
|
532
|
+
if (p.hasDefault) {
|
|
533
|
+
const dflt = p.defFactory ? `${this.ref(p.def)}()` : this.lit(p.def);
|
|
534
|
+
missing.push(`${ao}=${dflt};`);
|
|
535
|
+
}
|
|
536
|
+
else if (!p.opt) {
|
|
537
|
+
missing.push(`${this.fail(propSegs, expectedOf(p.val), "M")};`);
|
|
538
|
+
}
|
|
539
|
+
this.push(`if(!(${present})){${missing.join("")}}else{`);
|
|
540
|
+
if (morphChild) {
|
|
541
|
+
const t = this.next("t");
|
|
542
|
+
this.push(`let ${t};`);
|
|
543
|
+
this.emitProduce(p.val, av, propSegs, t);
|
|
544
|
+
this.push(`${ao}=${t};`);
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
this.emitCheck(p.val, av, propSegs);
|
|
548
|
+
if (fresh)
|
|
549
|
+
this.push(`${ao}=${av};`);
|
|
550
|
+
}
|
|
551
|
+
this.push("}");
|
|
552
|
+
}
|
|
553
|
+
if (node.index) {
|
|
554
|
+
const k = this.next("k");
|
|
555
|
+
this.push(`for(const ${k} in ${v})if(own.call(${v},${k})){`);
|
|
556
|
+
if (hasMorph(node.index)) {
|
|
557
|
+
const t = this.next("t");
|
|
558
|
+
this.push(`let ${t};`);
|
|
559
|
+
this.emitProduce(node.index, `${v}[${k}]`, [...segs, { d: k }], t);
|
|
560
|
+
this.push(`${o}[${k}]=${t};`);
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
this.emitCheck(node.index, `${v}[${k}]`, [...segs, { d: k }]);
|
|
564
|
+
}
|
|
565
|
+
this.push("}");
|
|
566
|
+
}
|
|
567
|
+
else if (node.extras === "reject") {
|
|
568
|
+
const k = this.next("k");
|
|
569
|
+
this.push(`for(const ${k} in ${v})if(own.call(${v},${k})&&!(${this.declaredCheck(node.props, k)}))${this.fail([...segs, { d: k }], "removed (undeclared key)", `${v}[${k}]`)};`);
|
|
570
|
+
}
|
|
571
|
+
this.push(`${out}=${o};`);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
case "refine": {
|
|
575
|
+
const refined = this.next("t");
|
|
576
|
+
this.push(`let ${refined};`);
|
|
577
|
+
this.emitProduce(node.base, v, segs, refined, failureData);
|
|
578
|
+
const failure = this.fail(segs, node.expected, refined);
|
|
579
|
+
this.push(`try{if(!${this.ref(node.pred)}(${refined}))${failure};}catch{${failure};}`);
|
|
580
|
+
this.push(`${out}=${refined};`);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
case "intersection": {
|
|
584
|
+
const current = this.next("t");
|
|
585
|
+
this.push(`let ${current}=${v};`);
|
|
586
|
+
for (const member of node.members) {
|
|
587
|
+
if (hasMorph(member))
|
|
588
|
+
this.emitProduce(member, current, segs, current);
|
|
589
|
+
else
|
|
590
|
+
this.emitCheck(member, current, segs);
|
|
591
|
+
}
|
|
592
|
+
this.push(`${out}=${current};`);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
default:
|
|
596
|
+
this.emitDelegate(node, v, segs, out);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
build(ir) {
|
|
600
|
+
let ret;
|
|
601
|
+
if (hasMorph(ir)) {
|
|
602
|
+
this.push("let o;");
|
|
603
|
+
// body emitted below needs `o` declared first, so splice ordering:
|
|
604
|
+
this.emitProduce(ir, "v", [], "o");
|
|
605
|
+
ret = "o";
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
this.emitCheck(ir, "v", []);
|
|
609
|
+
ret = "v";
|
|
610
|
+
}
|
|
611
|
+
const src = `return function(v){${this.#lines.join("")}return ${ret}}`;
|
|
612
|
+
const make = new Function("R", "AE", "M", "PF", "UF", "MC", "own", src);
|
|
613
|
+
return make(this.#refs, OmpErrors, MISSING, prefixErrors, unionFail, CompiledMorphContext, own);
|
|
614
|
+
}
|
|
615
|
+
emitAllows(node, v) {
|
|
616
|
+
switch (node.k) {
|
|
617
|
+
case "array": {
|
|
618
|
+
const array = this.next("a");
|
|
619
|
+
const index = this.next("i");
|
|
620
|
+
this.push(`const ${array}=${v};if(!Array.isArray(${array}))return false;`);
|
|
621
|
+
if (node.min !== undefined)
|
|
622
|
+
this.push(`if(${array}.length<${node.min})return false;`);
|
|
623
|
+
if (node.max !== undefined)
|
|
624
|
+
this.push(`if(${array}.length>${node.max})return false;`);
|
|
625
|
+
this.push(`for(let ${index}=0;${index}<${array}.length;${index}++){`);
|
|
626
|
+
this.emitAllows(node.el, `${array}[${index}]`);
|
|
627
|
+
this.push("}");
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
case "object": {
|
|
631
|
+
const object = this.next("o");
|
|
632
|
+
this.push(`const ${object}=${v};if(typeof ${object}!=="object"||${object}===null||Array.isArray(${object}))return false;`);
|
|
633
|
+
for (const prop of node.props) {
|
|
634
|
+
const value = this.next("p");
|
|
635
|
+
const present = `${JSON.stringify(prop.key)} in ${object}`;
|
|
636
|
+
this.push(`const ${value}=${access(object, prop.key)};`);
|
|
637
|
+
if (prop.opt || prop.hasDefault) {
|
|
638
|
+
if (rejectsUndefined(prop.val)) {
|
|
639
|
+
this.push(`if(${value}!==undefined){`);
|
|
640
|
+
this.emitAllows(prop.val, value);
|
|
641
|
+
this.push(`}else if(${present})return false;`);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
this.push(`if(${present}){`);
|
|
645
|
+
this.emitAllows(prop.val, value);
|
|
646
|
+
this.push("}");
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
if (!rejectsUndefined(prop.val))
|
|
651
|
+
this.push(`if(!(${present}))return false;`);
|
|
652
|
+
this.emitAllows(prop.val, value);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (node.index) {
|
|
656
|
+
const key = this.next("k");
|
|
657
|
+
this.push(`for(const ${key} in ${object}){if(!own.call(${object},${key}))continue;`);
|
|
658
|
+
this.emitAllows(node.index, `${object}[${key}]`);
|
|
659
|
+
this.push("}");
|
|
660
|
+
}
|
|
661
|
+
else if (node.extras === "reject") {
|
|
662
|
+
const key = this.next("k");
|
|
663
|
+
this.push(`for(const ${key} in ${object})if(own.call(${object},${key})&&!(${this.declaredCheck(node.props, key)}))return false;`);
|
|
664
|
+
}
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
case "union": {
|
|
668
|
+
const sources = [];
|
|
669
|
+
for (const member of node.members) {
|
|
670
|
+
if (!isPrimitiveLiteral(member))
|
|
671
|
+
break;
|
|
672
|
+
const source = litSource(member.v);
|
|
673
|
+
if (source === undefined)
|
|
674
|
+
break;
|
|
675
|
+
sources.push(source);
|
|
676
|
+
}
|
|
677
|
+
if (sources.length === node.members.length && sources.length >= 4) {
|
|
678
|
+
this.push(`switch(${v}){${sources.map(source => `case ${source}:`).join("")}break;default:return false;}`);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
this.push(`if(!(${this.predicate(node, v)}))return false;`);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
default:
|
|
685
|
+
this.push(`if(!(${this.predicate(node, v)}))return false;`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
buildAllows(ir) {
|
|
689
|
+
this.emitAllows(ir, "v");
|
|
690
|
+
const src = `return function(v){${this.#lines.join("")}return true}`;
|
|
691
|
+
const make = new Function("R", "AE", "own", src);
|
|
692
|
+
return make(this.#refs, OmpErrors, own);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function prefixErrors(errs, parts) {
|
|
696
|
+
for (let i = parts.length - 1; i >= 0; i--)
|
|
697
|
+
errs.prefix(parts[i]);
|
|
698
|
+
return errs;
|
|
699
|
+
}
|
|
700
|
+
const kWalk = Symbol("omptype.boundWalk");
|
|
701
|
+
/** Cached interpreter closure for recursive aliases and predicate-only fallbacks. */
|
|
702
|
+
function boundWalk(node) {
|
|
703
|
+
const tagged = node;
|
|
704
|
+
let fn = tagged[kWalk];
|
|
705
|
+
if (!fn) {
|
|
706
|
+
fn = (value) => walk(node, value);
|
|
707
|
+
tagged[kWalk] = fn;
|
|
708
|
+
}
|
|
709
|
+
return fn;
|
|
710
|
+
}
|
|
711
|
+
function resolvedRoot(ir) {
|
|
712
|
+
return ir.k === "alias" ? ir.resolve() : ir;
|
|
713
|
+
}
|
|
714
|
+
const compiledCache = new WeakMap();
|
|
715
|
+
const allowsCache = new WeakMap();
|
|
716
|
+
/** Compile `ir` into a specialized validator. */
|
|
717
|
+
export function compile(ir) {
|
|
718
|
+
const root = resolvedRoot(ir);
|
|
719
|
+
let validator = compiledCache.get(root);
|
|
720
|
+
if (validator === undefined) {
|
|
721
|
+
validator = new Builder().build(root);
|
|
722
|
+
compiledCache.set(root, validator);
|
|
723
|
+
}
|
|
724
|
+
return validator;
|
|
725
|
+
}
|
|
726
|
+
/** Compile `ir` into an allocation-free boolean validator. */
|
|
727
|
+
export function compileAllows(ir) {
|
|
728
|
+
const root = resolvedRoot(ir);
|
|
729
|
+
let validator = allowsCache.get(root);
|
|
730
|
+
if (validator === undefined) {
|
|
731
|
+
validator = new Builder().buildAllows(root);
|
|
732
|
+
allowsCache.set(root, validator);
|
|
733
|
+
}
|
|
734
|
+
return validator;
|
|
735
|
+
}
|
|
736
|
+
/** Generated source for inspection/debugging. */
|
|
737
|
+
export function compileToSource(ir) {
|
|
738
|
+
const root = resolvedRoot(ir);
|
|
739
|
+
const builder = new Builder();
|
|
740
|
+
if (hasMorph(root)) {
|
|
741
|
+
builder.push("let o;");
|
|
742
|
+
builder.emitProduce(root, "v", [], "o");
|
|
743
|
+
return `function(v){/* refs elided */return o}`;
|
|
744
|
+
}
|
|
745
|
+
builder.emitCheck(root, "v", []);
|
|
746
|
+
return `function(v){/* refs elided */return v}`;
|
|
747
|
+
}
|