ata-validator 1.32.3 → 1.33.1

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.
@@ -1002,9 +1002,13 @@ function withPlain (schema, v, lines, ctx, run) {
1002
1002
  // The same test at run time, for the closure compiler.
1003
1003
  const _hop = Object.prototype.hasOwnProperty
1004
1004
  const _PN = ({}).__proto__ === Object.prototype
1005
+ // One hasOwnProperty call. From 1.31.2 to 1.33.0 this first tested the name
1006
+ // against Object.prototype's names, then `in`, then read the prototype through
1007
+ // the __proto__ accessor, on every property of every object the eval-free
1008
+ // engine checked: a schema of 14 properties went from 641 to 828 ns a verdict.
1009
+ // hasOwnProperty gives the same answer and V8 inlines it.
1005
1010
  function hasOwnKey (d, key) {
1006
- if (PROTO_NAMES.has(key)) return _hop.call(d, key)
1007
- return key in d && ((_PN ? d.__proto__ : Object.getPrototypeOf(d)) === Object.prototype || _hop.call(d, key))
1011
+ return _hop.call(d, key)
1008
1012
  }
1009
1013
 
1010
1014
  const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'toString', 'valueOf',
@@ -1961,6 +1965,17 @@ function replaceTopLevel(code, onTrue = 'return R', onFalse = 'return E(d)') {
1961
1965
 
1962
1966
  // Returns true if a property sub-schema will generate 2+ lines that each access v,
1963
1967
  // meaning a local variable hoist is worthwhile.
1968
+ // A required object or array whose checks read it many times: into a local,
1969
+ // once. Reading \`d["customer"]\` afresh on every line cost a plain order
1970
+ // verdict 42 percent, since a helper call between two reads (a prototype
1971
+ // check, a length count) keeps V8 from reusing the first. Composition keeps
1972
+ // the direct access, the same line needsLocal draws.
1973
+ function isPlainContainer(schema) {
1974
+ if (typeof schema !== 'object' || schema === null) return false
1975
+ if (schema.$ref || schema.$dynamicRef || schema.allOf || schema.anyOf || schema.oneOf || schema.if || schema.not) return false
1976
+ return !!(schema.properties || schema.items || schema.prefixItems)
1977
+ }
1978
+
1964
1979
  function needsLocal(schema) {
1965
1980
  if (typeof schema !== 'object' || schema === null) return false
1966
1981
  // If it has $ref, allOf, anyOf etc., genCode handles it — don't hoist
@@ -2109,121 +2124,11 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2109
2124
  // Only when THIS schema has unevaluated keywords directly (not via $ref target)
2110
2125
  const hasSiblings = schema.$ref && (schema.unevaluatedProperties !== undefined || schema.unevaluatedItems !== undefined)
2111
2126
  if (schema.$ref) {
2112
- // Self-reference "#" — recursive call to root validator
2113
- if (schema.$ref === '#') {
2114
- if (ctx.nestedBoolean) throw DECLINE
2115
- ctx.usesRecursion = true
2116
- lines.push(`if(!_validate(${v}))return false`)
2117
- if (!hasSiblings) return
2118
- }
2119
- // 1. Local ref
2120
- const m = schema.$ref !== '#' && schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
2121
- if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
2122
- const defName = m[1]
2123
- if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
2124
- if (ctx.cyclicDefs.has(defName) && ctx.preamble) {
2125
- // A def on a cycle is a named function in the preamble, emitted once
2126
- // and called by name, so recursion terminates the way `$ref: "#"`
2127
- // does at the root. The name is registered before the body is
2128
- // generated so a self-reference inside it becomes a call.
2129
- if (!ctx.defFns) ctx.defFns = new Map()
2130
- let fnName = ctx.defFns.get(defName)
2131
- if (!fnName) {
2132
- fnName = '_def' + ctx.defFns.size + '_' + defName.replace(/[^A-Za-z0-9_]/g, '_')
2133
- ctx.defFns.set(defName, fnName)
2134
- const bodyLines = []
2135
- // The def is compiled as its own function over its own `d`, so the
2136
- // checks _deferOrInline holds back belong at the end of this body.
2137
- // The list hangs off ctx, so without the swap they are flushed into
2138
- // whatever function is compiling, carrying this def's key set.
2139
- const outerDeferred = ctx.deferredChecks
2140
- ctx.deferredChecks = null
2141
- genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)
2142
- if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
2143
- ctx.deferredChecks = outerDeferred
2144
- // Entered through the cycle guard: see emitGuardState. In the fast
2145
- // pass the wrapper only counts depth; the set is used by the second,
2146
- // guarded pass that runs when data turns out to point back at
2147
- // itself.
2148
- ctx.preamble.push(
2149
- `const ${fnName}_s=new Set()\n function ${fnName}(d){\n ` +
2150
- `if(_sg){if(typeof d!=='object'||d===null)return ${fnName}_b(d);if(${fnName}_s.has(d))return true;${fnName}_s.add(d);try{return ${fnName}_b(d)}finally{${fnName}_s.delete(d)}}\n ` +
2151
- `if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(d)\n _sd--\n return _r\n }\n ` +
2152
- `function ${fnName}_b(d){${bodyLines.join('\n ')}\n return true}`,
2153
- )
2154
- }
2155
- lines.push(`if(!${fnName}(${v}))return false`)
2156
- if (!hasSiblings) return
2157
- } else if (ctx.refStack.has(schema.$ref)) { if (ctx.nestedBoolean) throw DECLINE; if (!hasSiblings) return }
2158
- else {
2159
- ctx.refStack.add(schema.$ref)
2160
- genCode(ctx.rootDefs[defName], v, lines, ctx, knownType)
2161
- ctx.refStack.delete(schema.$ref)
2162
- if (!hasSiblings) return
2163
- }
2164
- } else if (schema.$ref !== '#' && !m && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')) {
2165
- // Anchor ref: "#foo" — resolve via rootDefs or anchors map
2166
- const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
2167
- const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
2168
- if (anchorTarget) {
2169
- if (ctx.refStack.has(schema.$ref)) { if (ctx.nestedBoolean) throw DECLINE; if (!hasSiblings) return }
2170
- else {
2171
- ctx.refStack.add(schema.$ref)
2172
- genCode(anchorTarget, v, lines, ctx, knownType)
2173
- ctx.refStack.delete(schema.$ref)
2174
- if (!hasSiblings) return
2175
- }
2176
- }
2177
- } else if (schema.$ref !== '#' && ctx.schemaMap) {
2178
- // 2. Cross-schema ref (exact match, relative URI, or JSON pointer fragment)
2179
- let resolved = ctx.schemaMap.get(schema.$ref)
2180
- if (!resolved && !schema.$ref.includes('://') && !schema.$ref.startsWith('#')) {
2181
- for (const [id, s] of ctx.schemaMap) {
2182
- if (id.endsWith('/' + schema.$ref)) { resolved = s; break }
2183
- }
2184
- }
2185
- if (!resolved && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
2186
- const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
2187
- if (r) resolved = r.schema
2188
- }
2189
- if (resolved) {
2190
- if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
2191
- else {
2192
- ctx.refStack.add(schema.$ref)
2193
- genCode(resolved, v, lines, ctx, knownType)
2194
- ctx.refStack.delete(schema.$ref)
2195
- if (!hasSiblings) return
2196
- }
2197
- } else {
2198
- if (!hasSiblings) return
2199
- }
2200
- } else {
2201
- if (!hasSiblings) return
2202
- }
2127
+ if (genRefNode(schema, v, lines, ctx, knownType, hasSiblings)) return
2203
2128
  }
2204
2129
 
2205
2130
  // $dynamicRef — resolve via anchors map
2206
- if (schema.$dynamicRef) {
2207
- const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
2208
- if (ctx.anchors && ctx.anchors[anchorKey]) {
2209
- const target = ctx.anchors[anchorKey]
2210
- if (target === ctx.rootSchema) {
2211
- // Self-recursive: generate _validate(v) call
2212
- if (ctx.nestedBoolean) throw DECLINE
2213
- ctx.usesRecursion = true
2214
- lines.push(`if(!_validate(${v}))return false`)
2215
- } else {
2216
- // Different schema: inline the target validation
2217
- const refKey = '$dynamicRef:' + anchorKey
2218
- if (ctx.refStack.has(refKey) && ctx.nestedBoolean) throw DECLINE
2219
- if (!ctx.refStack.has(refKey)) {
2220
- ctx.refStack.add(refKey)
2221
- genCode(target, v, lines, ctx, knownType)
2222
- ctx.refStack.delete(refKey)
2223
- }
2224
- }
2225
- }
2226
- }
2131
+ if (schema.$dynamicRef) genDynamicRefNode(schema, v, lines, ctx, knownType)
2227
2132
 
2228
2133
  // Determine the single known type after this schema's type check
2229
2134
  const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
@@ -2316,7 +2221,11 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2316
2221
  // Read directly: the object type is already established here, and a
2317
2222
  // flag declared for this alone costs more than it saves. The list is
2318
2223
  // built only on that path, which an ordinary object never takes.
2319
- lines.push(`if(!${protoIs(ctx, v)}&&!_hall(${v},${JSON.stringify(typed)}))return false`)
2224
+ // When withPlain already holds the answer for this object in a flag,
2225
+ // read the flag: the prototype test is an accessor call, and making it
2226
+ // twice per object was a tenth of a plain order's verdict.
2227
+ const pk = ctx.plainOf ? ctx.plainOf.get(v) : undefined
2228
+ lines.push(`if(!${pk || protoIs(ctx, v)}&&!_hall(${v},${JSON.stringify(typed)}))return false`)
2320
2229
  }
2321
2230
  } else if (schema.required && schema.required.length > 0) {
2322
2231
  const checks = schema.required.map(key => `!${ownKeyExpr(ctx, v, key)}`)
@@ -2447,31 +2356,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2447
2356
  }
2448
2357
 
2449
2358
  // uniqueItems — tiered strategy based on expected array size
2450
- if (schema.uniqueItems) {
2451
- const si = ctx.varCounter++
2452
- const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
2453
- const isPrimItems = itemType === 'string' || itemType === 'number' || itemType === 'integer'
2454
- const maxItems = schema.maxItems
2455
- // Small primitive arrays (maxItems <= 16): nested loop is 6x faster than Set
2456
- // No allocation, no hash computation — just direct === comparison
2457
- let inner
2458
- if (isPrimItems && maxItems && maxItems <= 16) {
2459
- inner = `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k])return false}}`
2460
- } else if (isPrimItems) {
2461
- inner = `const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){if(_s${si}.has(${v}[_i]))return false;_s${si}.add(${v}[_i])}`
2462
- } else if (ctx.preamble) {
2463
- // Unknown item types: one hoisted helper per compiled function. Short
2464
- // arrays compare pairwise with a structural equality that ignores key
2465
- // order and allocates nothing; longer ones fall back to a Set of
2466
- // primitives, or to sorted-key canonical strings once an object shows
2467
- // up. The previous form built the canonical closure and a Set on
2468
- // every call, even for [1, 2, 3].
2469
- inner = `if(!${emitUq(ctx)}(${v}))return false`
2470
- } else {
2471
- 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)}`
2472
- }
2473
- lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
2474
- }
2359
+ if (schema.uniqueItems) genUniqueItemsNode(schema, v, lines, ctx, knownType, hoisted, isArr, types)
2475
2360
 
2476
2361
  // additionalProperties -- deferred to end of function for better V8 optimization
2477
2362
  // (type checks run first in hot path, expensive prop count check last)
@@ -2504,28 +2389,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2504
2389
  // additionalProperties as a schema: validate every non-declared property
2505
2390
  // against that sub-schema. Skip if patternProperties is present (handled by
2506
2391
  // the unified loop). Composition cases are filtered out by codegenSafe.
2507
- if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null && !schema.patternProperties) {
2508
- // The loop variables carry a unique suffix: a record whose values are
2509
- // themselves records nests this loop inside itself, and a fixed `_av`
2510
- // made the inner `const _av=_av[_k]` a self-reference that threw at
2511
- // validation time.
2512
- const apId = ctx._apLoopId = (ctx._apLoopId || 0) + 1
2513
- const kVar = `_k${apId}`
2514
- const avVar = `_av${apId}`
2515
- const declared = schema.properties ? Object.keys(schema.properties) : []
2516
- const skipCheck = declared.length === 0
2517
- ? null
2518
- : declared.map(k => `${kVar}===${JSON.stringify(k)}`).join('||')
2519
- const subLines = []
2520
- genCode(schema.additionalProperties, avVar, subLines, ctx)
2521
- if (subLines.length > 0) {
2522
- const body = subLines.join(';')
2523
- const loop = skipCheck
2524
- ? `for(var ${kVar} in ${v}){if(${skipCheck})continue;const ${avVar}=${v}[${kVar}];${body}}`
2525
- : `for(var ${kVar} in ${v}){const ${avVar}=${v}[${kVar}];${body}}`
2526
- _deferOrInline(ctx, lines, v, isObj ? loop : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${loop}}`)
2527
- }
2528
- }
2392
+ if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null && !schema.patternProperties) genAdditionalSchemaNode(schema, v, lines, ctx, knownType, isObj)
2529
2393
 
2530
2394
  // dependentRequired
2531
2395
  if (schema.dependentRequired) {
@@ -2538,143 +2402,11 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2538
2402
  // patternProperties + propertyNames + additionalProperties — unified key iteration
2539
2403
  // Merges up to 3 separate for..in loops into one pass.
2540
2404
  if (schema.patternProperties) {
2541
- const ppEntries = Object.entries(schema.patternProperties)
2542
- const pn = schema.propertyNames && typeof schema.propertyNames === 'object' ? schema.propertyNames : null
2543
- const pi = ctx.varCounter++
2544
- const kVar = `_ppk${pi}`
2545
-
2546
- // Build pattern matchers: prefer charCodeAt for simple prefixes, fall back to regex
2547
- const matchers = []
2548
- for (const [pat] of ppEntries) {
2549
- const fast = fastPrefixCheck(pat, kVar)
2550
- if (fast) {
2551
- matchers.push({ check: fast })
2552
- } else {
2553
- const ri = ctx.varCounter++
2554
- ctx.closureVars.push(`_re${ri}`)
2555
- ctx.closureVals.push(safeReClosure(ctx, pat))
2556
- matchers.push({ check: `_re${ri}.test(${kVar})` })
2557
- }
2558
- }
2559
-
2560
- // Build sub-schema checks inline so they share the parent helper scope.
2561
- const subChecks = []
2562
- for (let i = 0; i < ppEntries.length; i++) {
2563
- const [, sub] = ppEntries[i]
2564
- const subLines = []
2565
- genCode(sub, `_ppv${pi}`, subLines, ctx)
2566
- subChecks.push(subLines.join(';'))
2567
- }
2568
-
2569
- const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
2570
-
2571
- const apSchema = typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null ? schema.additionalProperties : null
2572
- if ((schema.additionalProperties === false && schema.properties) || apSchema) {
2573
- // Unified loop: properties + patterns + propertyNames + additionalProperties.
2574
- // A key that is neither declared nor matched by any pattern is
2575
- // additional: rejected outright, or run through the additionalProperties
2576
- // schema when there is one.
2577
- ppHandledPropertyNames = !!pn
2578
- const propKeys = Object.keys(schema.properties || {})
2579
- let apCheck = null
2580
- if (apSchema) {
2581
- const apLines = []
2582
- genCode(apSchema, `_apv${pi}`, apLines, ctx)
2583
- apCheck = apLines.join(';')
2584
- }
2585
- lines.push(`${guard}{for(const ${kVar} in ${v}){`)
2586
- // propertyNames checks (merged into same loop)
2587
- if (pn) {
2588
- if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength})return false`)
2589
- if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength})return false`)
2590
- if (pn.pattern) {
2591
- const fast = fastPrefixCheck(pn.pattern, kVar)
2592
- if (fast) {
2593
- lines.push(`if(!(${fast}))return false`)
2594
- } else {
2595
- const ri = ctx.varCounter++
2596
- ctx.closureVars.push(`_re${ri}`)
2597
- ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
2598
- lines.push(`if(!_re${ri}.test(${kVar}))return false`)
2599
- }
2600
- }
2601
- if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)})return false`)
2602
- if (pn.enum) {
2603
- const ei = ctx.varCounter++
2604
- ctx.closureVars.push(`_es${ei}`)
2605
- ctx.closureVals.push(new Set(pn.enum))
2606
- lines.push(`if(!_es${ei}.has(${kVar}))return false`)
2607
- }
2608
- }
2609
- // Every matching pattern applies to every key, declared or not: a
2610
- // declared key that also matches a pattern is validated by both (the
2611
- // properties emitter handles its own subschema). A flag rather than an
2612
- // if/else chain, because a chain returned false at the first pattern
2613
- // that missed and wrongly rejected a key only a later pattern matched.
2614
- if (ppEntries.length > 0) {
2615
- lines.push(`let _pm${pi}=false`)
2616
- for (let i = 0; i < ppEntries.length; i++) {
2617
- lines.push(`if(${matchers[i].check}){_pm${pi}=true;const _ppv${pi}=${v}[${kVar}];${subChecks[i]}}`)
2618
- }
2619
- }
2620
- // A key that is neither declared nor matched is additional. switch on
2621
- // the declared names (V8 compiles string cases to a jump table); no
2622
- // switch at all when nothing is declared, since a switch with no case
2623
- // clause is a syntax error.
2624
- const additional = apCheck !== null ? `const _apv${pi}=${v}[${kVar}];${apCheck}` : `return false`
2625
- const notMatched = ppEntries.length > 0 ? `if(!_pm${pi}){${additional}}` : additional
2626
- if (propKeys.length) {
2627
- const switchCases = propKeys.map(k => `case ${JSON.stringify(k)}:`).join('')
2628
- lines.push(`switch(${kVar}){${switchCases}break;default:{${notMatched}}}`)
2629
- } else {
2630
- lines.push(notMatched)
2631
- }
2632
- lines.push(`}}`)
2633
- } else {
2634
- // No additionalProperties: validate matching keys + propertyNames
2635
- ppHandledPropertyNames = !!pn
2636
- lines.push(`${guard}{for(const ${kVar} in ${v}){`)
2637
- // propertyNames checks (merged)
2638
- if (pn) {
2639
- if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength})return false`)
2640
- if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength})return false`)
2641
- if (pn.pattern) {
2642
- const fast = fastPrefixCheck(pn.pattern, kVar)
2643
- if (fast) {
2644
- lines.push(`if(!(${fast}))return false`)
2645
- } else {
2646
- const ri = ctx.varCounter++
2647
- ctx.closureVars.push(`_re${ri}`)
2648
- ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
2649
- lines.push(`if(!_re${ri}.test(${kVar}))return false`)
2650
- }
2651
- }
2652
- if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)})return false`)
2653
- if (pn.enum) {
2654
- const ei = ctx.varCounter++
2655
- ctx.closureVars.push(`_es${ei}`)
2656
- ctx.closureVals.push(new Set(pn.enum))
2657
- lines.push(`if(!_es${ei}.has(${kVar}))return false`)
2658
- }
2659
- }
2660
- for (let i = 0; i < ppEntries.length; i++) {
2661
- lines.push(`if(${matchers[i].check}){const _ppv${pi}=${v}[${kVar}];${subChecks[i]}}`)
2662
- }
2663
- lines.push(`}}`)
2664
- }
2405
+ if (genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj)) ppHandledPropertyNames = true
2665
2406
  }
2666
2407
 
2667
2408
  // dependentSchemas
2668
- if (schema.dependentSchemas) {
2669
- for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
2670
- const guard = isObj ? '' : `typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&`
2671
- lines.push(`if(${guard}${ownKeyExpr(ctx, v, key)}){`)
2672
- ctx.condDepth = (ctx.condDepth || 0) + 1
2673
- genCode(depSchema, v, lines, ctx, effectiveType)
2674
- ctx.condDepth--
2675
- lines.push(`}`)
2676
- }
2677
- }
2409
+ if (schema.dependentSchemas) genDependentSchemasNode(schema, v, lines, ctx, knownType, effectiveType, isObj)
2678
2410
 
2679
2411
  // propertyNames: false rejects any key at all; true constrains nothing.
2680
2412
  if (schema.propertyNames === false) {
@@ -2684,33 +2416,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2684
2416
  }
2685
2417
 
2686
2418
  // propertyNames — only emit if not already merged into patternProperties loop
2687
- if (schema.propertyNames && typeof schema.propertyNames === 'object' && !ppHandledPropertyNames) {
2688
- const pn = schema.propertyNames
2689
- const ki = ctx.varCounter++
2690
- const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
2691
- lines.push(`${guard}{for(const _k${ki} in ${v}){`)
2692
- if (pn.minLength !== undefined) lines.push(`if(_k${ki}.length<${pn.minLength})return false`)
2693
- if (pn.maxLength !== undefined) lines.push(`if(_k${ki}.length>${pn.maxLength})return false`)
2694
- if (pn.pattern) {
2695
- const fast = fastPrefixCheck(pn.pattern, `_k${ki}`)
2696
- if (fast) {
2697
- lines.push(`if(!(${fast}))return false`)
2698
- } else {
2699
- const ri = ctx.varCounter++
2700
- ctx.closureVars.push(`_re${ri}`)
2701
- ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
2702
- lines.push(`if(!_re${ri}.test(_k${ki}))return false`)
2703
- }
2704
- }
2705
- if (pn.const !== undefined) lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)})return false`)
2706
- if (pn.enum) {
2707
- const ei = ctx.varCounter++
2708
- ctx.closureVars.push(`_es${ei}`)
2709
- ctx.closureVals.push(new Set(pn.enum))
2710
- lines.push(`if(!_es${ei}.has(_k${ki}))return false`)
2711
- }
2712
- lines.push(`}}`)
2713
- }
2419
+ if (schema.propertyNames && typeof schema.propertyNames === 'object' && !ppHandledPropertyNames) genPropertyNamesNode(schema, v, lines, ctx, knownType, isObj)
2714
2420
 
2715
2421
  // properties — use hoisted vars for required props, hoist optional to locals too
2716
2422
  if (schema.properties) {
@@ -2721,7 +2427,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2721
2427
  const combined = tryGenCombined(prop, access, ctx)
2722
2428
  if (combined) {
2723
2429
  lines.push(combined)
2724
- } else if (needsLocal(prop)) {
2430
+ } else if (needsLocal(prop) || isPlainContainer(prop)) {
2725
2431
  const oi = ctx.varCounter++
2726
2432
  const local = `_r${oi}`
2727
2433
  lines.push(`{const ${local}=${access}`)
@@ -2765,35 +2471,10 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2765
2471
  }
2766
2472
 
2767
2473
  // prefixItems
2768
- if (schema.prefixItems) {
2769
- const pfxVar = ctx.varCounter++
2770
- for (let i = 0; i < schema.prefixItems.length; i++) {
2771
- const elem = `_p${pfxVar}_${i}`
2772
- lines.push(isArr
2773
- ? `if(${v}.length>${i}){const ${elem}=${v}[${i}]`
2774
- : `if(Array.isArray(${v})&&${v}.length>${i}){const ${elem}=${v}[${i}]`)
2775
- genCode(schema.prefixItems[i], elem, lines, ctx)
2776
- lines.push(`}`)
2777
- }
2778
- }
2474
+ if (schema.prefixItems) genPrefixItemsNode(schema, v, lines, ctx, knownType, isArr)
2779
2475
 
2780
2476
  // contains — use helper function to avoid try/catch overhead
2781
- if (schema.contains !== undefined) {
2782
- const ci = ctx.varCounter++
2783
- const minC = schema.minContains !== undefined ? schema.minContains : 1
2784
- const maxC = schema.maxContains !== undefined ? schema.maxContains : Infinity
2785
- const subLines = []
2786
- genCode(schema.contains, `_cv`, subLines, ctx)
2787
- const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
2788
- const guard = isArr ? '' : `if(!Array.isArray(${v})){}else `
2789
- lines.push(`${guard}{const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0`)
2790
- lines.push(`for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
2791
- if (maxC === Infinity) {
2792
- lines.push(`if(_cc${ci}<${minC})return false}`)
2793
- } else {
2794
- lines.push(`if(_cc${ci}<${minC}||_cc${ci}>${maxC})return false}`)
2795
- }
2796
- }
2477
+ if (schema.contains !== undefined) genContainsNode(schema, v, lines, ctx, knownType, isArr)
2797
2478
 
2798
2479
  // allOf — pass known type through
2799
2480
  if (schema.allOf) {
@@ -2804,395 +2485,659 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2804
2485
 
2805
2486
  // anyOf — branch fns hoisted when safe, inline fallback when recursive.
2806
2487
  // Skip standard anyOf if unevaluatedProperties will handle it (single-pass optimization)
2807
- if (schema.anyOf && schema.unevaluatedProperties === undefined) {
2808
- const fi = ctx.varCounter++
2809
- const branchBodies = []
2810
- let canHoist = !!ctx.preamble
2811
- for (let i = 0; i < schema.anyOf.length; i++) {
2812
- const subLines = []
2813
- genCode(schema.anyOf[i], '_av', subLines, ctx)
2814
- const body = subLines.length === 0 ? 'return true' : `${subLines.join(';')};return true`
2815
- if (/\b_validate\b/.test(body)) canHoist = false
2816
- branchBodies.push(body)
2817
- }
2818
- if (canHoist) {
2819
- const names = branchBodies.map((body, i) => {
2820
- const name = `_af${fi}_b${i}`
2821
- ctx.preamble.push(`function ${name}(_av){${body}}`)
2822
- return name
2823
- })
2824
- const checks = names.map(n => `${n}(${v})`).join('||')
2825
- lines.push(`if(!(${checks}))return false`)
2826
- } else {
2827
- const fns = branchBodies.map(body => `function(_av){${body}}`)
2828
- lines.push(`{const _af${fi}=[${fns.join(',')}];let _am${fi}=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am${fi}=true;break}}if(!_am${fi})return false}`)
2829
- }
2830
- }
2488
+ if (schema.anyOf && schema.unevaluatedProperties === undefined) genAnyOfNode(schema, v, lines, ctx, knownType)
2831
2489
 
2832
2490
  // oneOf — branch fns hoisted to factory scope when safe (no recursion/ref).
2833
2491
  // Falls back to inline closures if any branch touches recursive validation.
2834
- if (schema.oneOf) {
2835
- const fi = ctx.varCounter++
2836
- const branchBodies = []
2837
- let canHoist = !!ctx.preamble
2838
- for (let i = 0; i < schema.oneOf.length; i++) {
2839
- const subLines = []
2840
- genCode(schema.oneOf[i], '_ov', subLines, ctx)
2841
- const body = subLines.length === 0 ? 'return true' : `${subLines.join(';')};return true`
2842
- // _validate is the recursive entry — hoisting branches above it breaks scope.
2843
- if (/\b_validate\b/.test(body)) canHoist = false
2844
- branchBodies.push(body)
2845
- }
2846
- if (canHoist) {
2847
- const names = branchBodies.map((body, i) => {
2848
- const name = `_of${fi}_b${i}`
2849
- ctx.preamble.push(`function ${name}(_ov){${body}}`)
2850
- return name
2851
- })
2852
- const calls = names.map(n => `if(${n}(${v})){_oc${fi}++;if(_oc${fi}>1)return false}`).join(';')
2853
- lines.push(`{let _oc${fi}=0;${calls};if(_oc${fi}!==1)return false}`)
2492
+ if (schema.oneOf) genOneOfNode(schema, v, lines, ctx, knownType)
2493
+
2494
+ // not
2495
+ if (schema.not !== undefined) genNotNode(schema, v, lines, ctx, knownType)
2496
+
2497
+ // if/then/else
2498
+ if (schema.if !== undefined) genIfNode(schema, v, lines, ctx, knownType)
2499
+
2500
+ // unevaluatedProperties
2501
+ if (schema.unevaluatedProperties !== undefined) genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount)
2502
+
2503
+ // unevaluatedItems
2504
+ if (schema.unevaluatedItems !== undefined) genUnevaluatedItemsNode(schema, v, lines, ctx, knownType, isArr)
2505
+ }
2506
+
2507
+ // The rarely used keyword families live outside genCodeNode, so a schema that
2508
+ // never uses them does not pay V8 to compile them on its first check. Each is
2509
+ // called with the locals it reads; genRefNode returns true when the node is done.
2510
+
2511
+ function genDynamicRefNode(schema, v, lines, ctx, knownType) {
2512
+ const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
2513
+ if (ctx.anchors && ctx.anchors[anchorKey]) {
2514
+ const target = ctx.anchors[anchorKey]
2515
+ if (target === ctx.rootSchema) {
2516
+ // Self-recursive: generate _validate(v) call
2517
+ if (ctx.nestedBoolean) throw DECLINE
2518
+ ctx.usesRecursion = true
2519
+ lines.push(`if(!_validate(${v}))return false`)
2854
2520
  } else {
2855
- const fns = branchBodies.map(body => `function(_ov){${body}}`)
2856
- lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc${fi}=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc${fi}++;if(_oc${fi}>1)return false}if(_oc${fi}!==1)return false}`)
2521
+ // Different schema: inline the target validation
2522
+ const refKey = '$dynamicRef:' + anchorKey
2523
+ if (ctx.refStack.has(refKey) && ctx.nestedBoolean) throw DECLINE
2524
+ if (!ctx.refStack.has(refKey)) {
2525
+ ctx.refStack.add(refKey)
2526
+ genCode(target, v, lines, ctx, knownType)
2527
+ ctx.refStack.delete(refKey)
2528
+ }
2857
2529
  }
2858
2530
  }
2531
+ }
2859
2532
 
2860
- // not
2861
- if (schema.not !== undefined) {
2862
- const subLines = []
2863
- genCode(schema.not, '_nv', subLines, ctx)
2864
- if (subLines.length === 0) {
2865
- lines.push(`return false`) // not:{} means nothing is valid
2533
+ function genUniqueItemsNode(schema, v, lines, ctx, knownType, hoisted, isArr, types) {
2534
+ const si = ctx.varCounter++
2535
+ const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
2536
+ const isPrimItems = itemType === 'string' || itemType === 'number' || itemType === 'integer'
2537
+ const maxItems = schema.maxItems
2538
+ // Small primitive arrays (maxItems <= 16): nested loop is 6x faster than Set
2539
+ // No allocation, no hash computation — just direct === comparison
2540
+ let inner
2541
+ if (isPrimItems && maxItems && maxItems <= 16) {
2542
+ inner = `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k])return false}}`
2543
+ } else if (isPrimItems) {
2544
+ inner = `const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){if(_s${si}.has(${v}[_i]))return false;_s${si}.add(${v}[_i])}`
2545
+ } else if (ctx.preamble) {
2546
+ // Unknown item types: one hoisted helper per compiled function. Short
2547
+ // arrays compare pairwise with a structural equality that ignores key
2548
+ // order and allocates nothing; longer ones fall back to a Set of
2549
+ // primitives, or to sorted-key canonical strings once an object shows
2550
+ // up. The previous form built the canonical closure and a Set on
2551
+ // every call, even for [1, 2, 3].
2552
+ inner = `if(!${emitUq(ctx)}(${v}))return false`
2553
+ } else {
2554
+ 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)}`
2555
+ }
2556
+ lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
2557
+ }
2558
+
2559
+ function genAdditionalSchemaNode(schema, v, lines, ctx, knownType, isObj) {
2560
+ // The loop variables carry a unique suffix: a record whose values are
2561
+ // themselves records nests this loop inside itself, and a fixed `_av`
2562
+ // made the inner `const _av=_av[_k]` a self-reference that threw at
2563
+ // validation time.
2564
+ const apId = ctx._apLoopId = (ctx._apLoopId || 0) + 1
2565
+ const kVar = `_k${apId}`
2566
+ const avVar = `_av${apId}`
2567
+ const declared = schema.properties ? Object.keys(schema.properties) : []
2568
+ const skipCheck = declared.length === 0
2569
+ ? null
2570
+ : declared.map(k => `${kVar}===${JSON.stringify(k)}`).join('||')
2571
+ const subLines = []
2572
+ genCode(schema.additionalProperties, avVar, subLines, ctx)
2573
+ if (subLines.length > 0) {
2574
+ const body = subLines.join(';')
2575
+ const loop = skipCheck
2576
+ ? `for(var ${kVar} in ${v}){if(${skipCheck})continue;const ${avVar}=${v}[${kVar}];${body}}`
2577
+ : `for(var ${kVar} in ${v}){const ${avVar}=${v}[${kVar}];${body}}`
2578
+ _deferOrInline(ctx, lines, v, isObj ? loop : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${loop}}`)
2579
+ }
2580
+ }
2581
+
2582
+ function genDependentSchemasNode(schema, v, lines, ctx, knownType, effectiveType, isObj) {
2583
+ for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
2584
+ const guard = isObj ? '' : `typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&`
2585
+ lines.push(`if(${guard}${ownKeyExpr(ctx, v, key)}){`)
2586
+ ctx.condDepth = (ctx.condDepth || 0) + 1
2587
+ genCode(depSchema, v, lines, ctx, effectiveType)
2588
+ ctx.condDepth--
2589
+ lines.push(`}`)
2590
+ }
2591
+ }
2592
+
2593
+ function genPropertyNamesNode(schema, v, lines, ctx, knownType, isObj) {
2594
+ const pn = schema.propertyNames
2595
+ const ki = ctx.varCounter++
2596
+ const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
2597
+ lines.push(`${guard}{for(const _k${ki} in ${v}){`)
2598
+ if (pn.minLength !== undefined) lines.push(`if(_k${ki}.length<${pn.minLength})return false`)
2599
+ if (pn.maxLength !== undefined) lines.push(`if(_k${ki}.length>${pn.maxLength})return false`)
2600
+ if (pn.pattern) {
2601
+ const fast = fastPrefixCheck(pn.pattern, `_k${ki}`)
2602
+ if (fast) {
2603
+ lines.push(`if(!(${fast}))return false`)
2866
2604
  } else {
2867
- const fi = ctx.varCounter++
2868
- lines.push(`{const _nf${fi}=(function(_nv){${subLines.join(';')};return true});if(_nf${fi}(${v}))return false}`)
2605
+ const ri = ctx.varCounter++
2606
+ ctx.closureVars.push(`_re${ri}`)
2607
+ ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
2608
+ lines.push(`if(!_re${ri}.test(_k${ki}))return false`)
2869
2609
  }
2870
2610
  }
2611
+ if (pn.const !== undefined) lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)})return false`)
2612
+ if (pn.enum) {
2613
+ const ei = ctx.varCounter++
2614
+ ctx.closureVars.push(`_es${ei}`)
2615
+ ctx.closureVals.push(new Set(pn.enum))
2616
+ lines.push(`if(!_es${ei}.has(_k${ki}))return false`)
2617
+ }
2618
+ lines.push(`}}`)
2619
+ }
2871
2620
 
2872
- // if/then/else
2873
- if (schema.if !== undefined) {
2874
- const ifLines = []
2875
- genCode(schema.if, '_iv', ifLines, ctx)
2621
+ function genPrefixItemsNode(schema, v, lines, ctx, knownType, isArr) {
2622
+ const pfxVar = ctx.varCounter++
2623
+ for (let i = 0; i < schema.prefixItems.length; i++) {
2624
+ const elem = `_p${pfxVar}_${i}`
2625
+ lines.push(isArr
2626
+ ? `if(${v}.length>${i}){const ${elem}=${v}[${i}]`
2627
+ : `if(Array.isArray(${v})&&${v}.length>${i}){const ${elem}=${v}[${i}]`)
2628
+ genCode(schema.prefixItems[i], elem, lines, ctx)
2629
+ lines.push(`}`)
2630
+ }
2631
+ }
2632
+
2633
+ function genContainsNode(schema, v, lines, ctx, knownType, isArr) {
2634
+ const ci = ctx.varCounter++
2635
+ const minC = schema.minContains !== undefined ? schema.minContains : 1
2636
+ const maxC = schema.maxContains !== undefined ? schema.maxContains : Infinity
2637
+ const subLines = []
2638
+ genCode(schema.contains, `_cv`, subLines, ctx)
2639
+ const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
2640
+ const guard = isArr ? '' : `if(!Array.isArray(${v})){}else `
2641
+ lines.push(`${guard}{const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0`)
2642
+ lines.push(`for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
2643
+ if (maxC === Infinity) {
2644
+ lines.push(`if(_cc${ci}<${minC})return false}`)
2645
+ } else {
2646
+ lines.push(`if(_cc${ci}<${minC}||_cc${ci}>${maxC})return false}`)
2647
+ }
2648
+ }
2649
+
2650
+ function genAnyOfNode(schema, v, lines, ctx, knownType) {
2651
+ const fi = ctx.varCounter++
2652
+ const branchBodies = []
2653
+ let canHoist = !!ctx.preamble
2654
+ for (let i = 0; i < schema.anyOf.length; i++) {
2655
+ const subLines = []
2656
+ genCode(schema.anyOf[i], '_av', subLines, ctx)
2657
+ const body = subLines.length === 0 ? 'return true' : `${subLines.join(';')};return true`
2658
+ if (/\b_validate\b/.test(body)) canHoist = false
2659
+ branchBodies.push(body)
2660
+ }
2661
+ if (canHoist) {
2662
+ const names = branchBodies.map((body, i) => {
2663
+ const name = `_af${fi}_b${i}`
2664
+ ctx.preamble.push(`function ${name}(_av){${body}}`)
2665
+ return name
2666
+ })
2667
+ const checks = names.map(n => `${n}(${v})`).join('||')
2668
+ lines.push(`if(!(${checks}))return false`)
2669
+ } else {
2670
+ const fns = branchBodies.map(body => `function(_av){${body}}`)
2671
+ lines.push(`{const _af${fi}=[${fns.join(',')}];let _am${fi}=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am${fi}=true;break}}if(!_am${fi})return false}`)
2672
+ }
2673
+ }
2674
+
2675
+ function genOneOfNode(schema, v, lines, ctx, knownType) {
2676
+ const fi = ctx.varCounter++
2677
+ const branchBodies = []
2678
+ let canHoist = !!ctx.preamble
2679
+ for (let i = 0; i < schema.oneOf.length; i++) {
2680
+ const subLines = []
2681
+ genCode(schema.oneOf[i], '_ov', subLines, ctx)
2682
+ const body = subLines.length === 0 ? 'return true' : `${subLines.join(';')};return true`
2683
+ // _validate is the recursive entry — hoisting branches above it breaks scope.
2684
+ if (/\b_validate\b/.test(body)) canHoist = false
2685
+ branchBodies.push(body)
2686
+ }
2687
+ if (canHoist) {
2688
+ const names = branchBodies.map((body, i) => {
2689
+ const name = `_of${fi}_b${i}`
2690
+ ctx.preamble.push(`function ${name}(_ov){${body}}`)
2691
+ return name
2692
+ })
2693
+ const calls = names.map(n => `if(${n}(${v})){_oc${fi}++;if(_oc${fi}>1)return false}`).join(';')
2694
+ lines.push(`{let _oc${fi}=0;${calls};if(_oc${fi}!==1)return false}`)
2695
+ } else {
2696
+ const fns = branchBodies.map(body => `function(_ov){${body}}`)
2697
+ lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc${fi}=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc${fi}++;if(_oc${fi}>1)return false}if(_oc${fi}!==1)return false}`)
2698
+ }
2699
+ }
2700
+
2701
+ function genNotNode(schema, v, lines, ctx, knownType) {
2702
+ const subLines = []
2703
+ genCode(schema.not, '_nv', subLines, ctx)
2704
+ if (subLines.length === 0) {
2705
+ lines.push(`return false`) // not:{} means nothing is valid
2706
+ } else {
2876
2707
  const fi = ctx.varCounter++
2877
- const ifFn = ifLines.length === 0
2878
- ? `function(_iv){return true}`
2879
- : `function(_iv){${ifLines.join(';')};return true}`
2708
+ lines.push(`{const _nf${fi}=(function(_nv){${subLines.join(';')};return true});if(_nf${fi}(${v}))return false}`)
2709
+ }
2710
+ }
2711
+
2712
+ function genIfNode(schema, v, lines, ctx, knownType) {
2713
+ const ifLines = []
2714
+ genCode(schema.if, '_iv', ifLines, ctx)
2715
+ const fi = ctx.varCounter++
2716
+ const ifFn = ifLines.length === 0
2717
+ ? `function(_iv){return true}`
2718
+ : `function(_iv){${ifLines.join(';')};return true}`
2719
+
2720
+ let thenFn = 'null', elseFn = 'null'
2721
+ if (schema.then !== undefined) {
2722
+ const thenLines = []
2723
+ genCode(schema.then, '_tv', thenLines, ctx)
2724
+ thenFn = thenLines.length === 0
2725
+ ? `function(_tv){return true}`
2726
+ : `function(_tv){${thenLines.join(';')};return true}`
2727
+ }
2728
+ if (schema.else !== undefined) {
2729
+ const elseLines = []
2730
+ genCode(schema.else, '_ev', elseLines, ctx)
2731
+ elseFn = elseLines.length === 0
2732
+ ? `function(_ev){return true}`
2733
+ : `function(_ev){${elseLines.join(';')};return true}`
2734
+ }
2735
+ lines.push(`{const _if${fi}=${ifFn};const _th${fi}=${thenFn};const _el${fi}=${elseFn}`)
2736
+ lines.push(`if(_if${fi}(${v})){if(_th${fi}&&!_th${fi}(${v}))return false}else{if(_el${fi}&&!_el${fi}(${v}))return false}}`)
2737
+ }
2880
2738
 
2881
- let thenFn = 'null', elseFn = 'null'
2882
- if (schema.then !== undefined) {
2883
- const thenLines = []
2884
- genCode(schema.then, '_tv', thenLines, ctx)
2885
- thenFn = thenLines.length === 0
2886
- ? `function(_tv){return true}`
2887
- : `function(_tv){${thenLines.join(';')};return true}`
2739
+ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
2740
+ // Self-reference "#" — recursive call to root validator
2741
+ if (schema.$ref === '#') {
2742
+ if (ctx.nestedBoolean) throw DECLINE
2743
+ ctx.usesRecursion = true
2744
+ lines.push(`if(!_validate(${v}))return false`)
2745
+ if (!hasSiblings) return true
2746
+ }
2747
+ // 1. Local ref
2748
+ const m = schema.$ref !== '#' && schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
2749
+ if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
2750
+ const defName = m[1]
2751
+ if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
2752
+ if (ctx.cyclicDefs.has(defName) && ctx.preamble) {
2753
+ // A def on a cycle is a named function in the preamble, emitted once
2754
+ // and called by name, so recursion terminates the way `$ref: "#"`
2755
+ // does at the root. The name is registered before the body is
2756
+ // generated so a self-reference inside it becomes a call.
2757
+ if (!ctx.defFns) ctx.defFns = new Map()
2758
+ let fnName = ctx.defFns.get(defName)
2759
+ if (!fnName) {
2760
+ fnName = '_def' + ctx.defFns.size + '_' + defName.replace(/[^A-Za-z0-9_]/g, '_')
2761
+ ctx.defFns.set(defName, fnName)
2762
+ const bodyLines = []
2763
+ // The def is compiled as its own function over its own `d`, so the
2764
+ // checks _deferOrInline holds back belong at the end of this body.
2765
+ // The list hangs off ctx, so without the swap they are flushed into
2766
+ // whatever function is compiling, carrying this def's key set.
2767
+ const outerDeferred = ctx.deferredChecks
2768
+ ctx.deferredChecks = null
2769
+ genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)
2770
+ if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
2771
+ ctx.deferredChecks = outerDeferred
2772
+ // Entered through the cycle guard: see emitGuardState. In the fast
2773
+ // pass the wrapper only counts depth; the set is used by the second,
2774
+ // guarded pass that runs when data turns out to point back at
2775
+ // itself.
2776
+ ctx.preamble.push(
2777
+ `const ${fnName}_s=new Set()\n function ${fnName}(d){\n ` +
2778
+ `if(_sg){if(typeof d!=='object'||d===null)return ${fnName}_b(d);if(${fnName}_s.has(d))return true;${fnName}_s.add(d);try{return ${fnName}_b(d)}finally{${fnName}_s.delete(d)}}\n ` +
2779
+ `if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(d)\n _sd--\n return _r\n }\n ` +
2780
+ `function ${fnName}_b(d){${bodyLines.join('\n ')}\n return true}`,
2781
+ )
2782
+ }
2783
+ lines.push(`if(!${fnName}(${v}))return false`)
2784
+ if (!hasSiblings) return true
2785
+ } else if (ctx.refStack.has(schema.$ref)) { if (ctx.nestedBoolean) throw DECLINE; if (!hasSiblings) return true }
2786
+ else {
2787
+ ctx.refStack.add(schema.$ref)
2788
+ genCode(ctx.rootDefs[defName], v, lines, ctx, knownType)
2789
+ ctx.refStack.delete(schema.$ref)
2790
+ if (!hasSiblings) return true
2791
+ }
2792
+ } else if (schema.$ref !== '#' && !m && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')) {
2793
+ // Anchor ref: "#foo" — resolve via rootDefs or anchors map
2794
+ const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
2795
+ const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
2796
+ if (anchorTarget) {
2797
+ if (ctx.refStack.has(schema.$ref)) { if (ctx.nestedBoolean) throw DECLINE; if (!hasSiblings) return true }
2798
+ else {
2799
+ ctx.refStack.add(schema.$ref)
2800
+ genCode(anchorTarget, v, lines, ctx, knownType)
2801
+ ctx.refStack.delete(schema.$ref)
2802
+ if (!hasSiblings) return true
2803
+ }
2888
2804
  }
2889
- if (schema.else !== undefined) {
2890
- const elseLines = []
2891
- genCode(schema.else, '_ev', elseLines, ctx)
2892
- elseFn = elseLines.length === 0
2893
- ? `function(_ev){return true}`
2894
- : `function(_ev){${elseLines.join(';')};return true}`
2805
+ } else if (schema.$ref !== '#' && ctx.schemaMap) {
2806
+ // 2. Cross-schema ref (exact match, relative URI, or JSON pointer fragment)
2807
+ let resolved = ctx.schemaMap.get(schema.$ref)
2808
+ if (!resolved && !schema.$ref.includes('://') && !schema.$ref.startsWith('#')) {
2809
+ for (const [id, s] of ctx.schemaMap) {
2810
+ if (id.endsWith('/' + schema.$ref)) { resolved = s; break }
2811
+ }
2895
2812
  }
2896
- lines.push(`{const _if${fi}=${ifFn};const _th${fi}=${thenFn};const _el${fi}=${elseFn}`)
2897
- lines.push(`if(_if${fi}(${v})){if(_th${fi}&&!_th${fi}(${v}))return false}else{if(_el${fi}&&!_el${fi}(${v}))return false}}`)
2813
+ if (!resolved && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
2814
+ const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
2815
+ if (r) resolved = r.schema
2816
+ }
2817
+ if (resolved) {
2818
+ if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return true }
2819
+ else {
2820
+ ctx.refStack.add(schema.$ref)
2821
+ genCode(resolved, v, lines, ctx, knownType)
2822
+ ctx.refStack.delete(schema.$ref)
2823
+ if (!hasSiblings) return true
2824
+ }
2825
+ } else {
2826
+ if (!hasSiblings) return true
2827
+ }
2828
+ } else {
2829
+ if (!hasSiblings) return true
2898
2830
  }
2831
+ return false
2832
+ }
2899
2833
 
2900
- // unevaluatedProperties
2901
- if (schema.unevaluatedProperties !== undefined) {
2902
- const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
2903
-
2904
- if (evalResult.allProps || schema.unevaluatedProperties === true) {
2905
- // All props evaluated or unevaluatedProperties:true — no-op
2906
- } else if (!evalResult.dynamic) {
2907
- // Tier 1-2: all evaluated props known at compile-time — ZERO COST
2908
- const knownKeys = evalResult.props
2909
- const propCount = knownKeys.length
2834
+ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
2835
+ let handledPropertyNames = false
2836
+ const ppEntries = Object.entries(schema.patternProperties)
2837
+ const pn = schema.propertyNames && typeof schema.propertyNames === 'object' ? schema.propertyNames : null
2838
+ const pi = ctx.varCounter++
2839
+ const kVar = `_ppk${pi}`
2840
+
2841
+ // Build pattern matchers: prefer charCodeAt for simple prefixes, fall back to regex
2842
+ const matchers = []
2843
+ for (const [pat] of ppEntries) {
2844
+ const fast = fastPrefixCheck(pat, kVar)
2845
+ if (fast) {
2846
+ matchers.push({ check: fast })
2847
+ } else {
2848
+ const ri = ctx.varCounter++
2849
+ ctx.closureVars.push(`_re${ri}`)
2850
+ ctx.closureVals.push(safeReClosure(ctx, pat))
2851
+ matchers.push({ check: `_re${ri}.test(${kVar})` })
2852
+ }
2853
+ }
2910
2854
 
2911
- if (schema.unevaluatedProperties === false) {
2912
- const allRequired = schema.required && schema.required.length >= propCount &&
2913
- knownKeys.every(k => schema.required.includes(k))
2914
-
2915
- let inner
2916
- if (allRequired && propCount > 0) {
2917
- // TRICK 1: required covers all — key count check only
2918
- if (!earlyKeyCount) {
2919
- // Adaptive: for-in for <=15 keys, Object.keys for >15
2920
- inner = propCount <= 15
2921
- ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
2922
- : `if(Object.keys(${v}).length!==${propCount})return false`
2923
- _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
2924
- }
2925
- // else: already emitted early (before properties)
2926
- } else if (propCount > 0) {
2927
- // TRICK 3: charCodeAt switch tree. Above the lookup threshold it is
2928
- // the additionalProperties quadratic wearing a disguise: keys that
2929
- // share a first character, ENV_0 through ENV_999 say, all land in
2930
- // one case whose body is a chain of every name, per key of the
2931
- // document. The hoisted name lookup is the same cure it was there.
2932
- inner = propCount >= AP_LOOKUP_MIN
2933
- ? apMembershipCheck(ctx, knownKeys, v)
2934
- : genCharCodeSwitch(knownKeys, v)
2935
- _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
2855
+ // Build sub-schema checks inline so they share the parent helper scope.
2856
+ const subChecks = []
2857
+ for (let i = 0; i < ppEntries.length; i++) {
2858
+ const [, sub] = ppEntries[i]
2859
+ const subLines = []
2860
+ genCode(sub, `_ppv${pi}`, subLines, ctx)
2861
+ subChecks.push(subLines.join(';'))
2862
+ }
2863
+
2864
+ const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
2865
+
2866
+ const apSchema = typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null ? schema.additionalProperties : null
2867
+ if ((schema.additionalProperties === false && schema.properties) || apSchema) {
2868
+ // Unified loop: properties + patterns + propertyNames + additionalProperties.
2869
+ // A key that is neither declared nor matched by any pattern is
2870
+ // additional: rejected outright, or run through the additionalProperties
2871
+ // schema when there is one.
2872
+ handledPropertyNames = !!pn
2873
+ const propKeys = Object.keys(schema.properties || {})
2874
+ let apCheck = null
2875
+ if (apSchema) {
2876
+ const apLines = []
2877
+ genCode(apSchema, `_apv${pi}`, apLines, ctx)
2878
+ apCheck = apLines.join(';')
2879
+ }
2880
+ lines.push(`${guard}{for(const ${kVar} in ${v}){`)
2881
+ // propertyNames checks (merged into same loop)
2882
+ if (pn) {
2883
+ if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength})return false`)
2884
+ if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength})return false`)
2885
+ if (pn.pattern) {
2886
+ const fast = fastPrefixCheck(pn.pattern, kVar)
2887
+ if (fast) {
2888
+ lines.push(`if(!(${fast}))return false`)
2936
2889
  } else {
2937
- inner = `for(var _k in ${v})return false`
2938
- _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
2939
- }
2940
- } else if (typeof schema.unevaluatedProperties === 'object') {
2941
- // unevaluatedProperties: {schema} — validate unknown keys
2942
- const ui = ctx.varCounter++
2943
- const ukVar = `_uk${ui}`
2944
- const subLines = []
2945
- genCode(schema.unevaluatedProperties, `${v}[${ukVar}]`, subLines, ctx)
2946
- if (subLines.length > 0) {
2947
- const check = subLines.join(';')
2948
- let skipKnown = ''
2949
- if (knownKeys.length >= AP_LOOKUP_MIN) {
2950
- const id = emitNameLookup(ctx, knownKeys)
2951
- skipKnown = id !== null ? `if(${id}[${ukVar}]!==undefined)continue;` : `if(${knownKeys.map(k => `${ukVar}===${JSON.stringify(k)}`).join('||')})continue;`
2952
- } else if (knownKeys.length > 0) {
2953
- skipKnown = `if(${knownKeys.map(k => `${ukVar}===${JSON.stringify(k)}`).join('||')})continue;`
2954
- }
2955
- const inner = `for(var ${ukVar} in ${v}){${skipKnown}${check}}`
2956
- _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
2890
+ const ri = ctx.varCounter++
2891
+ ctx.closureVars.push(`_re${ri}`)
2892
+ ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
2893
+ lines.push(`if(!_re${ri}.test(${kVar}))return false`)
2957
2894
  }
2958
2895
  }
2896
+ if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)})return false`)
2897
+ if (pn.enum) {
2898
+ const ei = ctx.varCounter++
2899
+ ctx.closureVars.push(`_es${ei}`)
2900
+ ctx.closureVals.push(new Set(pn.enum))
2901
+ lines.push(`if(!_es${ei}.has(${kVar}))return false`)
2902
+ }
2903
+ }
2904
+ // Every matching pattern applies to every key, declared or not: a
2905
+ // declared key that also matches a pattern is validated by both (the
2906
+ // properties emitter handles its own subschema). A flag rather than an
2907
+ // if/else chain, because a chain returned false at the first pattern
2908
+ // that missed and wrongly rejected a key only a later pattern matched.
2909
+ if (ppEntries.length > 0) {
2910
+ lines.push(`let _pm${pi}=false`)
2911
+ for (let i = 0; i < ppEntries.length; i++) {
2912
+ lines.push(`if(${matchers[i].check}){_pm${pi}=true;const _ppv${pi}=${v}[${kVar}];${subChecks[i]}}`)
2913
+ }
2914
+ }
2915
+ // A key that is neither declared nor matched is additional. switch on
2916
+ // the declared names (V8 compiles string cases to a jump table); no
2917
+ // switch at all when nothing is declared, since a switch with no case
2918
+ // clause is a syntax error.
2919
+ const additional = apCheck !== null ? `const _apv${pi}=${v}[${kVar}];${apCheck}` : `return false`
2920
+ const notMatched = ppEntries.length > 0 ? `if(!_pm${pi}){${additional}}` : additional
2921
+ if (propKeys.length) {
2922
+ const switchCases = propKeys.map(k => `case ${JSON.stringify(k)}:`).join('')
2923
+ lines.push(`switch(${kVar}){${switchCases}break;default:{${notMatched}}}`)
2959
2924
  } else {
2960
- // Tier 2.5 / Tier 3: dynamic — runtime tracking needed
2961
- // Compute base props: only unconditionally evaluated (properties, allOf-static, $ref)
2962
- const baseResult = { props: [], items: null, allProps: false, allItems: false, dynamic: false }
2963
- if (schema.properties) {
2964
- for (const k of Object.keys(schema.properties)) {
2965
- if (!baseResult.props.includes(k)) baseResult.props.push(k)
2925
+ lines.push(notMatched)
2926
+ }
2927
+ lines.push(`}}`)
2928
+ } else {
2929
+ // No additionalProperties: validate matching keys + propertyNames
2930
+ handledPropertyNames = !!pn
2931
+ lines.push(`${guard}{for(const ${kVar} in ${v}){`)
2932
+ // propertyNames checks (merged)
2933
+ if (pn) {
2934
+ if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength})return false`)
2935
+ if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength})return false`)
2936
+ if (pn.pattern) {
2937
+ const fast = fastPrefixCheck(pn.pattern, kVar)
2938
+ if (fast) {
2939
+ lines.push(`if(!(${fast}))return false`)
2940
+ } else {
2941
+ const ri = ctx.varCounter++
2942
+ ctx.closureVars.push(`_re${ri}`)
2943
+ ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
2944
+ lines.push(`if(!_re${ri}.test(${kVar}))return false`)
2966
2945
  }
2967
2946
  }
2968
- if (schema.allOf) {
2969
- for (const sub of schema.allOf) {
2970
- const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
2971
- if (!subR.dynamic && subR.props) {
2972
- for (const k of subR.props) {
2973
- if (!baseResult.props.includes(k)) baseResult.props.push(k)
2974
- }
2975
- }
2947
+ if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)})return false`)
2948
+ if (pn.enum) {
2949
+ const ei = ctx.varCounter++
2950
+ ctx.closureVars.push(`_es${ei}`)
2951
+ ctx.closureVals.push(new Set(pn.enum))
2952
+ lines.push(`if(!_es${ei}.has(${kVar}))return false`)
2953
+ }
2954
+ }
2955
+ for (let i = 0; i < ppEntries.length; i++) {
2956
+ lines.push(`if(${matchers[i].check}){const _ppv${pi}=${v}[${kVar}];${subChecks[i]}}`)
2957
+ }
2958
+ lines.push(`}}`)
2959
+ }
2960
+ return handledPropertyNames
2961
+ }
2962
+
2963
+ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount) {
2964
+ const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
2965
+
2966
+ if (evalResult.allProps || schema.unevaluatedProperties === true) {
2967
+ // All props evaluated or unevaluatedProperties:true — no-op
2968
+ } else if (!evalResult.dynamic) {
2969
+ // Tier 1-2: all evaluated props known at compile-time — ZERO COST
2970
+ const knownKeys = evalResult.props
2971
+ const propCount = knownKeys.length
2972
+
2973
+ if (schema.unevaluatedProperties === false) {
2974
+ const allRequired = schema.required && schema.required.length >= propCount &&
2975
+ knownKeys.every(k => schema.required.includes(k))
2976
+
2977
+ let inner
2978
+ if (allRequired && propCount > 0) {
2979
+ // TRICK 1: required covers all — key count check only
2980
+ if (!earlyKeyCount) {
2981
+ // Adaptive: for-in for <=15 keys, Object.keys for >15
2982
+ inner = propCount <= 15
2983
+ ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
2984
+ : `if(Object.keys(${v}).length!==${propCount})return false`
2985
+ _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
2976
2986
  }
2987
+ // else: already emitted early (before properties)
2988
+ } else if (propCount > 0) {
2989
+ // TRICK 3: charCodeAt switch tree. Above the lookup threshold it is
2990
+ // the additionalProperties quadratic wearing a disguise: keys that
2991
+ // share a first character, ENV_0 through ENV_999 say, all land in
2992
+ // one case whose body is a chain of every name, per key of the
2993
+ // document. The hoisted name lookup is the same cure it was there.
2994
+ inner = propCount >= AP_LOOKUP_MIN
2995
+ ? apMembershipCheck(ctx, knownKeys, v)
2996
+ : genCharCodeSwitch(knownKeys, v)
2997
+ _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
2998
+ } else {
2999
+ inner = `for(var _k in ${v})return false`
3000
+ _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
2977
3001
  }
2978
- const baseProps = baseResult.props
2979
- const branchKeyword = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
2980
-
2981
- if (schema.unevaluatedProperties === false) {
2982
- if (schema.if && (schema.then || schema.else) && !branchKeyword && !schema.patternProperties && !schema.dependentSchemas) {
2983
- // Tier 2.5: if/then/else — re-emit if function + branch-inline duplication
2984
- // Can't reuse _if from above (block-scoped), so regenerate
2985
- const ifLines2 = []
2986
- genCode(schema.if, '_iv2', ifLines2, ctx)
2987
- const ufi = ctx.varCounter++
2988
- const ifFn2 = ifLines2.length === 0
2989
- ? `function(_iv2){return true}`
2990
- : `function(_iv2){${ifLines2.join(';')};return true}`
2991
-
2992
- // if props are only evaluated when if matches (spec: failed applicators produce no annotations)
2993
- const ifProps = []
2994
- if (schema.if && schema.if.properties) ifProps.push(...Object.keys(schema.if.properties))
2995
- const thenEval = schema.then ? collectEvaluated(schema.then, ctx.schemaMap, ctx.rootDefs) : { props: [] }
2996
- const elseEval = schema.else ? collectEvaluated(schema.else, ctx.schemaMap, ctx.rootDefs) : { props: [] }
2997
- const uniqueThen = [...new Set([...baseProps, ...ifProps, ...(thenEval.props || [])])]
2998
- const uniqueElse = [...new Set([...baseProps, ...(elseEval.props || [])])]
2999
-
3000
- const thenCheck = genCharCodeSwitch(uniqueThen, v)
3001
- const elseCheck = genCharCodeSwitch(uniqueElse, v)
3002
- const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
3003
- lines.push(`${guard}{const _uif${ufi}=${ifFn2};if(_uif${ufi}(${v})){${thenCheck}}else{${elseCheck}}}`)
3004
- } else if (branchKeyword) {
3005
- // Tier 3: anyOf/oneOf — runtime tracking
3006
- const branches = schema[branchKeyword]
3007
- const branchProps = []
3008
- for (const sub of branches) {
3009
- const subResult = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
3010
- branchProps.push(subResult.props || [])
3002
+ } else if (typeof schema.unevaluatedProperties === 'object') {
3003
+ // unevaluatedProperties: {schema} — validate unknown keys
3004
+ const ui = ctx.varCounter++
3005
+ const ukVar = `_uk${ui}`
3006
+ const subLines = []
3007
+ genCode(schema.unevaluatedProperties, `${v}[${ukVar}]`, subLines, ctx)
3008
+ if (subLines.length > 0) {
3009
+ const check = subLines.join(';')
3010
+ let skipKnown = ''
3011
+ if (knownKeys.length >= AP_LOOKUP_MIN) {
3012
+ const id = emitNameLookup(ctx, knownKeys)
3013
+ skipKnown = id !== null ? `if(${id}[${ukVar}]!==undefined)continue;` : `if(${knownKeys.map(k => `${ukVar}===${JSON.stringify(k)}`).join('||')})continue;`
3014
+ } else if (knownKeys.length > 0) {
3015
+ skipKnown = `if(${knownKeys.map(k => `${ukVar}===${JSON.stringify(k)}`).join('||')})continue;`
3016
+ }
3017
+ const inner = `for(var ${ukVar} in ${v}){${skipKnown}${check}}`
3018
+ _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
3019
+ }
3020
+ }
3021
+ } else {
3022
+ // Tier 2.5 / Tier 3: dynamic — runtime tracking needed
3023
+ // Compute base props: only unconditionally evaluated (properties, allOf-static, $ref)
3024
+ const baseResult = { props: [], items: null, allProps: false, allItems: false, dynamic: false }
3025
+ if (schema.properties) {
3026
+ for (const k of Object.keys(schema.properties)) {
3027
+ if (!baseResult.props.includes(k)) baseResult.props.push(k)
3028
+ }
3029
+ }
3030
+ if (schema.allOf) {
3031
+ for (const sub of schema.allOf) {
3032
+ const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
3033
+ if (!subR.dynamic && subR.props) {
3034
+ for (const k of subR.props) {
3035
+ if (!baseResult.props.includes(k)) baseResult.props.push(k)
3011
3036
  }
3012
- const allDynamicKeys = [...new Set(branchProps.flat())]
3013
- const dynamicOnly = allDynamicKeys.filter(k => !baseProps.includes(k))
3014
-
3015
- if (dynamicOnly.length > 0 && dynamicOnly.length <= 32) {
3016
- // TRICK 5: bit-packed evaluated set — SINGLE PASS (validation + tracking combined)
3017
- const ei = ctx.varCounter++
3018
- const evVar = `_ev${ei}`
3019
- const bitMap = new Map()
3020
- dynamicOnly.forEach((k, i) => bitMap.set(k, i))
3021
- const branchMasks = branchProps.map(props => {
3022
- let mask = 0
3023
- for (const p of props) {
3024
- if (bitMap.has(p)) mask |= (1 << bitMap.get(p))
3025
- }
3026
- return mask
3027
- })
3037
+ }
3038
+ }
3039
+ }
3040
+ const baseProps = baseResult.props
3041
+ const branchKeyword = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
3028
3042
 
3029
- // TRICK 4: Direct function calls — no array, no loop, V8 can inline
3030
- const bfi = ctx.varCounter++
3031
- lines.push(`{let ${evVar}=0`)
3032
- const fnVars = []
3033
- for (let i = 0; i < branches.length; i++) {
3034
- const subLines2 = []
3035
- genCode(branches[i], '_bv', subLines2, ctx)
3036
- const fnVar = `_bf${bfi}_${i}`
3037
- fnVars.push(fnVar)
3038
- const fnBody = subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`
3039
- lines.push(`const ${fnVar}=${fnBody}`)
3040
- }
3041
- if (branchKeyword === 'oneOf') {
3042
- // oneOf: exactly one must match — direct calls
3043
- lines.push(`let _oc${bfi}=0`)
3044
- for (let i = 0; i < branches.length; i++) {
3045
- lines.push(`if(${fnVars[i]}(${v})){_oc${bfi}++;${evVar}=${branchMasks[i]};if(_oc${bfi}>1)return false}`)
3046
- }
3047
- lines.push(`if(_oc${bfi}!==1)return false`)
3048
- } else {
3049
- // anyOf: at least one must match — direct calls, collect all
3050
- lines.push(`let _am${bfi}=false`)
3051
- for (let i = 0; i < branches.length; i++) {
3052
- lines.push(`if(${fnVars[i]}(${v})){_am${bfi}=true;${evVar}|=${branchMasks[i]}}`)
3053
- }
3054
- lines.push(`if(!_am${bfi})return false`)
3055
- }
3043
+ if (schema.unevaluatedProperties === false) {
3044
+ if (schema.if && (schema.then || schema.else) && !branchKeyword && !schema.patternProperties && !schema.dependentSchemas) {
3045
+ // Tier 2.5: if/then/else — re-emit if function + branch-inline duplication
3046
+ // Can't reuse _if from above (block-scoped), so regenerate
3047
+ const ifLines2 = []
3048
+ genCode(schema.if, '_iv2', ifLines2, ctx)
3049
+ const ufi = ctx.varCounter++
3050
+ const ifFn2 = ifLines2.length === 0
3051
+ ? `function(_iv2){return true}`
3052
+ : `function(_iv2){${ifLines2.join(';')};return true}`
3053
+
3054
+ // if props are only evaluated when if matches (spec: failed applicators produce no annotations)
3055
+ const ifProps = []
3056
+ if (schema.if && schema.if.properties) ifProps.push(...Object.keys(schema.if.properties))
3057
+ const thenEval = schema.then ? collectEvaluated(schema.then, ctx.schemaMap, ctx.rootDefs) : { props: [] }
3058
+ const elseEval = schema.else ? collectEvaluated(schema.else, ctx.schemaMap, ctx.rootDefs) : { props: [] }
3059
+ const uniqueThen = [...new Set([...baseProps, ...ifProps, ...(thenEval.props || [])])]
3060
+ const uniqueElse = [...new Set([...baseProps, ...(elseEval.props || [])])]
3061
+
3062
+ const thenCheck = genCharCodeSwitch(uniqueThen, v)
3063
+ const elseCheck = genCharCodeSwitch(uniqueElse, v)
3064
+ const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
3065
+ lines.push(`${guard}{const _uif${ufi}=${ifFn2};if(_uif${ufi}(${v})){${thenCheck}}else{${elseCheck}}}`)
3066
+ } else if (branchKeyword) {
3067
+ // Tier 3: anyOf/oneOf — runtime tracking
3068
+ const branches = schema[branchKeyword]
3069
+ const branchProps = []
3070
+ for (const sub of branches) {
3071
+ const subResult = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
3072
+ branchProps.push(subResult.props || [])
3073
+ }
3074
+ const allDynamicKeys = [...new Set(branchProps.flat())]
3075
+ const dynamicOnly = allDynamicKeys.filter(k => !baseProps.includes(k))
3056
3076
 
3057
- // Final check: static keys inline + dynamic keys via bitmask
3058
- const staticCheck = baseProps.length > 0 ? baseProps.map(k => `_k===${JSON.stringify(k)}`).join('||') : ''
3059
- const groups = new Map()
3060
- for (const k of dynamicOnly) {
3061
- const cc = k.charCodeAt(0)
3062
- if (!groups.has(cc)) groups.set(cc, [])
3063
- groups.get(cc).push(k)
3064
- }
3065
- let switchCases = ''
3066
- for (const [cc, groupKeys] of groups) {
3067
- const cond = groupKeys.map(k => `_k===${JSON.stringify(k)}&&(${evVar}&${1 << bitMap.get(k)})`).join('||')
3068
- switchCases += `case ${cc}:if(${cond})continue;break;`
3069
- }
3070
- const dynamicCheck = `switch(_k.charCodeAt(0)){${switchCases}default:break}`
3071
- const inner = staticCheck
3072
- ? `for(var _k in ${v}){if(${staticCheck})continue;${dynamicCheck}return false}`
3073
- : `for(var _k in ${v}){${dynamicCheck}return false}`
3074
- _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3075
- } else {
3076
- // Fallback: plain object tracking
3077
- const ei = ctx.varCounter++
3078
- const evVar = `_ev${ei}`
3079
- const fns = []
3080
- for (let i = 0; i < branches.length; i++) {
3081
- const subLines2 = []
3082
- genCode(branches[i], '_bv', subLines2, ctx)
3083
- fns.push(subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`)
3084
- }
3085
- const bfi = ctx.varCounter++
3086
- ctx.closureVars.push(`_bk${bfi}`)
3087
- ctx.closureVals.push(branchProps)
3088
- lines.push(`{const ${evVar}={}`)
3089
- for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
3090
- lines.push(`const _bf${bfi}=[${fns.join(',')}]`)
3091
- if (branchKeyword === 'oneOf') {
3092
- // Single pass: validate oneOf (exactly one) + track evaluated
3093
- lines.push(`let _oc${bfi}=0;for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){_oc${bfi}++;for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1;if(_oc${bfi}>1)return false}}if(_oc${bfi}!==1)return false`)
3094
- } else {
3095
- // Single pass: validate anyOf (at least one) + track all matching
3096
- lines.push(`let _am${bfi}=false;for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){_am${bfi}=true;for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1}}if(!_am${bfi})return false`)
3097
- }
3098
- const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
3099
- _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3100
- }
3101
- } else if (schema.dependentSchemas) {
3102
- // dependentSchemas: conditional merge at runtime
3077
+ if (dynamicOnly.length > 0 && dynamicOnly.length <= 32) {
3078
+ // TRICK 5: bit-packed evaluated set — SINGLE PASS (validation + tracking combined)
3103
3079
  const ei = ctx.varCounter++
3104
3080
  const evVar = `_ev${ei}`
3105
- lines.push(`{const ${evVar}={}`)
3106
- for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
3107
- for (const [trigger, depSchema] of Object.entries(schema.dependentSchemas)) {
3108
- const depResult = collectEvaluated(depSchema, ctx.schemaMap, ctx.rootDefs)
3109
- if (depResult.props && depResult.props.length > 0) {
3110
- lines.push(`if(${ownKeyExpr(ctx, v, trigger)}){${depResult.props.map(k => `${evVar}[${JSON.stringify(k)}]=1`).join(';')}}`)
3111
- }
3112
- }
3113
- const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
3114
- _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3115
- } else {
3116
- // General fallback: collect all patternProperties from root + allOf sub-schemas + if
3117
- // and use runtime regex matching
3118
- const allPatterns = []
3119
- if (schema.patternProperties) {
3120
- allPatterns.push(...Object.keys(schema.patternProperties))
3121
- }
3122
- if (schema.allOf) {
3123
- for (const sub of schema.allOf) {
3124
- if (sub && sub.patternProperties) {
3125
- allPatterns.push(...Object.keys(sub.patternProperties))
3126
- }
3081
+ const bitMap = new Map()
3082
+ dynamicOnly.forEach((k, i) => bitMap.set(k, i))
3083
+ const branchMasks = branchProps.map(props => {
3084
+ let mask = 0
3085
+ for (const p of props) {
3086
+ if (bitMap.has(p)) mask |= (1 << bitMap.get(p))
3127
3087
  }
3088
+ return mask
3089
+ })
3090
+
3091
+ // TRICK 4: Direct function calls — no array, no loop, V8 can inline
3092
+ const bfi = ctx.varCounter++
3093
+ lines.push(`{let ${evVar}=0`)
3094
+ const fnVars = []
3095
+ for (let i = 0; i < branches.length; i++) {
3096
+ const subLines2 = []
3097
+ genCode(branches[i], '_bv', subLines2, ctx)
3098
+ const fnVar = `_bf${bfi}_${i}`
3099
+ fnVars.push(fnVar)
3100
+ const fnBody = subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`
3101
+ lines.push(`const ${fnVar}=${fnBody}`)
3128
3102
  }
3129
- // lone if (no then/else) still contributes annotations when it passes
3130
- if (schema.if && !schema.then && !schema.else && schema.if.patternProperties) {
3131
- allPatterns.push(...Object.keys(schema.if.patternProperties))
3132
- }
3133
- if (allPatterns.length > 0) {
3134
- const ei = ctx.varCounter++
3135
- const evVar = `_ev${ei}`
3136
- lines.push(`{const ${evVar}={}`)
3137
- for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
3138
- const reVars = []
3139
- for (const pat of allPatterns) {
3140
- const ri = ctx.varCounter++
3141
- ctx.closureVars.push(`_ure${ri}`)
3142
- ctx.closureVals.push(safeReClosure(ctx, pat))
3143
- reVars.push(`_ure${ri}`)
3103
+ if (branchKeyword === 'oneOf') {
3104
+ // oneOf: exactly one must match — direct calls
3105
+ lines.push(`let _oc${bfi}=0`)
3106
+ for (let i = 0; i < branches.length; i++) {
3107
+ lines.push(`if(${fnVars[i]}(${v})){_oc${bfi}++;${evVar}=${branchMasks[i]};if(_oc${bfi}>1)return false}`)
3144
3108
  }
3145
- if (schema.if && !schema.then && !schema.else) {
3146
- // Lone if: run the if check first; if it passes, its patternProperties contribute
3147
- const ifLines2 = []
3148
- genCode(schema.if, '_iv2', ifLines2, ctx)
3149
- const ufi = ctx.varCounter++
3150
- const ifFn = ifLines2.length === 0
3151
- ? `function(_iv2){return true}`
3152
- : `function(_iv2){${ifLines2.join(';')};return true}`
3153
- // Mark keys matching if's patterns as evaluated only when if passes
3154
- const ifPatterns = schema.if.patternProperties ? Object.keys(schema.if.patternProperties) : []
3155
- const ifReVars = []
3156
- for (const pat of ifPatterns) {
3157
- const ri = ctx.varCounter++
3158
- ctx.closureVars.push(`_ure${ri}`)
3159
- ctx.closureVals.push(safeReClosure(ctx, pat))
3160
- ifReVars.push(`_ure${ri}`)
3161
- }
3162
- const rootReVars = []
3163
- if (schema.patternProperties) {
3164
- for (const pat of Object.keys(schema.patternProperties)) {
3165
- const ri = ctx.varCounter++
3166
- ctx.closureVars.push(`_ure${ri}`)
3167
- ctx.closureVals.push(safeReClosure(ctx, pat))
3168
- rootReVars.push(`_ure${ri}`)
3169
- }
3170
- }
3171
- const rootPatCheck = rootReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
3172
- const ifPatCheck = ifReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
3173
- const inner = `const _uif${ufi}=${ifFn};if(_uif${ufi}(${v})){for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}${ifPatCheck}return false}}else{for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}return false}}`
3174
- _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3175
- } else {
3176
- const inner = `for(var _k in ${v}){if(${evVar}[_k])continue;${reVars.map(rv => `if(${rv}.test(_k)){${evVar}[_k]=1;continue}`).join('')}return false}`
3177
- _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3109
+ lines.push(`if(_oc${bfi}!==1)return false`)
3110
+ } else {
3111
+ // anyOf: at least one must match — direct calls, collect all
3112
+ lines.push(`let _am${bfi}=false`)
3113
+ for (let i = 0; i < branches.length; i++) {
3114
+ lines.push(`if(${fnVars[i]}(${v})){_am${bfi}=true;${evVar}|=${branchMasks[i]}}`)
3178
3115
  }
3116
+ lines.push(`if(!_am${bfi})return false`)
3179
3117
  }
3180
- }
3181
- } else if (typeof schema.unevaluatedProperties === 'object') {
3182
- // Tier 3 with schema: validate unknown keys against sub-schema
3183
- const ei = ctx.varCounter++
3184
- const evVar = `_ev${ei}`
3185
- const ukVar = `_uk${ei}`
3186
- lines.push(`{const ${evVar}={}`)
3187
- for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
3188
3118
 
3189
- if (branchKeyword) {
3190
- const branches = schema[branchKeyword]
3191
- const branchProps = []
3192
- for (const sub of branches) {
3193
- const subResult = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
3194
- branchProps.push(subResult.props || [])
3119
+ // Final check: static keys inline + dynamic keys via bitmask
3120
+ const staticCheck = baseProps.length > 0 ? baseProps.map(k => `_k===${JSON.stringify(k)}`).join('||') : ''
3121
+ const groups = new Map()
3122
+ for (const k of dynamicOnly) {
3123
+ const cc = k.charCodeAt(0)
3124
+ if (!groups.has(cc)) groups.set(cc, [])
3125
+ groups.get(cc).push(k)
3195
3126
  }
3127
+ let switchCases = ''
3128
+ for (const [cc, groupKeys] of groups) {
3129
+ const cond = groupKeys.map(k => `_k===${JSON.stringify(k)}&&(${evVar}&${1 << bitMap.get(k)})`).join('||')
3130
+ switchCases += `case ${cc}:if(${cond})continue;break;`
3131
+ }
3132
+ const dynamicCheck = `switch(_k.charCodeAt(0)){${switchCases}default:break}`
3133
+ const inner = staticCheck
3134
+ ? `for(var _k in ${v}){if(${staticCheck})continue;${dynamicCheck}return false}`
3135
+ : `for(var _k in ${v}){${dynamicCheck}return false}`
3136
+ _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3137
+ } else {
3138
+ // Fallback: plain object tracking
3139
+ const ei = ctx.varCounter++
3140
+ const evVar = `_ev${ei}`
3196
3141
  const fns = []
3197
3142
  for (let i = 0; i < branches.length; i++) {
3198
3143
  const subLines2 = []
@@ -3202,85 +3147,114 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3202
3147
  const bfi = ctx.varCounter++
3203
3148
  ctx.closureVars.push(`_bk${bfi}`)
3204
3149
  ctx.closureVals.push(branchProps)
3150
+ lines.push(`{const ${evVar}={}`)
3151
+ for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
3205
3152
  lines.push(`const _bf${bfi}=[${fns.join(',')}]`)
3206
3153
  if (branchKeyword === 'oneOf') {
3207
- lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1;break}}`)
3154
+ // Single pass: validate oneOf (exactly one) + track evaluated
3155
+ lines.push(`let _oc${bfi}=0;for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){_oc${bfi}++;for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1;if(_oc${bfi}>1)return false}}if(_oc${bfi}!==1)return false`)
3208
3156
  } else {
3209
- lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1}}`)
3157
+ // Single pass: validate anyOf (at least one) + track all matching
3158
+ lines.push(`let _am${bfi}=false;for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){_am${bfi}=true;for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1}}if(!_am${bfi})return false`)
3210
3159
  }
3160
+ const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
3161
+ _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3211
3162
  }
3212
-
3213
- const subLines2 = []
3214
- genCode(schema.unevaluatedProperties, `${v}[${ukVar}]`, subLines2, ctx)
3215
- if (subLines2.length > 0) {
3216
- const check = subLines2.join(';')
3217
- const inner = `for(var ${ukVar} in ${v}){if(${evVar}[${ukVar}])continue;${check}}`
3218
- _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3219
- } else {
3220
- lines.push('}')
3163
+ } else if (schema.dependentSchemas) {
3164
+ // dependentSchemas: conditional merge at runtime
3165
+ const ei = ctx.varCounter++
3166
+ const evVar = `_ev${ei}`
3167
+ lines.push(`{const ${evVar}={}`)
3168
+ for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
3169
+ for (const [trigger, depSchema] of Object.entries(schema.dependentSchemas)) {
3170
+ const depResult = collectEvaluated(depSchema, ctx.schemaMap, ctx.rootDefs)
3171
+ if (depResult.props && depResult.props.length > 0) {
3172
+ lines.push(`if(${ownKeyExpr(ctx, v, trigger)}){${depResult.props.map(k => `${evVar}[${JSON.stringify(k)}]=1`).join(';')}}`)
3173
+ }
3221
3174
  }
3222
- }
3223
- }
3224
- }
3225
-
3226
- // unevaluatedItems
3227
- if (schema.unevaluatedItems !== undefined) {
3228
- const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
3229
-
3230
- // Check if allItems from anyOf/oneOf branches with `items` keyword needs dynamic tracking
3231
- const branchKw = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
3232
- const hasConditionalItems = evalResult.allItems && evalResult.dynamic && branchKw &&
3233
- schema[branchKw].some(sub => sub && typeof sub === 'object' && ((sub.items && typeof sub.items === 'object') || sub.items === true))
3234
-
3235
- if (schema.unevaluatedItems === true || (evalResult.allItems && !hasConditionalItems)) {
3236
- // All items evaluated or unevaluatedItems:true — no-op
3237
- } else if (!evalResult.dynamic) {
3238
- // Static: all evaluated items known at compile-time
3239
- if (schema.unevaluatedItems === false) {
3240
- // TRICK 6: Array.length comparison only
3241
- const maxIdx = evalResult.items || 0
3242
- const inner = `if(${v}.length>${maxIdx})return false`
3243
- _deferOrInline(ctx, lines, v, isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
3244
- } else if (typeof schema.unevaluatedItems === 'object') {
3245
- const maxIdx = evalResult.items || 0
3246
- const ui = ctx.varCounter++
3247
- const elemVar = `_ue${ui}`
3248
- const idxVar = `_ui${ui}`
3249
- const subLines = []
3250
- genCode(schema.unevaluatedItems, elemVar, subLines, ctx)
3251
- if (subLines.length > 0) {
3252
- const check = subLines.join(';')
3253
- const inner = `for(let ${idxVar}=${maxIdx};${idxVar}<${v}.length;${idxVar}++){const ${elemVar}=${v}[${idxVar}];${check}}`
3254
- _deferOrInline(ctx, lines, v, isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
3175
+ const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
3176
+ _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3177
+ } else {
3178
+ // General fallback: collect all patternProperties from root + allOf sub-schemas + if
3179
+ // and use runtime regex matching
3180
+ const allPatterns = []
3181
+ if (schema.patternProperties) {
3182
+ allPatterns.push(...Object.keys(schema.patternProperties))
3255
3183
  }
3256
- }
3257
- } else {
3258
- // Dynamic: runtime tracking of max evaluated index
3259
- // Compute baseIdx from unconditional sources only (root prefixItems/items, allOf)
3260
- let baseIdx = 0
3261
- if (schema.prefixItems) baseIdx = Math.max(baseIdx, schema.prefixItems.length)
3262
- if (schema.items && typeof schema.items === 'object') baseIdx = Infinity // items: schema → all evaluated
3263
- if (schema.allOf) {
3264
- for (const sub of schema.allOf) {
3265
- const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
3266
- if (subR.items !== null) baseIdx = Math.max(baseIdx, subR.items)
3267
- if (subR.allItems) baseIdx = Infinity
3184
+ if (schema.allOf) {
3185
+ for (const sub of schema.allOf) {
3186
+ if (sub && sub.patternProperties) {
3187
+ allPatterns.push(...Object.keys(sub.patternProperties))
3188
+ }
3189
+ }
3190
+ }
3191
+ // lone if (no then/else) still contributes annotations when it passes
3192
+ if (schema.if && !schema.then && !schema.else && schema.if.patternProperties) {
3193
+ allPatterns.push(...Object.keys(schema.if.patternProperties))
3194
+ }
3195
+ if (allPatterns.length > 0) {
3196
+ const ei = ctx.varCounter++
3197
+ const evVar = `_ev${ei}`
3198
+ lines.push(`{const ${evVar}={}`)
3199
+ for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
3200
+ const reVars = []
3201
+ for (const pat of allPatterns) {
3202
+ const ri = ctx.varCounter++
3203
+ ctx.closureVars.push(`_ure${ri}`)
3204
+ ctx.closureVals.push(safeReClosure(ctx, pat))
3205
+ reVars.push(`_ure${ri}`)
3206
+ }
3207
+ if (schema.if && !schema.then && !schema.else) {
3208
+ // Lone if: run the if check first; if it passes, its patternProperties contribute
3209
+ const ifLines2 = []
3210
+ genCode(schema.if, '_iv2', ifLines2, ctx)
3211
+ const ufi = ctx.varCounter++
3212
+ const ifFn = ifLines2.length === 0
3213
+ ? `function(_iv2){return true}`
3214
+ : `function(_iv2){${ifLines2.join(';')};return true}`
3215
+ // Mark keys matching if's patterns as evaluated only when if passes
3216
+ const ifPatterns = schema.if.patternProperties ? Object.keys(schema.if.patternProperties) : []
3217
+ const ifReVars = []
3218
+ for (const pat of ifPatterns) {
3219
+ const ri = ctx.varCounter++
3220
+ ctx.closureVars.push(`_ure${ri}`)
3221
+ ctx.closureVals.push(safeReClosure(ctx, pat))
3222
+ ifReVars.push(`_ure${ri}`)
3223
+ }
3224
+ const rootReVars = []
3225
+ if (schema.patternProperties) {
3226
+ for (const pat of Object.keys(schema.patternProperties)) {
3227
+ const ri = ctx.varCounter++
3228
+ ctx.closureVars.push(`_ure${ri}`)
3229
+ ctx.closureVals.push(safeReClosure(ctx, pat))
3230
+ rootReVars.push(`_ure${ri}`)
3231
+ }
3232
+ }
3233
+ const rootPatCheck = rootReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
3234
+ const ifPatCheck = ifReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
3235
+ const inner = `const _uif${ufi}=${ifFn};if(_uif${ufi}(${v})){for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}${ifPatCheck}return false}}else{for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}return false}}`
3236
+ _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3237
+ } else {
3238
+ const inner = `for(var _k in ${v}){if(${evVar}[_k])continue;${reVars.map(rv => `if(${rv}.test(_k)){${evVar}[_k]=1;continue}`).join('')}return false}`
3239
+ _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3240
+ }
3268
3241
  }
3269
3242
  }
3270
- if (baseIdx === Infinity) baseIdx = 0 // allItems already handled above
3271
- const branchKeyword = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
3243
+ } else if (typeof schema.unevaluatedProperties === 'object') {
3244
+ // Tier 3 with schema: validate unknown keys against sub-schema
3245
+ const ei = ctx.varCounter++
3246
+ const evVar = `_ev${ei}`
3247
+ const ukVar = `_uk${ei}`
3248
+ lines.push(`{const ${evVar}={}`)
3249
+ for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
3272
3250
 
3273
- if (branchKeyword && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
3274
- // anyOf/oneOf: each branch may evaluate different number of items
3251
+ if (branchKeyword) {
3275
3252
  const branches = schema[branchKeyword]
3276
- const branchMaxIdx = []
3277
- const branchAllItems = []
3253
+ const branchProps = []
3278
3254
  for (const sub of branches) {
3279
- const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
3280
- branchMaxIdx.push(subR.items || 0)
3281
- branchAllItems.push(subR.allItems)
3255
+ const subResult = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
3256
+ branchProps.push(subResult.props || [])
3282
3257
  }
3283
- // Runtime: find max evaluated index across all matching branches
3284
3258
  const fns = []
3285
3259
  for (let i = 0; i < branches.length; i++) {
3286
3260
  const subLines2 = []
@@ -3288,108 +3262,194 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3288
3262
  fns.push(subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`)
3289
3263
  }
3290
3264
  const bfi = ctx.varCounter++
3291
- const ei = ctx.varCounter++
3292
- const evVar = `_eidx${ei}`
3293
- lines.push(`{let ${evVar}=${baseIdx}`)
3265
+ ctx.closureVars.push(`_bk${bfi}`)
3266
+ ctx.closureVals.push(branchProps)
3294
3267
  lines.push(`const _bf${bfi}=[${fns.join(',')}]`)
3295
- const maxExprs = branchMaxIdx.map((m, i) => {
3296
- if (branchAllItems[i]) return `_bi===${i}?${v}.length`
3297
- return `_bi===${i}?${Math.max(m, baseIdx)}`
3298
- }).join(':') + `:${baseIdx}`
3299
3268
  if (branchKeyword === 'oneOf') {
3300
- lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){${evVar}=${maxExprs};break}}`)
3301
- } else {
3302
- lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){const _m=${maxExprs};if(_m>${evVar})${evVar}=_m}}`)
3303
- }
3304
- if (schema.unevaluatedItems === false) {
3305
- const inner = `if(${v}.length>${evVar})return false`
3306
- _deferOrInline(ctx, lines, v, isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
3269
+ lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1;break}}`)
3307
3270
  } else {
3308
- const ui = ctx.varCounter++
3309
- const elemVar = `_ue${ui}`
3310
- const idxVar = `_ui${ui}`
3311
- const subLines = []
3312
- genCode(schema.unevaluatedItems, elemVar, subLines, ctx)
3313
- if (subLines.length > 0) {
3314
- const check = subLines.join(';')
3315
- const inner = `for(let ${idxVar}=${evVar};${idxVar}<${v}.length;${idxVar}++){const ${elemVar}=${v}[${idxVar}];${check}}`
3316
- _deferOrInline(ctx, lines, v, isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
3317
- } else {
3318
- lines.push('}')
3319
- }
3271
+ lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1}}`)
3320
3272
  }
3321
- } else if (schema.if && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
3322
- // if/then/else (or lone if): branch-specific max index
3323
- const ifEval = collectEvaluated(schema.if, ctx.schemaMap, ctx.rootDefs)
3324
- const thenEval = schema.then ? collectEvaluated(schema.then, ctx.schemaMap, ctx.rootDefs) : { items: null }
3325
- const elseEval = schema.else ? collectEvaluated(schema.else, ctx.schemaMap, ctx.rootDefs) : { items: null }
3326
- const ifIdx = ifEval.items || 0
3327
- const thenIdx = Math.max(baseIdx, ifIdx, thenEval.items || 0)
3328
- const elseIdx = Math.max(baseIdx, elseEval.items || 0)
3273
+ }
3329
3274
 
3330
- const ifLines2 = []
3331
- genCode(schema.if, '_iv3', ifLines2, ctx)
3332
- const ufi = ctx.varCounter++
3333
- const ifFn3 = ifLines2.length === 0
3334
- ? `function(_iv3){return true}`
3335
- : `function(_iv3){${ifLines2.join(';')};return true}`
3275
+ const subLines2 = []
3276
+ genCode(schema.unevaluatedProperties, `${v}[${ukVar}]`, subLines2, ctx)
3277
+ if (subLines2.length > 0) {
3278
+ const check = subLines2.join(';')
3279
+ const inner = `for(var ${ukVar} in ${v}){if(${evVar}[${ukVar}])continue;${check}}`
3280
+ _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3281
+ } else {
3282
+ lines.push('}')
3283
+ }
3284
+ }
3285
+ }
3286
+ }
3336
3287
 
3337
- if (schema.unevaluatedItems === false) {
3338
- const guard = isArr ? '' : `if(Array.isArray(${v}))`
3339
- lines.push(`${guard}{const _uif${ufi}=${ifFn3};if(_uif${ufi}(${v})){if(${v}.length>${thenIdx})return false}else{if(${v}.length>${elseIdx})return false}}`)
3340
- }
3341
- } else if ((schema.contains || (schema.allOf && schema.allOf.some(s => s && s.contains))) && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
3342
- // contains + unevaluatedItems: per-item tracking of which items are matched by contains
3343
- // Collect contains from root and allOf sub-schemas
3344
- const allContains = []
3345
- if (schema.contains) allContains.push(schema.contains)
3346
- if (schema.allOf) {
3347
- for (const sub of schema.allOf) {
3348
- if (sub && sub.contains) allContains.push(sub.contains)
3349
- }
3350
- }
3351
- const ci = ctx.varCounter++
3352
- const evArr = `_cev${ci}`
3353
- const containsFns = []
3354
- for (const c of allContains) {
3355
- const cLines = []
3356
- genCode(c, '_cv', cLines, ctx)
3357
- containsFns.push(cLines.length === 0
3358
- ? `function(_cv){return true}`
3359
- : `function(_cv){${cLines.join(';')};return true}`)
3288
+ function genUnevaluatedItemsNode(schema, v, lines, ctx, knownType, isArr) {
3289
+ const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
3290
+
3291
+ // Check if allItems from anyOf/oneOf branches with `items` keyword needs dynamic tracking
3292
+ const branchKw = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
3293
+ const hasConditionalItems = evalResult.allItems && evalResult.dynamic && branchKw &&
3294
+ schema[branchKw].some(sub => sub && typeof sub === 'object' && ((sub.items && typeof sub.items === 'object') || sub.items === true))
3295
+
3296
+ if (schema.unevaluatedItems === true || (evalResult.allItems && !hasConditionalItems)) {
3297
+ // All items evaluated or unevaluatedItems:true — no-op
3298
+ } else if (!evalResult.dynamic) {
3299
+ // Static: all evaluated items known at compile-time
3300
+ if (schema.unevaluatedItems === false) {
3301
+ // TRICK 6: Array.length comparison only
3302
+ const maxIdx = evalResult.items || 0
3303
+ const inner = `if(${v}.length>${maxIdx})return false`
3304
+ _deferOrInline(ctx, lines, v, isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
3305
+ } else if (typeof schema.unevaluatedItems === 'object') {
3306
+ const maxIdx = evalResult.items || 0
3307
+ const ui = ctx.varCounter++
3308
+ const elemVar = `_ue${ui}`
3309
+ const idxVar = `_ui${ui}`
3310
+ const subLines = []
3311
+ genCode(schema.unevaluatedItems, elemVar, subLines, ctx)
3312
+ if (subLines.length > 0) {
3313
+ const check = subLines.join(';')
3314
+ const inner = `for(let ${idxVar}=${maxIdx};${idxVar}<${v}.length;${idxVar}++){const ${elemVar}=${v}[${idxVar}];${check}}`
3315
+ _deferOrInline(ctx, lines, v, isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
3316
+ }
3317
+ }
3318
+ } else {
3319
+ // Dynamic: runtime tracking of max evaluated index
3320
+ // Compute baseIdx from unconditional sources only (root prefixItems/items, allOf)
3321
+ let baseIdx = 0
3322
+ if (schema.prefixItems) baseIdx = Math.max(baseIdx, schema.prefixItems.length)
3323
+ if (schema.items && typeof schema.items === 'object') baseIdx = Infinity // items: schema → all evaluated
3324
+ if (schema.allOf) {
3325
+ for (const sub of schema.allOf) {
3326
+ const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
3327
+ if (subR.items !== null) baseIdx = Math.max(baseIdx, subR.items)
3328
+ if (subR.allItems) baseIdx = Infinity
3329
+ }
3330
+ }
3331
+ if (baseIdx === Infinity) baseIdx = 0 // allItems already handled above
3332
+ const branchKeyword = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
3333
+
3334
+ if (branchKeyword && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
3335
+ // anyOf/oneOf: each branch may evaluate different number of items
3336
+ const branches = schema[branchKeyword]
3337
+ const branchMaxIdx = []
3338
+ const branchAllItems = []
3339
+ for (const sub of branches) {
3340
+ const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
3341
+ branchMaxIdx.push(subR.items || 0)
3342
+ branchAllItems.push(subR.allItems)
3343
+ }
3344
+ // Runtime: find max evaluated index across all matching branches
3345
+ const fns = []
3346
+ for (let i = 0; i < branches.length; i++) {
3347
+ const subLines2 = []
3348
+ genCode(branches[i], '_bv', subLines2, ctx)
3349
+ fns.push(subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`)
3350
+ }
3351
+ const bfi = ctx.varCounter++
3352
+ const ei = ctx.varCounter++
3353
+ const evVar = `_eidx${ei}`
3354
+ lines.push(`{let ${evVar}=${baseIdx}`)
3355
+ lines.push(`const _bf${bfi}=[${fns.join(',')}]`)
3356
+ const maxExprs = branchMaxIdx.map((m, i) => {
3357
+ if (branchAllItems[i]) return `_bi===${i}?${v}.length`
3358
+ return `_bi===${i}?${Math.max(m, baseIdx)}`
3359
+ }).join(':') + `:${baseIdx}`
3360
+ if (branchKeyword === 'oneOf') {
3361
+ lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){${evVar}=${maxExprs};break}}`)
3362
+ } else {
3363
+ lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){const _m=${maxExprs};if(_m>${evVar})${evVar}=_m}}`)
3364
+ }
3365
+ if (schema.unevaluatedItems === false) {
3366
+ const inner = `if(${v}.length>${evVar})return false`
3367
+ _deferOrInline(ctx, lines, v, isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
3368
+ } else {
3369
+ const ui = ctx.varCounter++
3370
+ const elemVar = `_ue${ui}`
3371
+ const idxVar = `_ui${ui}`
3372
+ const subLines = []
3373
+ genCode(schema.unevaluatedItems, elemVar, subLines, ctx)
3374
+ if (subLines.length > 0) {
3375
+ const check = subLines.join(';')
3376
+ const inner = `for(let ${idxVar}=${evVar};${idxVar}<${v}.length;${idxVar}++){const ${elemVar}=${v}[${idxVar}];${check}}`
3377
+ _deferOrInline(ctx, lines, v, isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
3378
+ } else {
3379
+ lines.push('}')
3360
3380
  }
3361
- const cfnArr = `_cfn${ci}`
3362
- lines.push(`{const ${cfnArr}=[${containsFns.join(',')}]`)
3363
- // Mark items evaluated by prefixItems
3364
- lines.push(`const ${evArr}=[]`)
3365
- if (baseIdx > 0) {
3366
- lines.push(`for(let _i=0;_i<${Math.min(baseIdx, 1000)};_i++)${evArr}[_i]=true`)
3381
+ }
3382
+ } else if (schema.if && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
3383
+ // if/then/else (or lone if): branch-specific max index
3384
+ const ifEval = collectEvaluated(schema.if, ctx.schemaMap, ctx.rootDefs)
3385
+ const thenEval = schema.then ? collectEvaluated(schema.then, ctx.schemaMap, ctx.rootDefs) : { items: null }
3386
+ const elseEval = schema.else ? collectEvaluated(schema.else, ctx.schemaMap, ctx.rootDefs) : { items: null }
3387
+ const ifIdx = ifEval.items || 0
3388
+ const thenIdx = Math.max(baseIdx, ifIdx, thenEval.items || 0)
3389
+ const elseIdx = Math.max(baseIdx, elseEval.items || 0)
3390
+
3391
+ const ifLines2 = []
3392
+ genCode(schema.if, '_iv3', ifLines2, ctx)
3393
+ const ufi = ctx.varCounter++
3394
+ const ifFn3 = ifLines2.length === 0
3395
+ ? `function(_iv3){return true}`
3396
+ : `function(_iv3){${ifLines2.join(';')};return true}`
3397
+
3398
+ if (schema.unevaluatedItems === false) {
3399
+ const guard = isArr ? '' : `if(Array.isArray(${v}))`
3400
+ lines.push(`${guard}{const _uif${ufi}=${ifFn3};if(_uif${ufi}(${v})){if(${v}.length>${thenIdx})return false}else{if(${v}.length>${elseIdx})return false}}`)
3401
+ }
3402
+ } else if ((schema.contains || (schema.allOf && schema.allOf.some(s => s && s.contains))) && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
3403
+ // contains + unevaluatedItems: per-item tracking of which items are matched by contains
3404
+ // Collect contains from root and allOf sub-schemas
3405
+ const allContains = []
3406
+ if (schema.contains) allContains.push(schema.contains)
3407
+ if (schema.allOf) {
3408
+ for (const sub of schema.allOf) {
3409
+ if (sub && sub.contains) allContains.push(sub.contains)
3367
3410
  }
3368
- // Mark items matched by each contains function
3369
- lines.push(`if(Array.isArray(${v})){for(let _ci=0;_ci<${v}.length;_ci++){for(let _cj=0;_cj<${cfnArr}.length;_cj++){if(${cfnArr}[_cj](${v}[_ci])){${evArr}[_ci]=true;break}}}}`)
3370
- if (schema.unevaluatedItems === false) {
3371
- const inner = `if(Array.isArray(${v})){for(let _ci=0;_ci<${v}.length;_ci++){if(!${evArr}[_ci])return false}}`
3372
- _deferOrInline(ctx, lines, v, inner + '}')
3411
+ }
3412
+ const ci = ctx.varCounter++
3413
+ const evArr = `_cev${ci}`
3414
+ const containsFns = []
3415
+ for (const c of allContains) {
3416
+ const cLines = []
3417
+ genCode(c, '_cv', cLines, ctx)
3418
+ containsFns.push(cLines.length === 0
3419
+ ? `function(_cv){return true}`
3420
+ : `function(_cv){${cLines.join(';')};return true}`)
3421
+ }
3422
+ const cfnArr = `_cfn${ci}`
3423
+ lines.push(`{const ${cfnArr}=[${containsFns.join(',')}]`)
3424
+ // Mark items evaluated by prefixItems
3425
+ lines.push(`const ${evArr}=[]`)
3426
+ if (baseIdx > 0) {
3427
+ lines.push(`for(let _i=0;_i<${Math.min(baseIdx, 1000)};_i++)${evArr}[_i]=true`)
3428
+ }
3429
+ // Mark items matched by each contains function
3430
+ lines.push(`if(Array.isArray(${v})){for(let _ci=0;_ci<${v}.length;_ci++){for(let _cj=0;_cj<${cfnArr}.length;_cj++){if(${cfnArr}[_cj](${v}[_ci])){${evArr}[_ci]=true;break}}}}`)
3431
+ if (schema.unevaluatedItems === false) {
3432
+ const inner = `if(Array.isArray(${v})){for(let _ci=0;_ci<${v}.length;_ci++){if(!${evArr}[_ci])return false}}`
3433
+ _deferOrInline(ctx, lines, v, inner + '}')
3434
+ } else {
3435
+ // unevaluatedItems: {schema}
3436
+ const ui = ctx.varCounter++
3437
+ const elemVar = `_ue${ui}`
3438
+ const subLines = []
3439
+ genCode(schema.unevaluatedItems, elemVar, subLines, ctx)
3440
+ if (subLines.length > 0) {
3441
+ const check = subLines.join(';')
3442
+ const inner = `if(Array.isArray(${v})){for(let _ci=0;_ci<${v}.length;_ci++){if(!${evArr}[_ci]){const ${elemVar}=${v}[_ci];${check}}}}`
3443
+ _deferOrInline(ctx, lines, v, inner + '}')
3373
3444
  } else {
3374
- // unevaluatedItems: {schema}
3375
- const ui = ctx.varCounter++
3376
- const elemVar = `_ue${ui}`
3377
- const subLines = []
3378
- genCode(schema.unevaluatedItems, elemVar, subLines, ctx)
3379
- if (subLines.length > 0) {
3380
- const check = subLines.join(';')
3381
- const inner = `if(Array.isArray(${v})){for(let _ci=0;_ci<${v}.length;_ci++){if(!${evArr}[_ci]){const ${elemVar}=${v}[_ci];${check}}}}`
3382
- _deferOrInline(ctx, lines, v, inner + '}')
3383
- } else {
3384
- lines.push('}')
3385
- }
3445
+ lines.push('}')
3386
3446
  }
3387
- } else if (schema.unevaluatedItems === false) {
3388
- // Fallback: use static base index (may not be fully correct for all dynamic cases)
3389
- const maxIdx = evalResult.items || 0
3390
- const inner = `if(${v}.length>${maxIdx})return false`
3391
- _deferOrInline(ctx, lines, v, isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
3392
3447
  }
3448
+ } else if (schema.unevaluatedItems === false) {
3449
+ // Fallback: use static base index (may not be fully correct for all dynamic cases)
3450
+ const maxIdx = evalResult.items || 0
3451
+ const inner = `if(${v}.length>${maxIdx})return false`
3452
+ _deferOrInline(ctx, lines, v, isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
3393
3453
  }
3394
3454
  }
3395
3455
  }
@@ -4064,24 +4124,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4064
4124
  }
4065
4125
 
4066
4126
  // $dynamicRef — resolve via anchors map
4067
- if (schema.$dynamicRef) {
4068
- const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
4069
- if (ctx.anchors && ctx.anchors[anchorKey]) {
4070
- const target = ctx.anchors[anchorKey]
4071
- if (target === ctx.rootSchema) {
4072
- // Self-recursive: generate _validateE call
4073
- ctx.usesRecursion = true
4074
- lines.push(`_validateE(${v},_all,_e)`)
4075
- } else {
4076
- const refKey = '$dynamicRef:' + anchorKey
4077
- if (!ctx.refStack.has(refKey)) {
4078
- ctx.refStack.add(refKey)
4079
- genCodeE(target, v, pathExpr, lines, ctx, schemaPrefix)
4080
- ctx.refStack.delete(refKey)
4081
- }
4082
- }
4083
- }
4084
- }
4127
+ if (schema.$dynamicRef) genDynamicRefE(schema, v, pathExpr, lines, ctx, schemaPrefix)
4085
4128
 
4086
4129
  const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
4087
4130
  if (types) {
@@ -4246,22 +4289,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4246
4289
  }
4247
4290
 
4248
4291
  // uniqueItems — tiered: small primitive arrays use nested loop (no allocation)
4249
- if (schema.uniqueItems) {
4250
- const si = ctx.varCounter++
4251
- const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
4252
- const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
4253
- const maxItems = schema.maxItems
4254
- const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
4255
- let inner
4256
- if (isPrim && maxItems && maxItems <= 16) {
4257
- inner = `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k]){${failExpr('_k', '_i')};break}}}`
4258
- } else if (isPrim) {
4259
- inner = `const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _prev=_s${si}.get(${v}[_i]);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(${v}[_i],_i)}`
4260
- } else {
4261
- 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 Map();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);const _prev=_s${si}.get(_k);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(_k,_i)}`
4262
- }
4263
- lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
4264
- }
4292
+ if (schema.uniqueItems) genUniqueItemsE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr)
4265
4293
 
4266
4294
  // object size
4267
4295
  if (schema.minProperties !== undefined) {
@@ -4311,45 +4339,13 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4311
4339
  // to emit. The error keeps the keyword's own identity: the interpreted
4312
4340
  // engine reports these as unevaluatedProperties with the key named, and the
4313
4341
  // two engines are pinned to identical output.
4314
- if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined) {
4315
- const allowedU = Object.keys(schema.properties || {}).map(k => `${JSON.stringify(k)}`).join(',')
4316
- const ui = ctx.varCounter++
4317
- const upSp = `${schemaPrefix}/unevaluatedProperties`
4318
- const upLit = buildErrorLiteral({ keyword: 'unevaluatedProperties', schemaPath: upSp, sourceMap: ctx.sourceMap })
4319
- const patChecksU = []
4320
- for (const pat of Object.keys(schema.patternProperties || {})) {
4321
- const pattern = JSON.stringify(pat)
4322
- if (!ctx.regExpMap.has(pattern)) {
4323
- const ri = ctx.varCounter++
4324
- ctx.regExpMap.set(pattern, ri)
4325
- if (patternIsSafe(pat)) {
4326
- ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
4327
- ctx.usesSafeRe = true
4328
- } else {
4329
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
4330
- }
4331
- }
4332
- patChecksU.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ui}[_i])`)
4333
- }
4334
- const isUneval = patChecksU.length ? `!_a${ui}.has(_k${ui}[_i])&&!(${patChecksU.join('||')})` : `!_a${ui}.has(_k${ui}[_i])`
4335
- const innerU = `const _k${ui}=Object.keys(${v});const _a${ui}=new Set([${allowedU}]);for(let _i=0;_i<_k${ui}.length;_i++){if(${isUneval}){_e.push({code:'${upLit.codeStr}',keyword:'unevaluatedProperties',instancePath:${pathExpr||'""'},schemaPath:'${upSp}'${ordinalField(ctx, `${upSp}`)},params:{unevaluatedProperty:_k${ui}[_i]},message:'must NOT have unevaluated properties',docUrl:'${upLit.docUrl}'${upLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
4336
- lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
4337
- }
4342
+ if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined) genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj)
4338
4343
 
4339
4344
  // unevaluatedItems: false under the same rule. The interpreted engine
4340
4345
  // reports one error for the array with the first unevaluated position as
4341
4346
  // the limit; with adjacent `items` everything is evaluated. The draft-07
4342
4347
  // array form of items acts as the prefix.
4343
- if (schema.unevaluatedItems === false) {
4344
- const itemsIsPrefix = Array.isArray(schema.items)
4345
- const allEval = schema.items !== undefined && !itemsIsPrefix
4346
- if (!allEval) {
4347
- const plen = itemsIsPrefix ? schema.items.length : (Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0)
4348
- const uiSp = `${schemaPrefix}/unevaluatedItems`
4349
- const uiLit = buildErrorLiteral({ keyword: 'unevaluatedItems', schemaPath: uiSp, sourceMap: ctx.sourceMap })
4350
- lines.push(`if(Array.isArray(${v})&&${v}.length>${plen}){_e.push({code:'${uiLit.codeStr}',keyword:'unevaluatedItems',instancePath:${pathExpr||'""'},schemaPath:'${uiSp}'${ordinalField(ctx, `${uiSp}`)},params:{limit:${plen}},message:'must NOT have more than ${plen} items',docUrl:'${uiLit.docUrl}'${uiLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
4351
- }
4352
- }
4348
+ if (schema.unevaluatedItems === false) genUnevaluatedItemsFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix)
4353
4349
 
4354
4350
  // dependentRequired
4355
4351
  if (schema.dependentRequired) {
@@ -4373,207 +4369,304 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4373
4369
  }
4374
4370
 
4375
4371
  // patternProperties
4376
- if (schema.patternProperties) {
4377
- for (const [pat, sub] of Object.entries(schema.patternProperties)) {
4378
- const pattern = JSON.stringify(pat);
4379
- if (!ctx.regExpMap.has(pattern)) {
4380
- const ri = ctx.varCounter++
4381
- ctx.regExpMap.set(pattern, ri)
4382
- if (patternIsSafe(pat)) {
4383
- ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
4384
- ctx.usesSafeRe = true
4385
- } else {
4386
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
4387
- }
4388
- }
4389
- const ri = ctx.regExpMap.get(pattern);
4390
- const ki = ctx.varCounter++
4391
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){if(_re${ri}.test(_k${ki})){`)
4392
- const _peFn = emitPtrEsc(ctx)
4393
- const p = pathExpr ? `${pathExpr}+'/'+${_peFn}(_k${ki})` : `'/'+${_peFn}(_k${ki})`
4394
- // The real pointer, so source maps can locate the failing keyword.
4395
- genCodeE(sub, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/patternProperties/' + pat.replace(/~/g, '~0').replace(/\//g, '~1'))
4396
- lines.push(`}}}`)
4397
- }
4398
- }
4372
+ if (schema.patternProperties) genPatternPropertiesE(schema, v, pathExpr, lines, ctx, schemaPrefix)
4399
4373
 
4400
4374
  // additionalProperties as a schema — validate each property not declared in
4401
4375
  // `properties` against the subschema, with a dynamic /<key> path. codegenSafe
4402
4376
  // guarantees no patternProperties or composition here, so "additional" is
4403
4377
  // simply "not declared".
4404
- if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) {
4405
- const ki = ctx.varCounter++
4406
- const known = Object.keys(schema.properties || {})
4407
- const guard = known.length ? `const _ak${ki}=new Set([${known.map((k) => JSON.stringify(k)).join(',')}]);` : ''
4408
- // A key matched by any patternProperties entry is not additional.
4409
- const patTests = []
4410
- for (const pat of Object.keys(schema.patternProperties || {})) {
4411
- const pattern = JSON.stringify(pat)
4412
- if (!ctx.regExpMap.has(pattern)) {
4413
- const ri = ctx.varCounter++
4414
- ctx.regExpMap.set(pattern, ri)
4415
- if (patternIsSafe(pat)) {
4416
- ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
4417
- ctx.usesSafeRe = true
4418
- } else {
4419
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
4420
- }
4421
- }
4422
- patTests.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ki})`)
4423
- }
4424
- const conds = []
4425
- if (known.length) conds.push(`!_ak${ki}.has(_k${ki})`)
4426
- if (patTests.length) conds.push(`!(${patTests.join('||')})`)
4427
- const keep = conds.length ? `if(${conds.join('&&')}){` : `{`
4428
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${guard}for(const _k${ki} in ${v}){${keep}`)
4429
- const _peFn = emitPtrEsc(ctx)
4430
- const p = pathExpr ? `${pathExpr}+'/'+${_peFn}(_k${ki})` : `'/'+${_peFn}(_k${ki})`
4431
- genCodeE(schema.additionalProperties, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/additionalProperties')
4432
- lines.push(`}}}`)
4433
- }
4378
+ if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) genAdditionalSchemaE(schema, v, pathExpr, lines, ctx, schemaPrefix)
4434
4379
 
4435
4380
  // dependentSchemas
4436
- if (schema.dependentSchemas) {
4437
- for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
4438
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
4439
- genCodeE(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
4440
- lines.push(`}`)
4441
- }
4442
- }
4381
+ if (schema.dependentSchemas) genDependentSchemasE(schema, v, pathExpr, lines, ctx, schemaPrefix)
4443
4382
 
4444
4383
  // propertyNames: false reports every key, at the key's own path, the way the
4445
4384
  // interpreter does (plan-compiler: instancePath + '/' + key).
4446
- if (schema.propertyNames === false) {
4447
- const ki = ctx.varCounter++
4448
- // The subschema is applied to the property NAME, so the failure belongs to
4449
- // the object that carries the name, not to `/<key>`, which is where the
4450
- // value lives. Same placement as the object-valued propertyNames branch.
4451
- const p = pathExpr || '""'
4452
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){_e.push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'});if(!_all)return{valid:false,errors:_e}}}`)
4453
- }
4385
+ if (schema.propertyNames === false) genPropertyNamesFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix)
4454
4386
 
4455
4387
  // propertyNames
4456
- if (schema.propertyNames && typeof schema.propertyNames === 'object') {
4457
- const pn = schema.propertyNames
4458
- const ki = ctx.varCounter++
4459
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
4460
- if (pn.minLength !== undefined) {
4461
- lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
4388
+ if (schema.propertyNames && typeof schema.propertyNames === 'object') genPropertyNamesE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
4389
+
4390
+ // items — starts after prefixItems (Draft 2020-12 semantics)
4391
+ if (schema.items !== undefined && schema.items !== true) {
4392
+ const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
4393
+ const idx = `_j${ctx.varCounter}`
4394
+ const elem = `_ei${ctx.varCounter}`
4395
+ ctx.varCounter++
4396
+ const childPath = childPathDynExpr(pathExpr, idx)
4397
+ lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
4398
+ genCodeE(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items')
4399
+ lines.push(`}}`)
4400
+ }
4401
+
4402
+ // prefixItems
4403
+ if (schema.prefixItems) genPrefixItemsE(schema, v, pathExpr, lines, ctx, schemaPrefix)
4404
+
4405
+ // contains
4406
+ if (schema.contains !== undefined) genContainsE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
4407
+
4408
+ // allOf
4409
+ if (schema.allOf) {
4410
+ for (let _ai = 0; _ai < schema.allOf.length; _ai++) {
4411
+ genCodeE(schema.allOf[_ai], v, pathExpr, lines, ctx, schemaPrefix+'/allOf/'+_ai)
4462
4412
  }
4463
- if (pn.maxLength !== undefined) {
4464
- lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
4413
+ }
4414
+
4415
+ // anyOf - collapse to single best-branch error via __ataCollapse
4416
+ if (schema.anyOf) genAnyOfE(schema, v, pathExpr, lines, ctx, schemaPrefix)
4417
+
4418
+ // oneOf - collapse to single best-branch error via __ataCollapse
4419
+ if (schema.oneOf) genOneOfE(schema, v, pathExpr, lines, ctx, schemaPrefix)
4420
+
4421
+ // not
4422
+ if (schema.not !== undefined) genNotE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
4423
+
4424
+ // if/then/else
4425
+ if (schema.if !== undefined) genIfE(schema, v, pathExpr, lines, ctx, schemaPrefix)
4426
+ }
4427
+
4428
+ // Rarely used keyword families of genCodeENode, kept out of it so their compile is
4429
+ // paid only by schemas that use them.
4430
+
4431
+ function genDynamicRefE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4432
+ const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
4433
+ if (ctx.anchors && ctx.anchors[anchorKey]) {
4434
+ const target = ctx.anchors[anchorKey]
4435
+ if (target === ctx.rootSchema) {
4436
+ // Self-recursive: generate _validateE call
4437
+ ctx.usesRecursion = true
4438
+ lines.push(`_validateE(${v},_all,_e)`)
4439
+ } else {
4440
+ const refKey = '$dynamicRef:' + anchorKey
4441
+ if (!ctx.refStack.has(refKey)) {
4442
+ ctx.refStack.add(refKey)
4443
+ genCodeE(target, v, pathExpr, lines, ctx, schemaPrefix)
4444
+ ctx.refStack.delete(refKey)
4445
+ }
4465
4446
  }
4466
- if (pn.pattern) {
4467
- const pattern = JSON.stringify(pn.pattern);
4468
- if (!ctx.regExpMap.has(pattern)) {
4469
- const ri = ctx.varCounter++
4470
- ctx.regExpMap.set(pattern, ri)
4471
- if (patternIsSafe(pn.pattern)) {
4472
- ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
4473
- ctx.usesSafeRe = true
4474
- } else {
4475
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
4476
- }
4447
+ }
4448
+ }
4449
+
4450
+ function genUniqueItemsE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr) {
4451
+ const si = ctx.varCounter++
4452
+ const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
4453
+ const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
4454
+ const maxItems = schema.maxItems
4455
+ const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
4456
+ let inner
4457
+ if (isPrim && maxItems && maxItems <= 16) {
4458
+ inner = `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k]){${failExpr('_k', '_i')};break}}}`
4459
+ } else if (isPrim) {
4460
+ inner = `const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _prev=_s${si}.get(${v}[_i]);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(${v}[_i],_i)}`
4461
+ } else {
4462
+ 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 Map();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);const _prev=_s${si}.get(_k);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(_k,_i)}`
4463
+ }
4464
+ lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
4465
+ }
4466
+
4467
+ function genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj) {
4468
+ const allowedU = Object.keys(schema.properties || {}).map(k => `${JSON.stringify(k)}`).join(',')
4469
+ const ui = ctx.varCounter++
4470
+ const upSp = `${schemaPrefix}/unevaluatedProperties`
4471
+ const upLit = buildErrorLiteral({ keyword: 'unevaluatedProperties', schemaPath: upSp, sourceMap: ctx.sourceMap })
4472
+ const patChecksU = []
4473
+ for (const pat of Object.keys(schema.patternProperties || {})) {
4474
+ const pattern = JSON.stringify(pat)
4475
+ if (!ctx.regExpMap.has(pattern)) {
4476
+ const ri = ctx.varCounter++
4477
+ ctx.regExpMap.set(pattern, ri)
4478
+ if (patternIsSafe(pat)) {
4479
+ ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
4480
+ ctx.usesSafeRe = true
4481
+ } else {
4482
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
4477
4483
  }
4478
- const ri = ctx.regExpMap.get(pattern);
4479
- lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
4480
- }
4481
- if (pn.const !== undefined) {
4482
- lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
4483
- }
4484
- if (pn.enum) {
4485
- const ei = ctx.varCounter++
4486
- ctx.helperCode.push(`const _es${ei}=new Set(${JSON.stringify(pn.enum)})`)
4487
- lines.push(`if(!_es${ei}.has(_k${ki})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
4488
4484
  }
4489
- lines.push(`}}`)
4485
+ patChecksU.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ui}[_i])`)
4490
4486
  }
4487
+ const isUneval = patChecksU.length ? `!_a${ui}.has(_k${ui}[_i])&&!(${patChecksU.join('||')})` : `!_a${ui}.has(_k${ui}[_i])`
4488
+ const innerU = `const _k${ui}=Object.keys(${v});const _a${ui}=new Set([${allowedU}]);for(let _i=0;_i<_k${ui}.length;_i++){if(${isUneval}){_e.push({code:'${upLit.codeStr}',keyword:'unevaluatedProperties',instancePath:${pathExpr||'""'},schemaPath:'${upSp}'${ordinalField(ctx, `${upSp}`)},params:{unevaluatedProperty:_k${ui}[_i]},message:'must NOT have unevaluated properties',docUrl:'${upLit.docUrl}'${upLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
4489
+ lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
4490
+ }
4491
4491
 
4492
- // items — starts after prefixItems (Draft 2020-12 semantics)
4493
- if (schema.items !== undefined && schema.items !== true) {
4494
- const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
4495
- const idx = `_j${ctx.varCounter}`
4496
- const elem = `_ei${ctx.varCounter}`
4497
- ctx.varCounter++
4498
- const childPath = childPathDynExpr(pathExpr, idx)
4499
- lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
4500
- genCodeE(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items')
4501
- lines.push(`}}`)
4492
+ function genUnevaluatedItemsFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4493
+ const itemsIsPrefix = Array.isArray(schema.items)
4494
+ const allEval = schema.items !== undefined && !itemsIsPrefix
4495
+ if (!allEval) {
4496
+ const plen = itemsIsPrefix ? schema.items.length : (Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0)
4497
+ const uiSp = `${schemaPrefix}/unevaluatedItems`
4498
+ const uiLit = buildErrorLiteral({ keyword: 'unevaluatedItems', schemaPath: uiSp, sourceMap: ctx.sourceMap })
4499
+ lines.push(`if(Array.isArray(${v})&&${v}.length>${plen}){_e.push({code:'${uiLit.codeStr}',keyword:'unevaluatedItems',instancePath:${pathExpr||'""'},schemaPath:'${uiSp}'${ordinalField(ctx, `${uiSp}`)},params:{limit:${plen}},message:'must NOT have more than ${plen} items',docUrl:'${uiLit.docUrl}'${uiLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
4502
4500
  }
4501
+ }
4503
4502
 
4504
- // prefixItems
4505
- if (schema.prefixItems) {
4506
- for (let i = 0; i < schema.prefixItems.length; i++) {
4507
- const childPath = childPathExpr(pathExpr, String(i))
4508
- lines.push(`if(Array.isArray(${v})&&${v}.length>${i}){`)
4509
- genCodeE(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx, schemaPrefix+'/prefixItems/'+i)
4510
- lines.push(`}`)
4503
+ function genPatternPropertiesE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4504
+ for (const [pat, sub] of Object.entries(schema.patternProperties)) {
4505
+ const pattern = JSON.stringify(pat);
4506
+ if (!ctx.regExpMap.has(pattern)) {
4507
+ const ri = ctx.varCounter++
4508
+ ctx.regExpMap.set(pattern, ri)
4509
+ if (patternIsSafe(pat)) {
4510
+ ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
4511
+ ctx.usesSafeRe = true
4512
+ } else {
4513
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
4514
+ }
4511
4515
  }
4516
+ const ri = ctx.regExpMap.get(pattern);
4517
+ const ki = ctx.varCounter++
4518
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){if(_re${ri}.test(_k${ki})){`)
4519
+ const _peFn = emitPtrEsc(ctx)
4520
+ const p = pathExpr ? `${pathExpr}+'/'+${_peFn}(_k${ki})` : `'/'+${_peFn}(_k${ki})`
4521
+ // The real pointer, so source maps can locate the failing keyword.
4522
+ genCodeE(sub, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/patternProperties/' + pat.replace(/~/g, '~0').replace(/\//g, '~1'))
4523
+ lines.push(`}}}`)
4512
4524
  }
4525
+ }
4513
4526
 
4514
- // contains
4515
- if (schema.contains !== undefined) {
4516
- const ci = ctx.varCounter++
4517
- const subLines = []
4518
- nestedGenCode(schema.contains, `_cv`, subLines, ctx)
4519
- const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
4520
- const minC = schema.minContains !== undefined ? schema.minContains : 1
4521
- const maxC = schema.maxContains
4522
- lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
4523
- lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{minContains:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
4524
- if (maxC !== undefined) {
4525
- lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{minContains:${minC},maxContains:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
4527
+ function genAdditionalSchemaE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4528
+ const ki = ctx.varCounter++
4529
+ const known = Object.keys(schema.properties || {})
4530
+ const guard = known.length ? `const _ak${ki}=new Set([${known.map((k) => JSON.stringify(k)).join(',')}]);` : ''
4531
+ // A key matched by any patternProperties entry is not additional.
4532
+ const patTests = []
4533
+ for (const pat of Object.keys(schema.patternProperties || {})) {
4534
+ const pattern = JSON.stringify(pat)
4535
+ if (!ctx.regExpMap.has(pattern)) {
4536
+ const ri = ctx.varCounter++
4537
+ ctx.regExpMap.set(pattern, ri)
4538
+ if (patternIsSafe(pat)) {
4539
+ ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
4540
+ ctx.usesSafeRe = true
4541
+ } else {
4542
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
4543
+ }
4526
4544
  }
4545
+ patTests.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ki})`)
4546
+ }
4547
+ const conds = []
4548
+ if (known.length) conds.push(`!_ak${ki}.has(_k${ki})`)
4549
+ if (patTests.length) conds.push(`!(${patTests.join('||')})`)
4550
+ const keep = conds.length ? `if(${conds.join('&&')}){` : `{`
4551
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${guard}for(const _k${ki} in ${v}){${keep}`)
4552
+ const _peFn = emitPtrEsc(ctx)
4553
+ const p = pathExpr ? `${pathExpr}+'/'+${_peFn}(_k${ki})` : `'/'+${_peFn}(_k${ki})`
4554
+ genCodeE(schema.additionalProperties, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/additionalProperties')
4555
+ lines.push(`}}}`)
4556
+ }
4557
+
4558
+ function genDependentSchemasE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4559
+ for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
4560
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
4561
+ genCodeE(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
4527
4562
  lines.push(`}`)
4528
4563
  }
4564
+ }
4529
4565
 
4530
- // allOf
4531
- if (schema.allOf) {
4532
- for (let _ai = 0; _ai < schema.allOf.length; _ai++) {
4533
- genCodeE(schema.allOf[_ai], v, pathExpr, lines, ctx, schemaPrefix+'/allOf/'+_ai)
4566
+ function genPropertyNamesFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4567
+ const ki = ctx.varCounter++
4568
+ // The subschema is applied to the property NAME, so the failure belongs to
4569
+ // the object that carries the name, not to `/<key>`, which is where the
4570
+ // value lives. Same placement as the object-valued propertyNames branch.
4571
+ const p = pathExpr || '""'
4572
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){_e.push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'});if(!_all)return{valid:false,errors:_e}}}`)
4573
+ }
4574
+
4575
+ function genPropertyNamesE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
4576
+ const pn = schema.propertyNames
4577
+ const ki = ctx.varCounter++
4578
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
4579
+ if (pn.minLength !== undefined) {
4580
+ lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
4581
+ }
4582
+ if (pn.maxLength !== undefined) {
4583
+ lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
4584
+ }
4585
+ if (pn.pattern) {
4586
+ const pattern = JSON.stringify(pn.pattern);
4587
+ if (!ctx.regExpMap.has(pattern)) {
4588
+ const ri = ctx.varCounter++
4589
+ ctx.regExpMap.set(pattern, ri)
4590
+ if (patternIsSafe(pn.pattern)) {
4591
+ ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
4592
+ ctx.usesSafeRe = true
4593
+ } else {
4594
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
4595
+ }
4534
4596
  }
4597
+ const ri = ctx.regExpMap.get(pattern);
4598
+ lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
4535
4599
  }
4536
-
4537
- // anyOf - collapse to single best-branch error via __ataCollapse
4538
- if (schema.anyOf) {
4539
- emitBranchCollapse(schema.anyOf, 'anyOf', v, pathExpr, lines, ctx, schemaPrefix)
4600
+ if (pn.const !== undefined) {
4601
+ lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
4540
4602
  }
4541
-
4542
- // oneOf - collapse to single best-branch error via __ataCollapse
4543
- if (schema.oneOf) {
4544
- emitBranchCollapse(schema.oneOf, 'oneOf', v, pathExpr, lines, ctx, schemaPrefix)
4603
+ if (pn.enum) {
4604
+ const ei = ctx.varCounter++
4605
+ ctx.helperCode.push(`const _es${ei}=new Set(${JSON.stringify(pn.enum)})`)
4606
+ lines.push(`if(!_es${ei}.has(_k${ki})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
4545
4607
  }
4608
+ lines.push(`}}`)
4609
+ }
4546
4610
 
4547
- // not
4548
- if (schema.not !== undefined) {
4549
- const subLines = []
4550
- nestedGenCode(schema.not, '_nv', subLines, ctx)
4551
- const nfn = subLines.length === 0 ? `function(_nv){return true}` : `function(_nv){${subLines.join(';')};return true}`
4552
- const fi = ctx.varCounter++
4553
- lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not', 'not', '{}', "'must NOT be valid'")}}}`)
4611
+ function genPrefixItemsE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4612
+ for (let i = 0; i < schema.prefixItems.length; i++) {
4613
+ const childPath = childPathExpr(pathExpr, String(i))
4614
+ lines.push(`if(Array.isArray(${v})&&${v}.length>${i}){`)
4615
+ genCodeE(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx, schemaPrefix+'/prefixItems/'+i)
4616
+ lines.push(`}`)
4554
4617
  }
4618
+ }
4555
4619
 
4556
- // if/then/else
4557
- if (schema.if !== undefined) {
4558
- const ifLines = []
4559
- nestedGenCode(schema.if, '_iv', ifLines, ctx)
4560
- const fi = ctx.varCounter++
4561
- const ifFn = ifLines.length === 0
4562
- ? `function(_iv){return true}`
4563
- : `function(_iv){${ifLines.join(';')};return true}`
4564
- lines.push(`{const _if${fi}=${ifFn}`)
4565
- if (schema.then !== undefined) {
4566
- lines.push(`if(_if${fi}(${v})){`)
4567
- genCodeE(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then')
4568
- lines.push(`}`)
4569
- }
4570
- if (schema.else !== undefined) {
4571
- lines.push(`${schema.then !== undefined ? 'else' : `if(!_if${fi}(${v}))`}{`)
4572
- genCodeE(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else')
4573
- lines.push(`}`)
4574
- }
4620
+ function genContainsE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
4621
+ const ci = ctx.varCounter++
4622
+ const subLines = []
4623
+ nestedGenCode(schema.contains, `_cv`, subLines, ctx)
4624
+ const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
4625
+ const minC = schema.minContains !== undefined ? schema.minContains : 1
4626
+ const maxC = schema.maxContains
4627
+ lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
4628
+ lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{minContains:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
4629
+ if (maxC !== undefined) {
4630
+ lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{minContains:${minC},maxContains:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
4631
+ }
4632
+ lines.push(`}`)
4633
+ }
4634
+
4635
+ function genAnyOfE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4636
+ emitBranchCollapse(schema.anyOf, 'anyOf', v, pathExpr, lines, ctx, schemaPrefix)
4637
+ }
4638
+
4639
+ function genOneOfE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4640
+ emitBranchCollapse(schema.oneOf, 'oneOf', v, pathExpr, lines, ctx, schemaPrefix)
4641
+ }
4642
+
4643
+ function genNotE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
4644
+ const subLines = []
4645
+ nestedGenCode(schema.not, '_nv', subLines, ctx)
4646
+ const nfn = subLines.length === 0 ? `function(_nv){return true}` : `function(_nv){${subLines.join(';')};return true}`
4647
+ const fi = ctx.varCounter++
4648
+ lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not', 'not', '{}', "'must NOT be valid'")}}}`)
4649
+ }
4650
+
4651
+ function genIfE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4652
+ const ifLines = []
4653
+ nestedGenCode(schema.if, '_iv', ifLines, ctx)
4654
+ const fi = ctx.varCounter++
4655
+ const ifFn = ifLines.length === 0
4656
+ ? `function(_iv){return true}`
4657
+ : `function(_iv){${ifLines.join(';')};return true}`
4658
+ lines.push(`{const _if${fi}=${ifFn}`)
4659
+ if (schema.then !== undefined) {
4660
+ lines.push(`if(_if${fi}(${v})){`)
4661
+ genCodeE(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then')
4662
+ lines.push(`}`)
4663
+ }
4664
+ if (schema.else !== undefined) {
4665
+ lines.push(`${schema.then !== undefined ? 'else' : `if(!_if${fi}(${v}))`}{`)
4666
+ genCodeE(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else')
4575
4667
  lines.push(`}`)
4576
4668
  }
4669
+ lines.push(`}`)
4577
4670
  }
4578
4671
 
4579
4672
  // --- Combined validator: single pass, validates + collects errors ---
@@ -4759,23 +4852,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4759
4852
  }
4760
4853
 
4761
4854
  // $dynamicRef — resolve via anchors map
4762
- if (schema.$dynamicRef) {
4763
- const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
4764
- if (ctx.anchors && ctx.anchors[anchorKey]) {
4765
- const target = ctx.anchors[anchorKey]
4766
- // This generator has no named functions, so it cannot follow a reference
4767
- // back to the root or around a cycle. It used to emit nothing in both
4768
- // cases, which the function then answered as valid: a document the other
4769
- // generators reject passed through validateJSON. It declines instead,
4770
- // and the schema takes the error generator.
4771
- if (target === ctx.rootSchema) throw DECLINE
4772
- const refKey = '$dynamicRef:' + anchorKey
4773
- if (ctx.refStack.has(refKey)) throw DECLINE
4774
- ctx.refStack.add(refKey)
4775
- genCodeC(target, v, pathExpr, lines, ctx, schemaPrefix)
4776
- ctx.refStack.delete(refKey)
4777
- }
4778
- }
4855
+ if (schema.$dynamicRef) genDynamicRefC(schema, v, pathExpr, lines, ctx, schemaPrefix)
4779
4856
 
4780
4857
  const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
4781
4858
  let isObj = false, isArr = false, isStr = false, isNum = false
@@ -4977,23 +5054,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4977
5054
  if (schema.maxItems !== undefined) { const c = isArr ? `${v}.length>${schema.maxItems}` : `Array.isArray(${v})&&${v}.length>${schema.maxItems}`; lines.push(`if(${c}){${fail('maxItems', 'maxItems', `{limit:${schema.maxItems}}`, `'must NOT have more than ${schema.maxItems} items'`)}}`) }
4978
5055
 
4979
5056
  // uniqueItems — tiered: small primitive arrays use nested loop (no allocation)
4980
- if (schema.uniqueItems) {
4981
- const si = ctx.varCounter++
4982
- const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
4983
- const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
4984
- const maxItems = schema.maxItems
4985
- const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
4986
- let inner
4987
- if (isPrim && maxItems && maxItems <= 16) {
4988
- // Small primitive arrays: O(n²) nested loop, zero allocation
4989
- inner = `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k]){${failExpr('_k', '_i')};break}}}`
4990
- } else if (isPrim) {
4991
- inner = `const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _prev=_s${si}.get(${v}[_i]);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(${v}[_i],_i)}`
4992
- } else {
4993
- 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 Map();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);const _prev=_s${si}.get(_k);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(_k,_i)}`
4994
- }
4995
- lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
4996
- }
5057
+ if (schema.uniqueItems) genUniqueItemsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr)
4997
5058
 
4998
5059
  // object size
4999
5060
  if (schema.minProperties !== undefined) lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length<${schema.minProperties}){${fail('minProperties', 'minProperties', `{limit:${schema.minProperties}}`, `'must NOT have fewer than ${schema.minProperties} properties'`)}}`)
@@ -5046,35 +5107,12 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5046
5107
  // unevaluatedProperties: false where it is provably additionalProperties
5047
5108
  // (the entry bail guaranteed that), keeping the keyword's own identity so
5048
5109
  // the two engines report the same error.
5049
- if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined && !schema.patternProperties) {
5050
- const propKeysU = Object.keys(schema.properties || {})
5051
- const ui = ctx.varCounter++
5052
- const failU = fail('unevaluatedProperties', 'unevaluatedProperties', `{unevaluatedProperty:_k${ui}[_i]}`, "'must NOT have unevaluated properties'")
5053
- let innerU
5054
- if (propKeysU.length === 0) {
5055
- innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++){${failU}}`
5056
- } else if (propKeysU.length <= 8) {
5057
- const checksU = propKeysU.map(k => `_k${ui}[_i]!==${JSON.stringify(k)}`).join('&&')
5058
- innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++)if(${checksU}){${failU}}`
5059
- } else {
5060
- const allowedU = propKeysU.map(k => JSON.stringify(k)).join(',')
5061
- 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}}`
5062
- }
5063
- lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
5064
- }
5110
+ if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined && !schema.patternProperties) genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)
5065
5111
 
5066
5112
  // unevaluatedItems: false under the same rule: one error for the array,
5067
5113
  // the first uncovered position as the limit. Adjacent schema-form `items`
5068
5114
  // evaluates everything; the draft-07 array form acts as the prefix.
5069
- if (schema.unevaluatedItems === false) {
5070
- const itemsIsPrefixC = Array.isArray(schema.items)
5071
- const allEvalC = schema.items !== undefined && !itemsIsPrefixC
5072
- if (!allEvalC) {
5073
- const plenC = itemsIsPrefixC ? schema.items.length : (Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0)
5074
- const failI = fail('unevaluatedItems', 'unevaluatedItems', `{limit:${plenC}}`, `'must NOT have more than ${plenC} items'`)
5075
- lines.push(`if(Array.isArray(${v})&&${v}.length>${plenC}){${failI}}`)
5076
- }
5077
- }
5115
+ if (schema.unevaluatedItems === false) genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
5078
5116
 
5079
5117
  // dependentRequired
5080
5118
  if (schema.dependentRequired) {
@@ -5120,153 +5158,16 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5120
5158
 
5121
5159
  // patternProperties — same optimizations as genCode: charCodeAt + inline key comparison + merged propertyNames
5122
5160
  if (schema.patternProperties) {
5123
- const ppEntries = Object.entries(schema.patternProperties)
5124
- const pn = schema.propertyNames && typeof schema.propertyNames === 'object' ? schema.propertyNames : null
5125
- const pi = ctx.varCounter++
5126
-
5127
- // Build pattern matchers: prefer charCodeAt for simple prefixes
5128
- const matchers = []
5129
- for (const [pat] of ppEntries) {
5130
- const kVar = `_k${pi}`
5131
- const fast = fastPrefixCheck(pat, kVar)
5132
- if (fast) {
5133
- matchers.push({ check: fast })
5134
- } else {
5135
- const ri = ctx.varCounter++
5136
- ctx.closureVars.push(`_re${ri}`)
5137
- ctx.closureVals.push(safeReClosure(ctx, pat))
5138
- matchers.push({ check: `_re${ri}.test(_k${pi})` })
5139
- }
5140
- }
5141
-
5142
- const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
5143
- const kVar = `_k${pi}`
5144
-
5145
- if (schema.additionalProperties === false && schema.properties) {
5146
- ppHandledPropertyNames = !!pn
5147
- const propKeys = Object.keys(schema.properties)
5148
- // Inline key comparison for small property sets
5149
- const keyCheck = propKeys.length <= 8
5150
- ? propKeys.map(k => `${kVar}===${JSON.stringify(k)}`).join('||')
5151
- : null
5152
- if (!keyCheck) {
5153
- const allowedSet = `_as${pi}`
5154
- ctx.closureVars.push(allowedSet)
5155
- ctx.closureVals.push(new Set(propKeys))
5156
- }
5157
-
5158
- lines.push(`${guard}{for(const ${kVar} in ${v}){`)
5159
- // propertyNames checks (merged)
5160
- if (pn) {
5161
- if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
5162
- if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
5163
- if (pn.pattern) {
5164
- const fast = fastPrefixCheck(pn.pattern, kVar)
5165
- if (fast) {
5166
- lines.push(`if(!(${fast})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
5167
- } else {
5168
- const ri = ctx.varCounter++
5169
- ctx.closureVars.push(`_re${ri}`)
5170
- ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
5171
- lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
5172
- }
5173
- }
5174
- if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
5175
- if (pn.enum) {
5176
- const ei = ctx.varCounter++
5177
- ctx.closureVars.push(`_es${ei}`)
5178
- ctx.closureVals.push(new Set(pn.enum))
5179
- lines.push(`if(!_es${ei}.has(${kVar})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
5180
- }
5181
- }
5182
- const matchExpr = keyCheck || `_as${pi}.has(${kVar})`
5183
- lines.push(`let _m${pi}=${matchExpr}`)
5184
- for (let i = 0; i < ppEntries.length; i++) {
5185
- // The subschema is generated in place so its own keywords report
5186
- // the real instance path and schema pointer.
5187
- lines.push(`if(${matchers[i].check}){_m${pi}=true;{const _ppv${pi}_${i}=${v}[${kVar}]`)
5188
- genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, `${emitPtrEsc(ctx)}(${kVar})`), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
5189
- lines.push(`}}`)
5190
- }
5191
- lines.push(`if(!_m${pi}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:${kVar}}`, "'must NOT have additional properties'")}}`)
5192
- lines.push(`}}`)
5193
- } else {
5194
- ppHandledPropertyNames = !!pn
5195
- lines.push(`${guard}{for(const ${kVar} in ${v}){`)
5196
- if (pn) {
5197
- if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
5198
- if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
5199
- if (pn.pattern) {
5200
- const fast = fastPrefixCheck(pn.pattern, kVar)
5201
- if (fast) {
5202
- lines.push(`if(!(${fast})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
5203
- } else {
5204
- const ri = ctx.varCounter++
5205
- ctx.closureVars.push(`_re${ri}`)
5206
- ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
5207
- lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
5208
- }
5209
- }
5210
- if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
5211
- if (pn.enum) {
5212
- const ei = ctx.varCounter++
5213
- ctx.closureVars.push(`_es${ei}`)
5214
- ctx.closureVals.push(new Set(pn.enum))
5215
- lines.push(`if(!_es${ei}.has(${kVar})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
5216
- }
5217
- }
5218
- for (let i = 0; i < ppEntries.length; i++) {
5219
- lines.push(`if(${matchers[i].check}){const _ppv${pi}_${i}=${v}[${kVar}]`)
5220
- genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, `${emitPtrEsc(ctx)}(${kVar})`), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
5221
- lines.push(`}`)
5222
- }
5223
- lines.push(`}}`)
5224
- }
5161
+ if (genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)) ppHandledPropertyNames = true
5225
5162
  }
5226
5163
 
5227
5164
  // dependentSchemas
5228
- if (schema.dependentSchemas) {
5229
- for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
5230
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
5231
- genCodeC(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
5232
- lines.push(`}`)
5233
- }
5234
- }
5165
+ if (schema.dependentSchemas) genDependentSchemasC(schema, v, pathExpr, lines, ctx, schemaPrefix)
5235
5166
 
5236
- if (schema.propertyNames === false) {
5237
- const ki = ctx.varCounter++
5238
- const p = pathExpr || '""'
5239
- lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){(_e||(_e=[])).push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'})}}`)
5240
- }
5167
+ if (schema.propertyNames === false) genPropertyNamesFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix)
5241
5168
 
5242
5169
  // propertyNames — skip if already merged into patternProperties loop
5243
- if (schema.propertyNames && typeof schema.propertyNames === 'object' && !ppHandledPropertyNames) {
5244
- const pn = schema.propertyNames
5245
- const ki = ctx.varCounter++
5246
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
5247
- if (pn.minLength !== undefined) {
5248
- lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
5249
- }
5250
- if (pn.maxLength !== undefined) {
5251
- lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
5252
- }
5253
- if (pn.pattern) {
5254
- const ri = ctx.varCounter++
5255
- ctx.closureVars.push(`_re${ri}`)
5256
- ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
5257
- lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
5258
- }
5259
- if (pn.const !== undefined) {
5260
- lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
5261
- }
5262
- if (pn.enum) {
5263
- const ei = ctx.varCounter++
5264
- ctx.closureVars.push(`_es${ei}`)
5265
- ctx.closureVals.push(new Set(pn.enum))
5266
- lines.push(`if(!_es${ei}.has(_k${ki})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
5267
- }
5268
- lines.push(`}}`)
5269
- }
5170
+ if (schema.propertyNames && typeof schema.propertyNames === 'object' && !ppHandledPropertyNames) genPropertyNamesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
5270
5171
 
5271
5172
  // items
5272
5173
  if (schema.items !== undefined && schema.items !== true) {
@@ -5280,64 +5181,25 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5280
5181
  }
5281
5182
 
5282
5183
  // prefixItems
5283
- if (schema.prefixItems) {
5284
- for (let i = 0; i < schema.prefixItems.length; i++) {
5285
- const childPath = childPathExpr(pathExpr, String(i))
5286
- lines.push(`if(Array.isArray(${v})&&${v}.length>${i}){`)
5287
- genCodeC(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx, schemaPrefix+'/prefixItems/'+i)
5288
- lines.push(`}`)
5289
- }
5290
- }
5184
+ if (schema.prefixItems) genPrefixItemsC(schema, v, pathExpr, lines, ctx, schemaPrefix)
5291
5185
 
5292
5186
  // contains
5293
- if (schema.contains !== undefined) {
5294
- const ci = ctx.varCounter++
5295
- const subLines = []
5296
- nestedGenCode(schema.contains, `_cv`, subLines, ctx)
5297
- const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
5298
- const minC = schema.minContains !== undefined ? schema.minContains : 1
5299
- const maxC = schema.maxContains
5300
- lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
5301
- lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{minContains:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
5302
- if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{minContains:${minC},maxContains:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
5303
- lines.push(`}`)
5304
- }
5187
+ if (schema.contains !== undefined) genContainsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
5305
5188
 
5306
5189
  // allOf
5307
5190
  if (schema.allOf) { for (let _ai = 0; _ai < schema.allOf.length; _ai++) genCodeC(schema.allOf[_ai], v, pathExpr, lines, ctx, schemaPrefix+'/allOf/'+_ai) }
5308
5191
 
5309
5192
  // anyOf
5310
- if (schema.anyOf) {
5311
- const fi = ctx.varCounter++
5312
- const fns = schema.anyOf.map(sub => { const sl = []; nestedGenCode(sub, '_av', sl, ctx); return sl.length === 0 ? `function(_av){return true}` : `function(_av){${sl.join(';')};return true}` })
5313
- lines.push(`{const _af${fi}=[${fns.join(',')}];let _am=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am=true;break}}if(!_am){${fail('anyOf', 'anyOf', '{}', "'must match a schema in anyOf'")}}}`)
5314
- }
5193
+ if (schema.anyOf) genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
5315
5194
 
5316
5195
  // oneOf
5317
- if (schema.oneOf) {
5318
- const fi = ctx.varCounter++
5319
- const fns = schema.oneOf.map(sub => { const sl = []; nestedGenCode(sub, '_ov', sl, ctx); return sl.length === 0 ? `function(_ov){return true}` : `function(_ov){${sl.join(';')};return true}` })
5320
- lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc++;if(_oc>1)break}if(_oc!==1){${fail('oneOf', 'oneOf', '{}', "'must match exactly one schema in oneOf'")}}}`)
5321
- }
5196
+ if (schema.oneOf) genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
5322
5197
 
5323
5198
  // not
5324
- if (schema.not !== undefined) {
5325
- const sl = []; nestedGenCode(schema.not, '_nv', sl, ctx)
5326
- const nfn = sl.length === 0 ? `function(_nv){return true}` : `function(_nv){${sl.join(';')};return true}`
5327
- const fi = ctx.varCounter++
5328
- lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not', 'not', '{}', "'must NOT be valid'")}}}`)
5329
- }
5199
+ if (schema.not !== undefined) genNotC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
5330
5200
 
5331
5201
  // if/then/else
5332
- if (schema.if !== undefined) {
5333
- const sl = []; nestedGenCode(schema.if, '_iv', sl, ctx)
5334
- const fi = ctx.varCounter++
5335
- const ifFn = sl.length === 0 ? `function(_iv){return true}` : `function(_iv){${sl.join(';')};return true}`
5336
- lines.push(`{const _if${fi}=${ifFn}`)
5337
- if (schema.then !== undefined) { lines.push(`if(_if${fi}(${v})){`); genCodeC(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then'); lines.push(`}`) }
5338
- if (schema.else !== undefined) { lines.push(`${schema.then !== undefined ? 'else' : `if(!_if${fi}(${v}))`}{`); genCodeC(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else'); lines.push(`}`) }
5339
- lines.push(`}`)
5340
- }
5202
+ if (schema.if !== undefined) genIfC(schema, v, pathExpr, lines, ctx, schemaPrefix)
5341
5203
 
5342
5204
  // Close type-success block if opened
5343
5205
  if (types) {
@@ -5345,6 +5207,272 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5345
5207
  }
5346
5208
  }
5347
5209
 
5210
+ // Rarely used keyword families of genCodeCNode, kept out of it so their compile is
5211
+ // paid only by schemas that use them.
5212
+
5213
+ function genDynamicRefC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5214
+ const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
5215
+ if (ctx.anchors && ctx.anchors[anchorKey]) {
5216
+ const target = ctx.anchors[anchorKey]
5217
+ // This generator has no named functions, so it cannot follow a reference
5218
+ // back to the root or around a cycle. It used to emit nothing in both
5219
+ // cases, which the function then answered as valid: a document the other
5220
+ // generators reject passed through validateJSON. It declines instead,
5221
+ // and the schema takes the error generator.
5222
+ if (target === ctx.rootSchema) throw DECLINE
5223
+ const refKey = '$dynamicRef:' + anchorKey
5224
+ if (ctx.refStack.has(refKey)) throw DECLINE
5225
+ ctx.refStack.add(refKey)
5226
+ genCodeC(target, v, pathExpr, lines, ctx, schemaPrefix)
5227
+ ctx.refStack.delete(refKey)
5228
+ }
5229
+ }
5230
+
5231
+ function genUniqueItemsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr) {
5232
+ const si = ctx.varCounter++
5233
+ const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
5234
+ const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
5235
+ const maxItems = schema.maxItems
5236
+ const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
5237
+ let inner
5238
+ if (isPrim && maxItems && maxItems <= 16) {
5239
+ // Small primitive arrays: O(n²) nested loop, zero allocation
5240
+ inner = `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k]){${failExpr('_k', '_i')};break}}}`
5241
+ } else if (isPrim) {
5242
+ inner = `const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _prev=_s${si}.get(${v}[_i]);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(${v}[_i],_i)}`
5243
+ } else {
5244
+ 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 Map();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);const _prev=_s${si}.get(_k);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(_k,_i)}`
5245
+ }
5246
+ lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
5247
+ }
5248
+
5249
+ function genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
5250
+ const propKeysU = Object.keys(schema.properties || {})
5251
+ const ui = ctx.varCounter++
5252
+ const failU = fail('unevaluatedProperties', 'unevaluatedProperties', `{unevaluatedProperty:_k${ui}[_i]}`, "'must NOT have unevaluated properties'")
5253
+ let innerU
5254
+ if (propKeysU.length === 0) {
5255
+ innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++){${failU}}`
5256
+ } else if (propKeysU.length <= 8) {
5257
+ const checksU = propKeysU.map(k => `_k${ui}[_i]!==${JSON.stringify(k)}`).join('&&')
5258
+ innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++)if(${checksU}){${failU}}`
5259
+ } else {
5260
+ const allowedU = propKeysU.map(k => JSON.stringify(k)).join(',')
5261
+ 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}}`
5262
+ }
5263
+ lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
5264
+ }
5265
+
5266
+ function genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
5267
+ const itemsIsPrefixC = Array.isArray(schema.items)
5268
+ const allEvalC = schema.items !== undefined && !itemsIsPrefixC
5269
+ if (!allEvalC) {
5270
+ const plenC = itemsIsPrefixC ? schema.items.length : (Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0)
5271
+ const failI = fail('unevaluatedItems', 'unevaluatedItems', `{limit:${plenC}}`, `'must NOT have more than ${plenC} items'`)
5272
+ lines.push(`if(Array.isArray(${v})&&${v}.length>${plenC}){${failI}}`)
5273
+ }
5274
+ }
5275
+
5276
+ function genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
5277
+ let handledPropertyNames = false
5278
+ const ppEntries = Object.entries(schema.patternProperties)
5279
+ const pn = schema.propertyNames && typeof schema.propertyNames === 'object' ? schema.propertyNames : null
5280
+ const pi = ctx.varCounter++
5281
+
5282
+ // Build pattern matchers: prefer charCodeAt for simple prefixes
5283
+ const matchers = []
5284
+ for (const [pat] of ppEntries) {
5285
+ const kVar = `_k${pi}`
5286
+ const fast = fastPrefixCheck(pat, kVar)
5287
+ if (fast) {
5288
+ matchers.push({ check: fast })
5289
+ } else {
5290
+ const ri = ctx.varCounter++
5291
+ ctx.closureVars.push(`_re${ri}`)
5292
+ ctx.closureVals.push(safeReClosure(ctx, pat))
5293
+ matchers.push({ check: `_re${ri}.test(_k${pi})` })
5294
+ }
5295
+ }
5296
+
5297
+ const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
5298
+ const kVar = `_k${pi}`
5299
+
5300
+ if (schema.additionalProperties === false && schema.properties) {
5301
+ handledPropertyNames = !!pn
5302
+ const propKeys = Object.keys(schema.properties)
5303
+ // Inline key comparison for small property sets
5304
+ const keyCheck = propKeys.length <= 8
5305
+ ? propKeys.map(k => `${kVar}===${JSON.stringify(k)}`).join('||')
5306
+ : null
5307
+ if (!keyCheck) {
5308
+ const allowedSet = `_as${pi}`
5309
+ ctx.closureVars.push(allowedSet)
5310
+ ctx.closureVals.push(new Set(propKeys))
5311
+ }
5312
+
5313
+ lines.push(`${guard}{for(const ${kVar} in ${v}){`)
5314
+ // propertyNames checks (merged)
5315
+ if (pn) {
5316
+ if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
5317
+ if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
5318
+ if (pn.pattern) {
5319
+ const fast = fastPrefixCheck(pn.pattern, kVar)
5320
+ if (fast) {
5321
+ lines.push(`if(!(${fast})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
5322
+ } else {
5323
+ const ri = ctx.varCounter++
5324
+ ctx.closureVars.push(`_re${ri}`)
5325
+ ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
5326
+ lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
5327
+ }
5328
+ }
5329
+ if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
5330
+ if (pn.enum) {
5331
+ const ei = ctx.varCounter++
5332
+ ctx.closureVars.push(`_es${ei}`)
5333
+ ctx.closureVals.push(new Set(pn.enum))
5334
+ lines.push(`if(!_es${ei}.has(${kVar})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
5335
+ }
5336
+ }
5337
+ const matchExpr = keyCheck || `_as${pi}.has(${kVar})`
5338
+ lines.push(`let _m${pi}=${matchExpr}`)
5339
+ for (let i = 0; i < ppEntries.length; i++) {
5340
+ // The subschema is generated in place so its own keywords report
5341
+ // the real instance path and schema pointer.
5342
+ lines.push(`if(${matchers[i].check}){_m${pi}=true;{const _ppv${pi}_${i}=${v}[${kVar}]`)
5343
+ genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, `${emitPtrEsc(ctx)}(${kVar})`), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
5344
+ lines.push(`}}`)
5345
+ }
5346
+ lines.push(`if(!_m${pi}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:${kVar}}`, "'must NOT have additional properties'")}}`)
5347
+ lines.push(`}}`)
5348
+ } else {
5349
+ handledPropertyNames = !!pn
5350
+ lines.push(`${guard}{for(const ${kVar} in ${v}){`)
5351
+ if (pn) {
5352
+ if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
5353
+ if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
5354
+ if (pn.pattern) {
5355
+ const fast = fastPrefixCheck(pn.pattern, kVar)
5356
+ if (fast) {
5357
+ lines.push(`if(!(${fast})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
5358
+ } else {
5359
+ const ri = ctx.varCounter++
5360
+ ctx.closureVars.push(`_re${ri}`)
5361
+ ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
5362
+ lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
5363
+ }
5364
+ }
5365
+ if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
5366
+ if (pn.enum) {
5367
+ const ei = ctx.varCounter++
5368
+ ctx.closureVars.push(`_es${ei}`)
5369
+ ctx.closureVals.push(new Set(pn.enum))
5370
+ lines.push(`if(!_es${ei}.has(${kVar})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
5371
+ }
5372
+ }
5373
+ for (let i = 0; i < ppEntries.length; i++) {
5374
+ lines.push(`if(${matchers[i].check}){const _ppv${pi}_${i}=${v}[${kVar}]`)
5375
+ genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, `${emitPtrEsc(ctx)}(${kVar})`), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
5376
+ lines.push(`}`)
5377
+ }
5378
+ lines.push(`}}`)
5379
+ }
5380
+ return handledPropertyNames
5381
+ }
5382
+
5383
+ function genDependentSchemasC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5384
+ for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
5385
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
5386
+ genCodeC(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
5387
+ lines.push(`}`)
5388
+ }
5389
+ }
5390
+
5391
+ function genPropertyNamesFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5392
+ const ki = ctx.varCounter++
5393
+ const p = pathExpr || '""'
5394
+ lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){(_e||(_e=[])).push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'})}}`)
5395
+ }
5396
+
5397
+ function genPropertyNamesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
5398
+ const pn = schema.propertyNames
5399
+ const ki = ctx.varCounter++
5400
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
5401
+ if (pn.minLength !== undefined) {
5402
+ lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
5403
+ }
5404
+ if (pn.maxLength !== undefined) {
5405
+ lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
5406
+ }
5407
+ if (pn.pattern) {
5408
+ const ri = ctx.varCounter++
5409
+ ctx.closureVars.push(`_re${ri}`)
5410
+ ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
5411
+ lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
5412
+ }
5413
+ if (pn.const !== undefined) {
5414
+ lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
5415
+ }
5416
+ if (pn.enum) {
5417
+ const ei = ctx.varCounter++
5418
+ ctx.closureVars.push(`_es${ei}`)
5419
+ ctx.closureVals.push(new Set(pn.enum))
5420
+ lines.push(`if(!_es${ei}.has(_k${ki})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
5421
+ }
5422
+ lines.push(`}}`)
5423
+ }
5424
+
5425
+ function genPrefixItemsC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5426
+ for (let i = 0; i < schema.prefixItems.length; i++) {
5427
+ const childPath = childPathExpr(pathExpr, String(i))
5428
+ lines.push(`if(Array.isArray(${v})&&${v}.length>${i}){`)
5429
+ genCodeC(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx, schemaPrefix+'/prefixItems/'+i)
5430
+ lines.push(`}`)
5431
+ }
5432
+ }
5433
+
5434
+ function genContainsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
5435
+ const ci = ctx.varCounter++
5436
+ const subLines = []
5437
+ nestedGenCode(schema.contains, `_cv`, subLines, ctx)
5438
+ const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
5439
+ const minC = schema.minContains !== undefined ? schema.minContains : 1
5440
+ const maxC = schema.maxContains
5441
+ lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
5442
+ lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{minContains:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
5443
+ if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{minContains:${minC},maxContains:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
5444
+ lines.push(`}`)
5445
+ }
5446
+
5447
+ function genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
5448
+ const fi = ctx.varCounter++
5449
+ const fns = schema.anyOf.map(sub => { const sl = []; nestedGenCode(sub, '_av', sl, ctx); return sl.length === 0 ? `function(_av){return true}` : `function(_av){${sl.join(';')};return true}` })
5450
+ lines.push(`{const _af${fi}=[${fns.join(',')}];let _am=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am=true;break}}if(!_am){${fail('anyOf', 'anyOf', '{}', "'must match a schema in anyOf'")}}}`)
5451
+ }
5452
+
5453
+ function genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
5454
+ const fi = ctx.varCounter++
5455
+ const fns = schema.oneOf.map(sub => { const sl = []; nestedGenCode(sub, '_ov', sl, ctx); return sl.length === 0 ? `function(_ov){return true}` : `function(_ov){${sl.join(';')};return true}` })
5456
+ lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc++;if(_oc>1)break}if(_oc!==1){${fail('oneOf', 'oneOf', '{}', "'must match exactly one schema in oneOf'")}}}`)
5457
+ }
5458
+
5459
+ function genNotC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
5460
+ const sl = []; nestedGenCode(schema.not, '_nv', sl, ctx)
5461
+ const nfn = sl.length === 0 ? `function(_nv){return true}` : `function(_nv){${sl.join(';')};return true}`
5462
+ const fi = ctx.varCounter++
5463
+ lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not', 'not', '{}', "'must NOT be valid'")}}}`)
5464
+ }
5465
+
5466
+ function genIfC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5467
+ const sl = []; nestedGenCode(schema.if, '_iv', sl, ctx)
5468
+ const fi = ctx.varCounter++
5469
+ const ifFn = sl.length === 0 ? `function(_iv){return true}` : `function(_iv){${sl.join(';')};return true}`
5470
+ lines.push(`{const _if${fi}=${ifFn}`)
5471
+ if (schema.then !== undefined) { lines.push(`if(_if${fi}(${v})){`); genCodeC(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then'); lines.push(`}`) }
5472
+ if (schema.else !== undefined) { lines.push(`${schema.then !== undefined ? 'else' : `if(!_if${fi}(${v}))`}{`); genCodeC(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else'); lines.push(`}`) }
5473
+ lines.push(`}`)
5474
+ }
5475
+
5348
5476
  // Collect statically-known evaluated properties/items from a schema.
5349
5477
  // Returns { props: string[], items: number|null, allProps: bool, allItems: bool, dynamic: bool }
5350
5478
  function collectEvaluated(schema, schemaMap, rootDefs) {