ata-validator 1.32.3 → 1.33.0

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