ata-validator 1.62.0 → 1.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +68 -13
  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/aot-impl.js +20 -0
  9. package/lib/codegen-engine.js +72 -3
  10. package/lib/compiled-common.js +2 -1
  11. package/lib/email-literal.js +4 -1
  12. package/lib/error-runtime-features.js +41 -0
  13. package/lib/error-runtime.browser.js +15 -0
  14. package/lib/error-runtime.js +1 -32
  15. package/lib/formats-source.js +39 -40
  16. package/lib/interpreter-core.js +17 -9
  17. package/lib/ir/emit-deps.mjs +131 -0
  18. package/lib/ir/emit.mjs +560 -0
  19. package/lib/ir/front.mjs +834 -0
  20. package/lib/ir/index.mjs +10 -0
  21. package/lib/ir/refs.mjs +108 -0
  22. package/lib/ir/run.mjs +520 -0
  23. package/lib/ir/table.mjs +233 -0
  24. package/lib/js-compiler-errors.js +25 -7
  25. package/lib/js-compiler.js +6 -2
  26. package/lib/kernels/collapse.mjs +114 -0
  27. package/lib/kernels/custom.mjs +70 -0
  28. package/lib/kernels/fill.mjs +6 -0
  29. package/lib/kernels/formats-small.mjs +40 -0
  30. package/lib/kernels/formats.mjs +29 -0
  31. package/lib/kernels/index.mjs +11 -0
  32. package/lib/kernels/kernels.mjs +285 -0
  33. package/lib/kernels/leaves.mjs +28 -0
  34. package/lib/kernels/order.mjs +25 -0
  35. package/lib/kernels/plan-rich.mjs +17 -0
  36. package/lib/kernels/plan.d.ts +9 -0
  37. package/lib/kernels/plan.mjs +128 -0
  38. package/lib/kernels/preprocess.mjs +223 -0
  39. package/lib/kernels/rank.mjs +97 -0
  40. package/lib/kernels/rt.mjs +249 -0
  41. package/lib/kernels/table.mjs +182 -0
  42. package/lib/kernels/unique.mjs +157 -0
  43. package/lib/native-positions.js +1 -1
  44. package/lib/openapi.d.ts +2 -0
  45. package/lib/openapi.js +128 -0
  46. package/lib/plan-compiler.js +84 -7
  47. package/lib/public-api.js +7 -2
  48. package/lib/rejections.js +3 -1
  49. package/lib/schema-helpers.js +12 -0
  50. package/lib/schema-helpers.mjs +20 -0
  51. package/lib/schema-order.js +4 -2
  52. package/lib/standard-schema.js +1 -1
  53. package/lib/validator-core.js +35 -30
  54. package/lib/version.js +1 -1
  55. package/lite.js +7 -2
  56. package/package.json +32 -13
@@ -0,0 +1,560 @@
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.
48
+ const WALK_MIN = 16
49
+ // Source characters past which a verdict body is its own function (the
50
+ // verdict threshold of the current engine's generator, SPLIT_PROP_SCHEMA).
51
+ const SPLIT_MIN = 2000
52
+ // Entries a verdict's walk looks up in a table rather than a switch. A call
53
+ // through the table is indirect, so V8 does not inline a small check behind
54
+ // it: when most entries call a function of INLINE_MAX characters or fewer,
55
+ // the switch's direct calls win below 64 entries (mocharc 650 to 229 ns,
56
+ // huskyrc 290 to 149, targets about 90 characters). With larger targets
57
+ // there is nothing to inline and the table stays from WALK_MIN entries
58
+ // (abc-supply-plan, targets of 230 characters and more, 8-16% slower as a
59
+ // switch).
60
+ const TABLE_MIN = 64
61
+ const INLINE_MAX = 160
62
+ // A verdict that visits an object's names moves even small entries out when
63
+ // they would add up past this many characters, and otherwise only the ones of
64
+ // SPLIT_SMALL characters or more (the current engine's SPLIT_FN_BUDGET and
65
+ // SPLIT_PROP_SMALL).
66
+ const SPLIT_BUDGET = 24 * 1024
67
+ const SPLIT_SMALL = 220
68
+
69
+ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, fill = null, richErrors = true } = {}) {
70
+ // richErrors false: the current engine's plain shape (keyword, instancePath,
71
+ // schemaPath, params, message), no enrichment, no related links.
72
+ const plain = richErrors === false
73
+ // Set when an object's names are visited in the object's order (`props`).
74
+ let WALKED = false
75
+ const uses = new Set(richErrors === false ? [] : ['E'])
76
+ const consts = []
77
+ const constOf = (src) => { const name = 'K' + consts.length; consts.push(`const ${name}=${src}`); return name }
78
+ const emitted = new Map()
79
+ const fns = []
80
+ const use = (...n) => { for (const x of n) if (x) uses.add(x) }
81
+ const ident = (src) => { for (const m of src.matchAll(/[A-Za-z_$][\w$]*/g)) use(m[0]); return src }
82
+
83
+ const hintFns = new Map()
84
+ const enumSets = new Map()
85
+ const knownFns = new Map()
86
+ // props nodes whose walk also rejects names properties does not list.
87
+ const folded = new WeakSet()
88
+ // Each emitted function's body size, for the choice between a switch of
89
+ // direct calls and a table (a procedure still being written has none).
90
+ const sizes = new Map()
91
+ // Imported from ata-validator/kernels (lib/kernels/formats.mjs, the same
92
+ // generated checks), so a page with several modules carries each once.
93
+ const formatFn = (name) => {
94
+ const fn = 'fmt_' + name.replace(/-/g, '_')
95
+ if (!KERNEL_NAMES.has(fn)) throw new Error(`ir emit: no generated check for format "${name}"`)
96
+ use(fn)
97
+ return fn
98
+ }
99
+ const procName = (id) => {
100
+ if (emitted.has(id)) return emitted.get(id)
101
+ const p = ir.procs.get(id)
102
+ const name = id
103
+ emitted.set(id, name)
104
+ const isVerdict = p.mode === 'verdict'
105
+ if (p.mode === 'fill') { fns.push(`function ${name}(d){\n${stmt(p.body, true)}}`); return name }
106
+ const body = stmt(p.body, isVerdict)
107
+ const head = p.mode === 'ann' ? 'if(A===null)A=newAnn()\n' : ''
108
+ if (p.mode === 'ann') use('newAnn')
109
+ const wrapped = ir.dynamic
110
+ ? `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}`
111
+ : `${body}return true`
112
+ fns.push(`function ${name}(d,e,s,P,V,SP,A){${head}${wrapped}}`)
113
+ sizes.set(name, wrapped.length)
114
+ return name
115
+ }
116
+
117
+ // A failure: `return false` in a verdict, a pushed error otherwise.
118
+ const fail = (er, v) => {
119
+ if (v) return 'return false'
120
+ 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)}})}`
121
+ let detail = 'void 0'; let suggest = 'void 0'
122
+ // A kernel function generated code names must have a name to import.
123
+ if ((er.detail && !er.detail.src) || (er.suggest && !er.suggest.src)) throw new Error(`ir emit: an unnamed enrichment function for ${er.kw}`)
124
+ if (er.detail) detail = ident(er.detail.src)
125
+ // A format's suggestion: only that format's hint, from the current
126
+ // engine's table (lib/suggestions.js FORMAT_HINTS), written in as source.
127
+ if (er.suggest && er.suggest.src === 'formatSuggest') {
128
+ const f = er.params.fields.find(([k]) => k === 'format')[1].v
129
+ const hint = FORMAT_HINTS[f]
130
+ 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'
131
+ } else if (er.suggest) suggest = /\(/.test(er.suggest.src) ? (ident(er.suggest.src), constOf(er.suggest.src)) : ident(er.suggest.src)
132
+ 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}))}`
133
+ }
134
+
135
+ // A procedure call: its arguments, its target (a dynamic pick or fixed).
136
+ const callOf = (n, eArg, aArg) => {
137
+ if (n.proc === null) throw new Error('ir: a v1 $dynamicRef with no target of its own needs the closure backend')
138
+ const fn = procName(n.proc)
139
+ let target = fn
140
+ if (n.dynamic) { const table = constOf(`new Map([${n.dynamic.map(([b, id]) => `[${lit(b)},${procName(id)}]`).join(',')}])`); target = `pick(${table},${fn})` }
141
+ // A call that gathers no errors never reads its paths (custom keywords,
142
+ // the one reader that could, are refused by this writer): no strings to build.
143
+ const quiet = eArg === 'null'
144
+ // A call on a cycle (lib/ir/front.mjs `guard`): through a function that
145
+ // keeps the values this call is on and answers a second entry with no
146
+ // error, as the current engine cuts a reference it meets again.
147
+ if (n.guard) {
148
+ const G = constOf('new Set()'); const w = 'G' + fns.length
149
+ 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)}}`)
150
+ target = w
151
+ }
152
+ 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})`
153
+ }
154
+
155
+ // A verdict statement as a function of `name`, called the way a procedure
156
+ // is (value, e, s): it leaves only by `return false`.
157
+ const hoist = (node, name, text = stmt(node, true)) => {
158
+ const fn = 'L' + fns.length
159
+ fns.push(`function ${fn}(${name},e,s){\n${text}return true}`)
160
+ sizes.set(fn, text.length)
161
+ return fn
162
+ }
163
+
164
+ // Whether a statement reads no name but `name` and the ones it declares,
165
+ // and nothing of annotations (the caller's collector).
166
+ const selfContained = (n, name) => {
167
+ const bound = new Set([name]); const used = []; let ann = false
168
+ const walk = (x) => {
169
+ if (x === null || typeof x !== 'object') return
170
+ if (Array.isArray(x)) { for (const y of x) walk(y); return }
171
+ if (typeof x.t === 'string') {
172
+ if (x.t === 'var' || x.t === 'incr') used.push(x.n ?? x.name)
173
+ else if (x.t === 'let' || x.t === 'count') bound.add(x.name)
174
+ else if (x.t === 'keys') bound.add(x.k)
175
+ else if (x.t === 'range') bound.add(x.i)
176
+ else if (x.t === 'bind') for (const [k] of x.names) bound.add(k)
177
+ if (x.t.startsWith('ann') || x.t === 'firstUneval' || x.ann) ann = true
178
+ }
179
+ for (const k in x) if (k !== 'proc' && k !== 'K') walk(x[k])
180
+ }
181
+ walk(n)
182
+ return !ann && used.every((u) => bound.has(u))
183
+ }
184
+
185
+ // A keyed entry that reads its value and, when present, requires one
186
+ // procedure of it gathering nothing: the call, or null.
187
+ const verdictCall = (n) => {
188
+ if (n.t !== 'let' || n.body.t !== 'if' || n.body.else) return null
189
+ let b = n.body.then
190
+ while (b.t === 'seq' && b.body.length === 1) b = b.body[0]
191
+ if (b.t !== 'require') return null
192
+ const c = b.e
193
+ 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
194
+ }
195
+
196
+
197
+ // A default's value as source: a literal, a fresh empty array, or a copy.
198
+ const defaultSrc = (val) => {
199
+ if (typeof val !== 'object' || val === null) return lit(val)
200
+ if (Array.isArray(val) && val.length === 0) return '[]'
201
+ use('copyOf'); return `copyOf(${constOf(lit(val))})`
202
+ }
203
+
204
+ function stmt (n, v) {
205
+ switch (n.t) {
206
+ case 'seq': {
207
+ // A verdict that walks an object's names for its properties and has
208
+ // additionalProperties: false over the same object finds the unknown
209
+ // names in that walk: the separate pass over the object goes.
210
+ let skip = null
211
+ if (v) {
212
+ 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')
213
+ const ap = walked && n.body.find((c) => c.t === 'if' && c.then.t === 'keys' && c.then.apClosed && c.then.obj.t === 'var' && c.then.obj.n === walked.then.x.n)
214
+ if (ap) { skip = ap; folded.add(walked.then) }
215
+ }
216
+ return n.body.filter((x) => x !== skip).map((x) => stmt(x, v)).join('')
217
+ }
218
+ // An object that lists many names: visit the object's own names, every
219
+ // one enumerable or not (an entry checks its own presence, so a name it
220
+ // skips is one the object lacks), instead of looking each listed name
221
+ // up. Errors then come in the object's key order, so the module sorts
222
+ // them into schema order as the current engine does (WALKED).
223
+ case 'props': {
224
+ if (n.keyed === null || n.keyed.length < WALK_MIN) return n.body.map((x) => stmt(x, v)).join('')
225
+ WALKED = true
226
+ const keyed = new Set(n.keyed.map(([, i]) => i))
227
+ const always = n.body.filter((_, i) => !keyed.has(i)).map((x) => stmt(x, v)).join('')
228
+ // In a verdict, the entries that only call a procedure on their value
229
+ // go in a table by name: one lookup per key instead of a comparison
230
+ // per listed name. An entry whose checks read only its value can move
231
+ // into a function of its own and go in the table too: a large one
232
+ // always, a small one when the names together would make a function
233
+ // too large for V8 to optimize (a call costs more than a short check).
234
+ const X = expr(n.x)
235
+ const rows = n.keyed.map(([p, i]) => {
236
+ const e = n.body[i]
237
+ const c = v ? verdictCall(e) : null
238
+ if (c !== null) return { p, fn: procName(c.proc) }
239
+ if (v && e.t === 'let' && e.body.t === 'if' && !e.body.else && selfContained(e.body.then, e.name)) {
240
+ const text = stmt(e.body.then, true)
241
+ return { p, e, text, src: `{const ${e.name}=${expr(e.value)}\nif(${expr(e.body.c)}){\n${text}}}\n` }
242
+ }
243
+ return { p, src: stmt(e, v) }
244
+ })
245
+ const big = rows.reduce((t, r) => t + (r.src ? r.src.length : 0), 0) > SPLIT_BUDGET
246
+ 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)
247
+ const tabled = rows.filter((r) => r.fn)
248
+ const small = tabled.filter((r) => (sizes.get(r.fn) ?? Infinity) <= INLINE_MAX).length
249
+ const useTable = tabled.length >= TABLE_MIN || (tabled.length >= WALK_MIN && small * 2 < tabled.length)
250
+ const left = rows.filter((r) => !useTable || !r.fn)
251
+ const body = (r) => r.fn ? `{const y=${X}[${lit(r.p)}];if(y!==void 0&&!${r.fn}(y,null,s,"",y,"",null))return false}\n` : r.src
252
+ // A key is told from the names by its length first, a switch on an
253
+ // integer the engine makes a jump table, and then compared with the
254
+ // few names of that length: a switch on the names compared each key
255
+ // with half of them, which lost to a lookup per name once an object
256
+ // carried most of fifty (htmlhint).
257
+ const byLen = new Map()
258
+ for (const r of left) { const l = r.p.length; if (!byLen.has(l)) byLen.set(l, []); byLen.get(l).push(r) }
259
+ // Folded (see seq): every listed name has a case, a name without
260
+ // checks an empty one, and a name none of them is, own and
261
+ // enumerable, fails the verdict.
262
+ const fold = folded.has(n)
263
+ const unknown = fold ? `if(${constOf('Object.prototype.propertyIsEnumerable')}.call(${X},k))return false\n` : ''
264
+ 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 }) } }
265
+ const dispatch = (k) => `switch(${k}.length){\n${[...byLen].map(([l, rs]) => `case ${l}:${rs.map((r) => `if(${k}===${lit(r.p)}){\n${r.empty ? '' : body(r)}}`).join('else ')}${fold ? `else{${unknown}}` : ''}break\n`).join('')}${fold ? `default:${unknown}` : ''}}`
266
+ // A name the table answers is listed; with the fold every other name
267
+ // goes through the dispatch, which knows the listed ones.
268
+ if (!useTable) return `${always}{const ks=Object.getOwnPropertyNames(${X});for(let j=0;j<ks.length;j++){const k=ks[j];${dispatch('k')}}}\n`
269
+ const T = constOf(`new Map([${tabled.map((r) => `[${lit(r.p)},${r.fn}]`).join(',')}])`)
270
+ return `${always}{const ks=Object.getOwnPropertyNames(${X});for(let j=0;j<ks.length;j++){const k=ks[j],f=${T}.get(k);if(f!==void 0){const y=${X}[k];if(y!==void 0&&!f(y,null,s,"",y,"",null))return false}${left.length || fold ? `else ${dispatch('k')}` : ''}}}\n`
271
+ }
272
+ case 'if': {
273
+ // A condition that is a call under annotations merges its collector.
274
+ if (n.c.t === 'pcall' && n.c.ann === 'merge') {
275
+ use('newAnn', 'mergeAnn')
276
+ 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`
277
+ }
278
+ return `if(${expr(n.c)}){\n${stmt(n.then, v)}}${n.else ? `else{\n${stmt(n.else, v)}}` : ''}\n`
279
+ }
280
+ case 'let': {
281
+ // A large verdict body becomes a function of its value: V8 does not
282
+ // optimize a function past its bytecode limit, and a schema inlined
283
+ // whole into one can pass it. A verdict body leaves only by `return
284
+ // false`, so the call keeps its meaning; what moves must read nothing
285
+ // of the enclosing function but the value. A presence test in front
286
+ // (it reads the parent) stays where it is.
287
+ const guarded = n.body.t === 'if' && !n.body.else && !(n.body.c.t === 'pcall' && n.body.c.ann === 'merge')
288
+ const inner = guarded ? n.body.then : n.body
289
+ const text = stmt(inner, v)
290
+ if (v && !ir.dynamic && text.length > SPLIT_MIN && selfContained(inner, n.name)) {
291
+ const call = `if(!${hoist(inner, n.name, text)}(${n.name},null,s))return false\n`
292
+ return `{const ${n.name}=${expr(n.value)}\n${guarded ? `if(${expr(n.body.c)}){\n${call}}\n` : call}}\n`
293
+ }
294
+ return `{const ${n.name}=${expr(n.value)}\n${guarded ? `if(${expr(n.body.c)}){\n${text}}\n` : text}}\n`
295
+ }
296
+ case 'bind': return `const ${n.names.map(([k, e]) => `${k}=${expr(e)}`).join(',')}\n`
297
+ case 'keys': {
298
+ if (n.order === 'forin') return `for(const ${n.k} in ${expr(n.obj)}){\n${stmt(n.body, v)}}\n`
299
+ // A verdict stops at the first failure whatever the order, so it
300
+ // needs no __proto__ moved last. (for-in with an own check, which
301
+ // allocates no array, made a schema alone up to 3x faster but 13 of
302
+ // SchemaStore's slower by 6-80% when all of them run in one process.)
303
+ const last = n.order === 'protoLast' && !v
304
+ if (last) use('keysOf')
305
+ return `for(const ${n.k} of ${last ? 'keysOf' : 'Object.keys'}(${expr(n.obj)})){\n${stmt(n.body, v)}}\n`
306
+ }
307
+ 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`
308
+ case 'count': return `{let ${n.name}=0\n${stmt(n.body, v)}}\n`
309
+ case 'incr': return `${n.name}++\n`
310
+ case 'do': return `${expr(n.e)}\n`
311
+ case 'ret': return 'return false\n'
312
+ case 'require': {
313
+ const c = n.e
314
+ if (c.ann === 'merge') {
315
+ use('newAnn', 'mergeAnn')
316
+ return `{const a=newAnn(),c=e===null?0:e.length;if(${callOf(c, 'e', 'a')}&&(e===null||e.length===c))mergeAnn(A,a);else if(e===null)return false}\n`
317
+ }
318
+ if (c.e === 'inherit') return v ? `if(!${callOf(c, 'null', 'null')})return false\n` : `if(!${callOf(c, 'e', 'null')}&&e===null)return false\n`
319
+ return `if(!${callOf(c, 'null', 'null')})${v ? 'return false' : '{if(e===null)return false}'}\n`
320
+ }
321
+ case 'fail': return fail(n.err, v) + '\n'
322
+ case 'branches': return branches(n, v)
323
+ case 'setDefault': {
324
+ use('own')
325
+ if (n.k === '__proto__') use('setOwn')
326
+ const val = n.v
327
+ let src
328
+ if (typeof val !== 'object' || val === null) src = lit(val)
329
+ else if (Array.isArray(val) && val.length === 0) src = '[]'
330
+ else { use('copyOf'); src = `copyOf(${constOf(lit(val))})` }
331
+ const o = expr(n.o)
332
+ // A store under the name itself: through setOwn's computed key, V8
333
+ // turned an object given more than a few defaults into a dictionary,
334
+ // slow from then on for every reader (htmlhint's root, 23 defaults:
335
+ // getOwnPropertyNames 10 to 999 ns). __proto__ needs defineProperty.
336
+ if (n.k === '__proto__') return `if(!own(${o},${lit(n.k)}))setOwn(${o},${lit(n.k)},${src})\n`
337
+ // A read or `in` before hasOwnProperty, as the current engine's fill
338
+ // does, was measured both ways: faster where the defaults are mostly
339
+ // present (jshintrc 1195 to 522 ns), slower where they are mostly
340
+ // missing (prometheus 3671 to 4601). hasOwnProperty alone is kept.
341
+ return `if(!own(${o},${lit(n.k)}))${o}[${lit(n.k)}]=${src}\n`
342
+ }
343
+ case 'fcall': return `${procName(n.proc)}(${expr(n.x)})\n`
344
+ case 'defaultsMask': {
345
+ // Bits in 32-bit words, set from the object's own names told apart by
346
+ // length and then by name; a default is written where its bit is clear.
347
+ const o = expr(n.o)
348
+ const words = Math.ceil(n.entries.length / 32)
349
+ const m = (i) => 'm' + Math.floor(i / 32)
350
+ const byLen = new Map()
351
+ n.entries.forEach(([k], i) => { if (!byLen.has(k.length)) byLen.set(k.length, []); byLen.get(k.length).push([k, i]) })
352
+ 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('')
353
+ const sets = n.entries.map(([k, val], i) => `if((${m(i)}&${(1 << (i % 32)) >>> 0})===0)${o}[${lit(k)}]=${defaultSrc(val)}\n`).join('')
354
+ 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`
355
+ }
356
+ // The object's own names, each looked up in a table of the fills of
357
+ // the properties that have one (lib/ir/front.mjs fillProc).
358
+ case 'fillWalk': {
359
+ const o = expr(n.o)
360
+ const T = constOf(`new Map([${n.entries.map(([k, id]) => `[${lit(k)},${procName(id)}]`).join(',')}])`)
361
+ 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`
362
+ }
363
+ }
364
+ throw new Error('ir emit: statement ' + n.t)
365
+ }
366
+
367
+ function branches (n, v) {
368
+ const isAny = n.kw === 'anyOf'
369
+ const ps = n.procs.map(procName)
370
+ // The calls that gather no errors get no paths (see callOf).
371
+ const x = expr(n.x); const path = expr(n.path); const ev = expr(n.ev); const sps = n.sps.map(expr)
372
+ if (v) {
373
+ const calls = ps.map((p) => `${p}(${x},null,s,"",${ev},"",null)`)
374
+ return `if(${isAny ? `!(${calls.join('||')})` : `(${calls.map((c) => `(${c}?1:0)`).join('+')})!==1`})return false\n`
375
+ }
376
+ use('collapse')
377
+ const titles = constOf(lit(n.titles))
378
+ const tgt = n.ann === 'onlyIfOne' ? 'h' : 'A'
379
+ if (n.ann) use('newAnn', 'mergeAnn')
380
+ const vcalls = ps.map((p, i) => n.ann
381
+ ? `{const a=newAnn();if(${p}(${x},null,s,"",${ev},"",a)){c++;mergeAnn(${tgt},a)}}`
382
+ : `if(${p}(${x},null,s,"",${ev},"",null)){c++${isAny ? ';break b' : ''}}`).join('')
383
+ 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(',')
384
+ const sp = expr(n.sp)
385
+ // An anyOf with no annotations runs each branch once, filling a list of
386
+ // its own, and stops at the first that passes: no branch runs twice,
387
+ // whether the keyword passes or fails.
388
+ if (isAny && !n.ann) {
389
+ 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('')
390
+ 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`
391
+ }
392
+ // Otherwise the verdict first, with or without a list to fill: the branches' errors
393
+ // are built only when the keyword fails (collapse answers null exactly
394
+ // when it passes). Building them for a passing anyOf under propertyNames
395
+ // made a 257-key object 24x slower than the current engine. A failing
396
+ // run calls the branches again, merging the same annotations twice,
397
+ // which changes nothing.
398
+ 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`
399
+ }
400
+
401
+ function pvExpr (n) {
402
+ const pv = n.pv
403
+ if (pv instanceof RegExp) return constOf(`new RegExp(${lit(pv.source)},${lit(pv.flags)})`)
404
+ if (typeof pv === 'function') { use(pv.name); return pv.name }
405
+ if (typeof pv === 'object' && pv !== null) return constOf(lit(pv))
406
+ return lit(pv)
407
+ }
408
+
409
+ function expr (n) {
410
+ switch (n.t) {
411
+ case 'lit': return lit(n.v)
412
+ case 'var': return n.n
413
+ case 'get': return `${expr(n.o)}[${expr(n.k)}]`
414
+ case 'field': return `${expr(n.o)}.${n.k}`
415
+ case 'own': use('own'); return `own(${expr(n.o)},${expr(n.k)})`
416
+ case 'not': return `!(${expr(n.e)})`
417
+ case 'and': return `(${expr(n.a)}&&${expr(n.b)})`
418
+ case 'or': return `(${expr(n.a)}||${expr(n.b)})`
419
+ case 'cmp': return `${expr(n.a)}${n.op}${expr(n.b)}`
420
+ case 'isObj': use('isObj'); return `isObj(${expr(n.x)})`
421
+ case 'isArr': return `Array.isArray(${expr(n.x)})`
422
+ case 'isObjAny': { const x = expr(n.x); return `(typeof ${x}==="object"&&${x}!==null)` }
423
+ case 'len': return `${expr(n.x)}.length`
424
+ case 'plainProto': { const x = expr(n.x); return `(PN?${x}.__proto__:Object.getPrototypeOf(${x}))===Object.prototype` }
425
+ case 'type': {
426
+ const x = expr(n.x)
427
+ if (n.types.length === 1 && TYPES[n.types[0]]) return inline(TYPES[n.types[0]], x)
428
+ // A union over a variable: each type's own predicate, joined, as
429
+ // isTypes would try them; the table stays out of the module.
430
+ if (/^[A-Za-z_$][\w$]*$/.test(x) && n.types.every((t) => TYPES[t])) return `(${n.types.map((t) => inline(TYPES[t], x)).join('||')})`
431
+ use('isTypes'); return `isTypes(${x},${lit(n.types)})`
432
+ }
433
+ case 'guard': return GUARD[n.g](expr(n.x))
434
+ case 'kernel': {
435
+ const x = expr(n.x)
436
+ if (n.kw === 'format') return `${formatFn(n.n)}(${x})`
437
+ // 64 or more strings: a Set, one lookup instead of a chain that
438
+ // grows with the list (the threshold and its measurements are
439
+ // ENUM_SET_MIN in lib/js-compiler.js).
440
+ if (n.kw === 'enum' && Array.isArray(n.n) && n.n.length >= 64 && n.n.every((v) => typeof v === 'string')) {
441
+ const key = JSON.stringify(n.n)
442
+ if (!enumSets.has(key)) enumSets.set(key, constOf(`new Set(${key})`))
443
+ return `${enumSets.get(key)}.has(${x})`
444
+ }
445
+ const fast = FAST[n.kw] ? FAST[n.kw](n.n, x) : null
446
+ const src = fast ? `(${fast})` : inline(n.K.ok, x, pvExpr(n))
447
+ for (const id of ['cpLen', 'deq']) if (src.includes(id + '(')) use(id)
448
+ return src
449
+ }
450
+ case 'kfail': use(n.K.fail.name); return `${n.K.fail.name}(${expr(n.x)},${pvExpr(n)})`
451
+ case 'known': {
452
+ const k = expr(n.k)
453
+ // More than eight names: told apart by length first, a switch the
454
+ // engine makes a jump table, then compared with the few of that
455
+ // length, unless one length holds many (then a Set).
456
+ let names
457
+ if (n.names.length <= 8) names = n.names.map((q) => `${k}===${lit(q)}`)
458
+ else {
459
+ const byLen = new Map()
460
+ for (const q of n.names) { if (!byLen.has(q.length)) byLen.set(q.length, []); byLen.get(q.length).push(q) }
461
+ if (Math.max(...[...byLen.values()].map((a) => a.length)) <= 6) {
462
+ const key = JSON.stringify(n.names)
463
+ 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}`))
464
+ names = [`${knownFns.get(key)}(${k})`]
465
+ } else names = [`${constOf(`new Set(${lit(n.names)})`)}.has(${k})`]
466
+ }
467
+ const pats = n.pats.map((p) => `${constOf(`new RegExp(${lit(p)},"u")`)}.test(${k})`)
468
+ const all = [...names, ...pats]
469
+ return all.length ? `(${all.join('||')})` : 'false'
470
+ }
471
+ case 're': return `${constOf(`new RegExp(${lit(n.src)},"u")`)}.test(${expr(n.k)})`
472
+ case 'seg': use('seg'); return `seg(${expr(n.k)})`
473
+ case 'cat': return `${expr(n.a)}+${expr(n.b)}`
474
+ case 'str': return n.parts.map(expr).join('+')
475
+ case 'obj': return `{${n.fields.map(([k, v]) => `${JSON.stringify(k)}:${expr(v)}`).join(',')}}`
476
+ case 'annKey': return `(A.p===null?A.p=new Set():A.p).add(${expr(n.k)})`
477
+ case 'annHasKey': return `(A.p!==null&&A.p.has(${expr(n.k)}))`
478
+ case 'annPrefix': return `A.n=Math.max(A.n,Math.min(${n.n},${expr(n.arr)}.length))`
479
+ case 'annAll': return 'A.all=true'
480
+ case 'annAllSet': return 'A.all'
481
+ case 'annAt': return `(A.at===null?A.at=new Set():A.at).add(${expr(n.i)})`
482
+ case 'annAtHas': return `(A.at!==null&&A.at.has(${expr(n.i)}))`
483
+ case 'annN': return 'A.n'
484
+ case 'firstUneval': use('firstUneval'); return `firstUneval(A,${expr(n.x)})`
485
+ case 'pcall': return callOf(n, n.e === 'inherit' ? 'e' : 'null', 'null')
486
+ }
487
+ throw new Error('ir emit: expression ' + n.t)
488
+ }
489
+
490
+ const fillName = fill ? procName(fill) : null
491
+ const pre = fillName ? `${fillName}(d);` : ''
492
+ const dual = procName(ir.dual)
493
+ const ver = verdict ? procName(ir.verdict) : null
494
+ if (ir.relate && !plain) use('relate')
495
+ const sorts = WALKED || needsOrder(ir.schema)
496
+ if (sorts) use('ordered')
497
+ const names = [...uses].filter((u) => KERNEL_NAMES.has(u)).sort()
498
+ const imports = names.length ? `import {${names.join(',')}} from ${JSON.stringify(from)}` : ''
499
+ return `${imports}
500
+ const PN=({}).__proto__===Object.prototype
501
+ ${ir.dynamic ? `let SC=null
502
+ 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}` : ''}
503
+ ${consts.join('\n')}
504
+ ${fns.join('\n')}
505
+ const NONE=Object.freeze([])
506
+ ${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)}
507
+ 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'}}}
508
+ `
509
+ }
510
+
511
+ export function specialize (schema, opts = {}) {
512
+ // A module cannot carry the caller's functions.
513
+ if (opts.formats && Object.values(opts.formats).some((f) => typeof f === 'function')) throw new Error('ir: custom formats need the closure backend')
514
+ if (opts.keywords && Object.keys(opts.keywords).length) throw new Error('ir: custom keywords need the closure backend')
515
+ // Options that change the document or the shape of the answer: a module
516
+ // does not apply them, so it refuses them rather than answer without them.
517
+ for (const k of ['coerceTypes', 'removeAdditional', 'verbose', 'abortEarly']) if (opts[k]) throw new Error(`ir: ${k} needs the closure backend`)
518
+ const ir = front(schema, { assertFormat: opts.assertFormat !== false, schemas: opts.schemas || null, metaschemas: opts.metaschemas || null, baseURI: opts.baseURI || null })
519
+ const wantFill = opts.useDefaults !== false && hasDefault(schema)
520
+ // The specialized fill from the IR; the shared pass where it declined.
521
+ const src = emitIR(ir, { ...opts, fill: wantFill && ir.fill && ir.fill !== 'generic' ? ir.fill : null })
522
+ if (!wantFill || ir.fill !== 'generic') return src
523
+ // Where the IR declines the fill, the caller may hand the runtime's
524
+ // generated pass as source (`fillSource`, ata-validator/build's
525
+ // defaultsSource): written into the module as a function, the same pass the
526
+ // runtime and the previous compiled modules run. The interpreted shared pass
527
+ // took 780 of codeclimate's 855 ns.
528
+ // Not with other documents registered: the pass is built from this schema
529
+ // alone and would not follow a reference into them.
530
+ const own = typeof opts.fillSource === 'function' && !opts.schemas ? opts.fillSource(schema) : null
531
+ if (typeof own === 'string') {
532
+ return src
533
+ .replace('export function isValid(d){return ', `function FILL(d){\n${own}\n}\nexport function isValid(d){FILL(d);return `)
534
+ .replace('export function validate(d){', 'export function validate(d){FILL(d);')
535
+ }
536
+ // The shared default pass (lib/defaults.js), built from the normalized
537
+ // schema at load.
538
+ return src
539
+ .replace(/^import \{([^}]*)\} from /m, (m, list) => `import {${['buildDefaultsApplier', ...list.split(',')].filter(Boolean).sort().join(',')}} from `)
540
+ .replace('export function isValid(d){return ', `const FILL=buildDefaultsApplier(${JSON.stringify(ir.schema)})\nexport function isValid(d){FILL(d);return `)
541
+ .replace('export function validate(d){', 'export function validate(d){FILL(d);')
542
+ }
543
+
544
+ function hasDefault (s) {
545
+ if (typeof s !== 'object' || s === null) return false
546
+ if (Array.isArray(s)) return s.some(hasDefault)
547
+ if ('default' in s) return true
548
+ for (const k in s) if (hasDefault(s[k])) return true
549
+ return false
550
+ }
551
+
552
+ // A module's text as the body of a function that takes the kernels object and
553
+ // returns { isValid, validate }: for a page that may evaluate code but loads
554
+ // no module from a string. The text is this file's own output, in which every
555
+ // value from the schema is written by JSON.stringify.
556
+ export function functionBody (src) {
557
+ const m = /^import \{([^}]*)\} from "[^"\n]*"\n/.exec(src)
558
+ const head = m ? `const {${m[1]}}=K\n` : ''
559
+ return head + (m ? src.slice(m[0].length) : src).replace(/^export function /gm, 'function ') + 'return{isValid,validate}\n'
560
+ }