ata-validator 1.7.1 → 1.7.3

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.
@@ -7,23 +7,47 @@
7
7
  // child call is a direct monomorphic call, which is the same advantage the
8
8
  // code generator has, minus the codegen.
9
9
  //
10
- // Scope, deliberately narrow:
11
- // - single schema resource only (no embedded `$id`, no external documents),
12
- // so the base URI and the dynamic scope are compile-time constants and
13
- // `$ref` / `$dynamicRef` targets resolve once, here;
14
- // - no `unevaluatedProperties` / `unevaluatedItems` anywhere (annotation
15
- // flow stays eval()'s job).
16
- // Anything outside that gate keeps the generic evaluator. Error output must
10
+ // Everything eval() decides at run time from the schema path is decided
11
+ // here at compile time instead, because the schema path is static:
12
+ // - the base URI in effect, so `$ref` targets resolve once;
13
+ // - the dynamic scope, kept as the ordered list of resources entered so
14
+ // far, so `$dynamicRef` targets resolve once too. eval() pushes a
15
+ // resource every time the base changes, which can repeat entries; the
16
+ // outermost-first anchor search gives the same answer on the list with
17
+ // repeats removed, and that list is finite, so compilation terminates
18
+ // on cyclic schemas;
19
+ // - whether a node's annotations are read by anyone. A node under an
20
+ // in-place applicator of a collecting node collects; one under a child
21
+ // applicator (properties, items, not, ...) does not, exactly as eval()
22
+ // hands those the discard sink;
23
+ // - whether the schema can recurse at all. eval() guards every reference
24
+ // against revisiting a (schema, data) pair; that guard can only fire
25
+ // when the compiled graph has a cycle, which compilation notices, so an
26
+ // acyclic schema compiles without the guard and without the stack.
27
+ // A `$ref` or `$dynamicRef` that does not resolve compiles into the same
28
+ // runtime rejection eval() produces, so nothing declines. Error output must
17
29
  // be byte-for-byte what eval() produces; tests/test_plan_compiler.js diffs
18
30
  // the two over the official suite.
19
31
  //
20
- // Compiled signature: fn(data, errors, instancePath, schemaPath, stack) ->
21
- // boolean. `errors` may be the NOERRORS sentinel for verdict-only runs, in
22
- // which case a failed step returns immediately; when collecting, every step
23
- // runs, exactly like eval().
32
+ // Compiled pairs: `c(data, errors, instancePath, schemaPath, stack[, rec])`
33
+ // collects errors and returns the verdict; `v(data, stack[, rec])` returns
34
+ // the verdict alone, with no path strings and no error objects, and returns
35
+ // on the first failure. `errors` may be the NOERRORS sentinel, in which case
36
+ // the collect variant also stops at the first failure.
37
+ //
38
+ // Annotations live in a record {props, n, x}: the property names evaluated
39
+ // so far, the count of items evaluated contiguously from index 0, and any
40
+ // further item indexes (from `contains`). A node that reads annotations
41
+ // (unevaluated*) owns a fresh record per call; every in-place child writes
42
+ // straight into the record it is handed. eval() keeps a failed child's
43
+ // annotations out of its parent by merging only on success; here the
44
+ // caller notes the record's lengths before a child that may fail without
45
+ // failing the caller, and truncates back on failure. Same result, no
46
+ // allocation per child.
24
47
 
25
48
  function install(deps) {
26
- const { Plan, NOERRORS, err, evalLeaf, evalLeafV, deepEqual, dataBits, escapePointer,
49
+ const { Plan, NOERRORS, err, evalLeaf, evalLeafV, dataBits, escapePointer,
50
+ deepEqual, multipleOfOk, cpAtLeast, cpAtMost,
27
51
  T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri } = deps;
28
52
 
29
53
  const TRUE_FN = () => true;
@@ -36,136 +60,307 @@ function install(deps) {
36
60
  },
37
61
  };
38
62
 
39
- // Whether the whole tree reachable from `node` fits the compiler's scope.
40
- // `base` is the base URI in effect, updated at compile time exactly the way
41
- // eval() updates it at run time: the schema path is static, so it can be.
42
- function compilable(interp, node, base, seen) {
43
- if (node === true || node === false) return true;
44
- if (!(node instanceof Plan)) return true;
45
- const P = node;
46
- if (P.nodeBase !== null && P.nodeBase !== base) base = P.nodeBase;
47
- const key = P;
48
- const prev = seen.get(key);
49
- if (prev !== undefined) { if (prev === base || prev === true) return true; return false; }
50
- seen.set(key, base);
51
- if (P.hasUnevaluated) return false;
52
- if (P.ref !== null) {
53
- const t = resolveRef(P.ref, base, interp.state);
54
- if (t.node === undefined || !compilable(interp, interp.node(t.node), t.base, seen)) return false;
55
- }
56
- if (P.dynamicRef !== null) {
57
- // $dynamicRef stays compiled only in a single-resource schema, where
58
- // the dynamic scope is a constant. Anything larger keeps eval().
59
- if (interp.state.resources.size !== 1) return false;
60
- const t = dynTarget(interp, P.dynamicRef);
61
- if (t === undefined || !compilable(interp, interp.node(t), base, seen)) return false;
62
- }
63
- const kids = [];
64
- if (P.properties !== null) for (const v of P.properties.values()) kids.push(v.node);
65
- if (P.patternProperties !== null) for (const e of P.patternProperties) kids.push(e.node);
66
- if (P.additionalProperties !== undefined) kids.push(P.additionalProperties);
67
- if (P.propertyNames !== undefined) kids.push(P.propertyNames);
68
- if (P.dependentSchemas !== null) for (const [, v] of P.dependentSchemas) kids.push(v);
69
- if (P.propertyDependencies !== null) for (const [, m] of P.propertyDependencies) for (const v of m.values()) kids.push(v);
70
- if (P.prefixItems !== null) for (const v of P.prefixItems) kids.push(v);
71
- if (P.items !== undefined) kids.push(P.items);
72
- if (P.contains !== undefined) kids.push(P.contains);
73
- if (P.allOf !== null) for (const v of P.allOf) kids.push(v);
74
- if (P.anyOf !== null) for (const v of P.anyOf) kids.push(v);
75
- if (P.oneOf !== null) for (const v of P.oneOf) kids.push(v);
76
- if (P.not !== undefined) kids.push(P.not);
77
- if (P.if !== undefined) kids.push(P.if);
78
- if (P.then !== undefined) kids.push(P.then);
79
- if (P.else !== undefined) kids.push(P.else);
80
- for (const k of kids) if (!compilable(interp, k, base, seen)) return false;
81
- return true;
82
- }
83
-
84
- // Static $dynamicRef target: with a single resource the dynamic scope is
85
- // always [rootBase], so the outermost-scope search collapses to a lookup.
86
- function dynTarget(interp, ref) {
87
- const base = interp.state.rootBase;
88
- let { node } = resolveRef(ref, base, interp.state);
63
+ // Static $dynamicRef target, mirroring the `$dynamicRef` branch of eval()
64
+ // with the compile-time scope list in place of the runtime one.
65
+ function dynTarget(interp, ref, base, chain) {
66
+ const state = interp.state;
67
+ let { node, base: refBase } = resolveRef(ref, base, state);
89
68
  const [, fragment] = splitFragment(resolveUri(base, ref));
90
69
  if (fragment && !fragment.startsWith('/')) {
91
- const dyn = interp.state.dynamicAnchors.get(base);
92
- const bookended = node !== undefined && dyn && dyn.get(fragment) === node;
70
+ const initialDyn = state.dynamicAnchors.get(refBase);
71
+ const bookended = node !== undefined && initialDyn && initialDyn.get(fragment) === node;
93
72
  if (bookended || !interp.bookending) {
94
- if (dyn && dyn.has(fragment)) node = dyn.get(fragment);
73
+ for (let i = 0; i < chain.length; i++) {
74
+ const dyn = state.dynamicAnchors.get(chain[i]);
75
+ if (dyn && dyn.has(fragment)) { node = dyn.get(fragment); refBase = chain[i]; break; }
76
+ }
77
+ }
78
+ }
79
+ return { node, base: refBase };
80
+ }
81
+
82
+ // ---- annotation records ----
83
+ function fresh() { return { props: null, n: 0, x: null, keys: null }; }
84
+ // Object.keys once per record: every node that shares a record sees the
85
+ // same data object.
86
+ function keysOf(rec, data) {
87
+ if (rec.keys === null) rec.keys = Object.keys(data);
88
+ return rec.keys;
89
+ }
90
+ function hasProp(rec, key) {
91
+ const p = rec.props;
92
+ if (p === null) return false;
93
+ for (let i = 0; i < p.length; i++) if (p[i] === key) return true;
94
+ return false;
95
+ }
96
+ function addProp(rec, key) {
97
+ if (rec.props === null) rec.props = [key];
98
+ else rec.props.push(key);
99
+ }
100
+ function hasItem(rec, i) {
101
+ if (i < rec.n) return true;
102
+ const x = rec.x;
103
+ if (x === null) return false;
104
+ for (let j = 0; j < x.length; j++) if (x[j] === i) return true;
105
+ return false;
106
+ }
107
+ function addItem(rec, i) {
108
+ if (i < rec.n) return;
109
+ if (i === rec.n) { rec.n = i + 1; return; }
110
+ if (rec.x === null) rec.x = [i];
111
+ else rec.x.push(i);
112
+ }
113
+ function markRange(rec, from, to) {
114
+ if (from >= to) return;
115
+ if (from <= rec.n) { if (to > rec.n) rec.n = to; return; }
116
+ if (rec.x === null) rec.x = [];
117
+ for (let i = from; i < to; i++) rec.x.push(i);
118
+ }
119
+ function mergeRec(target, from) {
120
+ if (from.props !== null) {
121
+ if (target.props === null) target.props = from.props;
122
+ else for (let i = 0; i < from.props.length; i++) target.props.push(from.props[i]);
123
+ }
124
+ if (from.n > target.n) target.n = from.n;
125
+ if (from.x !== null) {
126
+ if (target.x === null) target.x = from.x;
127
+ else for (let i = 0; i < from.x.length; i++) target.x.push(from.x[i]);
128
+ }
129
+ }
130
+ // Rollback marks: the two array lengths and the contiguous count.
131
+ function pLen(rec) { return rec.props === null ? 0 : rec.props.length; }
132
+ function xLen(rec) { return rec.x === null ? 0 : rec.x.length; }
133
+ function undo(rec, pl, n0, xl) {
134
+ if (rec.props !== null) rec.props.length = pl;
135
+ rec.n = n0;
136
+ if (rec.x !== null) rec.x.length = xl;
137
+ }
138
+
139
+ // The value-level keywords of one node, compiled. evalLeafV() answers the
140
+ // same question by re-reading a dozen presence flags off the plan on every
141
+ // call, which is work the schema already decided: a plan's keywords never
142
+ // change. So each present keyword becomes one closure over its own operand
143
+ // and the node keeps only those, in the same order evalLeafV() runs them.
144
+ // Checks take (data, bits) because the type of the value is worth computing
145
+ // once per node rather than once per keyword; `needsBits` stays false when
146
+ // nothing in the node asks for it.
147
+ function compileLeafV(P) {
148
+ const checks = [];
149
+ let needsBits = false;
150
+
151
+ if (P.hasType) {
152
+ const mask = P.typeMask;
153
+ needsBits = true;
154
+ checks.push((data, bits) => (bits & mask) !== 0);
155
+ }
156
+ if (P.enum !== null) {
157
+ const vals = P.enum;
158
+ if (vals.length === 0) checks.push(() => false);
159
+ else checks.push((data) => {
160
+ for (let i = 0; i < vals.length; i++) if (deepEqual(vals[i], data)) return true;
161
+ return false;
162
+ });
163
+ }
164
+ if (P.hasConst) {
165
+ const c = P.const;
166
+ checks.push((data) => deepEqual(c, data));
167
+ }
168
+ if (P.hasNumber) {
169
+ const nums = [];
170
+ if (P.minimum !== undefined) { const m = P.minimum; nums.push((d) => d >= m); }
171
+ if (P.maximum !== undefined) { const m = P.maximum; nums.push((d) => d <= m); }
172
+ if (P.exclusiveMinimum !== undefined) { const m = P.exclusiveMinimum; nums.push((d) => d > m); }
173
+ if (P.exclusiveMaximum !== undefined) { const m = P.exclusiveMaximum; nums.push((d) => d < m); }
174
+ if (P.multipleOf !== undefined) { const m = P.multipleOf; nums.push((d) => multipleOfOk(d, m)); }
175
+ const run = combineValue(nums);
176
+ if (run !== null) checks.push((data) => typeof data !== 'number' || run(data));
177
+ }
178
+ if (P.hasString) {
179
+ const strs = [];
180
+ if (P.minLength !== undefined) { const m = P.minLength; strs.push((d) => cpAtLeast(d, m)); }
181
+ if (P.maxLength !== undefined) { const m = P.maxLength; strs.push((d) => cpAtMost(d, m)); }
182
+ if (P.pattern !== null) { const re = P.pattern; strs.push((d) => re.test(d)); }
183
+ if (P.formatFn !== null) { const f = P.formatFn; strs.push((d) => f(d)); }
184
+ const run = combineValue(strs);
185
+ if (run !== null) { needsBits = true; checks.push((data, bits) => bits !== T_STRING || run(data)); }
186
+ }
187
+ // `hasArray` also covers prefixItems/items/contains, which are not leaf
188
+ // keywords; only the three value-level ones belong here, exactly as
189
+ // evalLeafV() reads them.
190
+ if (P.minItems !== undefined || P.maxItems !== undefined || P.uniqueItems) {
191
+ const arrs = [];
192
+ if (P.minItems !== undefined) { const m = P.minItems; arrs.push((d) => d.length >= m); }
193
+ if (P.maxItems !== undefined) { const m = P.maxItems; arrs.push((d) => d.length <= m); }
194
+ if (P.uniqueItems) {
195
+ arrs.push((d) => {
196
+ for (let i = 0; i < d.length; i++) {
197
+ for (let j = i + 1; j < d.length; j++) if (deepEqual(d[i], d[j])) return false;
198
+ }
199
+ return true;
200
+ });
95
201
  }
202
+ const run = combineValue(arrs);
203
+ if (run !== null) { needsBits = true; checks.push((data, bits) => bits !== T_ARRAY || run(data)); }
204
+ }
205
+ if (P.required !== null || P.minProperties !== undefined || P.maxProperties !== undefined || P.dependentRequired !== null) {
206
+ const objs = [];
207
+ if (P.required !== null) {
208
+ const req = P.required;
209
+ if (req.length === 1) { const k = req[0]; objs.push((d) => Object.hasOwn(d, k)); }
210
+ else objs.push((d) => {
211
+ for (let i = 0; i < req.length; i++) if (!Object.hasOwn(d, req[i])) return false;
212
+ return true;
213
+ });
214
+ }
215
+ if (P.minProperties !== undefined) { const m = P.minProperties; objs.push((d) => Object.keys(d).length >= m); }
216
+ if (P.maxProperties !== undefined) { const m = P.maxProperties; objs.push((d) => Object.keys(d).length <= m); }
217
+ if (P.dependentRequired !== null) {
218
+ const entries = P.dependentRequired;
219
+ objs.push((d) => {
220
+ for (let i = 0; i < entries.length; i++) {
221
+ const [key, deps] = entries[i];
222
+ if (!Object.hasOwn(d, key)) continue;
223
+ for (let j = 0; j < deps.length; j++) if (!Object.hasOwn(d, deps[j])) return false;
224
+ }
225
+ return true;
226
+ });
227
+ }
228
+ const run = combineValue(objs);
229
+ if (run !== null) { needsBits = true; checks.push((data, bits) => bits !== T_OBJECT || run(data)); }
230
+ }
231
+
232
+ if (checks.length === 0) return TRUE_FN;
233
+ if (!needsBits) {
234
+ if (checks.length === 1) { const a = checks[0]; return (data) => a(data, 0); }
235
+ return (data) => {
236
+ for (let i = 0; i < checks.length; i++) if (!checks[i](data, 0)) return false;
237
+ return true;
238
+ };
96
239
  }
97
- return node;
240
+ if (checks.length === 1) { const a = checks[0]; return (data) => a(data, dataBits(data)); }
241
+ if (checks.length === 2) {
242
+ const [a, b] = checks;
243
+ return (data) => { const bits = dataBits(data); return a(data, bits) && b(data, bits); };
244
+ }
245
+ return (data) => {
246
+ const bits = dataBits(data);
247
+ for (let i = 0; i < checks.length; i++) if (!checks[i](data, bits)) return false;
248
+ return true;
249
+ };
98
250
  }
99
251
 
100
- function compileNode(interp, node, base, memo) {
252
+ // Folds a group's checks into one call, or null when the group is empty.
253
+ function combineValue(fns) {
254
+ if (fns.length === 0) return null;
255
+ if (fns.length === 1) return fns[0];
256
+ if (fns.length === 2) { const [a, b] = fns; return (d) => a(d) && b(d); }
257
+ return (d) => {
258
+ for (let i = 0; i < fns.length; i++) if (!fns[i](d)) return false;
259
+ return true;
260
+ };
261
+ }
262
+
263
+ function compileNode(ctx, node, base, chain, ann) {
101
264
  if (node === true) return TRUE_PAIR;
102
265
  if (node === false) return FALSE_PAIR;
103
266
  if (!(node instanceof Plan)) return TRUE_PAIR;
267
+ const interp = ctx.interp;
104
268
  const P = node;
105
269
  if (P.nodeBase !== null && P.nodeBase !== base) base = P.nodeBase;
106
- // A plan reached under two bases compiles once per base; nearly every
107
- // plan only ever has one.
108
- let perBase = memo.get(P);
109
- if (perBase === undefined) { perBase = new Map(); memo.set(P, perBase); }
110
- const cached = perBase.get(base);
111
- if (cached !== undefined) return cached;
270
+ if (chain.indexOf(base) === -1) chain = chain.concat(base);
271
+ const collect = ann || P.hasUnevaluated;
272
+
273
+ let perKey = ctx.memo.get(P);
274
+ if (perKey === undefined) { perKey = new Map(); ctx.memo.set(P, perKey); }
275
+ const key = (ann ? 'a ' : 'p ') + base + ' ' + chain.join(' ');
276
+ const cached = perKey.get(key);
277
+ if (cached !== undefined) {
278
+ if (cached.open) ctx.cyclic = true;
279
+ return cached;
280
+ }
112
281
  // Cycles: register trampolines before compiling children.
113
282
  const box = { v: null, c: null };
114
283
  const pair = {
115
- v: (d, st) => box.v(d, st),
116
- c: (d, e, ip, sp, st) => box.c(d, e, ip, sp, st),
284
+ open: true,
285
+ v: (d, st, rec) => box.v(d, st, rec),
286
+ c: (d, e, ip, sp, st, rec) => box.c(d, e, ip, sp, st, rec),
117
287
  };
118
- perBase.set(base, pair);
288
+ perKey.set(key, pair);
119
289
 
120
- const steps = []; // collect variant: (data, errors, ip, sp, stack)
121
- const vsteps = []; // verdict variant: (data, stack), no strings at all
290
+ // In-place children collect when this node does; child applicators
291
+ // never do. `rec` is threaded as the trailing argument of every step
292
+ // and is undefined on non-collecting nodes, where nothing reads it.
293
+ const inPlace = (n, b) => compileNode(ctx, n, b, chain, collect);
294
+ const child = (n) => compileNode(ctx, n, base, chain, false);
122
295
 
123
- // $ref / $dynamicRef, resolved at compile time. The cycle guard mirrors
124
- // eval(): a (schema, data) pair already on the stack is a fixed point.
125
- if (P.ref !== null) {
126
- const t = resolveRef(P.ref, base, interp.state);
127
- const target = compileNode(interp, interp.node(t.node), t.base, memo);
128
- const schema = P.schema;
129
- steps.push((data, errors, instancePath, schemaPath, stack) => {
130
- for (let i = stack.length - 2; i >= 0; i -= 2) {
131
- if (stack[i] === schema && stack[i + 1] === data) return true;
132
- }
133
- stack.push(schema, data);
134
- const ok = target.c(data, errors, instancePath, schemaPath + '/$ref', stack);
135
- stack.length -= 2;
136
- return ok;
137
- });
138
- vsteps.push((data, stack) => {
139
- for (let i = stack.length - 2; i >= 0; i -= 2) {
140
- if (stack[i] === schema && stack[i + 1] === data) return true;
141
- }
142
- stack.push(schema, data);
143
- const ok = target.v(data, stack);
144
- stack.length -= 2;
145
- return ok;
146
- });
147
- }
148
- if (P.dynamicRef !== null) {
149
- const target = compileNode(interp, interp.node(dynTarget(interp, P.dynamicRef)), base, memo);
150
- const schema = P.schema;
151
- steps.push((data, errors, instancePath, schemaPath, stack) => {
152
- for (let i = stack.length - 2; i >= 0; i -= 2) {
153
- if (stack[i] === schema && stack[i + 1] === data) return true;
154
- }
155
- stack.push(schema, data);
156
- const ok = target.c(data, errors, instancePath, schemaPath + '/$dynamicRef', stack);
157
- stack.length -= 2;
158
- return ok;
159
- });
160
- vsteps.push((data, stack) => {
161
- for (let i = stack.length - 2; i >= 0; i -= 2) {
162
- if (stack[i] === schema && stack[i + 1] === data) return true;
163
- }
164
- stack.push(schema, data);
165
- const ok = target.v(data, stack);
166
- stack.length -= 2;
167
- return ok;
168
- });
296
+ const steps = []; // collect variant: (data, errors, ip, sp, stack, rec)
297
+ const vsteps = []; // verdict variant: (data, stack, rec)
298
+
299
+ // $ref / $dynamicRef, resolved at compile time.
300
+ for (let which = 0; which < 2; which++) {
301
+ let t, seg;
302
+ if (which === 0) {
303
+ if (P.ref === null) continue;
304
+ t = resolveRef(P.ref, base, interp.state);
305
+ seg = '/$ref';
306
+ } else {
307
+ if (P.dynamicRef === null) continue;
308
+ t = dynTarget(interp, P.dynamicRef, base, chain);
309
+ seg = '/$dynamicRef';
310
+ }
311
+ if (t.node === undefined) {
312
+ // Reported when reached, exactly as eval() does; a metaschema-sized
313
+ // tree with one dangling reference in a branch the data never
314
+ // enters still compiles.
315
+ const ref = which === 0 ? P.ref : P.dynamicRef;
316
+ const kw = which === 0 ? '$ref' : '$dynamicRef';
317
+ steps.push((data, errors, instancePath, schemaPath) => {
318
+ if (errors !== NOERRORS) errors.push(err(kw, kw, instancePath, schemaPath + seg, { ref }, `cannot resolve ${kw} ${ref}`));
319
+ return false;
320
+ });
321
+ vsteps.push(() => false);
322
+ continue;
323
+ }
324
+ const target = inPlace(interp.node(t.node), t.base);
325
+ let cstep, vstep;
326
+ if (collect) {
327
+ cstep = (data, errors, instancePath, schemaPath, stack, rec) => {
328
+ const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
329
+ const ok = target.c(data, errors, instancePath, schemaPath + seg, stack, rec);
330
+ if (!ok) undo(rec, pl, n0, xl);
331
+ return ok;
332
+ };
333
+ vstep = (data, stack, rec) => target.v(data, stack, rec);
334
+ } else {
335
+ cstep = (data, errors, instancePath, schemaPath, stack) => target.c(data, errors, instancePath, schemaPath + seg, stack);
336
+ vstep = target.v;
337
+ }
338
+ if (ctx.guard) {
339
+ // The cycle guard mirrors eval(): a (schema, data) pair already on
340
+ // the stack is a fixed point.
341
+ const schema = P.schema;
342
+ const ic = cstep, iv = vstep;
343
+ cstep = (data, errors, instancePath, schemaPath, stack, rec) => {
344
+ for (let i = stack.length - 2; i >= 0; i -= 2) {
345
+ if (stack[i] === schema && stack[i + 1] === data) return true;
346
+ }
347
+ stack.push(schema, data);
348
+ const ok = ic(data, errors, instancePath, schemaPath, stack, rec);
349
+ stack.length -= 2;
350
+ return ok;
351
+ };
352
+ vstep = (data, stack, rec) => {
353
+ for (let i = stack.length - 2; i >= 0; i -= 2) {
354
+ if (stack[i] === schema && stack[i + 1] === data) return true;
355
+ }
356
+ stack.push(schema, data);
357
+ const ok = iv(data, stack, rec);
358
+ stack.length -= 2;
359
+ return ok;
360
+ };
361
+ }
362
+ steps.push(cstep);
363
+ vsteps.push(vstep);
169
364
  }
170
365
 
171
366
  // Every value-level keyword in one step, reusing the leaf evaluator so
@@ -175,13 +370,13 @@ function install(deps) {
175
370
  P.required !== null || P.minProperties !== undefined || P.maxProperties !== undefined ||
176
371
  P.dependentRequired !== null) {
177
372
  steps.push((data, errors, instancePath, schemaPath) => evalLeaf(P, data, errors, instancePath, schemaPath));
178
- vsteps.push((data) => evalLeafV(P, data));
373
+ vsteps.push(compileLeafV(P));
179
374
  }
180
375
 
181
376
  // Arrays
182
377
  if (P.prefixItems !== null) {
183
- const fns = P.prefixItems.map((v) => compileNode(interp, v, base, memo));
184
- steps.push((data, errors, instancePath, schemaPath, stack) => {
378
+ const fns = P.prefixItems.map(child);
379
+ steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
185
380
  if (dataBits(data) !== T_ARRAY) return true;
186
381
  let ok = true;
187
382
  const n = Math.min(fns.length, data.length);
@@ -191,19 +386,21 @@ function install(deps) {
191
386
  if (errors === NOERRORS) return false;
192
387
  }
193
388
  }
389
+ if (collect) markRange(rec, 0, n);
194
390
  return ok;
195
391
  });
196
- vsteps.push((data, stack) => {
392
+ vsteps.push((data, stack, rec) => {
197
393
  if (dataBits(data) !== T_ARRAY) return true;
198
394
  const n = Math.min(fns.length, data.length);
199
395
  for (let i = 0; i < n; i++) if (!fns[i].v(data[i], stack)) return false;
396
+ if (collect) markRange(rec, 0, n);
200
397
  return true;
201
398
  });
202
399
  }
203
400
  if (P.items !== undefined) {
204
- const fn = compileNode(interp, P.items, base, memo);
401
+ const fn = child(P.items);
205
402
  const start = P.prefixItems !== null ? P.prefixItems.length : 0;
206
- steps.push((data, errors, instancePath, schemaPath, stack) => {
403
+ steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
207
404
  if (dataBits(data) !== T_ARRAY) return true;
208
405
  let ok = true;
209
406
  for (let i = start; i < data.length; i++) {
@@ -212,24 +409,29 @@ function install(deps) {
212
409
  if (errors === NOERRORS) return false;
213
410
  }
214
411
  }
412
+ if (collect) markRange(rec, start, data.length);
215
413
  return ok;
216
414
  });
217
415
  const fv = fn.v;
218
- vsteps.push((data, stack) => {
416
+ vsteps.push((data, stack, rec) => {
219
417
  if (dataBits(data) !== T_ARRAY) return true;
220
418
  for (let i = start; i < data.length; i++) if (!fv(data[i], stack)) return false;
419
+ if (collect) markRange(rec, start, data.length);
221
420
  return true;
222
421
  });
223
422
  }
224
423
  if (P.contains !== undefined) {
225
- const fn = compileNode(interp, P.contains, base, memo);
424
+ const fn = child(P.contains);
226
425
  const minC = P.minContains !== undefined ? P.minContains : 1;
227
426
  const maxC = P.maxContains;
228
- steps.push((data, errors, instancePath, schemaPath, stack) => {
427
+ steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
229
428
  if (dataBits(data) !== T_ARRAY) return true;
230
429
  let matched = 0;
231
430
  for (let i = 0; i < data.length; i++) {
232
- if (fn.v(data[i], stack)) matched++;
431
+ if (fn.v(data[i], stack)) {
432
+ matched++;
433
+ if (collect) addItem(rec, i);
434
+ }
233
435
  }
234
436
  let ok = true;
235
437
  if (matched < minC) {
@@ -242,35 +444,55 @@ function install(deps) {
242
444
  }
243
445
  return ok;
244
446
  });
245
- vsteps.push((data, stack) => {
246
- if (dataBits(data) !== T_ARRAY) return true;
247
- let matched = 0;
248
- for (let i = 0; i < data.length; i++) {
249
- if (fn.v(data[i], stack)) { matched++; if (maxC === undefined && matched >= minC) return true; }
250
- }
251
- return matched >= minC && (maxC === undefined || matched <= maxC);
252
- });
447
+ if (collect) {
448
+ // Every matching index is an annotation, so the scan runs to the end.
449
+ vsteps.push((data, stack, rec) => {
450
+ if (dataBits(data) !== T_ARRAY) return true;
451
+ let matched = 0;
452
+ for (let i = 0; i < data.length; i++) {
453
+ if (fn.v(data[i], stack)) { matched++; addItem(rec, i); }
454
+ }
455
+ return matched >= minC && (maxC === undefined || matched <= maxC);
456
+ });
457
+ } else {
458
+ vsteps.push((data, stack) => {
459
+ if (dataBits(data) !== T_ARRAY) return true;
460
+ let matched = 0;
461
+ for (let i = 0; i < data.length; i++) {
462
+ if (fn.v(data[i], stack)) { matched++; if (maxC === undefined && matched >= minC) return true; }
463
+ }
464
+ return matched >= minC && (maxC === undefined || matched <= maxC);
465
+ });
466
+ }
253
467
  }
254
468
 
255
469
  // Objects
256
470
  if (P.properties !== null || P.patternProperties !== null || P.additionalProperties !== undefined || P.propertyNames !== undefined) {
257
471
  const props = P.properties;
258
- const propFns = props !== null ? new Map() : null;
259
- if (props !== null) {
260
- for (const [key, entry] of props) propFns.set(key, { fn: compileNode(interp, entry.node, base, memo), seg: entry.seg, schemaSeg: entry.schemaSeg });
261
- }
472
+ // Declared properties: a linear scan over a short list beats a Map
473
+ // lookup, and most schemas declare a handful.
474
+ const propKeys = props !== null ? [...props.keys()] : null;
475
+ const propList = props !== null
476
+ ? [...props.values()].map((entry) => ({ fn: child(entry.node), seg: entry.seg, schemaSeg: entry.schemaSeg }))
477
+ : null;
478
+ const propMap = props !== null && props.size > 8 ? new Map(propKeys.map((k, i) => [k, propList[i]])) : null;
479
+ const lookup = propMap !== null
480
+ ? (key) => propMap.get(key)
481
+ : propKeys !== null
482
+ ? (key) => { for (let i = 0; i < propKeys.length; i++) if (propKeys[i] === key) return propList[i]; return undefined; }
483
+ : null;
262
484
  const patterns = P.patternProperties !== null
263
- ? P.patternProperties.map((e) => ({ re: e.re, src: e.src, fn: compileNode(interp, e.node, base, memo) }))
485
+ ? P.patternProperties.map((e) => ({ re: e.re, src: e.src, fn: child(e.node) }))
264
486
  : null;
265
- const apFn = P.additionalProperties !== undefined ? compileNode(interp, P.additionalProperties, base, memo) : null;
266
- const pnFn = P.propertyNames !== undefined ? compileNode(interp, P.propertyNames, base, memo) : null;
267
- steps.push((data, errors, instancePath, schemaPath, stack) => {
487
+ const apFn = P.additionalProperties !== undefined ? child(P.additionalProperties) : null;
488
+ const pnFn = P.propertyNames !== undefined ? child(P.propertyNames) : null;
489
+ steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
268
490
  if (dataBits(data) !== T_OBJECT) return true;
269
491
  let ok = true;
270
- const keys = Object.keys(data);
492
+ const keys = collect ? keysOf(rec, data) : Object.keys(data);
271
493
  if (pnFn !== null) {
272
- for (const key of keys) {
273
- if (!pnFn.c(key, errors, instancePath + '/' + escapePointer(key), schemaPath + '/propertyNames', stack)) {
494
+ for (const k of keys) {
495
+ if (!pnFn.c(k, errors, instancePath + '/' + escapePointer(k), schemaPath + '/propertyNames', stack)) {
274
496
  ok = false;
275
497
  if (errors === NOERRORS) return false;
276
498
  }
@@ -279,7 +501,7 @@ function install(deps) {
279
501
  for (let k = 0; k < keys.length; k++) {
280
502
  const key = keys[k];
281
503
  let evaluated = false;
282
- const prop = propFns !== null ? propFns.get(key) : undefined;
504
+ const prop = lookup !== null ? lookup(key) : undefined;
283
505
  if (prop !== undefined) {
284
506
  if (!prop.fn.c(data[key], errors, instancePath + prop.seg, schemaPath + prop.schemaSeg, stack)) {
285
507
  ok = false;
@@ -304,20 +526,39 @@ function install(deps) {
304
526
  ok = false;
305
527
  if (errors === NOERRORS) return false;
306
528
  }
529
+ evaluated = true;
307
530
  }
531
+ if (evaluated && collect) addProp(rec, key);
308
532
  }
309
533
  return ok;
310
534
  });
311
- vsteps.push((data, stack) => {
535
+ if (patterns === null && apFn === null && pnFn === null && propKeys.length <= 8) {
536
+ // Only declared properties matter and there are few: walk the
537
+ // schema's list and look each one up, no key array. Own-property
538
+ // test on purpose, so an inherited name (`toString`, `__proto__`) is
539
+ // not mistaken for data. A long list walks the data's keys instead,
540
+ // since the data usually carries far fewer.
541
+ const n = propKeys.length;
542
+ vsteps.push((data, stack, rec) => {
543
+ if (dataBits(data) !== T_OBJECT) return true;
544
+ for (let i = 0; i < n; i++) {
545
+ const key = propKeys[i];
546
+ if (!Object.hasOwn(data, key)) continue;
547
+ if (!propList[i].fn.v(data[key], stack)) return false;
548
+ if (collect) addProp(rec, key);
549
+ }
550
+ return true;
551
+ });
552
+ } else vsteps.push((data, stack, rec) => {
312
553
  if (dataBits(data) !== T_OBJECT) return true;
313
- const keys = Object.keys(data);
554
+ const keys = collect ? keysOf(rec, data) : Object.keys(data);
314
555
  if (pnFn !== null) {
315
- for (const key of keys) if (!pnFn.v(key, stack)) return false;
556
+ for (const k of keys) if (!pnFn.v(k, stack)) return false;
316
557
  }
317
558
  for (let k = 0; k < keys.length; k++) {
318
559
  const key = keys[k];
319
560
  let evaluated = false;
320
- const prop = propFns !== null ? propFns.get(key) : undefined;
561
+ const prop = lookup !== null ? lookup(key) : undefined;
321
562
  if (prop !== undefined) {
322
563
  if (!prop.fn.v(data[key], stack)) return false;
323
564
  evaluated = true;
@@ -333,19 +574,28 @@ function install(deps) {
333
574
  }
334
575
  if (!evaluated && apFn !== null) {
335
576
  if (!apFn.v(data[key], stack)) return false;
577
+ evaluated = true;
336
578
  }
579
+ if (evaluated && collect) addProp(rec, key);
337
580
  }
338
581
  return true;
339
582
  });
340
583
  }
341
584
  if (P.dependentSchemas !== null) {
342
- const entries = P.dependentSchemas.map(([key, v]) => [key, compileNode(interp, v, base, memo), escapePointer(key)]);
343
- steps.push((data, errors, instancePath, schemaPath, stack) => {
585
+ const entries = P.dependentSchemas.map(([k, v]) => [k, inPlace(v, base), escapePointer(k)]);
586
+ steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
344
587
  if (dataBits(data) !== T_OBJECT) return true;
345
588
  let ok = true;
346
- for (const [key, fn, ek] of entries) {
347
- if (Object.hasOwn(data, key)) {
348
- if (!fn.c(data, errors, instancePath, schemaPath + '/dependentSchemas/' + ek, stack)) {
589
+ for (const [k, fn, ek] of entries) {
590
+ if (Object.hasOwn(data, k)) {
591
+ if (collect) {
592
+ const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
593
+ if (!fn.c(data, errors, instancePath, schemaPath + '/dependentSchemas/' + ek, stack, rec)) {
594
+ undo(rec, pl, n0, xl);
595
+ ok = false;
596
+ if (errors === NOERRORS) return false;
597
+ }
598
+ } else if (!fn.c(data, errors, instancePath, schemaPath + '/dependentSchemas/' + ek, stack)) {
349
599
  ok = false;
350
600
  if (errors === NOERRORS) return false;
351
601
  }
@@ -353,45 +603,52 @@ function install(deps) {
353
603
  }
354
604
  return ok;
355
605
  });
356
- vsteps.push((data, stack) => {
606
+ vsteps.push((data, stack, rec) => {
357
607
  if (dataBits(data) !== T_OBJECT) return true;
358
- for (const [key, fn] of entries) {
359
- if (Object.hasOwn(data, key) && !fn.v(data, stack)) return false;
608
+ for (const [k, fn] of entries) {
609
+ if (Object.hasOwn(data, k) && !fn.v(data, stack, rec)) return false;
360
610
  }
361
611
  return true;
362
612
  });
363
613
  }
364
614
  if (P.propertyDependencies !== null) {
365
- const entries = P.propertyDependencies.map(([key, choices]) => {
615
+ const entries = P.propertyDependencies.map(([k, choices]) => {
366
616
  const m = new Map();
367
- for (const [value, v] of choices) m.set(value, compileNode(interp, v, base, memo));
368
- return [key, m];
617
+ for (const [value, v] of choices) m.set(value, inPlace(v, base));
618
+ return [k, m];
369
619
  });
370
- steps.push((data, errors, instancePath, schemaPath, stack) => {
620
+ steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
371
621
  if (dataBits(data) !== T_OBJECT) return true;
372
622
  let ok = true;
373
- for (const [key, choices] of entries) {
374
- if (!Object.hasOwn(data, key)) continue;
375
- const value = data[key];
623
+ for (const [k, choices] of entries) {
624
+ if (!Object.hasOwn(data, k)) continue;
625
+ const value = data[k];
376
626
  if (typeof value !== 'string') continue;
377
627
  const fn = choices.get(value);
378
628
  if (fn === undefined) continue;
379
- const branchPath = schemaPath + '/propertyDependencies/' + escapePointer(key) + '/' + escapePointer(value);
380
- if (!fn.c(data, errors, instancePath, branchPath, stack)) {
629
+ const branchPath = schemaPath + '/propertyDependencies/' + escapePointer(k) + '/' + escapePointer(value);
630
+ if (collect) {
631
+ const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
632
+ if (!fn.c(data, errors, instancePath, branchPath, stack, rec)) {
633
+ undo(rec, pl, n0, xl);
634
+ ok = false;
635
+ if (errors === NOERRORS) return false;
636
+ }
637
+ } else if (!fn.c(data, errors, instancePath, branchPath, stack)) {
381
638
  ok = false;
382
639
  if (errors === NOERRORS) return false;
383
640
  }
384
641
  }
385
642
  return ok;
386
643
  });
387
- vsteps.push((data, stack) => {
644
+ vsteps.push((data, stack, rec) => {
388
645
  if (dataBits(data) !== T_OBJECT) return true;
389
- for (const [key, choices] of entries) {
390
- if (!Object.hasOwn(data, key)) continue;
391
- const value = data[key];
646
+ for (const [k, choices] of entries) {
647
+ if (!Object.hasOwn(data, k)) continue;
648
+ const value = data[k];
392
649
  if (typeof value !== 'string') continue;
393
650
  const fn = choices.get(value);
394
- if (fn !== undefined && !fn.v(data, stack)) return false;
651
+ if (fn !== undefined && !fn.v(data, stack, rec)) return false;
395
652
  }
396
653
  return true;
397
654
  });
@@ -399,29 +656,40 @@ function install(deps) {
399
656
 
400
657
  // In-place applicators
401
658
  if (P.allOf !== null) {
402
- const fns = P.allOf.map((v) => compileNode(interp, v, base, memo));
403
- steps.push((data, errors, instancePath, schemaPath, stack) => {
659
+ const fns = P.allOf.map((v) => inPlace(v, base));
660
+ steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
404
661
  let ok = true;
405
662
  for (let i = 0; i < fns.length; i++) {
406
- if (!fns[i].c(data, errors, instancePath, schemaPath + '/allOf/' + i, stack)) {
663
+ if (collect) {
664
+ const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
665
+ if (!fns[i].c(data, errors, instancePath, schemaPath + '/allOf/' + i, stack, rec)) {
666
+ undo(rec, pl, n0, xl);
667
+ ok = false;
668
+ if (errors === NOERRORS) return false;
669
+ }
670
+ } else if (!fns[i].c(data, errors, instancePath, schemaPath + '/allOf/' + i, stack)) {
407
671
  ok = false;
408
672
  if (errors === NOERRORS) return false;
409
673
  }
410
674
  }
411
675
  return ok;
412
676
  });
413
- vsteps.push((data, stack) => {
414
- for (let i = 0; i < fns.length; i++) if (!fns[i].v(data, stack)) return false;
677
+ vsteps.push((data, stack, rec) => {
678
+ for (let i = 0; i < fns.length; i++) if (!fns[i].v(data, stack, rec)) return false;
415
679
  return true;
416
680
  });
417
681
  }
418
682
  if (P.anyOf !== null) {
419
- const fns = P.anyOf.map((v) => compileNode(interp, v, base, memo));
420
- steps.push((data, errors, instancePath, schemaPath, stack) => {
683
+ const fns = P.anyOf.map((v) => inPlace(v, base));
684
+ steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
421
685
  const scratch = errors === NOERRORS ? NOERRORS : [];
422
686
  let any = false;
423
687
  for (let i = 0; i < fns.length; i++) {
424
- if (fns[i].c(data, scratch, instancePath, schemaPath + '/anyOf/' + i, stack)) any = true;
688
+ if (collect) {
689
+ const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
690
+ if (fns[i].c(data, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, rec)) any = true;
691
+ else undo(rec, pl, n0, xl);
692
+ } else if (fns[i].c(data, scratch, instancePath, schemaPath + '/anyOf/' + i, stack)) any = true;
425
693
  }
426
694
  if (!any) {
427
695
  if (errors !== NOERRORS) {
@@ -432,20 +700,42 @@ function install(deps) {
432
700
  }
433
701
  return true;
434
702
  });
435
- vsteps.push((data, stack) => {
436
- for (let i = 0; i < fns.length; i++) if (fns[i].v(data, stack)) return true;
437
- return false;
438
- });
703
+ if (collect) {
704
+ // Every passing branch contributes annotations, so all of them run.
705
+ vsteps.push((data, stack, rec) => {
706
+ let any = false;
707
+ for (let i = 0; i < fns.length; i++) {
708
+ const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
709
+ if (fns[i].v(data, stack, rec)) any = true;
710
+ else undo(rec, pl, n0, xl);
711
+ }
712
+ return any;
713
+ });
714
+ } else {
715
+ vsteps.push((data, stack) => {
716
+ for (let i = 0; i < fns.length; i++) if (fns[i].v(data, stack)) return true;
717
+ return false;
718
+ });
719
+ }
439
720
  }
440
721
  if (P.oneOf !== null) {
441
- const fns = P.oneOf.map((v) => compileNode(interp, v, base, memo));
442
- steps.push((data, errors, instancePath, schemaPath, stack) => {
722
+ const fns = P.oneOf.map((v) => inPlace(v, base));
723
+ steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
443
724
  const scratch = errors === NOERRORS ? NOERRORS : [];
444
725
  let count = 0;
726
+ // With a record: the winner's annotations stay, every other branch's
727
+ // are undone, and a second winner undoes everything since the node
728
+ // fails and merges nothing.
729
+ const pl0 = collect ? pLen(rec) : 0, n00 = collect ? rec.n : 0, xl0 = collect ? xLen(rec) : 0;
445
730
  for (let i = 0; i < fns.length; i++) {
446
- if (fns[i].c(data, scratch, instancePath, schemaPath + '/oneOf/' + i, stack)) count++;
731
+ if (collect) {
732
+ const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
733
+ if (fns[i].c(data, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, rec)) count++;
734
+ else undo(rec, pl, n0, xl);
735
+ } else if (fns[i].c(data, scratch, instancePath, schemaPath + '/oneOf/' + i, stack)) count++;
447
736
  }
448
737
  if (count !== 1) {
738
+ if (collect) undo(rec, pl0, n00, xl0);
449
739
  if (errors !== NOERRORS) {
450
740
  if (count === 0) for (const e of scratch) errors.push(e);
451
741
  errors.push(err('oneOf', 'oneOf', instancePath, schemaPath + '/oneOf', { passingSchemas: count }, 'must match exactly one schema in oneOf'));
@@ -454,16 +744,28 @@ function install(deps) {
454
744
  }
455
745
  return true;
456
746
  });
457
- vsteps.push((data, stack) => {
458
- let count = 0;
459
- for (let i = 0; i < fns.length; i++) {
460
- if (fns[i].v(data, stack)) { count++; if (count > 1) return false; }
461
- }
462
- return count === 1;
463
- });
747
+ if (collect) {
748
+ vsteps.push((data, stack, rec) => {
749
+ let count = 0;
750
+ for (let i = 0; i < fns.length; i++) {
751
+ const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
752
+ if (fns[i].v(data, stack, rec)) { count++; if (count > 1) return false; }
753
+ else undo(rec, pl, n0, xl);
754
+ }
755
+ return count === 1;
756
+ });
757
+ } else {
758
+ vsteps.push((data, stack) => {
759
+ let count = 0;
760
+ for (let i = 0; i < fns.length; i++) {
761
+ if (fns[i].v(data, stack)) { count++; if (count > 1) return false; }
762
+ }
763
+ return count === 1;
764
+ });
765
+ }
464
766
  }
465
767
  if (P.not !== undefined) {
466
- const fn = compileNode(interp, P.not, base, memo);
768
+ const fn = child(P.not);
467
769
  steps.push((data, errors, instancePath, schemaPath, stack) => {
468
770
  if (fn.v(data, stack)) {
469
771
  if (errors !== NOERRORS) errors.push(err('not', 'not', instancePath, schemaPath + '/not', {}, 'must NOT be valid'));
@@ -474,36 +776,125 @@ function install(deps) {
474
776
  vsteps.push((data, stack) => !fn.v(data, stack));
475
777
  }
476
778
  if (P.if !== undefined) {
477
- const ifFn = compileNode(interp, P.if, base, memo);
478
- const thenFn = P.then !== undefined ? compileNode(interp, P.then, base, memo) : null;
479
- const elseFn = P.else !== undefined ? compileNode(interp, P.else, base, memo) : null;
480
- steps.push((data, errors, instancePath, schemaPath, stack) => {
481
- if (ifFn.v(data, stack)) {
482
- if (thenFn !== null) return thenFn.c(data, errors, instancePath, schemaPath + '/then', stack);
483
- } else if (elseFn !== null) {
484
- return elseFn.c(data, errors, instancePath, schemaPath + '/else', stack);
779
+ const ifFn = inPlace(P.if, base);
780
+ const thenFn = P.then !== undefined ? inPlace(P.then, base) : null;
781
+ const elseFn = P.else !== undefined ? inPlace(P.else, base) : null;
782
+ // The `if` branch keeps its annotations only when it passes; then and
783
+ // else fail the node when they fail, and the collect variant undoes
784
+ // theirs so the node's own unevaluated* sees what eval() sees.
785
+ steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
786
+ let pl = collect ? pLen(rec) : 0, n0 = collect ? rec.n : 0, xl = collect ? xLen(rec) : 0;
787
+ let ok;
788
+ if (ifFn.v(data, stack, rec)) {
789
+ if (thenFn === null) return true;
790
+ if (collect) { pl = pLen(rec); n0 = rec.n; xl = xLen(rec); }
791
+ ok = thenFn.c(data, errors, instancePath, schemaPath + '/then', stack, rec);
792
+ } else {
793
+ if (collect) undo(rec, pl, n0, xl);
794
+ if (elseFn === null) return true;
795
+ ok = elseFn.c(data, errors, instancePath, schemaPath + '/else', stack, rec);
485
796
  }
486
- return true;
797
+ if (!ok && collect) undo(rec, pl, n0, xl);
798
+ return ok;
487
799
  });
488
- vsteps.push((data, stack) => {
489
- if (ifFn.v(data, stack)) {
490
- if (thenFn !== null) return thenFn.v(data, stack);
491
- } else if (elseFn !== null) {
492
- return elseFn.v(data, stack);
800
+ vsteps.push((data, stack, rec) => {
801
+ const pl = collect ? pLen(rec) : 0, n0 = collect ? rec.n : 0, xl = collect ? xLen(rec) : 0;
802
+ if (ifFn.v(data, stack, rec)) {
803
+ if (thenFn !== null) return thenFn.v(data, stack, rec);
804
+ return true;
493
805
  }
806
+ if (collect) undo(rec, pl, n0, xl);
807
+ if (elseFn !== null) return elseFn.v(data, stack, rec);
494
808
  return true;
495
809
  });
496
810
  }
497
811
 
812
+ // unevaluated*: run last, against the annotations of everything above.
813
+ if (P.unevaluatedProperties !== undefined) {
814
+ const fn = child(P.unevaluatedProperties);
815
+ steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
816
+ if (dataBits(data) !== T_OBJECT) return true;
817
+ let ok = true;
818
+ const keys = keysOf(rec, data);
819
+ for (let k = 0; k < keys.length; k++) {
820
+ const key = keys[k];
821
+ if (hasProp(rec, key)) continue;
822
+ if (!fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/unevaluatedProperties', stack)) {
823
+ ok = false;
824
+ if (errors === NOERRORS) return false;
825
+ }
826
+ addProp(rec, key);
827
+ }
828
+ return ok;
829
+ });
830
+ if (fn === FALSE_PAIR) {
831
+ // Every key must already be evaluated; on success there is nothing
832
+ // new to record.
833
+ vsteps.push((data, stack, rec) => {
834
+ if (dataBits(data) !== T_OBJECT) return true;
835
+ const keys = keysOf(rec, data);
836
+ if (keys.length === 0) return true;
837
+ if (rec.props === null) return false;
838
+ for (let k = 0; k < keys.length; k++) if (!hasProp(rec, keys[k])) return false;
839
+ return true;
840
+ });
841
+ } else {
842
+ vsteps.push((data, stack, rec) => {
843
+ if (dataBits(data) !== T_OBJECT) return true;
844
+ const keys = keysOf(rec, data);
845
+ for (let k = 0; k < keys.length; k++) {
846
+ const key = keys[k];
847
+ if (hasProp(rec, key)) continue;
848
+ if (!fn.v(data[key], stack)) return false;
849
+ addProp(rec, key);
850
+ }
851
+ return true;
852
+ });
853
+ }
854
+ }
855
+ if (P.unevaluatedItems !== undefined) {
856
+ const fn = child(P.unevaluatedItems);
857
+ steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
858
+ if (dataBits(data) !== T_ARRAY) return true;
859
+ let ok = true;
860
+ for (let i = 0; i < data.length; i++) {
861
+ if (hasItem(rec, i)) continue;
862
+ if (!fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/unevaluatedItems', stack)) {
863
+ ok = false;
864
+ if (errors === NOERRORS) return false;
865
+ }
866
+ }
867
+ if (data.length > rec.n) rec.n = data.length;
868
+ return ok;
869
+ });
870
+ if (fn === FALSE_PAIR) {
871
+ vsteps.push((data, stack, rec) => {
872
+ if (dataBits(data) !== T_ARRAY) return true;
873
+ for (let i = rec.n; i < data.length; i++) if (!hasItem(rec, i)) return false;
874
+ return true;
875
+ });
876
+ } else {
877
+ vsteps.push((data, stack, rec) => {
878
+ if (dataBits(data) !== T_ARRAY) return true;
879
+ for (let i = 0; i < data.length; i++) {
880
+ if (hasItem(rec, i)) continue;
881
+ if (!fn.v(data[i], stack)) return false;
882
+ }
883
+ if (data.length > rec.n) rec.n = data.length;
884
+ return true;
885
+ });
886
+ }
887
+ }
888
+
498
889
  let cfn;
499
890
  if (steps.length === 0) cfn = TRUE_FN;
500
891
  else if (steps.length === 1) cfn = steps[0];
501
892
  else {
502
893
  const arr = steps;
503
- cfn = (d, e, ip, sp, st) => {
894
+ cfn = (d, e, ip, sp, st, rec) => {
504
895
  let ok = true;
505
896
  for (let i = 0; i < arr.length; i++) {
506
- if (!arr[i](d, e, ip, sp, st)) {
897
+ if (!arr[i](d, e, ip, sp, st, rec)) {
507
898
  if (e === NOERRORS) return false;
508
899
  ok = false;
509
900
  }
@@ -516,27 +907,60 @@ function install(deps) {
516
907
  else if (vsteps.length === 1) vfn = vsteps[0];
517
908
  else if (vsteps.length === 2) {
518
909
  const [a, b] = vsteps;
519
- vfn = (d, st) => a(d, st) && b(d, st);
910
+ vfn = (d, st, rec) => a(d, st, rec) && b(d, st, rec);
520
911
  } else {
521
912
  const arr = vsteps;
522
- vfn = (d, st) => {
523
- for (let i = 0; i < arr.length; i++) if (!arr[i](d, st)) return false;
913
+ vfn = (d, st, rec) => {
914
+ for (let i = 0; i < arr.length; i++) if (!arr[i](d, st, rec)) return false;
524
915
  return true;
525
916
  };
526
917
  }
918
+ if (P.hasUnevaluated) {
919
+ // This node reads annotations, so it owns a record that starts empty
920
+ // (siblings' annotations are not its to see) and hands it up on
921
+ // success when a caller reads it.
922
+ const inner = cfn;
923
+ const innerV = vfn;
924
+ if (ann) {
925
+ cfn = (d, e, ip, sp, st, rec) => {
926
+ const own = fresh();
927
+ const ok = inner(d, e, ip, sp, st, own);
928
+ if (ok) mergeRec(rec, own);
929
+ return ok;
930
+ };
931
+ vfn = (d, st, rec) => {
932
+ const own = fresh();
933
+ const ok = innerV(d, st, own);
934
+ if (ok) mergeRec(rec, own);
935
+ return ok;
936
+ };
937
+ } else {
938
+ cfn = (d, e, ip, sp, st) => inner(d, e, ip, sp, st, fresh());
939
+ vfn = (d, st) => innerV(d, st, fresh());
940
+ }
941
+ }
527
942
  box.c = cfn;
528
943
  box.v = vfn;
529
944
  pair.c = cfn;
530
945
  pair.v = vfn;
531
- perBase.set(base, pair);
946
+ pair.open = false;
532
947
  return pair;
533
948
  }
534
949
 
535
- // Entry point: returns a compiled root function or null when the schema is
536
- // outside the compiler's scope.
950
+ // Entry point: every schema the interpreter accepts compiles. The first
951
+ // pass carries the cycle guard and reports whether the graph has a cycle;
952
+ // an acyclic schema is compiled again without the guard, since eval()'s
953
+ // guard can never fire on it.
537
954
  function compileInterpreter(interp) {
538
- if (!compilable(interp, interp.rootNode, interp.state.rootBase, new Map())) return null;
539
- return compileNode(interp, interp.rootNode, interp.state.rootBase, new Map());
955
+ const root = interp.rootNode;
956
+ const base = interp.state.rootBase;
957
+ let ctx = { interp, memo: new Map(), guard: true, cyclic: false };
958
+ let pair = compileNode(ctx, root, base, [base], false);
959
+ if (!ctx.cyclic) {
960
+ ctx = { interp, memo: new Map(), guard: false, cyclic: false };
961
+ pair = compileNode(ctx, root, base, [base], false);
962
+ }
963
+ return { v: pair.v, c: pair.c, cyclic: ctx.guard };
540
964
  }
541
965
 
542
966
  return { compileInterpreter };