ata-validator 1.44.0 → 1.46.0

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.
@@ -10,21 +10,43 @@
10
10
  // forms answers a different question and costs a string per call.
11
11
  const DEQ_HELPER = "function _deq(a,b){if(a===b)return true;if(a===null||b===null||typeof a!=='object'||typeof b!=='object')return false;var aa=Array.isArray(a);if(aa!==Array.isArray(b))return false;var i;if(aa){if(a.length!==b.length)return false;for(i=0;i<a.length;i++)if(!_deq(a[i],b[i]))return false;return true}var ka=Object.keys(a);if(ka.length!==Object.keys(b).length)return false;for(i=0;i<ka.length;i++){var k=ka[i];if(!Object.prototype.hasOwnProperty.call(b,k)||!_deq(a[k],b[k]))return false}return true}"
12
12
 
13
- // uniqueItems helpers for the boolean generator. `_uq` answers whether every
14
- // item is distinct: short arrays compare pairwise and allocate nothing,
15
- // longer ones use a Set of primitives and fall back to sorted-key canonical
16
- // strings only once an object actually appears.
17
- const UQ_HELPERS = "function _cn(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn(x[k])}).join(',')+'}'}" +
18
- "function _uq(a){var n=a.length,i,k;if(n<2)return true;if(n<=12){for(i=1;i<n;i++)for(k=0;k<i;k++)if(_deq(a[i],a[k]))return false;return true}var s=new Set();for(i=0;i<n;i++){var x=a[i];if(x!==null&&typeof x==='object')break;if(s.has(x))return false;s.add(x)}if(i===n)return true;s=new Set();for(i=0;i<n;i++){var c=_cn(a[i]);if(s.has(c))return false;s.add(c)}return true}"
13
+ // uniqueItems helpers, emitted from the functions the interpreted engine
14
+ // calls (lib/unique-items.js): `_uq` answers whether every item is distinct.
15
+ // Loaded at the first schema with uniqueItems, not on every cold start.
16
+ // Comment lines and indentation are dropped: the text lands in every
17
+ // generated function and ahead-of-time module that checks uniqueItems.
18
+ let _uqSources = null
19
+ function uqSources () {
20
+ if (_uqSources === null) {
21
+ const u = require('./unique-items')
22
+ const compact = (fn) => String(fn).replace(/\n\s*\/\/[^\n]*/g, '').replace(/\n\s*/g, '\n')
23
+ _uqSources = { uq: compact(u._uqh) + compact(u._uq), uqp: 'var _uqpI=0;' + compact(u._uqp) }
24
+ }
25
+ return _uqSources
26
+ }
19
27
 
20
28
  // Where a generator hoists a declaration so it runs once per compiled
21
29
  // function rather than once per call. The boolean generator has a preamble
22
30
  // for exactly this; the error and combined generators emit `helperCode` into
23
31
  // their function source. The boolean generator's `helperCode` is only
24
32
  // scanned for regex entries and must not be used here.
33
+ // The helpers the one-pass generator takes from lib/combined-runtime.js as
34
+ // closure values instead of writing into each program, by the hoistOnce key
35
+ // that would have written them.
36
+ const SHARED_RT = {
37
+ _deqHoisted: ['_deq'], _peHoisted: ['_pe'],
38
+ _own_PN: ['_PN', '_OP', '_gp'], _own_hop: ['_hop'], _own_h: ['_h'], _own_ok: ['_ok'], _own_hall: ['_hall'],
39
+ }
40
+ let _rt = null
41
+ function sharedRt () { return _rt || (_rt = require('./combined-runtime')) }
42
+ function bindRt (ctx, names) {
43
+ const rt = sharedRt()
44
+ for (const n of names) { ctx.closureVars.push(n); ctx.closureVals.push(rt[n]) }
45
+ }
25
46
  function hoistOnce(ctx, key, code) {
26
47
  if (ctx[key]) return
27
48
  ctx[key] = true
49
+ if (ctx.sharedRuntime && SHARED_RT[key] !== undefined) { bindRt(ctx, SHARED_RT[key]); return }
28
50
  // Recorded as shared: the text is a constant and the name it declares is
29
51
  // fixed, so several compiled schemas in one bundle can use one copy. The
30
52
  // rest of the preamble cannot move, because emitConstant and the generated
@@ -97,7 +119,7 @@ function emitPtrEsc(ctx) {
97
119
  // `_uq` compares items with `_deq`, so it can never be hoisted alone.
98
120
  function emitUq(ctx) {
99
121
  emitDeq(ctx)
100
- hoistOnce(ctx, '_uqHoisted', UQ_HELPERS)
122
+ hoistOnce(ctx, '_uqHoisted', uqSources().uq)
101
123
  return '_uq'
102
124
  }
103
125
 
@@ -107,15 +129,11 @@ function emitUq(ctx) {
107
129
  // a later equal, paired with its first later equal. That is the pair the
108
130
  // interpreted engine reports, and the generators used to report a different
109
131
  // one (the earliest repeat, paired with what it repeats), so the same document
110
- // named a different pair depending on the engine that answered. Short arrays
111
- // compare pairwise and allocate nothing; longer ones key a Map by value, or by
112
- // canonical string once an object appears. Building a canonical string for
113
- // every item on every call, as the generators did, made reading a uniqueItems
114
- // error up to ten times the cost of the verdict.
115
- const UQP_HELPER = "var _uqpI=0;function _uqp(a){var n=a.length,i,k;if(n<2)return -1;if(n<=12){for(i=0;i<n-1;i++)for(k=i+1;k<n;k++)if(_deq(a[i],a[k])){_uqpI=i;return k}return -1}var prim=true;for(i=0;i<n;i++){var x=a[i];if(x!==null&&typeof x==='object'){prim=false;break}}var m=new Map(),bi=-1,bj=-1;for(i=0;i<n;i++){var c=prim?a[i]:_cn(a[i]);var p=m.get(c);if(p===undefined)m.set(c,i);else if(p>=0){if(bi<0||p<bi){bi=p;bj=i}m.set(c,-1)}}if(bi<0)return -1;_uqpI=bi;return bj}"
132
+ // named a different pair depending on the engine that answered. See
133
+ // lib/unique-items.js for how it finds the pair.
116
134
  function emitUqp(ctx) {
117
135
  emitUq(ctx)
118
- hoistOnce(ctx, '_uqpHoisted', UQP_HELPER)
136
+ hoistOnce(ctx, '_uqpHoisted', uqSources().uqp)
119
137
  return '_uqp'
120
138
  }
121
139
 
@@ -165,7 +183,7 @@ function enumCondition(ctx, vals, v) {
165
183
 
166
184
 
167
185
  const { codeFor } = require('./error-codes')
168
- const { ordinalFor } = require('./schema-order')
186
+ const { ordinalFor, rankFor } = require('./schema-order')
169
187
  const { compileSafe, patternIsSafe } = require('./safe-regex')
170
188
  // Whether a pattern goes to the linear-time engine. A pattern the platform
171
189
  // RegExp also runs in linear time (nativeIsLinear) takes the RegExp: it answers
@@ -1033,6 +1051,11 @@ function withPlain (schema, v, lines, ctx, run) {
1033
1051
  return run()
1034
1052
  }
1035
1053
  if (!ctx.plainOf) ctx.plainOf = new Map()
1054
+ // A flag an enclosing node already declared for this same value stands:
1055
+ // the straight-line unevaluatedProperties code (inlineUnevalBlock) declares
1056
+ // one for the object and writes every branch's verdict under it, where each
1057
+ // branch used to read the prototype again.
1058
+ if (ctx.plainReuse === v && ctx.plainOf.get(v) !== undefined) return run()
1036
1059
  const name = '_pk' + ctx.varCounter++
1037
1060
  const at = lines.length
1038
1061
  const decl = `const ${name}=typeof ${v}==='object'&&${v}!==null&&${protoIs(ctx, v)}`
@@ -2257,12 +2280,165 @@ function refsAllPlainLocal (root, text) {
2257
2280
  // character of a schema without references, so a short one is known not to
2258
2281
  // reach EXPANDED_SHARE without walking it again at every compile.
2259
2282
  const _refFreeLength = new WeakMap()
2283
+ // `$dynamicRef` resolved at compile time, the way the interpreted engine
2284
+ // resolves it: the target depends on the dynamic scope, the resources entered
2285
+ // on the way to the reference, so the schema is unrolled into one copy per
2286
+ // (node, scope) pair and every reference in a copy becomes a plain local
2287
+ // reference to the copy its scope selects. Two scopes are the same when every
2288
+ // dynamic anchor name resolves to the same resource in both, which is what
2289
+ // the lookup reads; keyed that way, a schema whose anchors all sit at the
2290
+ // root (the 2020-12 meta-schema) gets one copy per node, not one per path.
2291
+ // Nested `$id`, `$anchor` and `$dynamicAnchor` are dropped from the copies,
2292
+ // since nothing refers by them any more. Any reference that does not resolve
2293
+ // makes the whole rewrite give up (null) and the schema goes to the
2294
+ // interpreted engine, which reports it where it is reached: an unrolled copy
2295
+ // must never leave a reference out, because a reference that is not written
2296
+ // checks nothing. 2020-12 bookending is assumed: the v1 dialect, which drops
2297
+ // it, is routed to the interpreted engine before code generation is asked.
2298
+ // Dropped from every copy: identifiers, which nothing refers by any more,
2299
+ // and definition containers, whose members are reached only through the
2300
+ // references that name them and are copied where those land. A definition
2301
+ // nothing uses would otherwise sit in the output and could cost it the
2302
+ // generated code.
2303
+ const DYN_DROP = new Set(['$id', '$anchor', '$dynamicAnchor', '$defs', 'definitions'])
2304
+ const BOOL_TRUE = { bool: true }
2305
+ const BOOL_FALSE = { bool: false }
2306
+ function expandDynamicScopes(root, schemaMap) {
2307
+ if (root === null || typeof root !== 'object' || Array.isArray(root)) return null
2308
+ const { indexSchemas, resolveRef, splitFragment, resolveUri } = require('./interpreter')._refInternals
2309
+ let state
2310
+ try { state = indexSchemas(root, schemaMap && schemaMap.size ? schemaMap : null) } catch { return null }
2311
+ const anchorNames = []
2312
+ for (const m of state.dynamicAnchors.values()) for (const name of m.keys()) if (!anchorNames.includes(name)) anchorNames.push(name)
2313
+ anchorNames.sort()
2314
+ // The scope signature: for each anchor name, the first resource in the
2315
+ // chain that declares it.
2316
+ const signature = (chain) => {
2317
+ let sig = ''
2318
+ for (const name of anchorNames) {
2319
+ let hit = ''
2320
+ for (let i = 0; i < chain.length; i++) { const dyn = state.dynamicAnchors.get(chain[i]); if (dyn && dyn.has(name)) { hit = chain[i]; break } }
2321
+ sig += name + '=' + hit + ';'
2322
+ }
2323
+ return sig
2324
+ }
2325
+ const resolveDynamic = (ref, base, chain) => {
2326
+ let { node, base: refBase } = resolveRef(ref, base, state)
2327
+ const [, fragment] = splitFragment(resolveUri(base, ref))
2328
+ if (fragment && !fragment.startsWith('/')) {
2329
+ const initialDyn = state.dynamicAnchors.get(refBase)
2330
+ const bookended = node !== undefined && initialDyn !== undefined && initialDyn.get(fragment) === node
2331
+ if (bookended) {
2332
+ for (let i = 0; i < chain.length; i++) {
2333
+ const dyn = state.dynamicAnchors.get(chain[i])
2334
+ if (dyn && dyn.has(fragment)) { node = dyn.get(fragment); refBase = chain[i]; break }
2335
+ }
2336
+ }
2337
+ }
2338
+ return { node, base: refBase }
2339
+ }
2340
+ const copies = new Map() // node (or a boolean sentinel) -> Map(signature -> entry)
2341
+ const order = []
2342
+ let fail = false
2343
+ // The entry for a node in a scope: its copy, made on the first ask, and
2344
+ // later its name. A `$ref` slot holds the entry until names are given.
2345
+ const entryFor = (node, base, chain) => {
2346
+ if (node === true || node === false) {
2347
+ const key = node ? BOOL_TRUE : BOOL_FALSE
2348
+ let bySig = copies.get(key)
2349
+ if (bySig === undefined) { bySig = new Map(); copies.set(key, bySig) }
2350
+ let e = bySig.get('')
2351
+ if (e === undefined) { e = { copy: node, sig: '', name: null, referenced: false }; bySig.set('', e); order.push(e) }
2352
+ return e
2353
+ }
2354
+ if (node === null || typeof node !== 'object') { fail = true; return { copy: node, sig: '', name: null } }
2355
+ if (state.nodeBase.has(node) && state.nodeBase.get(node) !== base) base = state.nodeBase.get(node)
2356
+ if (!chain.includes(base)) chain = chain.concat(base)
2357
+ const sig = signature(chain)
2358
+ let bySig = copies.get(node)
2359
+ if (bySig === undefined) { bySig = new Map(); copies.set(node, bySig) }
2360
+ const hit = bySig.get(sig)
2361
+ if (hit !== undefined) return hit
2362
+ const e = { copy: Array.isArray(node) ? [] : {}, sig, name: null, referenced: false }
2363
+ bySig.set(sig, e)
2364
+ order.push(e)
2365
+ if (Array.isArray(node)) { for (const x of node) e.copy.push(copyValue(x, base, chain)); return e }
2366
+ const out = e.copy
2367
+ // Only a schema node reads `$ref` as a reference and carries an `$id`; in
2368
+ // a `properties` map those are property names. The index registers
2369
+ // every schema node, so that is the test.
2370
+ const isSchema = state.nodeBase.has(node)
2371
+ for (const k of Object.keys(node)) {
2372
+ if (fail) return e
2373
+ const v = node[k]
2374
+ if (isSchema && (k === '$ref' || k === '$dynamicRef')) {
2375
+ if (typeof v !== 'string') { fail = true; return e }
2376
+ const t = k === '$ref' ? resolveRef(v, base, state) : resolveDynamic(v, base, chain)
2377
+ if (t.node === undefined || t.node === null || (typeof t.node !== 'object' && typeof t.node !== 'boolean') || Array.isArray(t.node)) { fail = true; return e }
2378
+ const target = entryFor(t.node, t.base, chain)
2379
+ target.referenced = true
2380
+ out.$ref = target
2381
+ continue
2382
+ }
2383
+ if (isSchema && DYN_DROP.has(k) && !(k === '$id' && node === root)) continue
2384
+ setOwn(out, k, SKIP_VALUE_KEYS.has(k) ? v : copyValue(v, base, chain))
2385
+ }
2386
+ return e
2387
+ }
2388
+ // A value under a keyword: a schema or a list is copied, anything else
2389
+ // (a type name, a limit, a required name) is kept as it is.
2390
+ const copyValue = (v, base, chain) => (v !== null && typeof v === 'object') || typeof v === 'boolean' ? entryFor(v, base, chain).copy : v
2391
+ const rootBase = state.nodeBase.get(root)
2392
+ const rootEntry = entryFor(root, rootBase, [rootBase])
2393
+ if (fail) return null
2394
+ const rootCopy = rootEntry.copy
2395
+ const defsKey = root.$defs && typeof root.$defs === 'object' ? '$defs' : (root.definitions && typeof root.definitions === 'object' ? 'definitions' : '$defs')
2396
+ const existing = {}
2397
+ let n = 0
2398
+ // Only a copy something refers to becomes a definition; the rest sit
2399
+ // where they were copied.
2400
+ for (const e of order) {
2401
+ if (e === rootEntry) { e.name = '#'; continue }
2402
+ if (!e.referenced) continue
2403
+ let name
2404
+ do { name = '__ata_dyn_' + n++ } while (Object.prototype.hasOwnProperty.call(existing, name))
2405
+ e.name = name
2406
+ }
2407
+ // Write the names into the `$ref` slots.
2408
+ const seen = new Set()
2409
+ const fix = (o) => {
2410
+ if (o === null || typeof o !== 'object' || seen.has(o)) return
2411
+ seen.add(o)
2412
+ if (Array.isArray(o)) { for (const x of o) fix(x); return }
2413
+ for (const k of Object.keys(o)) {
2414
+ const v = o[k]
2415
+ if (k === '$ref' && v !== null && typeof v === 'object' && !Array.isArray(v) && typeof v.sig === 'string') {
2416
+ o.$ref = v.name === '#' ? '#' : '#/' + defsKey + '/' + v.name
2417
+ continue
2418
+ }
2419
+ if (!SKIP_VALUE_KEYS.has(k)) fix(v)
2420
+ }
2421
+ }
2422
+ fix(rootCopy)
2423
+ const defs = {}
2424
+ for (const e of order) {
2425
+ if (e === rootEntry || !e.referenced) continue
2426
+ if (typeof e.copy === 'object') fix(e.copy)
2427
+ defs[e.name] = e.copy
2428
+ }
2429
+ rootCopy[defsKey] = defs
2430
+ return rootCopy
2431
+ }
2432
+
2260
2433
  function normalizeRefsWalk(root, schemaMap) {
2261
2434
  const text = JSON.stringify(root)
2262
2435
  const hasRef = text.includes('"$ref"')
2263
2436
  _refFreeLength.set(root, hasRef ? -1 : text.length)
2437
+ if (/"\$(?:recursiveRef|recursiveAnchor)"/.test(text)) return null
2438
+ // A schema with dynamic references is unrolled by dynamic scope first; the
2439
+ // result holds plain local references only.
2440
+ if (/"\$(?:dynamicRef|dynamicAnchor)"/.test(text)) return expandDynamicScopes(root, schemaMap)
2264
2441
  if (!hasRef) return root
2265
- if (/"\$(?:dynamicRef|recursiveRef|dynamicAnchor|recursiveAnchor)"/.test(text)) return null
2266
2442
  if (refsAllPlainLocal(root, text)) return root
2267
2443
  const { indexSchemas, resolveRef } = require('./interpreter')._refInternals
2268
2444
  const state = indexSchemas(root, schemaMap && schemaMap.size ? schemaMap : null)
@@ -2273,14 +2449,16 @@ function normalizeRefsWalk(root, schemaMap) {
2273
2449
  const refTarget = new Map() // reference site node -> target node
2274
2450
  const visited = new Set()
2275
2451
  let fail = false
2452
+ let dynamic = false
2276
2453
  const visit = (node) => {
2277
2454
  if (fail || node === null || typeof node !== 'object' || visited.has(node)) return
2278
2455
  visited.add(node)
2279
2456
  if (Array.isArray(node)) { for (const n of node) visit(n); return }
2280
2457
  if (node !== root && typeof node.$schema === 'string' && rootDialect !== null && node.$schema.replace(/#$/, '') !== rootDialect) { fail = true; return }
2281
- // A dynamic reference reached through another document means the same as
2282
- // one in the root: its target depends on the resources at run time.
2283
- if (node.$dynamicRef !== undefined || node.$dynamicAnchor !== undefined || node.$recursiveRef !== undefined || node.$recursiveAnchor !== undefined) { fail = true; return }
2458
+ // A dynamic reference reached through another document is unrolled by
2459
+ // scope the same way one in the root is (expandDynamicScopes, below).
2460
+ if (node.$recursiveRef !== undefined || node.$recursiveAnchor !== undefined) { fail = true; return }
2461
+ if (node.$dynamicRef !== undefined || node.$dynamicAnchor !== undefined) { dynamic = true; return }
2284
2462
  if (typeof node.$ref === 'string' && state.nodeBase.has(node)) {
2285
2463
  const base = state.nodeBase.get(node)
2286
2464
  const ref = node.$ref
@@ -2300,6 +2478,7 @@ function normalizeRefsWalk(root, schemaMap) {
2300
2478
  }
2301
2479
  visit(root)
2302
2480
  if (fail) return null
2481
+ if (dynamic) return expandDynamicScopes(root, schemaMap)
2303
2482
  if (refTarget.size === 0) return root
2304
2483
  const defsKey = root.$defs && typeof root.$defs === 'object' ? '$defs' : (root.definitions && typeof root.definitions === 'object' ? 'definitions' : '$defs')
2305
2484
  const existing = root[defsKey] && typeof root[defsKey] === 'object' ? root[defsKey] : {}
@@ -3416,11 +3595,20 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3416
3595
  // once, there. Only that model does: with unevaluatedProperties true or a
3417
3596
  // schema, skipping it here left anyOf unchecked and accepted a document no
3418
3597
  // branch matched.
3419
- if (schema.anyOf && !unevalChecksAnyOf(schema, ctx)) genAnyOfNode(schema, v, lines, ctx, knownType)
3598
+ // Where unevaluatedProperties builds its evaluated set at run time, the
3599
+ // annotation pass already decides each anyOf and oneOf branch, so the
3600
+ // keywords' own blocks would decide them a second time: on a schema of
3601
+ // nested references that doubled the cost of accepting a document. Their
3602
+ // blocks go to the `else` of that pass instead, for a value that is not an
3603
+ // object, which the pass does not look at and which they must still judge.
3604
+ const nonObjApplicators = schema.unevaluatedProperties !== undefined && schema.additionalProperties === undefined &&
3605
+ (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) && unevalBuiltAtRuntime(schema, ctx) ? [] : null
3606
+ const applicatorLines = nonObjApplicators || lines
3607
+ if (schema.anyOf && !unevalChecksAnyOf(schema, ctx)) genAnyOfNode(schema, v, applicatorLines, ctx, knownType)
3420
3608
 
3421
3609
  // oneOf — branch fns hoisted to factory scope when safe (no recursion/ref).
3422
3610
  // Falls back to inline closures if any branch touches recursive validation.
3423
- if (schema.oneOf) genOneOfNode(schema, v, lines, ctx, knownType)
3611
+ if (schema.oneOf) genOneOfNode(schema, v, applicatorLines, ctx, knownType)
3424
3612
 
3425
3613
  // not
3426
3614
  if (schema.not !== undefined) genNotNode(schema, v, lines, ctx, knownType)
@@ -3429,7 +3617,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3429
3617
  if (schema.if !== undefined) genIfNode(schema, v, lines, ctx, knownType)
3430
3618
 
3431
3619
  // unevaluatedProperties
3432
- if (schema.unevaluatedProperties !== undefined) genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount)
3620
+ if (schema.unevaluatedProperties !== undefined) genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount, nonObjApplicators)
3433
3621
 
3434
3622
  // unevaluatedItems
3435
3623
  if (schema.unevaluatedItems !== undefined) genUnevaluatedItemsNode(schema, v, lines, ctx, knownType, isArr)
@@ -3481,6 +3669,10 @@ function genUniqueItemsNode(schema, v, lines, ctx, knownType, hoisted, isArr, ty
3481
3669
  // up. The previous form built the canonical closure and a Set on
3482
3670
  // every call, even for [1, 2, 3].
3483
3671
  inner = `if(!${emitUq(ctx)}(${v}))return false`
3672
+ } else if (ctx.helperCode) {
3673
+ // Inside the error or combined function, whose helper code is emitted
3674
+ // into its source, the same hoisted helper.
3675
+ inner = `if(!${emitUq(ctx)}(${v}))return false`
3484
3676
  } else {
3485
3677
  inner = `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);if(_s${si}.has(_k))return false;_s${si}.add(_k)}`
3486
3678
  }
@@ -3755,7 +3947,9 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
3755
3947
  // whatever function is compiling, carrying this def's key set.
3756
3948
  const outerDeferred = ctx.deferredChecks
3757
3949
  ctx.deferredChecks = null
3758
- genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)
3950
+ const outerReuse = ctx.plainReuse
3951
+ ctx.plainReuse = undefined
3952
+ try { genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx) } finally { ctx.plainReuse = outerReuse }
3759
3953
  if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
3760
3954
  ctx.deferredChecks = outerDeferred
3761
3955
  // The def is hoisted above the entry function, where the recursive
@@ -3790,10 +3984,13 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
3790
3984
  const outerDeferred = ctx.deferredChecks
3791
3985
  ctx.deferredChecks = null
3792
3986
  ctx.refStack.add(schema.$ref)
3987
+ const outerReuse = ctx.plainReuse
3988
+ ctx.plainReuse = undefined
3793
3989
  try {
3794
3990
  genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)
3795
3991
  } finally {
3796
3992
  ctx.refStack.delete(schema.$ref)
3993
+ ctx.plainReuse = outerReuse
3797
3994
  }
3798
3995
  if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
3799
3996
  ctx.deferredChecks = outerDeferred
@@ -4000,12 +4197,15 @@ function unevalBuiltAtRuntime(schema, ctx, r) {
4000
4197
  return r.dynamic && (r.allProps || !unevalPropsModeled(schema, ctx.schemaMap))
4001
4198
  }
4002
4199
 
4003
- function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount) {
4200
+ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount, nonObjApplicators) {
4004
4201
  const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
4005
4202
  if (unevalBuiltAtRuntime(schema, ctx, evalResult)) {
4006
- genUnevalDynamicVerdict(schema, v, lines, ctx, isObj)
4203
+ genUnevalDynamicVerdict(schema, v, lines, ctx, isObj, nonObjApplicators)
4007
4204
  return
4008
4205
  }
4206
+ // genCodeNode moved the anyOf and oneOf blocks here only for the run-time
4207
+ // form; any other outcome would leave them unwritten.
4208
+ if (nonObjApplicators !== null && nonObjApplicators !== undefined) throw DECLINE
4009
4209
 
4010
4210
  if (evalResult.allProps || schema.unevaluatedProperties === true) {
4011
4211
  // All props evaluated or unevaluatedProperties:true — no-op
@@ -4947,7 +5147,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4947
5147
  if (hasUnresolvableRef(schema, eRootDefs, eAnchors, schemaMap, new Set())) return null
4948
5148
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
4949
5149
 
4950
- const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, shared: [], refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootSchema: inputSchema, userFormats: userFormats || null,
5150
+ const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, shared: [], refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootE: schema, rootSchema: inputSchema, userFormats: userFormats || null,
4951
5151
  // Custom format checkers referenced by the body, bound as
4952
5152
  // closure parameters below the way the other entry points do.
4953
5153
  closureVars: [], closureVals: [],
@@ -5142,6 +5342,224 @@ function holdsRef (node, seen = new Set()) {
5142
5342
  return false
5143
5343
  }
5144
5344
 
5345
+ // Every `$ref` under `node`, outside the values of enum, const, default and
5346
+ // examples. Null when one is not a plain pointer into the root's definitions
5347
+ // (after prepareForCodegen those are what remain, besides `#` itself) or
5348
+ // when the node carries a scope keyword of its own, which would not mean the
5349
+ // same thing under a wrapper.
5350
+ const BRANCH_SCOPE_KEYS = new Set(['$id', '$anchor', '$dynamicAnchor', '$dynamicRef', '$defs', 'definitions', '$schema'])
5351
+ function branchRefsLocal (node, defsKey, seen) {
5352
+ if (node === null || typeof node !== 'object') return true
5353
+ if (seen.has(node)) return true
5354
+ seen.add(node)
5355
+ if (Array.isArray(node)) { for (const x of node) if (!branchRefsLocal(x, defsKey, seen)) return false; return true }
5356
+ for (const k of Object.keys(node)) {
5357
+ if (SKIP_VALUE_KEYS.has(k)) continue
5358
+ if (BRANCH_SCOPE_KEYS.has(k)) return false
5359
+ const v = node[k]
5360
+ if (k === '$ref') { if (typeof v !== 'string' || !v.startsWith('#/' + defsKey + '/')) return false; continue }
5361
+ if (!branchRefsLocal(v, defsKey, seen)) return false
5362
+ }
5363
+ return true
5364
+ }
5365
+ // One verdict function per branch, compiled on its own with the root's
5366
+ // definitions under it, or null when a branch cannot be wrapped that way
5367
+ // (then the site keeps its error functions for the decision). The
5368
+ // definitions themselves are checked once per root.
5369
+ const _defsLocal = new WeakMap()
5370
+ function compileBranchVerdicts (branches, ctx) {
5371
+ const root = ctx.rootC
5372
+ if (root === null || typeof root !== 'object') return null
5373
+ const defsKey = root.$defs && typeof root.$defs === 'object' ? '$defs' : (root.definitions && typeof root.definitions === 'object' ? 'definitions' : null)
5374
+ if (defsKey !== null) {
5375
+ let ok = _defsLocal.get(root)
5376
+ if (ok === undefined) { ok = branchRefsLocal(root[defsKey], defsKey, new Set()); _defsLocal.set(root, ok) }
5377
+ if (!ok) return null
5378
+ }
5379
+ const out = []
5380
+ for (const sub of branches) {
5381
+ if (sub === true) { out.push(() => true); continue }
5382
+ if (sub === false) { out.push(() => false); continue }
5383
+ if (sub === null || typeof sub !== 'object' || Array.isArray(sub)) return null
5384
+ if (!branchRefsLocal(sub, defsKey === null ? '$defs' : defsKey, new Set())) return null
5385
+ const wrapped = Object.assign({}, sub)
5386
+ if (defsKey !== null) wrapped[defsKey] = root[defsKey]
5387
+ if (typeof ctx.rootSchema === 'object' && ctx.rootSchema !== null && typeof ctx.rootSchema.$schema === 'string') wrapped.$schema = ctx.rootSchema.$schema
5388
+ let fn = null
5389
+ try { fn = compileToJSCodegen(wrapped, ctx.schemaMap, ctx.userFormats) } catch (e) { if (e !== DECLINE) throw e }
5390
+ if (typeof fn !== 'function') return null
5391
+ out.push(fn)
5392
+ }
5393
+ return out
5394
+ }
5395
+
5396
+ // A verdict for one subtree of the combined function, as a function in the
5397
+ // preamble, so the collecting code of that subtree runs only for a value the
5398
+ // verdict rejects. The collecting code is slower than the verdict on an
5399
+ // accepted value (it keeps paths, builds nothing it can avoid but tests more
5400
+ // than the verdict's cheapest-first order), so without this the whole
5401
+ // function was put behind one verdict of the whole document (`_VF`), and a
5402
+ // rejected document was validated twice in full. With a guard at every
5403
+ // array element and every object-valued property, an accepted value costs
5404
+ // its verdict and a rejected one costs its verdict plus the collection of
5405
+ // the subtree that failed. Only in the combined function, the runtime path;
5406
+ // null for a subtree whose verdict declines, is a leaf, or is too large.
5407
+ const GUARD_KEYS = ['oneOf', 'anyOf', 'allOf', '$ref', 'items', 'prefixItems', 'properties', 'patternProperties', 'additionalProperties', 'not', 'if', 'contains', 'dependentSchemas', 'dependencies', 'propertyNames', 'unevaluatedProperties', 'unevaluatedItems']
5408
+ const GUARD_MAX = 16 * 1024
5409
+ const GUARD_BUDGET = 512 * 1024
5410
+ // Whether a schema node is worth a guard of its own: it applies something
5411
+ // beyond leaf checks. A node whose children are themselves worth guarding is
5412
+ // not guarded; the guards go on the children. Guarding both ran the parent's
5413
+ // verdict in full and then, when it failed, every child's verdict again: on a
5414
+ // list of 1000 users with one bad one that was two verdicts of the list
5415
+ // before the one user was collected.
5416
+ const weighty = (n) => n !== null && typeof n === 'object' && !Array.isArray(n) && GUARD_KEYS.some((k) => n[k] !== undefined)
5417
+ function childrenWeighty (sub) {
5418
+ if (weighty(sub.items)) return true
5419
+ if (Array.isArray(sub.prefixItems) && sub.prefixItems.some(weighty)) return true
5420
+ if (weighty(sub.additionalProperties)) return true
5421
+ if (sub.properties && typeof sub.properties === 'object') for (const k of Object.keys(sub.properties)) if (weighty(sub.properties[k])) return true
5422
+ if (sub.patternProperties && typeof sub.patternProperties === 'object') for (const k of Object.keys(sub.patternProperties)) if (weighty(sub.patternProperties[k])) return true
5423
+ return false
5424
+ }
5425
+ // Below this much verdict source the guard is written into the function
5426
+ // itself as a labelled block (`gN:{...break gN...}`), not as a function it
5427
+ // calls: in the first thousands of calls of a process, before the optimizing
5428
+ // compiler has inlined anything, each call is paid in full, and a schema
5429
+ // with six small guards paid six. Above it the guard stays a function, so
5430
+ // one large subtree does not swell the function around it.
5431
+ const GUARD_INLINE_MAX = 2048
5432
+ function subtreeGuard (sub, ctx, gv) {
5433
+ if (!ctx.inCombined || !weighty(sub) || childrenWeighty(sub)) return null
5434
+ if ((ctx.guardChars || 0) >= GUARD_BUDGET) return null
5435
+ // The verdict generator registers the definition functions it writes under
5436
+ // `defFns`, and so does the error generator for the branch functions of
5437
+ // this same program: shared, the verdict found the error generator's
5438
+ // `_defE` function under the definition's name and called it with one
5439
+ // argument. The guards keep a registry of their own.
5440
+ if (!ctx.guardDefFns) ctx.guardDefFns = new Map()
5441
+ const saved = snapshotCtx(ctx)
5442
+ const outerDefFns = ctx.defFns
5443
+ const outerShare = ctx.shareDefs
5444
+ ctx.defFns = ctx.guardDefFns
5445
+ // Definitions referenced from several places are written once, as
5446
+ // functions in the preamble (the verdict generator's shared mode), not
5447
+ // expanded into every guard that reaches them: expanded, the guards of a
5448
+ // 25-definition API schema came to 55 KB of source in a 133 KB function,
5449
+ // and a function that size reaches the optimizing compiler late.
5450
+ ctx.shareDefs = true
5451
+ try {
5452
+ const bl = []
5453
+ try { nestedGenCode(sub, gv, bl, ctx) } finally { ctx.defFns = outerDefFns; ctx.shareDefs = outerShare }
5454
+ if (bl.length === 0) { restoreCtx(ctx, saved); return null }
5455
+ const body = bl.join(';')
5456
+ // The verdict generator's recursion names (`_validate`, the cycle guard
5457
+ // state) are not this function's; a subtree that reaches them stays
5458
+ // unguarded.
5459
+ const added = ctx.preamble.slice(saved.get('preamble').n).join(';')
5460
+ if (body.length > GUARD_MAX || body.includes(SP_PLACEHOLDER) || /\b(?:_validate|_stk|_sd|_sg|_CYC)\b/.test(body + added)) { restoreCtx(ctx, saved); return null }
5461
+ ctx.guardChars = (ctx.guardChars || 0) + body.length
5462
+ if (body.length <= GUARD_INLINE_MAX) return { body }
5463
+ const name = `_vg${ctx.varCounter++}`
5464
+ ctx.helperCode.push(`const ${name}=function(${gv}){${body};return true}`)
5465
+ return { name }
5466
+ } catch (e) {
5467
+ if (e !== DECLINE) throw e
5468
+ restoreCtx(ctx, saved)
5469
+ return null
5470
+ }
5471
+ }
5472
+ // The collecting code for `sub` at `v`, behind its guard when there is one.
5473
+ function genGuardedC (sub, v, pathExpr, lines, ctx, schemaPrefix) {
5474
+ const gi = ctx.varCounter++
5475
+ const gv = `_gv${gi}`
5476
+ const g = subtreeGuard(sub, ctx, gv)
5477
+ if (g === null) { genCodeC(sub, v, pathExpr, lines, ctx, schemaPrefix); return }
5478
+ if (g.name !== undefined) lines.push(`if(!${g.name}(${v})){`)
5479
+ else {
5480
+ // The verdict's exits become jumps out of the block: a failure leaves
5481
+ // the flag set, success clears it. replaceTopLevel keeps nested
5482
+ // closures' returns as they are (see the hybrid rewrite).
5483
+ lines.push(`{const ${gv}=${v};let _g${gi}=1;g${gi}:{${replaceTopLevel(g.body, `{_g${gi}=0;break g${gi}}`, `break g${gi}`)};_g${gi}=0}if(_g${gi}){`)
5484
+ }
5485
+ ctx.guardDepth = (ctx.guardDepth || 0) + 1
5486
+ try { genCodeC(sub, v, pathExpr, lines, ctx, schemaPrefix) } finally { ctx.guardDepth-- }
5487
+ lines.push(g.name !== undefined ? '}' : '}}')
5488
+ }
5489
+
5490
+ // A branch's error-collecting body rewritten to count what it would have
5491
+ // collected: each `_e.push({keyword:'k',...})` becomes a count and a severity
5492
+ // (lib/branch-collapse.js, __ataScore), and the body returns the score the
5493
+ // collapse would compute from the errors. When no branch of a oneOf or anyOf
5494
+ // holds, the collapse reports the closest branch's errors; it used to run
5495
+ // every branch's error function to score them, building every branch's
5496
+ // errors to keep one branch's. Now the counting bodies score, and only the
5497
+ // closest branch's error function runs. Null for a body the rewrite cannot
5498
+ // follow (a shared definition call, a nested collapse, a push that is not a
5499
+ // literal); that site keeps the old path.
5500
+ function countingBody (body) {
5501
+ // A nested oneOf or anyOf that does not hold is one collapsed error in the
5502
+ // branch's list (keyword oneOf or anyOf, severity 4): its marked section,
5503
+ // which would run the inner branches to build it, becomes that count.
5504
+ // Two kinds of section: CB is one error whatever the branches would say;
5505
+ // CU is a section the copy cannot score (the value is not the one the
5506
+ // counts were taken for), so the copy answers -1 and the picker runs the
5507
+ // branches as before.
5508
+ while (true) {
5509
+ const b1 = body.indexOf('/*CB*/'), b2 = body.indexOf('/*CU*/')
5510
+ const b = b1 < 0 ? b2 : b2 < 0 ? b1 : Math.min(b1, b2)
5511
+ if (b < 0) break
5512
+ const unknown = b === b2
5513
+ let depth = 1
5514
+ let j = b + 6
5515
+ while (j < body.length && depth > 0) {
5516
+ if (body.startsWith('/*CB*/', j) || body.startsWith('/*CU*/', j)) { depth++; j += 6; continue }
5517
+ if (body.startsWith('/*CE*/', j)) { depth--; j += 6; continue }
5518
+ j++
5519
+ }
5520
+ if (depth !== 0) return null
5521
+ body = body.slice(0, b) + (unknown ? 'return -1' : '(_n++,_s+=4)') + body.slice(j)
5522
+ }
5523
+ if (/\b(?:_defE\d|__ataCollapse|__ataMulti|_brf\d|_bsf\d)/.test(body)) return null
5524
+ const SEV = require('./branch-collapse').SEVERITY
5525
+ let out = ''
5526
+ let i = 0
5527
+ const head = 'const _e=[];'
5528
+ if (!body.startsWith(head)) return null
5529
+ out += 'let _n=0,_s=0;'
5530
+ i = head.length
5531
+ while (i < body.length) {
5532
+ const ch = body[i]
5533
+ if (ch === '"' || ch === "'" || ch === '`') { const j = skipString(body, i); out += body.slice(i, j); i = j; continue }
5534
+ if (body.startsWith('_e.push(', i)) {
5535
+ let j = i + 8
5536
+ let depth = 1
5537
+ while (j < body.length && depth > 0) {
5538
+ const c = body[j]
5539
+ if (c === '"' || c === "'" || c === '`') { j = skipString(body, j); continue }
5540
+ if (c === '(' || c === '{' || c === '[') depth++
5541
+ else if (c === ')' || c === '}' || c === ']') depth--
5542
+ j++
5543
+ }
5544
+ if (depth !== 0) return null
5545
+ const arg = body.slice(i + 8, j - 1)
5546
+ // The literal may still carry its `code` (the shape pass strips it).
5547
+ const m = /^\{(?:code:'ATA\d{4}',)?keyword:'([A-Za-z$][\w$]*)',/.exec(arg)
5548
+ if (m === null) return null
5549
+ out += `(_n++,_s+=${SEV[m[1]] || 4})`
5550
+ i = j
5551
+ continue
5552
+ }
5553
+ if (body.startsWith('return{valid:false,errors:_e}', i)) { out += 'return _n*100+_s'; i += 29; continue }
5554
+ if (body.startsWith('return{valid:_e.length===0,errors:_e}', i)) { out += 'return _n*100+_s'; i += 37; continue }
5555
+ if (body.startsWith('_e.length', i)) { out += '_n'; i += 9; continue }
5556
+ out += ch
5557
+ i++
5558
+ }
5559
+ if (/\b_e\b/.test(out.replace(/'[^']*'|"[^"]*"/g, ''))) return null
5560
+ return out
5561
+ }
5562
+
5145
5563
  function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaPrefix) {
5146
5564
  ctx.usesBranchCollapse = true
5147
5565
  const fi = ctx.varCounter++
@@ -5203,23 +5621,59 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
5203
5621
  // In the combined function `_e` is created on the first error and every
5204
5622
  // error is collected (`_all` is true there), so the report only pushes.
5205
5623
  const report = ctx.inCombined && !ctx.inBranchFn
5206
- ? `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed})(_e||(_e=[])).push(${collapsed})`
5624
+ ? `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed})(_e=_ap(_A,_e,${collapsed},${apOrd(ctx, branchSp)}))`
5207
5625
  : `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
5626
+ // Score first, collect once (countingBody), in the combined function where
5627
+ // the collapsed error is built in its final shape (no ordering key).
5628
+ let noneReport = null
5629
+ if (ctx.inCombined && (ctx.noOrdinal ? 'null' : '') === 'null') {
5630
+ const cbs = fns.map((b) => b === '' ? 'return 0' : countingBody(b))
5631
+ if (!cbs.some((c) => c === null)) {
5632
+ const sfx = usesSp ? ',_sp' : ''
5633
+ const code = branchKw === 'oneOf' ? 'ATA4001' : 'ATA4003'
5634
+ // `code: ` with the space: the runtime shape strips this generator's own
5635
+ // `code:'ATA....',` from the literals and keeps the collapse's, which
5636
+ // the reader expects to carry it (see the shape pass below).
5637
+ const lit = `{code: '${code}',keyword:'${branchKw}',instancePath:_pp${fi}||'',path:_pp${fi}||'',schemaPath:'${branchSp}',message:'value matched 0 of ${branches.length} ${branchKw} variants',params:{variants:${branches.length},closest:_bi${fi},closestName:_bt${fi}[_bi${fi}]||('variant '+(_bi${fi}+1))},branchErrors:_br${fi}.errors}`
5638
+ // In a branch function the error joins that function's list (the
5639
+ // combined function's `_ap` and ordering state are not its).
5640
+ const push = ctx.inBranchFn ? `_e.push(${lit});if(!_all)return{valid:false,errors:_e}` : `(_e=_ap(_A,_e,${lit},${apOrd(ctx, branchSp)}))`
5641
+ if (cbs.length === 1) {
5642
+ // One branch is the closest one: nothing to score, no counting copy.
5643
+ // Scoring it ran the branch a third time (verdict, count, errors).
5644
+ noneReport = `{let _bi${fi}=0;const _br${fi}=${fnArr}[0](${v},_pp${fi},_all${sfx});${push}}`
5645
+ } else {
5646
+ ctx.helperCode.push(cbs.map((b, i) => `const _bsf${fi}_${i}=function(${params}){${b}}`).join(';'))
5647
+ const picks = cbs.map((_, i) => `{const _s=_bsf${fi}_${i}(${v},_pp${fi},_all${sfx});if(_s<0)_ng${fi}=1;else if(_s<_bs${fi}){_bs${fi}=_s;_bi${fi}=${i}}}`).join('')
5648
+ // A copy that could not score (-1): every branch runs, as before.
5649
+ const noneOld = branchKw === 'anyOf'
5650
+ ? `const ${resArr}=[];for(let _bi=0;_bi<${branches.length};_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}`
5651
+ : `const ${resArr}=[];for(let _bi=0;_bi<${branches.length};_bi++)${resArr}.push(${call});${report}`
5652
+ noneReport = `{let _bi${fi}=0,_bs${fi}=Infinity,_ng${fi}=0;${picks}if(_ng${fi}){${noneOld}}else{const _br${fi}=${fnArr}[_bi${fi}](${v},_pp${fi},_all${sfx});${push}}}`
5653
+ }
5654
+ }
5655
+ }
5208
5656
  // The node's runtime unevaluatedProperties already counted how many of
5209
5657
  // these branches pass, through the annotation functions, which answer the
5210
5658
  // same validity. A oneOf with exactly one, an anyOf with any, holds and
5211
5659
  // reports nothing, so its branches are not run a second time to say so.
5212
5660
  // Only in the combined function proper, where that count is in scope.
5213
- const counted = ctx.inCombined && !ctx.inBranchFn && ctx.branchCounts ? ctx.branchCounts.get(branches) : undefined
5661
+ // Inside a branch function only a factory-scope count (factoryCount) is
5662
+ // in reach; the node's own counts are locals of the combined function.
5663
+ const countedRaw = ctx.inCombined && ctx.branchCounts ? ctx.branchCounts.get(branches) : undefined
5664
+ const counted = countedRaw !== undefined && (!ctx.inBranchFn || countedRaw.factory) ? countedRaw : undefined
5214
5665
  const same = counted !== undefined ? `${v}!==null&&typeof ${v}==='object'&&${counted.of}===${v}` : ''
5215
5666
  let guard = counted !== undefined
5216
5667
  ? (keyword === 'anyOf' ? `if(!(${same}&&${counted.count}>=1))` : `if(!(${same}&&${counted.count}===1))`)
5217
5668
  : ''
5218
5669
  // More than one oneOf branch passed: the error names them and carries no
5219
5670
  // branch errors, so it is built from the bits the count left.
5220
- if (guard !== '' && keyword === 'oneOf' && counted.mask && ctx.inCombined && !ctx.inBranchFn) {
5671
+ if (guard !== '' && keyword === 'oneOf' && counted.mask && ctx.inCombined) {
5221
5672
  const ord0 = ctx.noOrdinal ? 'null' : (() => { const o = ordinalFor(ctx.rootSchema, unescapeSp(branchSp)); return o === null ? 'null' : o })()
5222
- guard = `if(${same}&&${counted.count}>1){const _pi${fi}=[];for(let _k=0;_k<${branches.length};_k++)if((${counted.mask}>>_k)&1)_pi${fi}.push(_k);(_e||(_e=[])).push(__ataMulti(_pi${fi},${branches.length},${pp},'${branchSp}',${ord0}))}else if(!(${same}&&${counted.count}===1))`
5673
+ const multiC = `__ataMulti(_pi${fi},${branches.length},${pp},'${branchSp}',${ord0})`
5674
+ const pushC = ctx.inBranchFn ? `_e.push(${multiC});if(!_all)return{valid:false,errors:_e}` : `(_e=_ap(_A,_e,${multiC},${apOrd(ctx, branchSp)}))`
5675
+ // The marked section is one error in a counting copy (countingBody).
5676
+ guard = `if(${same}&&${counted.count}>1){/*CB*/const _pi${fi}=[];for(let _k=0;_k<${branches.length};_k++)if((${counted.mask}>>_k)&1)_pi${fi}.push(_k);${pushC}/*CE*/}else if(!(${same}&&${counted.count}===1))`
5223
5677
  }
5224
5678
  // Without such a count, each branch also gets a verdict function, and the
5225
5679
  // passing branches are counted with those: a oneOf with exactly one and an
@@ -5264,29 +5718,49 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
5264
5718
  restoreCtx(ctx, saved)
5265
5719
  }
5266
5720
  }
5721
+ // In the combined function (the runtime path; a standalone module cannot
5722
+ // carry a compiled function) a branch that holds a reference, or a site
5723
+ // whose inline verdicts were too large, gets a verdict compiled on its own
5724
+ // and handed in as a closure value: the branch with the root's definitions
5725
+ // under it, through compileToJSCodegen, with its own shared definitions and
5726
+ // its own regex engines. Without one, every branch's error function ran on
5727
+ // every value the keyword saw, accepted or not: json-patch's six-way oneOf
5728
+ // cost 1363 ns on a valid list of patches where its verdict costs 261.
5729
+ if (vfArr === null && guard === '' && ctx.inCombined) {
5730
+ const compiled = compileBranchVerdicts(branches, ctx)
5731
+ if (compiled !== null) {
5732
+ vfArr = `_bvf${fi}`
5733
+ for (let i = 0; i < compiled.length; i++) { ctx.closureVars.push(`${vfArr}_${i}`); ctx.closureVals.push(compiled[i]) }
5734
+ }
5735
+ }
5267
5736
  if (vfArr !== null) {
5268
5737
  const L = branches.length
5269
5738
  const top = ctx.inCombined && !ctx.inBranchFn
5270
5739
  // An accepted document is still answered by the verdict first (see the
5271
5740
  // result shape in compileToJSCombined): that is the fastest path to it,
5272
5741
  // and dropping it measured 20 ns slower per accepted oneOf.
5273
- if (top) ctx.unguardedCollapse = true
5742
+ if (top && !ctx.guardDepth) ctx.unguardedCollapse = true
5274
5743
  const tests = branches.map((_, i) => `${vfArr}_${i}(${v})`)
5275
5744
  if (keyword === 'anyOf') {
5276
- lines.push(`{const _pp${fi}=${pp};if(!(${tests.join('||')})){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${L};_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}}}`)
5745
+ const none = noneReport !== null ? noneReport : `const ${resArr}=[];for(let _bi=0;_bi<${L};_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}`
5746
+ // In a branch function the section a counting copy replaces is marked
5747
+ // (countingBody): when the keyword does not hold, the error function
5748
+ // reports one collapsed error, whatever the branches would say.
5749
+ lines.push(`{const _pp${fi}=${pp};if(!(${tests.join('||')})){${top ? '' : '/*CB*/'}if(_all===0){_e.push(null);return{valid:false,errors:_e}}${none}${top ? '' : '/*CE*/'}}}`)
5277
5750
  } else {
5278
5751
  const mk = L <= 30
5279
5752
  const collect = mk
5280
5753
  ? `const _pi${fi}=[];for(let _k=0;_k<${L};_k++)if((_m${fi}>>_k)&1)_pi${fi}.push(_k);`
5281
5754
  : ''
5282
5755
  const multi = `__ataMulti(_pi${fi},${L},_pp${fi},'${branchSp}',${ord})`
5283
- const pushMulti = top ? `(_e||(_e=[])).push(${multi})` : `_e.push(${multi});if(!_all)return{valid:false,errors:_e}`
5756
+ const pushMulti = top ? `(_e=_ap(_A,_e,${multi},${apOrd(ctx, branchSp)}))` : `_e.push(${multi});if(!_all)return{valid:false,errors:_e}`
5284
5757
  const count = tests.map((t, i) => `if(${t}){_n${fi}++;${mk ? `_m${fi}|=${1 << i}` : `_pi${fi}.push(${i})`}}`).join('')
5285
- lines.push(`{const _pp${fi}=${pp};let _n${fi}=0${mk ? `,_m${fi}=0` : `,_pi${fi}=[]`};${count}if(_n${fi}!==1){if(_all===0){_e.push(null);return{valid:false,errors:_e}}if(_n${fi}>1){${collect}${pushMulti}}else{const ${resArr}=[];for(let _bi=0;_bi<${L};_bi++)${resArr}.push(${call});${report}}}}`)
5758
+ const none = noneReport !== null ? noneReport : `const ${resArr}=[];for(let _bi=0;_bi<${L};_bi++)${resArr}.push(${call});${report}`
5759
+ lines.push(`{const _pp${fi}=${pp};let _n${fi}=0${mk ? `,_m${fi}=0` : `,_pi${fi}=[]`};${count}if(_n${fi}!==1){${top ? '' : '/*CB*/'}if(_all===0){_e.push(null);return{valid:false,errors:_e}}if(_n${fi}>1){${collect}${pushMulti}}else{${none}}${top ? '' : '/*CE*/'}}}`)
5286
5760
  }
5287
5761
  return
5288
5762
  }
5289
- if (guard === '' && ctx.inCombined && !ctx.inBranchFn) ctx.unguardedCollapse = true
5763
+ if (guard === '' && ctx.inCombined && !ctx.inBranchFn && !ctx.guardDepth) ctx.unguardedCollapse = true
5290
5764
  if (keyword === 'anyOf') {
5291
5765
  // anyOf reports nothing once a branch passes, so the branches are first
5292
5766
  // only asked whether they pass. They are called with `_all` set to 0, a
@@ -5300,12 +5774,24 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
5300
5774
  // no one read. Only when no branch passes do they run again with the
5301
5775
  // caller's `_all`, and the collapse sees them all, as before.
5302
5776
  const probe = `${fnArr}[_bi](${v},_pp${fi},0${usesSp ? ',_sp' : ''})`
5303
- lines.push(`${guard}{const _pp${fi}=${pp};let _bp${fi}=false;for(let _bi=0;_bi<${fnArr}.length;_bi++){if(${probe}.valid){_bp${fi}=true;break}}if(!_bp${fi}){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}}}`)
5777
+ const none = noneReport !== null ? noneReport : `const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}`
5778
+ const probeOld = `let _bp${fi}=false;for(let _bi=0;_bi<${fnArr}.length;_bi++){if(${probe}.valid){_bp${fi}=true;break}}if(!_bp${fi}){if(_all===0){_e.push(null);return{valid:false,errors:_e}}${none}}`
5779
+ // With a count: on the counted object the guard already said no branch
5780
+ // held (CB, one error in a counting copy); on another object the branches
5781
+ // are probed as before (CU, a counting copy cannot know).
5782
+ if (counted !== undefined) lines.push(`${guard}{const _pp${fi}=${pp};if(${same}){/*CB*/if(_all===0){_e.push(null);return{valid:false,errors:_e}}${none}/*CE*/}else{/*CU*/${probeOld}/*CE*/}}`)
5783
+ else lines.push(`${guard}{const _pp${fi}=${pp};${probeOld}}`)
5304
5784
  } else {
5305
5785
  // Asked only whether it passes (`_all` 0, see anyOf above), oneOf counts
5306
5786
  // the passing branches and answers; the branch errors of a probe are not
5307
5787
  // errors anyone reads and are not collapsed.
5308
- lines.push(`${guard}{const _pp${fi}=${pp};const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++)${resArr}.push(${call});if(_all===0){let _n=0;for(const _r of ${resArr})if(_r.valid)_n++;if(_n!==1){_e.push(null);return{valid:false,errors:_e}}}else{${report}}}`)
5788
+ // With a count from the node's unevaluatedProperties (`counted`), a count
5789
+ // of zero on the counted object means no branch held, which is the case
5790
+ // the closest-branch path answers without running every branch.
5791
+ const oldO = `const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++)${resArr}.push(${call});if(_all===0){let _n=0;for(const _r of ${resArr})if(_r.valid)_n++;if(_n!==1){_e.push(null);return{valid:false,errors:_e}}}else{${report}}`
5792
+ const noneO = noneReport !== null ? noneReport : `const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++)${resArr}.push(${call});${report}`
5793
+ if (counted !== undefined) lines.push(`${guard}{const _pp${fi}=${pp};if(${same}){/*CB*/if(_all===0){_e.push(null);return{valid:false,errors:_e}}${noneO}/*CE*/}else{/*CU*/${oldO}/*CE*/}}`)
5794
+ else lines.push(`${guard}{const _pp${fi}=${pp};${oldO}}`)
5309
5795
  }
5310
5796
  }
5311
5797
 
@@ -5332,19 +5818,60 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5332
5818
  ctx.regExpMap = new Map();
5333
5819
  }
5334
5820
  // $ref — resolve local and cross-schema refs
5335
- if (schema.$ref) {
5336
- // The reference is followed and the rest of the node is not compiled; a
5337
- // keyword beside it that validates would be dropped, so the node
5338
- // declines (as in genCodeCNode). Reached since unevaluatedProperties
5339
- // next to a $ref stopped declining the combined function, whose oneOf
5340
- // and anyOf branches come from this generator: a branch was taken as
5341
- // passing with its siblings unchecked.
5342
- for (const k of Object.keys(schema)) if (!REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-')) throw DECLINE
5343
- // A self-reference cannot be expressed here yet: the root helper takes no
5344
- // path argument, so errors raised through it would carry the path of the
5345
- // root rather than of the value that failed. This used to fall through as
5346
- // a permissive no-op, which accepted whatever the reference guarded.
5347
- if (schema.$ref === '#') throw DECLINE
5821
+ if (schema.$ref && !schema[REF_DONE]) {
5822
+ // A keyword beside $ref that validates applies too (2020-12): the
5823
+ // reference is written on its own, then the node again with the
5824
+ // reference marked done, on a fresh copy that keeps `$ref` for the
5825
+ // annotation readers, as genCodeCNode does. This used to decline, which
5826
+ // kept the 2020-12 meta-schema (every vocabulary schema is a reference
5827
+ // with siblings once unrolled by scope) off both error generators and on
5828
+ // the interpreter for its errors, about 2 microseconds a rejection.
5829
+ if (Object.keys(schema).some((k) => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))) {
5830
+ genCodeENode({ $ref: schema.$ref }, v, pathExpr, lines, ctx, schemaPrefix)
5831
+ const rest = { ...schema }
5832
+ Object.defineProperty(rest, REF_DONE, { value: true })
5833
+ genCodeENode(rest, v, pathExpr, lines, ctx, schemaPrefix)
5834
+ return
5835
+ }
5836
+ // A self-reference is a call to a helper over the root schema that takes
5837
+ // the path and the schema path as arguments, as a definition on a cycle
5838
+ // does below: the root helper takes no path, so errors raised through it
5839
+ // carried the root's path, and this declined, which left a standalone
5840
+ // module reporting one stub error for a schema with `$ref: "#"` anywhere
5841
+ // in it. This used to fall through as a permissive no-op, which accepted
5842
+ // whatever the reference guarded.
5843
+ if (schema.$ref === '#') {
5844
+ // The body comes from the prepared root (`rootE`), whose references
5845
+ // are the resolvable ones; from the schema as given, a reference by
5846
+ // `$id` resolved to nothing and the helper for the suite's recursive
5847
+ // tree checked no node.
5848
+ if (!ctx.rootE || typeof ctx.rootE !== 'object') throw DECLINE
5849
+ if (!ctx.defFns) ctx.defFns = new Map()
5850
+ let fnName = ctx.defFns.get('#')
5851
+ if (!fnName) {
5852
+ fnName = '_defE' + ctx.defFns.size + '_root'
5853
+ ctx.defFns.set('#', fnName)
5854
+ // The helper is its own function, so the references expanded in it
5855
+ // start from nothing: inside the expansion of a definition that led
5856
+ // here, the definition's own reference would otherwise read as
5857
+ // already being expanded.
5858
+ const bodyLines = []
5859
+ const outerStack = ctx.refStack
5860
+ ctx.refStack = new Set()
5861
+ try { genCodeE(ctx.rootE, 'd', '_p', bodyLines, ctx, SP_PLACEHOLDER) } finally { ctx.refStack = outerStack }
5862
+ let body = bodyLines.join('\n ')
5863
+ body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
5864
+ if (body.includes(SP_PLACEHOLDER)) throw DECLINE
5865
+ ctx.helperCode.push(
5866
+ `const ${fnName}_s=new Set()\n function ${fnName}(d,_p,_all,_e,_sp){\n ` +
5867
+ `if(_sg){if(typeof d!=='object'||d===null)return ${fnName}_b(d,_p,_all,_e,_sp);if(${fnName}_s.has(d))return;${fnName}_s.add(d);try{return ${fnName}_b(d,_p,_all,_e,_sp)}finally{${fnName}_s.delete(d)}}\n ` +
5868
+ `if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(d,_p,_all,_e,_sp)\n _sd--\n return _r\n }\n ` +
5869
+ `function ${fnName}_b(d,_p,_all,_e,_sp){${body}}`,
5870
+ )
5871
+ }
5872
+ lines.push(`${fnName}(${v},${pathExpr || '""'},_all,_e,'${schemaPrefix}');if(!_all&&_e.length)return{valid:false,errors:_e}`)
5873
+ return
5874
+ }
5348
5875
  const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
5349
5876
  if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
5350
5877
  const defName = m[1]
@@ -5384,7 +5911,10 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5384
5911
  lines.push(`${fnName}(${v},${pathExpr || '""'},_all,_e,'${schemaPrefix}');if(!_all&&_e.length)return{valid:false,errors:_e}`)
5385
5912
  return
5386
5913
  }
5387
- if (ctx.refStack.has(schema.$ref)) return
5914
+ // A reference back into what is being expanded, on a cycle the
5915
+ // definitions alone do not show (through the root, or another
5916
+ // document): declined. Returning emitted nothing, which read as valid.
5917
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5388
5918
  ctx.refStack.add(schema.$ref)
5389
5919
  genCodeE(ctx.rootDefs[defName], v, pathExpr, lines, ctx, schemaPrefix)
5390
5920
  ctx.refStack.delete(schema.$ref)
@@ -5395,7 +5925,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5395
5925
  const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
5396
5926
  const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
5397
5927
  if (anchorTarget) {
5398
- if (ctx.refStack.has(schema.$ref)) return
5928
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5399
5929
  ctx.refStack.add(schema.$ref)
5400
5930
  genCodeE(anchorTarget, v, pathExpr, lines, ctx, schemaPrefix)
5401
5931
  ctx.refStack.delete(schema.$ref)
@@ -5403,7 +5933,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5403
5933
  }
5404
5934
  }
5405
5935
  if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
5406
- if (ctx.refStack.has(schema.$ref)) return
5936
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5407
5937
  ctx.refStack.add(schema.$ref)
5408
5938
  genCodeE(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx, schemaPrefix)
5409
5939
  ctx.refStack.delete(schema.$ref)
@@ -5413,13 +5943,16 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5413
5943
  if (ctx.schemaMap && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
5414
5944
  const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
5415
5945
  if (r) {
5416
- if (ctx.refStack.has(schema.$ref)) return
5946
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5417
5947
  ctx.refStack.add(schema.$ref)
5418
5948
  genCodeE(r.schema, v, pathExpr, lines, ctx, schemaPrefix)
5419
5949
  ctx.refStack.delete(schema.$ref)
5420
5950
  return
5421
5951
  }
5422
5952
  }
5953
+ // A reference none of the above resolved: declined. Falling through
5954
+ // emitted nothing for it, which read as valid.
5955
+ throw DECLINE
5423
5956
  }
5424
5957
 
5425
5958
  // $dynamicRef — resolve via anchors map
@@ -6011,8 +6544,63 @@ function sharedErr (lit, rootSchema) {
6011
6544
  // the value at `pathExpr`, which enrichment otherwise finds again by walking
6012
6545
  // the document. `paramsShared` names a frozen params object hoisted for a
6013
6546
  // static site, so a static error's params stay one shared object as before.
6547
+ // The ordinal `_ap` is handed with each error of the combined function: the
6548
+ // error's place in schema order when its path is fixed at compile time, -1
6549
+ // when it is not (a path through a shared definition, a collapsed branch
6550
+ // error, an enriched error). `_ap` keeps whether the list is still in order
6551
+ // as it grows; a list that came out in order, which is most of them, is
6552
+ // handed out without the sort that looked every error's path up in a map.
6553
+ function apOrd (ctx, sp, coarse) {
6554
+ // Inside a shared definition's function every error takes the ordinal of
6555
+ // the reference that reached it, handed in as `_so`: errors behind one
6556
+ // `$ref` sort where the reference is written, in the order the definition
6557
+ // reports them.
6558
+ if (typeof sp === 'string' && sp.includes(SP_PLACEHOLDER) && ctx && ctx.inDefC) return '_so'
6559
+ if (typeof sp !== 'string' || sp.includes(SP_PLACEHOLDER) || sp.includes('+')) return -1
6560
+ if (!ctx || !ctx.rootSchema) return -1
6561
+ const path = unescapeSp(sp)
6562
+ const o = ordinalFor(ctx.rootSchema, path)
6563
+ if (typeof o !== 'number') return -1
6564
+ // The reader orders by ordinal and, between equal ordinals, by the rank of
6565
+ // the path (tieOrder in lib/rejections.js). Both are known here, so a site
6566
+ // gets a token that assignOrdKeys turns into a dense integer in exactly that
6567
+ // order when the program is assembled: two errors with one key are ones the
6568
+ // reader keeps as they came. Keyed by the ordinal alone, two errors under
6569
+ // one synthetic definition shared a key and sent every such list to the
6570
+ // reader, which ranked the paths again: 45 percent of reading a list
6571
+ // schema's two errors.
6572
+ if (!ctx.ordSites) ctx.ordSites = new Map()
6573
+ let site = ctx.ordSites.get(path)
6574
+ if (site === undefined) { site = { i: ctx.ordSites.size, o, r: rankFor(ctx.rootSchema, path), key: -1 }; ctx.ordSites.set(path, site) }
6575
+ // A coarse key, for the errors behind a reference into a shared definition
6576
+ // (see `ap` in lib/combined-runtime.js): -(k+2), written when k is known.
6577
+ return coarse ? `@@C${site.i}@@` : `@@O${site.i}@@`
6578
+ }
6579
+
6580
+ // The dense keys of a program's error sites, in the reader's order: ordinal,
6581
+ // then rank as tieOrder compares it, element by element and the shorter
6582
+ // first; equal pairs share a key, and a site without a rank keeps -1, which
6583
+ // hands its list to the reader. Replaces the tokens apOrd wrote.
6584
+ function assignOrdKeys (ctx, src) {
6585
+ if (!ctx.ordSites || ctx.ordSites.size === 0) return src
6586
+ const sites = [...ctx.ordSites.values()]
6587
+ const cmp = (a, b) => {
6588
+ if (a.o !== b.o) return a.o - b.o
6589
+ const m = Math.min(a.r.length, b.r.length)
6590
+ for (let k = 0; k < m; k++) if (a.r[k] !== b.r[k]) return a.r[k] - b.r[k]
6591
+ return a.r.length - b.r.length
6592
+ }
6593
+ const ranked = sites.filter((x) => x.r !== null).sort(cmp)
6594
+ let key = 0
6595
+ for (let i = 0; i < ranked.length; i++) { if (i > 0 && cmp(ranked[i - 1], ranked[i]) !== 0) key++; ranked[i].key = key }
6596
+ return src.replace(/@@([OC])(\d+)@@/g, (_, t, i) => { const k = sites[+i].key; return String(t === 'O' || k < 0 ? k : -(k + 2)) })
6597
+ }
6598
+
6599
+ // The error list and its order live in lib/combined-runtime.js (`ap`, `sr`):
6600
+ // `_A` is this program's state, reset at the top of each call.
6601
+ const AP_RESET = '_A.mo=true;_A.lo=-1;_A.on=0;'
6014
6602
  function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, lean, paramsShared) {
6015
- if (!ctx.rich) return `(_e||(_e=[])).push(${lean})`
6603
+ if (!ctx.rich) return `(_e=_ap(_A,_e,${lean},${apOrd(ctx, sp)}))`
6016
6604
  if (!ctx.richMk) {
6017
6605
  ctx.richMk = true
6018
6606
  const { makeRich } = require('./enrich-site')
@@ -6028,7 +6616,7 @@ function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, le
6028
6616
  // the definition and the reference's own comes in as `_sp`.
6029
6617
  const rel = sp.startsWith(SP_PLACEHOLDER)
6030
6618
  ctx.closureVals.push(errorSite(keyword, unescapeSp(rel ? sp.slice(SP_PLACEHOLDER.length) : sp), paramsVal, fmt))
6031
- return `(_e||(_e=[])).push(_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''}))`
6619
+ return `(_e=_ap(_A,_e,_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''}),-1))`
6032
6620
  }
6033
6621
 
6034
6622
  function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
@@ -6110,7 +6698,11 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
6110
6698
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
6111
6699
 
6112
6700
  // runtimeShape: errors without the ordinal, see compileToJSCodegenWithErrors.
6113
- const ctx = { noOrdinal: !!(opts && opts.runtimeShape), rich: !!(opts && opts.rich), inCombined: true, hoisted: [], rootC: schema, varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
6701
+ // `_ap` adds an error to a list that may not exist yet: the first one
6702
+ // creates it as a one-element literal. Pushing onto an empty `[]` grew its
6703
+ // backing store on every rejection, about 7% of reading errors across the
6704
+ // official suite.
6705
+ const ctx = { noOrdinal: !!(opts && opts.runtimeShape), rich: !!(opts && opts.rich), inCombined: true, hoisted: [], rootC: schema, varCounter: 0, preamble: [], helperCode: [], shared: [], closureVars: ['_cpLen', '_A', '_ap', '_sr', '_srX'], closureVals: [_cpLen, sharedRt().apState(), sharedRt().ap, sharedRt().sr, sharedRt().srX], sharedRuntime: true,
6114
6706
  rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: inputSchema, userFormats: userFormats || null }
6115
6707
  const lines = []
6116
6708
  try {
@@ -6131,6 +6723,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
6131
6723
  ctx.closureVars.push('_ER', '_EE', '_SRT', '_FB', '_VF')
6132
6724
  ctx.closureVals.push(opts.resultShape.Rejection, opts.resultShape.empty, opts.resultShape.sort, opts.resultShape.fallback, opts.resultShape.verdict || null)
6133
6725
  }
6726
+ if (ctx.usesBranchCollapse) bindRt(ctx, ['__ataCollapse', '__ataMulti'])
6134
6727
  const closureNames = ctx.usesSafeRe ? [...ctx.closureVars, '__ataSafeRe'] : ctx.closureVars
6135
6728
  const closureArgs = ctx.usesSafeRe ? [...ctx.closureVals, compileSafe] : ctx.closureVals
6136
6729
  const closureParams = closureNames.join(',')
@@ -6149,7 +6742,6 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
6149
6742
  // the branch functions (this function collects everything), and the
6150
6743
  // runtime shape the error generator gives its literals, without code and
6151
6744
  // docUrl. The collapsed error itself keeps its code, as everywhere.
6152
- if (ctx.usesBranchCollapse) ctx.helperCode.push(require('./branch-collapse').embedSource())
6153
6745
  if (ctx.usesBranchCollapse && opts && opts.runtimeShape) {
6154
6746
  const shape = (src) => src.replace(/code:'ATA\d{4}',/g, '').replace(/,docUrl:'https:\/\/ata-validator\.com\/e\/ATA\d{4}'/g, '')
6155
6747
  for (let i = 0; i < ctx.helperCode.length; i++) ctx.helperCode[i] = shape(ctx.helperCode[i])
@@ -6170,7 +6762,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
6170
6762
  ctx.helperCode.push(`function _bodyC(d){${hoistDecl}${allDecl}let _e;\n ${checks}\n return _e}`)
6171
6763
  hoistDecl = ''
6172
6764
  allDecl = ''
6173
- checks = `_sd=0;try{_e=_bodyC(d)}catch(_err){if(_err!==_CYC)throw _err;_sg=true;${clears};try{_e=_bodyC(d)}finally{_sg=false}}`
6765
+ checks = `_sd=0;try{_e=_bodyC(d)}catch(_err){if(_err!==_CYC)throw _err;_sg=true;${clears};${AP_RESET}try{_e=_bodyC(d)}finally{_sg=false}}`
6174
6766
  }
6175
6767
  // In the result shape this function is validate() itself, so a document
6176
6768
  // that makes it throw is answered by the caller's previous path instead
@@ -6183,18 +6775,21 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
6183
6775
  // Not when every collapse is guarded by a count (emitBranchCollapse): an
6184
6776
  // accepted document then runs no branch twice, and a rejected one would
6185
6777
  // pay for the verdict for nothing.
6186
- ? `try{${hoistDecl}${ctx.usesBranchCollapse && ctx.unguardedCollapse ? 'if(_VF!==null&&_VF(d))return{valid:true,data:d,errors:_EE};' : ''}${allDecl}let _e;\n ` + checks +
6778
+ ? `try{${hoistDecl}${ctx.usesBranchCollapse && ctx.unguardedCollapse ? 'if(_VF!==null&&_VF(d))return{valid:true,data:d,errors:_EE};' : ''}${allDecl}let _e;${AP_RESET}\n ` + checks +
6187
6779
  // A collapsed branch error is presented even alone (its ordinal and
6188
6780
  // raw branch errors stay inside); otherwise only a list of two or more
6189
6781
  // needs ordering.
6190
- `\n return _e?new _ER(${ctx.usesBranchCollapse ? '_SRT(_e)' : '_e.length>1?_SRT(_e):_e'}):{valid:true,data:d,errors:_EE}}catch(_x){return _FB(d)}`
6191
- : hoistDecl + allDecl + `let _e;\n ` + checks + `\n return _e?{valid:false,errors:_e}:R`
6192
- const helpers = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
6782
+ `\n return _e?new _ER(_A.mo?_e:${process.env.ATA_CHECK_SORT ? '_srX(_A,_e,_SRT)' : '_sr(_A,_e,_SRT)'}):{valid:true,data:d,errors:_EE}}catch(_x){return _FB(d)}`
6783
+ : hoistDecl + allDecl + `let _e;${AP_RESET}\n ` + checks + `\n return _e?{valid:false,errors:_e}:R`
6784
+ // The verdict generator's hoisted functions (subtreeGuard writes guards
6785
+ // through it) come first: a oneOf branch it could not hoist was a closure
6786
+ // built at every call of the guard, six per element of a json-patch list.
6787
+ const helpers = (ctx.preamble.length ? ctx.preamble.join('\n ') + '\n ' : '') + (ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '')
6193
6788
 
6194
6789
  try {
6195
6790
  if (typeof process !== 'undefined' && process.env && process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + helpers + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
6196
6791
  const factory = new Function('R' + (closureParams ? ',' + closureParams : ''),
6197
- `${helpers}return function(d){${inner}}`)
6792
+ assignOrdKeys(ctx, `${helpers}return function _vC(d){${inner}}`))
6198
6793
  return factory(VALID_RESULT, ...closureArgs)
6199
6794
  } catch (e) {
6200
6795
  if (typeof process !== 'undefined' && process.env && process.env.ATA_DEBUG) console.error('compileToJSCombined error:', e.message, '\n', inner.slice(0, 500))
@@ -6250,11 +6845,32 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6250
6845
  // declares its own branch counts (genUnevaluatedDynamicC). The document root
6251
6846
  // comes in as `d` for the rich errors that read it. Not for a definition on a
6252
6847
  // reference cycle, which keeps its existing path. False when not shared.
6848
+ const COMBINED_DEF_FN_MIN = 160
6849
+ const _defOwnChars = new WeakMap()
6850
+ // The definition's own size, its annotations left out, measured once.
6851
+ function defOwnChars (def) {
6852
+ if (def === null || typeof def !== 'object') return 0
6853
+ let n = _defOwnChars.get(def)
6854
+ if (n === undefined) {
6855
+ n = 0
6856
+ const walk = (x) => { if (x === null || typeof x !== 'object') { n += 6; return } if (Array.isArray(x)) { for (const y of x) walk(y); return } for (const k of Object.keys(x)) { if (SIZE_SKIPS.has(k)) continue; n += k.length + 4; walk(x[k]) } }
6857
+ walk(def)
6858
+ _defOwnChars.set(def, n)
6859
+ }
6860
+ return n
6861
+ }
6862
+
6253
6863
  function emitSharedDefC(defName, v, pathExpr, lines, ctx, schemaPrefix) {
6254
6864
  if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
6255
6865
  if (!ctx.sharedDefs) ctx.sharedDefs = sharedDefNames(ctx.rootSchema, ctx.rootDefs)
6256
6866
  const cyclic = ctx.cyclicDefs.has(defName)
6257
- if (!cyclic && !ctx.sharedDefs.has(defName)) return false
6867
+ // In the combined function every definition of some size is a function of
6868
+ // its own, referenced once or not: expanded, a 25-definition API schema
6869
+ // was one 122 KB function, which the optimizing compiler took tens of
6870
+ // thousands of calls to reach, so its first twenty thousand answers cost
6871
+ // 89 ns each where the hundred-thousandth costs 10. Functions the size of
6872
+ // one definition tier up early, and the hot ones first.
6873
+ if (!cyclic && !ctx.sharedDefs.has(defName) && !(ctx.inCombined && defOwnChars(ctx.rootDefs[defName]) >= COMBINED_DEF_FN_MIN)) return false
6258
6874
  emitDefFnC(defName, ctx.rootDefs[defName], cyclic, v, pathExpr, lines, ctx, schemaPrefix)
6259
6875
  return true
6260
6876
  }
@@ -6273,31 +6889,42 @@ function emitDefFnC(key, target, cyclic, v, pathExpr, lines, ctx, schemaPrefix)
6273
6889
  ctx.defFnsC.set(key, fnName)
6274
6890
  const outerHoisted = ctx.hoisted
6275
6891
  ctx.hoisted = []
6892
+ // The body is a function of its own: a reference the caller was in the
6893
+ // middle of expanding is not on the stack inside it, where reaching it
6894
+ // again is a call (this function, or another one) rather than a loop.
6895
+ // With the caller's stack, a definition expanded once on the way into
6896
+ // the root's function was taken for a cycle inside it, and the schema
6897
+ // (a tree whose nodes hold trees) lost its one-pass function.
6898
+ const outerRefs = ctx.refStack
6899
+ ctx.refStack = new Set()
6276
6900
  const bodyLines = []
6277
6901
  let hoisted
6278
6902
  try {
6903
+ ctx.inDefC = (ctx.inDefC || 0) + 1
6279
6904
  genCodeC(target, '_dv', '_p', bodyLines, ctx, SP_PLACEHOLDER)
6280
6905
  } finally {
6906
+ ctx.inDefC--
6281
6907
  hoisted = ctx.hoisted
6282
6908
  ctx.hoisted = outerHoisted
6909
+ ctx.refStack = outerRefs
6283
6910
  }
6284
6911
  let body = bodyLines.join('\n ')
6285
6912
  body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
6286
6913
  if (body.includes(SP_PLACEHOLDER)) throw DECLINE
6287
6914
  const decl = hoisted.length ? `let ${hoisted.map((n) => n + '=null').join(',')};` : ''
6288
- const fn = `function ${cyclic ? fnName + '_b' : fnName}(_dv,_p,_sp,d){const _all=true;${decl}let _e;\n ${body}\n return _e}`
6915
+ const fn = `function ${cyclic ? fnName + '_b' : fnName}(_dv,_p,_sp,d,_so){const _all=true;${decl}let _e;\n ${body}\n return _e}`
6289
6916
  if (cyclic) {
6290
6917
  if (!ctx.cycFnsC) ctx.cycFnsC = []
6291
6918
  ctx.cycFnsC.push(fnName)
6292
6919
  ctx.helperCode.push(
6293
- `const ${fnName}_s=new Set()\n function ${fnName}(_dv,_p,_sp,d){\n ` +
6294
- `if(_sg){if(typeof _dv!=='object'||_dv===null)return ${fnName}_b(_dv,_p,_sp,d);if(${fnName}_s.has(_dv))return;${fnName}_s.add(_dv);try{return ${fnName}_b(_dv,_p,_sp,d)}finally{${fnName}_s.delete(_dv)}}\n ` +
6295
- `if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(_dv,_p,_sp,d)\n _sd--\n return _r\n }\n ` + fn)
6920
+ `const ${fnName}_s=new Set()\n function ${fnName}(_dv,_p,_sp,d,_so){\n ` +
6921
+ `if(_sg){if(typeof _dv!=='object'||_dv===null)return ${fnName}_b(_dv,_p,_sp,d,_so);if(${fnName}_s.has(_dv))return;${fnName}_s.add(_dv);try{return ${fnName}_b(_dv,_p,_sp,d,_so)}finally{${fnName}_s.delete(_dv)}}\n ` +
6922
+ `if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(_dv,_p,_sp,d,_so)\n _sd--\n return _r\n }\n ` + fn)
6296
6923
  } else {
6297
6924
  ctx.helperCode.push(fn)
6298
6925
  }
6299
6926
  }
6300
- lines.push(`{const _r=${fnName}(${v},${pathExpr || '""'},'${schemaPrefix}',d);if(_r!==undefined){if(_e===undefined)_e=_r;else for(let _i=0;_i<_r.length;_i++)_e.push(_r[_i])}}`)
6927
+ lines.push(`{const _r=${fnName}(${v},${pathExpr || '""'},'${schemaPrefix}',d,${apOrd(ctx, `${schemaPrefix}/$ref`, true)});if(_r!==undefined){if(_e===undefined)_e=_r;else for(let _i=0;_i<_r.length;_i++)_e.push(_r[_i])}}`)
6301
6928
  }
6302
6929
 
6303
6930
  const REF_DONE = Symbol('ata.refDone')
@@ -6438,12 +7065,12 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6438
7065
  const lit = {keyword, instancePath: pathVal, schemaPath: spVal, params: Object.freeze(paramsVal), message: msgVal}
6439
7066
  if (o !== null) lit._o = o
6440
7067
  ctx.closureVals.push(sharedErr(lit, ctx.rootSchema))
6441
- return `(_e||(_e=[])).push(${errVar})`
7068
+ return `(_e=_ap(_A,_e,${errVar},${apOrd(ctx, sp)}))`
6442
7069
  }
6443
7070
  }
6444
7071
  }
6445
7072
  // Dynamic path (e.g., array index): inline as before
6446
- return `(_e||(_e=[])).push({keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}${ordinalField(ctx, sp)}})`
7073
+ return `(_e=_ap(_A,_e,{keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}${ordinalField(ctx, sp)}},${apOrd(ctx, sp)}))`
6447
7074
  }
6448
7075
 
6449
7076
  // enum and const apply to a value of any type, so a value that fails
@@ -6533,7 +7160,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6533
7160
  const pathVal = pathExpr ? literalValue(pathExpr) : ''
6534
7161
  ctx.closureVars.push(errVar)
6535
7162
  ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
6536
- lines.push(`if(${check}){(_e||(_e=[])).push(${errVar})}`)
7163
+ lines.push(`if(${check}){(_e=_ap(_A,_e,${errVar},${apOrd(ctx, `${schemaPrefix}/required`)}))}`)
6537
7164
  } else {
6538
7165
  lines.push(`if(${check}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
6539
7166
  }
@@ -6547,7 +7174,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6547
7174
  const pathVal = pathExpr ? literalValue(pathExpr) : ''
6548
7175
  ctx.closureVars.push(errVar)
6549
7176
  ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
6550
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e||(_e=[])).push(${errVar})}`)
7177
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e=_ap(_A,_e,${errVar},${apOrd(ctx, `${schemaPrefix}/required`)}))}`)
6551
7178
  } else {
6552
7179
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
6553
7180
  }
@@ -6646,8 +7273,12 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6646
7273
  const checks = propKeys.map(k => `_k${ci}[_i]!==${JSON.stringify(k)}`).join('&&') || 'true'
6647
7274
  body = `let _x${ci}=0;for(const _f${ci} in ${v}){if(${gate}){_x${ci}=1;break}}if(_x${ci}){${detail(checks)}}`
6648
7275
  } else {
6649
- const allowed = propKeys.map(k => JSON.stringify(k)).join(',')
6650
- body = `const _a${ci}=new Set([${allowed}]);let _x${ci}=0;for(const _f${ci} in ${v}){if(!_a${ci}.has(_f${ci})){_x${ci}=1;break}}if(_x${ci}){${detail(`!_a${ci}.has(_k${ci}[_i])`)}}`
7276
+ // The name set lives in the closure, built once with the function: built
7277
+ // in the body it was a 33-name Set on every call of this function, which
7278
+ // is where a rejected npm-badges document spent most of its 701 ns.
7279
+ ctx.closureVars.push(`_a${ci}`)
7280
+ ctx.closureVals.push(new Set(propKeys))
7281
+ body = `let _x${ci}=0;for(const _f${ci} in ${v}){if(!_a${ci}.has(_f${ci})){_x${ci}=1;break}}if(_x${ci}){${detail(`!_a${ci}.has(_k${ci}[_i])`)}}`
6651
7282
  }
6652
7283
  // When every declared property is required, a clean object has exactly the
6653
7284
  // declared keys: if the enumerated count equals the declared count and
@@ -6699,7 +7330,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6699
7330
  const pathVal = pathExpr ? literalValue(pathExpr) : ''
6700
7331
  ctx.closureVars.push(errVar)
6701
7332
  ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/dependentRequired`), params: Object.freeze({missingProperty: dep}), message: `must have required property '${dep}'`}, ctx.rootSchema))
6702
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e||(_e=[])).push(${errVar})}`)
7333
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e=_ap(_A,_e,${errVar},-1))}`)
6703
7334
  } else {
6704
7335
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){${errPushC(ctx, 'required', `${schemaPrefix}/dependentRequired`, pathExpr, `{missingProperty:'${esc(dep)}'}`, `"must have required property '${esc(dep)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired'${ordinalField(ctx, `${schemaPrefix}/dependentRequired`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"}`)}}`)
6705
7336
  }
@@ -6715,16 +7346,16 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6715
7346
  if (requiredSet.has(key) && isObj) {
6716
7347
  // A hoisted local was already cleared if the value is only inherited.
6717
7348
  lines.push(`if(${pv}!==undefined${hoisted[key] ? '' : '&&' + ownGuard(ctx, v, key)}){`)
6718
- genCodeC(prop, pv, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
7349
+ genGuardedC(prop, pv, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
6719
7350
  lines.push(`}`)
6720
7351
  } else if (isObj) {
6721
7352
  const oi = ctx.varCounter++
6722
7353
  lines.push(`{const _o${oi}=${v}[${JSON.stringify(key)}];if(_o${oi}!==undefined&&${ownGuard(ctx, v, key)}){`)
6723
- genCodeC(prop, `_o${oi}`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
7354
+ genGuardedC(prop, `_o${oi}`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
6724
7355
  lines.push(`}}`)
6725
7356
  } else {
6726
7357
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
6727
- genCodeC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
7358
+ genGuardedC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
6728
7359
  lines.push(`}`)
6729
7360
  }
6730
7361
  }
@@ -6754,7 +7385,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6754
7385
  ctx.varCounter++
6755
7386
  const childPath = childPathDynExpr(pathExpr, idx)
6756
7387
  lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
6757
- genCodeC(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items')
7388
+ genGuardedC(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items')
6758
7389
  lines.push(`}}`)
6759
7390
  }
6760
7391
 
@@ -6832,13 +7463,25 @@ function genUniqueItemsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, is
6832
7463
 
6833
7464
  // The verdict form of genUnevaluatedDynamicC: the same set, built the same
6834
7465
  // way, and the answer false at the first key outside it.
6835
- function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj) {
6836
- if (schema.additionalProperties !== undefined) return
7466
+ function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj, nonObjApplicators) {
7467
+ if (schema.additionalProperties !== undefined) {
7468
+ if (nonObjApplicators) throw DECLINE
7469
+ return
7470
+ }
6837
7471
  const ui = ctx.varCounter++
6838
7472
  const S = `_ev${ui}`
6839
7473
  const body = [`const ${S}=[]`]
6840
7474
  addLocalAnnotations(schema, v, S, body, ctx)
6841
- addTopApplicatorAnnotations(schema, v, S, body, ctx)
7475
+ // With the anyOf and oneOf blocks moved here (see genCodeNode), their
7476
+ // passing branches are counted by the annotation pass and judged from the
7477
+ // count: a oneOf holds with exactly one, an anyOf with at least one.
7478
+ const counts = nonObjApplicators ? { anyOf: `_ac${ui}`, oneOf: `_oc${ui}`, oneOfMask: null, of: null, verdict: true } : null
7479
+ if (counts) body.push(`let ${counts.anyOf}=0,${counts.oneOf}=0`)
7480
+ addTopApplicatorAnnotations(schema, v, S, body, ctx, counts)
7481
+ if (counts) {
7482
+ if (Array.isArray(schema.anyOf)) body.push(`if(${counts.anyOf}<1)return false`)
7483
+ if (Array.isArray(schema.oneOf)) body.push(`if(${counts.oneOf}!==1)return false`)
7484
+ }
6842
7485
  const kVar = `_uk${ui}`
6843
7486
  const up = schema.unevaluatedProperties
6844
7487
  if (up === false) {
@@ -6850,7 +7493,10 @@ function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj) {
6850
7493
  } else {
6851
7494
  throw DECLINE
6852
7495
  }
6853
- lines.push(isObj ? `{${body.join(';')}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body.join(';')}}`)
7496
+ // A value that is not an object skips the pass, so the blocks moved here
7497
+ // run for it in the `else`; a value known to be an object has no `else`.
7498
+ const other = nonObjApplicators && nonObjApplicators.length && !isObj ? `else{${nonObjApplicators.join('\n ')}}` : ''
7499
+ lines.push(isObj ? `{${body.join(';')}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body.join(';')}}${other}`)
6854
7500
  }
6855
7501
 
6856
7502
  // unevaluatedProperties where what is evaluated depends on which branches
@@ -6860,6 +7506,236 @@ function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj) {
6860
7506
  // branch, an if, then or else, or a $ref target counts only when it passes,
6861
7507
  // a oneOf only when exactly one branch passes, and `not` never. Each branch is
6862
7508
  // asked through an annotation function (emitAnnot).
7509
+ // Run-time unevaluatedProperties written as straight-line code. The names a
7510
+ // node and its in-place applicators can evaluate are known when the code is
7511
+ // written (their `properties`), so the evaluated set is a bit per name in an
7512
+ // integer, each branch's verdict a labelled block, and a failed oneOf's
7513
+ // rollback the integer it started from. The annotation functions this
7514
+ // replaces (emitAnnot) pushed names onto an array, asked it with `includes`
7515
+ // for every key, and were a call per branch per level: ten functions and
7516
+ // twenty calls for the suite's nested-reference case, which in the first
7517
+ // thousands of calls of a process, before anything is inlined, cost 548 ns
7518
+ // against 131. Null when the shape is not modelled: names that are only known
7519
+ // at run time (patternProperties, additionalProperties), a nested
7520
+ // unevaluatedProperties, a reference cycle, more than 30 names, or too much
7521
+ // code; the annotation functions then stand in as before.
7522
+ const ANNOT_INLINE_MAX = 8192
7523
+ const ANNOT_NAME_MAX = 30
7524
+ const ANNOT_BLOCKERS = ['unevaluatedItems', '$dynamicRef', '$recursiveRef', 'dependencies']
7525
+ // Besides a bit per declared name, the mask has a bit per pattern of a
7526
+ // patternProperties (set when the subschema holding it holds: the keys it
7527
+ // matches are evaluated) and one bit, `all`, for a subschema that evaluates
7528
+ // every key when it holds (additionalProperties of any value, or a nested
7529
+ // `unevaluatedProperties: true`). A nested `unevaluatedProperties` that
7530
+ // checks something is not modelled.
7531
+ const ALL_BIT_KEY = '\u0000all'
7532
+ function annotSubs (node, ctx) {
7533
+ const subs = []
7534
+ for (const b of node.allOf || []) subs.push(b)
7535
+ for (const b of node.anyOf || []) subs.push(b)
7536
+ for (const b of node.oneOf || []) subs.push(b)
7537
+ if (node.if !== undefined) subs.push(node.if)
7538
+ if (node.then !== undefined) subs.push(node.then)
7539
+ if (node.else !== undefined) subs.push(node.else)
7540
+ if (node.dependentSchemas && typeof node.dependentSchemas === 'object') for (const ds of Object.values(node.dependentSchemas)) subs.push(ds)
7541
+ if (typeof node.$ref === 'string') subs.push(refTargetFor(node.$ref, ctx))
7542
+ return subs
7543
+ }
7544
+ function annotNames (node, ctx, names, seen, top) {
7545
+ if (node === true || node === false) return true
7546
+ if (node === null || typeof node !== 'object' || Array.isArray(node)) return false
7547
+ if (seen.has(node)) return false
7548
+ seen.add(node)
7549
+ try {
7550
+ for (const k of ANNOT_BLOCKERS) if (node[k] !== undefined) return false
7551
+ if (!top && node.unevaluatedProperties !== undefined && node.unevaluatedProperties !== true) return false
7552
+ const add = (key) => { if (!names.has(key)) { if (names.size >= ANNOT_NAME_MAX) return false; names.set(key, names.size) } return true }
7553
+ if (node.properties && typeof node.properties === 'object') for (const k of Object.keys(node.properties)) if (!add(k)) return false
7554
+ if (node.patternProperties && typeof node.patternProperties === 'object') for (const pat of Object.keys(node.patternProperties)) if (!add('/' + pat)) return false
7555
+ if ((!top && node.additionalProperties !== undefined) || (!top && node.unevaluatedProperties === true)) if (!add(ALL_BIT_KEY)) return false
7556
+ let subs
7557
+ try { subs = annotSubs(node, ctx) } catch { return false }
7558
+ for (const b of subs) if (!annotNames(b, ctx, names, seen, false)) return false
7559
+ return true
7560
+ } finally { seen.delete(node) }
7561
+ }
7562
+ // The bits a subschema's own keywords evaluate when it holds: its declared
7563
+ // names that are present, every pattern it declares, and `all` for a
7564
+ // subschema that evaluates every key.
7565
+ function ownBits (sub, v, names, ctx, t, top) {
7566
+ const out = []
7567
+ for (const k of Object.keys(sub.properties || {})) out.push(`if(${ownKeyExpr(ctx, v, k)})${t}|=${1 << names.get(k)}`)
7568
+ let fixed = 0
7569
+ for (const pat of Object.keys(sub.patternProperties || {})) fixed |= 1 << names.get('/' + pat)
7570
+ if (!top && (sub.additionalProperties !== undefined || sub.unevaluatedProperties === true)) fixed |= 1 << names.get(ALL_BIT_KEY)
7571
+ if (fixed !== 0) out.push(`${t}|=${fixed}`)
7572
+ return out
7573
+ }
7574
+ // The test for one pattern bit against a key variable, a closure regex or a
7575
+ // fixed prefix test (fastPrefixCheck) as addLocalAnnotations writes them.
7576
+ function patternKeyTest (ctx, pat, kv) {
7577
+ const fast = fastPrefixCheck(pat, kv)
7578
+ if (fast) return fast
7579
+ const ri = ctx.varCounter++
7580
+ ctx.closureVars.push(`_re${ri}`)
7581
+ ctx.closureVals.push(safeReClosure(ctx, pat))
7582
+ return `_re${ri}.test(${kv})`
7583
+ }
7584
+ // Factory-scope count variables for one anyOf or oneOf array (by identity),
7585
+ // registered in ctx.branchCounts for emitBranchCollapse: `of` the object
7586
+ // they were counted for, `count` how many branches held, `mask` which (oneOf
7587
+ // up to 30 branches). Declared once per program; a stale value is never
7588
+ // taken, since every reader checks `of` against the value at hand.
7589
+ function factoryCount (ctx, arr, withMask) {
7590
+ if (!ctx.inCombined) return null
7591
+ if (!ctx.branchCounts) ctx.branchCounts = new Map()
7592
+ const hit = ctx.branchCounts.get(arr)
7593
+ if (hit !== undefined) return hit.factory ? hit : null
7594
+ const id = ctx.varCounter++
7595
+ const fc = { of: `_fa${id}`, count: `_fn${id}`, mask: withMask ? `_fm${id}` : null, factory: true }
7596
+ ctx.helperCode.push(`let ${fc.of}=null,${fc.count}=0${withMask ? `,${fc.mask}=0` : ''}`)
7597
+ ctx.branchCounts.set(arr, fc)
7598
+ return fc
7599
+ }
7600
+ // Lines that set `ok` (declared by the caller, 1 or 0) for `sub` at `v`, an
7601
+ // object, and OR the names it evaluated into `tv` when it holds.
7602
+ function emitAnnotInline (sub, v, names, ctx, out, ok, tv) {
7603
+ if (sub === true) { out.push(`${ok}=1`); return }
7604
+ if (sub === false) { out.push(`${ok}=0`); return }
7605
+ const id = ctx.varCounter++
7606
+ const t = `_t${id}`
7607
+ const local = {}
7608
+ for (const k of Object.keys(sub)) if (!ANNOT_APPLICATORS.has(k)) setOwn(local, k, sub[k])
7609
+ const vl = []
7610
+ // The value is known to be an object here (the block runs for objects
7611
+ // only), which the verdict is told when the branch does not say otherwise:
7612
+ // told a type, the generator takes it for the value's and skips the
7613
+ // branch's own `type`, so a `type: "string"` branch has to keep its check.
7614
+ ctx.nestedBoolean = (ctx.nestedBoolean || 0) + 1
7615
+ try { genCode(local, v, vl, ctx, local.type === undefined || local.type === 'object' ? 'object' : undefined) } finally { ctx.nestedBoolean-- }
7616
+ out.push(`let ${t}=0;${ok}=1`)
7617
+ if (vl.length) out.push(`a${id}:{${replaceTopLevel(vl.join(';'), `break a${id}`, `{${ok}=0;break a${id}}`)}}`)
7618
+ if (sub.not !== undefined) {
7619
+ const nl = []
7620
+ nestedGenCode(sub.not, v, nl, ctx)
7621
+ if (nl.length) out.push(`if(${ok}){let _nk${id}=1;n${id}:{${replaceTopLevel(nl.join(';'), `break n${id}`, `{_nk${id}=0;break n${id}}`)}};if(_nk${id})${ok}=0}`)
7622
+ else out.push(`${ok}=0`)
7623
+ }
7624
+ const own = ownBits(sub, v, names, ctx, t, false)
7625
+ if (own.length) out.push(`if(${ok}){${own.join(';')}}`)
7626
+ let bi = 0
7627
+ const child = (b) => { const o = `_ok${id}_${bi}`, tb = `_tb${id}_${bi}`; bi++; out.push(`let ${o}=1,${tb}=0`); emitAnnotInline(b, v, names, ctx, out, o, tb); return [o, tb] }
7628
+ for (const b of sub.allOf || []) { const [o, tb] = child(b); out.push(`if(!${o})${ok}=0;else ${t}|=${tb}`) }
7629
+ // The counts of a nested anyOf or oneOf go to factory-scope variables as
7630
+ // well (factoryCount), where the keyword's own error code in a branch
7631
+ // function, and the counting copies, read them instead of deciding the
7632
+ // branches again: the suite's nested-reference case decided each branch
7633
+ // four times over (this pass, the verdict closures, the counting copies,
7634
+ // the chosen branch's error function).
7635
+ if (Array.isArray(sub.anyOf)) {
7636
+ const na = `_na${id}`
7637
+ const fc = factoryCount(ctx, sub.anyOf, false)
7638
+ out.push(`let ${na}=0${fc ? `;${fc.of}=${v};${fc.count}=0` : ''}`)
7639
+ for (const b of sub.anyOf) { const [o, tb] = child(b); out.push(`if(${o}){${na}++;${t}|=${tb}${fc ? `;${fc.count}++` : ''}}`) }
7640
+ out.push(`if(${na}===0)${ok}=0`)
7641
+ }
7642
+ if (Array.isArray(sub.oneOf)) {
7643
+ const nc = `_nc${id}`, tc = `_tc${id}`
7644
+ const fc = factoryCount(ctx, sub.oneOf, sub.oneOf.length <= 30)
7645
+ out.push(`let ${nc}=0,${tc}=0${fc ? `;${fc.of}=${v};${fc.count}=0${fc.mask ? `;${fc.mask}=0` : ''}` : ''}`)
7646
+ sub.oneOf.forEach((b, i) => { const [o, tb] = child(b); out.push(`if(${o}){${nc}++;${tc}=${tb}${fc ? `;${fc.count}++${fc.mask ? `;${fc.mask}|=${1 << i}` : ''}` : ''}}`) })
7647
+ out.push(`if(${nc}===1)${t}|=${tc};else ${ok}=0`)
7648
+ }
7649
+ if (sub.if !== undefined) {
7650
+ const [oi, ti] = child(sub.if)
7651
+ const thenPart = []
7652
+ if (sub.then !== undefined) { const saved = out.length; const [ot, tt] = child(sub.then); thenPart.push(...out.splice(saved)); thenPart.push(`if(!${ot})${ok}=0;else ${t}|=${tt}`) }
7653
+ const elsePart = []
7654
+ if (sub.else !== undefined) { const saved = out.length; const [oe, te] = child(sub.else); elsePart.push(...out.splice(saved)); elsePart.push(`if(!${oe})${ok}=0;else ${t}|=${te}`) }
7655
+ out.push(`if(${oi}){${t}|=${ti};${thenPart.join(';')}}else{${elsePart.join(';')}}`)
7656
+ }
7657
+ if (sub.dependentSchemas && typeof sub.dependentSchemas === 'object') {
7658
+ for (const [k, ds] of Object.entries(sub.dependentSchemas)) { const saved = out.length; const [o, tb] = child(ds); const body = out.splice(saved); out.push(`if(${ownKeyExpr(ctx, v, k)}){${body.join(';')};if(!${o})${ok}=0;else ${t}|=${tb}}`) }
7659
+ }
7660
+ if (typeof sub.$ref === 'string') { const [o, tb] = child(refTargetFor(sub.$ref, ctx)); out.push(`if(!${o})${ok}=0;else ${t}|=${tb}`) }
7661
+ out.push(`if(${ok})${tv}|=${t}`)
7662
+ }
7663
+ // The whole run-time unevaluatedProperties block for `schema` at `v`, or
7664
+ // null. `counts` as in genUnevaluatedDynamicC: how many anyOf and oneOf
7665
+ // branches held, for the keywords' own code to read.
7666
+ function inlineUnevalBlock (schema, v, pathExpr, ctx, schemaPrefix, fail, counts) {
7667
+ const names = new Map()
7668
+ if (!annotNames(schema, ctx, names, new Set(), true)) return null
7669
+ const saved = snapshotCtx(ctx)
7670
+ try {
7671
+ const id = ctx.varCounter++
7672
+ const m = `_m${id}`
7673
+ const out = [`let ${m}=0`]
7674
+ // One prototype flag for the object, read by every presence test below.
7675
+ if (!ctx.plainOf) ctx.plainOf = new Map()
7676
+ const prevPk = ctx.plainOf.get(v)
7677
+ const pk = prevPk !== undefined ? prevPk : `_pk${id}`
7678
+ const prevReuse = ctx.plainReuse
7679
+ if (prevPk === undefined) { out.push(`const ${pk}=${protoIs(ctx, v)}`); ctx.plainOf.set(v, pk); (ctx.plainUsed || (ctx.plainUsed = new Set())).add(pk) }
7680
+ ctx.plainReuse = v
7681
+ out.push(...ownBits(schema, v, names, ctx, m, true))
7682
+ let bi = 0
7683
+ const child = (b) => { const o = `_ok${id}_${bi}`, tb = `_tb${id}_${bi}`; bi++; out.push(`let ${o}=1,${tb}=0`); emitAnnotInline(b, v, names, ctx, out, o, tb); return [o, tb] }
7684
+ for (const b of schema.allOf || []) { const [, tb] = child(b); out.push(`${m}|=${tb}`) }
7685
+ if (Array.isArray(schema.anyOf)) {
7686
+ const na = counts ? counts.anyOf : `_na${id}`
7687
+ out.push(counts ? `${na}=0` : `let ${na}=0`)
7688
+ for (const b of schema.anyOf) { const [o, tb] = child(b); out.push(`if(${o}){${na}++;${m}|=${tb}}`) }
7689
+ }
7690
+ if (Array.isArray(schema.oneOf)) {
7691
+ const nc = counts ? counts.oneOf : `_nc${id}`, tc = `_tc${id}`
7692
+ out.push(counts ? `${nc}=0;let ${tc}=0` : `let ${nc}=0,${tc}=0`)
7693
+ schema.oneOf.forEach((b, i) => { const [o, tb] = child(b); out.push(`if(${o}){${nc}++;${tc}=${tb};${counts && counts.oneOfMask && schema.oneOf.length <= 30 ? `${counts.oneOfMask}|=${1 << i};` : ''}}`) })
7694
+ out.push(`if(${nc}===1)${m}|=${tc}`)
7695
+ }
7696
+ if (schema.if !== undefined) {
7697
+ const [oi, ti] = child(schema.if)
7698
+ const thenPart = []
7699
+ if (schema.then !== undefined) { const sv = out.length; const [ot, tt] = child(schema.then); thenPart.push(...out.splice(sv)); thenPart.push(`if(${ot})${m}|=${tt}`) }
7700
+ const elsePart = []
7701
+ if (schema.else !== undefined) { const sv = out.length; const [oe, te] = child(schema.else); elsePart.push(...out.splice(sv)); elsePart.push(`if(${oe})${m}|=${te}`) }
7702
+ out.push(`if(${oi}){${m}|=${ti};${thenPart.join(';')}}else{${elsePart.join(';')}}`)
7703
+ }
7704
+ if (schema.dependentSchemas && typeof schema.dependentSchemas === 'object') {
7705
+ for (const [k, ds] of Object.entries(schema.dependentSchemas)) { const sv = out.length; const [o, tb] = child(ds); const body = out.splice(sv); out.push(`if(${ownKeyExpr(ctx, v, k)}){${body.join(';')};if(${o})${m}|=${tb}}`) }
7706
+ }
7707
+ if (typeof schema.$ref === 'string') { const [o, tb] = child(refTargetFor(schema.$ref, ctx)); out.push(`if(${o})${m}|=${tb}`) }
7708
+ // What is left: every own key whose bit is not set.
7709
+ const kVar = `_uk${id}`
7710
+ const cases = [...names].filter(([k]) => k !== ALL_BIT_KEY && k[0] !== '/').map(([k, i]) => `case ${JSON.stringify(k)}:_ub${id}=${1 << i};break`).join(';')
7711
+ const allBit = names.has(ALL_BIT_KEY) ? `if((${m}&${1 << names.get(ALL_BIT_KEY)})!==0)continue;` : ''
7712
+ const patBits = [...names].filter(([k]) => k[0] === '/').map(([k, i]) => `if((${m}&${1 << i})!==0&&${patternKeyTest(ctx, k.slice(1), `_uk${id}`)})continue;`).join('')
7713
+ const up = schema.unevaluatedProperties
7714
+ let body
7715
+ if (up === false) {
7716
+ body = fail('unevaluatedProperties', 'unevaluatedProperties', `{unevaluatedProperty:${kVar}}`, "'must NOT have unevaluated properties'")
7717
+ } else if (typeof up === 'object' && up !== null) {
7718
+ const pe = emitPtrEsc(ctx)
7719
+ const p = pathExpr ? `${pathExpr}+'/'+${pe}(${kVar})` : `'/'+${pe}(${kVar})`
7720
+ const sub = []
7721
+ genCodeC(up, `${v}[${kVar}]`, p, sub, ctx, schemaPrefix + '/unevaluatedProperties')
7722
+ body = sub.join('\n ')
7723
+ } else {
7724
+ throw DECLINE
7725
+ }
7726
+ if (body) out.push(`for(const ${kVar} in ${v}){if(!${ownKeyExpr(ctx, v, kVar).split(JSON.stringify(kVar)).join(kVar)})continue;let _ub${id}=0;switch(${kVar}){${cases}}if(_ub${id}!==0&&(${m}&_ub${id})!==0)continue;${allBit}${patBits}${body}}`)
7727
+ const text = out.join(';')
7728
+ ctx.plainReuse = prevReuse
7729
+ if (prevPk === undefined) ctx.plainOf.delete(v)
7730
+ if (text.length > ANNOT_INLINE_MAX || /\b(?:_validate|_stk|_sd|_sg|_CYC)\b/.test(text)) { restoreCtx(ctx, saved); return null }
7731
+ return text
7732
+ } catch (e) {
7733
+ if (e !== DECLINE) throw e
7734
+ restoreCtx(ctx, saved)
7735
+ return null
7736
+ }
7737
+ }
7738
+
6863
7739
  function genUnevaluatedDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
6864
7740
  // additionalProperties evaluates every key the others do not, pass or fail.
6865
7741
  if (schema.additionalProperties !== undefined) return
@@ -6883,6 +7759,12 @@ function genUnevaluatedDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, f
6883
7759
  if (Array.isArray(schema.anyOf)) ctx.branchCounts.set(schema.anyOf, { of: counts.of, count: counts.anyOf, mask: null })
6884
7760
  if (Array.isArray(schema.oneOf)) ctx.branchCounts.set(schema.oneOf, { of: counts.of, count: counts.oneOf, mask: schema.oneOf.length <= 30 ? counts.oneOfMask : null })
6885
7761
  }
7762
+ const inline = inlineUnevalBlock(schema, v, pathExpr, ctx, schemaPrefix, fail, counts)
7763
+ if (inline !== null) {
7764
+ const pre = share ? `${counts.of}=${v};${counts.oneOfMask}=0;` : ''
7765
+ lines.push(isObj ? `{${pre}${inline}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${pre}${inline}}`)
7766
+ return
7767
+ }
6886
7768
  addTopApplicatorAnnotations(schema, v, S, body, ctx, counts)
6887
7769
  const kVar = `_uk${ui}`
6888
7770
  const up = schema.unevaluatedProperties
@@ -6945,7 +7827,9 @@ function addTopApplicatorAnnotations(node, v, S, out, ctx, counts) {
6945
7827
  const t = counts ? counts.oneOf : `_oc${ctx.varCounter++}`
6946
7828
  // Which branches passed, as bits, for a collapse that has to name them.
6947
7829
  const mask = counts && node.oneOf.length <= 30 ? counts.oneOfMask : null
6948
- out.push(`{const ${t}m=${S}.length;${counts ? '' : 'let '}${t}=0;${node.oneOf.map((b, i) => `if(${emitAnnot(b, ctx)}(${v},${S})){${t}++;${mask ? `${mask}|=${1 << i};` : ''}}`).join('')}if(${t}!==1)_rb(${S},${t}m)}`)
7830
+ // In a verdict (counts.verdict), a second passing branch is the answer.
7831
+ const over = counts && counts.verdict ? `if(${t}>1)return false;` : ''
7832
+ out.push(`{const ${t}m=${S}.length;${counts ? '' : 'let '}${t}=0;${node.oneOf.map((b, i) => `if(${emitAnnot(b, ctx)}(${v},${S})){${t}++;${over}${mask ? `${mask}|=${1 << i};` : ''}}`).join('')}if(${t}!==1)_rb(${S},${t}m)}`)
6949
7833
  }
6950
7834
  if (node.if !== undefined) {
6951
7835
  const thenCall = node.then !== undefined ? `${emitAnnot(node.then, ctx)}(${v},${S})` : ''
@@ -6982,7 +7866,8 @@ function emitAnnot(sub, ctx) {
6982
7866
  if (sub === null || typeof sub !== 'object') throw DECLINE
6983
7867
  if (!ctx.annFns) {
6984
7868
  ctx.annFns = new Map()
6985
- ;(ctx.preamble || ctx.helperCode).push('function _anT(){return true}function _anF(){return false}function _rb(S,m){if(S.length!==m)S.length=m}')
7869
+ if (ctx.sharedRuntime) bindRt(ctx, ['_anT', '_anF', '_rb'])
7870
+ else (ctx.preamble || ctx.helperCode).push('function _anT(){return true}function _anF(){return false}function _rb(S,m){if(S.length!==m)S.length=m}')
6986
7871
  }
6987
7872
  const hit = ctx.annFns.get(sub)
6988
7873
  if (hit !== undefined) {
@@ -7014,7 +7899,8 @@ function emitAnnot(sub, ctx) {
7014
7899
  const failOut = '{_rb(S,_m);return false}'
7015
7900
  for (const b of sub.allOf || []) body.push(`if(!${emitAnnot(b, ctx)}(d,S))${failOut}`)
7016
7901
  if (Array.isArray(sub.anyOf)) body.push(`{let _a=false;${sub.anyOf.map((b) => `if(${emitAnnot(b, ctx)}(d,S))_a=true;`).join('')}if(!_a)${failOut}}`)
7017
- if (Array.isArray(sub.oneOf)) body.push(`{let _c=0;${sub.oneOf.map((b) => `if(${emitAnnot(b, ctx)}(d,S))_c++;`).join('')}if(_c!==1)${failOut}}`)
7902
+ // A second passing branch settles the oneOf; the rest are not run.
7903
+ if (Array.isArray(sub.oneOf)) body.push(`{let _c=0;${sub.oneOf.map((b) => `if(${emitAnnot(b, ctx)}(d,S)&&++_c>1)${failOut}`).join('')}if(_c!==1)${failOut}}`)
7018
7904
  if (sub.if !== undefined) {
7019
7905
  const thenCheck = sub.then !== undefined ? `if(!${emitAnnot(sub.then, ctx)}(d,S))${failOut}` : ''
7020
7906
  const elseCheck = sub.else !== undefined ? `if(!${emitAnnot(sub.else, ctx)}(d,S))${failOut}` : ''
@@ -7055,9 +7941,153 @@ function emitAnnot(sub, ctx) {
7055
7941
  // subschema marks them only when it passes (emitAnnotI); what is left must
7056
7942
  // pass unevaluatedItems. `false` reports once, at the first position left,
7057
7943
  // as the interpreter does.
7944
+ // Run-time unevaluatedItems written as straight-line code, the way
7945
+ // inlineUnevalBlock writes unevaluatedProperties. Without `contains` the
7946
+ // positions a node and its in-place applicators evaluate are a prefix: items
7947
+ // evaluates every position, prefixItems the first p, a nested
7948
+ // `unevaluatedItems: true` every position once its subschema holds; the
7949
+ // record is one integer, merged with max where a branch holds, and what is
7950
+ // left starts at that index. `contains` (positions that are not a prefix), a
7951
+ // tuple `items`, a nested unevaluatedItems that checks something, dynamic
7952
+ // references and reference cycles keep the annotation functions.
7953
+ const ANNOT_I_BLOCKERS = ['contains', 'additionalItems', '$dynamicRef', '$recursiveRef', 'dependencies']
7954
+ function annotItemsFit (node, ctx, seen, top) {
7955
+ if (node === true || node === false) return true
7956
+ if (node === null || typeof node !== 'object' || Array.isArray(node)) return false
7957
+ if (seen.has(node)) return false
7958
+ seen.add(node)
7959
+ try {
7960
+ for (const k of ANNOT_I_BLOCKERS) if (node[k] !== undefined) return false
7961
+ if (Array.isArray(node.items)) return false
7962
+ if (!top && node.unevaluatedItems !== undefined && node.unevaluatedItems !== true) return false
7963
+ if (node.unevaluatedProperties !== undefined && node.unevaluatedProperties !== true) return false
7964
+ let subs
7965
+ try { subs = annotSubs(node, ctx) } catch { return false }
7966
+ for (const b of subs) if (!annotItemsFit(b, ctx, seen, false)) return false
7967
+ return true
7968
+ } finally { seen.delete(node) }
7969
+ }
7970
+ // The positions a node's own keywords evaluate, as an expression of the
7971
+ // array length `len`: null when none.
7972
+ function ownPrefix (node, len, top) {
7973
+ if (node.items !== undefined) return len
7974
+ if (!top && node.unevaluatedItems === true) return len
7975
+ if (Array.isArray(node.prefixItems) && node.prefixItems.length) return `(${len}<${node.prefixItems.length}?${len}:${node.prefixItems.length})`
7976
+ return null
7977
+ }
7978
+ // Lines that set `ok` for `sub` at `v`, an array of length `len`, and raise
7979
+ // `tn` to the positions it evaluated when it holds.
7980
+ function emitAnnotInlineI (sub, v, len, ctx, out, ok, tn) {
7981
+ if (sub === true) { out.push(`${ok}=1`); return }
7982
+ if (sub === false) { out.push(`${ok}=0`); return }
7983
+ const id = ctx.varCounter++
7984
+ const t = `_t${id}`
7985
+ const local = {}
7986
+ for (const k of Object.keys(sub)) if (!ANNOT_ITEM_APPLICATORS.has(k)) setOwn(local, k, sub[k])
7987
+ const vl = []
7988
+ ctx.nestedBoolean = (ctx.nestedBoolean || 0) + 1
7989
+ try { genCode(local, v, vl, ctx, local.type === undefined || local.type === 'array' ? 'array' : undefined) } finally { ctx.nestedBoolean-- }
7990
+ out.push(`let ${t}=0;${ok}=1`)
7991
+ if (vl.length) out.push(`a${id}:{${replaceTopLevel(vl.join(';'), `break a${id}`, `{${ok}=0;break a${id}}`)}}`)
7992
+ if (sub.not !== undefined) {
7993
+ const nl = []
7994
+ nestedGenCode(sub.not, v, nl, ctx)
7995
+ if (nl.length) out.push(`if(${ok}){let _nk${id}=1;n${id}:{${replaceTopLevel(nl.join(';'), `break n${id}`, `{_nk${id}=0;break n${id}}`)}};if(_nk${id})${ok}=0}`)
7996
+ else out.push(`${ok}=0`)
7997
+ }
7998
+ const own = ownPrefix(sub, len, false)
7999
+ if (own !== null) out.push(`if(${ok})${t}=${own}`)
8000
+ let bi = 0
8001
+ const child = (b) => { const o = `_ok${id}_${bi}`, tb = `_tb${id}_${bi}`; bi++; out.push(`let ${o}=1,${tb}=0`); emitAnnotInlineI(b, v, len, ctx, out, o, tb); return [o, tb] }
8002
+ const raise = (tb) => `if(${tb}>${t})${t}=${tb}`
8003
+ for (const b of sub.allOf || []) { const [o, tb] = child(b); out.push(`if(!${o})${ok}=0;else ${raise(tb)}`) }
8004
+ if (Array.isArray(sub.anyOf)) {
8005
+ const na = `_na${id}`
8006
+ const fc = factoryCount(ctx, sub.anyOf, false)
8007
+ out.push(`let ${na}=0${fc ? `;${fc.of}=${v};${fc.count}=0` : ''}`)
8008
+ for (const b of sub.anyOf) { const [o, tb] = child(b); out.push(`if(${o}){${na}++;${raise(tb)}${fc ? `;${fc.count}++` : ''}}`) }
8009
+ out.push(`if(${na}===0)${ok}=0`)
8010
+ }
8011
+ if (Array.isArray(sub.oneOf)) {
8012
+ const nc = `_nc${id}`, tc = `_tc${id}`
8013
+ const fc = factoryCount(ctx, sub.oneOf, sub.oneOf.length <= 30)
8014
+ out.push(`let ${nc}=0,${tc}=0${fc ? `;${fc.of}=${v};${fc.count}=0${fc.mask ? `;${fc.mask}=0` : ''}` : ''}`)
8015
+ sub.oneOf.forEach((b, i) => { const [o, tb] = child(b); out.push(`if(${o}){${nc}++;${tc}=${tb}${fc ? `;${fc.count}++${fc.mask ? `;${fc.mask}|=${1 << i}` : ''}` : ''}}`) })
8016
+ out.push(`if(${nc}===1){${raise(tc)}}else ${ok}=0`)
8017
+ }
8018
+ if (sub.if !== undefined) {
8019
+ const [oi, ti] = child(sub.if)
8020
+ const thenPart = []
8021
+ if (sub.then !== undefined) { const sv = out.length; const [ot, tt] = child(sub.then); thenPart.push(...out.splice(sv)); thenPart.push(`if(!${ot})${ok}=0;else ${raise(tt)}`) }
8022
+ const elsePart = []
8023
+ if (sub.else !== undefined) { const sv = out.length; const [oe, te] = child(sub.else); elsePart.push(...out.splice(sv)); elsePart.push(`if(!${oe})${ok}=0;else ${raise(te)}`) }
8024
+ out.push(`if(${oi}){${raise(ti)};${thenPart.join(';')}}else{${elsePart.join(';')}}`)
8025
+ }
8026
+ if (typeof sub.$ref === 'string') { const [o, tb] = child(refTargetFor(sub.$ref, ctx)); out.push(`if(!${o})${ok}=0;else ${raise(tb)}`) }
8027
+ out.push(`if(${ok}&&${t}>${tn})${tn}=${t}`)
8028
+ }
8029
+ function inlineUnevalItemsBlock (schema, v, pathExpr, ctx, schemaPrefix, fail) {
8030
+ if (!ctx.inCombined || !annotItemsFit(schema, ctx, new Set(), true)) return null
8031
+ const saved = snapshotCtx(ctx)
8032
+ try {
8033
+ const id = ctx.varCounter++
8034
+ const n = `_un${id}`, len = `_ul${id}`
8035
+ const out = [`const ${len}=${v}.length;let ${n}=0`]
8036
+ const own = ownPrefix(schema, len, true)
8037
+ if (own !== null) out.push(`${n}=${own}`)
8038
+ let bi = 0
8039
+ const child = (b) => { const o = `_ok${id}_${bi}`, tb = `_tb${id}_${bi}`; bi++; out.push(`let ${o}=1,${tb}=0`); emitAnnotInlineI(b, v, len, ctx, out, o, tb); return [o, tb] }
8040
+ const raise = (tb) => `if(${tb}>${n})${n}=${tb}`
8041
+ for (const b of schema.allOf || []) { const [, tb] = child(b); out.push(raise(tb)) }
8042
+ if (Array.isArray(schema.anyOf)) {
8043
+ const fc = factoryCount(ctx, schema.anyOf, false)
8044
+ if (fc) out.push(`${fc.of}=${v};${fc.count}=0`)
8045
+ for (const b of schema.anyOf) { const [o, tb] = child(b); out.push(`if(${o}){${raise(tb)}${fc ? `;${fc.count}++` : ''}}`) }
8046
+ }
8047
+ if (Array.isArray(schema.oneOf)) {
8048
+ const fc = factoryCount(ctx, schema.oneOf, schema.oneOf.length <= 30)
8049
+ const nc = `_nc${id}`, tc = `_tc${id}`
8050
+ out.push(`let ${nc}=0,${tc}=0${fc ? `;${fc.of}=${v};${fc.count}=0${fc.mask ? `;${fc.mask}=0` : ''}` : ''}`)
8051
+ schema.oneOf.forEach((b, i) => { const [o, tb] = child(b); out.push(`if(${o}){${nc}++;${tc}=${tb}${fc ? `;${fc.count}++${fc.mask ? `;${fc.mask}|=${1 << i}` : ''}` : ''}}`) })
8052
+ out.push(`if(${nc}===1)${raise(tc)}`)
8053
+ }
8054
+ if (schema.if !== undefined) {
8055
+ const [oi, ti] = child(schema.if)
8056
+ const thenPart = []
8057
+ if (schema.then !== undefined) { const sv = out.length; const [ot, tt] = child(schema.then); thenPart.push(...out.splice(sv)); thenPart.push(`if(${ot})${raise(tt)}`) }
8058
+ const elsePart = []
8059
+ if (schema.else !== undefined) { const sv = out.length; const [oe, te] = child(schema.else); elsePart.push(...out.splice(sv)); elsePart.push(`if(${oe})${raise(te)}`) }
8060
+ out.push(`if(${oi}){${raise(ti)};${thenPart.join(';')}}else{${elsePart.join(';')}}`)
8061
+ }
8062
+ if (typeof schema.$ref === 'string') { const [o, tb] = child(refTargetFor(schema.$ref, ctx)); out.push(`if(${o})${raise(tb)}`) }
8063
+ const iv = `_ix${id}`
8064
+ const ui2 = schema.unevaluatedItems
8065
+ if (ui2 === false) {
8066
+ // Reported once, at the first position left, as the interpreter does.
8067
+ out.push(`if(${n}<${len}){const ${iv}=${n};${fail('unevaluatedItems', 'unevaluatedItems', `{limit:${iv}}`, `'must NOT have more than '+${iv}+' items'`)}}`)
8068
+ } else if (typeof ui2 === 'object' && ui2 !== null) {
8069
+ const pe = pathExpr ? `${pathExpr}+'/'+${iv}` : `'/'+${iv}`
8070
+ const sub = []
8071
+ genCodeC(ui2, `${v}[${iv}]`, pe, sub, ctx, schemaPrefix + '/unevaluatedItems')
8072
+ if (sub.length) out.push(`for(let ${iv}=${n};${iv}<${len};${iv}++){${sub.join('\n ')}\n }`)
8073
+ } else {
8074
+ throw DECLINE
8075
+ }
8076
+ const text = out.join(';')
8077
+ if (text.length > ANNOT_INLINE_MAX || /\b(?:_validate|_stk|_sd|_sg|_CYC)\b/.test(text)) { restoreCtx(ctx, saved); return null }
8078
+ return text
8079
+ } catch (e) {
8080
+ if (e !== DECLINE) throw e
8081
+ restoreCtx(ctx, saved)
8082
+ return null
8083
+ }
8084
+ }
8085
+
7058
8086
  function genUnevaluatedItemsDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
7059
8087
  if (schema.$dynamicRef !== undefined || schema.$recursiveRef !== undefined) throw DECLINE
7060
8088
  if (Array.isArray(schema.items) || schema.additionalItems !== undefined) throw DECLINE
8089
+ const inline = inlineUnevalItemsBlock(schema, v, pathExpr, ctx, schemaPrefix, fail)
8090
+ if (inline !== null) { lines.push(`if(Array.isArray(${v})){${inline}}`); return }
7061
8091
  annItemHelpers(ctx)
7062
8092
  const ui = ctx.varCounter++
7063
8093
  const S = `_es${ui}`
@@ -7164,7 +8194,7 @@ function emitAnnotI(sub, ctx) {
7164
8194
  if (locals.length) body.push(`if(Array.isArray(d)){${locals.join(';')}}`)
7165
8195
  for (const b of sub.allOf || []) body.push(`if(!${emitAnnotI(b, ctx)}(d,${R}))${failOut}`)
7166
8196
  if (Array.isArray(sub.anyOf)) body.push(`{let _a=false;${sub.anyOf.map((b) => `if(${emitAnnotI(b, ctx)}(d,${R}))_a=true;`).join('')}if(!_a)${failOut}}`)
7167
- if (Array.isArray(sub.oneOf)) body.push(`{let _c=0;${sub.oneOf.map((b) => `if(${emitAnnotI(b, ctx)}(d,${R}))_c++;`).join('')}if(_c!==1)${failOut}}`)
8197
+ if (Array.isArray(sub.oneOf)) body.push(`{let _c=0;${sub.oneOf.map((b) => `if(${emitAnnotI(b, ctx)}(d,${R})&&++_c>1)${failOut}`).join('')}if(_c!==1)${failOut}}`)
7168
8198
  if (sub.if !== undefined) {
7169
8199
  const thenCheck = sub.then !== undefined ? `if(!${emitAnnotI(sub.then, ctx)}(d,${R}))${failOut}` : ''
7170
8200
  const elseCheck = sub.else !== undefined ? `if(!${emitAnnotI(sub.else, ctx)}(d,${R}))${failOut}` : ''
@@ -7206,8 +8236,9 @@ function genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fai
7206
8236
  const checksU = propKeysU.map(k => `_k${ui}[_i]!==${JSON.stringify(k)}`).join('&&')
7207
8237
  innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++)if(${checksU}){${failU}}`
7208
8238
  } else {
7209
- const allowedU = propKeysU.map(k => JSON.stringify(k)).join(',')
7210
- innerU = `const _k${ui}=Object.keys(${v});const _a${ui}=new Set([${allowedU}]);for(let _i=0;_i<_k${ui}.length;_i++)if(!_a${ui}.has(_k${ui}[_i])){${failU}}`
8239
+ ctx.closureVars.push(`_a${ui}`)
8240
+ ctx.closureVals.push(new Set(propKeysU))
8241
+ innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++)if(!_a${ui}.has(_k${ui}[_i])){${failU}}`
7211
8242
  }
7212
8243
  lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
7213
8244
  }
@@ -7609,5 +8640,7 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
7609
8640
  }
7610
8641
 
7611
8642
  module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, unevalContributions, expandedChars, AJV_MESSAGES, _setCodeBudget }
8643
+ // For tests: the dynamic-scope unrolling, to hold its output to the interpreter.
8644
+ Object.defineProperty(module.exports, '_expandDynamicScopes', { value: expandDynamicScopes, enumerable: false })
7612
8645
  // For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
7613
8646
  Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })