@oh-my-pi/omptype 17.2.6 → 17.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -2
- package/README.md +83 -14
- package/dist/js/ark.js +16 -0
- package/dist/js/compile.js +942 -0
- package/dist/js/errors.js +394 -0
- package/dist/js/from-json-schema.js +234 -0
- package/dist/js/index.js +13 -0
- package/dist/js/infer.js +2 -0
- package/dist/js/interp.js +858 -0
- package/dist/js/ir.js +1700 -0
- package/dist/js/json-schema.js +312 -0
- package/dist/js/keywords.js +340 -0
- package/dist/js/type.js +3062 -0
- package/dist/js/typebox.js +355 -0
- package/dist/js/zod.js +269 -0
- package/dist/types/ark.d.ts +3 -11
- package/dist/types/compile.d.ts +2 -0
- package/dist/types/errors.d.ts +53 -16
- package/dist/types/from-json-schema.d.ts +9 -0
- package/dist/types/index.d.ts +4 -4
- package/dist/types/infer.d.ts +147 -31
- package/dist/types/interp.d.ts +14 -5
- package/dist/types/ir.d.ts +129 -9
- package/dist/types/json-schema.d.ts +15 -1
- package/dist/types/keywords.d.ts +7 -0
- package/dist/types/type.d.ts +525 -60
- package/dist/types/typebox.d.ts +78 -50
- package/dist/types/zod.d.ts +1 -1
- package/package.json +24 -8
- package/src/ark.ts +4 -14
- package/src/compile.ts +887 -154
- package/src/errors.ts +347 -37
- package/src/from-json-schema.ts +231 -0
- package/src/index.ts +4 -4
- package/src/infer.ts +363 -81
- package/src/interp.ts +585 -102
- package/src/ir.ts +1444 -128
- package/src/json-schema.ts +162 -19
- package/src/keywords.ts +380 -0
- package/src/type.ts +3842 -198
- package/src/typebox.ts +142 -99
- package/src/zod.ts +41 -28
package/src/compile.ts
CHANGED
|
@@ -11,21 +11,16 @@
|
|
|
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, materializeDefault, 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
|
-
|
|
23
22
|
const IDENT = /^[A-Za-z_$][\w$]*$/;
|
|
24
23
|
|
|
25
|
-
function access(base: string, key: string): string {
|
|
26
|
-
return IDENT.test(key) ? `${base}.${key}` : `${base}[${JSON.stringify(key)}]`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
24
|
/** Inline-able literal, else undefined (caller hoists into the refs pool). */
|
|
30
25
|
function litSource(v: unknown): string | undefined {
|
|
31
26
|
if (v === null) return "null";
|
|
@@ -43,9 +38,57 @@ function litSource(v: unknown): string | undefined {
|
|
|
43
38
|
|
|
44
39
|
type PathSeg = { s: PropertyKey } | { d: string };
|
|
45
40
|
|
|
41
|
+
type LiteralIR = Extract<IR, { k: "lit" }>;
|
|
42
|
+
|
|
43
|
+
function isPrimitiveLiteral(node: IR): node is LiteralIR {
|
|
44
|
+
return node.k === "lit" && (node.v === null || (typeof node.v !== "object" && typeof node.v !== "function"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Whether `undefined` necessarily fails, allowing property-presence checks to be elided. */
|
|
48
|
+
function rejectsUndefined(node: IR): boolean {
|
|
49
|
+
switch (node.k) {
|
|
50
|
+
case "unknown":
|
|
51
|
+
case "undefined":
|
|
52
|
+
case "alias":
|
|
53
|
+
case "sub":
|
|
54
|
+
return false;
|
|
55
|
+
case "lit":
|
|
56
|
+
return node.v !== undefined;
|
|
57
|
+
case "union":
|
|
58
|
+
return node.members.every(rejectsUndefined);
|
|
59
|
+
case "intersection":
|
|
60
|
+
return node.members.some(rejectsUndefined);
|
|
61
|
+
case "refine":
|
|
62
|
+
return rejectsUndefined(node.base);
|
|
63
|
+
case "morph":
|
|
64
|
+
return rejectsUndefined(node.input);
|
|
65
|
+
default:
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class CompiledMorphContext implements MorphContext {
|
|
71
|
+
#path: PropertyKey[] | PropertyKey | undefined;
|
|
72
|
+
#data: unknown;
|
|
73
|
+
|
|
74
|
+
constructor(path: PropertyKey[] | PropertyKey | undefined, data: unknown) {
|
|
75
|
+
this.#path = path;
|
|
76
|
+
this.#data = data;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
error(expectation: string): OmpErrors {
|
|
80
|
+
return new OmpErrors(this.#path, expectation, this.#data);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
reject(expectation: string): OmpErrors {
|
|
84
|
+
return this.error(expectation);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
46
88
|
class Builder {
|
|
47
89
|
#lines: string[] = [];
|
|
48
90
|
#refs: unknown[] = [];
|
|
91
|
+
#activeAliases: Set<IR> | undefined;
|
|
49
92
|
#id = 0;
|
|
50
93
|
|
|
51
94
|
next(prefix: string): string {
|
|
@@ -67,13 +110,39 @@ class Builder {
|
|
|
67
110
|
return litSource(v) ?? this.ref(v);
|
|
68
111
|
}
|
|
69
112
|
|
|
113
|
+
access(base: string, key: PropertyKey): string {
|
|
114
|
+
return typeof key === "string" && IDENT.test(key) ? `${base}.${key}` : `${base}[${this.lit(key)}]`;
|
|
115
|
+
}
|
|
116
|
+
|
|
70
117
|
pathExpr(segs: PathSeg[]): string {
|
|
71
|
-
const parts = segs.map(seg => ("s" in seg ?
|
|
118
|
+
const parts = segs.map(seg => ("s" in seg ? this.lit(seg.s) : seg.d));
|
|
72
119
|
return `[${parts.join(",")}]`;
|
|
73
120
|
}
|
|
74
121
|
|
|
122
|
+
storedPathExpr(segs: PathSeg[]): string {
|
|
123
|
+
if (segs.length === 0) return "undefined";
|
|
124
|
+
if (segs.length === 1) {
|
|
125
|
+
const seg = segs[0];
|
|
126
|
+
return "s" in seg ? this.lit(seg.s) : seg.d;
|
|
127
|
+
}
|
|
128
|
+
const staticParts: PropertyKey[] = [];
|
|
129
|
+
for (const seg of segs) {
|
|
130
|
+
if ("d" in seg) return this.pathExpr(segs);
|
|
131
|
+
staticParts.push(seg.s);
|
|
132
|
+
}
|
|
133
|
+
return this.ref(staticParts);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
error(segs: PathSeg[], expected: string, dataExpr: string): string {
|
|
137
|
+
return `new AE(${this.storedPathExpr(segs)},${JSON.stringify(expected)},${dataExpr})`;
|
|
138
|
+
}
|
|
139
|
+
|
|
75
140
|
fail(segs: PathSeg[], expected: string, dataExpr: string): string {
|
|
76
|
-
return `return
|
|
141
|
+
return `return ${this.error(segs, expected, dataExpr)}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
appendError(errors: string, error: string): string {
|
|
145
|
+
return `if(${errors}===undefined)${errors}=${error};else ${errors}.append(${error});`;
|
|
77
146
|
}
|
|
78
147
|
|
|
79
148
|
/** Pure boolean predicate for a morph-free subtree. */
|
|
@@ -89,10 +158,18 @@ class Builder {
|
|
|
89
158
|
return `typeof ${v}==="boolean"`;
|
|
90
159
|
case "bigint":
|
|
91
160
|
return `typeof ${v}==="bigint"`;
|
|
161
|
+
case "symbol":
|
|
162
|
+
return `typeof ${v}==="symbol"`;
|
|
163
|
+
case "never":
|
|
164
|
+
return "false";
|
|
92
165
|
case "anyobject":
|
|
93
166
|
return `(typeof ${v}==="object"&&${v}!==null)`;
|
|
94
167
|
case "lit":
|
|
95
|
-
return
|
|
168
|
+
return node.v instanceof Date
|
|
169
|
+
? `(${v} instanceof Date&&${v}.valueOf()===${node.v.valueOf()})`
|
|
170
|
+
: `${v}===${this.lit(node.v)}`;
|
|
171
|
+
case "instance":
|
|
172
|
+
return `${v} instanceof ${this.ref(node.ctor)}`;
|
|
96
173
|
case "string": {
|
|
97
174
|
let out = `typeof ${v}==="string"`;
|
|
98
175
|
if (node.min !== undefined) out += `&&${v}.length>=${node.min}`;
|
|
@@ -101,57 +178,93 @@ class Builder {
|
|
|
101
178
|
return out;
|
|
102
179
|
}
|
|
103
180
|
case "number": {
|
|
104
|
-
let out = `
|
|
105
|
-
if (node.
|
|
181
|
+
let out = node.int ? `Number.isInteger(${v})` : `Number.isFinite(${v})`;
|
|
182
|
+
if (node.divisor !== undefined) out += `&&${v}%${node.divisor}===0`;
|
|
106
183
|
if (node.min !== undefined) out += `&&${v}${node.xmin ? ">" : ">="}${node.min}`;
|
|
107
184
|
if (node.max !== undefined) out += `&&${v}${node.xmax ? "<" : "<="}${node.max}`;
|
|
108
185
|
return out;
|
|
109
186
|
}
|
|
110
187
|
case "union": {
|
|
111
|
-
const lits = node.members.filter(
|
|
112
|
-
if (lits.length >
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
188
|
+
const lits = node.members.filter(isPrimitiveLiteral);
|
|
189
|
+
if (lits.length > 8) {
|
|
190
|
+
const values = this.ref(new Set(lits.map(member => member.v)));
|
|
191
|
+
const literalNodes = new Set<IR>(lits);
|
|
192
|
+
const rest = node.members.filter(member => !literalNodes.has(member));
|
|
193
|
+
let out = `${values}.has(${v})`;
|
|
116
194
|
for (const m of rest) out += `||(${this.predicate(m, v)})`;
|
|
117
195
|
return `(${out})`;
|
|
118
196
|
}
|
|
119
197
|
return `(${node.members.map(m => `(${this.predicate(m, v)})`).join("||")})`;
|
|
120
198
|
}
|
|
199
|
+
case "intersection":
|
|
200
|
+
return `(${node.members.map(member => `(${this.predicate(member, v)})`).join("&&")})`;
|
|
121
201
|
case "array": {
|
|
122
|
-
const
|
|
202
|
+
const array = this.next("a");
|
|
203
|
+
const index = this.next("i");
|
|
123
204
|
let out = `Array.isArray(${v})`;
|
|
124
205
|
if (node.min !== undefined) out += `&&${v}.length>=${node.min}`;
|
|
125
206
|
if (node.max !== undefined) out += `&&${v}.length<=${node.max}`;
|
|
126
|
-
|
|
207
|
+
const item = `${array}[${index}]`;
|
|
208
|
+
out += `&&((${array})=>{for(let ${index}=0;${index}<${array}.length;${index}++)if(!(${this.predicate(node.el, item)}))return false;return true})(${v})`;
|
|
127
209
|
return out;
|
|
128
210
|
}
|
|
129
211
|
case "object": {
|
|
130
|
-
const checks = [`typeof ${v}==="object"`, `${v}!==null
|
|
212
|
+
const checks = [`typeof ${v}==="object"`, `${v}!==null`];
|
|
131
213
|
for (const p of node.props) {
|
|
132
|
-
const av = access(v, p.key);
|
|
133
|
-
const present = `${
|
|
214
|
+
const av = this.access(v, p.key);
|
|
215
|
+
const present = `${this.lit(p.key)} in ${v}`;
|
|
216
|
+
const predicate = this.predicate(p.val, av);
|
|
134
217
|
checks.push(
|
|
135
218
|
p.opt || p.hasDefault
|
|
136
|
-
?
|
|
137
|
-
|
|
219
|
+
? rejectsUndefined(p.val)
|
|
220
|
+
? `((${av}!==undefined&&(${predicate}))||!(${present}))`
|
|
221
|
+
: `(!(${present})||(${predicate}))`
|
|
222
|
+
: rejectsUndefined(p.val)
|
|
223
|
+
? predicate
|
|
224
|
+
: `((${present})&&(${predicate}))`,
|
|
138
225
|
);
|
|
139
226
|
}
|
|
140
|
-
|
|
141
|
-
|
|
227
|
+
const stringKey = this.next("k");
|
|
228
|
+
if (node.index !== undefined) {
|
|
142
229
|
checks.push(
|
|
143
|
-
`(()=>{for(const ${
|
|
230
|
+
`(()=>{for(const ${stringKey} in ${v})if(own.call(${v},${stringKey})&&!(${this.predicate(node.index, `${v}[${stringKey}]`)}))return false;return true})()`,
|
|
144
231
|
);
|
|
145
|
-
}
|
|
146
|
-
|
|
232
|
+
}
|
|
233
|
+
if (node.patternIndexes !== undefined) {
|
|
234
|
+
for (const pattern of node.patternIndexes) {
|
|
235
|
+
checks.push(
|
|
236
|
+
`(()=>{for(const ${stringKey} in ${v})if(own.call(${v},${stringKey})&&(${this.predicate(pattern.key, stringKey)})&&!(${this.predicate(pattern.val, `${v}[${stringKey}]`)}))return false;return true})()`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (node.symbolIndex !== undefined) {
|
|
241
|
+
const symbol = this.next("s");
|
|
147
242
|
checks.push(
|
|
148
|
-
`(()=>{for(const ${
|
|
243
|
+
`(()=>{for(const ${symbol} of Object.getOwnPropertySymbols(${v}))if(Object.prototype.propertyIsEnumerable.call(${v},${symbol})&&!(${this.predicate(node.symbolIndex, `${v}[${symbol}]`)}))return false;return true})()`,
|
|
149
244
|
);
|
|
150
245
|
}
|
|
246
|
+
if (node.extras === "reject") {
|
|
247
|
+
const patternMatch =
|
|
248
|
+
node.patternIndexes?.map(pattern => `(${this.predicate(pattern.key, stringKey)})`).join("||") ??
|
|
249
|
+
"false";
|
|
250
|
+
if (node.index === undefined) {
|
|
251
|
+
checks.push(
|
|
252
|
+
`(()=>{for(const ${stringKey} in ${v})if(own.call(${v},${stringKey})&&!(${this.declaredCheck(node.props, stringKey)})&&!(${patternMatch}))return false;return true})()`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (node.symbolIndex === undefined) {
|
|
256
|
+
const symbol = this.next("s");
|
|
257
|
+
checks.push(
|
|
258
|
+
`(()=>{for(const ${symbol} of Object.getOwnPropertySymbols(${v}))if(Object.prototype.propertyIsEnumerable.call(${v},${symbol})&&!(${this.declaredCheck(node.props, symbol)}))return false;return true})()`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
151
262
|
return `(${checks.join("&&")})`;
|
|
152
263
|
}
|
|
153
264
|
case "sub":
|
|
154
265
|
return `!(${this.ref(node.schema.run)}(${v}) instanceof AE)`;
|
|
266
|
+
default:
|
|
267
|
+
return `!(${this.ref(boundWalk(node))}(${v}) instanceof AE)`;
|
|
155
268
|
}
|
|
156
269
|
}
|
|
157
270
|
|
|
@@ -161,89 +274,431 @@ class Builder {
|
|
|
161
274
|
const set = this.ref(new Set(props.map(p => p.key)));
|
|
162
275
|
return `${set}.has(${keyVar})`;
|
|
163
276
|
}
|
|
164
|
-
return `(${props.map(p => `${keyVar}===${
|
|
277
|
+
return `(${props.map(p => `${keyVar}===${this.lit(p.key)}`).join("||")})`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Run a node through its interpreter/sub-schema runner, appending any
|
|
282
|
+
* failure to `errors`. `brk` (when given) exits the enclosing block on
|
|
283
|
+
* failure so dependent statements (output assignment, morph fns) are
|
|
284
|
+
* skipped. Both runner kinds receive the absolute path so nested step
|
|
285
|
+
* callbacks observe ctx.path; walk-produced errors are already absolute,
|
|
286
|
+
* while sub runners return schema-relative errors that need prefixing.
|
|
287
|
+
*/
|
|
288
|
+
emitCollectDelegate(node: IR, v: string, segs: PathSeg[], errors: string, brk?: string, out?: string): void {
|
|
289
|
+
const sub = node.k === "sub";
|
|
290
|
+
const runner = sub ? node.schema.run : boundWalk(node);
|
|
291
|
+
const result = this.next("r");
|
|
292
|
+
const args = segs.length > 0 ? `${v},${this.pathExpr(segs)}` : v;
|
|
293
|
+
this.push(`const ${result}=${this.ref(runner)}(${args});`);
|
|
294
|
+
const failure = sub && segs.length > 0 ? `PF(${result},${this.pathExpr(segs)})` : result;
|
|
295
|
+
this.push(
|
|
296
|
+
`if(${result} instanceof AE){${this.appendError(errors, failure)}${brk === undefined ? "" : `break ${brk};`}}`,
|
|
297
|
+
);
|
|
298
|
+
if (out !== undefined) this.push(`${out}=${result};`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Snapshot the error count so sequencing sites can detect soft failures. */
|
|
302
|
+
markErrors(errors: string): string {
|
|
303
|
+
const mark = this.next("n");
|
|
304
|
+
this.push(`const ${mark}=${errors}===void 0?0:${errors}.length;`);
|
|
305
|
+
return mark;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Exit `brk` when errors were appended since `mark` (interp's return-on-error). */
|
|
309
|
+
guardGrowth(errors: string, mark: string, brk: string): void {
|
|
310
|
+
this.push(`if((${errors}===void 0?0:${errors}.length)!==${mark})break ${brk};`);
|
|
165
311
|
}
|
|
166
312
|
|
|
167
|
-
/**
|
|
168
|
-
|
|
313
|
+
/** Aggregate every independent failure in a morph-free subtree. */
|
|
314
|
+
emitCollectCheck(node: IR, v: string, segs: PathSeg[], errors: string, failureData = v): void {
|
|
315
|
+
if (node.cfg !== undefined || node.k === "refine") {
|
|
316
|
+
this.emitCollectDelegate(node, v, segs, errors);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
169
319
|
switch (node.k) {
|
|
170
320
|
case "unknown":
|
|
171
321
|
return;
|
|
172
322
|
case "array": {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
323
|
+
this.push(
|
|
324
|
+
`if(!Array.isArray(${v})){${this.appendError(errors, this.error(segs, "an array", failureData))}}`,
|
|
325
|
+
);
|
|
326
|
+
if (node.min !== undefined) {
|
|
327
|
+
this.push(
|
|
328
|
+
`else if(${v}.length<${node.min}){${this.appendError(
|
|
329
|
+
errors,
|
|
330
|
+
this.error(segs, `at least length ${node.min}`, `${v}.length`),
|
|
331
|
+
)}}`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (node.max !== undefined) {
|
|
335
|
+
this.push(
|
|
336
|
+
`else if(${v}.length>${node.max}){${this.appendError(
|
|
337
|
+
errors,
|
|
338
|
+
this.error(segs, `at most length ${node.max}`, `${v}.length`),
|
|
339
|
+
)}}`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
this.push("else{");
|
|
343
|
+
const index = this.next("i");
|
|
344
|
+
this.push(`for(let ${index}=0;${index}<${v}.length;${index}++){`);
|
|
345
|
+
this.emitCollectCheck(node.el, `${v}[${index}]`, [...segs, { d: index }], errors);
|
|
346
|
+
this.push("}}");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
case "tuple": {
|
|
350
|
+
this.push(
|
|
351
|
+
`if(!Array.isArray(${v})){${this.appendError(errors, this.error(segs, "an array", failureData))}}else{`,
|
|
352
|
+
);
|
|
353
|
+
const requiredPrefix = node.prefix.filter(item => !item.opt && !item.hasDefault).length;
|
|
354
|
+
const minimum = requiredPrefix + node.postfix.length;
|
|
355
|
+
const maximum = node.prefix.length + node.postfix.length;
|
|
356
|
+
if (minimum > 0) {
|
|
357
|
+
this.push(
|
|
358
|
+
`if(${v}.length<${minimum}){${this.appendError(
|
|
359
|
+
errors,
|
|
360
|
+
this.error(segs, `an array of at least length ${minimum}`, failureData),
|
|
361
|
+
)}}else{`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
if (node.variadic === undefined) {
|
|
365
|
+
this.push(
|
|
366
|
+
`if(${v}.length>${maximum}){${this.appendError(
|
|
367
|
+
errors,
|
|
368
|
+
this.error(segs, `an array of at most length ${maximum}`, failureData),
|
|
369
|
+
)}}else{`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
let postfixStart = `${v}.length`;
|
|
373
|
+
if (node.postfix.length > 0) {
|
|
374
|
+
postfixStart = this.next("p");
|
|
375
|
+
this.push(`const ${postfixStart}=${v}.length-${node.postfix.length};`);
|
|
376
|
+
}
|
|
377
|
+
let prefixCount = String(node.prefix.length);
|
|
378
|
+
if (requiredPrefix !== node.prefix.length) {
|
|
379
|
+
prefixCount = this.next("n");
|
|
380
|
+
this.push(`const ${prefixCount}=Math.min(${node.prefix.length},${postfixStart});`);
|
|
381
|
+
}
|
|
382
|
+
for (let index = 0; index < node.prefix.length; index++) {
|
|
383
|
+
if (index >= requiredPrefix) this.push(`if(${index}<${prefixCount}){`);
|
|
384
|
+
this.emitCollectCheck(node.prefix[index].val, `${v}[${index}]`, [...segs, { d: String(index) }], errors);
|
|
385
|
+
if (index >= requiredPrefix) this.push("}");
|
|
386
|
+
}
|
|
387
|
+
if (node.variadic !== undefined) {
|
|
388
|
+
const index = this.next("i");
|
|
389
|
+
this.push(`for(let ${index}=${prefixCount};${index}<${postfixStart};${index}++){`);
|
|
390
|
+
this.emitCollectCheck(node.variadic, `${v}[${index}]`, [...segs, { d: index }], errors);
|
|
391
|
+
this.push("}");
|
|
392
|
+
}
|
|
393
|
+
for (let index = 0; index < node.postfix.length; index++) {
|
|
394
|
+
const inputIndex = index === 0 ? postfixStart : `${postfixStart}+${index}`;
|
|
395
|
+
this.emitCollectCheck(node.postfix[index], `${v}[${inputIndex}]`, [...segs, { d: inputIndex }], errors);
|
|
396
|
+
}
|
|
397
|
+
if (node.variadic === undefined) this.push("}");
|
|
398
|
+
if (minimum > 0) this.push("}");
|
|
181
399
|
this.push("}");
|
|
182
400
|
return;
|
|
183
401
|
}
|
|
184
402
|
case "object": {
|
|
403
|
+
if (
|
|
404
|
+
node.patternIndexes !== undefined ||
|
|
405
|
+
node.symbolIndex !== undefined ||
|
|
406
|
+
node.props.some(prop => typeof prop.key === "symbol")
|
|
407
|
+
) {
|
|
408
|
+
this.emitCollectDelegate(node, v, segs, errors);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
185
411
|
this.push(
|
|
186
|
-
`if(typeof ${v}!=="object"||${v}===null
|
|
412
|
+
`if(typeof ${v}!=="object"||${v}===null){${this.appendError(
|
|
413
|
+
errors,
|
|
414
|
+
this.error(segs, "an object", failureData),
|
|
415
|
+
)}}else{`,
|
|
187
416
|
);
|
|
188
|
-
for (const
|
|
189
|
-
const present = `${
|
|
190
|
-
const propSegs: PathSeg[] = [...segs, { s:
|
|
191
|
-
if (
|
|
417
|
+
for (const prop of node.props) {
|
|
418
|
+
const present = `${this.lit(prop.key)} in ${v}`;
|
|
419
|
+
const propSegs: PathSeg[] = [...segs, { s: prop.key }];
|
|
420
|
+
if (prop.opt || prop.hasDefault) {
|
|
192
421
|
this.push(`if(${present}){`);
|
|
193
|
-
this.
|
|
422
|
+
this.emitCollectCheck(prop.val, this.access(v, prop.key), propSegs, errors);
|
|
194
423
|
this.push("}");
|
|
195
424
|
} else {
|
|
196
|
-
this.push(
|
|
197
|
-
|
|
425
|
+
this.push(
|
|
426
|
+
`if(!(${present})){${this.appendError(
|
|
427
|
+
errors,
|
|
428
|
+
this.error(propSegs, expectedOf(prop.val), "M"),
|
|
429
|
+
)}}else{`,
|
|
430
|
+
);
|
|
431
|
+
this.emitCollectCheck(prop.val, this.access(v, prop.key), propSegs, errors);
|
|
432
|
+
this.push("}");
|
|
198
433
|
}
|
|
199
434
|
}
|
|
200
|
-
if (node.index) {
|
|
201
|
-
const
|
|
202
|
-
this.push(`for(const ${
|
|
203
|
-
this.
|
|
435
|
+
if (node.index !== undefined) {
|
|
436
|
+
const key = this.next("k");
|
|
437
|
+
this.push(`for(const ${key} in ${v})if(own.call(${v},${key})){`);
|
|
438
|
+
this.emitCollectCheck(node.index, `${v}[${key}]`, [...segs, { d: key }], errors);
|
|
204
439
|
this.push("}");
|
|
205
440
|
} else if (node.extras === "reject") {
|
|
206
|
-
const
|
|
441
|
+
const key = this.next("k");
|
|
442
|
+
this.push(
|
|
443
|
+
`for(const ${key} in ${v})if(own.call(${v},${key})&&!(${this.declaredCheck(node.props, key)})){`,
|
|
444
|
+
);
|
|
445
|
+
this.push(this.appendError(errors, this.error([...segs, { d: key }], "removed", `${v}[${key}]`)));
|
|
446
|
+
this.push("}");
|
|
447
|
+
}
|
|
448
|
+
if (node.extras === "reject") {
|
|
449
|
+
const symbol = this.next("s");
|
|
450
|
+
this.push(
|
|
451
|
+
`for(const ${symbol} of Object.getOwnPropertySymbols(${v}))if(Object.prototype.propertyIsEnumerable.call(${v},${symbol})&&!(${this.declaredCheck(node.props, symbol)})){${this.appendError(errors, this.error([...segs, { d: symbol }], "removed", `${v}[${symbol}]`))}}`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
this.push("}");
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
case "union": {
|
|
458
|
+
const failure = node.members.some(canRefineUnionFailure)
|
|
459
|
+
? `UF(${this.ref(node)},${failureData},${this.pathExpr(segs)},${JSON.stringify(expectedOf(node))})`
|
|
460
|
+
: this.error(segs, expectedOf(node), failureData);
|
|
461
|
+
this.push(`if(!(${this.predicate(node, v)})){${this.appendError(errors, failure)}}`);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
case "string": {
|
|
465
|
+
this.push(
|
|
466
|
+
`if(typeof ${v}!=="string"){${this.appendError(errors, this.error(segs, "a string", failureData))}}`,
|
|
467
|
+
);
|
|
468
|
+
if (node.min !== undefined) {
|
|
469
|
+
this.push(
|
|
470
|
+
`else if(${v}.length<${node.min}){${this.appendError(
|
|
471
|
+
errors,
|
|
472
|
+
this.error(segs, `at least length ${node.min}`, `${v}.length`),
|
|
473
|
+
)}}`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
if (node.max !== undefined) {
|
|
207
477
|
this.push(
|
|
208
|
-
`
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
478
|
+
`else if(${v}.length>${node.max}){${this.appendError(
|
|
479
|
+
errors,
|
|
480
|
+
this.error(segs, `at most length ${node.max}`, `${v}.length`),
|
|
481
|
+
)}}`,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
if (node.url) {
|
|
485
|
+
this.push(
|
|
486
|
+
`else if(!URL.canParse(${v})){${this.appendError(
|
|
487
|
+
errors,
|
|
488
|
+
this.error(segs, "a URL string", failureData),
|
|
489
|
+
)}}`,
|
|
213
490
|
);
|
|
214
491
|
}
|
|
215
492
|
return;
|
|
216
493
|
}
|
|
217
|
-
case "
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
494
|
+
case "number": {
|
|
495
|
+
this.push(
|
|
496
|
+
`if(typeof ${v}!=="number"||!Number.isFinite(${v})){${this.appendError(
|
|
497
|
+
errors,
|
|
498
|
+
this.error(segs, node.int ? "an integer" : "a number", failureData),
|
|
499
|
+
)}}else{`,
|
|
500
|
+
);
|
|
501
|
+
if (node.int) {
|
|
502
|
+
this.push(
|
|
503
|
+
`if(!Number.isInteger(${v})){${this.appendError(errors, this.error(segs, "an integer", failureData))}}`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
if (node.divisor !== undefined) {
|
|
507
|
+
this.push(
|
|
508
|
+
`if(${v}%${node.divisor}!==0){${this.appendError(
|
|
509
|
+
errors,
|
|
510
|
+
this.error(segs, `a number divisible by ${node.divisor}`, failureData),
|
|
511
|
+
)}}`,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
if (node.min !== undefined) {
|
|
515
|
+
const expected =
|
|
516
|
+
node.min === 0
|
|
517
|
+
? node.xmin
|
|
518
|
+
? "positive"
|
|
519
|
+
: "non-negative"
|
|
520
|
+
: `a number ${node.xmin ? "more than" : "at least"} ${node.min}`;
|
|
521
|
+
this.push(
|
|
522
|
+
`if(${v}${node.xmin ? "<=" : "<"}${node.min}){${this.appendError(
|
|
523
|
+
errors,
|
|
524
|
+
this.error(segs, expected, failureData),
|
|
525
|
+
)}}`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
if (node.max !== undefined) {
|
|
529
|
+
const expected =
|
|
530
|
+
node.max === 0
|
|
531
|
+
? node.xmax
|
|
532
|
+
? "negative"
|
|
533
|
+
: "non-positive"
|
|
534
|
+
: `a number ${node.xmax ? "less than" : "at most"} ${node.max}`;
|
|
535
|
+
this.push(
|
|
536
|
+
`if(${v}${node.xmax ? ">=" : ">"}${node.max}){${this.appendError(
|
|
537
|
+
errors,
|
|
538
|
+
this.error(segs, expected, failureData),
|
|
539
|
+
)}}`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
this.push("}");
|
|
221
543
|
return;
|
|
222
544
|
}
|
|
223
|
-
case "
|
|
224
|
-
|
|
545
|
+
case "lit":
|
|
546
|
+
if (
|
|
547
|
+
(node.v !== null && typeof node.v === "object" && !(node.v instanceof Date)) ||
|
|
548
|
+
typeof node.v === "function"
|
|
549
|
+
) {
|
|
550
|
+
this.emitCollectDelegate(node, v, segs, errors);
|
|
551
|
+
} else {
|
|
552
|
+
this.push(
|
|
553
|
+
`if(!(${this.predicate(node, v)})){${this.appendError(
|
|
554
|
+
errors,
|
|
555
|
+
this.error(segs, expectedOf(node), failureData),
|
|
556
|
+
)}}`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
case "intersection":
|
|
561
|
+
case "sub":
|
|
562
|
+
this.emitCollectDelegate(node, v, segs, errors);
|
|
563
|
+
return;
|
|
564
|
+
case "null":
|
|
565
|
+
case "undefined":
|
|
566
|
+
case "boolean":
|
|
567
|
+
case "bigint":
|
|
568
|
+
case "symbol":
|
|
569
|
+
case "never":
|
|
570
|
+
case "anyobject":
|
|
571
|
+
case "instance":
|
|
572
|
+
this.push(
|
|
573
|
+
`if(!(${this.predicate(node, v)})){${this.appendError(
|
|
574
|
+
errors,
|
|
575
|
+
this.error(segs, expectedOf(node), failureData),
|
|
576
|
+
)}}`,
|
|
577
|
+
);
|
|
225
578
|
return;
|
|
226
579
|
default:
|
|
227
|
-
this.
|
|
580
|
+
this.emitCollectDelegate(node, v, segs, errors);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
emitTupleShape(
|
|
585
|
+
node: TupleIR,
|
|
586
|
+
v: string,
|
|
587
|
+
segs: PathSeg[],
|
|
588
|
+
errors: string,
|
|
589
|
+
brk: string,
|
|
590
|
+
failureData: string,
|
|
591
|
+
): { postfixStart: string; prefixCount: string; requiredPrefix: number } {
|
|
592
|
+
this.push(
|
|
593
|
+
`if(!Array.isArray(${v})){${this.appendError(errors, this.error(segs, "an array", failureData))}break ${brk};}`,
|
|
594
|
+
);
|
|
595
|
+
const requiredPrefix = node.prefix.filter(item => !item.opt && !item.hasDefault).length;
|
|
596
|
+
const minimum = requiredPrefix + node.postfix.length;
|
|
597
|
+
if (minimum > 0) {
|
|
598
|
+
this.push(
|
|
599
|
+
`if(${v}.length<${minimum}){${this.appendError(
|
|
600
|
+
errors,
|
|
601
|
+
this.error(segs, `an array of at least length ${minimum}`, failureData),
|
|
602
|
+
)}break ${brk};}`,
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
if (node.variadic === undefined) {
|
|
606
|
+
const maximum = node.prefix.length + node.postfix.length;
|
|
607
|
+
this.push(
|
|
608
|
+
`if(${v}.length>${maximum}){${this.appendError(
|
|
609
|
+
errors,
|
|
610
|
+
this.error(segs, `an array of at most length ${maximum}`, failureData),
|
|
611
|
+
)}break ${brk};}`,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
let postfixStart = `${v}.length`;
|
|
615
|
+
if (node.postfix.length > 0) {
|
|
616
|
+
postfixStart = this.next("p");
|
|
617
|
+
this.push(`const ${postfixStart}=${v}.length-${node.postfix.length};`);
|
|
618
|
+
}
|
|
619
|
+
let prefixCount = String(node.prefix.length);
|
|
620
|
+
if (requiredPrefix !== node.prefix.length) {
|
|
621
|
+
prefixCount = this.next("n");
|
|
622
|
+
this.push(`const ${prefixCount}=Math.min(${node.prefix.length},${postfixStart});`);
|
|
623
|
+
}
|
|
624
|
+
return { postfixStart, prefixCount, requiredPrefix };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Fill `target` with a validated default (factory output revalidated per call). */
|
|
628
|
+
emitDefaultFill(val: IR, def: unknown, isFactory: boolean, target: string, segs: PathSeg[], errors: string): void {
|
|
629
|
+
if (isFactory && typeof def === "function") {
|
|
630
|
+
const candidate = this.next("d");
|
|
631
|
+
const resolved = this.next("t");
|
|
632
|
+
const label = this.next("L");
|
|
633
|
+
this.push(`const ${candidate}=${this.ref(def)}();let ${resolved};${label}:{`);
|
|
634
|
+
this.emitCollectProduce(val, candidate, segs, resolved, errors, label);
|
|
635
|
+
this.push(`${target}=${resolved};}`);
|
|
636
|
+
} else {
|
|
637
|
+
// Static defaults were prevalidated at construction; MD clones
|
|
638
|
+
// mutable payloads so callers cannot alias the schema's copy.
|
|
639
|
+
this.push(`${target}=${litSource(def) ?? `MD(${this.ref(def)})`};`);
|
|
228
640
|
}
|
|
229
641
|
}
|
|
230
642
|
|
|
231
643
|
/**
|
|
232
644
|
* Validate `v` against a morphing subtree and assign the produced output
|
|
233
|
-
* to `out` (an already-declared `let`).
|
|
645
|
+
* to `out` (an already-declared `let`). Failures append to `errors` and
|
|
646
|
+
* `break ${brk}` (skipping the output assignment), mirroring interp: an
|
|
647
|
+
* error in one child never suppresses sibling validation or morphs.
|
|
234
648
|
*/
|
|
235
|
-
|
|
649
|
+
emitCollectProduce(
|
|
650
|
+
node: IR,
|
|
651
|
+
v: string,
|
|
652
|
+
segs: PathSeg[],
|
|
653
|
+
out: string,
|
|
654
|
+
errors: string,
|
|
655
|
+
brk: string,
|
|
656
|
+
failureData = v,
|
|
657
|
+
): void {
|
|
658
|
+
if (node.cfg !== undefined || node.k === "refine") {
|
|
659
|
+
this.emitCollectDelegate(node, v, segs, errors, brk, out);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
236
662
|
if (!hasMorph(node)) {
|
|
237
|
-
this.
|
|
663
|
+
this.emitCollectCheck(node, v, segs, errors, failureData);
|
|
238
664
|
this.push(`${out}=${v};`);
|
|
239
665
|
return;
|
|
240
666
|
}
|
|
241
667
|
switch (node.k) {
|
|
242
|
-
case "sub":
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
this.
|
|
668
|
+
case "sub":
|
|
669
|
+
this.emitCollectDelegate(node, v, segs, errors, brk, out);
|
|
670
|
+
return;
|
|
671
|
+
case "morph": {
|
|
672
|
+
const input = this.next("t");
|
|
673
|
+
this.push(`let ${input};`);
|
|
674
|
+
const mark = this.markErrors(errors);
|
|
675
|
+
this.emitCollectProduce(node.input, v, segs, input, errors, brk, failureData);
|
|
676
|
+
this.guardGrowth(errors, mark, brk);
|
|
677
|
+
const context = this.next("c");
|
|
678
|
+
const result = this.next("r");
|
|
679
|
+
this.push(`const ${context}=new MC(${this.storedPathExpr(segs)},${input});`);
|
|
680
|
+
this.push(`const ${result}=${this.ref(node.fn)}(${input},${context});`);
|
|
681
|
+
this.push(`if(${result} instanceof AE){${this.appendError(errors, result)}break ${brk};}`);
|
|
682
|
+
if (node.out === undefined) this.push(`${out}=${result};`);
|
|
683
|
+
else this.emitCollectProduce(node.out, result, segs, out, errors, brk);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
case "alias": {
|
|
687
|
+
let active = this.#activeAliases;
|
|
688
|
+
if (active === undefined) {
|
|
689
|
+
active = new Set();
|
|
690
|
+
this.#activeAliases = active;
|
|
691
|
+
}
|
|
692
|
+
if (active.has(node)) {
|
|
693
|
+
this.emitCollectDelegate(node, v, segs, errors, brk, out);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
active.add(node);
|
|
697
|
+
try {
|
|
698
|
+
this.emitCollectProduce(node.resolve(), v, segs, out, errors, brk, failureData);
|
|
699
|
+
} finally {
|
|
700
|
+
active.delete(node);
|
|
701
|
+
}
|
|
247
702
|
return;
|
|
248
703
|
}
|
|
249
704
|
case "union": {
|
|
@@ -258,120 +713,359 @@ class Builder {
|
|
|
258
713
|
}
|
|
259
714
|
for (const m of node.members) {
|
|
260
715
|
if (m.k === "sub" || hasMorph(m)) {
|
|
261
|
-
const runner = m.k === "sub" ? m.schema.run : boundWalk(m);
|
|
716
|
+
const runner = m.k === "sub" ? m.schema.run : m.k === "alias" ? boundWalk(m) : compile(m);
|
|
262
717
|
const r = this.next("r");
|
|
263
718
|
this.push(`const ${r}=${this.ref(runner)}(${v});`);
|
|
264
719
|
this.push(`if(!(${r} instanceof AE)){${out}=${r};${ok}=true;break ${label};}`);
|
|
265
720
|
}
|
|
266
721
|
}
|
|
267
722
|
this.push("}");
|
|
268
|
-
|
|
723
|
+
const failure = `UF(${this.ref(node)},${failureData},${this.pathExpr(segs)},${JSON.stringify(expectedOf(node))})`;
|
|
724
|
+
this.push(`if(!${ok}){${this.appendError(errors, failure)}break ${brk};}`);
|
|
269
725
|
return;
|
|
270
726
|
}
|
|
271
727
|
case "array": {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
728
|
+
this.push(
|
|
729
|
+
`if(!Array.isArray(${v})){${this.appendError(errors, this.error(segs, "an array", failureData))}break ${brk};}`,
|
|
730
|
+
);
|
|
731
|
+
if (node.min !== undefined) {
|
|
732
|
+
this.push(
|
|
733
|
+
`if(${v}.length<${node.min}){${this.appendError(
|
|
734
|
+
errors,
|
|
735
|
+
this.error(segs, `at least length ${node.min}`, `${v}.length`),
|
|
736
|
+
)}break ${brk};}`,
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
if (node.max !== undefined) {
|
|
740
|
+
this.push(
|
|
741
|
+
`if(${v}.length>${node.max}){${this.appendError(
|
|
742
|
+
errors,
|
|
743
|
+
this.error(segs, `at most length ${node.max}`, `${v}.length`),
|
|
744
|
+
)}break ${brk};}`,
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
const array = this.next("a");
|
|
748
|
+
const index = this.next("i");
|
|
749
|
+
const input = this.next("x");
|
|
750
|
+
const element = this.next("t");
|
|
751
|
+
const label = this.next("L");
|
|
752
|
+
this.push(`const ${array}=new Array(${v}.length);`);
|
|
753
|
+
this.push(
|
|
754
|
+
`for(let ${index}=0;${index}<${v}.length;${index}++){const ${input}=${v}[${index}];let ${element};${label}:{`,
|
|
755
|
+
);
|
|
756
|
+
this.emitCollectProduce(node.el, input, [...segs, { d: index }], element, errors, label);
|
|
757
|
+
this.push(`${array}[${index}]=${element};}}`);
|
|
758
|
+
this.push(`${out}=${array};`);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
case "tuple": {
|
|
762
|
+
const { postfixStart, prefixCount, requiredPrefix } = this.emitTupleShape(
|
|
763
|
+
node,
|
|
764
|
+
v,
|
|
765
|
+
segs,
|
|
766
|
+
errors,
|
|
767
|
+
brk,
|
|
768
|
+
failureData,
|
|
769
|
+
);
|
|
770
|
+
const tuple = this.next("a");
|
|
771
|
+
this.push(`const ${tuple}=[...${v}];`);
|
|
772
|
+
for (let index = 0; index < node.prefix.length; index++) {
|
|
773
|
+
const item = node.prefix[index];
|
|
774
|
+
const itemSegs: PathSeg[] = [...segs, { s: index }];
|
|
775
|
+
const input = `${v}[${index}]`;
|
|
776
|
+
const output = `${tuple}[${index}]`;
|
|
777
|
+
const label = this.next("L");
|
|
778
|
+
if (index >= requiredPrefix) this.push(`if(${index}<${prefixCount}){`);
|
|
779
|
+
this.push(`${label}:{`);
|
|
780
|
+
if (hasMorph(item.val)) {
|
|
781
|
+
const temporary = this.next("t");
|
|
782
|
+
this.push(`let ${temporary};`);
|
|
783
|
+
this.emitCollectProduce(item.val, input, itemSegs, temporary, errors, label);
|
|
784
|
+
this.push(`${output}=${temporary};`);
|
|
785
|
+
} else {
|
|
786
|
+
this.emitCollectCheck(item.val, input, itemSegs, errors);
|
|
787
|
+
}
|
|
788
|
+
this.push("}");
|
|
789
|
+
if (index >= requiredPrefix) {
|
|
790
|
+
if (item.hasDefault) {
|
|
791
|
+
this.push("}else{");
|
|
792
|
+
this.emitDefaultFill(item.val, item.def, item.defFactory === true, output, itemSegs, errors);
|
|
793
|
+
this.push("}");
|
|
794
|
+
} else {
|
|
795
|
+
this.push("}");
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (node.variadic !== undefined) {
|
|
800
|
+
const index = this.next("i");
|
|
801
|
+
const input = this.next("x");
|
|
802
|
+
const label = this.next("L");
|
|
803
|
+
this.push(
|
|
804
|
+
`for(let ${index}=${prefixCount};${index}<${postfixStart};${index}++){const ${input}=${v}[${index}];${label}:{`,
|
|
805
|
+
);
|
|
806
|
+
if (hasMorph(node.variadic)) {
|
|
807
|
+
const temporary = this.next("t");
|
|
808
|
+
this.push(`let ${temporary};`);
|
|
809
|
+
this.emitCollectProduce(node.variadic, input, [...segs, { d: index }], temporary, errors, label);
|
|
810
|
+
this.push(`${tuple}[${index}]=${temporary};`);
|
|
811
|
+
} else {
|
|
812
|
+
this.emitCollectCheck(node.variadic, input, [...segs, { d: index }], errors);
|
|
813
|
+
}
|
|
814
|
+
this.push("}}");
|
|
815
|
+
}
|
|
816
|
+
for (let index = 0; index < node.postfix.length; index++) {
|
|
817
|
+
const inputIndex = index === 0 ? postfixStart : `${postfixStart}+${index}`;
|
|
818
|
+
const input = `${v}[${inputIndex}]`;
|
|
819
|
+
const item = node.postfix[index];
|
|
820
|
+
const label = this.next("L");
|
|
821
|
+
this.push(`${label}:{`);
|
|
822
|
+
if (hasMorph(item)) {
|
|
823
|
+
const temporary = this.next("t");
|
|
824
|
+
this.push(`let ${temporary};`);
|
|
825
|
+
this.emitCollectProduce(item, input, [...segs, { d: inputIndex }], temporary, errors, label);
|
|
826
|
+
this.push(`${tuple}[${inputIndex}]=${temporary};`);
|
|
827
|
+
} else {
|
|
828
|
+
this.emitCollectCheck(item, input, [...segs, { d: inputIndex }], errors);
|
|
829
|
+
}
|
|
830
|
+
this.push("}");
|
|
831
|
+
}
|
|
832
|
+
this.push(`${out}=${tuple};`);
|
|
285
833
|
return;
|
|
286
834
|
}
|
|
287
835
|
case "object": {
|
|
836
|
+
if (
|
|
837
|
+
node.patternIndexes !== undefined ||
|
|
838
|
+
node.symbolIndex !== undefined ||
|
|
839
|
+
node.props.some(prop => typeof prop.key === "symbol")
|
|
840
|
+
) {
|
|
841
|
+
this.emitCollectDelegate(node, v, segs, errors, brk, out);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
288
844
|
this.push(
|
|
289
|
-
`if(typeof ${v}!=="object"||${v}===null
|
|
845
|
+
`if(typeof ${v}!=="object"||${v}===null){${this.appendError(
|
|
846
|
+
errors,
|
|
847
|
+
this.error(segs, "an object", failureData),
|
|
848
|
+
)}break ${brk};}`,
|
|
290
849
|
);
|
|
291
|
-
const
|
|
292
|
-
const fresh = node.extras === "delete" &&
|
|
293
|
-
this.push(fresh ? `const ${
|
|
294
|
-
for (const
|
|
295
|
-
const present = `${
|
|
296
|
-
const propSegs: PathSeg[] = [...segs, { s:
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
missing.push(`${this.fail(propSegs, expectedOf(p.val), "M")};`);
|
|
306
|
-
} else if (fresh) {
|
|
307
|
-
// optional + absent under "delete": nothing to copy
|
|
850
|
+
const object = this.next("o");
|
|
851
|
+
const fresh = node.extras === "delete" && node.index === undefined;
|
|
852
|
+
this.push(fresh ? `const ${object}={};` : `const ${object}={...${v}};`);
|
|
853
|
+
for (const prop of node.props) {
|
|
854
|
+
const present = `${this.lit(prop.key)} in ${v}`;
|
|
855
|
+
const propSegs: PathSeg[] = [...segs, { s: prop.key }];
|
|
856
|
+
const input = this.access(v, prop.key);
|
|
857
|
+
const output = this.access(object, prop.key);
|
|
858
|
+
const label = this.next("L");
|
|
859
|
+
this.push(`if(!(${present})){`);
|
|
860
|
+
if (prop.hasDefault) {
|
|
861
|
+
this.emitDefaultFill(prop.val, prop.def, prop.defFactory === true, output, propSegs, errors);
|
|
862
|
+
} else if (!prop.opt) {
|
|
863
|
+
this.push(this.appendError(errors, this.error(propSegs, expectedOf(prop.val), "M")));
|
|
308
864
|
}
|
|
309
|
-
this.push(`
|
|
310
|
-
if (
|
|
311
|
-
const
|
|
312
|
-
this.push(`let ${
|
|
313
|
-
this.
|
|
314
|
-
this.push(`${
|
|
865
|
+
this.push(`}else{${label}:{`);
|
|
866
|
+
if (hasMorph(prop.val)) {
|
|
867
|
+
const temporary = this.next("t");
|
|
868
|
+
this.push(`let ${temporary};`);
|
|
869
|
+
this.emitCollectProduce(prop.val, input, propSegs, temporary, errors, label);
|
|
870
|
+
this.push(`${output}=${temporary};`);
|
|
315
871
|
} else {
|
|
316
|
-
this.
|
|
317
|
-
if (fresh) this.push(`${
|
|
872
|
+
this.emitCollectCheck(prop.val, input, propSegs, errors);
|
|
873
|
+
if (fresh) this.push(`${output}=${input};`);
|
|
318
874
|
}
|
|
319
|
-
this.push("}");
|
|
875
|
+
this.push("}}");
|
|
320
876
|
}
|
|
321
|
-
if (node.index) {
|
|
322
|
-
const
|
|
323
|
-
|
|
877
|
+
if (node.index !== undefined) {
|
|
878
|
+
const key = this.next("k");
|
|
879
|
+
const label = this.next("L");
|
|
880
|
+
this.push(`for(const ${key} in ${v})if(own.call(${v},${key})){${label}:{`);
|
|
324
881
|
if (hasMorph(node.index)) {
|
|
325
|
-
const
|
|
326
|
-
this.push(`let ${
|
|
327
|
-
this.
|
|
328
|
-
this.push(`${
|
|
882
|
+
const temporary = this.next("t");
|
|
883
|
+
this.push(`let ${temporary};`);
|
|
884
|
+
this.emitCollectProduce(node.index, `${v}[${key}]`, [...segs, { d: key }], temporary, errors, label);
|
|
885
|
+
this.push(`${object}[${key}]=${temporary};`);
|
|
329
886
|
} else {
|
|
330
|
-
this.
|
|
887
|
+
this.emitCollectCheck(node.index, `${v}[${key}]`, [...segs, { d: key }], errors);
|
|
331
888
|
}
|
|
332
|
-
this.push("}");
|
|
889
|
+
this.push("}}");
|
|
333
890
|
} else if (node.extras === "reject") {
|
|
334
|
-
const
|
|
891
|
+
const key = this.next("k");
|
|
892
|
+
this.push(
|
|
893
|
+
`for(const ${key} in ${v})if(own.call(${v},${key})&&!(${this.declaredCheck(node.props, key)})){${this.appendError(
|
|
894
|
+
errors,
|
|
895
|
+
this.error([...segs, { d: key }], "removed", `${v}[${key}]`),
|
|
896
|
+
)}}`,
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
if (node.extras === "reject") {
|
|
900
|
+
const symbol = this.next("s");
|
|
335
901
|
this.push(
|
|
336
|
-
`for(const ${
|
|
337
|
-
[...segs, { d: k }],
|
|
338
|
-
"removed (undeclared key)",
|
|
339
|
-
`${v}[${k}]`,
|
|
340
|
-
)};`,
|
|
902
|
+
`for(const ${symbol} of Object.getOwnPropertySymbols(${v}))if(Object.prototype.propertyIsEnumerable.call(${v},${symbol})&&!(${this.declaredCheck(node.props, symbol)})){${this.appendError(errors, this.error([...segs, { d: symbol }], "removed", `${v}[${symbol}]`))}}`,
|
|
341
903
|
);
|
|
342
904
|
}
|
|
343
|
-
this.push(`${out}=${
|
|
905
|
+
this.push(`${out}=${object};`);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
case "intersection": {
|
|
909
|
+
const current = this.next("t");
|
|
910
|
+
this.push(`let ${current}=${v};`);
|
|
911
|
+
const mark = this.markErrors(errors);
|
|
912
|
+
for (let index = 0; index < node.members.length; index++) {
|
|
913
|
+
if (index > 0) this.guardGrowth(errors, mark, brk);
|
|
914
|
+
const member = node.members[index];
|
|
915
|
+
if (hasMorph(member)) this.emitCollectProduce(member, current, segs, current, errors, brk);
|
|
916
|
+
else this.emitCollectCheck(member, current, segs, errors);
|
|
917
|
+
}
|
|
918
|
+
this.push(`${out}=${current};`);
|
|
344
919
|
return;
|
|
345
920
|
}
|
|
346
921
|
default:
|
|
347
|
-
|
|
348
|
-
this.emitCheck(node, v, segs);
|
|
349
|
-
this.push(`${out}=${v};`);
|
|
922
|
+
this.emitCollectDelegate(node, v, segs, errors, brk, out);
|
|
350
923
|
}
|
|
351
924
|
}
|
|
352
925
|
|
|
353
926
|
build(ir: IR): (value: unknown) => unknown {
|
|
927
|
+
const errors = this.next("e");
|
|
354
928
|
let ret: string;
|
|
355
929
|
if (hasMorph(ir)) {
|
|
356
|
-
this.
|
|
357
|
-
|
|
358
|
-
this.
|
|
930
|
+
const label = this.next("L");
|
|
931
|
+
this.push(`let ${errors};let o;${label}:{`);
|
|
932
|
+
this.emitCollectProduce(ir, "v", [], "o", errors, label);
|
|
933
|
+
this.push("}");
|
|
359
934
|
ret = "o";
|
|
360
935
|
} else {
|
|
361
|
-
this.
|
|
936
|
+
this.push(`let ${errors};`);
|
|
937
|
+
this.emitCollectCheck(ir, "v", [], errors);
|
|
362
938
|
ret = "v";
|
|
363
939
|
}
|
|
940
|
+
this.push(`if(${errors}!==undefined)return ${errors};`);
|
|
364
941
|
const src = `return function(v){${this.#lines.join("")}return ${ret}}`;
|
|
365
|
-
const make = new Function("R", "
|
|
942
|
+
const make = new Function("R", "AE", "M", "PF", "UF", "MC", "own", "MD", src) as (
|
|
366
943
|
refs: unknown[],
|
|
367
|
-
e: typeof OmpErrors.single,
|
|
368
944
|
ae: typeof OmpErrors,
|
|
369
945
|
m: typeof MISSING,
|
|
370
946
|
pf: typeof prefixErrors,
|
|
371
947
|
uf: typeof unionFail,
|
|
948
|
+
mc: typeof CompiledMorphContext,
|
|
372
949
|
ownFn: typeof own,
|
|
950
|
+
md: typeof materializeDefault,
|
|
373
951
|
) => (value: unknown) => unknown;
|
|
374
|
-
return make(
|
|
952
|
+
return make(
|
|
953
|
+
this.#refs,
|
|
954
|
+
OmpErrors,
|
|
955
|
+
MISSING,
|
|
956
|
+
prefixErrors,
|
|
957
|
+
unionFail,
|
|
958
|
+
CompiledMorphContext,
|
|
959
|
+
own,
|
|
960
|
+
materializeDefault,
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
emitAllows(node: IR, v: string): void {
|
|
965
|
+
switch (node.k) {
|
|
966
|
+
case "array": {
|
|
967
|
+
const array = this.next("a");
|
|
968
|
+
const index = this.next("i");
|
|
969
|
+
this.push(`const ${array}=${v};if(!Array.isArray(${array}))return false;`);
|
|
970
|
+
if (node.min !== undefined) this.push(`if(${array}.length<${node.min})return false;`);
|
|
971
|
+
if (node.max !== undefined) this.push(`if(${array}.length>${node.max})return false;`);
|
|
972
|
+
this.push(`for(let ${index}=0;${index}<${array}.length;${index}++){`);
|
|
973
|
+
this.emitAllows(node.el, `${array}[${index}]`);
|
|
974
|
+
this.push("}");
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
case "object": {
|
|
978
|
+
const object = this.next("o");
|
|
979
|
+
this.push(`const ${object}=${v};if(typeof ${object}!=="object"||${object}===null)return false;`);
|
|
980
|
+
for (const prop of node.props) {
|
|
981
|
+
const value = this.next("p");
|
|
982
|
+
const present = `${this.lit(prop.key)} in ${object}`;
|
|
983
|
+
this.push(`const ${value}=${this.access(object, prop.key)};`);
|
|
984
|
+
if (prop.opt || prop.hasDefault) {
|
|
985
|
+
if (rejectsUndefined(prop.val)) {
|
|
986
|
+
this.push(`if(${value}!==undefined){`);
|
|
987
|
+
this.emitAllows(prop.val, value);
|
|
988
|
+
this.push(`}else if(${present})return false;`);
|
|
989
|
+
} else {
|
|
990
|
+
this.push(`if(${present}){`);
|
|
991
|
+
this.emitAllows(prop.val, value);
|
|
992
|
+
this.push("}");
|
|
993
|
+
}
|
|
994
|
+
} else {
|
|
995
|
+
if (!rejectsUndefined(prop.val)) this.push(`if(!(${present}))return false;`);
|
|
996
|
+
this.emitAllows(prop.val, value);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const stringKey = this.next("k");
|
|
1000
|
+
if (node.index !== undefined) {
|
|
1001
|
+
this.push(`for(const ${stringKey} in ${object}){if(!own.call(${object},${stringKey}))continue;`);
|
|
1002
|
+
this.emitAllows(node.index, `${object}[${stringKey}]`);
|
|
1003
|
+
this.push("}");
|
|
1004
|
+
}
|
|
1005
|
+
if (node.patternIndexes !== undefined) {
|
|
1006
|
+
for (const pattern of node.patternIndexes) {
|
|
1007
|
+
this.push(
|
|
1008
|
+
`for(const ${stringKey} in ${object})if(own.call(${object},${stringKey})&&(${this.predicate(pattern.key, stringKey)})&&!(${this.predicate(pattern.val, `${object}[${stringKey}]`)}))return false;`,
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
if (node.symbolIndex !== undefined) {
|
|
1013
|
+
const symbol = this.next("s");
|
|
1014
|
+
this.push(
|
|
1015
|
+
`for(const ${symbol} of Object.getOwnPropertySymbols(${object})){if(!Object.prototype.propertyIsEnumerable.call(${object},${symbol}))continue;`,
|
|
1016
|
+
);
|
|
1017
|
+
this.emitAllows(node.symbolIndex, `${object}[${symbol}]`);
|
|
1018
|
+
this.push("}");
|
|
1019
|
+
}
|
|
1020
|
+
if (node.extras === "reject") {
|
|
1021
|
+
const patternMatch =
|
|
1022
|
+
node.patternIndexes?.map(pattern => `(${this.predicate(pattern.key, stringKey)})`).join("||") ??
|
|
1023
|
+
"false";
|
|
1024
|
+
if (node.index === undefined) {
|
|
1025
|
+
this.push(
|
|
1026
|
+
`for(const ${stringKey} in ${object})if(own.call(${object},${stringKey})&&!(${this.declaredCheck(node.props, stringKey)})&&!(${patternMatch}))return false;`,
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
if (node.symbolIndex === undefined) {
|
|
1030
|
+
const symbol = this.next("s");
|
|
1031
|
+
this.push(
|
|
1032
|
+
`for(const ${symbol} of Object.getOwnPropertySymbols(${object}))if(Object.prototype.propertyIsEnumerable.call(${object},${symbol})&&!(${this.declaredCheck(node.props, symbol)}))return false;`,
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
case "union": {
|
|
1039
|
+
const sources: string[] = [];
|
|
1040
|
+
for (const member of node.members) {
|
|
1041
|
+
if (!isPrimitiveLiteral(member)) break;
|
|
1042
|
+
const source = litSource(member.v);
|
|
1043
|
+
if (source === undefined) break;
|
|
1044
|
+
sources.push(source);
|
|
1045
|
+
}
|
|
1046
|
+
if (sources.length === node.members.length && sources.length >= 4) {
|
|
1047
|
+
this.push(
|
|
1048
|
+
`switch(${v}){${sources.map(source => `case ${source}:`).join("")}break;default:return false;}`,
|
|
1049
|
+
);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
this.push(`if(!(${this.predicate(node, v)}))return false;`);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
default:
|
|
1056
|
+
this.push(`if(!(${this.predicate(node, v)}))return false;`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
buildAllows(ir: IR): (value: unknown) => value is unknown {
|
|
1061
|
+
this.emitAllows(ir, "v");
|
|
1062
|
+
const src = `return function(v){${this.#lines.join("")}return true}`;
|
|
1063
|
+
const make = new Function("R", "AE", "own", src) as (
|
|
1064
|
+
refs: unknown[],
|
|
1065
|
+
ae: typeof OmpErrors,
|
|
1066
|
+
ownFn: typeof own,
|
|
1067
|
+
) => (value: unknown) => value is unknown;
|
|
1068
|
+
return make(this.#refs, OmpErrors, own);
|
|
375
1069
|
}
|
|
376
1070
|
}
|
|
377
1071
|
|
|
@@ -383,33 +1077,72 @@ function prefixErrors(errs: OmpErrors, parts: PropertyKey[]): OmpErrors {
|
|
|
383
1077
|
const kWalk = Symbol("omptype.boundWalk");
|
|
384
1078
|
|
|
385
1079
|
interface WalkTagged {
|
|
386
|
-
[kWalk]?: (value: unknown) => unknown;
|
|
1080
|
+
[kWalk]?: (value: unknown, path?: PropertyKey[]) => unknown;
|
|
387
1081
|
}
|
|
388
1082
|
|
|
389
|
-
/**
|
|
390
|
-
function boundWalk(node: IR): (value: unknown) => unknown {
|
|
1083
|
+
/** Cached interpreter closure for recursive aliases and predicate-only fallbacks. */
|
|
1084
|
+
function boundWalk(node: IR): (value: unknown, path?: PropertyKey[]) => unknown {
|
|
391
1085
|
const tagged = node as IR & WalkTagged;
|
|
392
1086
|
let fn = tagged[kWalk];
|
|
393
1087
|
if (!fn) {
|
|
394
|
-
fn = (value: unknown) => walk(node, value);
|
|
1088
|
+
fn = (value: unknown, path?: PropertyKey[]) => walk(node, value, path);
|
|
395
1089
|
tagged[kWalk] = fn;
|
|
396
1090
|
}
|
|
397
1091
|
return fn;
|
|
398
1092
|
}
|
|
399
1093
|
|
|
1094
|
+
function resolvedRoot(ir: IR): IR {
|
|
1095
|
+
return ir.k === "alias" ? ir.resolve() : ir;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const compiledCache = new WeakMap<IR, (value: unknown) => unknown>();
|
|
1099
|
+
const allowsCache = new WeakMap<IR, (value: unknown) => value is unknown>();
|
|
1100
|
+
|
|
400
1101
|
/** Compile `ir` into a specialized validator. */
|
|
401
1102
|
export function compile(ir: IR): (value: unknown) => unknown {
|
|
402
|
-
|
|
1103
|
+
const root = resolvedRoot(ir);
|
|
1104
|
+
const validator = compiledCache.get(root);
|
|
1105
|
+
if (validator === undefined) {
|
|
1106
|
+
// Publish a deferred wrapper before building: recursive schemas re-enter
|
|
1107
|
+
// compile() for the same root mid-build (e.g. an alias element inside an
|
|
1108
|
+
// array), and each re-entry must reuse this build instead of starting a
|
|
1109
|
+
// fresh one forever. The wrapper resolves to the built validator by call
|
|
1110
|
+
// time; the interpreter is a safety net that never triggers post-build.
|
|
1111
|
+
let built: ((value: unknown) => unknown) | undefined;
|
|
1112
|
+
compiledCache.set(root, value => (built === undefined ? walk(root, value) : built(value)));
|
|
1113
|
+
built = new Builder().build(root);
|
|
1114
|
+
compiledCache.set(root, built);
|
|
1115
|
+
return built;
|
|
1116
|
+
}
|
|
1117
|
+
return validator;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/** Compile `ir` into an allocation-free boolean validator. */
|
|
1121
|
+
export function compileAllows(ir: IR): (value: unknown) => value is unknown {
|
|
1122
|
+
const root = resolvedRoot(ir);
|
|
1123
|
+
const validator = allowsCache.get(root);
|
|
1124
|
+
if (validator === undefined) {
|
|
1125
|
+
let built: ((value: unknown) => value is unknown) | undefined;
|
|
1126
|
+
allowsCache.set(root, ((value: unknown) =>
|
|
1127
|
+
built === undefined ? !(walk(root, value) instanceof OmpErrors) : built(value)) as (
|
|
1128
|
+
value: unknown,
|
|
1129
|
+
) => value is unknown);
|
|
1130
|
+
built = new Builder().buildAllows(root);
|
|
1131
|
+
allowsCache.set(root, built);
|
|
1132
|
+
return built;
|
|
1133
|
+
}
|
|
1134
|
+
return validator;
|
|
403
1135
|
}
|
|
404
1136
|
|
|
405
1137
|
/** Generated source for inspection/debugging. */
|
|
406
1138
|
export function compileToSource(ir: IR): string {
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
1139
|
+
const root = resolvedRoot(ir);
|
|
1140
|
+
const builder = new Builder();
|
|
1141
|
+
if (hasMorph(root)) {
|
|
1142
|
+
builder.push("let o;");
|
|
1143
|
+
builder.emitCollectProduce(root, "v", [], "o", "e", "L0");
|
|
411
1144
|
return `function(v){/* refs elided */return o}`;
|
|
412
1145
|
}
|
|
413
|
-
|
|
1146
|
+
builder.emitCollectCheck(root, "v", [], "e");
|
|
414
1147
|
return `function(v){/* refs elided */return v}`;
|
|
415
1148
|
}
|