ata-validator 1.62.1 → 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.
Files changed (53) hide show
  1. package/README.md +63 -8
  2. package/build.mjs +1 -0
  3. package/index.browser.mjs +23 -4
  4. package/index.bundle.mjs +14 -4
  5. package/index.edge.mjs +14 -4
  6. package/index.js +7 -2
  7. package/lib/aot-build.js +18 -0
  8. package/lib/codegen-engine.js +72 -3
  9. package/lib/compiled-common.js +2 -1
  10. package/lib/email-literal.js +4 -1
  11. package/lib/formats-source.js +39 -40
  12. package/lib/interpreter-core.js +148 -70
  13. package/lib/ir/emit-deps.mjs +131 -0
  14. package/lib/ir/emit.mjs +707 -0
  15. package/lib/ir/front.mjs +865 -0
  16. package/lib/ir/index.mjs +10 -0
  17. package/lib/ir/refs.mjs +108 -0
  18. package/lib/ir/run.mjs +520 -0
  19. package/lib/ir/table.mjs +233 -0
  20. package/lib/js-compiler-errors.js +9 -4
  21. package/lib/kernels/collapse.mjs +114 -0
  22. package/lib/kernels/custom.mjs +70 -0
  23. package/lib/kernels/fill.mjs +6 -0
  24. package/lib/kernels/formats-small.mjs +40 -0
  25. package/lib/kernels/formats.mjs +29 -0
  26. package/lib/kernels/index.mjs +11 -0
  27. package/lib/kernels/kernels.mjs +285 -0
  28. package/lib/kernels/leaves.mjs +28 -0
  29. package/lib/kernels/order.mjs +25 -0
  30. package/lib/kernels/plan-rich.mjs +17 -0
  31. package/lib/kernels/plan.d.ts +9 -0
  32. package/lib/kernels/plan.mjs +128 -0
  33. package/lib/kernels/preprocess.mjs +223 -0
  34. package/lib/kernels/rank.mjs +97 -0
  35. package/lib/kernels/rt.mjs +249 -0
  36. package/lib/kernels/table.mjs +182 -0
  37. package/lib/kernels/unique.mjs +157 -0
  38. package/lib/native-positions.js +1 -1
  39. package/lib/openapi.d.ts +2 -0
  40. package/lib/openapi.js +128 -0
  41. package/lib/plan-compiler.js +113 -14
  42. package/lib/public-api.js +7 -2
  43. package/lib/rejections.js +3 -1
  44. package/lib/schema-helpers.js +12 -0
  45. package/lib/schema-helpers.mjs +20 -0
  46. package/lib/schema-order.js +4 -2
  47. package/lib/shape-classifier.js +13 -1
  48. package/lib/standard-schema.js +1 -1
  49. package/lib/tier0.js +38 -8
  50. package/lib/validator-core.js +59 -36
  51. package/lib/version.js +1 -1
  52. package/lite.js +7 -2
  53. package/package.json +26 -8
@@ -0,0 +1,865 @@
1
+ // The IR. Statements: seq, if, let, bind (locals for the rest of a sequence),
2
+ // keys (each key: Object.keys, for-in with an own filter, or own __proto__
3
+ // last), range, count/incr, do (an expression for its effect), ret, require
4
+ // (an in-place call whose failure is this node's), fail (verdict: return
5
+ // false; errors: push), branches (anyOf/oneOf through the collapse),
6
+ // setDefault and fcall (the default fill). Expressions: var, lit, get,
7
+ // field, own, not, and, or, cmp, isObj, isObjAny, isArr, len, plainProto,
8
+ // type, guard, kernel, kfail, known, re, seg, cat, str, obj, pcall, and the
9
+ // annotation operations (annKey, annHasKey, annPrefix, annAll, annAllSet,
10
+ // annAt, annAtHas, annN, firstUneval). A procedure is a schema compiled once
11
+ // per mode (verdict, dual, ann, fill) with the parameters d, e, s, P, V, SP,
12
+ // A; both backends memoize procedures by schema object, which closes
13
+ // reference cycles.
14
+ //
15
+ // IR front end: the only place keyword semantics live. It turns a
16
+ // schema into procedures of IR (README.md) that the two backends read:
17
+ // emit.mjs writes JavaScript, run.mjs builds closures.
18
+ //
19
+ // Everything the schema fixes is decided here: messages, expected text,
20
+ // params, schema paths where they are static, which guards a known type makes
21
+ // redundant, whether a node collects annotations, which resources a
22
+ // $dynamicRef can land on.
23
+
24
+ import { LEAVES } from '../kernels/leaves.mjs'
25
+ import { type, required, additionalProperties, ANNOTATIONS, TYPES, falseSchema, contains, not as notK, branchRank, unevaluatedProperties as uPK, unevaluatedItems as uIK, collapse, makeHintSuggest, GUARD_NAME } from '../kernels/kernels.mjs'
26
+ import { isObj } from '../kernels/rt.mjs'
27
+ import { buildIndex } from './refs.mjs'
28
+ import draft7 from '../draft7.js'
29
+
30
+ // Under ctx, where each keyword runs in the current engine's walk of one node
31
+ // (lib/interpreter-core.js eval): references, the value's own checks, the
32
+ // custom keywords, arrays, objects, the in-place applicators, then the
33
+ // unevaluated keywords and the custom keywords declared post.
34
+ const CTX_RANK = { $ref: 0, $dynamicRef: 1, type: 2, enum: 3, const: 4, multipleOf: 5, maximum: 5, exclusiveMaximum: 5, minimum: 5, exclusiveMinimum: 5, maxLength: 6, minLength: 6, pattern: 6, format: 6, minItems: 8, maxItems: 8.1, uniqueItems: 8.2, prefixItems: 8.3, items: 8.4, additionalItems: 8.45, contains: 8.5, required: 9, minProperties: 10, maxProperties: 10, dependentRequired: 11, propertyNames: 12, properties: 13, dependentSchemas: 14, allOf: 15, anyOf: 17, oneOf: 18, not: 19, if: 20, then: 20, else: 20 }
35
+ const CTX_CUSTOM = 7
36
+ const CTX_MACRO = 16
37
+ const CTX_LAST = 21
38
+
39
+ // Every keyword name some dialect or the current engine gives a meaning to.
40
+ // One of these the front end does not handle above is refused; a name
41
+ // outside the set is ignored, as the current engine ignores it.
42
+ const MEANINGFUL = new Set(['$schema', '$id', 'id', '$ref', '$defs', 'definitions', '$anchor', '$dynamicRef', '$dynamicAnchor', '$recursiveRef', '$recursiveAnchor', '$vocabulary', '$comment', 'type', 'enum', 'const', 'multipleOf', 'maximum', 'exclusiveMaximum', 'minimum', 'exclusiveMinimum', 'maxLength', 'minLength', 'pattern', 'maxItems', 'minItems', 'uniqueItems', 'maxContains', 'minContains', 'maxProperties', 'minProperties', 'required', 'dependentRequired', 'dependencies', 'items', 'prefixItems', 'additionalItems', 'contains', 'properties', 'patternProperties', 'additionalProperties', 'propertyNames', 'unevaluatedItems', 'unevaluatedProperties', 'allOf', 'anyOf', 'oneOf', 'not', 'if', 'then', 'else', 'dependentSchemas', 'format', 'contentEncoding', 'contentMediaType', 'contentSchema', 'title', 'description', 'default', 'deprecated', 'readOnly', 'writeOnly', 'examples',
43
+ // The current engine's own: OpenAPI's nullable, custom error messages and
44
+ // $data references. (The discriminator means something only to the compat
45
+ // layer; the Validator ignores it, and so does the IR.)
46
+ 'nullable', 'errorMessage', '$data'])
47
+ import dialect from '../dialect.js'
48
+ import keywordsLib from '../keywords.js'
49
+ import vocabularies from '../vocabularies.js'
50
+ import { orderCustom } from '../kernels/custom.mjs'
51
+
52
+ // Older dialects are read the way the current engine reads them: normalized
53
+ // into 2020-12 by its own lib/draft7.js (tuple items to prefixItems,
54
+ // dependencies split, definitions, exclusive bounds, nullable), so errors
55
+ // carry the same normalized schema paths. The decision is
56
+ // lib/validator-core.js _normalizeCallerSchemaUncached, without its
57
+ // nothing-to-do scan.
58
+ const DIALECTS = new Set(['https://json-schema.org/draft/2020-12/schema', 'http://json-schema.org/draft-07/schema#', 'http://json-schema.org/draft-07/schema', 'http://json-schema.org/draft-06/schema#', 'http://json-schema.org/draft-06/schema', 'http://json-schema.org/draft-04/schema#', 'http://json-schema.org/draft-04/schema', 'https://json-schema.org/draft/2019-09/schema'])
59
+ export function normalize (s, inherit = 0) {
60
+ if (s === null || typeof s !== 'object') return s
61
+ const code = s.$schema !== undefined ? dialect.draftCode(s.$schema) : inherit
62
+ const needsDraft7 = code === 1 || code === 2 || code === 4
63
+ const copy = structuredClone(s)
64
+ if (needsDraft7) draft7.normalizeDraft7(copy, true, code === 2 || code === 4, code === 4)
65
+ if (code === 3) draft7.normalizeDraft2019(copy, true)
66
+ draft7.normalizeNullable(copy)
67
+ draft7.normalizeExclusiveBounds(copy)
68
+ return copy
69
+ }
70
+
71
+ // ---- IR constructors -------------------------------------------------------
72
+ export const lit = (v) => ({ t: 'lit', v })
73
+ export const vr = (n) => ({ t: 'var', n })
74
+ const seq = (...body) => ({ t: 'seq', body: body.flat().filter(Boolean) })
75
+ const iff = (c, then, els = null) => ({ t: 'if', c, then, else: els })
76
+ const not = (e) => ({ t: 'not', e })
77
+ const and = (...a) => a.reduce((x, y) => ({ t: 'and', a: x, b: y }))
78
+ const or = (...a) => a.reduce((x, y) => ({ t: 'or', a: x, b: y }))
79
+ const get = (o, k) => ({ t: 'get', o, k })
80
+ const own = (o, k) => ({ t: 'own', o, k })
81
+ const cmp = (op, a, b) => ({ t: 'cmp', op, a, b })
82
+ const obj = (fields) => ({ t: 'obj', fields })
83
+ const isLit = (e) => e.t === 'lit'
84
+ // A statement that does nothing a caller can see: no failure, no return, no
85
+ // call, no write. Its expressions come from a list of reads without effects,
86
+ // so a loop or a read whose body is one can be left out.
87
+ const PURE = new Set(['and', 'or', 'not', 'cmp', 'get', 'own', 'lit', 'var', 'known', 're', 'isArr', 'isObj', 'isObjAny', 'len', 'type', 'plainProto', 'seg', 'cat', 'str'])
88
+ const pure = (e) => !e || e.t === 'lit' || (PURE.has(e.t) && Object.values(e).every((v) => v === null || typeof v !== 'object' || Array.isArray(v) || pure(v)))
89
+ const nop = (n) => !n || (n.t === 'seq' && n.body.every(nop)) || (n.t === 'let' && pure(n.value) && nop(n.body)) || (n.t === 'if' && pure(n.c) && nop(n.then) && nop(n.else))
90
+ // A path or schema path: literals fold now, the rest joins at run time.
91
+ export const cat = (a, suffix) => catE(a, lit(suffix))
92
+ const catE = (a, b) => {
93
+ if (isLit(a) && isLit(b)) return lit(a.v + b.v)
94
+ // x + "a" + "b" is x + "ab"
95
+ if (a.t === 'cat' && isLit(a.b) && isLit(b)) return { t: 'cat', a: a.a, b: lit(a.b.v + b.v) }
96
+ return { t: 'cat', a, b }
97
+ }
98
+
99
+ const seg = (k) => k.replace(/~/g, '~0').replace(/\//g, '~1')
100
+ const INHERITED = (k) => k in Object.prototype
101
+ const IMPLIED = { string: ['isStr'], integer: ['isNum'], number: ['isNum'], array: ['isArr'], object: ['isObjV'] }
102
+
103
+ // `schemas`: the documents a reference may leave this one for, as the
104
+ // current engine's `schemas` option takes them (an object keyed by URI, or an
105
+ // array of schemas with $id); each is normalized in its own dialect, a
106
+ // document without $schema in the root's. `metaschemas`: a Map of the
107
+ // standard metaschemas, loaded when a reference names one.
108
+ export function front (schema, opts = {}) {
109
+ // Whether the dynamic scope is needed is only known once every document a
110
+ // reference reaches has been read: a $dynamicRef can sit in a registry
111
+ // document or a metaschema. A build that met one without tracking the
112
+ // scope is thrown away and done again with it (it never answers on its own).
113
+ const first = build(schema, opts, false)
114
+ return first.sawDynamic && !first.dynamic ? build(schema, opts, true) : first
115
+ }
116
+
117
+ function build (schema, { assertFormat = true, schemas = null, metaschemas = null, errorMessages = false, formats = null, keywords = null, baseURI = null } = {}, forceDynamic) {
118
+ const rootCode = schema && typeof schema === 'object' && schema.$schema !== undefined ? dialect.draftCode(schema.$schema) : 0
119
+ schema = normalize(schema)
120
+ // The address the schema came from is its base when it declares no $id of
121
+ // its own (lib/validator-core.js _materializeSchema).
122
+ if (typeof baseURI === 'string' && baseURI !== '' && schema && typeof schema === 'object' && !Array.isArray(schema) && typeof schema.$id !== 'string') schema = { ...schema, $id: baseURI }
123
+ let registry = null
124
+ if (schemas) {
125
+ registry = new Map()
126
+ const add = (key, doc) => {
127
+ const n = normalize(doc, rootCode)
128
+ if (key) registry.set(key, n)
129
+ const id = n && typeof n === 'object' && typeof n.$id === 'string' ? n.$id : null
130
+ if (id && id !== key) registry.set(id, n)
131
+ }
132
+ if (Array.isArray(schemas)) for (const d of schemas) add(null, d)
133
+ else for (const [k, d] of Object.entries(schemas)) add(k, d)
134
+ }
135
+ // A root that names a registered meta-schema with a $vocabulary loses the
136
+ // keywords of the vocabularies it leaves out, which then mean nothing, as
137
+ // in the current engine (lib/validator-core.js _applyVocabularies).
138
+ if (registry !== null && schema && typeof schema === 'object' && typeof schema.$schema === 'string') {
139
+ const meta = registry.get(schema.$schema)
140
+ if (meta && typeof meta === 'object' && meta.$vocabulary && typeof meta.$vocabulary === 'object') {
141
+ const enabled = vocabularies.enabledKeywords(meta)
142
+ if (enabled) schema = vocabularies.stripDisabledKeywords(schema, enabled)
143
+ }
144
+ }
145
+ const external = metaschemas ? (uri) => { const d = metaschemas.get(uri) || metaschemas.get(uri + '#'); return d === undefined ? undefined : normalize(d) } : null
146
+ const index = buildIndex(schema, registry, external)
147
+ // The v1 dialect, declared at the root (lib/dialect.js isV1Dialect).
148
+ const v1 = dialect.isV1Dialect(schema)
149
+ // Custom keywords (the `keywords` option), normalized as the current engine
150
+ // normalizes them, so a definition it refuses is refused here with the
151
+ // same message. One that reads its data's context is not taken yet.
152
+ const kw = keywords ? keywordsLib.normalizeKeywords(keywords) : null
153
+ // A keyword that reads its data's context (dataContext) may change the
154
+ // value where it sits. The current engine then answers from its collecting
155
+ // walk alone, in its own order, every subschema walked with paths; the IR
156
+ // follows that walk for such a schema (ctx).
157
+ const ctx = kw !== null && keywordsLib.schemaUsesDataContext(schema, kw)
158
+ // What a node's custom keyword compiled to, or its macro expanded to: once
159
+ // per node, as the interpreted engine builds them once per plan, though the
160
+ // IR builds a node for each mode.
161
+ const built = new WeakMap()
162
+ const customOf = (s, k) => {
163
+ let m = built.get(s); if (m === undefined) built.set(s, m = new Map())
164
+ if (m.has(k)) return m.get(k)
165
+ const def = kw[k]; const n = s[k]; let out
166
+ if (def.macro !== null) out = { macro: def.macro(n, s) }
167
+ else if (def.compile !== null) {
168
+ const fn = def.compile(n, s)
169
+ if (typeof fn !== 'function') throw new Error(`keyword "${k}": compile must return a function`)
170
+ out = { keyword: k, fn, types: def.types, before: def.before || null, post: def.post === true, ctx: def.dataContext === true }
171
+ } else {
172
+ const validate = def.validate
173
+ const fn = def.dataContext ? (data, c) => validate(n, data, s, c) : (data) => validate(n, data, s)
174
+ // Errors the function writes land on itself, read from here.
175
+ fn.source = validate
176
+ out = { keyword: k, fn, types: def.types, before: def.before || null, post: def.post === true, ctx: def.dataContext === true }
177
+ }
178
+ m.set(k, out)
179
+ return out
180
+ }
181
+ const usesDyn = (v) => v !== null && typeof v === 'object' && (Array.isArray(v) ? v.some(usesDyn) : ('$dynamicRef' in v || Object.values(v).some(usesDyn)))
182
+ const dynamic = forceDynamic || usesDyn(schema)
183
+ let sawDynamic = false
184
+ const procs = new Map() // id -> { id, mode, base, body }
185
+ const memo = new Map() // mode -> Map(schema -> id)
186
+ let tmp = 0
187
+ const fresh = (p) => p + (++tmp)
188
+ let needsRelate = false
189
+
190
+ // A procedure: one schema object compiled once per mode. `verdict` returns
191
+ // at the first failure and builds no errors; `dual` takes the error list e
192
+ // (null for a verdict); `ann` is dual plus the annotation collector A.
193
+ // Named before its body so a reference cycle calls itself.
194
+ // `entry`: the root as the entry point, whose path is "" and schema path
195
+ // "#", so they fold into literals; a $ref back to the root uses the
196
+ // general procedure.
197
+ function proc (s, mode, entry = false) {
198
+ const key = entry ? mode + ':entry' : mode
199
+ let m = memo.get(key)
200
+ if (!m) memo.set(key, m = new Map())
201
+ if (m.has(s)) return m.get(s)
202
+ const id = fresh(mode[0])
203
+ m.set(s, id)
204
+ const p = { id, mode, base: index.baseOf(s), body: null }
205
+ procs.set(id, p)
206
+ const saved = cur
207
+ cur = { mode, lexBase: index.baseOf(s), ann: mode === 'ann' }
208
+ p.body = entry ? node(s, vr('d'), lit(''), lit('#'), vr('d')) : node(s, vr('d'), vr('P'), vr('SP'), vr('V'))
209
+ cur = saved
210
+ return id
211
+ }
212
+ let cur = null
213
+
214
+ // A failure: the backends turn it into `return false` in a verdict and a
215
+ // pushed error otherwise.
216
+ const fail = (err) => ({ t: 'fail', err })
217
+ // detail and suggest are kernel functions; `src` is how generated code
218
+ // names them (an import, or a construction such as a format's hint).
219
+ const ref = (fn, src) => fn ? { fn, src: src || fn.name } : null
220
+ const err = (code, kw, path, sp, params, msg, exp, rank, ev, detail, suggest) =>
221
+ ({ code, kw, path, sp, params, msg, exp, rank, ev, detail: detail && detail.fn ? detail : ref(detail), suggest: suggest && suggest.fn ? suggest : ref(suggest) })
222
+ const paramsOf = (o) => obj(Object.entries(o).map(([k, v]) => [k, lit(v)]))
223
+
224
+ // Run a subschema in place (same value): call its procedure. Under
225
+ // annotations the call gets a collector of its own, merged when valid.
226
+ // `ann`: whether the calling node collects annotations (nodeBody's own
227
+ // flag; cur.ann is already cleared for the node's children by then).
228
+ const inPlace = (s, x, path, sp, ev, ann = cur.ann) => {
229
+ if (cur.mode === 'verdict' && !ann) return { t: 'require', e: { t: 'pcall', proc: proc(s, 'verdict'), x, e: 'null' } }
230
+ return { t: 'require', e: { t: 'pcall', proc: proc(s, ann ? 'ann' : 'dual'), x, path, sp, ev, e: 'inherit', ann: ann ? 'merge' : null } }
231
+ }
232
+ // A subschema's verdict. Under ctx the full walk with its paths, its errors
233
+ // set aside, as the current engine walks it (every keyword runs).
234
+ const verdictOf = (s, x, path, sp, ev) => ctx ? { t: 'pcall', proc: proc(s, 'dual'), x, path, sp, ev: ev || x, e: 'fresh' } : { t: 'pcall', proc: proc(s, 'verdict'), x, e: 'null' }
235
+
236
+ function node (s, x, path, sp, ev) {
237
+ if (s === true) return seq()
238
+ if (s === false) return fail(err(falseSchema.code, falseSchema.keyword, path, sp, obj([]), lit(falseSchema.msg), undefined, falseSchema.rank, ev))
239
+ if (!isObj(s)) throw new Error('ir: a schema must be an object or a boolean')
240
+ // A subschema of another resource puts that resource on the dynamic
241
+ // scope: it runs as a procedure, which does the push.
242
+ if (dynamic && !cur.ann && index.baseOf(s) !== cur.lexBase) return inPlace(s, x, path, sp, ev)
243
+ // A node that reads annotations runs as an `ann` procedure with a fresh
244
+ // collector; inside one, the collector is A.
245
+ if (!cur.ann && ('unevaluatedProperties' in s || 'unevaluatedItems' in s)) {
246
+ // In a verdict the names a node evaluates can often be read off the
247
+ // schema: properties and patternProperties here and behind $ref and
248
+ // allOf, which must all pass anyway, so a failing subschema dropping
249
+ // its annotations changes no verdict. Then unevaluatedProperties is a
250
+ // check of names, with no collector (yamllint, 25 such nodes).
251
+ if (cur.mode === 'verdict' && !dynamic && !('unevaluatedItems' in s)) {
252
+ const st = staticNames(s, 0, true)
253
+ if (st !== null) {
254
+ const saved = cur.ann
255
+ cur.ann = false
256
+ try { return nodeBody(s, x, path, sp, ev, false, st) } finally { cur.ann = saved }
257
+ }
258
+ }
259
+ if (cur.mode === 'verdict') return { t: 'require', e: { t: 'pcall', proc: proc(s, 'ann'), x, path, sp, ev, e: 'null', ann: 'none' } }
260
+ return { t: 'require', e: { t: 'pcall', proc: proc(s, 'ann'), x, path, sp, ev, e: 'inherit', ann: 'none' } }
261
+ }
262
+ const ann = cur.ann
263
+ // Children at other instance locations start without a collector.
264
+ const saved = cur.ann
265
+ cur.ann = false
266
+ try { return nodeBody(s, x, path, sp, ev, ann) } finally { cur.ann = saved }
267
+ }
268
+
269
+ // Children at another instance location: no collector, same procedure mode.
270
+ const child = (s, x, path, sp) => {
271
+ const saved = cur.ann
272
+ cur.ann = false
273
+ try { return node(s, x, path, sp, x) } finally { cur.ann = saved }
274
+ }
275
+
276
+ // The property names and patterns a schema evaluates whenever it passes,
277
+ // when the schema alone says so: null if a keyword makes them depend on the
278
+ // value (anyOf, oneOf, if, dependentSchemas, a dynamic reference) or
279
+ // evaluates every name (additionalProperties, unevaluatedProperties).
280
+ const STATIC_STOP = new Set(['additionalProperties', 'unevaluatedProperties', 'unevaluatedItems', 'anyOf', 'oneOf', 'if', 'then', 'else', 'dependentSchemas', 'dependencies', '$dynamicRef', '$recursiveRef'])
281
+ // A schema met again on the way down (a cycle through $ref) gives null;
282
+ // answers are kept per schema, so shared subschemas are read once (a null
283
+ // kept from a cycle only sends a node to the collector, which is exact).
284
+ const staticActive = new Set()
285
+ const staticMemo = new Map()
286
+ function staticNames (s, depth, top = false) {
287
+ if (s === true) return { names: [], pats: [] }
288
+ if (!isObj(s) || depth > 100 || staticActive.has(s)) return null
289
+ if (!top && staticMemo.has(s)) return staticMemo.get(s)
290
+ staticActive.add(s)
291
+ let r
292
+ try { r = staticWalk(s, depth, top) } finally { staticActive.delete(s) }
293
+ if (!top) staticMemo.set(s, r)
294
+ return r
295
+ }
296
+ function staticWalk (s, depth, top) {
297
+ const names = []; const pats = []
298
+ const add = (r) => { if (r === null) return false; names.push(...r.names); pats.push(...r.pats); return true }
299
+ for (const k in s) {
300
+ if (top && k === 'unevaluatedProperties') continue
301
+ if (STATIC_STOP.has(k)) return null
302
+ if (k === 'properties' && isObj(s.properties)) names.push(...Object.keys(s.properties))
303
+ else if (k === 'patternProperties' && isObj(s.patternProperties)) pats.push(...Object.keys(s.patternProperties))
304
+ else if (k === '$ref') {
305
+ let t; try { t = index.resolve(s.$ref, index.baseOf(s)) } catch { return null }
306
+ if (!add(staticNames(t, depth + 1))) return null
307
+ } else if (k === 'allOf') {
308
+ if (!Array.isArray(s.allOf)) return null
309
+ for (const b of s.allOf) if (!add(staticNames(b, depth + 1))) return null
310
+ }
311
+ }
312
+ return { names: [...new Set(names)], pats: [...new Set(pats)] }
313
+ }
314
+
315
+ function nodeBody (s, x, path, sp, ev, ann, staticUP = null) {
316
+ const verdict = cur.mode === 'verdict'
317
+ // In a verdict the first failure returns, so a single type checked first
318
+ // makes the type known to every later keyword: the guards it implies go.
319
+ const known = verdict && typeof s.type === 'string' && TYPES[s.type] ? s.type : null
320
+ const implied = (g) => known !== null && (IMPLIED[known] || []).includes(g)
321
+ const objKw = ['properties', 'required', 'additionalProperties', 'patternProperties', 'propertyNames', 'dependentRequired', 'dependentSchemas', 'unevaluatedProperties'].some((k) => k in s)
322
+ const knownObj = known === 'object' && objKw
323
+ const o = vr(fresh('o')); const plain = vr(fresh('p'))
324
+ // Is x an object, and is its prototype Object.prototype (then a defined
325
+ // value under a name Object.prototype lacks is an own property).
326
+ const has = (name, kExpr) => name !== null && INHERITED(name) ? own(x, kExpr) : or(plain, own(x, kExpr))
327
+ const props = isObj(s.properties) ? s.properties : {}
328
+ const req = knownObj && Array.isArray(s.required) ? s.required : []
329
+ const typedReq = new Set(req.filter((r) => !INHERITED(r) && isObj(props[r]) && 'type' in props[r]))
330
+ let out = []
331
+ const lets = []
332
+ if (objKw && !knownObj) lets.push([o.n, { t: 'isObj', x }], [plain.n, and(o, { t: 'plainProto', x })])
333
+ const keys = known !== null ? ['type', ...Object.keys(s).filter((k) => k !== 'type')] : Object.keys(s)
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)
356
+ // Under ctx each keyword's statements go to its place in the current
357
+ // engine's walk (lib/interpreter-core.js eval), not schema order.
358
+ const buckets = ctx ? new Map() : null
359
+ const bucket = (r) => { let b = buckets.get(r); if (b === undefined) buckets.set(r, b = []); return b }
360
+ for (const k of keys) {
361
+ const n = s[k]
362
+ if (ctx) {
363
+ out = bucket(kw[k] !== undefined && kw[k].macro !== null ? CTX_MACRO : CTX_RANK[k] !== undefined ? CTX_RANK[k] : CTX_LAST)
364
+ // Folded into the one walk over the object's own keys, below.
365
+ if (k === 'properties' || k === 'patternProperties' || k === 'additionalProperties') continue
366
+ // Missing, read the current engine's way: an own key, and for a name
367
+ // properties does not declare, a defined value.
368
+ if (k === 'required' && Array.isArray(n)) {
369
+ needsRelate = needsRelate || s.additionalProperties !== undefined
370
+ out.push(iff(o, seq(n.map((r) => iff(r in props ? not(own(x, lit(r))) : or(not(own(x, lit(r))), cmp('===', get(x, lit(r)), lit(undefined))), fail(err(required.code, 'required', path, cat(sp, '/required'), paramsOf(required.params(r)), lit(required.msg(r)), required.exp(r), required.rank, ev, required.detail, required.suggest)))))))
371
+ continue
372
+ }
373
+ if (k === 'dependentRequired' && isObj(n)) {
374
+ const inner = []
375
+ for (const a in n) inner.push(iff(own(x, lit(a)), seq(n[a].map((r) => iff(not(own(x, 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)))))))
376
+ out.push(iff(o, seq(inner)))
377
+ continue
378
+ }
379
+ }
380
+ if (kw !== null && kw[k] !== undefined) {
381
+ const c = customOf(s, k)
382
+ if (c.macro === undefined) { if (c.fn) customOps.push(c); continue }
383
+ // A macro: the schema it returns runs in place, under the keyword's
384
+ // path, and the keyword reports an error of its own when it fails.
385
+ const m = c.macro
386
+ if (m !== null && typeof m === 'object' && JSON.stringify(m).includes('"$ref"')) throw new Error(`ir: macro keyword "${k}" returns a $ref`)
387
+ const msp = cat(sp, '/' + k)
388
+ const own = fail(err('ATA9001', k, path, msp, paramsOf({ keyword: k }), lit(`must pass "${k}" keyword validation`), undefined, 2, ev))
389
+ out.push(iff(not(verdict ? verdictOf(m, x, path, msp, ev) : { t: 'pcall', proc: proc(m, ann ? 'ann' : 'dual'), x, path, sp: msp, ev, e: 'inherit', ann: ann ? 'merge' : null }), own))
390
+ continue
391
+ }
392
+ if (k === 'type') {
393
+ out.push(iff(not({ t: 'type', x, types: typeof n === 'string' ? [n] : n }), fail(err(type.code, 'type', path, cat(sp, '/type'), obj([['type', lit(n)]]), lit(type.msg(n)), type.exp(n), type.rank, ev, type.detail, type.suggest))))
394
+ if (knownObj) {
395
+ out.push({ t: 'bind', names: [[o.n, lit(true)], [plain.n, { t: 'plainProto', x }]] })
396
+ if (req.length) {
397
+ out.push(iff(and(not(plain), not(and(...req.map((r) => own(x, lit(r)))))), { t: 'ret' }))
398
+ for (const r of req) if (!typedReq.has(r)) out.push(iff(INHERITED(r) ? or(cmp('===', get(x, lit(r)), lit(undefined)), not(own(x, lit(r)))) : cmp('===', get(x, lit(r)), lit(undefined)), { t: 'ret' }))
399
+ }
400
+ }
401
+ } else if (k === 'properties') {
402
+ const inner = []
403
+ // The entries that check their own presence, by name, for a backend
404
+ // that would rather visit the object's keys than look up every name
405
+ // (a verdict on an object that names hundreds of properties). Not
406
+ // with annotations or custom keywords, whose order is observable.
407
+ const keyed = []
408
+ for (const p in n) {
409
+ const y = vr(fresh('x'))
410
+ const body = child(n[p], y, isLit(path) && path.v === '' ? lit('/' + seg(p)) : cat(path, '/' + seg(p)), cat(sp, '/properties/' + seg(p)))
411
+ // An own key is evaluated even when its value is undefined.
412
+ if (ann) inner.push(iff(own(x, lit(p)), { t: 'do', e: { t: 'annKey', k: lit(p) } }))
413
+ if (!ann && nop(body)) continue
414
+ if (typedReq.has(p)) inner.push({ t: 'let', name: y.n, value: get(x, lit(p)), body })
415
+ else { keyed.push([p, inner.length]); inner.push({ t: 'let', name: y.n, value: get(x, lit(p)), body: iff(and(cmp('!==', y, lit(undefined)), has(p, lit(p))), body) }) }
416
+ }
417
+ // closed: every name properties lists, where additionalProperties is
418
+ // false with no patterns, for a verdict that tells the unknown names
419
+ // apart in the same pass over the object's names (lib/ir/emit.mjs).
420
+ const closed = s.additionalProperties === false && !isObj(s.patternProperties) && !ann && kw === null ? Object.keys(props) : null
421
+ if (inner.length) out.push(iff(o, { t: 'props', x, body: inner, keyed: ann || kw !== null ? null : keyed, closed }))
422
+ } else if (k === 'patternProperties') {
423
+ if (patsFused) continue
424
+ if (canFuse) { out.push(fusedPats()); continue }
425
+ const kv = vr(fresh('k'))
426
+ const inner = []
427
+ for (const p in n) {
428
+ const y = vr(fresh('x'))
429
+ const body = child(n[p], y, catE(path, catE(lit('/'), { t: 'seg', k: kv })), cat(sp, '/patternProperties/' + seg(p)))
430
+ if (!ann && nop(body)) continue
431
+ inner.push(iff({ t: 're', src: p, k: kv }, seq(ann ? { t: 'do', e: { t: 'annKey', k: kv } } : null, { t: 'let', name: y.n, value: get(x, kv), body })))
432
+ }
433
+ if (inner.length) out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'keys', body: seq(inner) }))
434
+ } else if (k === 'required') {
435
+ needsRelate = needsRelate || s.additionalProperties !== undefined
436
+ if (knownObj) continue
437
+ out.push(iff(o, seq(n.map((r) => iff(or(cmp('===', get(x, lit(r)), lit(undefined)), not(has(r, lit(r)))), fail(err(required.code, 'required', path, cat(sp, '/required'), paramsOf(required.params(r)), lit(required.msg(r)), required.exp(r), required.rank, ev, required.detail, required.suggest)))))))
438
+ } else if (k === 'dependentRequired') {
439
+ const inner = []
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)))))))
441
+ out.push(iff(o, seq(inner)))
442
+ } else if (k === 'additionalProperties') {
443
+ if (patsFused) continue
444
+ if (canFuse) { out.push(fusedPats()); continue }
445
+ const known2 = Object.keys(s.properties || {})
446
+ const pats = Object.keys(s.patternProperties || {})
447
+ const kv = vr(fresh('k'))
448
+ // Is the key named by properties or matched by patternProperties.
449
+ const isKnown = { t: 'known', k: kv, names: known2, pats }
450
+ if (n === false) {
451
+ // for-in visits own enumerable keys in Object.keys order; the own
452
+ // check drops inherited ones.
453
+ out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'forin', apClosed: !pats.length && !ann && kw === null, body: iff(and(not(isKnown), has(null, kv)), seq(ann ? { t: 'do', e: { t: 'annKey', k: kv } } : null, fail(err(additionalProperties.code, 'additionalProperties', path, cat(sp, '/additionalProperties'), obj([['additionalProperty', kv]]), lit(additionalProperties.msg()), undefined, additionalProperties.rank, ev, additionalProperties.detail)))) }))
454
+ } else {
455
+ const y = vr(fresh('x'))
456
+ const body = child(n, y, catE(path, catE(lit('/'), { t: 'seg', k: kv })), cat(sp, '/additionalProperties'))
457
+ if (!ann && nop(body)) continue
458
+ out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'protoLast', body: iff(not(isKnown), seq(ann ? { t: 'do', e: { t: 'annKey', k: kv } } : null, { t: 'let', name: y.n, value: get(x, kv), body })) }))
459
+ }
460
+ } else if (k === 'propertyNames') {
461
+ // The name is checked; the errors report the object.
462
+ const kv = vr(fresh('k'))
463
+ const saved = cur.ann; cur.ann = false
464
+ const body = node(n, kv, path, cat(sp, '/propertyNames'), x)
465
+ cur.ann = saved
466
+ out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'keys', body }))
467
+ } else if (k === 'prefixItems') {
468
+ const inner = []
469
+ if (ann) inner.push({ t: 'do', e: { t: 'annPrefix', n: n.length, arr: x } })
470
+ n.forEach((ps, i) => {
471
+ const y = vr(fresh('x'))
472
+ inner.push(iff(cmp('>', { t: 'len', x }, lit(i)), { t: 'let', name: y.n, value: get(x, lit(i)), body: child(ps, y, isLit(path) && path.v === '' ? lit('/' + i) : cat(path, '/' + i), cat(sp, '/prefixItems/' + i)) }))
473
+ })
474
+ out.push(iff({ t: 'isArr', x }, seq(inner)))
475
+ } else if (k === 'items') {
476
+ const start = Array.isArray(s.prefixItems) ? s.prefixItems.length : 0
477
+ const j = vr(fresh('j')); const y = vr(fresh('x'))
478
+ const body = child(n, y, catE(path, catE(lit('/'), j)), cat(sp, '/items'))
479
+ out.push(iff({ t: 'isArr', x }, seq(ann ? iff(cmp('>', { t: 'len', x }, lit(start)), { t: 'do', e: { t: 'annAll' } }) : null, { t: 'range', arr: x, from: start, i: j.n, body: { t: 'let', name: y.n, value: get(x, j), body } })))
480
+ } else if (k === 'contains') {
481
+ const min = s.minContains === undefined ? 1 : s.minContains
482
+ const max = s.maxContains
483
+ const c = vr(fresh('c')); const i = vr(fresh('i'))
484
+ const checks = [iff(cmp('<', c, lit(min)), fail(err(contains.code, 'contains', path, cat(sp, '/contains'), obj([['minContains', lit(min)]]), lit(`must contain at least ${min} valid item(s)`), undefined, contains.rank, ev)))]
485
+ if (max !== undefined) checks.push(iff(cmp('>', c, lit(max)), fail(err(contains.code, 'contains', path, cat(sp, '/contains'), obj([['minContains', lit(min)], ['maxContains', lit(max)]]), lit(`must NOT contain more than ${max} valid item(s)`), undefined, contains.rank, ev))))
486
+ out.push(iff({ t: 'isArr', x }, { t: 'count', name: c.n, body: seq({ t: 'range', arr: x, from: 0, i: i.n, body: iff(ctx ? verdictOf(n, get(x, i), catE(path, catE(lit('/'), i)), cat(sp, '/contains')) : { t: 'pcall', proc: proc(n, 'verdict'), x: get(x, i), e: 'null' }, seq({ t: 'incr', name: c.n }, ann ? { t: 'do', e: { t: 'annAt', i } } : null)) }, checks) }))
487
+ } else if (k === 'allOf') {
488
+ n.forEach((sub, i) => { out.push(ann ? inPlace(sub, x, path, cat(sp, '/allOf/' + i), ev, true) : node(sub, x, path, cat(sp, '/allOf/' + i), ev)) })
489
+ } else if (k === 'anyOf' || k === 'oneOf') {
490
+ const mode = verdict ? 'verdict' : ann ? 'ann' : 'dual'
491
+ out.push({ t: 'branches', kw: k, procs: n.map((sub) => proc(sub, mode)), sps: n.map((_, i) => cat(sp, '/' + k + '/' + i)), titles: n.map((sub) => isObj(sub) ? sub.title : undefined), x, path, sp: cat(sp, '/' + k), ev, ann: ann ? (k === 'oneOf' ? 'onlyIfOne' : 'each') : null, rank: branchRank, collapse })
492
+ } else if (k === 'not') {
493
+ out.push(iff(verdictOf(n, x, path, cat(sp, '/not'), ev), fail(err(notK.code, notK.keyword, path, cat(sp, '/not'), obj([]), lit(notK.msg), undefined, notK.rank, ev))))
494
+ } else if (k === 'if') {
495
+ const thenB = 'then' in s ? (ann ? inPlace(s.then, x, path, cat(sp, '/then'), ev, true) : node(s.then, x, path, cat(sp, '/then'), ev)) : seq()
496
+ const elseB = 'else' in s ? (ann ? inPlace(s.else, x, path, cat(sp, '/else'), ev, true) : node(s.else, x, path, cat(sp, '/else'), ev)) : seq()
497
+ const cond = ann ? { t: 'pcall', proc: proc(n, 'ann'), x, path, sp: cat(sp, '/if'), ev, e: ctx ? 'fresh' : 'null', ann: 'merge' } : verdictOf(n, x, path, cat(sp, '/if'), ev)
498
+ out.push(iff(cond, thenB, elseB))
499
+ } else if (k === 'then' || k === 'else') {
500
+ // Read by if; without an if they do nothing.
501
+ } else if (k === 'dependentSchemas') {
502
+ for (const a in n) out.push(iff(and(o, own(x, lit(a))), ann ? inPlace(n[a], x, path, cat(sp, '/dependentSchemas/' + seg(a)), ev, true) : node(n[a], x, path, cat(sp, '/dependentSchemas/' + seg(a)), ev)))
503
+ } else if (k === '$ref') {
504
+ // The target runs as if it stood here: same value, same paths. One
505
+ // that names nothing fails every value that reaches it, with the
506
+ // current engine's error, rather than refusing the schema.
507
+ let target
508
+ try { target = index.resolve(n, index.baseOf(s)) } catch (e) {
509
+ if (e.unresolved !== true) throw e
510
+ out.push(fail(err('ATA5001', '$ref', path, cat(sp, '/$ref'), paramsOf({ ref: n }), lit(`cannot resolve $ref ${n}`), undefined, 2, ev)))
511
+ continue
512
+ }
513
+ out.push(inPlace(target, x, path, sp, ev, ann))
514
+ } else if (k === '$dynamicRef') {
515
+ sawDynamic = true
516
+ // Resolved like $ref first; when that target declares the anchor the
517
+ // fragment names, the outermost resource on the scope that declares
518
+ // it wins. The candidates are known now, the choice at run time.
519
+ const hash = n.indexOf('#')
520
+ const name = hash !== -1 && !n.slice(hash + 1).startsWith('/') ? n.slice(hash + 1) : null
521
+ // Under v1 the scope is searched even when the reference resolves to
522
+ // nothing on its own; with no anchor of that name on the scope it
523
+ // fails where it runs (lib/interpreter-core.js).
524
+ let target
525
+ try { target = index.resolve(n, index.baseOf(s)) } catch (e) { if (!(v1 && e.unresolved && name)) throw e }
526
+ if (target === undefined) {
527
+ const missing = err('ATA9001', '$dynamicRef', path, cat(sp, '/$dynamicRef'), paramsOf({ ref: n }), lit(`cannot resolve $dynamicRef ${n}`), undefined, 2, ev)
528
+ const candidates = index.dynCandidates(name)
529
+ if (candidates.length === 0) { out.push(fail(missing)); continue }
530
+ const mode = verdict && !ann ? 'verdict' : ann ? 'ann' : 'dual'
531
+ const call = mode === 'verdict' ? { t: 'pcall', proc: null, x, e: 'null' } : { t: 'pcall', proc: null, x, path, sp, ev, e: 'inherit', ann: ann ? 'merge' : null }
532
+ call.dynamic = candidates.map(([b, t]) => [b, proc(t, mode)])
533
+ call.missing = missing
534
+ out.push({ t: 'require', e: call })
535
+ continue
536
+ }
537
+ // Under 2020-12 the target must carry the anchor itself (bookending)
538
+ // before the scope is consulted; v1 drops that.
539
+ const dyn = name !== null && name !== '' && (v1 || (isObj(target) && target.$dynamicAnchor === name))
540
+ const r = inPlace(target, x, path, sp, ev, ann)
541
+ if (dyn) {
542
+ const mode = r.e.proc[0] === 'v' ? 'verdict' : r.e.proc[0] === 'a' ? 'ann' : 'dual'
543
+ r.e.dynamic = index.dynCandidates(name).map(([b, t]) => [b, proc(t, mode)])
544
+ }
545
+ out.push(r)
546
+ } else if (k === 'unevaluatedProperties' || k === 'unevaluatedItems') {
547
+ // Run after every other keyword of the node, below.
548
+ } else if (k === '$id' || k === '$anchor' || k === '$dynamicAnchor' || k === '$vocabulary' || k === '$defs' || k === 'definitions' || k === 'minContains' || k === 'maxContains' || k === 'additionalItems' || k === 'contentMediaType' || k === 'contentEncoding' || k === 'contentSchema') {
549
+ // additionalItems that normalization left in place has no effect (items is not a tuple);
550
+ // the content keywords are annotations, as in the current engine.
551
+ // Read by the index or by another keyword.
552
+ } else if (k === 'errorMessage' && errorMessages) {
553
+ // Applied to the finished errors by the backend that asked for it
554
+ // (lib/error-messages.js, as the current engine applies it).
555
+ } else if (k === '$schema') {
556
+ // Any other URI is read as the default dialect, as the current engine
557
+ // reads it (lib/dialect.js); v1 is told apart at the root.
558
+ } else if (k === 'format' && !assertFormat) {
559
+ // An annotation unless format assertion is asked for.
560
+ } else if (LEAVES[k]) {
561
+ const K = LEAVES[k]
562
+ // A format the caller registered as a function (the `formats` option)
563
+ // replaces the built-in check of that name, keeps its error code and
564
+ // loses its hint, as in the current engine; any other value there is
565
+ // ignored.
566
+ const user = k === 'format' && formats !== null && typeof n === 'string' && Object.prototype.hasOwnProperty.call(formats, n) && typeof formats[n] === 'function' ? formats[n] : null
567
+ const pv = user !== null ? user : K.prepare ? K.prepare(n) : n
568
+ // A format the current engine does not know is not checked.
569
+ if (k === 'format' && pv === null) continue
570
+ const gname = GUARD_NAME.get(K.on)
571
+ if (gname === undefined) throw new Error(`ir: keyword "${k}" has no guard`)
572
+ const guard = implied(gname) ? null : { t: 'guard', g: gname, x }
573
+ const code = typeof K.code === 'function' ? K.code(n) : K.code
574
+ let suggest = K.suggest || null
575
+ if (K.suggestFor) { const sf = user === null ? K.suggestFor(n) : null; suggest = sf ? ref(sf, `makeHintSuggest(${n}Hint)`) : null }
576
+ if (K.fail) {
577
+ const f = vr(fresh('f'))
578
+ out.push(iff(guard || lit(true), { t: 'let', name: f.n, value: { t: 'kfail', K, x, n, pv }, body: iff(cmp('!==', f, lit(null)), fail(err(code, k, path, cat(sp, '/' + k), { t: 'field', o: f, k: 'params' }, { t: 'field', o: f, k: 'msg' }, K.exp(n), K.rank, ev, K.detail, suggest))) }))
579
+ continue
580
+ }
581
+ const check = not({ t: 'kernel', K, kw: k, x, n, pv })
582
+ out.push(iff(guard ? and(guard, check) : check, fail(err(code, k, path, cat(sp, '/' + k), paramsOf(K.params(n)), lit(K.msg(n)), K.exp(n), K.rank, ev, K.detail, suggest))))
583
+ } else if (MEANINGFUL.has(k) && !ANNOTATIONS.has(k)) {
584
+ throw new Error(`ir: keyword "${k}" is not supported`)
585
+ }
586
+ // Any other name means nothing to the current engine, which ignores it
587
+ // (tsType, markdownDescription, x-...), and so does the IR.
588
+ }
589
+ // The node's custom keywords, in the order they declare (orderCustom):
590
+ // each runs on the value when its `type` admits it.
591
+ const ordered = customOps.length ? orderCustom(customOps) : []
592
+ let post = null
593
+ if (!ctx) { if (ordered.length) out.push({ t: 'custom', ops: ordered, x, path, sp, ev }) } else {
594
+ const now = ordered.filter((op) => !op.post)
595
+ if (now.length) bucket(CTX_CUSTOM).push({ t: 'custom', ops: now, x, path, sp, ev })
596
+ const later = ordered.filter((op) => op.post)
597
+ // Run after everything else at the node, on the value read again from
598
+ // where it sits.
599
+ if (later.length) post = { t: 'custom', ops: later, x, path, sp, ev, here: true }
600
+ if ('properties' in s || 'patternProperties' in s || 'additionalProperties' in s) bucket(CTX_RANK.properties).push(ctxObjectWalk(s, x, path, sp, ev, ann, o, props))
601
+ out = []
602
+ for (const r of [...buckets.keys()].sort((a, b) => a - b)) out.push(...buckets.get(r))
603
+ }
604
+ if (staticUP !== null) {
605
+ // A verdict whose evaluated names are known: the names outside them
606
+ // meet unevaluatedProperties, as additionalProperties checks its own.
607
+ const n = s.unevaluatedProperties
608
+ const kv = vr(fresh('k'))
609
+ const isKnown = { t: 'known', k: kv, names: staticUP.names, pats: staticUP.pats }
610
+ if (n === false) out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'keys', body: iff(not(isKnown), { t: 'ret' }) }))
611
+ else if (n !== true) {
612
+ const y = vr(fresh('x'))
613
+ const body = child(n, y, catE(path, catE(lit('/'), { t: 'seg', k: kv })), cat(sp, '/unevaluatedProperties'))
614
+ out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'keys', body: iff(not(isKnown), { t: 'let', name: y.n, value: get(x, kv), body }) }))
615
+ }
616
+ } else if ('unevaluatedProperties' in s) {
617
+ const n = s.unevaluatedProperties
618
+ const kv = vr(fresh('k'))
619
+ if (n === false) {
620
+ out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'keys', body: iff(not({ t: 'annHasKey', k: kv }), fail(err(uPK.code, 'unevaluatedProperties', path, cat(sp, '/unevaluatedProperties'), obj([['unevaluatedProperty', kv]]), lit(uPK.msg), undefined, uPK.rank, ev, uPK.detail))) }))
621
+ } else {
622
+ const y = vr(fresh('x'))
623
+ const body = child(n, y, catE(path, catE(lit('/'), { t: 'seg', k: kv })), cat(sp, '/unevaluatedProperties'))
624
+ out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'protoLast', body: iff(not({ t: 'annHasKey', k: kv }), seq({ t: 'let', name: y.n, value: get(x, kv), body }, { t: 'do', e: { t: 'annKey', k: kv } })) }))
625
+ }
626
+ }
627
+ if ('unevaluatedItems' in s) {
628
+ const n = s.unevaluatedItems
629
+ const j = vr(fresh('j'))
630
+ if (n === false) {
631
+ out.push(iff(and({ t: 'isArr', x }, not({ t: 'annAllSet' })), seq({ t: 'let', name: j.n, value: { t: 'firstUneval', x }, body: iff(cmp('<', j, { t: 'len', x }), fail(err(uIK.code, 'unevaluatedItems', path, cat(sp, '/unevaluatedItems'), obj([['limit', j]]), { t: 'str', parts: [lit('must NOT have more than '), j, lit(' items')] }, undefined, uIK.rank, ev))) }, { t: 'do', e: { t: 'annAll' } })))
632
+ } else {
633
+ const y = vr(fresh('x'))
634
+ const body = child(n, y, catE(path, catE(lit('/'), j)), cat(sp, '/unevaluatedItems'))
635
+ out.push(iff(and({ t: 'isArr', x }, not({ t: 'annAllSet' })), seq({ t: 'range', arr: x, from: { t: 'annN' }, i: j.n, body: iff(not({ t: 'annAtHas', i: j }), { t: 'let', name: y.n, value: get(x, j), body }) }, { t: 'do', e: { t: 'annAll' } })))
636
+ }
637
+ }
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)
645
+ let body = seq(out)
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 }
647
+ return body
648
+ }
649
+
650
+ // The object's own keys, read once, each sent where the current engine
651
+ // sends it, in the document's order: to its declared property (a value of
652
+ // undefined is skipped, and is missing when required), to every pattern it
653
+ // matches, and to additionalProperties when neither took it.
654
+ function ctxObjectWalk (s, x, path, sp, ev, ann, o, props) {
655
+ const kv = vr(fresh('k'))
656
+ const reqIn = new Set(Array.isArray(s.required) ? s.required.filter((r) => r in props) : [])
657
+ const body = []
658
+ for (const p in props) {
659
+ const y = vr(fresh('x'))
660
+ const missing = reqIn.has(p) ? fail(err(required.code, 'required', path, cat(sp, '/required'), paramsOf(required.params(p)), lit(required.msg(p)), required.exp(p), required.rank, ev, required.detail, required.suggest)) : seq()
661
+ body.push(iff(cmp('===', kv, lit(p)), seq(ann ? { t: 'do', e: { t: 'annKey', k: kv } } : null, { t: 'let', name: y.n, value: get(x, kv), body: iff(cmp('!==', y, lit(undefined)), child(props[p], y, isLit(path) && path.v === '' ? lit('/' + seg(p)) : cat(path, '/' + seg(p)), cat(sp, '/properties/' + seg(p))), missing) })))
662
+ }
663
+ const pats = isObj(s.patternProperties) ? s.patternProperties : {}
664
+ for (const src in pats) {
665
+ const y = vr(fresh('x'))
666
+ body.push(iff({ t: 're', src, k: kv }, seq(ann ? { t: 'do', e: { t: 'annKey', k: kv } } : null, { t: 'let', name: y.n, value: get(x, kv), body: child(pats[src], y, catE(path, catE(lit('/'), { t: 'seg', k: kv })), cat(sp, '/patternProperties/' + seg(src))) })))
667
+ }
668
+ const ap = s.additionalProperties
669
+ if (ap !== undefined) {
670
+ const isKnown = { t: 'known', k: kv, names: Object.keys(props), pats: Object.keys(pats) }
671
+ if (ap === false) body.push(iff(not(isKnown), seq(ann ? { t: 'do', e: { t: 'annKey', k: kv } } : null, fail(err(additionalProperties.code, 'additionalProperties', path, cat(sp, '/additionalProperties'), obj([['additionalProperty', kv]]), lit(additionalProperties.msg()), undefined, additionalProperties.rank, ev, additionalProperties.detail)))))
672
+ else {
673
+ const y = vr(fresh('x'))
674
+ body.push(iff(not(isKnown), seq(ann ? { t: 'do', e: { t: 'annKey', k: kv } } : null, { t: 'let', name: y.n, value: get(x, kv), body: child(ap, y, catE(path, catE(lit('/'), { t: 'seg', k: kv })), cat(sp, '/additionalProperties')) })))
675
+ }
676
+ }
677
+ return iff(o, { t: 'keys', obj: x, k: kv.n, order: 'keys', body: seq(body) })
678
+ }
679
+
680
+ // ---- Defaults ------------------------------------------------------------
681
+ // A specialized fill, in the order and by the rules of lib/defaults.js:
682
+ // a node's own property defaults (absent own keys get a fresh copy), then
683
+ // the property schemas for own keys present, then array elements (prefixItems
684
+ // by position, items for the rest), then dependentSchemas for keys present,
685
+ // then $ref targets and allOf members on the same value. Declined (null)
686
+ // when the fill graph has a cycle or patternProperties/additionalProperties
687
+ // lead to a default: there lib/defaults.js, with its guards for values that
688
+ // contain themselves, is used as it is.
689
+ const fills = new Map()
690
+ let fillDeclined = false
691
+ const fillStack = new Set()
692
+ const reaches = new Map()
693
+ const reachesDefault = (n) => {
694
+ if (!isObj(n)) return false
695
+ if (reaches.has(n)) return reaches.get(n)
696
+ reaches.set(n, false)
697
+ const props = isObj(n.properties) ? Object.values(n.properties) : []
698
+ let yes = props.some((p) => isObj(p) && p.default !== undefined)
699
+ const below = [...props, typeof n.$ref === 'string' ? safeResolve(n.$ref, n) : null, ...(Array.isArray(n.allOf) ? n.allOf : []), ...(Array.isArray(n.prefixItems) ? n.prefixItems : []), n.items, n.additionalProperties, ...(isObj(n.patternProperties) ? Object.values(n.patternProperties) : []), ...(isObj(n.dependentSchemas) ? Object.values(n.dependentSchemas) : [])]
700
+ for (const b of below) if (!yes && reachesDefault(b)) yes = true
701
+ reaches.set(n, yes)
702
+ return yes
703
+ }
704
+ const safeResolve = (ref, n) => { try { return index.resolve(ref, index.baseOf(n)) } catch { return null } }
705
+ // From this many properties with defaults inside, a fill walks the object's
706
+ // own names (see fillWalk below).
707
+ const FILL_WALK_MIN = 16
708
+ const MASK_MIN = 24
709
+ const fillProc = (n) => {
710
+ if (!isObj(n) || !reachesDefault(n)) return null
711
+ if (fills.has(n)) return fills.get(n)
712
+ if (fillStack.has(n)) { fillDeclined = true; return null }
713
+ if (isObj(n.patternProperties) && Object.values(n.patternProperties).some(reachesDefault)) fillDeclined = true
714
+ if (reachesDefault(n.additionalProperties)) fillDeclined = true
715
+ fillStack.add(n)
716
+ const id = fresh('f')
717
+ const d = vr('d')
718
+ const out = []
719
+ const props = isObj(n.properties) ? n.properties : {}
720
+ // Whether d is an ordinary object, read once: then a defined value under a
721
+ // name Object.prototype lacks is an own property, and a read decides
722
+ // whether a property's own fill runs.
723
+ const pl = vr(fresh('p'))
724
+ const fast = (k) => !(k in Object.prototype) && k !== '__proto__'
725
+ const inner = Object.keys(props).map((k) => [k, fillProc(props[k])]).filter(([, c]) => c)
726
+ const walk = inner.length >= FILL_WALK_MIN && inner.every(([k]) => fast(k))
727
+ const defaults = Object.keys(props).filter((k) => isObj(props[k]) && props[k].default !== undefined)
728
+ if (!walk && inner.some(([k]) => fast(k))) out.push({ t: 'bind', names: [[pl.n, { t: 'plainProto', x: d }]] })
729
+ // Many defaults: one pass over the object's own names sets a bit for each
730
+ // default name present, then each default reads its bit. A question per
731
+ // name cost about 10 ns each, and an object carrying most of eighty
732
+ // defaults paid it eighty times (jshintrc 934 to 543 ns, rustfmt 1371 to
733
+ // 751). Below MASK_MIN the pass over every name an object has can cost
734
+ // more than the questions (expo with 12 defaults, 6-9% slower at 8).
735
+ const masked = defaults.filter(fast)
736
+ if (masked.length >= MASK_MIN) {
737
+ out.push({ t: 'defaultsMask', o: d, entries: masked.map((k) => [k, props[k].default]) })
738
+ for (const k of defaults) if (!fast(k)) out.push({ t: 'setDefault', o: d, k, v: props[k].default })
739
+ } else for (const k of defaults) out.push({ t: 'setDefault', o: d, k, v: props[k].default })
740
+ // The properties whose values have defaults inside: many of them (a
741
+ // linter's rules, each a schema with its own defaults) are looked up in a
742
+ // table from the object's own names, so the cost follows the names the
743
+ // object has, not the names the schema lists (tslint: 150 listed, about
744
+ // 20 present). A name Object.prototype has keeps the lookup per name.
745
+ if (walk) out.push({ t: 'fillWalk', o: d, entries: inner })
746
+ else for (const [k, c] of inner) {
747
+ // An own name holding undefined has nothing to fill either.
748
+ out.push(iff(fast(k) ? or(and(pl, cmp('!==', get(d, lit(k)), lit(undefined))), and(not(pl), own(d, lit(k)))) : own(d, lit(k)), { t: 'fcall', proc: c, x: get(d, lit(k)) }))
749
+ }
750
+ const tuple = Array.isArray(n.prefixItems) ? n.prefixItems.map(fillProc) : []
751
+ const each = fillProc(n.items)
752
+ if (tuple.some(Boolean) || each) {
753
+ const i = vr(fresh('i'))
754
+ const per = tuple.map((t, ix) => t ? iff(cmp('===', i, lit(ix)), { t: 'fcall', proc: t, x: get(d, i) }) : null).filter(Boolean)
755
+ out.push(iff({ t: 'isArr', x: d }, { t: 'range', arr: d, from: 0, i: i.n, body: seq(...per, each ? iff(not(cmp('<', i, lit(tuple.length))), { t: 'fcall', proc: each, x: get(d, i) }) : null) }))
756
+ }
757
+ if (isObj(n.dependentSchemas)) for (const k of Object.keys(n.dependentSchemas)) { const c = fillProc(n.dependentSchemas[k]); if (c) out.push(iff(and(not({ t: 'isArr', x: d }), own(d, lit(k))), { t: 'fcall', proc: c, x: d })) }
758
+ if (typeof n.$ref === 'string') { const c = fillProc(safeResolve(n.$ref, n)); if (c) out.push({ t: 'fcall', proc: c, x: d }) }
759
+ if (Array.isArray(n.allOf)) for (const sub of n.allOf) { const c = fillProc(sub); if (c) out.push({ t: 'fcall', proc: c, x: d }) }
760
+ fillStack.delete(n)
761
+ // A value that is not an object has nothing to fill.
762
+ procs.set(id, { id, mode: 'fill', base: index.baseOf(n), body: iff({ t: 'isObjAny', x: d }, seq(out)) })
763
+ fills.set(n, id)
764
+ return id
765
+ }
766
+ const fillRoot = fillProc(schema)
767
+
768
+ // The root as dual and as verdict procedures.
769
+ cur = { mode: 'dual', lexBase: index.baseOf(schema), ann: false }
770
+ const dual = proc(schema, 'dual', true)
771
+ const verdictRoot = proc(schema, 'verdict', true)
772
+ // `schema`: the normalized schema, which the generic default pass must read
773
+ // too (it fills what validation applies: a draft-07 $ref drops its siblings).
774
+ // A call on the same value that lies on a cycle (a schema whose allOf or
775
+ // anyOf refers back to it, as SchemaStore's cloudify does) would go round
776
+ // without end. The current engine cuts a reference that it meets again with
777
+ // the same value, the inner entry answering with no error; such calls are
778
+ // marked `guard` for the backends to do the same, each with its own record
779
+ // of the values it is on. A call is on the same value when its value is the
780
+ // calling procedure's own `d`.
781
+ const same = (x) => x && x.t === 'var' && x.n === 'd'
782
+ const eachNode = (n, f) => {
783
+ if (n === null || typeof n !== 'object') return
784
+ if (Array.isArray(n)) { for (const x of n) eachNode(x, f); return }
785
+ f(n)
786
+ for (const k in n) if (k !== 'proc' && k !== 'procs' && k !== 'K' && k !== 'dynamic') eachNode(n[k], f)
787
+ }
788
+ const targets = (n) => n.t === 'pcall' && n.proc !== null ? [n.proc, ...(n.dynamic ? n.dynamic.map(([, t]) => t) : [])] : n.t === 'branches' ? n.procs : []
789
+ const inPlaceCalls = new Map()
790
+ for (const [id, pr] of procs) {
791
+ if (pr.mode === 'fill') continue
792
+ const out = new Set()
793
+ eachNode(pr.body, (n) => { if ((n.t === 'pcall' || n.t === 'branches') && same(n.x)) for (const t of targets(n)) out.add(t) })
794
+ inPlaceCalls.set(id, out)
795
+ }
796
+ const reachesInPlace = (from, to) => {
797
+ const seen = new Set(); const stack = [from]
798
+ while (stack.length) { const t = stack.pop(); if (t === to) return true; if (seen.has(t)) continue; seen.add(t); const next = inPlaceCalls.get(t); if (next) stack.push(...next) }
799
+ return false
800
+ }
801
+ for (const [id, pr] of procs) {
802
+ if (pr.mode === 'fill' || !inPlaceCalls.get(id).size) continue
803
+ eachNode(pr.body, (n) => { if (n.t === 'pcall' && n.proc !== null && same(n.x) && targets(n).some((t) => reachesInPlace(t, id))) n.guard = true })
804
+ }
805
+ return { procs, dual, verdict: verdictRoot, dynamic, sawDynamic, relate: needsRelate, fill: fillDeclined ? 'generic' : fillRoot, schema, custom: kw !== null && keywordsLib.schemaUsesKeywords(schema, kw), ctx }
806
+ }
807
+
808
+ // What the error order reads of a schema: every key in its place, every
809
+ // subschema, and the values that identify a resource or point at one; any
810
+ // other value (a description, an enum list) becomes 0. Ranks over it are the
811
+ // ranks over the schema.
812
+ const KEEP = new Set(['$ref', '$id', 'id', '$anchor', '$dynamicAnchor', '$dynamicRef', '$recursiveRef', '$recursiveAnchor', '$schema'])
813
+ const SUBS = new Set(['additionalProperties', 'additionalItems', 'items', 'contains', 'propertyNames', 'if', 'then', 'else', 'not', 'unevaluatedItems', 'unevaluatedProperties', 'allOf', 'anyOf', 'oneOf', 'prefixItems', 'properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas', 'dependencies'])
814
+ export function orderSkeleton (node, inMap = false) {
815
+ if (node === null || typeof node !== 'object') return 0
816
+ if (Array.isArray(node)) return node.map((x) => orderSkeleton(x))
817
+ const out = {}
818
+ for (const k of Object.keys(node)) {
819
+ const v = node[k]
820
+ const s = inMap ? orderSkeleton(v) : KEEP.has(k) && (typeof v === 'string' || typeof v === 'boolean') ? v : SUBS.has(k) ? orderSkeleton(v, ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas', 'dependencies'].includes(k)) : 0
821
+ Object.defineProperty(out, k, { value: s, enumerable: true, writable: true, configurable: true })
822
+ }
823
+ return out
824
+ }
825
+
826
+ // Whether the errors of a schema can come out of the IR in an order other
827
+ // than schema declaration order, so that the module must sort them. The IR
828
+ // writes each node's keywords in the order they are declared, and a
829
+ // reference's target where the reference stands, so they can not only where
830
+ // a loop over the instance repeats a body that fails in more than one place
831
+ // (every item's type before any item's pattern, in schema order; a body
832
+ // behind a reference counts as such), and wherever this check does not look
833
+ // further: dynamic references, branches, conditionals, annotations collected
834
+ // across keywords. Erring towards true costs size, never order.
835
+ const ALWAYS = new Set(['$dynamicRef', '$recursiveRef', 'anyOf', 'oneOf', 'if', 'unevaluatedProperties', 'unevaluatedItems', 'dependentSchemas', 'dependencies', 'contains', 'propertyNames', 'patternProperties'])
836
+ const LOOPS = new Set(['items', 'prefixItems', 'additionalItems', 'additionalProperties'])
837
+ function failPlaces (s) {
838
+ if (s === false) return 1
839
+ if (s === null || typeof s !== 'object') return 0
840
+ if (Array.isArray(s)) { let n = 0; for (const x of s) n += failPlaces(x); return n }
841
+ let n = 0
842
+ for (const k of Object.keys(s)) {
843
+ if (ANNOTATIONS.has(k) || k === '$defs' || k === 'definitions' || k === '$id' || k === '$anchor' || k === '$dynamicAnchor' || k === '$comment' || k === '$schema') continue
844
+ // A reference can fail anywhere its target can: counted as several.
845
+ if (k === '$ref' || k === '$dynamicRef' || k === '$recursiveRef') { n += 2; continue }
846
+ if (k === 'properties' || k === 'patternProperties' || k === 'dependentSchemas') { for (const x of Object.values(s[k] || {})) n += failPlaces(x); continue }
847
+ if (k === 'allOf' || k === 'anyOf' || k === 'oneOf' || k === 'prefixItems' || k === 'items' || k === 'additionalItems' || k === 'additionalProperties' || k === 'not' || k === 'contains' || k === 'propertyNames' || k === 'if' || k === 'then' || k === 'else') { n += k === 'not' ? 1 : Math.max(1, failPlaces(s[k])); continue }
848
+ n++
849
+ }
850
+ return n
851
+ }
852
+ export function needsOrder (s) {
853
+ if (s === null || typeof s !== 'object') return false
854
+ if (Array.isArray(s)) return s.some(needsOrder)
855
+ for (const k of Object.keys(s)) {
856
+ if (ALWAYS.has(k)) return true
857
+ if (LOOPS.has(k) && failPlaces(s[k]) > 1) return true
858
+ }
859
+ for (const k of Object.keys(s)) {
860
+ if (ANNOTATIONS.has(k) && k !== 'default') continue
861
+ if (k === 'enum' || k === 'const' || k === 'default' || k === 'required' || k === 'examples') continue
862
+ if (needsOrder(s[k])) return true
863
+ }
864
+ return false
865
+ }