ata-validator 1.53.0 → 1.55.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.
@@ -460,6 +460,21 @@ class Plan {
460
460
  this.properties.set(key, { node: child(schema.properties[key]), seg: '/' + ek, schemaSeg: '/properties/' + ek });
461
461
  }
462
462
  }
463
+ // A required key holding undefined is missing. Where the key is also a
464
+ // declared property, the property walk, which reads the value anyway,
465
+ // finds that (requiredInProps); the required check reads the value only
466
+ // for the other keys (requiredUndef), so a valid object pays no second
467
+ // read of each required value.
468
+ this.requiredInProps = null;
469
+ this.requiredUndef = null;
470
+ if (this.required !== null) {
471
+ const undef = this.required.map((k) => this.properties === null || !this.properties.has(k));
472
+ if (undef.includes(true)) this.requiredUndef = undef;
473
+ if (this.properties !== null) {
474
+ const inProps = this.required.filter((k) => this.properties.has(k));
475
+ if (inProps.length > 0) this.requiredInProps = new Set(inProps);
476
+ }
477
+ }
463
478
  this.patternProperties = null;
464
479
  if (isObjectMap(schema.patternProperties)) {
465
480
  this.patternProperties = Object.keys(schema.patternProperties).map((src) => ({ src, re: interp.pattern(src), node: child(schema.patternProperties[src]) }));
@@ -601,7 +616,8 @@ function evalLeafV(P, data) {
601
616
  if (P.required !== null) {
602
617
  const req = P.required;
603
618
  for (let i = 0; i < req.length; i++) {
604
- if (!Object.hasOwn(data, req[i])) return false;
619
+ // A key holding undefined is missing, as in the default validator.
620
+ if (!Object.hasOwn(data, req[i]) || (P.requiredUndef !== null && P.requiredUndef[i] && data[req[i]] === undefined)) return false;
605
621
  }
606
622
  }
607
623
  if (P.minProperties !== undefined && Object.keys(data).length < P.minProperties) return false;
@@ -659,71 +675,6 @@ function dataCtx(instancePath, schemaPath, keyword) {
659
675
  return { instancePath, parentData: parent, parentDataProperty: key, rootData: root, schemaPath: schemaPath + '/' + keyword };
660
676
  }
661
677
 
662
- function isDiscriminated(schema) {
663
- const d = schema.discriminator;
664
- return d !== null && typeof d === 'object' && typeof d.propertyName === 'string';
665
- }
666
-
667
- // The reference's coercion: the first type in the list the value converts to,
668
- // after unwrapping a one-element array in 'array' mode, which then converts
669
- // the element.
670
- const NO_COERCION = Symbol('no coercion');
671
- function fitsType(d, t) {
672
- switch (t) {
673
- case 'string': return typeof d === 'string';
674
- case 'number': return typeof d === 'number' && isFinite(d);
675
- case 'integer': return typeof d === 'number' && Number.isInteger(d);
676
- case 'boolean': return typeof d === 'boolean';
677
- case 'null': return d === null;
678
- case 'array': return Array.isArray(d);
679
- case 'object': return d !== null && typeof d === 'object' && !Array.isArray(d);
680
- default: return false;
681
- }
682
- }
683
- // Whether an unwrapped element is kept as it is. The reference tests a list
684
- // of types here with every type at once, so a list of two or more keeps
685
- // nothing (`number` stands in for `integer` beside it), and the element goes
686
- // on to be converted, as one of a single type does when it does not fit.
687
- function unwrappedFits(d, types) {
688
- let list = types;
689
- if (list.length > 1 && list.includes('number')) list = list.filter((t) => t !== 'integer');
690
- if (list.length > 1 && list.includes('array') && list.includes('object')) return false;
691
- for (const t of list) if (!fitsType(d, t)) return false;
692
- return true;
693
- }
694
- function coerceValue(data, types, arrayMode) {
695
- if (arrayMode && Array.isArray(data) && data.length === 1) {
696
- data = data[0];
697
- if (unwrappedFits(data, types)) return data;
698
- }
699
- const kind = data === null ? 'null' : typeof data;
700
- for (const t of types) {
701
- switch (t) {
702
- case 'number':
703
- case 'integer':
704
- if (kind === 'boolean' || kind === 'null') return +data;
705
- // eslint-disable-next-line eqeqeq
706
- if (kind === 'string' && data !== '' && data == +data && (t === 'number' || !(data % 1))) return +data;
707
- break;
708
- case 'string':
709
- if (kind === 'number' || kind === 'boolean') return '' + data;
710
- if (kind === 'null') return '';
711
- break;
712
- case 'boolean':
713
- if (data === 'false' || data === 0 || data === null) return false;
714
- if (data === 'true' || data === 1) return true;
715
- break;
716
- case 'null':
717
- if (data === '' || data === 0 || data === false) return null;
718
- break;
719
- case 'array':
720
- if (arrayMode && (kind === 'string' || kind === 'number' || kind === 'boolean' || kind === 'null')) return [data];
721
- break;
722
- }
723
- }
724
- return NO_COERCION;
725
- }
726
-
727
678
  // After a context-reading keyword, the value is read again from where it sits:
728
679
  // the keyword may have replaced it (a date deserialized, a value serialized),
729
680
  // and the keywords after it check what it left, as the default validator's
@@ -855,7 +806,7 @@ function evalLeaf(P, data, errors, instancePath, schemaPath) {
855
806
  const req = P.required;
856
807
  for (let i = 0; i < req.length; i++) {
857
808
  const key = req[i];
858
- if (!Object.hasOwn(data, key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
809
+ 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; }
859
810
  }
860
811
  }
861
812
  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; }
@@ -909,6 +860,9 @@ class Interpreter {
909
860
  // `discriminator` option: the tag picks the one oneOf branch to apply.
910
861
  this.discriminator = opts.discriminator === true;
911
862
  this._discr = null;
863
+ // Both come from compat, which installs the code for them (see
864
+ // lib/interpreter-compat.js), so a bundle without compat carries none of it.
865
+ this.ext = this.coerce !== false || this.discriminator ? compatExt() : null;
912
866
  // The compiled closures know none of these.
913
867
  if (this.dataContext || this.discriminator || this.numberFormats !== null) this._fast = null;
914
868
  this._ctxRoot = undefined;
@@ -922,7 +876,14 @@ class Interpreter {
922
876
  if (this._fast === undefined) {
923
877
  const compile = planCompiler();
924
878
  if (compile === null) this._fast = null;
925
- else try { this._fast = compile(this); } catch { this._fast = null; }
879
+ else {
880
+ try { this._fast = compile(this); } catch (e) {
881
+ // The plain walk answers instead, which tests cannot tell apart;
882
+ // tests/test_plan_compiles.js holds the compiler to the suite.
883
+ if (typeof process === 'object' && process !== null && process.env && process.env.ATA_DEBUG) console.error('ata: plan compiler failed, plain walk used:', e && e.message);
884
+ this._fast = null;
885
+ }
886
+ }
926
887
  }
927
888
  return this._fast;
928
889
  }
@@ -1028,75 +989,6 @@ class Interpreter {
1028
989
  }
1029
990
  }
1030
991
 
1031
- // The discriminator in place of oneOf: the branch the tag names is applied,
1032
- // and a tag that is not a string, or names no branch, is the error.
1033
- discriminate(P, data, base, dynScope, errors, instancePath, schemaPath, stack, local) {
1034
- const d = this.discrMap(P, base);
1035
- const tag = data[d.tagName];
1036
- if (typeof tag !== 'string') {
1037
- if (errors !== NOERRORS) errors.push(err('discriminator', 'discriminator', instancePath, schemaPath + '/discriminator', { error: 'tag', tag: d.tagName, tagValue: tag }, `tag "${d.tagName}" must be string`));
1038
- return false;
1039
- }
1040
- const i = d.map.get(tag);
1041
- if (i === undefined) {
1042
- if (errors !== NOERRORS) errors.push(err('discriminator', 'discriminator', instancePath, schemaPath + '/discriminator', { error: 'mapping', tag: d.tagName, tagValue: tag }, `value of tag "${d.tagName}" must be in oneOf`));
1043
- return false;
1044
- }
1045
- const sub = local !== DISCARD ? { props: null, items: null } : DISCARD;
1046
- const ok = this.eval(P.oneOf[i], data, base, dynScope, errors, instancePath, schemaPath + '/oneOf/' + i, stack, sub);
1047
- if (ok && local !== DISCARD) mergeAnnotations(local, sub);
1048
- return ok;
1049
- }
1050
-
1051
- // Tag value to branch index, read from each branch's `properties`, through
1052
- // a `$ref`, with the reference's rules: a `const` or `enum` of unique
1053
- // strings, and the tag required by the node or by every branch.
1054
- discrMap(P, base) {
1055
- if (this._discr === null) this._discr = new Map();
1056
- let d = this._discr.get(P);
1057
- if (d !== undefined) return d;
1058
- const tagName = P.schema.discriminator.propertyName;
1059
- if (P.schema.discriminator.mapping !== undefined) throw new Error('discriminator: mapping is not supported');
1060
- const hasRequired = (s) => s !== null && typeof s === 'object' && Array.isArray(s.required) && s.required.includes(tagName);
1061
- let topRequired = hasRequired(P.schema);
1062
- const map = new Map();
1063
- for (let i = 0; i < P.oneOf.length; i++) {
1064
- let node = P.oneOf[i];
1065
- if (node !== null && typeof node === 'object' && node.ref !== null && node.ref !== undefined && (node.schema.properties === undefined)) {
1066
- const r = this.resolveRefCached(node, base);
1067
- if (r.child === undefined) throw new Error(`discriminator: cannot resolve $ref ${node.ref}`);
1068
- node = r.child;
1069
- }
1070
- const sch = node !== null && typeof node === 'object' ? node.schema : undefined;
1071
- const propSch = sch && sch.properties ? sch.properties[tagName] : undefined;
1072
- if (propSch === null || typeof propSch !== 'object') throw new Error(`discriminator: oneOf subschemas (or referenced schemas) must have "properties/${tagName}"`);
1073
- topRequired = topRequired || hasRequired(sch);
1074
- const values = propSch.const !== undefined ? [propSch.const] : Array.isArray(propSch.enum) ? propSch.enum : null;
1075
- if (values === null) throw new Error(`discriminator: "properties/${tagName}" must have "const" or "enum"`);
1076
- for (const v of values) {
1077
- if (typeof v !== 'string' || map.has(v)) throw new Error(`discriminator: "${tagName}" values must be unique strings`);
1078
- map.set(v, i);
1079
- }
1080
- }
1081
- if (!topRequired) throw new Error(`discriminator: "${tagName}" must be required`);
1082
- d = { tagName, map };
1083
- this._discr.set(P, d);
1084
- return d;
1085
- }
1086
-
1087
- // Coerces the value at `instancePath` to the node's type and writes it where
1088
- // it sits; at the root, where it sits nowhere, the walk carries it on.
1089
- coerceAt(P, data, instancePath) {
1090
- const to = coerceValue(data, P.typeNames, this.coerce === 'array');
1091
- if (to === NO_COERCION) return data;
1092
- if (instancePath === '') this._ctxRoot = to;
1093
- else {
1094
- const ctx = dataCtx(instancePath, '', '');
1095
- if (ctx.parentData !== null && typeof ctx.parentData === 'object') ctx.parentData[ctx.parentDataProperty] = to;
1096
- }
1097
- return to;
1098
- }
1099
-
1100
992
  // The value at `instancePath` as it is now, after a subschema may have
1101
993
  // coerced it.
1102
994
  here(instancePath, data) {
@@ -1183,7 +1075,7 @@ class Interpreter {
1183
1075
  const local = collect ? { props: null, items: null } : DISCARD;
1184
1076
 
1185
1077
  // Coercion first, before anything reads the value, as the reference does it.
1186
- if (this.coerce !== false && P.hasType && (dataBits(data) & P.typeMask) === 0) data = this.coerceAt(P, data, instancePath);
1078
+ if (this.coerce !== false && P.hasType && (dataBits(data) & P.typeMask) === 0) data = this.ext.coerceAt(this, P, data, instancePath);
1187
1079
 
1188
1080
  // $ref / $dynamicRef: in-place applicators, annotations flow through
1189
1081
  if (P.ref !== null) {
@@ -1331,7 +1223,7 @@ class Interpreter {
1331
1223
  const req = P.required;
1332
1224
  for (let i = 0; i < req.length; i++) {
1333
1225
  const key = req[i];
1334
- if (!Object.hasOwn(data, key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
1226
+ 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; }
1335
1227
  }
1336
1228
  }
1337
1229
  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; }
@@ -1359,8 +1251,12 @@ class Interpreter {
1359
1251
  if (prop !== undefined) {
1360
1252
  // A declared property whose value is undefined is not checked, as the
1361
1253
  // generated code does not check it: no JSON document carries one, and
1362
- // serialized the key is not there.
1363
- if (data[key] !== undefined && !this.eval(prop.node, data[key], base, dynScope, errors, E ? instancePath + prop.seg : '', E ? schemaPath + prop.schemaSeg : '', stack, DISCARD)) valid = false;
1254
+ // serialized the key is not there. Required, it is missing (see
1255
+ // requiredInProps).
1256
+ const value = data[key];
1257
+ if (value !== undefined) {
1258
+ if (!this.eval(prop.node, value, base, dynScope, errors, E ? instancePath + prop.seg : '', E ? schemaPath + prop.schemaSeg : '', stack, DISCARD)) valid = false;
1259
+ } 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; }
1364
1260
  evaluated = true;
1365
1261
  }
1366
1262
  if (patterns !== null) {
@@ -1467,8 +1363,8 @@ class Interpreter {
1467
1363
  valid = false;
1468
1364
  }
1469
1365
  }
1470
- if (P.oneOf !== null && this.discriminator && isDiscriminated(schema)) {
1471
- if (bits === T_OBJECT && !this.discriminate(P, data, base, dynScope, errors, instancePath, schemaPath, stack, collect ? local : DISCARD)) valid = false;
1366
+ if (P.oneOf !== null && this.discriminator && this.ext.isDiscriminated(schema)) {
1367
+ if (bits === T_OBJECT && !this.ext.discriminate(this, P, data, base, dynScope, errors, instancePath, schemaPath, stack, collect ? local : DISCARD)) valid = false;
1472
1368
  } else if (P.oneOf !== null) {
1473
1369
  let count = 0;
1474
1370
  let winner = null;
@@ -1613,18 +1509,33 @@ function planCompiler() {
1613
1509
  compileInterpreter = loadPlanCompiler()({
1614
1510
  Plan, NOERRORS, err, evalLeaf, evalLeafV, runCustomV, dataBits, escapePointer,
1615
1511
  deepEqual, enumMember, allDistinct, multipleOfOk, cpAtLeast, cpAtMost,
1616
- T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri,
1512
+ T_STRING, T_NUMBER, T_INTEGER, T_BOOLEAN, T_NULL, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri,
1617
1513
  }).compileInterpreter;
1618
1514
  }
1619
1515
  return compileInterpreter;
1620
1516
  }
1621
1517
 
1518
+ let installCompat = null;
1519
+ let compatInstalled = null;
1520
+ function setCompat(install) {
1521
+ installCompat = install;
1522
+ compatInstalled = null;
1523
+ }
1524
+ function compatExt() {
1525
+ if (compatInstalled === null) {
1526
+ if (installCompat === null) throw new Error('coercion where each type is checked and the discriminator are installed by ata-validator/compat');
1527
+ compatInstalled = installCompat({ NOERRORS, DISCARD, err, mergeAnnotations, dataCtx });
1528
+ }
1529
+ return compatInstalled;
1530
+ }
1531
+
1622
1532
  function createInterpreter(schema, options) {
1623
1533
  return new Interpreter(schema, options);
1624
1534
  }
1625
1535
 
1626
1536
  module.exports = { createInterpreter };
1627
1537
  Object.defineProperty(module.exports, '_setPlanCompiler', { value: setPlanCompiler, enumerable: false });
1538
+ Object.defineProperty(module.exports, '_setCompat', { value: setCompat, enumerable: false });
1628
1539
  // The resolver, for the code generator: it resolves references at compile
1629
1540
  // time the way this engine does at run time (normalizeRefs in js-compiler).
1630
1541
  Object.defineProperty(module.exports, '_refInternals', {
@@ -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 { 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
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, keyLoopEligible, SAME_KEYS_HELPER, seqTag } = core._internal
13
13
 
14
14
 
15
15
  function emitPtrEsc(ctx) {
@@ -590,9 +590,19 @@ function subtreeGuard (sub, ctx, gv) {
590
590
  // 25-definition API schema came to 55 KB of source in a 133 KB function,
591
591
  // and a function that size reaches the optimizing compiler late.
592
592
  ctx.shareDefs = true
593
+ // A record of many optional properties takes the verdict's key-loop form
594
+ // (keyLoopEligible): its optional properties are checked from the record's
595
+ // own keys. Written one read per property, the guard of a 150-property
596
+ // record was 16.9 KB, over GUARD_MAX, so there was no guard at all and
597
+ // every record of the list was collected in full; on records of mixed
598
+ // shapes those reads are megamorphic, and 2000 of them cost 18 ms where
599
+ // the verdict took 0.3. The key-loop form is 10 KB and reads only the keys
600
+ // the record has.
601
+ const outerKeyLoop = ctx.keyLoopNode
602
+ if (keyLoopEligible(sub)) ctx.keyLoopNode = sub
593
603
  try {
594
604
  const bl = []
595
- try { nestedGenCode(sub, gv, bl, ctx) } finally { ctx.defFns = outerDefFns; ctx.shareDefs = outerShare }
605
+ try { nestedGenCode(sub, gv, bl, ctx) } finally { ctx.defFns = outerDefFns; ctx.shareDefs = outerShare; ctx.keyLoopNode = outerKeyLoop }
596
606
  if (bl.length === 0) { restoreCtx(ctx, saved); return null }
597
607
  const body = bl.join(';')
598
608
  // The verdict generator's recursion names (`_validate`, the cycle guard
@@ -612,17 +622,20 @@ function subtreeGuard (sub, ctx, gv) {
612
622
  }
613
623
  }
614
624
  // The collecting code for `sub` at `v`, behind its guard when there is one.
615
- function genGuardedC (sub, v, pathExpr, lines, ctx, schemaPrefix) {
625
+ // `when`, a boolean variable, makes the guard conditional: when it is false
626
+ // the value is collected without a verdict first (see the items case).
627
+ function genGuardedC (sub, v, pathExpr, lines, ctx, schemaPrefix, when) {
616
628
  const gi = ctx.varCounter++
617
629
  const gv = `_gv${gi}`
618
630
  const g = subtreeGuard(sub, ctx, gv)
619
631
  if (g === null) { genCodeC(sub, v, pathExpr, lines, ctx, schemaPrefix); return }
620
- if (g.name !== undefined) lines.push(`if(!${g.name}(${v})){`)
632
+ const cond = when ? `!${when}||` : ''
633
+ if (g.name !== undefined) lines.push(`if(${cond}!${g.name}(${v})){`)
621
634
  else {
622
635
  // The verdict's exits become jumps out of the block: a failure leaves
623
636
  // the flag set, success clears it. replaceTopLevel keeps nested
624
637
  // closures' returns as they are (see the hybrid rewrite).
625
- lines.push(`{const ${gv}=${v};let _g${gi}=1;g${gi}:{${replaceTopLevel(g.body, `{_g${gi}=0;break g${gi}}`, `break g${gi}`)};_g${gi}=0}if(_g${gi}){`)
638
+ lines.push(`{const ${gv}=${v};let _g${gi}=1;${when ? `if(${when})` : ''}g${gi}:{${replaceTopLevel(g.body, `{_g${gi}=0;break g${gi}}`, `break g${gi}`)};_g${gi}=0}if(_g${gi}){`)
626
639
  }
627
640
  ctx.guardDepth = (ctx.guardDepth || 0) + 1
628
641
  try { genCodeC(sub, v, pathExpr, lines, ctx, schemaPrefix) } finally { ctx.guardDepth-- }
@@ -1164,7 +1177,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1164
1177
  const reqSp = `${schemaPrefix}/required`
1165
1178
  const reqLit = buildErrorLiteral({ keyword: 'required', schemaPath: reqSp, sourceMap: ctx.sourceMap })
1166
1179
  for (const key of schema.required) {
1167
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){_e.push({code:'${reqLit.codeStr}',keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${reqSp}'${ordinalField(ctx, `${reqSp}`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'",docUrl:'${reqLit.docUrl}'${reqLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
1180
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&(${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)})){_e.push({code:'${reqLit.codeStr}',keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${reqSp}'${ordinalField(ctx, `${reqSp}`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'",docUrl:'${reqLit.docUrl}'${reqLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
1168
1181
  }
1169
1182
  }
1170
1183
 
@@ -1354,7 +1367,8 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1354
1367
  const childPath = childPathExpr(pathExpr, ptrSeg(key))
1355
1368
  const sp = schemaPrefix+'/properties/'+ptrSeg(key)
1356
1369
  const from = lines.length
1357
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
1370
+ // A value that is undefined is not checked (see the verdict generator).
1371
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${v}[${JSON.stringify(key)}]!==undefined&&${ownKeyExpr(ctx, v, key)}){`)
1358
1372
  // A large subschema as a function of its own (emitPropFnE); false means inline.
1359
1373
  if (!propFnCandidateE(prop, ctx, sp) || !emitPropFnE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, sp)) {
1360
1374
  genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, sp)
@@ -1769,7 +1783,10 @@ function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, le
1769
1783
  // the definition and the reference's own comes in as `_sp`.
1770
1784
  const rel = sp.startsWith(SP_PLACEHOLDER)
1771
1785
  ctx.closureVals.push(errorSite(keyword, unescapeSp(rel ? sp.slice(SP_PLACEHOLDER.length) : sp), paramsVal, fmt))
1772
- return `(_e=_ap(_A,_e,_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''}),-1))`
1786
+ // The site's ordinal key, as the plain form passes it: a list that comes out
1787
+ // in order then needs no sorting, and one that does not is put in order from
1788
+ // the keys collected beside it (sr) instead of looking up each error's path.
1789
+ return `(_e=_ap(_A,_e,_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''}),${apOrd(ctx, sp)}))`
1773
1790
  }
1774
1791
 
1775
1792
  function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
@@ -1935,7 +1952,10 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
1935
1952
  // The checked sort (_srX) compares with the reader, which a module
1936
1953
  // does not carry: there `_SRT` keeps the list as it is.
1937
1954
  `\n return _e?new _ER(_A.mo?_e:${env('ATA_CHECK_SORT') && !(opts && opts.emit) ? '_srX(_A,_e,_SRT)' : '_sr(_A,_e,_SRT)'}):${opts && opts.moduleShape ? 'R' : '{valid:true,data:d,errors:_EE}'}}catch(_x){return _FB(d)}`
1938
- : hoistDecl + allDecl + `let _e;${AP_RESET}\n ` + checks + `\n return _e?{valid:false,errors:_e}:R`
1955
+ // The rich form also says whether its list came out in the reader's
1956
+ // order (`mo`, kept by _ap from each site's ordinal key), so the reader
1957
+ // (richBuilder) sorts only a list that did not.
1958
+ : hoistDecl + allDecl + `let _e;${AP_RESET}\n ` + checks + `\n return _e?{valid:false,errors:_e${ctx.rich ? ',mo:_A.mo' : ''}}:R`
1939
1959
  // The verdict generator's hoisted functions (subtreeGuard writes guards
1940
1960
  // through it) come first: a oneOf branch it could not hoist was a closure
1941
1961
  // built at every call of the guard, six per element of a json-patch list.
@@ -1952,7 +1972,8 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
1952
1972
  console.log('=== COMBINED PARTS ===', JSON.stringify(Object.entries(acc).sort((a, b) => b[1] - a[1])))
1953
1973
  }
1954
1974
  const programSrc = assignOrdKeys(ctx, `${helpers}return function _vC(d){${inner}}`)
1955
- const factory = compileFunction('R' + (closureParams ? ',' + closureParams : ''), programSrc)
1975
+ // The run-time program is tagged (seqTag); the emitted source is not.
1976
+ const factory = compileFunction('R' + (closureParams ? ',' + closureParams : ''), (opts && opts.emit ? '' : seqTag()) + programSrc)
1956
1977
  const made = factory(VALID_RESULT, ...closureArgs)
1957
1978
  // For a standalone module (lib/aot-impl.js): the program's source and the
1958
1979
  // closure values it takes, after the function was built and tried on
@@ -2405,7 +2426,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2405
2426
  }
2406
2427
  }
2407
2428
  for (const key of schema.required) {
2408
- const check = hoisted[key] ? `${hoisted[key]}===undefined` : `!${ownKeyExpr(ctx, v, key)}`
2429
+ const check = hoisted[key] ? `${hoisted[key]}===undefined` : `(${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)})`
2409
2430
  if (isStaticPath && !ctx.rich && !ctx.noOrdinal) {
2410
2431
  const ei = ctx.varCounter++
2411
2432
  const errVar = `_E${ei}`
@@ -2428,7 +2449,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2428
2449
  ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
2429
2450
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e=_ap(_A,_e,${errVar},${apOrd(ctx, `${schemaPrefix}/required`)}))}`)
2430
2451
  } else {
2431
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
2452
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&(${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)})){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
2432
2453
  }
2433
2454
  }
2434
2455
  }
@@ -2612,7 +2633,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2612
2633
  sub(`_o${oi}`)
2613
2634
  lines.push(`}}`)
2614
2635
  } else {
2615
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
2636
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${v}[${JSON.stringify(key)}]!==undefined&&${ownKeyExpr(ctx, v, key)}){`)
2616
2637
  sub(`${v}[${JSON.stringify(key)}]`)
2617
2638
  lines.push(`}`)
2618
2639
  }
@@ -2643,9 +2664,24 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2643
2664
  const idx = `_j${ctx.varCounter}`, elem = `_ei${ctx.varCounter}`
2644
2665
  ctx.varCounter++
2645
2666
  const childPath = childPathDynExpr(pathExpr, idx)
2646
- lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
2647
- genGuardedC(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items')
2648
- lines.push(`}}`)
2667
+ if (ctx.inCombined && keyLoopEligible(schema.items)) {
2668
+ // Records of many optional properties (see subtreeGuard): the guard,
2669
+ // a key loop per record, is run only for a list of mixed shapes,
2670
+ // decided as the verdict decides (the first, middle and last record's
2671
+ // keys). On records of one shape the collecting code's reads are
2672
+ // monomorphic and cheaper than any guard: 2000 uniform records read
2673
+ // for errors in 16 us, with a guard on each 69.
2674
+ hoistOnce(ctx, 'sameKeysHelper', SAME_KEYS_HELPER)
2675
+ const n = `_n${ctx.varCounter++}`
2676
+ const mx = `_mx${ctx.varCounter++}`
2677
+ lines.push(`if(Array.isArray(${v})){const ${n}=${v}.length;const ${mx}=${n}-${startIdx}>2&&(!_sameKeys(${v}[${startIdx}],${v}[(${n}+${startIdx})>>1])||!_sameKeys(${v}[${startIdx}],${v}[${n}-1]));for(let ${idx}=${startIdx};${idx}<${n};${idx}++){const ${elem}=${v}[${idx}]`)
2678
+ genGuardedC(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items', mx)
2679
+ lines.push(`}}`)
2680
+ } else {
2681
+ lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
2682
+ genGuardedC(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items')
2683
+ lines.push(`}}`)
2684
+ }
2649
2685
  }
2650
2686
 
2651
2687
  // prefixItems