ata-validator 1.63.0 → 1.63.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -247,6 +247,7 @@ function enumMember(P, data) {
247
247
  // JSON equality: the same function the code generator emits as `_deq`, so the
248
248
  // engines agree on what equal means (an object's own keys, in any order).
249
249
  const deepEqual = _deq;
250
+ const propEnum = Object.prototype.propertyIsEnumerable;
250
251
 
251
252
  function codePointLength(s) {
252
253
  let n = 0;
@@ -369,6 +370,60 @@ function isObjectMap(v) {
369
370
  // per section so eval() can skip whole blocks without touching the schema
370
371
  // object. Plans have a fixed shape, which keeps eval() monomorphic where the
371
372
  // raw schema objects are not.
373
+ // The keyword values of a schema node, from one pass over its enumerable
374
+ // names (its own, as JSON gives them), in a record of one shape for link().
375
+ const KEYWORD_RECORD = () => ({ '$ref': undefined, '$dynamicRef': undefined, type: undefined, enum: undefined, const: undefined, minimum: undefined, maximum: undefined, exclusiveMinimum: undefined, exclusiveMaximum: undefined, multipleOf: undefined, minLength: undefined, maxLength: undefined, pattern: undefined, format: undefined, minItems: undefined, maxItems: undefined, uniqueItems: undefined, prefixItems: undefined, items: undefined, contains: undefined, minContains: undefined, maxContains: undefined, required: undefined, minProperties: undefined, maxProperties: undefined, dependentRequired: undefined, propertyNames: undefined, properties: undefined, patternProperties: undefined, additionalProperties: undefined, dependentSchemas: undefined, propertyDependencies: undefined, allOf: undefined, anyOf: undefined, oneOf: undefined, not: undefined, if: undefined, then: undefined, else: undefined, unevaluatedProperties: undefined, unevaluatedItems: undefined });
376
+ function readKeywords (schema) {
377
+ const S = KEYWORD_RECORD();
378
+ for (const k in schema) {
379
+ const v = schema[k];
380
+ switch (k) {
381
+ case '$ref': S['$ref'] = v; break;
382
+ case '$dynamicRef': S['$dynamicRef'] = v; break;
383
+ case 'type': S.type = v; break;
384
+ case 'enum': S.enum = v; break;
385
+ case 'const': S.const = v; break;
386
+ case 'minimum': S.minimum = v; break;
387
+ case 'maximum': S.maximum = v; break;
388
+ case 'exclusiveMinimum': S.exclusiveMinimum = v; break;
389
+ case 'exclusiveMaximum': S.exclusiveMaximum = v; break;
390
+ case 'multipleOf': S.multipleOf = v; break;
391
+ case 'minLength': S.minLength = v; break;
392
+ case 'maxLength': S.maxLength = v; break;
393
+ case 'pattern': S.pattern = v; break;
394
+ case 'format': S.format = v; break;
395
+ case 'minItems': S.minItems = v; break;
396
+ case 'maxItems': S.maxItems = v; break;
397
+ case 'uniqueItems': S.uniqueItems = v; break;
398
+ case 'prefixItems': S.prefixItems = v; break;
399
+ case 'items': S.items = v; break;
400
+ case 'contains': S.contains = v; break;
401
+ case 'minContains': S.minContains = v; break;
402
+ case 'maxContains': S.maxContains = v; break;
403
+ case 'required': S.required = v; break;
404
+ case 'minProperties': S.minProperties = v; break;
405
+ case 'maxProperties': S.maxProperties = v; break;
406
+ case 'dependentRequired': S.dependentRequired = v; break;
407
+ case 'propertyNames': S.propertyNames = v; break;
408
+ case 'properties': S.properties = v; break;
409
+ case 'patternProperties': S.patternProperties = v; break;
410
+ case 'additionalProperties': S.additionalProperties = v; break;
411
+ case 'dependentSchemas': S.dependentSchemas = v; break;
412
+ case 'propertyDependencies': S.propertyDependencies = v; break;
413
+ case 'allOf': S.allOf = v; break;
414
+ case 'anyOf': S.anyOf = v; break;
415
+ case 'oneOf': S.oneOf = v; break;
416
+ case 'not': S.not = v; break;
417
+ case 'if': S.if = v; break;
418
+ case 'then': S.then = v; break;
419
+ case 'else': S.else = v; break;
420
+ case 'unevaluatedProperties': S.unevaluatedProperties = v; break;
421
+ case 'unevaluatedItems': S.unevaluatedItems = v; break;
422
+ }
423
+ }
424
+ return S;
425
+ }
426
+
372
427
  class Plan {
373
428
  // The plan is registered in the cache before `link` runs, so a schema
374
429
  // object that contains itself structurally resolves to the same plan
@@ -379,6 +434,13 @@ class Plan {
379
434
 
380
435
  link(interp) {
381
436
  const schema = this.schema;
437
+ // The keywords read once, by one pass over the schema's own names, into
438
+ // a record of one shape: read by name on the schema, each absent keyword
439
+ // was a lookup that missed on an object of yet another shape, and with
440
+ // many schemas in a process the 41 reads cost 3.7 us a node against 8 ns
441
+ // for the pass (measured on 64 shapes). Reads on the record hit one
442
+ // shape every time.
443
+ const S = readKeywords(schema);
382
444
  // Children are linked as plans (or booleans) up front, so a visit never
383
445
  // goes back through the plan cache. Reference targets are resolved at
384
446
  // evaluation time and cached per base URI.
@@ -387,21 +449,21 @@ class Plan {
387
449
  this.refBase0 = undefined;
388
450
  this.refRes0 = null;
389
451
  this.dynCache = null;
390
- this.ref = typeof schema.$ref === 'string' ? schema.$ref : null;
391
- this.dynamicRef = typeof schema.$dynamicRef === 'string' ? schema.$dynamicRef : null;
452
+ this.ref = typeof S.$ref === 'string' ? S.$ref : null;
453
+ this.dynamicRef = typeof S.$dynamicRef === 'string' ? S.$dynamicRef : null;
392
454
  this.tracked = this.ref !== null || this.dynamicRef !== null;
393
455
 
394
456
  this.typeMask = 0;
395
457
  this.typeNames = null;
396
- if (schema.type !== undefined) {
397
- const types = Array.isArray(schema.type) ? schema.type : [schema.type];
458
+ if (S.type !== undefined) {
459
+ const types = Array.isArray(S.type) ? S.type : [S.type];
398
460
  let mask = 0;
399
461
  for (const t of types) mask |= typeBit(t);
400
462
  this.typeMask = mask;
401
463
  this.typeNames = types;
402
464
  }
403
- this.hasType = schema.type !== undefined;
404
- this.enum = schema.enum !== undefined ? schema.enum : null;
465
+ this.hasType = S.type !== undefined;
466
+ this.enum = S.enum !== undefined ? S.enum : null;
405
467
  // Membership for enumMember(): the primitive values in a Set, so a long
406
468
  // enum answers in one lookup, and the object and array values, which only
407
469
  // deepEqual can compare, in a list. Scanning every value with deepEqual
@@ -417,28 +479,28 @@ class Plan {
417
479
  else if (!(typeof v === 'number' && v !== v)) this.enumPrims.add(v);
418
480
  }
419
481
  }
420
- this.hasConst = schema.const !== undefined;
421
- this.const = schema.const;
422
-
423
- this.minimum = typeof schema.minimum === 'number' ? schema.minimum : undefined;
424
- this.maximum = typeof schema.maximum === 'number' ? schema.maximum : undefined;
425
- this.exclusiveMinimum = typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum : undefined;
426
- this.exclusiveMaximum = typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum : undefined;
427
- this.multipleOf = typeof schema.multipleOf === 'number' ? schema.multipleOf : undefined;
482
+ this.hasConst = S.const !== undefined;
483
+ this.const = S.const;
484
+
485
+ this.minimum = typeof S.minimum === 'number' ? S.minimum : undefined;
486
+ this.maximum = typeof S.maximum === 'number' ? S.maximum : undefined;
487
+ this.exclusiveMinimum = typeof S.exclusiveMinimum === 'number' ? S.exclusiveMinimum : undefined;
488
+ this.exclusiveMaximum = typeof S.exclusiveMaximum === 'number' ? S.exclusiveMaximum : undefined;
489
+ this.multipleOf = typeof S.multipleOf === 'number' ? S.multipleOf : undefined;
428
490
  this.hasNumber = this.minimum !== undefined || this.maximum !== undefined || this.exclusiveMinimum !== undefined || this.exclusiveMaximum !== undefined || this.multipleOf !== undefined;
429
491
 
430
- this.minLength = schema.minLength;
431
- this.maxLength = schema.maxLength;
432
- this.pattern = schema.pattern !== undefined ? interp.pattern(schema.pattern) : null;
433
- this.patternSource = schema.pattern;
434
- this.format = schema.format;
492
+ this.minLength = S.minLength;
493
+ this.maxLength = S.maxLength;
494
+ this.pattern = S.pattern !== undefined ? interp.pattern(S.pattern) : null;
495
+ this.patternSource = S.pattern;
496
+ this.format = S.format;
435
497
  this.formatFn = null;
436
- if (schema.format !== undefined) {
498
+ if (S.format !== undefined) {
437
499
  // Own properties only, as the generated code reads them: an inherited
438
500
  // Object member ('hasOwnProperty', 'constructor') is not a check.
439
501
  const own = Object.prototype.hasOwnProperty;
440
502
  const uf = interp.userFormats;
441
- const f = schema.format;
503
+ const f = S.format;
442
504
  const table = require('./formats').formatChecks();
443
505
  const fc = (uf && typeof f === 'string' && own.call(uf, f) && typeof uf[f] === 'function') ? uf[f]
444
506
  : (typeof f === 'string' && own.call(table, f) && typeof table[f] === 'function' ? table[f] : null);
@@ -448,31 +510,31 @@ class Plan {
448
510
  // A format compat registered for numbers (`type: 'number'`), checked on
449
511
  // numbers as the reference checks it.
450
512
  const nf = interp.numberFormats;
451
- this.numFormatFn = schema.format !== undefined && nf !== null && typeof nf[schema.format] === 'function' ? nf[schema.format] : null;
513
+ this.numFormatFn = S.format !== undefined && nf !== null && typeof nf[S.format] === 'function' ? nf[S.format] : null;
452
514
  if (this.numFormatFn !== null) this.hasNumber = true;
453
515
 
454
- this.minItems = schema.minItems;
455
- this.maxItems = schema.maxItems;
456
- this.uniqueItems = schema.uniqueItems === true;
457
- this.prefixItems = Array.isArray(schema.prefixItems) ? schema.prefixItems.map(child) : null;
458
- this.items = schema.items !== undefined ? child(schema.items) : undefined;
459
- this.contains = schema.contains !== undefined ? child(schema.contains) : undefined;
460
- this.minContains = schema.minContains;
461
- this.maxContains = schema.maxContains;
516
+ this.minItems = S.minItems;
517
+ this.maxItems = S.maxItems;
518
+ this.uniqueItems = S.uniqueItems === true;
519
+ this.prefixItems = Array.isArray(S.prefixItems) ? S.prefixItems.map(child) : null;
520
+ this.items = S.items !== undefined ? child(S.items) : undefined;
521
+ this.contains = S.contains !== undefined ? child(S.contains) : undefined;
522
+ this.minContains = S.minContains;
523
+ this.maxContains = S.maxContains;
462
524
  this.hasArray = this.minItems !== undefined || this.maxItems !== undefined || this.uniqueItems || this.prefixItems !== null || this.items !== undefined || this.contains !== undefined;
463
525
 
464
- this.required = Array.isArray(schema.required) ? schema.required : null;
465
- this.minProperties = schema.minProperties;
466
- this.maxProperties = schema.maxProperties;
467
- this.dependentRequired = isObjectMap(schema.dependentRequired) ? Object.entries(schema.dependentRequired) : null;
468
- this.propertyNames = schema.propertyNames !== undefined ? child(schema.propertyNames) : undefined;
526
+ this.required = Array.isArray(S.required) ? S.required : null;
527
+ this.minProperties = S.minProperties;
528
+ this.maxProperties = S.maxProperties;
529
+ this.dependentRequired = isObjectMap(S.dependentRequired) ? Object.entries(S.dependentRequired) : null;
530
+ this.propertyNames = S.propertyNames !== undefined ? child(S.propertyNames) : undefined;
469
531
  this.properties = null;
470
- if (isObjectMap(schema.properties)) {
532
+ if (isObjectMap(S.properties)) {
471
533
  this.properties = new Map();
472
- for (const key of Object.keys(schema.properties)) {
534
+ for (const key of Object.keys(S.properties)) {
473
535
  const ek = escapePointer(key);
474
536
  // Escaped once here; the walk only concatenates.
475
- this.properties.set(key, { node: child(schema.properties[key]), seg: '/' + ek, schemaSeg: '/properties/' + ek });
537
+ this.properties.set(key, { node: child(S.properties[key]), seg: '/' + ek, schemaSeg: '/properties/' + ek });
476
538
  }
477
539
  }
478
540
  // A required key holding undefined is missing. Where the key is also a
@@ -491,26 +553,26 @@ class Plan {
491
553
  }
492
554
  }
493
555
  this.patternProperties = null;
494
- if (isObjectMap(schema.patternProperties)) {
495
- this.patternProperties = Object.keys(schema.patternProperties).map((src) => ({ src, re: interp.pattern(src), node: child(schema.patternProperties[src]) }));
556
+ if (isObjectMap(S.patternProperties)) {
557
+ this.patternProperties = Object.keys(S.patternProperties).map((src) => ({ src, re: interp.pattern(src), node: child(S.patternProperties[src]) }));
496
558
  }
497
- this.additionalProperties = schema.additionalProperties !== undefined ? child(schema.additionalProperties) : undefined;
498
- this.dependentSchemas = isObjectMap(schema.dependentSchemas) ? Object.entries(schema.dependentSchemas).map(([k, v]) => [k, child(v)]) : null;
559
+ this.additionalProperties = S.additionalProperties !== undefined ? child(S.additionalProperties) : undefined;
560
+ this.dependentSchemas = isObjectMap(S.dependentSchemas) ? Object.entries(S.dependentSchemas).map(([k, v]) => [k, child(v)]) : null;
499
561
  this.propertyDependencies = null;
500
- if (isObjectMap(schema.propertyDependencies)) {
501
- this.propertyDependencies = Object.entries(schema.propertyDependencies)
562
+ if (isObjectMap(S.propertyDependencies)) {
563
+ this.propertyDependencies = Object.entries(S.propertyDependencies)
502
564
  .filter(([, choices]) => isObjectMap(choices))
503
565
  .map(([k, choices]) => [k, new Map(Object.keys(choices).map((v) => [v, child(choices[v])]))]);
504
566
  }
505
567
  this.hasObject = this.required !== null || this.minProperties !== undefined || this.maxProperties !== undefined || this.dependentRequired !== null || this.propertyNames !== undefined || this.properties !== null || this.patternProperties !== null || this.additionalProperties !== undefined || this.dependentSchemas !== null || this.propertyDependencies !== null;
506
568
 
507
- this.allOf = Array.isArray(schema.allOf) ? schema.allOf.map(child) : null;
508
- this.anyOf = Array.isArray(schema.anyOf) ? schema.anyOf.map(child) : null;
509
- this.oneOf = Array.isArray(schema.oneOf) ? schema.oneOf.map(child) : null;
510
- this.not = schema.not !== undefined ? child(schema.not) : undefined;
511
- this.if = schema.if !== undefined ? child(schema.if) : undefined;
512
- this.then = schema.then !== undefined ? child(schema.then) : undefined;
513
- this.else = schema.else !== undefined ? child(schema.else) : undefined;
569
+ this.allOf = Array.isArray(S.allOf) ? S.allOf.map(child) : null;
570
+ this.anyOf = Array.isArray(S.anyOf) ? S.anyOf.map(child) : null;
571
+ this.oneOf = Array.isArray(S.oneOf) ? S.oneOf.map(child) : null;
572
+ this.not = S.not !== undefined ? child(S.not) : undefined;
573
+ this.if = S.if !== undefined ? child(S.if) : undefined;
574
+ this.then = S.then !== undefined ? child(S.then) : undefined;
575
+ this.else = S.else !== undefined ? child(S.else) : undefined;
514
576
  // Macro keywords: the schema the macro returns is applied in place, the
515
577
  // way the reference validator does it, so its errors carry the keyword's
516
578
  // path and the keyword adds an error of its own when that schema fails.
@@ -527,13 +589,13 @@ class Plan {
527
589
  }
528
590
  this.hasApplicators = this.allOf !== null || this.anyOf !== null || this.oneOf !== null || this.not !== undefined || this.if !== undefined || this.macros !== null;
529
591
 
530
- this.unevaluatedProperties = schema.unevaluatedProperties !== undefined ? child(schema.unevaluatedProperties) : undefined;
531
- this.unevaluatedItems = schema.unevaluatedItems !== undefined ? child(schema.unevaluatedItems) : undefined;
592
+ this.unevaluatedProperties = S.unevaluatedProperties !== undefined ? child(S.unevaluatedProperties) : undefined;
593
+ this.unevaluatedItems = S.unevaluatedItems !== undefined ? child(S.unevaluatedItems) : undefined;
532
594
  // A `false` here rejects the member outright, and the error belongs to this
533
595
  // keyword on this object, not to a boolean schema that happens to sit
534
596
  // under it. Recorded at compile time so the check below is one test.
535
- this.unevaluatedPropertiesFalse = schema.unevaluatedProperties === false;
536
- this.unevaluatedItemsFalse = schema.unevaluatedItems === false;
597
+ this.unevaluatedPropertiesFalse = S.unevaluatedProperties === false;
598
+ this.unevaluatedItemsFalse = S.unevaluatedItems === false;
537
599
  this.hasUnevaluated = this.unevaluatedProperties !== undefined || this.unevaluatedItems !== undefined;
538
600
 
539
601
  // Custom keywords present on this node, in schema order. Each op holds
@@ -1251,9 +1313,15 @@ class Interpreter {
1251
1313
  }
1252
1314
  }
1253
1315
 
1254
- // objects
1316
+ // objects. The object's own names, enumerable or not: a declared
1317
+ // property is read by name, as the generated code reads it, so one
1318
+ // hidden under its name is checked; patternProperties, propertyNames
1319
+ // and additionalProperties see the names for-in sees, as there too, and
1320
+ // minProperties and maxProperties count those.
1255
1321
  if (P.hasObject && bits === T_OBJECT) {
1256
- const keys = Object.keys(data);
1322
+ const keys = Object.getOwnPropertyNames(data);
1323
+ let hidden = 0;
1324
+ if (P.minProperties !== undefined || P.maxProperties !== undefined) for (let k = 0; k < keys.length; k++) if (!propEnum.call(data, keys[k])) hidden++;
1257
1325
  if (P.required !== null) {
1258
1326
  const req = P.required;
1259
1327
  for (let i = 0; i < req.length; i++) {
@@ -1261,8 +1329,8 @@ class Interpreter {
1261
1329
  if (!Object.hasOwn(data, key) || (P.requiredUndef !== null && P.requiredUndef[i] && data[key] === undefined)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
1262
1330
  }
1263
1331
  }
1264
- if (P.minProperties !== undefined && keys.length < P.minProperties) { errors === NOERRORS || errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: P.minProperties }, `must NOT have fewer than ${P.minProperties} properties`)); valid = false; }
1265
- if (P.maxProperties !== undefined && keys.length > P.maxProperties) { errors === NOERRORS || errors.push(err('maxProperties', 'maxProperties', instancePath, schemaPath + '/maxProperties', { limit: P.maxProperties }, `must NOT have more than ${P.maxProperties} properties`)); valid = false; }
1332
+ if (P.minProperties !== undefined && keys.length - hidden < P.minProperties) { errors === NOERRORS || errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: P.minProperties }, `must NOT have fewer than ${P.minProperties} properties`)); valid = false; }
1333
+ if (P.maxProperties !== undefined && keys.length - hidden > P.maxProperties) { errors === NOERRORS || errors.push(err('maxProperties', 'maxProperties', instancePath, schemaPath + '/maxProperties', { limit: P.maxProperties }, `must NOT have more than ${P.maxProperties} properties`)); valid = false; }
1266
1334
  if (P.dependentRequired !== null) {
1267
1335
  for (const [key, deps] of P.dependentRequired) {
1268
1336
  if (Object.hasOwn(data, key)) {
@@ -1274,6 +1342,7 @@ class Interpreter {
1274
1342
  }
1275
1343
  if (P.propertyNames !== undefined) {
1276
1344
  for (const key of keys) {
1345
+ if (!propEnum.call(data, key)) continue;
1277
1346
  if (!this.eval(P.propertyNames, key, base, dynScope, errors, instancePath, E ? schemaPath + '/propertyNames' : '', stack, DISCARD)) valid = false;
1278
1347
  }
1279
1348
  }
@@ -1283,6 +1352,7 @@ class Interpreter {
1283
1352
  const key = keys[k];
1284
1353
  let evaluated = false;
1285
1354
  const prop = props !== null ? props.get(key) : undefined;
1355
+ const seen = prop !== undefined && patterns === null && P.additionalProperties === undefined || propEnum.call(data, key);
1286
1356
  if (prop !== undefined) {
1287
1357
  // A declared property whose value is undefined is not checked, as the
1288
1358
  // generated code does not check it: no JSON document carries one, and
@@ -1294,7 +1364,7 @@ class Interpreter {
1294
1364
  } else if (P.requiredInProps !== null && P.requiredInProps.has(key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
1295
1365
  evaluated = true;
1296
1366
  }
1297
- if (patterns !== null) {
1367
+ if (patterns !== null && seen) {
1298
1368
  for (let pi = 0; pi < patterns.length; pi++) {
1299
1369
  const pp = patterns[pi];
1300
1370
  if (pp.re.test(key)) {
@@ -1303,7 +1373,7 @@ class Interpreter {
1303
1373
  }
1304
1374
  }
1305
1375
  }
1306
- if (!evaluated && P.additionalProperties !== undefined) {
1376
+ if (!evaluated && seen && P.additionalProperties !== undefined) {
1307
1377
  // `additionalProperties: false` keeps its own keyword and names the
1308
1378
  // property it turned away. Running it as a plain false subschema
1309
1379
  // reported `not` at the key's path, which drops both.
package/lib/ir/emit.mjs CHANGED
@@ -44,7 +44,14 @@ function inline (fn, vExpr, nExpr) {
44
44
  }
45
45
 
46
46
  // How many names an object must list before a module visits the object's own
47
- // names instead of looking each one up.
47
+ // names instead of looking each one up. Measured on SchemaStore's samples,
48
+ // warmed, against the compiled modules: from 16 names the geometric mean is
49
+ // 0.878 with 23 schemas under half the time, from 32 it is 0.914 with 15,
50
+ // from 64 0.941 with 8, and the count slower by a fifth is 47 at each. A
51
+ // dense document under a short list (sil-kit, csslintrc: most of 16 to 38
52
+ // names present) pays the walk's array and dispatch, 2 to 4x the direct
53
+ // reads; a sparse one under a long list (putout, 475 names) would pay a
54
+ // read per name, 100x the walk.
48
55
  const WALK_MIN = 16
49
56
  // Source characters past which a verdict body is its own function (the
50
57
  // verdict threshold of the current engine's generator, SPLIT_PROP_SCHEMA).
@@ -96,14 +103,90 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
96
103
  use(fn)
97
104
  return fn
98
105
  }
106
+ // An own-property test written in the module with the module's own
107
+ // reference to hasOwnProperty. The kernels' own() is one function for every
108
+ // module of a page, so its property lookup met the objects of every schema
109
+ // and stopped being cached for any (rc3-environment alone 1.0x the compiled
110
+ // module, beside 438 other schemas 2.1x).
111
+ let HOP = null
112
+ const ownSrc = (o, k) => {
113
+ if (HOP === null) HOP = constOf('Object.prototype.hasOwnProperty')
114
+ return `${HOP}.call(${o},${k})`
115
+ }
116
+ // A small verdict written in at its call, as the compiled modules write a
117
+ // referenced schema in at each property: every copy keeps its own record of
118
+ // the shapes it meets, where one function called from forty places met all
119
+ // of them and V8 stopped inlining it (coffeelint, 38 properties naming one
120
+ // definition). Only a verdict with no cycle through it, no guard and no
121
+ // dynamic scope, whose body is INLINE_BODY characters or fewer; a verdict
122
+ // body leaves only by `return false`, so it runs as a block. Its parameters
123
+ // are bound in the block: the value, and the empty paths and list of a
124
+ // verdict call.
125
+ const INLINE_BODY = 500
126
+ // Inlined source one function may take in all: past it a function grows
127
+ // beyond what V8 optimizes (putout's 475 properties, 0.01x the compiled
128
+ // module before, 1.08x with every call written in).
129
+ // 2000 left coffeelint's 38 names calling one 300-character check (2.2x
130
+ // the compiled module); 8000 inlines them and keeps putout's 475-name
131
+ // function optimizable (both measured on SchemaStore, warmed).
132
+ const INLINE_BUDGET = 8000
133
+ let inlineBudget = INLINE_BUDGET
134
+ const inlining = new Set()
135
+ const inlineBodies = new Map()
136
+ const inlineCall = (c) => {
137
+ if (c.proc === null || c.dynamic || c.guard || ir.dynamic || inlining.has(c.proc)) return null
138
+ const p = ir.procs.get(c.proc)
139
+ if (!p || p.mode !== 'verdict') return null
140
+ // Follow a verdict that is only a call of another (procName's alias).
141
+ const b = p.body.t === 'seq' && p.body.body.length === 1 ? p.body.body[0] : p.body
142
+ if (b.t === 'require' && b.e.t === 'pcall' && b.e.e === 'null' && !b.e.dynamic && !b.e.guard && b.e.x.t === 'var' && b.e.x.n === 'd' && b.e.proc !== c.proc) return inlineCall({ ...b.e, x: c.x })
143
+ // Each procedure is written once, with a full budget for what it
144
+ // inlines itself, and that text is its size: a site inlines it when the
145
+ // text fits the site's remaining budget. Written at every call site
146
+ // under the site's budget, the text depended on the order of the sites,
147
+ // and a definition too large under a deep nesting was rendered again
148
+ // for each site below each site (a suite schema took ten minutes).
149
+ let body = inlineBodies.get(c.proc)
150
+ if (body === undefined) {
151
+ inlining.add(c.proc); const saved = inlineBudget; inlineBudget = INLINE_BUDGET
152
+ try { body = stmt(p.body, true) } finally { inlining.delete(c.proc); inlineBudget = saved }
153
+ if (body.length > INLINE_BODY || /\breturn true\b/.test(body)) body = null
154
+ inlineBodies.set(c.proc, body)
155
+ }
156
+ if (body === null || body.length > inlineBudget) return null
157
+ inlineBudget -= body.length
158
+ const x = expr(c.x); const t = 'i' + (++inlineN)
159
+ return `{const ${t}=${x}\n{const d=${t},V=${t},e=null,P="",SP="",A=null\n${body}}}\n`
160
+ }
161
+ let inlineN = 0
162
+ // The verdict calls already made in the row of statements being written
163
+ // (seq), handed to a row nested directly in it.
164
+ let seqSeen = null
165
+ // The plain-prototype flags a fill has bound, by the object's expression.
166
+ const fillPlain = new Map()
99
167
  const procName = (id) => {
100
168
  if (emitted.has(id)) return emitted.get(id)
101
169
  const p = ir.procs.get(id)
170
+ // A verdict that is one call of another on its own value (a branch that
171
+ // is only a $ref) is that other verdict: no function of its own, and a
172
+ // caller that already ran it can see the repeat (seq).
173
+ if (p.mode === 'verdict' && !ir.dynamic) {
174
+ const b = p.body.t === 'seq' && p.body.body.length === 1 ? p.body.body[0] : p.body
175
+ if (b.t === 'require' && b.e.t === 'pcall' && b.e.e === 'null' && !b.e.dynamic && !b.e.guard && b.e.x.t === 'var' && b.e.x.n === 'd' && b.e.proc !== id) {
176
+ const to = procName(b.e.proc)
177
+ emitted.set(id, to)
178
+ return to
179
+ }
180
+ }
102
181
  const name = id
103
182
  emitted.set(id, name)
104
183
  const isVerdict = p.mode === 'verdict'
105
- if (p.mode === 'fill') { fns.push(`function ${name}(d){\n${stmt(p.body, true)}}`); return name }
106
- const body = stmt(p.body, isVerdict)
184
+ if (p.mode === 'fill') { fillPlain.clear(); fns.push(`function ${name}(d){\n${stmt(p.body, true)}}`); return name }
185
+ // Each function written gets its own budget of inlined source (inlineCall).
186
+ const savedBudget = inlineBudget
187
+ inlineBudget = INLINE_BUDGET
188
+ let body
189
+ try { body = stmt(p.body, isVerdict) } finally { inlineBudget = savedBudget }
107
190
  const head = p.mode === 'ann' ? 'if(A===null)A=newAnn()\n' : ''
108
191
  if (p.mode === 'ann') use('newAnn')
109
192
  const wrapped = ir.dynamic
@@ -213,7 +296,23 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
213
296
  const ap = walked && n.body.find((c) => c.t === 'if' && c.then.t === 'keys' && c.then.apClosed && c.then.obj.t === 'var' && c.then.obj.n === walked.then.x.n)
214
297
  if (ap) { skip = ap; folded.add(walked.then) }
215
298
  }
216
- return n.body.filter((x) => x !== skip).map((x) => stmt(x, v)).join('')
299
+ // In a verdict the same check of the same value twice in a row of
300
+ // statements is once: the first returned false or passed, and a
301
+ // verdict has no side effects (custom keywords and formats, which
302
+ // could, are refused above). A schema that names one definition
303
+ // under both allOf and anyOf ran it twice per item (devinit).
304
+ // A row nested directly in a row runs unconditionally after what came
305
+ // before it, so it shares the set; anything under a condition, a
306
+ // loop or a let starts its own.
307
+ const seen = v ? (seqSeen || new Set()) : null
308
+ seqSeen = null
309
+ return n.body.filter((x) => x !== skip).map((x) => {
310
+ seqSeen = x.t === 'seq' ? seen : null
311
+ const t = stmt(x, v)
312
+ seqSeen = null
313
+ if (seen !== null && (x.t === 'require' || x.t === 'branches')) { if (seen.has(t)) return ''; seen.add(t) }
314
+ return t
315
+ }).join('')
217
316
  }
218
317
  // An object that lists many names: visit the object's own names, every
219
318
  // one enumerable or not (an entry checks its own presence, so a name it
@@ -235,7 +334,14 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
235
334
  const rows = n.keyed.map(([p, i]) => {
236
335
  const e = n.body[i]
237
336
  const c = v ? verdictCall(e) : null
238
- if (c !== null) return { p, fn: procName(c.proc) }
337
+ if (c !== null) {
338
+ // A small check written in at its name (inlineCall), as the
339
+ // compiled modules write a referenced definition at each
340
+ // property: csslintrc's 31 names each called one enum check.
341
+ const t = inlineCall({ ...c, x: { t: 'var', n: 'y' } })
342
+ if (t !== null) return { p, inl: t }
343
+ return { p, fn: procName(c.proc) }
344
+ }
239
345
  if (v && e.t === 'let' && e.body.t === 'if' && !e.body.else && selfContained(e.body.then, e.name)) {
240
346
  const text = stmt(e.body.then, true)
241
347
  return { p, e, text, src: `{const ${e.name}=${expr(e.value)}\nif(${expr(e.body.c)}){\n${text}}}\n` }
@@ -248,7 +354,9 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
248
354
  const small = tabled.filter((r) => (sizes.get(r.fn) ?? Infinity) <= INLINE_MAX).length
249
355
  const useTable = tabled.length >= TABLE_MIN || (tabled.length >= WALK_MIN && small * 2 < tabled.length)
250
356
  const left = rows.filter((r) => !useTable || !r.fn)
251
- const body = (r) => r.fn ? `{const y=${X}[${lit(r.p)}];if(y!==void 0&&!${r.fn}(y,null,s,"",y,"",null))return false}\n` : r.src
357
+ // In the walk a name comes from the object's own names, so its value
358
+ // is read without a presence test.
359
+ const body = (r) => r.fn ? `{const y=${X}[${lit(r.p)}];if(y!==void 0&&!${r.fn}(y,null,s,"",y,"",null))return false}\n` : r.inl ? `{const y=${X}[${lit(r.p)}];if(y!==void 0){${r.inl}}}\n` : r.src
252
360
  // A key is told from the names by its length first, a switch on an
253
361
  // integer the engine makes a jump table, and then compared with the
254
362
  // few names of that length: a switch on the names compared each key
@@ -265,9 +373,20 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
265
373
  const dispatch = (k) => `switch(${k}.length){\n${[...byLen].map(([l, rs]) => `case ${l}:${rs.map((r) => `if(${k}===${lit(r.p)}){\n${r.empty ? '' : body(r)}}`).join('else ')}${fold ? `else{${unknown}}` : ''}break\n`).join('')}${fold ? `default:${unknown}` : ''}}`
266
374
  // A name the table answers is listed; with the fold every other name
267
375
  // goes through the dispatch, which knows the listed ones.
268
- if (!useTable) return `${always}{const ks=Object.getOwnPropertyNames(${X});for(let j=0;j<ks.length;j++){const k=ks[j];${dispatch('k')}}}\n`
269
- const T = constOf(`new Map([${tabled.map((r) => `[${lit(r.p)},${r.fn}]`).join(',')}])`)
270
- return `${always}{const ks=Object.getOwnPropertyNames(${X});for(let j=0;j<ks.length;j++){const k=ks[j],f=${T}.get(k);if(f!==void 0){const y=${X}[k];if(y!==void 0&&!f(y,null,s,"",y,"",null))return false}${left.length || fold ? `else ${dispatch('k')}` : ''}}}\n`
376
+ let walk
377
+ if (!useTable) walk = `for(let j=0;j<ks.length;j++){const k=ks[j];${dispatch('k')}}`
378
+ else {
379
+ const T = constOf(`new Map([${tabled.map((r) => `[${lit(r.p)},${r.fn}]`).join(',')}])`)
380
+ walk = `for(let j=0;j<ks.length;j++){const k=ks[j],f=${T}.get(k);if(f!==void 0){const y=${X}[k];if(y!==void 0&&!f(y,null,s,"",y,"",null))return false}${left.length || fold ? `else ${dispatch('k')}` : ''}}`
381
+ }
382
+ // A self-measuring switch to a direct read of each listed name on
383
+ // dense documents was tried here and taken out: it won on five
384
+ // config schemas (csslintrc, coffeelint, jshintrc) and lost as much
385
+ // on others (liquibase 0.27 to 1.46 of the compiled module, a
386
+ // 142-name object read in dictionary mode 8x slower than the walk),
387
+ // no rule from the measurements told the two apart, and its direct
388
+ // form once rejected {} through an inherited toString.
389
+ return `${always}{const ks=Object.getOwnPropertyNames(${X});${walk}}\n`
271
390
  }
272
391
  case 'if': {
273
392
  // A condition that is a call under annotations merges its collector.
@@ -302,6 +421,13 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
302
421
  // SchemaStore's slower by 6-80% when all of them run in one process.)
303
422
  const last = n.order === 'protoLast' && !v
304
423
  if (last) use('keysOf')
424
+ // A verdict walks the object's own names with for-in, no array, each
425
+ // name tested as the object's own: a name enumerable on
426
+ // Object.prototype is not one of them (tests/test_ir_module_split.mjs).
427
+ if (v) {
428
+ const o = expr(n.obj)
429
+ return `for(const ${n.k} in ${o}){if(!${ownSrc(o, n.k)})continue\n${stmt(n.body, v)}}\n`
430
+ }
305
431
  return `for(const ${n.k} of ${last ? 'keysOf' : 'Object.keys'}(${expr(n.obj)})){\n${stmt(n.body, v)}}\n`
306
432
  }
307
433
  case 'range': return `for(let ${n.i}=${typeof n.from === 'number' ? n.from : expr(n.from)};${n.i}<${expr(n.arr)}.length;${n.i}++){\n${stmt(n.body, v)}}\n`
@@ -315,30 +441,48 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
315
441
  use('newAnn', 'mergeAnn')
316
442
  return `{const a=newAnn(),c=e===null?0:e.length;if(${callOf(c, 'e', 'a')}&&(e===null||e.length===c))mergeAnn(A,a);else if(e===null)return false}\n`
317
443
  }
444
+ if (v && c.e === 'null') { const t = inlineCall(c); if (t !== null) return t }
318
445
  if (c.e === 'inherit') return v ? `if(!${callOf(c, 'null', 'null')})return false\n` : `if(!${callOf(c, 'e', 'null')}&&e===null)return false\n`
319
446
  return `if(!${callOf(c, 'null', 'null')})${v ? 'return false' : '{if(e===null)return false}'}\n`
320
447
  }
321
448
  case 'fail': return fail(n.err, v) + '\n'
322
449
  case 'branches': return branches(n, v)
323
450
  case 'setDefault': {
324
- use('own')
325
451
  if (n.k === '__proto__') use('setOwn')
326
452
  const val = n.v
327
453
  let src
328
454
  if (typeof val !== 'object' || val === null) src = lit(val)
329
455
  else if (Array.isArray(val) && val.length === 0) src = '[]'
330
- else { use('copyOf'); src = `copyOf(${constOf(lit(val))})` }
456
+ else {
457
+ // A small default written as a literal: a fresh value each time
458
+ // it is read, as the compiled modules write `{}`. copyOf's JSON
459
+ // round trip was 72% of revola's verdict and 39% of changepacks'.
460
+ // Not when a key in it is `__proto__`, which a literal would read
461
+ // as the prototype, nor a large one, which would swell the fill.
462
+ const text = lit(val)
463
+ if (text.length <= 400 && !text.includes('"__proto__"')) src = text
464
+ else { use('copyOf'); src = `copyOf(${constOf(text)})` }
465
+ }
331
466
  const o = expr(n.o)
332
467
  // A store under the name itself: through setOwn's computed key, V8
333
468
  // turned an object given more than a few defaults into a dictionary,
334
469
  // slow from then on for every reader (htmlhint's root, 23 defaults:
335
470
  // getOwnPropertyNames 10 to 999 ns). __proto__ needs defineProperty.
336
- if (n.k === '__proto__') return `if(!own(${o},${lit(n.k)}))setOwn(${o},${lit(n.k)},${src})\n`
471
+ if (n.k === '__proto__') return `if(!${ownSrc(o, lit(n.k))})setOwn(${o},${lit(n.k)},${src})\n`
472
+ // A defined value under the name on an ordinary object is the
473
+ // object's own: the own test runs only for an undefined read, or on
474
+ // an object with another prototype, as the current engine's fill
475
+ // reads it. The flag is bound once per object in the fill.
476
+ if (!(n.k in Object.prototype)) {
477
+ let pl = fillPlain.get(o); let bind = ''
478
+ if (pl === undefined) { pl = 'pl' + fillPlain.size; fillPlain.set(o, pl); bind = `const ${pl}=(PN?${o}.__proto__:Object.getPrototypeOf(${o}))===Object.prototype\n` }
479
+ return `${bind}if((${o}[${lit(n.k)}]===void 0||!${pl})&&!${ownSrc(o, lit(n.k))})${o}[${lit(n.k)}]=${src}\n`
480
+ }
337
481
  // A read or `in` before hasOwnProperty, as the current engine's fill
338
482
  // does, was measured both ways: faster where the defaults are mostly
339
483
  // present (jshintrc 1195 to 522 ns), slower where they are mostly
340
484
  // missing (prometheus 3671 to 4601). hasOwnProperty alone is kept.
341
- return `if(!own(${o},${lit(n.k)}))${o}[${lit(n.k)}]=${src}\n`
485
+ return `if(!${ownSrc(o, lit(n.k))})${o}[${lit(n.k)}]=${src}\n`
342
486
  }
343
487
  case 'fcall': return `${procName(n.proc)}(${expr(n.x)})\n`
344
488
  case 'defaultsMask': {
@@ -371,6 +515,9 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
371
515
  const x = expr(n.x); const path = expr(n.path); const ev = expr(n.ev); const sps = n.sps.map(expr)
372
516
  if (v) {
373
517
  const calls = ps.map((p) => `${p}(${x},null,s,"",${ev},"",null)`)
518
+ // One branch passes exactly when it passes, for anyOf and oneOf: the
519
+ // form of a required call, so a repeat of it is seen (seq).
520
+ if (calls.length === 1) return `if(!${calls[0]})return false\n`
374
521
  return `if(${isAny ? `!(${calls.join('||')})` : `(${calls.map((c) => `(${c}?1:0)`).join('+')})!==1`})return false\n`
375
522
  }
376
523
  use('collapse')
@@ -412,7 +559,7 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
412
559
  case 'var': return n.n
413
560
  case 'get': return `${expr(n.o)}[${expr(n.k)}]`
414
561
  case 'field': return `${expr(n.o)}.${n.k}`
415
- case 'own': use('own'); return `own(${expr(n.o)},${expr(n.k)})`
562
+ case 'own': return ownSrc(expr(n.o), expr(n.k))
416
563
  case 'not': return `!(${expr(n.e)})`
417
564
  case 'and': return `(${expr(n.a)}&&${expr(n.b)})`
418
565
  case 'or': return `(${expr(n.a)}||${expr(n.b)})`
package/lib/ir/front.mjs CHANGED
@@ -332,6 +332,27 @@ function build (schema, { assertFormat = true, schemas = null, metaschemas = nul
332
332
  if (objKw && !knownObj) lets.push([o.n, { t: 'isObj', x }], [plain.n, and(o, { t: 'plainProto', x })])
333
333
  const keys = known !== null ? ['type', ...Object.keys(s).filter((k) => k !== 'type')] : Object.keys(s)
334
334
  const customOps = []
335
+ // A verdict with patternProperties beside additionalProperties: false
336
+ // walks the object's names once: each pattern tested once per name, a
337
+ // name no pattern matches and properties does not list fails. Two loops
338
+ // tested every pattern twice per name (minecraft-custom-main-menu-mod,
339
+ // 2.7x the compiled module). Errors keep the two loops: their order is
340
+ // the current engine's.
341
+ let patsFused = false
342
+ const fusedPats = () => {
343
+ const pats = s.patternProperties; const kv = vr(fresh('k')); const m = fresh('m')
344
+ const inner = []
345
+ for (const p in pats) {
346
+ const y = vr(fresh('x'))
347
+ const body = child(pats[p], y, catE(path, catE(lit('/'), { t: 'seg', k: kv })), cat(sp, '/patternProperties/' + seg(p)))
348
+ inner.push(iff({ t: 're', src: p, k: kv }, seq({ t: 'incr', name: m }, nop(body) ? null : { t: 'let', name: y.n, value: get(x, kv), body })))
349
+ }
350
+ const names = Object.keys(props)
351
+ inner.push(iff(and(cmp('===', vr(m), lit(0)), ...(names.length ? [not({ t: 'known', k: kv, names, pats: [] })] : []), has(null, kv)), { t: 'ret' }))
352
+ patsFused = true
353
+ return iff(o, { t: 'keys', obj: x, k: kv.n, order: 'keys', body: { t: 'count', name: m, body: seq(inner) } })
354
+ }
355
+ const canFuse = verdict && !ann && kw === null && s.additionalProperties === false && isObj(s.patternProperties)
335
356
  // Under ctx each keyword's statements go to its place in the current
336
357
  // engine's walk (lib/interpreter-core.js eval), not schema order.
337
358
  const buckets = ctx ? new Map() : null
@@ -399,6 +420,8 @@ function build (schema, { assertFormat = true, schemas = null, metaschemas = nul
399
420
  const closed = s.additionalProperties === false && !isObj(s.patternProperties) && !ann && kw === null ? Object.keys(props) : null
400
421
  if (inner.length) out.push(iff(o, { t: 'props', x, body: inner, keyed: ann || kw !== null ? null : keyed, closed }))
401
422
  } else if (k === 'patternProperties') {
423
+ if (patsFused) continue
424
+ if (canFuse) { out.push(fusedPats()); continue }
402
425
  const kv = vr(fresh('k'))
403
426
  const inner = []
404
427
  for (const p in n) {
@@ -417,6 +440,8 @@ function build (schema, { assertFormat = true, schemas = null, metaschemas = nul
417
440
  for (const a in n) inner.push(iff(own(x, lit(a)), seq(n[a].map((r) => iff(or(cmp('===', get(x, lit(r)), lit(undefined)), not(has(r, lit(r)))), fail(err('ATA7005', 'required', path, cat(sp, '/dependentRequired'), paramsOf(required.params(r)), lit(required.msg(r)), required.exp(r), required.rank, ev, required.detail, required.suggest)))))))
418
441
  out.push(iff(o, seq(inner)))
419
442
  } else if (k === 'additionalProperties') {
443
+ if (patsFused) continue
444
+ if (canFuse) { out.push(fusedPats()); continue }
420
445
  const known2 = Object.keys(s.properties || {})
421
446
  const pats = Object.keys(s.patternProperties || {})
422
447
  const kv = vr(fresh('k'))
@@ -611,8 +636,14 @@ function build (schema, { assertFormat = true, schemas = null, metaschemas = nul
611
636
  }
612
637
  }
613
638
  if (post !== null) out.push(post)
639
+ // A value nothing below reads is not computed: an item schema of a type
640
+ // alone read each array element's prototype for a flag no check used
641
+ // (bitrise-step, half its verdict).
642
+ const text = JSON.stringify(out)
643
+ for (const n of out) if (n.t === 'bind') n.names = n.names.filter(([name]) => text.includes('"n":' + JSON.stringify(name)))
644
+ out = out.filter((n) => n.t !== 'bind' || n.names.length)
614
645
  let body = seq(out)
615
- for (let i = lets.length - 1; i >= 0; i--) body = { t: 'let', name: lets[i][0], value: lets[i][1], body }
646
+ for (let i = lets.length - 1; i >= 0; i--) if (text.includes('"n":' + JSON.stringify(lets[i][0]))) body = { t: 'let', name: lets[i][0], value: lets[i][1], body }
616
647
  return body
617
648
  }
618
649
 
@@ -107,6 +107,7 @@ function install(deps) {
107
107
  function fresh() { return { props: null, n: 0, x: null, keys: null }; }
108
108
  // Object.keys once per record: every node that shares a record sees the
109
109
  // same data object.
110
+ const propEnum = Object.prototype.propertyIsEnumerable;
110
111
  function keysOf(rec, data) {
111
112
  if (rec.keys === null) rec.keys = Object.keys(data);
112
113
  return rec.keys;
@@ -609,9 +610,10 @@ function install(deps) {
609
610
  steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
610
611
  if (dataBits(data) !== T_OBJECT) return true;
611
612
  let ok = true;
612
- const keys = collect ? keysOf(rec, data) : Object.keys(data);
613
+ const keys = collect ? keysOf(rec, data) : Object.getOwnPropertyNames(data);
613
614
  if (pnFn !== null) {
614
615
  for (const k of keys) {
616
+ if (!collect && !propEnum.call(data, k)) continue;
615
617
  // propertyNames applies its subschema to the property NAME, not to
616
618
  // the value sitting at that key, so the failure is reported against
617
619
  // the object that carries the name rather than against `/<key>`.
@@ -643,7 +645,11 @@ function install(deps) {
643
645
  }
644
646
  evaluated = true;
645
647
  }
646
- if (patterns !== null) {
648
+ // A name only properties lists is read whether enumerable or not,
649
+ // as the short walk above and the generated code read it by name;
650
+ // patterns and additionalProperties see the names for-in sees.
651
+ const seen = collect || prop !== undefined && patterns === null && apFn === null || propEnum.call(data, key);
652
+ if (patterns !== null && seen) {
647
653
  for (let pi = 0; pi < patterns.length; pi++) {
648
654
  const pp = patterns[pi];
649
655
  if (pp.re.test(key)) {
@@ -655,7 +661,7 @@ function install(deps) {
655
661
  }
656
662
  }
657
663
  }
658
- if (!evaluated && apFn !== null) {
664
+ if (!evaluated && apFn !== null && seen) {
659
665
  if (apFalse) {
660
666
  ok = false;
661
667
  if (errors === NOERRORS) return false;
@@ -715,9 +721,9 @@ function install(deps) {
715
721
  vsteps.push((data, stack, rec) => {
716
722
  if (fusedLeaf !== null && !fusedLeaf(data)) return false;
717
723
  if (dataBits(data) !== T_OBJECT) return true;
718
- const keys = collect ? keysOf(rec, data) : Object.keys(data);
724
+ const keys = collect ? keysOf(rec, data) : Object.getOwnPropertyNames(data);
719
725
  if (pnFn !== null) {
720
- for (const k of keys) if (!pnFn.v(k, stack)) return false;
726
+ for (const k of keys) if ((collect || propEnum.call(data, k)) && !pnFn.v(k, stack)) return false;
721
727
  }
722
728
  let seenReq = 0;
723
729
  for (let k = 0; k < keys.length; k++) {
@@ -731,7 +737,8 @@ function install(deps) {
731
737
  if (prop.req) seenReq++;
732
738
  evaluated = true;
733
739
  }
734
- if (patterns !== null) {
740
+ const seen = collect || prop !== undefined && patterns === null && apFn === null || propEnum.call(data, key);
741
+ if (patterns !== null && seen) {
735
742
  for (let pi = 0; pi < patterns.length; pi++) {
736
743
  const pp = patterns[pi];
737
744
  if (pp.re.test(key)) {
@@ -740,7 +747,7 @@ function install(deps) {
740
747
  }
741
748
  }
742
749
  }
743
- if (!evaluated && apFn !== null) {
750
+ if (!evaluated && apFn !== null && seen) {
744
751
  if (!apFn.v(data[key], stack)) return false;
745
752
  evaluated = true;
746
753
  }
@@ -762,6 +769,13 @@ function install(deps) {
762
769
  const n = propKeys.length;
763
770
  const fns = propList.map((e) => e.fn);
764
771
  const reqs = propList.map((e) => e.req);
772
+ // Names met, as bits: the required ones must all be set, and a
773
+ // listed name not met may still be the object's own, hidden from
774
+ // for-in (defineProperty, enumerable: false), which the generated
775
+ // code and this engine's error path read by name: those are read
776
+ // after the walk, which on an ordinary document is the absent ones.
777
+ const reqMask = reqs.reduce((m, r, i) => r ? m | (1 << i) : m, 0);
778
+ const allMask = n === 32 ? -1 : (1 << n) - 1;
765
779
  // A property whose check is a coded leaf is checked in place.
766
780
  const codes = propList.map((e) => (e.fn.leaf && e.fn.v.lc !== undefined ? e.fn.v.lc : null));
767
781
  vsteps.push((data, stack, rec) => {
@@ -772,13 +786,21 @@ function install(deps) {
772
786
  let i = 0;
773
787
  while (i < n && propKeys[i] !== key) i++;
774
788
  if (i === n) { if (Object.hasOwn(data, key)) return false; continue; }
789
+ seen |= 1 << i;
775
790
  const value = data[key];
776
791
  if (value === undefined) { if (reqs[i]) return false; continue; }
777
792
  const lc = codes[i];
778
793
  if (lc !== null ? !leafOk(lc, value) : !fns[i].v(value, stack)) return false;
779
- if (reqs[i]) seen++;
780
794
  }
781
- return seen === reqN;
795
+ if ((seen & reqMask) !== reqMask) return false;
796
+ for (let miss = seen ^ allMask; miss !== 0; miss &= miss - 1) {
797
+ const i = 31 - Math.clz32(miss & -miss);
798
+ const value = data[propKeys[i]];
799
+ if (value === undefined || !Object.hasOwn(data, propKeys[i])) continue;
800
+ const lc = codes[i];
801
+ if (lc !== null ? !leafOk(lc, value) : !fns[i].v(value, stack)) return false;
802
+ }
803
+ return true;
782
804
  });
783
805
  }
784
806
  }
@@ -40,9 +40,21 @@ function isPrimitiveEnumValue(v) {
40
40
  return v === null || t === 'string' || t === 'number' || t === 'boolean';
41
41
  }
42
42
 
43
+ // A node of `enum` or `const` alone, no type: the values decide, as the full
44
+ // engines decide it (enumMember, deepEqual), and every value is a primitive,
45
+ // so a Set lookup or a === is the whole check. Form fields are written this
46
+ // way often (`{ enum: ['TR', 'DE'] }`, `{ const: true }`), and one of them
47
+ // sent the whole object to the slower engines.
48
+ const TIER0_UNTYPED_ALLOWED = new Set(['enum', 'const', ...META_KEYS]);
49
+
43
50
  function isTier0Primitive(schema) {
44
51
  if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) return false;
45
- if (!isPrimitiveType(schema.type)) return false;
52
+ if (!isPrimitiveType(schema.type)) {
53
+ if (schema.type !== undefined || (schema.enum === undefined && schema.const === undefined)) return false;
54
+ for (const k of Object.keys(schema)) {
55
+ if (!TIER0_UNTYPED_ALLOWED.has(k)) return false;
56
+ }
57
+ }
46
58
  for (const k of Object.keys(schema)) {
47
59
  if (!TIER0_PRIMITIVE_ALLOWED.has(k)) return false;
48
60
  }
package/lib/tier0.js CHANGED
@@ -166,9 +166,10 @@ function checkPrimitive(c, v) {
166
166
  }
167
167
  } else if (m === T_BOOLEAN) {
168
168
  if (typeof v !== 'boolean') return false;
169
- } else {
169
+ } else if (m !== 0) {
170
170
  return false;
171
171
  }
172
+ // m === 0: a node of enum or const alone; the checks below decide.
172
173
  if (c.hasEnum && !c.enumSet.has(v)) return false;
173
174
  if (c.hasConst && v !== c.constVal) return false;
174
175
  return true;
@@ -176,18 +177,32 @@ function checkPrimitive(c, v) {
176
177
 
177
178
  // Inlined object validator. Separating the primitive path removes a dead
178
179
  // branch from the hot object path.
179
- function tier0ValidateObject(plan, data) {
180
+ // The object verdict as one closure over the plan's fields, read once: the
181
+ // bound isValidObject() is then one call into a function whose loop reads
182
+ // captured constants, where a plan passed each call cost two calls and a
183
+ // field load per use (an empty object 17 ns against 7 for a validator built
184
+ // the same way by hand; a number 9 against 4).
185
+ function makeObjectVerdict(plan) {
186
+ const P_additionalAllowed = plan.additionalAllowed;
187
+ const P_constraints = plan.constraints;
188
+ const P_knownKeys = plan.knownKeys;
189
+ const P_requiredMask = plan.requiredMask;
190
+ return function tier0ObjectVerdict(data) {
180
191
  if (typeof data !== 'object' || data === null || Array.isArray(data)) return false;
181
- const cs = plan.constraints;
192
+ const cs = P_constraints;
182
193
  const n = cs.length;
183
- const reqMask = plan.requiredMask;
194
+ const reqMask = P_requiredMask;
195
+ // Whether the object is an ordinary one, read once: then a defined value
196
+ // under a name Object.prototype lacks is the object's own. Read for each
197
+ // present field, it was one test per field on a valid document.
198
+ const plain = (_PN ? data.__proto__ : Object.getPrototypeOf(data)) === Object.prototype;
184
199
  let seenMask = 0;
185
200
  for (let i = 0; i < n; i++) {
186
201
  const c = cs[i];
187
202
  let v = data[c.key];
188
203
  // Presence is an own-property question: a value the object inherits is
189
204
  // treated as absent, as the interpreted engine treats it.
190
- if (v !== undefined && (c.protoName || (_PN ? data.__proto__ : Object.getPrototypeOf(data)) !== Object.prototype) && !_hop.call(data, c.key)) v = undefined;
205
+ if (v !== undefined && (c.protoName || !plain) && !_hop.call(data, c.key)) v = undefined;
191
206
  if (v === undefined) {
192
207
  // A required key holding undefined is missing, as in every engine.
193
208
  if (reqMask & (1 << i)) return false;
@@ -225,21 +240,35 @@ function tier0ValidateObject(plan, data) {
225
240
  }
226
241
  } else if (m === T_BOOLEAN) {
227
242
  if (typeof v !== 'boolean') return false;
228
- } else {
243
+ } else if (m !== 0) {
229
244
  return false;
230
245
  }
246
+ // m === 0: a node of enum or const alone; the checks below decide.
231
247
  if (c.hasEnum && !c.enumSet.has(v)) return false;
232
248
  if (c.hasConst && v !== c.constVal) return false;
233
249
  }
234
250
  if ((seenMask & reqMask) !== reqMask) return false;
235
- if (!plan.additionalAllowed) {
236
- const known = plan.knownKeys;
251
+ if (!P_additionalAllowed) {
252
+ const known = P_knownKeys;
237
253
  for (const k in data) {
238
254
  if (!Object.prototype.hasOwnProperty.call(data, k)) continue;
239
255
  if (!known.has(k)) return false;
240
256
  }
241
257
  }
242
258
  return true;
259
+ };
260
+ }
261
+ function tier0ValidateObject(plan, data) {
262
+ let f = plan._objectVerdict;
263
+ if (f === undefined) f = plan._objectVerdict = makeObjectVerdict(plan);
264
+ return f(data);
265
+ }
266
+ // The verdict for a plan as one closure, for a validator to bind directly.
267
+ function tier0Verdict(plan) {
268
+ if (plan.isPrimitive) { const c = plan.constraints[0]; return (data) => checkPrimitive(c, data); }
269
+ let f = plan._objectVerdict;
270
+ if (f === undefined) f = plan._objectVerdict = makeObjectVerdict(plan);
271
+ return f;
243
272
  }
244
273
 
245
274
  function tier0Validate(plan, data) {
@@ -250,6 +279,7 @@ function tier0Validate(plan, data) {
250
279
  module.exports = {
251
280
  buildTier0Plan,
252
281
  tier0Validate,
282
+ tier0Verdict,
253
283
  tier0ValidateObject,
254
284
  checkPrimitive,
255
285
  TYPE_MASK,
@@ -1669,8 +1669,13 @@ class Validator {
1669
1669
  }
1670
1670
 
1671
1671
  addSchema(schema) {
1672
- if (this._initialized) {
1673
- throw new Error('Cannot add schema after compilation — call addSchema() before validate()')
1672
+ // After a compile of either kind: the verdict-only one isValidObject()
1673
+ // makes (_jsFn) binds a function over the registry as it was, and a
1674
+ // schema added after it reached validate() but not isValidObject(),
1675
+ // which kept answering against the old definition. A call the
1676
+ // interpreted twin answered compiled nothing, and the twin is rebuilt.
1677
+ if (this._initialized || this._jsFn) {
1678
+ throw new Error('Cannot add schema after compilation: call addSchema() before validate(), isValidObject(), validateJSON() or isValidJSON()')
1674
1679
  }
1675
1680
  if (!schema || !schema.$id) {
1676
1681
  throw new Error('Schema must have $id')
@@ -2356,16 +2361,22 @@ function _ticks(self) {
2356
2361
  // _ticks says so.
2357
2362
  function _tier0Verdict(self) {
2358
2363
  if (require('./shape-classifier').classify(self._schemaObj).tier !== 0) return null;
2359
- const { buildTier0Plan, tier0Validate } = require('./tier0');
2364
+ const { buildTier0Plan, tier0Verdict } = require('./tier0');
2360
2365
  const plan = buildTier0Plan(self._schemaObj, self._userFormats);
2361
- if (!_ticks(self)) return (d) => tier0Validate(plan, d);
2366
+ const verdict = tier0Verdict(plan);
2367
+ if (!_ticks(self)) return verdict;
2362
2368
  const tick = _tiering.counter(self);
2363
- return (d) => { tick(); return tier0Validate(plan, d); };
2369
+ return (d) => { tick(); return verdict(d); };
2364
2370
  }
2365
2371
 
2366
2372
  Validator.prototype._coldTwin = function () {
2367
2373
  const cg = _loadCodegen();
2368
- return cg === null || cg.coldTwin === undefined ? null : cg.coldTwin(this, Validator);
2374
+ // Where no code can be generated (a strict CSP), the interpreted engine is
2375
+ // the compile itself: a twin would only delay it, and its size test read
2376
+ // the schema's text for nothing (6.5% of a schema's first check there).
2377
+ // The probe of the realm (codegenAvailable) is not run for a validator
2378
+ // asked for the interpreted engine, which never generates code.
2379
+ return cg === null || cg.coldTwin === undefined || this._interpretOnly || !codegenAvailable() ? null : cg.coldTwin(this, Validator);
2369
2380
  };
2370
2381
 
2371
2382
  _defineLazyMethod('validate', (self) => (data) => {
@@ -2402,7 +2413,14 @@ _defineLazyMethod('isValidObject', (self) => {
2402
2413
  _bindVerdict(self, _ticks(self) ? _t0 : (d) => {
2403
2414
  const r = _t0(d);
2404
2415
  if (++_n === 2) {
2416
+ const before = self.isValidObject;
2405
2417
  try { self._ensureCodegen(); } catch {}
2418
+ // Where the generated code did not take the method over (no code
2419
+ // generation in this realm, or the generator declined), the plan's
2420
+ // closure answers directly from now on: this wrapper cost every
2421
+ // later call a count and a call, 13.5 against 4.7 ns on an empty
2422
+ // object, on exactly the pages and workers that have no generator.
2423
+ if (self.isValidObject === before) _bindVerdict(self, _t0);
2406
2424
  }
2407
2425
  return r;
2408
2426
  });
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.63.0';
10
+ module.exports = '1.63.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.63.0",
3
+ "version": "1.63.1",
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",
@@ -266,13 +266,13 @@
266
266
  "codegen.d.ts"
267
267
  ],
268
268
  "optionalDependencies": {
269
- "@ata-validator/native-darwin-arm64": "1.63.0",
270
- "@ata-validator/native-darwin-x64": "1.63.0",
271
- "@ata-validator/native-linux-arm64-gnu": "1.63.0",
272
- "@ata-validator/native-linux-arm64-musl": "1.63.0",
273
- "@ata-validator/native-linux-x64-gnu": "1.63.0",
274
- "@ata-validator/native-linux-x64-musl": "1.63.0",
275
- "@ata-validator/native-win32-x64": "1.63.0"
269
+ "@ata-validator/native-darwin-arm64": "1.63.1",
270
+ "@ata-validator/native-darwin-x64": "1.63.1",
271
+ "@ata-validator/native-linux-arm64-gnu": "1.63.1",
272
+ "@ata-validator/native-linux-arm64-musl": "1.63.1",
273
+ "@ata-validator/native-linux-x64-gnu": "1.63.1",
274
+ "@ata-validator/native-linux-x64-musl": "1.63.1",
275
+ "@ata-validator/native-win32-x64": "1.63.1"
276
276
  },
277
277
  "peerDependencies": {
278
278
  "yaml": "^2.0.0"