ata-validator 1.51.0 → 1.52.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.
package/README.md CHANGED
@@ -138,18 +138,19 @@ errors (7.6M with `richErrors: false`).
138
138
 
139
139
  | Dimension | Schema | ata-AOT | runtime validator | Difference |
140
140
  |---|---|---|---|---|
141
- | Bundle (gzipped) | simple | 1.4 KB | 52.7 KB | 38.4x smaller |
142
- | Bundle (gzipped) | complex | 3.0 KB | 52.7 KB | 17.5x smaller |
143
- | Bundle (gzipped) | nested | 4.5 KB | 52.7 KB | 11.6x smaller |
141
+ | Bundle (gzipped) | simple | 1.3 KB | 52.7 KB | 39.0x smaller |
142
+ | Bundle (gzipped) | complex | 3.0 KB | 52.7 KB | 17.7x smaller |
143
+ | Bundle (gzipped) | nested | 4.5 KB | 52.7 KB | 11.8x smaller |
144
144
  | Cold start | simple | 20 ms | 37 ms | 1.9x faster |
145
- | Throughput (1M ops) | simple | 289 Mops/s | 96 Mops/s | 3.0x faster |
146
- | Compile time | simple | 18 µs | 1.56 ms | 85x faster |
145
+ | Throughput (1M ops) | simple | 315 Mops/s | 99 Mops/s | 3.2x faster |
146
+ | Compile time | simple | 24 µs | 1.57 ms | 65x faster |
147
147
 
148
148
  The runtime column is the default validator most frameworks ship. Reproduce on your machine
149
- with `npm run bench:aot-vs-ajv`. Numbers from one run on Apple M4 Pro, Node 25.2.1, 2026-10-06,
150
- on ata-validator 1.46.0. Across four runs throughput moved between 288 and 329 Mops/s against
151
- 96 to 116, cold start between 19 and 20 ms against 37 to 39, and the compile ratio between 77x
152
- and 85x. The throughput row times a single one-million-call
149
+ with `npm run bench:aot-vs-ajv`. Numbers from one run on Apple M4 Pro, Node 25.2.1, 2026-10-11,
150
+ on ata-validator 1.51.0. Across five runs throughput moved between 263 and 325 Mops/s against
151
+ 97 to 110, cold start between 20 and 22 ms against 37 to 40, and the compile ratio between 61x
152
+ and 65x. The compile row is the time to write a module, a build-time cost; it was 18 µs on
153
+ 1.46.0, and modules have carried the one-pass error function since 1.47.0. The throughput row times a single one-million-call
153
154
  loop of a few milliseconds, so it moves the most from run to run; treat the last three rows as
154
155
  an order of magnitude rather than a constant.
155
156
 
@@ -279,13 +280,13 @@ is the most common way to get a misleading number out of this library.
279
280
 
280
281
  | | compiled with `ata build` | runtime `new Validator(schema)` |
281
282
  |---|---|---|
282
- | In a bundle, gzipped | **2.1 KB** | 126.1 KB |
283
+ | In a bundle, gzipped | **2.1 KB** | 123.9 KB |
283
284
  | Time to a served request | **3.5 ms** | 7.6 ms |
284
285
  | Schema known when | build time | any time |
285
286
 
286
287
  The bundle row is the ten-field user schema in
287
288
  `tests/fixtures/error-dx/user.schema.json`, every export of the compiled module against
288
- `new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.48.0.
289
+ `new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.51.0.
289
290
  The startup row is a Hono route on Bun 1.4, the median of three rounds of best-of-seven, from
290
291
  `benchmark/bundle`, against 3.5 ms for the same app doing no validation at all, so the
291
292
  compiled path costs nothing measurable to start. The runtime figure is what it is because
@@ -293,7 +294,7 @@ a schema that arrives at run time can use any keyword, so the whole engine has t
293
294
  there. The compiled module imports nothing and contains only the checks your schema asks
294
295
  for.
295
296
 
296
- **On a server, use whichever fits your schemas.** 113 KB of JavaScript on a Node or Bun
297
+ **On a server, use whichever fits your schemas.** 124 KB of JavaScript on a Node or Bun
297
298
  process is not a cost anyone notices, and the runtime API is the simpler thing to reach
298
299
  for. Speed is the same either way once warm.
299
300
 
@@ -535,7 +536,7 @@ const v = new Validator(schema, {
535
536
 
536
537
  ### Build-time compile (`ata compile`)
537
538
 
538
- The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. For the ten-field user schema in `tests/fixtures/error-dx/user.schema.json` the module is 2.1 KB gzipped, full error detail included, against 113.3 KB for the runtime bundled for the browser (ata 1.45.0).
539
+ The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. For the ten-field user schema in `tests/fixtures/error-dx/user.schema.json` the module is 2.1 KB gzipped, full error detail included, against 123.9 KB for the runtime bundled for the browser (ata 1.51.0).
539
540
 
540
541
  ```bash
541
542
  npx ata compile schemas/user.json -o src/generated/user.validator.mjs
@@ -581,20 +582,25 @@ error collector out of the modules: each one carries its verdict and its schema,
581
582
  its errors from one `ata-errors.mjs` the build writes beside them. The build does this only
582
583
  where it makes the output smaller, so a single small schema keeps its self-contained module.
583
584
  The errors are the ones `new Validator(schema, { richErrors: false })` reports, in the same
584
- order. Seven SchemaStore schemas imported into one app, minified with esbuild, go from 148.9 KB
585
- to 63.3 KB gzipped, and from 35.8 ms to 13.8 ms to import with code generation blocked. A
585
+ order. Forty SchemaStore schemas imported into one app, minified with esbuild, go from 513.8 KB
586
+ to 213.3 KB gzipped, and from 60.4 ms to 28.4 ms to import with code generation blocked. A
586
587
  rejected document costs more this way, since its errors come from an interpreted walk:
587
588
  `--errors inline` keeps every module self-contained. `--mode schema` writes each module as
588
589
  its schema and takes the verdict from the runtime too: forty SchemaStore schemas in one app
589
- come to 101.6 KB gzipped against 212.6 KB as compact modules, and load and check each once in
590
- 23.0 ms against 35.5, while a check once warm costs about five times what generated code costs.
590
+ come to 102.4 KB gzipped against 213.3 KB as compact modules, and load and check each once in
591
+ 23.9 ms against 34.5, while a check once warm costs about five times what generated code costs.
592
+ Schemas that reference each other by `$id` are built against each other, and in schema mode
593
+ they are written once to a shared registry that keeps only the definitions the modules reach;
594
+ `--base-uri <uri>` names files that declare no `$id`. Eight Kubernetes kinds, whose schemas
595
+ share one 1.1 MB definitions file, come to 32.6 KB gzipped as schema modules against 68.6 KB
596
+ self-contained, on ata 1.51.0.
591
597
 
592
598
  Bundle sizes for the 10-field user schema in `tests/fixtures/error-dx/user.schema.json`,
593
- minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.45.0:
599
+ minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.51.0:
594
600
 
595
601
  | What the app imports | Size | Notes |
596
602
  |---|---|---|
597
- | `Validator` from `ata-validator` | 113.3 KB | The compiler ships with it, because a runtime schema can use any keyword |
603
+ | `Validator` from `ata-validator` | 123.9 KB | The compiler ships with it, because a runtime schema can use any keyword |
598
604
  | `isValid` from the compiled module | **1.3 KB** | Nothing else is reachable, so the error collector is dropped |
599
605
  | `validate` from the compiled module | **1.9 KB** | Adds the detailed error collector |
600
606
 
@@ -632,7 +638,7 @@ through a `setFormats()` export the module carries. `docs/API.md` has the
632
638
  details.
633
639
 
634
640
  **Fastify startup, 10 route schemas, from a cold process to the first validated request:
635
- ajv 19.7 ms, ata 2.75 ms, no build step required.** ata registers in 0.25 ms of that and
641
+ ajv 19.0 ms, ata 2.81 ms, no build step required.** ata registers in 0.20 ms of that and
636
642
  compiles on the first request, so counting only registration would overstate the gap.
637
643
  Reproduce with `node benchmark/bench_fastify_boot.mjs`.
638
644
 
@@ -0,0 +1,15 @@
1
+ // ESM entry for bundlers that target a server (Bun, esbuild or Rollup for
2
+ // Node) and for Node before 22.10, which does not match `module-sync`: the
3
+ // validator core with the code generator registered, as index.js, but with
4
+ // each tool imported on its own (lib/tools.mjs), so the tools an application
5
+ // does not import stay out of its bundle. Through index.js a bundler sees one
6
+ // CommonJS module and keeps all of them.
7
+ import core from './lib/validator-core.js';
8
+ import engine from './lib/codegen-engine.js';
9
+ import { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } from './lib/tools.mjs';
10
+
11
+ core._registerCodegen(engine);
12
+
13
+ export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema } = core;
14
+ export { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
15
+ export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
package/lib/aot-impl.js CHANGED
@@ -251,7 +251,7 @@ function onePassCore(validator, opts) {
251
251
  const decls = [];
252
252
  const args = [];
253
253
  const own = new Set();
254
- const addOwn = (n) => { if (own.has(n)) return; own.add(n); if (n === '_h' || n === '_hall') addOwn('_hop'); if (n === '_ok') { addOwn('_PN'); addOwn('_hop'); } decls.push(OWN_HELPER_CODE[n]); };
254
+ const addOwn = (n) => { if (own.has(n)) return; own.add(n); if (n === '_h' || n === '_hall') addOwn('_hop'); if (n === '_ok') { addOwn('_PN'); addOwn('_hop'); } if (n === '_nk') addOwn('_PN'); decls.push(OWN_HELPER_CODE[n]); };
255
255
  let usesSafeRe = emit.usesSafeRe, usesCollapse = false;
256
256
  for (let i = 0; i < emit.names.length; i++) {
257
257
  const name = emit.names[i], val = emit.values[i];
@@ -272,7 +272,7 @@ function onePassCore(validator, opts) {
272
272
  if (name === '_VF') { args.push('null'); continue; }
273
273
  if (name === '_PN') { addOwn('_PN'); args.push('_PN'); continue; }
274
274
  if (name === '_OP' || name === '_gp') { addOwn('_PN'); args.push(name); continue; }
275
- if (name === '_hop' || name === '_h' || name === '_ok' || name === '_hall') { addOwn(name); args.push(name); continue; }
275
+ if (name === '_hop' || name === '_h' || name === '_ok' || name === '_hall' || name === '_nk') { addOwn(name); args.push(name); continue; }
276
276
  if (name === '_pe') { decls.push(PTR_ESC_HELPER); args.push('_pe'); continue; }
277
277
  if (name === '_deq') { decls.push(DEQ_HELPER); args.push('_deq'); continue; }
278
278
  if (name === '_anT') { decls.push('function _anT(){return true}'); args.push('_anT'); continue; }
@@ -121,6 +121,9 @@ const _hop = Object.prototype.hasOwnProperty
121
121
  function _h (o, k) { return _hop.call(o, k) }
122
122
  function _ok (o, k) { return (_PN ? o.__proto__ : _gp(o)) === _OP || _hop.call(o, k) } // eslint-disable-line no-proto
123
123
  function _hall (o, ks) { for (let i = 0; i < ks.length; i++) if (!_hop.call(o, ks[i])) return false; return true }
124
+ // A key count that came out wrong, counted again over own keys unless the
125
+ // object is ordinary.
126
+ function _nk (o, n) { return (_PN ? o.__proto__ : _gp(o)) === _OP || Object.keys(o).length !== n } // eslint-disable-line no-proto
124
127
 
125
128
  // RFC 6901 escaping for a key only known at run time (the static ones are
126
129
  // escaped when the code is written).
@@ -140,7 +143,7 @@ function _rb (S, m) { if (S.length !== m) S.length = m }
140
143
 
141
144
  module.exports = {
142
145
  apState, ap, sr, srX, multiBranch,
143
- _PN, _OP, _gp, _hop, _h, _ok, _hall, _pe, _anT, _anF, _rb,
146
+ _PN, _OP, _gp, _hop, _h, _ok, _hall, _nk, _pe, _anT, _anF, _rb,
144
147
  }
145
148
  // Loaded when a program first asks for them: a schema without const, enum,
146
149
  // uniqueItems, oneOf or anyOf never needs these modules on its cold path.
@@ -94,7 +94,11 @@ function errorRuntimeSource(format, version, features, schemaMode) {
94
94
  const src = OPTIONAL[name] !== undefined && !want(name)
95
95
  ? OPTIONAL[name].stub
96
96
  : dropCommentLines(fs.readFileSync(path.join(__dirname, name + '.js'), 'utf8'), name);
97
- defs.push(`${JSON.stringify(name)}: function (module, exports, require) {\n${src}\n}`);
97
+ // The files' own require('./x') calls go to the table under another name:
98
+ // a tool that reads the runtime without scope (knip, a lint rule) took a
99
+ // local `require` for an import of a file that is not there.
100
+ const own = src.replace(/\brequire\((['"])\.\//g, '__ataReq($1./');
101
+ defs.push(`${JSON.stringify(name)}: function (module, exports, __ataReq) {\n${own}\n}`);
98
102
  }
99
103
  const { byKw, byFmt } = codeTable();
100
104
  const body = `// Auto-generated by ata-validator ${version}, do not edit.
@@ -9,7 +9,7 @@ const { compileFunction } = require('./compile-fn')
9
9
  // the first call that compiles errors. Everything here was written as part
10
10
  // of js-compiler.js and reads its helpers from that module's _internal.
11
11
  const core = require('./js-compiler')
12
- const { hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun } = core._internal
12
+ const { ownHead, hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun } = core._internal
13
13
 
14
14
 
15
15
  function emitPtrEsc(ctx) {
@@ -1313,7 +1313,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1313
1313
  // The name set is built once per call of the error function, not at every
1314
1314
  // object it checks: on a list of 1723 tokens it was 1723 Sets.
1315
1315
  ctx.helperCode.push(`const _a${ci}=new Set([${allowed}])`)
1316
- const inner = `let _x${ci}=0;for(const _f${ci} in ${v}){if(${gateTest}){_x${ci}=1;break}}if(_x${ci}){${detail}}`
1316
+ const inner = `let _x${ci}=0;${ownHead(ctx, v, `_f${ci}`, 'const')}{if(${gateTest}){_x${ci}=1;break}}if(_x${ci}){${detail}}`
1317
1317
  lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
1318
1318
  }
1319
1319
 
@@ -1504,7 +1504,7 @@ function genPatternPropertiesE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1504
1504
  }
1505
1505
  const ri = ctx.regExpMap.get(pattern);
1506
1506
  const ki = ctx.varCounter++
1507
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){if(_re${ri}.test(_k${ki})){`)
1507
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${ownHead(ctx, v, `_k${ki}`, 'const')}{if(_re${ri}.test(_k${ki})){`)
1508
1508
  const _peFn = emitPtrEsc(ctx)
1509
1509
  const p = pathExpr ? `${pathExpr}+'/'+${_peFn}(_k${ki})` : `'/'+${_peFn}(_k${ki})`
1510
1510
  // The real pointer, so source maps can locate the failing keyword.
@@ -1538,7 +1538,7 @@ function genAdditionalSchemaE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1538
1538
  if (known.length) conds.push(`!_ak${ki}.has(_k${ki})`)
1539
1539
  if (patTests.length) conds.push(`!(${patTests.join('||')})`)
1540
1540
  const keep = conds.length ? `if(${conds.join('&&')}){` : `{`
1541
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${guard}for(const _k${ki} in ${v}){${keep}`)
1541
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${guard}${ownHead(ctx, v, `_k${ki}`, 'const')}{${keep}`)
1542
1542
  const _peFn = emitPtrEsc(ctx)
1543
1543
  const p = pathExpr ? `${pathExpr}+'/'+${_peFn}(_k${ki})` : `'/'+${_peFn}(_k${ki})`
1544
1544
  genCodeE(schema.additionalProperties, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/additionalProperties')
@@ -1559,13 +1559,13 @@ function genPropertyNamesFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1559
1559
  // the object that carries the name, not to `/<key>`, which is where the
1560
1560
  // value lives. Same placement as the object-valued propertyNames branch.
1561
1561
  const p = pathExpr || '""'
1562
- 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}}}`)
1562
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${ownHead(ctx, v, `_k${ki}`, 'const')}{_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}}}`)
1563
1563
  }
1564
1564
 
1565
1565
  function genPropertyNamesE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
1566
1566
  const pn = schema.propertyNames
1567
1567
  const ki = ctx.varCounter++
1568
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
1568
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${ownHead(ctx, v, `_k${ki}`, 'const')}{`)
1569
1569
  // The subschema checks the key as a value, and its errors are reported at
1570
1570
  // the object, as the interpreter reports them.
1571
1571
  if (!isSimplePN(pn)) {
@@ -2523,14 +2523,14 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2523
2523
  // leave `if()`, so the test is `true`.
2524
2524
  const gate = propKeys.map(k => `_f${ci}!==${JSON.stringify(k)}`).join('&&') || 'true'
2525
2525
  const checks = propKeys.map(k => `_k${ci}[_i]!==${JSON.stringify(k)}`).join('&&') || 'true'
2526
- body = `let _x${ci}=0;for(const _f${ci} in ${v}){if(${gate}){_x${ci}=1;break}}if(_x${ci}){${detail(checks)}}`
2526
+ body = `let _x${ci}=0;${ownHead(ctx, v, `_f${ci}`, 'const')}{if(${gate}){_x${ci}=1;break}}if(_x${ci}){${detail(checks)}}`
2527
2527
  } else {
2528
2528
  // The name set lives in the closure, built once with the function: built
2529
2529
  // in the body it was a 33-name Set on every call of this function, which
2530
2530
  // is where a rejected npm-badges document spent most of its 701 ns.
2531
2531
  ctx.closureVars.push(`_a${ci}`)
2532
2532
  ctx.closureVals.push(new Set(propKeys))
2533
- body = `let _x${ci}=0;for(const _f${ci} in ${v}){if(!_a${ci}.has(_f${ci})){_x${ci}=1;break}}if(_x${ci}){${detail(`!_a${ci}.has(_k${ci}[_i])`)}}`
2533
+ body = `let _x${ci}=0;${ownHead(ctx, v, `_f${ci}`, 'const')}{if(!_a${ci}.has(_f${ci})){_x${ci}=1;break}}if(_x${ci}){${detail(`!_a${ci}.has(_k${ci}[_i])`)}}`
2534
2534
  }
2535
2535
  // When every declared property is required, a clean object has exactly the
2536
2536
  // declared keys: if the enumerated count equals the declared count and
@@ -2545,7 +2545,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2545
2545
  const req = new Set(Array.isArray(schema.required) ? schema.required : [])
2546
2546
  if (propKeys.length > 0 && propKeys.every((k) => req.has(k) && !PROTO_NAMES.has(k))) {
2547
2547
  const present = propKeys.map((k) => `${JSON.stringify(k)} in ${v}`).join('&&')
2548
- body = `let _n${ci}=0;for(const _f${ci} in ${v})_n${ci}++;if(_n${ci}!==${propKeys.length}||!(${present})){${body}}`
2548
+ body = `let _n${ci}=0;${ownHead(ctx, v, `_f${ci}`, 'const')}_n${ci}++;if(_n${ci}!==${propKeys.length}||!(${present})){${body}}`
2549
2549
  }
2550
2550
  lines.push(isObj
2551
2551
  ? `{${body}}`
@@ -2945,7 +2945,7 @@ function inlineUnevalBlock (schema, v, pathExpr, ctx, schemaPrefix, fail, counts
2945
2945
  } else {
2946
2946
  throw DECLINE
2947
2947
  }
2948
- if (body) out.push(`for(const ${kVar} in ${v}){if(!${ownKeyExpr(ctx, v, kVar).split(JSON.stringify(kVar)).join(kVar)})continue;let _ub${id}=0;switch(${kVar}){${cases}}if(_ub${id}!==0&&(${m}&_ub${id})!==0)continue;${allBit}${patBits}${body}}`)
2948
+ if (body) out.push(`${ownHead(ctx, v, kVar, 'const')}{if(!${ownKeyExpr(ctx, v, kVar).split(JSON.stringify(kVar)).join(kVar)})continue;let _ub${id}=0;switch(${kVar}){${cases}}if(_ub${id}!==0&&(${m}&_ub${id})!==0)continue;${allBit}${patBits}${body}}`)
2949
2949
  const text = out.join(';')
2950
2950
  ctx.plainReuse = prevReuse
2951
2951
  if (prevPk === undefined) ctx.plainOf.delete(v)
@@ -3357,7 +3357,7 @@ function genAdditionalSchemaC(schema, v, pathExpr, lines, ctx, schemaPrefix, isO
3357
3357
  const sub = []
3358
3358
  genCodeC(schema.additionalProperties, `${v}[${kVar}]`, p, sub, ctx, schemaPrefix + '/additionalProperties')
3359
3359
  if (sub.length === 0) return
3360
- lines.push(`${guard}{for(const ${kVar} in ${v}){${keep}{${sub.join('\n ')}\n }}}`)
3360
+ lines.push(`${guard}{${ownHead(ctx, v, kVar, 'const')}{${keep}{${sub.join('\n ')}\n }}}`)
3361
3361
  }
3362
3362
 
3363
3363
  function genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
@@ -3397,7 +3397,7 @@ function genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fa
3397
3397
  ctx.closureVals.push(new Set(propKeys))
3398
3398
  }
3399
3399
 
3400
- lines.push(`${guard}{for(const ${kVar} in ${v}){`)
3400
+ lines.push(`${guard}{${ownHead(ctx, v, kVar, 'const')}{`)
3401
3401
  // propertyNames checks (merged)
3402
3402
  if (pn) {
3403
3403
  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'`)}}`)
@@ -3434,7 +3434,7 @@ function genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fa
3434
3434
  lines.push(`}}`)
3435
3435
  } else {
3436
3436
  handledPropertyNames = !!pn
3437
- lines.push(`${guard}{for(const ${kVar} in ${v}){`)
3437
+ lines.push(`${guard}{${ownHead(ctx, v, kVar, 'const')}{`)
3438
3438
  if (pn) {
3439
3439
  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'`)}}`)
3440
3440
  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'`)}}`)
@@ -3478,7 +3478,7 @@ function genDependentSchemasC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3478
3478
  function genPropertyNamesFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3479
3479
  const ki = ctx.varCounter++
3480
3480
  const p = pathExpr || '""'
3481
- lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){${errPushC(ctx, 'not', `${schemaPrefix}/propertyNames`, pathExpr, '{}', "'boolean schema is false'", v, `{keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'}`)}}}`)
3481
+ lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${ownHead(ctx, v, `_k${ki}`, 'const')}{${errPushC(ctx, 'not', `${schemaPrefix}/propertyNames`, pathExpr, '{}', "'boolean schema is false'", v, `{keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'}`)}}}`)
3482
3482
  }
3483
3483
 
3484
3484
  function genPropertyNamesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
@@ -3488,7 +3488,7 @@ function genPropertyNamesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
3488
3488
  // here that would be the key where the interpreter's error points at the
3489
3489
  // object, so that one is left to the plain function and enrichment.
3490
3490
  if (!isSimplePN(pn) && ctx.rich) throw DECLINE
3491
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
3491
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${ownHead(ctx, v, `_k${ki}`, 'const')}{`)
3492
3492
  if (!isSimplePN(pn)) {
3493
3493
  genCodeC(pn, `_k${ki}`, pathExpr, lines, ctx, schemaPrefix + '/propertyNames')
3494
3494
  lines.push(`}}`)
@@ -36,7 +36,7 @@ function uqSources () {
36
36
  // that would have written them.
37
37
  const SHARED_RT = {
38
38
  _deqHoisted: ['_deq'], _peHoisted: ['_pe'],
39
- _own_PN: ['_PN', '_OP', '_gp'], _own_hop: ['_hop'], _own_h: ['_h'], _own_ok: ['_ok'], _own_hall: ['_hall'],
39
+ _own_PN: ['_PN', '_OP', '_gp'], _own_hop: ['_hop'], _own_h: ['_h'], _own_ok: ['_ok'], _own_hall: ['_hall'], _own_nk: ['_nk'],
40
40
  }
41
41
  let _rt = null
42
42
  function sharedRt () { return _rt || (_rt = require('./combined-runtime')) }
@@ -96,8 +96,8 @@ function apMembershipCheck (ctx, names, v) {
96
96
  }
97
97
  }
98
98
  // With nothing declared every key is additional; `if()` would not parse.
99
- if (names.length === 0) return `for(var _k in ${v})return false`
100
- return `for(var _k in ${v})if(${names.map((k) => `_k!==${JSON.stringify(k)}`).join('&&')})return false`
99
+ if (names.length === 0) return `for(var _k in ${v})if(${ownFail(ctx, v)})return false`
100
+ return `for(var _k in ${v})if(${names.map((k) => `_k!==${JSON.stringify(k)}`).join('&&')}&&${ownFail(ctx, v)})return false`
101
101
  }
102
102
 
103
103
  function emitDeq(ctx) {
@@ -907,8 +907,9 @@ const OWN_HELPER_CODE = {
907
907
  _h: 'function _h(o,k){return _hop.call(o,k)}',
908
908
  _ok: 'function _ok(o,k){return (_PN?o.__proto__:_gp(o))===_OP||_hop.call(o,k)}',
909
909
  _hall: 'function _hall(o,ks){for(let i=0;i<ks.length;i++)if(!_hop.call(o,ks[i]))return false;return true}',
910
+ _nk: 'function _nk(o,n){return (_PN?o.__proto__:_gp(o))===_OP||Object.keys(o).length!==n}',
910
911
  }
911
- const OWN_HELPER_DEPS = { _h: ['_hop'], _ok: ['_PN', '_hop'], _hall: ['_hop'] }
912
+ const OWN_HELPER_DEPS = { _h: ['_hop'], _ok: ['_PN', '_hop'], _hall: ['_hop'], _nk: ['_PN'] }
912
913
  function useOwnHelper (ctx, name) {
913
914
  for (const dep of OWN_HELPER_DEPS[name] || []) useOwnHelper(ctx, dep)
914
915
  hoistOnce(ctx, '_own' + name, OWN_HELPER_CODE[name])
@@ -1208,6 +1209,17 @@ function propFnCandidate (prop, ctx) {
1208
1209
  // inline after all": an empty body (the inline path then asserts it the
1209
1210
  // usual way) or one that calls the recursive entry, which is out of scope
1210
1211
  // in the preamble.
1212
+ // Generates a body that will become a function of its own in the preamble.
1213
+ // The ordinary-object flags withPlain declared belong to the function being
1214
+ // written now and are out of scope there; a hoisted body takes the same
1215
+ // parameter names (`d`, `_av`, `_ov`) and would otherwise read a flag the
1216
+ // enclosing node declared for a value of that name.
1217
+ function genIsolated (ctx, run) {
1218
+ const outerPlain = ctx.plainOf
1219
+ ctx.plainOf = undefined
1220
+ try { return run() } finally { ctx.plainOf = outerPlain }
1221
+ }
1222
+
1211
1223
  function emitPropFn (prop, ctx) {
1212
1224
  const bodyLines = []
1213
1225
  const outerDeferred = ctx.deferredChecks
@@ -1216,7 +1228,7 @@ function emitPropFn (prop, ctx) {
1216
1228
  ctx.plainReuse = undefined
1217
1229
  const outerBytes = ctx.fnBytes
1218
1230
  ctx.fnBytes = 0
1219
- try { genCode(prop, 'd', bodyLines, ctx) } finally { ctx.plainReuse = outerReuse; ctx.fnBytes = outerBytes }
1231
+ try { genIsolated(ctx, () => genCode(prop, 'd', bodyLines, ctx)) } finally { ctx.plainReuse = outerReuse; ctx.fnBytes = outerBytes }
1220
1232
  if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
1221
1233
  ctx.deferredChecks = outerDeferred
1222
1234
  if (bodyLines.length === 0 || bodyLines.some((l) => /\b_validate\b/.test(l))) return null
@@ -1341,6 +1353,54 @@ function sharedDefNames (root, defs) {
1341
1353
  // known trade: on that shape the shared function costs 27 ns a call.
1342
1354
  const SHARED_DEF_MIN_CHARS = 120
1343
1355
 
1356
+ // A small definition applied at many places is called with objects of many
1357
+ // shapes, one or two per place, and V8 gives a function's property reads up
1358
+ // to four shapes before it falls back to a generic lookup several times
1359
+ // slower: SchemaStore's detekt sends 72 shapes through one four-property rule
1360
+ // definition. Such a definition is emitted as several functions with the same
1361
+ // body, each serving at most SITES_PER_COPY references, so every copy stays
1362
+ // within four shapes. Only small bodies are copied, so the source grows by a
1363
+ // bounded amount; a large or rarely referenced definition keeps one function.
1364
+ const SITES_PER_COPY = 4
1365
+ const COPY_MIN_SITES = 16
1366
+ const COPY_MAX_BODY = 1500
1367
+ function sharedDefCopy (ctx, defName, fnName) {
1368
+ const body = ctx.sharedDefBody && ctx.sharedDefBody.get(fnName)
1369
+ if (body === undefined || body.length > COPY_MAX_BODY) return fnName
1370
+ // Only a definition that reads properties gains: a string, a number or a
1371
+ // wrapper around another reference has no shapes to keep apart, and more
1372
+ // copies only mean more functions to warm up.
1373
+ const def = ctx.rootDefs && ctx.rootDefs[defName]
1374
+ if (def === null || typeof def !== 'object' || typeof def.properties !== 'object' || def.properties === null) return fnName
1375
+ if (!ctx.sharedDefSites) ctx.sharedDefSites = new Map()
1376
+ if (ctx.sharedDefSiteCounts === undefined) ctx.sharedDefSiteCounts = refCounts(ctx.rootSchema)
1377
+ if ((ctx.sharedDefSiteCounts.get(defName) || 0) < COPY_MIN_SITES) return fnName
1378
+ const site = ctx.sharedDefSites.get(fnName) || 0
1379
+ ctx.sharedDefSites.set(fnName, site + 1)
1380
+ const copy = Math.floor(site / SITES_PER_COPY)
1381
+ if (copy === 0) return fnName
1382
+ const name = `${fnName}_c${copy}`
1383
+ if (site % SITES_PER_COPY === 0) hoist(ctx, `function ${name}(d){${body}\n return true}`)
1384
+ return name
1385
+ }
1386
+
1387
+ // How many `$ref`s name each local definition, by name.
1388
+ function refCounts (root) {
1389
+ const counts = new Map()
1390
+ const seen = new WeakSet()
1391
+ const walk = (node) => {
1392
+ if (node === null || typeof node !== 'object' || seen.has(node)) return
1393
+ seen.add(node)
1394
+ if (typeof node.$ref === 'string') {
1395
+ const m = /^#\/(?:\$defs|definitions)\/([^/]+)$/.exec(node.$ref)
1396
+ if (m) counts.set(m[1], (counts.get(m[1]) || 0) + 1)
1397
+ }
1398
+ for (const v of Object.values(node)) walk(v)
1399
+ }
1400
+ walk(root)
1401
+ return counts
1402
+ }
1403
+
1344
1404
  function cyclicDefNames (defs) {
1345
1405
  const out = new Set()
1346
1406
  if (!defs || typeof defs !== 'object') return out
@@ -3419,7 +3479,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3419
3479
  if (allRequired && propCount > 0) {
3420
3480
  // Adaptive: for-in for <=15 keys (V8 fast path), Object.keys for >15
3421
3481
  if (propCount <= 15) {
3422
- lines.push(`var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`)
3482
+ lines.push(`var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount}&&${ownCountOff(ctx, v, propCount, plainFlag(ctx, v))})return false`)
3423
3483
  } else {
3424
3484
  lines.push(`if(Object.keys(${v}).length!==${propCount})return false`)
3425
3485
  }
@@ -3534,7 +3594,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3534
3594
  if (schema.additionalProperties === false && schema.properties && !schema.patternProperties && ctx.removeNodes && ctx.removeNodes.has(schema)) {
3535
3595
  const kv = `_rk${ctx.varCounter++}`
3536
3596
  const declared = Object.keys(schema.properties)
3537
- let loop = `for(var ${kv} in ${v})if(${declared.map((k) => `${kv}!==${JSON.stringify(k)}`).join('&&')})delete ${v}[${kv}]`
3597
+ let loop = `${ownHead(ctx, v, kv, 'var')}if(${declared.map((k) => `${kv}!==${JSON.stringify(k)}`).join('&&')})delete ${v}[${kv}]`
3538
3598
  // When every declared property is required, the verdict has already
3539
3599
  // established they are all there, so a key count equal to the declared
3540
3600
  // count means nothing is extra and the deleting walk, which compares every
@@ -3542,7 +3602,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3542
3602
  const req = new Set(schema.required || [])
3543
3603
  if (declared.length <= 15 && declared.every((k) => req.has(k))) {
3544
3604
  const n = `_rn${ctx.varCounter++}`
3545
- loop = `{var ${n}=0;for(var ${kv} in ${v})${n}++;if(${n}!==${declared.length}){${loop}}}`
3605
+ loop = `{var ${n}=0;${ownHead(ctx, v, kv, 'var')}${n}++;if(${n}!==${declared.length}){${loop}}}`
3546
3606
  }
3547
3607
  _deferOrInline(ctx, lines, v, isObj ? loop : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${loop}}`)
3548
3608
  } else if (schema.additionalProperties === false && !schema.patternProperties) {
@@ -3559,7 +3619,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3559
3619
  const allRequired = !!schema.required && declaredKeys.every((k) => req.has(k))
3560
3620
  const inner = allRequired
3561
3621
  ? (propCount <= 15
3562
- ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
3622
+ ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount}&&${ownCountOff(ctx, v, propCount, plainFlag(ctx, v))})return false`
3563
3623
  : `if(Object.keys(${v}).length!==${propCount})return false`)
3564
3624
  : apMembershipCheck(ctx, Object.keys(schema.properties || {}), v)
3565
3625
  _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
@@ -3590,8 +3650,8 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3590
3650
  // propertyNames: false rejects any key at all; true constrains nothing.
3591
3651
  if (schema.propertyNames === false) {
3592
3652
  lines.push(isObj
3593
- ? `for(const _k in ${v})return false`
3594
- : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k in ${v})return false}`)
3653
+ ? `${ownHead(ctx, v, `_k`, 'const')}return false`
3654
+ : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${ownHead(ctx, v, `_k`, 'const')}return false}`)
3595
3655
  }
3596
3656
 
3597
3657
  // propertyNames — only emit if not already merged into patternProperties loop
@@ -3779,8 +3839,8 @@ function genAdditionalSchemaNode(schema, v, lines, ctx, knownType, isObj) {
3779
3839
  if (subLines.length > 0) {
3780
3840
  const body = subLines.join(';')
3781
3841
  const loop = skipCheck
3782
- ? `for(var ${kVar} in ${v}){if(${skipCheck})continue;const ${avVar}=${v}[${kVar}];${body}}`
3783
- : `for(var ${kVar} in ${v}){const ${avVar}=${v}[${kVar}];${body}}`
3842
+ ? `${ownHead(ctx, v, kVar, 'var')}{if(${skipCheck})continue;const ${avVar}=${v}[${kVar}];${body}}`
3843
+ : `${ownHead(ctx, v, kVar, 'var')}{const ${avVar}=${v}[${kVar}];${body}}`
3784
3844
  _deferOrInline(ctx, lines, v, isObj ? loop : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${loop}}`)
3785
3845
  }
3786
3846
  }
@@ -3804,10 +3864,10 @@ function genPropertyNamesNode(schema, v, lines, ctx, knownType, isObj) {
3804
3864
  const sub = []
3805
3865
  genCode(pn, '_pv', sub, ctx)
3806
3866
  if (sub.length === 0) return
3807
- lines.push(`${guard}{const _pnf${ki}=function(_pv){${sub.join(';')};return true};for(const _k${ki} in ${v}){if(!_pnf${ki}(_k${ki}))return false}}`)
3867
+ lines.push(`${guard}{const _pnf${ki}=function(_pv){${sub.join(';')};return true};${ownHead(ctx, v, `_k${ki}`, 'const')}{if(!_pnf${ki}(_k${ki}))return false}}`)
3808
3868
  return
3809
3869
  }
3810
- lines.push(`${guard}{for(const _k${ki} in ${v}){`)
3870
+ lines.push(`${guard}{${ownHead(ctx, v, `_k${ki}`, 'const')}{`)
3811
3871
  if (pn.minLength !== undefined) lines.push(`if(_k${ki}.length<${pn.minLength})return false`)
3812
3872
  if (pn.maxLength !== undefined) lines.push(`if(_k${ki}.length>${pn.maxLength})return false`)
3813
3873
  if (pn.pattern) {
@@ -3878,7 +3938,8 @@ function genAnyOfNode(schema, v, lines, ctx, knownType) {
3878
3938
  continue
3879
3939
  }
3880
3940
  const subLines = []
3881
- genCode(sub, '_av', subLines, ctx)
3941
+ if (canHoist) genIsolated(ctx, () => genCode(sub, '_av', subLines, ctx))
3942
+ else genCode(sub, '_av', subLines, ctx)
3882
3943
  const body = subLines.length === 0 ? 'return true' : `${subLines.join(';')};return true`
3883
3944
  if (/\b_validate\b/.test(body)) canHoist = false
3884
3945
  branchBodies.push(body)
@@ -3916,6 +3977,35 @@ function branchKey (sub, ctx) {
3916
3977
  return sub
3917
3978
  }
3918
3979
 
3980
+ // A oneOf whose branches each allow only their own properties, the way
3981
+ // Traefik's middlewares and many tagged unions are written: every branch is
3982
+ // `{ properties: {...}, additionalProperties: false }` (an object `type` and
3983
+ // annotations aside), and no property belongs to two branches. Then an
3984
+ // object passes a branch only if every key it has is that branch's, so its
3985
+ // first key names the one branch that can pass, and an object with no keys
3986
+ // passes all of them. Returns the branch index per property name, or null.
3987
+ const KEYED_BRANCH_KEYS = new Set(['properties', 'additionalProperties', 'type', 'title', 'description', '$comment'])
3988
+ function keyedBranches (branches) {
3989
+ if (branches.length < 4) return null
3990
+ const owner = new Map()
3991
+ for (let i = 0; i < branches.length; i++) {
3992
+ const b = branches[i]
3993
+ if (typeof b !== 'object' || b === null || Array.isArray(b)) return null
3994
+ for (const k of Object.keys(b)) if (!KEYED_BRANCH_KEYS.has(k)) return null
3995
+ if (b.additionalProperties !== false) return null
3996
+ if (b.type !== undefined && b.type !== 'object') return null
3997
+ const props = b.properties
3998
+ if (typeof props !== 'object' || props === null || Array.isArray(props)) return null
3999
+ const names = Object.keys(props)
4000
+ if (names.length === 0) return null
4001
+ for (const n of names) {
4002
+ if (owner.has(n)) return null
4003
+ owner.set(n, i)
4004
+ }
4005
+ }
4006
+ return owner
4007
+ }
4008
+
3919
4009
  function genOneOfNode(schema, v, lines, ctx, knownType) {
3920
4010
  const fi = ctx.varCounter++
3921
4011
  const branchBodies = []
@@ -3939,7 +4029,8 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
3939
4029
  continue
3940
4030
  }
3941
4031
  const subLines = []
3942
- genCode(sub, '_ov', subLines, ctx)
4032
+ if (canHoist) genIsolated(ctx, () => genCode(sub, '_ov', subLines, ctx))
4033
+ else genCode(sub, '_ov', subLines, ctx)
3943
4034
  const body = subLines.length === 0 ? 'return true' : `${subLines.join(';')};return true`
3944
4035
  // _validate is the recursive entry — hoisting branches above it breaks scope.
3945
4036
  if (/\b_validate\b/.test(body)) canHoist = false
@@ -3955,6 +4046,17 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
3955
4046
  return name
3956
4047
  })
3957
4048
  const calls = names.map(n => `if(${n}(${v})){_oc${fi}++;if(_oc${fi}>1)return false}`).join(';')
4049
+ const owner = keyedBranches(schema.oneOf)
4050
+ if (owner !== null) {
4051
+ // An object goes to the branch its first key names; anything else, and
4052
+ // an object with no keys, takes the loop over every branch.
4053
+ const entries = [...owner].map(([k, i]) => `[${JSON.stringify(k)},${names[i]}]`).join(',')
4054
+ hoist(ctx, `const _ofm${fi}=new Map([${entries}])`)
4055
+ lines.push(`{let _ofk${fi};if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${ownHead(ctx, v, '_q', 'const')}{_ofk${fi}=_q;break}}` +
4056
+ `if(_ofk${fi}!==undefined){const _ob${fi}=_ofm${fi}.get(_ofk${fi});if(_ob${fi}===undefined||!_ob${fi}(${v}))return false}` +
4057
+ `else{let _oc${fi}=0;${calls};if(_oc${fi}!==1)return false}}`)
4058
+ return
4059
+ }
3958
4060
  lines.push(`{let _oc${fi}=0;${calls};if(_oc${fi}!==1)return false}`)
3959
4061
  } else {
3960
4062
  const fns = branchBodies.map(body => `function(_ov){${body}}`)
@@ -4032,7 +4134,7 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
4032
4134
  ctx.deferredChecks = null
4033
4135
  const outerReuse = ctx.plainReuse
4034
4136
  ctx.plainReuse = undefined
4035
- try { genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx) } finally { ctx.plainReuse = outerReuse }
4137
+ try { genIsolated(ctx, () => genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)) } finally { ctx.plainReuse = outerReuse }
4036
4138
  if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
4037
4139
  ctx.deferredChecks = outerDeferred
4038
4140
  // The def is hoisted above the entry function, where the recursive
@@ -4070,7 +4172,7 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
4070
4172
  const outerReuse = ctx.plainReuse
4071
4173
  ctx.plainReuse = undefined
4072
4174
  try {
4073
- genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)
4175
+ genIsolated(ctx, () => genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx))
4074
4176
  } finally {
4075
4177
  ctx.refStack.delete(schema.$ref)
4076
4178
  ctx.plainReuse = outerReuse
@@ -4078,9 +4180,12 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
4078
4180
  if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
4079
4181
  ctx.deferredChecks = outerDeferred
4080
4182
  if (bodyLines.some((l) => /\b_validate\b/.test(l))) throw DECLINE
4081
- hoist(ctx, `function ${fnName}(d){${bodyLines.join('\n ')}\n return true}`)
4183
+ const body = bodyLines.join('\n ')
4184
+ hoist(ctx, `function ${fnName}(d){${body}\n return true}`)
4185
+ if (!ctx.sharedDefBody) ctx.sharedDefBody = new Map()
4186
+ ctx.sharedDefBody.set(fnName, body)
4082
4187
  }
4083
- lines.push(`if(!${fnName}(${v}))return false`)
4188
+ lines.push(`if(!${sharedDefCopy(ctx, defName, fnName)}(${v}))return false`)
4084
4189
  if (!hasSiblings) return true
4085
4190
  } else if (ctx.refStack.has(schema.$ref)) { if (ctx.nestedBoolean) throw DECLINE; if (!hasSiblings) return true }
4086
4191
  else {
@@ -4177,7 +4282,7 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
4177
4282
  genCode(apSchema, `_apv${pi}`, apLines, ctx)
4178
4283
  apCheck = apLines.join(';')
4179
4284
  }
4180
- lines.push(`${guard}{for(const ${kVar} in ${v}){`)
4285
+ lines.push(`${guard}{${ownHead(ctx, v, kVar, 'const')}{`)
4181
4286
  // propertyNames checks (merged into same loop)
4182
4287
  if (pn) {
4183
4288
  if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength})return false`)
@@ -4228,7 +4333,7 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
4228
4333
  } else {
4229
4334
  // No additionalProperties: validate matching keys + propertyNames
4230
4335
  handledPropertyNames = !!pn
4231
- lines.push(`${guard}{for(const ${kVar} in ${v}){`)
4336
+ lines.push(`${guard}{${ownHead(ctx, v, kVar, 'const')}{`)
4232
4337
  // propertyNames checks (merged)
4233
4338
  if (pn) {
4234
4339
  if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength})return false`)
@@ -4307,7 +4412,7 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4307
4412
  if (!earlyKeyCount) {
4308
4413
  // Adaptive: for-in for <=15 keys, Object.keys for >15
4309
4414
  inner = propCount <= 15
4310
- ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
4415
+ ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount}&&${ownCountOff(ctx, v, propCount, plainFlag(ctx, v))})return false`
4311
4416
  : `if(Object.keys(${v}).length!==${propCount})return false`
4312
4417
  _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
4313
4418
  }
@@ -4320,10 +4425,10 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4320
4425
  // document. The hoisted name lookup is the same cure it was there.
4321
4426
  inner = propCount >= AP_LOOKUP_MIN
4322
4427
  ? apMembershipCheck(ctx, knownKeys, v)
4323
- : genCharCodeSwitch(knownKeys, v)
4428
+ : genCharCodeSwitch(knownKeys, v, ctx)
4324
4429
  _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
4325
4430
  } else {
4326
- inner = `for(var _k in ${v})return false`
4431
+ inner = `for(var _k in ${v})if(${ownFail(ctx, v)})return false`
4327
4432
  _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
4328
4433
  }
4329
4434
  } else if (typeof schema.unevaluatedProperties === 'object') {
@@ -4341,7 +4446,7 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4341
4446
  } else if (knownKeys.length > 0) {
4342
4447
  skipKnown = `if(${knownKeys.map(k => `${ukVar}===${JSON.stringify(k)}`).join('||')})continue;`
4343
4448
  }
4344
- const inner = `for(var ${ukVar} in ${v}){${skipKnown}${check}}`
4449
+ const inner = `${ownHead(ctx, v, ukVar, 'var')}{${skipKnown}${check}}`
4345
4450
  _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
4346
4451
  }
4347
4452
  }
@@ -4386,8 +4491,8 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4386
4491
  const uniqueThen = [...new Set([...baseProps, ...ifProps, ...(thenEval.props || [])])]
4387
4492
  const uniqueElse = [...new Set([...baseProps, ...(elseEval.props || [])])]
4388
4493
 
4389
- const thenCheck = genCharCodeSwitch(uniqueThen, v)
4390
- const elseCheck = genCharCodeSwitch(uniqueElse, v)
4494
+ const thenCheck = genCharCodeSwitch(uniqueThen, v, ctx)
4495
+ const elseCheck = genCharCodeSwitch(uniqueElse, v, ctx)
4391
4496
  const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
4392
4497
  lines.push(`${guard}{const _uif${ufi}=${ifFn2};if(_uif${ufi}(${v})){${thenCheck}}else{${elseCheck}}}`)
4393
4498
  } else if (branchKeyword) {
@@ -4458,8 +4563,8 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4458
4563
  }
4459
4564
  const dynamicCheck = `switch(_k.charCodeAt(0)){${switchCases}default:break}`
4460
4565
  const inner = staticCheck
4461
- ? `for(var _k in ${v}){if(${staticCheck})continue;${dynamicCheck}return false}`
4462
- : `for(var _k in ${v}){${dynamicCheck}return false}`
4566
+ ? `for(var _k in ${v}){if(${staticCheck})continue;${dynamicCheck}if(${ownFail(ctx, v)})return false}`
4567
+ : `for(var _k in ${v}){${dynamicCheck}if(${ownFail(ctx, v)})return false}`
4463
4568
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
4464
4569
  } else {
4465
4570
  // Fallback: plain object tracking
@@ -4484,7 +4589,7 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4484
4589
  // Single pass: validate anyOf (at least one) + track all matching
4485
4590
  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`)
4486
4591
  }
4487
- const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
4592
+ const inner = `for(var _k in ${v}){if(!${evVar}[_k]&&${ownFail(ctx, v)})return false}`
4488
4593
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
4489
4594
  }
4490
4595
  } else if (schema.dependentSchemas) {
@@ -4499,7 +4604,7 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4499
4604
  lines.push(`if(${ownKeyExpr(ctx, v, trigger)}){${depResult.props.map(k => `${evVar}[${JSON.stringify(k)}]=1`).join(';')}}`)
4500
4605
  }
4501
4606
  }
4502
- const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
4607
+ const inner = `for(var _k in ${v}){if(!${evVar}[_k]&&${ownFail(ctx, v)})return false}`
4503
4608
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
4504
4609
  } else {
4505
4610
  // General fallback: collect all patternProperties from root + allOf sub-schemas + if
@@ -4559,10 +4664,10 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4559
4664
  }
4560
4665
  const rootPatCheck = rootReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
4561
4666
  const ifPatCheck = ifReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
4562
- 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}}`
4667
+ const inner = `const _uif${ufi}=${ifFn};if(_uif${ufi}(${v})){for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}${ifPatCheck}if(${ownFail(ctx, v)})return false}}else{for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}if(${ownFail(ctx, v)})return false}}`
4563
4668
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
4564
4669
  } else {
4565
- 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}`
4670
+ const inner = `for(var _k in ${v}){if(${evVar}[_k])continue;${reVars.map(rv => `if(${rv}.test(_k)){${evVar}[_k]=1;continue}`).join('')}if(${ownFail(ctx, v)})return false}`
4566
4671
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
4567
4672
  }
4568
4673
  } else {
@@ -4573,7 +4678,7 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4573
4678
  const evVar = `_ev${ei}`
4574
4679
  lines.push(`{const ${evVar}={}`)
4575
4680
  for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
4576
- const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
4681
+ const inner = `for(var _k in ${v}){if(!${evVar}[_k]&&${ownFail(ctx, v)})return false}`
4577
4682
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
4578
4683
  }
4579
4684
  }
@@ -4613,7 +4718,7 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4613
4718
  genCode(schema.unevaluatedProperties, `${v}[${ukVar}]`, subLines2, ctx)
4614
4719
  if (subLines2.length > 0) {
4615
4720
  const check = subLines2.join(';')
4616
- const inner = `for(var ${ukVar} in ${v}){if(${evVar}[${ukVar}])continue;${check}}`
4721
+ const inner = `${ownHead(ctx, v, ukVar, 'var')}{if(${evVar}[${ukVar}])continue;${check}}`
4617
4722
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
4618
4723
  } else {
4619
4724
  lines.push('}')
@@ -5051,13 +5156,57 @@ function fastPrefixCheck(pattern, keyVar) {
5051
5156
  return `${keyVar}.startsWith(${JSON.stringify(prefix)})`
5052
5157
  }
5053
5158
 
5159
+ // The head of a loop over the own enumerable keys of `v`, written as
5160
+ // `for...in`, the fast way to walk an object, which also visits enumerable
5161
+ // keys its prototype carries. An ordinary object's prototype is
5162
+ // Object.prototype, and its keys are taken as they come; any other object, a
5163
+ // class instance or one made with Object.create, has each key checked as its
5164
+ // own, as the interpreter reads them. One statement, so the body that follows
5165
+ // is written as it was: `for(var _k in v)if(...)continue;else BODY`.
5166
+ // For a loop whose only work is to reject on a key it does not expect: the
5167
+ // key is the object's own, or the object is ordinary, whose prototype's keys
5168
+ // are taken as they come. Read only on the way to `return false`, so an
5169
+ // object that has only expected keys pays nothing.
5170
+ // `_ok` reads the prototype rather than the node's flag: this check can land
5171
+ // in a hoisted branch function the flag is not declared in.
5172
+ function ownFail (ctx, v, k = '_k') {
5173
+ useOwnHelper(ctx, '_ok')
5174
+ return `_ok(${v},${k})`
5175
+ }
5176
+ // For a key count that came out wrong: whether it is wrong counting own keys
5177
+ // only. An ordinary object's count stands; any other object is counted again.
5178
+ // A oneOf of closed objects fails its other branches here on every value, so
5179
+ // this is not only a cold path.
5180
+ function ownCountOff (ctx, v, n, pk) {
5181
+ // A call to a shared helper rather than the test written in place: a oneOf
5182
+ // branch this small is inlined into its caller by V8, and the longer
5183
+ // bytecode cost that, 8% on a document of patch operations. The node's
5184
+ // ordinary-object flag is not used: this can land in a hoisted branch
5185
+ // function it is not declared in.
5186
+ useOwnHelper(ctx, '_nk')
5187
+ // `pk`: the node's own ordinary-object flag, where the caller holds it in
5188
+ // the same function as this check. An ordinary object needs no recount.
5189
+ return pk ? `(${pk}||_nk(${v},${n}))` : `_nk(${v},${n})`
5190
+ }
5191
+
5192
+ function ownHead (ctx, v, k = '_k', kind = 'var') {
5193
+ useOwnHelper(ctx, '_hop')
5194
+ // Read here rather than taken from the node's flag: generated bodies that
5195
+ // become functions of their own (branches, annotations, definitions) take
5196
+ // the same parameter names and do not see the flag. Written in place, not
5197
+ // through a shared helper, which would see every object shape of the
5198
+ // document and read the prototype megamorphically: 7% on validate().
5199
+ const i = ctx.varCounter++
5200
+ return `var _pl${i}=${v}!==null&&typeof ${v}==='object'&&${protoIs(ctx, v)};for(${kind} ${k} in ${v})if(!_pl${i}&&!_hop.call(${v},${k}))continue;else `
5201
+ }
5202
+
5054
5203
  // Generate a charCodeAt(0)-based switch tree for fast key validation.
5055
5204
  // V8 compiles switch to jump tables — O(1) dispatch vs O(n) chain.
5056
- function genCharCodeSwitch(keys, v) {
5057
- if (keys.length === 0) return `for(var _k in ${v})return false`
5205
+ function genCharCodeSwitch(keys, v, ctx) {
5206
+ if (keys.length === 0) return `for(var _k in ${v})if(${ownFail(ctx, v)})return false`
5058
5207
  if (keys.length <= 3) {
5059
5208
  // Small set: simple chain is faster than switch overhead
5060
- return `for(var _k in ${v})if(${keys.map(k => `_k!==${JSON.stringify(k)}`).join('&&')})return false`
5209
+ return `for(var _k in ${v})if(${keys.map(k => `_k!==${JSON.stringify(k)}`).join('&&')}&&${ownFail(ctx, v)})return false`
5061
5210
  }
5062
5211
 
5063
5212
  // Group keys by first charCode
@@ -5074,7 +5223,7 @@ function genCharCodeSwitch(keys, v) {
5074
5223
  cases += `case ${cc}:if(${cond})continue;break;`
5075
5224
  }
5076
5225
 
5077
- return `for(var _k in ${v}){switch(_k.charCodeAt(0)){${cases}default:break}return false}`
5226
+ return `for(var _k in ${v}){switch(_k.charCodeAt(0)){${cases}default:break}if(${ownFail(ctx, v)})return false}`
5078
5227
  }
5079
5228
 
5080
5229
  // The verdict form of genUnevaluatedDynamicC: the same set, built the same
@@ -5120,7 +5269,7 @@ function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj, nonObjApplicators
5120
5269
  // additionalProperties is there.
5121
5270
  function addLocalAnnotations(node, v, S, out, ctx) {
5122
5271
  if (node.additionalProperties !== undefined) {
5123
- out.push(`for(const _ka in ${v})${S}.push(_ka)`)
5272
+ out.push(`${ownHead(ctx, v, `_ka`, 'const')}${S}.push(_ka)`)
5124
5273
  return
5125
5274
  }
5126
5275
  for (const k of Object.keys(node.properties || {})) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${S}.push(${JSON.stringify(k)})`)
@@ -5136,7 +5285,7 @@ function addLocalAnnotations(node, v, S, out, ctx) {
5136
5285
  ctx.closureVals.push(safeReClosure(ctx, pat))
5137
5286
  tests.push(`_re${ri}.test(${kv})`)
5138
5287
  }
5139
- out.push(`for(const ${kv} in ${v})if(${tests.join('||')})${S}.push(${kv})`)
5288
+ out.push(`${ownHead(ctx, v, kv, 'const')}if(${tests.join('||')})${S}.push(${kv})`)
5140
5289
  }
5141
5290
  }
5142
5291
 
@@ -5419,4 +5568,4 @@ Object.defineProperty(module.exports, '_expandDynamicScopes', { value: expandDyn
5419
5568
  // For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
5420
5569
  Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })
5421
5570
 
5422
- module.exports._internal = { hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun }
5571
+ module.exports._internal = { ownHead, hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun }
package/lib/safe-regex.js CHANGED
@@ -381,6 +381,24 @@ function __ataSafeRegex () {
381
381
  const states = []
382
382
  const stateIds = new Map()
383
383
  let overflow = false
384
+ // Transitions in one flat table per end flag, 128 slots per state (ASCII),
385
+ // -2 for not computed yet, -1 for overflow; `flags` holds isMatch (1) and
386
+ // dead (2) per state. The loop below then reads two typed arrays per
387
+ // character instead of a state object and the tables hanging off it. The
388
+ // tables grow with the states, so a pattern with few states stays small.
389
+ // State ids stay below MAX_STATES, so 16 bits hold them and the two
390
+ // sentinels: half the memory of the per-state tables this replaced.
391
+ let cap = 0
392
+ let trans = new Int16Array(0)
393
+ let transEnd = new Int16Array(0)
394
+ let flags = new Uint8Array(0)
395
+ function grow () {
396
+ const next = cap === 0 ? 2 : cap * 2
397
+ const t = new Int16Array(next * 128).fill(-2); t.set(trans); trans = t
398
+ const te = new Int16Array(next * 128).fill(-2); te.set(transEnd); transEnd = te
399
+ const f = new Uint8Array(next); f.set(flags); flags = f
400
+ cap = next
401
+ }
384
402
 
385
403
  function closure (list, count, pc, atStart, atEnd) {
386
404
  // Same walk as addThread, with the position replaced by the two flags.
@@ -411,7 +429,9 @@ function __ataSafeRegex () {
411
429
  let isMatch = false
412
430
  for (let k = 0; k < pcs.length; k++) if (ops[pcs[k]] === OP_MATCH) { isMatch = true; break }
413
431
  // `dead`: an anchored match with no thread left cannot recover.
414
- states.push({ pcs: Int32Array.from(pcs), isMatch, dead: anchored && pcs.length === 0, next: new Int32Array(128).fill(-2), nextEnd: new Int32Array(128).fill(-2) })
432
+ states.push({ pcs: Int32Array.from(pcs) })
433
+ if (id >= cap) grow()
434
+ flags[id] = (isMatch ? 1 : 0) | (anchored && pcs.length === 0 ? 2 : 0)
415
435
  stateIds.set(key, id)
416
436
  return id
417
437
  }
@@ -452,37 +472,36 @@ function __ataSafeRegex () {
452
472
  id = startNonEmpty
453
473
  }
454
474
  if (id < 0) return testNFA(s)
455
- let state = states[id]
456
- if (len === 0) return state.isMatch
475
+ if (len === 0) return (flags[id] & 1) === 1
457
476
  // The last character is stepped with the end-of-input flag set and is
458
477
  // handled after the loop, so the loop body selects no table and tests
459
478
  // no position.
460
479
  const last = len - 1
461
480
  for (let pos = 0; pos < last; pos++) {
462
- if (state.isMatch) return true
481
+ if ((flags[id] & 1) === 1) return true
463
482
  const c = s.charCodeAt(pos)
464
483
  let nid
465
484
  if (c < 128) {
466
- nid = state.next[c]
467
- if (nid === -2) { nid = step(state, c, false); state.next[c] = nid }
485
+ nid = trans[(id << 7) | c]
486
+ if (nid === -2) { nid = step(states[id], c, false); if (nid >= 0 || overflow) trans[(id << 7) | c] = nid }
468
487
  } else {
469
- nid = step(state, c, false)
488
+ nid = step(states[id], c, false)
470
489
  }
471
490
  if (nid < 0) return testNFA(s)
472
- state = states[nid]
473
- if (state.dead) return false
491
+ id = nid
492
+ if ((flags[id] & 2) === 2) return false
474
493
  }
475
- if (state.isMatch) return true
494
+ if ((flags[id] & 1) === 1) return true
476
495
  const c = s.charCodeAt(last)
477
496
  let nid
478
497
  if (c < 128) {
479
- nid = state.nextEnd[c]
480
- if (nid === -2) { nid = step(state, c, true); state.nextEnd[c] = nid }
498
+ nid = transEnd[(id << 7) | c]
499
+ if (nid === -2) { nid = step(states[id], c, true); if (nid >= 0 || overflow) transEnd[(id << 7) | c] = nid }
481
500
  } else {
482
- nid = step(state, c, true)
501
+ nid = step(states[id], c, true)
483
502
  }
484
503
  if (nid < 0) return testNFA(s)
485
- return states[nid].isMatch
504
+ return (flags[nid] & 1) === 1
486
505
  }
487
506
 
488
507
  return function test (s) {
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.51.0';
10
+ module.exports = '1.52.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.51.0",
3
+ "version": "1.52.0",
4
4
  "description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
5
5
  "type": "commonjs",
6
6
  "main": "index.js",
@@ -24,6 +24,7 @@
24
24
  "require": "./index.js"
25
25
  },
26
26
  "import": {
27
+ "module": "./index.bundle.mjs",
27
28
  "module-sync": "./index.node.mjs",
28
29
  "default": "./index.mjs"
29
30
  },
@@ -137,6 +138,7 @@
137
138
  "index.js",
138
139
  "index.mjs",
139
140
  "index.node.mjs",
141
+ "index.bundle.mjs",
140
142
  "index.browser.mjs",
141
143
  "index.edge.js",
142
144
  "index.edge.mjs",
@@ -172,13 +174,13 @@
172
174
  "LICENSE"
173
175
  ],
174
176
  "optionalDependencies": {
175
- "@ata-validator/native-darwin-arm64": "1.51.0",
176
- "@ata-validator/native-darwin-x64": "1.51.0",
177
- "@ata-validator/native-linux-arm64-gnu": "1.51.0",
178
- "@ata-validator/native-linux-arm64-musl": "1.51.0",
179
- "@ata-validator/native-linux-x64-gnu": "1.51.0",
180
- "@ata-validator/native-linux-x64-musl": "1.51.0",
181
- "@ata-validator/native-win32-x64": "1.51.0"
177
+ "@ata-validator/native-darwin-arm64": "1.52.0",
178
+ "@ata-validator/native-darwin-x64": "1.52.0",
179
+ "@ata-validator/native-linux-arm64-gnu": "1.52.0",
180
+ "@ata-validator/native-linux-arm64-musl": "1.52.0",
181
+ "@ata-validator/native-linux-x64-gnu": "1.52.0",
182
+ "@ata-validator/native-linux-x64-musl": "1.52.0",
183
+ "@ata-validator/native-win32-x64": "1.52.0"
182
184
  },
183
185
  "peerDependencies": {
184
186
  "yaml": "^2.0.0"