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,707 @@
1
+ // IR backend: JavaScript source. It knows the IR operations
2
+ // (README.md) and nothing about JSON Schema; a leaf kernel's check is written
3
+ // from the kernel's own source (or a build-time fast path held to it).
4
+
5
+ import { TYPES } from '../kernels/kernels.mjs'
6
+ import { front, orderSkeleton, needsOrder } from './front.mjs'
7
+ import * as KERNELS from '../kernels/index.mjs'
8
+
9
+ // The names a module may import: exactly the exports of ata-validator/kernels.
10
+ const KERNEL_NAMES = new Set(Object.keys(KERNELS))
11
+ // The pattern unrolling of the code generator and the format hints of
12
+ // lib/suggestions.js, as a generated copy (scripts/gen-ir-emit-deps.js), so
13
+ // this file loads where neither the code generator nor the file system is.
14
+ import { compilePatternInline, FORMAT_HINTS } from './emit-deps.mjs'
15
+ const FAST = {
16
+ pattern: (n, x) => compilePatternInline(n, x),
17
+ // A string has between length/2 and length code points, so the length
18
+ // alone settles most cases and the count runs only near the bound.
19
+ minLength: (n, x) => `${x}.length>=${2 * n}||${x}.length>=${n}&&cpLen(${x})>=${n}`,
20
+ maxLength: (n, x) => `${x}.length<=${n}||${x}.length<=${2 * n}&&cpLen(${x})<=${n}`,
21
+ }
22
+ // enum and const with only primitive values need no deep equality: the
23
+ // value is one of them exactly when it is === one of them (JSON numbers have
24
+ // no NaN, and 0 and -0 are one value to JSON equality too).
25
+ const prim = (v) => v === null || typeof v !== 'object'
26
+ FAST.enum = (n, x) => Array.isArray(n) && n.every(prim) ? (n.length ? n.map((v) => `${x}===${JSON.stringify(v)}`).join('||') : 'false') : null
27
+ FAST.const = (n, x) => prim(n) ? `${x}===${JSON.stringify(n)}` : null
28
+ const GUARD = {
29
+ isStr: (x) => `typeof ${x}==="string"`,
30
+ isNum: (x) => `typeof ${x}==="number"`,
31
+ isArr: (x) => `Array.isArray(${x})`,
32
+ isObjV: (x) => `(typeof ${x}==="object"&&${x}!==null&&!Array.isArray(${x}))`,
33
+ isAny: () => 'true',
34
+ }
35
+ const lit = (v) => v === undefined ? 'void 0' : JSON.stringify(v)
36
+
37
+ // The body of a one-expression arrow `(v, n) => body` or `(v) => body`, with
38
+ // v and n replaced: the kernel's own source, specialized to the schema value.
39
+ function inline (fn, vExpr, nExpr) {
40
+ const src = fn.toString()
41
+ const m = /^\(\s*v\s*(?:,\s*n\s*)?\)\s*=>\s*([\s\S]+)$/.exec(src)
42
+ if (!m) throw new Error('ir: kernel ok() must be (v, n) => expression: ' + src)
43
+ return '(' + m[1].replace(/\bv\b/g, vExpr).replace(/\bn\b/g, nExpr) + ')'
44
+ }
45
+
46
+ // How many names an object must list before a module visits the object's own
47
+ // names instead of looking each one up. Measured on SchemaStore's samples,
48
+ // warmed, against the compiled modules: from 16 names the geometric mean is
49
+ // 0.878 with 23 schemas under half the time, from 32 it is 0.914 with 15,
50
+ // from 64 0.941 with 8, and the count slower by a fifth is 47 at each. A
51
+ // dense document under a short list (sil-kit, csslintrc: most of 16 to 38
52
+ // names present) pays the walk's array and dispatch, 2 to 4x the direct
53
+ // reads; a sparse one under a long list (putout, 475 names) would pay a
54
+ // read per name, 100x the walk.
55
+ const WALK_MIN = 16
56
+ // Source characters past which a verdict body is its own function (the
57
+ // verdict threshold of the current engine's generator, SPLIT_PROP_SCHEMA).
58
+ const SPLIT_MIN = 2000
59
+ // Entries a verdict's walk looks up in a table rather than a switch. A call
60
+ // through the table is indirect, so V8 does not inline a small check behind
61
+ // it: when most entries call a function of INLINE_MAX characters or fewer,
62
+ // the switch's direct calls win below 64 entries (mocharc 650 to 229 ns,
63
+ // huskyrc 290 to 149, targets about 90 characters). With larger targets
64
+ // there is nothing to inline and the table stays from WALK_MIN entries
65
+ // (abc-supply-plan, targets of 230 characters and more, 8-16% slower as a
66
+ // switch).
67
+ const TABLE_MIN = 64
68
+ const INLINE_MAX = 160
69
+ // A verdict that visits an object's names moves even small entries out when
70
+ // they would add up past this many characters, and otherwise only the ones of
71
+ // SPLIT_SMALL characters or more (the current engine's SPLIT_FN_BUDGET and
72
+ // SPLIT_PROP_SMALL).
73
+ const SPLIT_BUDGET = 24 * 1024
74
+ const SPLIT_SMALL = 220
75
+
76
+ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, fill = null, richErrors = true } = {}) {
77
+ // richErrors false: the current engine's plain shape (keyword, instancePath,
78
+ // schemaPath, params, message), no enrichment, no related links.
79
+ const plain = richErrors === false
80
+ // Set when an object's names are visited in the object's order (`props`).
81
+ let WALKED = false
82
+ const uses = new Set(richErrors === false ? [] : ['E'])
83
+ const consts = []
84
+ const constOf = (src) => { const name = 'K' + consts.length; consts.push(`const ${name}=${src}`); return name }
85
+ const emitted = new Map()
86
+ const fns = []
87
+ const use = (...n) => { for (const x of n) if (x) uses.add(x) }
88
+ const ident = (src) => { for (const m of src.matchAll(/[A-Za-z_$][\w$]*/g)) use(m[0]); return src }
89
+
90
+ const hintFns = new Map()
91
+ const enumSets = new Map()
92
+ const knownFns = new Map()
93
+ // props nodes whose walk also rejects names properties does not list.
94
+ const folded = new WeakSet()
95
+ // Each emitted function's body size, for the choice between a switch of
96
+ // direct calls and a table (a procedure still being written has none).
97
+ const sizes = new Map()
98
+ // Imported from ata-validator/kernels (lib/kernels/formats.mjs, the same
99
+ // generated checks), so a page with several modules carries each once.
100
+ const formatFn = (name) => {
101
+ const fn = 'fmt_' + name.replace(/-/g, '_')
102
+ if (!KERNEL_NAMES.has(fn)) throw new Error(`ir emit: no generated check for format "${name}"`)
103
+ use(fn)
104
+ return fn
105
+ }
106
+ // An own-property test written in the module with the module's own
107
+ // reference to hasOwnProperty. The kernels' own() is one function for every
108
+ // module of a page, so its property lookup met the objects of every schema
109
+ // and stopped being cached for any (rc3-environment alone 1.0x the compiled
110
+ // module, beside 438 other schemas 2.1x).
111
+ let HOP = null
112
+ const ownSrc = (o, k) => {
113
+ if (HOP === null) HOP = constOf('Object.prototype.hasOwnProperty')
114
+ return `${HOP}.call(${o},${k})`
115
+ }
116
+ // A small verdict written in at its call, as the compiled modules write a
117
+ // referenced schema in at each property: every copy keeps its own record of
118
+ // the shapes it meets, where one function called from forty places met all
119
+ // of them and V8 stopped inlining it (coffeelint, 38 properties naming one
120
+ // definition). Only a verdict with no cycle through it, no guard and no
121
+ // dynamic scope, whose body is INLINE_BODY characters or fewer; a verdict
122
+ // body leaves only by `return false`, so it runs as a block. Its parameters
123
+ // are bound in the block: the value, and the empty paths and list of a
124
+ // verdict call.
125
+ const INLINE_BODY = 500
126
+ // Inlined source one function may take in all: past it a function grows
127
+ // beyond what V8 optimizes (putout's 475 properties, 0.01x the compiled
128
+ // module before, 1.08x with every call written in).
129
+ // 2000 left coffeelint's 38 names calling one 300-character check (2.2x
130
+ // the compiled module); 8000 inlines them and keeps putout's 475-name
131
+ // function optimizable (both measured on SchemaStore, warmed).
132
+ const INLINE_BUDGET = 8000
133
+ let inlineBudget = INLINE_BUDGET
134
+ const inlining = new Set()
135
+ const inlineBodies = new Map()
136
+ const inlineCall = (c) => {
137
+ if (c.proc === null || c.dynamic || c.guard || ir.dynamic || inlining.has(c.proc)) return null
138
+ const p = ir.procs.get(c.proc)
139
+ if (!p || p.mode !== 'verdict') return null
140
+ // Follow a verdict that is only a call of another (procName's alias).
141
+ const b = p.body.t === 'seq' && p.body.body.length === 1 ? p.body.body[0] : p.body
142
+ if (b.t === 'require' && b.e.t === 'pcall' && b.e.e === 'null' && !b.e.dynamic && !b.e.guard && b.e.x.t === 'var' && b.e.x.n === 'd' && b.e.proc !== c.proc) return inlineCall({ ...b.e, x: c.x })
143
+ // Each procedure is written once, with a full budget for what it
144
+ // inlines itself, and that text is its size: a site inlines it when the
145
+ // text fits the site's remaining budget. Written at every call site
146
+ // under the site's budget, the text depended on the order of the sites,
147
+ // and a definition too large under a deep nesting was rendered again
148
+ // for each site below each site (a suite schema took ten minutes).
149
+ let body = inlineBodies.get(c.proc)
150
+ if (body === undefined) {
151
+ inlining.add(c.proc); const saved = inlineBudget; inlineBudget = INLINE_BUDGET
152
+ try { body = stmt(p.body, true) } finally { inlining.delete(c.proc); inlineBudget = saved }
153
+ if (body.length > INLINE_BODY || /\breturn true\b/.test(body)) body = null
154
+ inlineBodies.set(c.proc, body)
155
+ }
156
+ if (body === null || body.length > inlineBudget) return null
157
+ inlineBudget -= body.length
158
+ const x = expr(c.x); const t = 'i' + (++inlineN)
159
+ return `{const ${t}=${x}\n{const d=${t},V=${t},e=null,P="",SP="",A=null\n${body}}}\n`
160
+ }
161
+ let inlineN = 0
162
+ // The verdict calls already made in the row of statements being written
163
+ // (seq), handed to a row nested directly in it.
164
+ let seqSeen = null
165
+ // The plain-prototype flags a fill has bound, by the object's expression.
166
+ const fillPlain = new Map()
167
+ const procName = (id) => {
168
+ if (emitted.has(id)) return emitted.get(id)
169
+ const p = ir.procs.get(id)
170
+ // A verdict that is one call of another on its own value (a branch that
171
+ // is only a $ref) is that other verdict: no function of its own, and a
172
+ // caller that already ran it can see the repeat (seq).
173
+ if (p.mode === 'verdict' && !ir.dynamic) {
174
+ const b = p.body.t === 'seq' && p.body.body.length === 1 ? p.body.body[0] : p.body
175
+ if (b.t === 'require' && b.e.t === 'pcall' && b.e.e === 'null' && !b.e.dynamic && !b.e.guard && b.e.x.t === 'var' && b.e.x.n === 'd' && b.e.proc !== id) {
176
+ const to = procName(b.e.proc)
177
+ emitted.set(id, to)
178
+ return to
179
+ }
180
+ }
181
+ const name = id
182
+ emitted.set(id, name)
183
+ const isVerdict = p.mode === 'verdict'
184
+ if (p.mode === 'fill') { fillPlain.clear(); fns.push(`function ${name}(d){\n${stmt(p.body, true)}}`); return name }
185
+ // Each function written gets its own budget of inlined source (inlineCall).
186
+ const savedBudget = inlineBudget
187
+ inlineBudget = INLINE_BUDGET
188
+ let body
189
+ try { body = stmt(p.body, isVerdict) } finally { inlineBudget = savedBudget }
190
+ const head = p.mode === 'ann' ? 'if(A===null)A=newAnn()\n' : ''
191
+ if (p.mode === 'ann') use('newAnn')
192
+ const wrapped = ir.dynamic
193
+ ? `const sv=SC;if(SC===null||SC.b!==${lit(p.base)})SC={b:${lit(p.base)},up:SC};try{\n${body}return true}finally{SC=sv}`
194
+ : `${body}return true`
195
+ fns.push(`function ${name}(d,e,s,P,V,SP,A){${head}${wrapped}}`)
196
+ sizes.set(name, wrapped.length)
197
+ return name
198
+ }
199
+
200
+ // A failure: `return false` in a verdict, a pushed error otherwise.
201
+ const fail = (er, v) => {
202
+ if (v) return 'return false'
203
+ if (plain) return `{if(e===null)return false;e.push({keyword:${lit(er.kw)},instancePath:${expr(er.path)},schemaPath:${expr(er.sp)},params:${expr(er.params)},message:${expr(er.msg)}})}`
204
+ let detail = 'void 0'; let suggest = 'void 0'
205
+ // A kernel function generated code names must have a name to import.
206
+ if ((er.detail && !er.detail.src) || (er.suggest && !er.suggest.src)) throw new Error(`ir emit: an unnamed enrichment function for ${er.kw}`)
207
+ if (er.detail) detail = ident(er.detail.src)
208
+ // A format's suggestion: only that format's hint, from the current
209
+ // engine's table (lib/suggestions.js FORMAT_HINTS), written in as source.
210
+ if (er.suggest && er.suggest.src === 'formatSuggest') {
211
+ const f = er.params.fields.find(([k]) => k === 'format')[1].v
212
+ const hint = FORMAT_HINTS[f]
213
+ suggest = hint ? (hintFns.get(f) || (hintFns.set(f, constOf(`(v)=>{const t=(${hint.toString()})(v);return t?{text:t,kind:"format"}:null}`)), hintFns.get(f))) : 'void 0'
214
+ } else if (er.suggest) suggest = /\(/.test(er.suggest.src) ? (ident(er.suggest.src), constOf(er.suggest.src)) : ident(er.suggest.src)
215
+ return `{if(e===null)return false;e.push(E(${lit(er.code)},${lit(er.kw)},${expr(er.path)},${expr(er.sp)},${expr(er.params)},${expr(er.msg)},${lit(er.exp)},${er.rank},${expr(er.ev)},s,${detail},${suggest}))}`
216
+ }
217
+
218
+ // A procedure call: its arguments, its target (a dynamic pick or fixed).
219
+ const callOf = (n, eArg, aArg) => {
220
+ if (n.proc === null) throw new Error('ir: a v1 $dynamicRef with no target of its own needs the closure backend')
221
+ const fn = procName(n.proc)
222
+ let target = fn
223
+ if (n.dynamic) { const table = constOf(`new Map([${n.dynamic.map(([b, id]) => `[${lit(b)},${procName(id)}]`).join(',')}])`); target = `pick(${table},${fn})` }
224
+ // A call that gathers no errors never reads its paths (custom keywords,
225
+ // the one reader that could, are refused by this writer): no strings to build.
226
+ const quiet = eArg === 'null'
227
+ // A call on a cycle (lib/ir/front.mjs `guard`): through a function that
228
+ // keeps the values this call is on and answers a second entry with no
229
+ // error, as the current engine cuts a reference it meets again.
230
+ if (n.guard) {
231
+ const G = constOf('new Set()'); const w = 'G' + fns.length
232
+ fns.push(`function ${w}(d,e,s,P,V,SP,A){if(${G}.has(d))return true\n${G}.add(d);try{return ${target}(d,e,s,P,V,SP,A)}finally{${G}.delete(d)}}`)
233
+ target = w
234
+ }
235
+ return `${target}(${expr(n.x)},${eArg},s,${n.path && !quiet ? expr(n.path) : '""'},${n.ev ? expr(n.ev) : expr(n.x)},${n.sp && !quiet ? expr(n.sp) : '""'},${aArg})`
236
+ }
237
+
238
+ // A verdict statement as a function of `name`, called the way a procedure
239
+ // is (value, e, s): it leaves only by `return false`.
240
+ const hoist = (node, name, text = stmt(node, true)) => {
241
+ const fn = 'L' + fns.length
242
+ fns.push(`function ${fn}(${name},e,s){\n${text}return true}`)
243
+ sizes.set(fn, text.length)
244
+ return fn
245
+ }
246
+
247
+ // Whether a statement reads no name but `name` and the ones it declares,
248
+ // and nothing of annotations (the caller's collector).
249
+ const selfContained = (n, name) => {
250
+ const bound = new Set([name]); const used = []; let ann = false
251
+ const walk = (x) => {
252
+ if (x === null || typeof x !== 'object') return
253
+ if (Array.isArray(x)) { for (const y of x) walk(y); return }
254
+ if (typeof x.t === 'string') {
255
+ if (x.t === 'var' || x.t === 'incr') used.push(x.n ?? x.name)
256
+ else if (x.t === 'let' || x.t === 'count') bound.add(x.name)
257
+ else if (x.t === 'keys') bound.add(x.k)
258
+ else if (x.t === 'range') bound.add(x.i)
259
+ else if (x.t === 'bind') for (const [k] of x.names) bound.add(k)
260
+ if (x.t.startsWith('ann') || x.t === 'firstUneval' || x.ann) ann = true
261
+ }
262
+ for (const k in x) if (k !== 'proc' && k !== 'K') walk(x[k])
263
+ }
264
+ walk(n)
265
+ return !ann && used.every((u) => bound.has(u))
266
+ }
267
+
268
+ // A keyed entry that reads its value and, when present, requires one
269
+ // procedure of it gathering nothing: the call, or null.
270
+ const verdictCall = (n) => {
271
+ if (n.t !== 'let' || n.body.t !== 'if' || n.body.else) return null
272
+ let b = n.body.then
273
+ while (b.t === 'seq' && b.body.length === 1) b = b.body[0]
274
+ if (b.t !== 'require') return null
275
+ const c = b.e
276
+ return c.t === 'pcall' && (c.e === 'null' || c.e === 'inherit') && !c.ann && !c.dynamic && c.x.t === 'var' && c.x.n === n.name ? c : null
277
+ }
278
+
279
+
280
+ // A default's value as source: a literal, a fresh empty array, or a copy.
281
+ const defaultSrc = (val) => {
282
+ if (typeof val !== 'object' || val === null) return lit(val)
283
+ if (Array.isArray(val) && val.length === 0) return '[]'
284
+ use('copyOf'); return `copyOf(${constOf(lit(val))})`
285
+ }
286
+
287
+ function stmt (n, v) {
288
+ switch (n.t) {
289
+ case 'seq': {
290
+ // A verdict that walks an object's names for its properties and has
291
+ // additionalProperties: false over the same object finds the unknown
292
+ // names in that walk: the separate pass over the object goes.
293
+ let skip = null
294
+ if (v) {
295
+ const walked = n.body.find((c) => c.t === 'if' && c.then.t === 'props' && c.then.closed && c.then.keyed !== null && c.then.keyed.length >= WALK_MIN && c.then.x.t === 'var')
296
+ const ap = walked && n.body.find((c) => c.t === 'if' && c.then.t === 'keys' && c.then.apClosed && c.then.obj.t === 'var' && c.then.obj.n === walked.then.x.n)
297
+ if (ap) { skip = ap; folded.add(walked.then) }
298
+ }
299
+ // In a verdict the same check of the same value twice in a row of
300
+ // statements is once: the first returned false or passed, and a
301
+ // verdict has no side effects (custom keywords and formats, which
302
+ // could, are refused above). A schema that names one definition
303
+ // under both allOf and anyOf ran it twice per item (devinit).
304
+ // A row nested directly in a row runs unconditionally after what came
305
+ // before it, so it shares the set; anything under a condition, a
306
+ // loop or a let starts its own.
307
+ const seen = v ? (seqSeen || new Set()) : null
308
+ seqSeen = null
309
+ return n.body.filter((x) => x !== skip).map((x) => {
310
+ seqSeen = x.t === 'seq' ? seen : null
311
+ const t = stmt(x, v)
312
+ seqSeen = null
313
+ if (seen !== null && (x.t === 'require' || x.t === 'branches')) { if (seen.has(t)) return ''; seen.add(t) }
314
+ return t
315
+ }).join('')
316
+ }
317
+ // An object that lists many names: visit the object's own names, every
318
+ // one enumerable or not (an entry checks its own presence, so a name it
319
+ // skips is one the object lacks), instead of looking each listed name
320
+ // up. Errors then come in the object's key order, so the module sorts
321
+ // them into schema order as the current engine does (WALKED).
322
+ case 'props': {
323
+ if (n.keyed === null || n.keyed.length < WALK_MIN) return n.body.map((x) => stmt(x, v)).join('')
324
+ WALKED = true
325
+ const keyed = new Set(n.keyed.map(([, i]) => i))
326
+ const always = n.body.filter((_, i) => !keyed.has(i)).map((x) => stmt(x, v)).join('')
327
+ // In a verdict, the entries that only call a procedure on their value
328
+ // go in a table by name: one lookup per key instead of a comparison
329
+ // per listed name. An entry whose checks read only its value can move
330
+ // into a function of its own and go in the table too: a large one
331
+ // always, a small one when the names together would make a function
332
+ // too large for V8 to optimize (a call costs more than a short check).
333
+ const X = expr(n.x)
334
+ const rows = n.keyed.map(([p, i]) => {
335
+ const e = n.body[i]
336
+ const c = v ? verdictCall(e) : null
337
+ if (c !== null) {
338
+ // A small check written in at its name (inlineCall), as the
339
+ // compiled modules write a referenced definition at each
340
+ // property: csslintrc's 31 names each called one enum check.
341
+ const t = inlineCall({ ...c, x: { t: 'var', n: 'y' } })
342
+ if (t !== null) return { p, inl: t }
343
+ return { p, fn: procName(c.proc) }
344
+ }
345
+ if (v && e.t === 'let' && e.body.t === 'if' && !e.body.else && selfContained(e.body.then, e.name)) {
346
+ const text = stmt(e.body.then, true)
347
+ return { p, e, text, src: `{const ${e.name}=${expr(e.value)}\nif(${expr(e.body.c)}){\n${text}}}\n` }
348
+ }
349
+ return { p, src: stmt(e, v) }
350
+ })
351
+ const big = rows.reduce((t, r) => t + (r.src ? r.src.length : 0), 0) > SPLIT_BUDGET
352
+ for (const r of rows) if (r.text !== undefined && (big || r.text.length >= SPLIT_SMALL)) r.fn = hoist(r.e.body.then, r.e.name, r.text)
353
+ const tabled = rows.filter((r) => r.fn)
354
+ const small = tabled.filter((r) => (sizes.get(r.fn) ?? Infinity) <= INLINE_MAX).length
355
+ const useTable = tabled.length >= TABLE_MIN || (tabled.length >= WALK_MIN && small * 2 < tabled.length)
356
+ const left = rows.filter((r) => !useTable || !r.fn)
357
+ // In the walk a name comes from the object's own names, so its value
358
+ // is read without a presence test.
359
+ const body = (r) => r.fn ? `{const y=${X}[${lit(r.p)}];if(y!==void 0&&!${r.fn}(y,null,s,"",y,"",null))return false}\n` : r.inl ? `{const y=${X}[${lit(r.p)}];if(y!==void 0){${r.inl}}}\n` : r.src
360
+ // A key is told from the names by its length first, a switch on an
361
+ // integer the engine makes a jump table, and then compared with the
362
+ // few names of that length: a switch on the names compared each key
363
+ // with half of them, which lost to a lookup per name once an object
364
+ // carried most of fifty (htmlhint).
365
+ const byLen = new Map()
366
+ for (const r of left) { const l = r.p.length; if (!byLen.has(l)) byLen.set(l, []); byLen.get(l).push(r) }
367
+ // Folded (see seq): every listed name has a case, a name without
368
+ // checks an empty one, and a name none of them is, own and
369
+ // enumerable, fails the verdict.
370
+ const fold = folded.has(n)
371
+ const unknown = fold ? `if(${constOf('Object.prototype.propertyIsEnumerable')}.call(${X},k))return false\n` : ''
372
+ if (fold) { const have = new Set(rows.map((r) => r.p)); for (const q of n.closed) if (!have.has(q)) { const l = q.length; if (!byLen.has(l)) byLen.set(l, []); byLen.get(l).push({ p: q, empty: true }) } }
373
+ const dispatch = (k) => `switch(${k}.length){\n${[...byLen].map(([l, rs]) => `case ${l}:${rs.map((r) => `if(${k}===${lit(r.p)}){\n${r.empty ? '' : body(r)}}`).join('else ')}${fold ? `else{${unknown}}` : ''}break\n`).join('')}${fold ? `default:${unknown}` : ''}}`
374
+ // A name the table answers is listed; with the fold every other name
375
+ // goes through the dispatch, which knows the listed ones.
376
+ let walk
377
+ if (!useTable) walk = `for(let j=0;j<ks.length;j++){const k=ks[j];${dispatch('k')}}`
378
+ else {
379
+ const T = constOf(`new Map([${tabled.map((r) => `[${lit(r.p)},${r.fn}]`).join(',')}])`)
380
+ walk = `for(let j=0;j<ks.length;j++){const k=ks[j],f=${T}.get(k);if(f!==void 0){const y=${X}[k];if(y!==void 0&&!f(y,null,s,"",y,"",null))return false}${left.length || fold ? `else ${dispatch('k')}` : ''}}`
381
+ }
382
+ // A self-measuring switch to a direct read of each listed name on
383
+ // dense documents was tried here and taken out: it won on five
384
+ // config schemas (csslintrc, coffeelint, jshintrc) and lost as much
385
+ // on others (liquibase 0.27 to 1.46 of the compiled module, a
386
+ // 142-name object read in dictionary mode 8x slower than the walk),
387
+ // no rule from the measurements told the two apart, and its direct
388
+ // form once rejected {} through an inherited toString.
389
+ return `${always}{const ks=Object.getOwnPropertyNames(${X});${walk}}\n`
390
+ }
391
+ case 'if': {
392
+ // A condition that is a call under annotations merges its collector.
393
+ if (n.c.t === 'pcall' && n.c.ann === 'merge') {
394
+ use('newAnn', 'mergeAnn')
395
+ return `{const a=newAnn();if(${callOf(n.c, 'null', 'a')}){mergeAnn(A,a)\n${stmt(n.then, v)}}else{\n${n.else ? stmt(n.else, v) : ''}}}\n`
396
+ }
397
+ return `if(${expr(n.c)}){\n${stmt(n.then, v)}}${n.else ? `else{\n${stmt(n.else, v)}}` : ''}\n`
398
+ }
399
+ case 'let': {
400
+ // A large verdict body becomes a function of its value: V8 does not
401
+ // optimize a function past its bytecode limit, and a schema inlined
402
+ // whole into one can pass it. A verdict body leaves only by `return
403
+ // false`, so the call keeps its meaning; what moves must read nothing
404
+ // of the enclosing function but the value. A presence test in front
405
+ // (it reads the parent) stays where it is.
406
+ const guarded = n.body.t === 'if' && !n.body.else && !(n.body.c.t === 'pcall' && n.body.c.ann === 'merge')
407
+ const inner = guarded ? n.body.then : n.body
408
+ const text = stmt(inner, v)
409
+ if (v && !ir.dynamic && text.length > SPLIT_MIN && selfContained(inner, n.name)) {
410
+ const call = `if(!${hoist(inner, n.name, text)}(${n.name},null,s))return false\n`
411
+ return `{const ${n.name}=${expr(n.value)}\n${guarded ? `if(${expr(n.body.c)}){\n${call}}\n` : call}}\n`
412
+ }
413
+ return `{const ${n.name}=${expr(n.value)}\n${guarded ? `if(${expr(n.body.c)}){\n${text}}\n` : text}}\n`
414
+ }
415
+ case 'bind': return `const ${n.names.map(([k, e]) => `${k}=${expr(e)}`).join(',')}\n`
416
+ case 'keys': {
417
+ if (n.order === 'forin') return `for(const ${n.k} in ${expr(n.obj)}){\n${stmt(n.body, v)}}\n`
418
+ // A verdict stops at the first failure whatever the order, so it
419
+ // needs no __proto__ moved last. (for-in with an own check, which
420
+ // allocates no array, made a schema alone up to 3x faster but 13 of
421
+ // SchemaStore's slower by 6-80% when all of them run in one process.)
422
+ const last = n.order === 'protoLast' && !v
423
+ if (last) use('keysOf')
424
+ // A verdict walks the object's own names with for-in, no array, each
425
+ // name tested as the object's own: a name enumerable on
426
+ // Object.prototype is not one of them (tests/test_ir_module_split.mjs).
427
+ if (v) {
428
+ const o = expr(n.obj)
429
+ return `for(const ${n.k} in ${o}){if(!${ownSrc(o, n.k)})continue\n${stmt(n.body, v)}}\n`
430
+ }
431
+ return `for(const ${n.k} of ${last ? 'keysOf' : 'Object.keys'}(${expr(n.obj)})){\n${stmt(n.body, v)}}\n`
432
+ }
433
+ case 'range': return `for(let ${n.i}=${typeof n.from === 'number' ? n.from : expr(n.from)};${n.i}<${expr(n.arr)}.length;${n.i}++){\n${stmt(n.body, v)}}\n`
434
+ case 'count': return `{let ${n.name}=0\n${stmt(n.body, v)}}\n`
435
+ case 'incr': return `${n.name}++\n`
436
+ case 'do': return `${expr(n.e)}\n`
437
+ case 'ret': return 'return false\n'
438
+ case 'require': {
439
+ const c = n.e
440
+ if (c.ann === 'merge') {
441
+ use('newAnn', 'mergeAnn')
442
+ return `{const a=newAnn(),c=e===null?0:e.length;if(${callOf(c, 'e', 'a')}&&(e===null||e.length===c))mergeAnn(A,a);else if(e===null)return false}\n`
443
+ }
444
+ if (v && c.e === 'null') { const t = inlineCall(c); if (t !== null) return t }
445
+ if (c.e === 'inherit') return v ? `if(!${callOf(c, 'null', 'null')})return false\n` : `if(!${callOf(c, 'e', 'null')}&&e===null)return false\n`
446
+ return `if(!${callOf(c, 'null', 'null')})${v ? 'return false' : '{if(e===null)return false}'}\n`
447
+ }
448
+ case 'fail': return fail(n.err, v) + '\n'
449
+ case 'branches': return branches(n, v)
450
+ case 'setDefault': {
451
+ if (n.k === '__proto__') use('setOwn')
452
+ const val = n.v
453
+ let src
454
+ if (typeof val !== 'object' || val === null) src = lit(val)
455
+ else if (Array.isArray(val) && val.length === 0) src = '[]'
456
+ else {
457
+ // A small default written as a literal: a fresh value each time
458
+ // it is read, as the compiled modules write `{}`. copyOf's JSON
459
+ // round trip was 72% of revola's verdict and 39% of changepacks'.
460
+ // Not when a key in it is `__proto__`, which a literal would read
461
+ // as the prototype, nor a large one, which would swell the fill.
462
+ const text = lit(val)
463
+ if (text.length <= 400 && !text.includes('"__proto__"')) src = text
464
+ else { use('copyOf'); src = `copyOf(${constOf(text)})` }
465
+ }
466
+ const o = expr(n.o)
467
+ // A store under the name itself: through setOwn's computed key, V8
468
+ // turned an object given more than a few defaults into a dictionary,
469
+ // slow from then on for every reader (htmlhint's root, 23 defaults:
470
+ // getOwnPropertyNames 10 to 999 ns). __proto__ needs defineProperty.
471
+ if (n.k === '__proto__') return `if(!${ownSrc(o, lit(n.k))})setOwn(${o},${lit(n.k)},${src})\n`
472
+ // A defined value under the name on an ordinary object is the
473
+ // object's own: the own test runs only for an undefined read, or on
474
+ // an object with another prototype, as the current engine's fill
475
+ // reads it. The flag is bound once per object in the fill.
476
+ if (!(n.k in Object.prototype)) {
477
+ let pl = fillPlain.get(o); let bind = ''
478
+ if (pl === undefined) { pl = 'pl' + fillPlain.size; fillPlain.set(o, pl); bind = `const ${pl}=(PN?${o}.__proto__:Object.getPrototypeOf(${o}))===Object.prototype\n` }
479
+ return `${bind}if((${o}[${lit(n.k)}]===void 0||!${pl})&&!${ownSrc(o, lit(n.k))})${o}[${lit(n.k)}]=${src}\n`
480
+ }
481
+ // A read or `in` before hasOwnProperty, as the current engine's fill
482
+ // does, was measured both ways: faster where the defaults are mostly
483
+ // present (jshintrc 1195 to 522 ns), slower where they are mostly
484
+ // missing (prometheus 3671 to 4601). hasOwnProperty alone is kept.
485
+ return `if(!${ownSrc(o, lit(n.k))})${o}[${lit(n.k)}]=${src}\n`
486
+ }
487
+ case 'fcall': return `${procName(n.proc)}(${expr(n.x)})\n`
488
+ case 'defaultsMask': {
489
+ // Bits in 32-bit words, set from the object's own names told apart by
490
+ // length and then by name; a default is written where its bit is clear.
491
+ const o = expr(n.o)
492
+ const words = Math.ceil(n.entries.length / 32)
493
+ const m = (i) => 'm' + Math.floor(i / 32)
494
+ const byLen = new Map()
495
+ n.entries.forEach(([k], i) => { if (!byLen.has(k.length)) byLen.set(k.length, []); byLen.get(k.length).push([k, i]) })
496
+ const cases = [...byLen].map(([l, ks]) => `case ${l}:${ks.map(([k, i]) => `if(k===${lit(k)})${m(i)}|=${(1 << (i % 32)) >>> 0}`).join(';else ')};break\n`).join('')
497
+ const sets = n.entries.map(([k, val], i) => `if((${m(i)}&${(1 << (i % 32)) >>> 0})===0)${o}[${lit(k)}]=${defaultSrc(val)}\n`).join('')
498
+ return `{let ${[...Array(words)].map((_, w) => 'm' + w + '=0').join(',')}\nconst ks=Object.getOwnPropertyNames(${o});for(let j=0;j<ks.length;j++){const k=ks[j];switch(k.length){\n${cases}}}\n${sets}}\n`
499
+ }
500
+ // The object's own names, each looked up in a table of the fills of
501
+ // the properties that have one (lib/ir/front.mjs fillProc).
502
+ case 'fillWalk': {
503
+ const o = expr(n.o)
504
+ const T = constOf(`new Map([${n.entries.map(([k, id]) => `[${lit(k)},${procName(id)}]`).join(',')}])`)
505
+ return `{const ks=Object.getOwnPropertyNames(${o});for(let j=0;j<ks.length;j++){const f=${T}.get(ks[j]);if(f!==void 0)f(${o}[ks[j]])}}\n`
506
+ }
507
+ }
508
+ throw new Error('ir emit: statement ' + n.t)
509
+ }
510
+
511
+ function branches (n, v) {
512
+ const isAny = n.kw === 'anyOf'
513
+ const ps = n.procs.map(procName)
514
+ // The calls that gather no errors get no paths (see callOf).
515
+ const x = expr(n.x); const path = expr(n.path); const ev = expr(n.ev); const sps = n.sps.map(expr)
516
+ if (v) {
517
+ const calls = ps.map((p) => `${p}(${x},null,s,"",${ev},"",null)`)
518
+ // One branch passes exactly when it passes, for anyOf and oneOf: the
519
+ // form of a required call, so a repeat of it is seen (seq).
520
+ if (calls.length === 1) return `if(!${calls[0]})return false\n`
521
+ return `if(${isAny ? `!(${calls.join('||')})` : `(${calls.map((c) => `(${c}?1:0)`).join('+')})!==1`})return false\n`
522
+ }
523
+ use('collapse')
524
+ const titles = constOf(lit(n.titles))
525
+ const tgt = n.ann === 'onlyIfOne' ? 'h' : 'A'
526
+ if (n.ann) use('newAnn', 'mergeAnn')
527
+ const vcalls = ps.map((p, i) => n.ann
528
+ ? `{const a=newAnn();if(${p}(${x},null,s,"",${ev},"",a)){c++;mergeAnn(${tgt},a)}}`
529
+ : `if(${p}(${x},null,s,"",${ev},"",null)){c++${isAny ? ';break b' : ''}}`).join('')
530
+ const ecalls = ps.map((p, i) => `(()=>{const be=[]${n.ann ? ',a=newAnn()' : ''};${p}(${x},be,s,${path},${ev},${sps[i]},${n.ann ? 'a' : 'null'});const ok=be.length===0;${n.ann ? `if(ok)mergeAnn(${tgt},a);` : ''}return{valid:ok,errors:be}})()`).join(',')
531
+ const sp = expr(n.sp)
532
+ // An anyOf with no annotations runs each branch once, filling a list of
533
+ // its own, and stops at the first that passes: no branch runs twice,
534
+ // whether the keyword passes or fails.
535
+ if (isAny && !n.ann) {
536
+ const tries = ps.map((p, i) => `{const be=[];${p}(${x},be,s,${path},${ev},${sps[i]},null);if(be.length===0){ok=true;break b}br.push({valid:false,errors:be})}`).join('')
537
+ return `if(e===null){let c=0;b:{${vcalls}}if(c===0)return false}else{let ok=false;const br=[];b:{${tries}}if(!ok){const r=collapse(${lit(n.kw)},br,${path},${sp},null,${titles});if(r!==null)e.push(${plain ? 'r' : `E(r.code,${lit(n.kw)},${path},${sp},r.params,r.message,void 0,${n.rank},${ev},s,void 0,void 0,r.branchErrors)`})}}\n`
538
+ }
539
+ // Otherwise the verdict first, with or without a list to fill: the branches' errors
540
+ // are built only when the keyword fails (collapse answers null exactly
541
+ // when it passes). Building them for a passing anyOf under propertyNames
542
+ // made a 257-key object 24x slower than the current engine. A failing
543
+ // run calls the branches again, merging the same annotations twice,
544
+ // which changes nothing.
545
+ return `{${n.ann === 'onlyIfOne' ? 'const h=newAnn();' : ''}let c=0;b:{${vcalls}}if(${isAny ? 'c!==0' : 'c===1'}){${n.ann === 'onlyIfOne' ? 'mergeAnn(A,h)' : ''}}else if(e===null)return false;else{const r=collapse(${lit(n.kw)},[${ecalls}],${path},${sp},null,${titles});if(r!==null)e.push(${plain ? 'r' : `E(r.code,${lit(n.kw)},${path},${sp},r.params,r.message,void 0,${n.rank},${ev},s,void 0,void 0,r.branchErrors)`})}}\n`
546
+ }
547
+
548
+ function pvExpr (n) {
549
+ const pv = n.pv
550
+ if (pv instanceof RegExp) return constOf(`new RegExp(${lit(pv.source)},${lit(pv.flags)})`)
551
+ if (typeof pv === 'function') { use(pv.name); return pv.name }
552
+ if (typeof pv === 'object' && pv !== null) return constOf(lit(pv))
553
+ return lit(pv)
554
+ }
555
+
556
+ function expr (n) {
557
+ switch (n.t) {
558
+ case 'lit': return lit(n.v)
559
+ case 'var': return n.n
560
+ case 'get': return `${expr(n.o)}[${expr(n.k)}]`
561
+ case 'field': return `${expr(n.o)}.${n.k}`
562
+ case 'own': return ownSrc(expr(n.o), expr(n.k))
563
+ case 'not': return `!(${expr(n.e)})`
564
+ case 'and': return `(${expr(n.a)}&&${expr(n.b)})`
565
+ case 'or': return `(${expr(n.a)}||${expr(n.b)})`
566
+ case 'cmp': return `${expr(n.a)}${n.op}${expr(n.b)}`
567
+ case 'isObj': use('isObj'); return `isObj(${expr(n.x)})`
568
+ case 'isArr': return `Array.isArray(${expr(n.x)})`
569
+ case 'isObjAny': { const x = expr(n.x); return `(typeof ${x}==="object"&&${x}!==null)` }
570
+ case 'len': return `${expr(n.x)}.length`
571
+ case 'plainProto': { const x = expr(n.x); return `(PN?${x}.__proto__:Object.getPrototypeOf(${x}))===Object.prototype` }
572
+ case 'type': {
573
+ const x = expr(n.x)
574
+ if (n.types.length === 1 && TYPES[n.types[0]]) return inline(TYPES[n.types[0]], x)
575
+ // A union over a variable: each type's own predicate, joined, as
576
+ // isTypes would try them; the table stays out of the module.
577
+ if (/^[A-Za-z_$][\w$]*$/.test(x) && n.types.every((t) => TYPES[t])) return `(${n.types.map((t) => inline(TYPES[t], x)).join('||')})`
578
+ use('isTypes'); return `isTypes(${x},${lit(n.types)})`
579
+ }
580
+ case 'guard': return GUARD[n.g](expr(n.x))
581
+ case 'kernel': {
582
+ const x = expr(n.x)
583
+ if (n.kw === 'format') return `${formatFn(n.n)}(${x})`
584
+ // 64 or more strings: a Set, one lookup instead of a chain that
585
+ // grows with the list (the threshold and its measurements are
586
+ // ENUM_SET_MIN in lib/js-compiler.js).
587
+ if (n.kw === 'enum' && Array.isArray(n.n) && n.n.length >= 64 && n.n.every((v) => typeof v === 'string')) {
588
+ const key = JSON.stringify(n.n)
589
+ if (!enumSets.has(key)) enumSets.set(key, constOf(`new Set(${key})`))
590
+ return `${enumSets.get(key)}.has(${x})`
591
+ }
592
+ const fast = FAST[n.kw] ? FAST[n.kw](n.n, x) : null
593
+ const src = fast ? `(${fast})` : inline(n.K.ok, x, pvExpr(n))
594
+ for (const id of ['cpLen', 'deq']) if (src.includes(id + '(')) use(id)
595
+ return src
596
+ }
597
+ case 'kfail': use(n.K.fail.name); return `${n.K.fail.name}(${expr(n.x)},${pvExpr(n)})`
598
+ case 'known': {
599
+ const k = expr(n.k)
600
+ // More than eight names: told apart by length first, a switch the
601
+ // engine makes a jump table, then compared with the few of that
602
+ // length, unless one length holds many (then a Set).
603
+ let names
604
+ if (n.names.length <= 8) names = n.names.map((q) => `${k}===${lit(q)}`)
605
+ else {
606
+ const byLen = new Map()
607
+ for (const q of n.names) { if (!byLen.has(q.length)) byLen.set(q.length, []); byLen.get(q.length).push(q) }
608
+ if (Math.max(...[...byLen.values()].map((a) => a.length)) <= 6) {
609
+ const key = JSON.stringify(n.names)
610
+ if (!knownFns.has(key)) knownFns.set(key, constOf(`(k)=>{switch(k.length){${[...byLen].map(([l, qs]) => `case ${l}:return ${qs.map((q) => `k===${lit(q)}`).join('||')}`).join(';')}}return false}`))
611
+ names = [`${knownFns.get(key)}(${k})`]
612
+ } else names = [`${constOf(`new Set(${lit(n.names)})`)}.has(${k})`]
613
+ }
614
+ const pats = n.pats.map((p) => `${constOf(`new RegExp(${lit(p)},"u")`)}.test(${k})`)
615
+ const all = [...names, ...pats]
616
+ return all.length ? `(${all.join('||')})` : 'false'
617
+ }
618
+ case 're': return `${constOf(`new RegExp(${lit(n.src)},"u")`)}.test(${expr(n.k)})`
619
+ case 'seg': use('seg'); return `seg(${expr(n.k)})`
620
+ case 'cat': return `${expr(n.a)}+${expr(n.b)}`
621
+ case 'str': return n.parts.map(expr).join('+')
622
+ case 'obj': return `{${n.fields.map(([k, v]) => `${JSON.stringify(k)}:${expr(v)}`).join(',')}}`
623
+ case 'annKey': return `(A.p===null?A.p=new Set():A.p).add(${expr(n.k)})`
624
+ case 'annHasKey': return `(A.p!==null&&A.p.has(${expr(n.k)}))`
625
+ case 'annPrefix': return `A.n=Math.max(A.n,Math.min(${n.n},${expr(n.arr)}.length))`
626
+ case 'annAll': return 'A.all=true'
627
+ case 'annAllSet': return 'A.all'
628
+ case 'annAt': return `(A.at===null?A.at=new Set():A.at).add(${expr(n.i)})`
629
+ case 'annAtHas': return `(A.at!==null&&A.at.has(${expr(n.i)}))`
630
+ case 'annN': return 'A.n'
631
+ case 'firstUneval': use('firstUneval'); return `firstUneval(A,${expr(n.x)})`
632
+ case 'pcall': return callOf(n, n.e === 'inherit' ? 'e' : 'null', 'null')
633
+ }
634
+ throw new Error('ir emit: expression ' + n.t)
635
+ }
636
+
637
+ const fillName = fill ? procName(fill) : null
638
+ const pre = fillName ? `${fillName}(d);` : ''
639
+ const dual = procName(ir.dual)
640
+ const ver = verdict ? procName(ir.verdict) : null
641
+ if (ir.relate && !plain) use('relate')
642
+ const sorts = WALKED || needsOrder(ir.schema)
643
+ if (sorts) use('ordered')
644
+ const names = [...uses].filter((u) => KERNEL_NAMES.has(u)).sort()
645
+ const imports = names.length ? `import {${names.join(',')}} from ${JSON.stringify(from)}` : ''
646
+ return `${imports}
647
+ const PN=({}).__proto__===Object.prototype
648
+ ${ir.dynamic ? `let SC=null
649
+ function pick(T,f){const a=[];for(let c=SC;c!==null;c=c.up)a.push(c.b);for(let i=a.length-1;i>=0;i--){const g=T.get(a[i]);if(g!==undefined)return g}return f}` : ''}
650
+ ${consts.join('\n')}
651
+ ${fns.join('\n')}
652
+ const NONE=Object.freeze([])
653
+ ${sorts ? `const SK=JSON.parse(${JSON.stringify(JSON.stringify(orderSkeleton(ir.schema)))})\n` : ''}export function isValid(d){${pre}return ${ver || dual}(d,null,true,"",d,"#",null)}
654
+ export function validate(d){${pre}if(${ver || dual}(d,null,true,"",d,"#",null))return{valid:true,data:d,errors:NONE};const e=[];${dual}(d,e,!(!d&&d!==0&&d!==false),"",d,"#",null);const o=${sorts ? 'ordered(SK,e)' : 'e'};return{valid:false,errors:${ir.relate && !plain ? 'o.length>1?relate(o):o' : 'o'}}}
655
+ `
656
+ }
657
+
658
+ export function specialize (schema, opts = {}) {
659
+ // A module cannot carry the caller's functions.
660
+ if (opts.formats && Object.values(opts.formats).some((f) => typeof f === 'function')) throw new Error('ir: custom formats need the closure backend')
661
+ if (opts.keywords && Object.keys(opts.keywords).length) throw new Error('ir: custom keywords need the closure backend')
662
+ // Options that change the document or the shape of the answer: a module
663
+ // does not apply them, so it refuses them rather than answer without them.
664
+ for (const k of ['coerceTypes', 'removeAdditional', 'verbose', 'abortEarly']) if (opts[k]) throw new Error(`ir: ${k} needs the closure backend`)
665
+ const ir = front(schema, { assertFormat: opts.assertFormat !== false, schemas: opts.schemas || null, metaschemas: opts.metaschemas || null, baseURI: opts.baseURI || null })
666
+ const wantFill = opts.useDefaults !== false && hasDefault(schema)
667
+ // The specialized fill from the IR; the shared pass where it declined.
668
+ const src = emitIR(ir, { ...opts, fill: wantFill && ir.fill && ir.fill !== 'generic' ? ir.fill : null })
669
+ if (!wantFill || ir.fill !== 'generic') return src
670
+ // Where the IR declines the fill, the caller may hand the runtime's
671
+ // generated pass as source (`fillSource`, ata-validator/build's
672
+ // defaultsSource): written into the module as a function, the same pass the
673
+ // runtime and the previous compiled modules run. The interpreted shared pass
674
+ // took 780 of codeclimate's 855 ns.
675
+ // Not with other documents registered: the pass is built from this schema
676
+ // alone and would not follow a reference into them.
677
+ const own = typeof opts.fillSource === 'function' && !opts.schemas ? opts.fillSource(schema) : null
678
+ if (typeof own === 'string') {
679
+ return src
680
+ .replace('export function isValid(d){return ', `function FILL(d){\n${own}\n}\nexport function isValid(d){FILL(d);return `)
681
+ .replace('export function validate(d){', 'export function validate(d){FILL(d);')
682
+ }
683
+ // The shared default pass (lib/defaults.js), built from the normalized
684
+ // schema at load.
685
+ return src
686
+ .replace(/^import \{([^}]*)\} from /m, (m, list) => `import {${['buildDefaultsApplier', ...list.split(',')].filter(Boolean).sort().join(',')}} from `)
687
+ .replace('export function isValid(d){return ', `const FILL=buildDefaultsApplier(${JSON.stringify(ir.schema)})\nexport function isValid(d){FILL(d);return `)
688
+ .replace('export function validate(d){', 'export function validate(d){FILL(d);')
689
+ }
690
+
691
+ function hasDefault (s) {
692
+ if (typeof s !== 'object' || s === null) return false
693
+ if (Array.isArray(s)) return s.some(hasDefault)
694
+ if ('default' in s) return true
695
+ for (const k in s) if (hasDefault(s[k])) return true
696
+ return false
697
+ }
698
+
699
+ // A module's text as the body of a function that takes the kernels object and
700
+ // returns { isValid, validate }: for a page that may evaluate code but loads
701
+ // no module from a string. The text is this file's own output, in which every
702
+ // value from the schema is written by JSON.stringify.
703
+ export function functionBody (src) {
704
+ const m = /^import \{([^}]*)\} from "[^"\n]*"\n/.exec(src)
705
+ const head = m ? `const {${m[1]}}=K\n` : ''
706
+ return head + (m ? src.slice(m[0].length) : src).replace(/^export function /gm, 'function ') + 'return{isValid,validate}\n'
707
+ }