ata-validator 1.62.1 → 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.
- package/README.md +63 -8
- package/build.mjs +1 -0
- package/index.browser.mjs +23 -4
- package/index.bundle.mjs +14 -4
- package/index.edge.mjs +14 -4
- package/index.js +7 -2
- package/lib/aot-build.js +18 -0
- package/lib/codegen-engine.js +72 -3
- package/lib/compiled-common.js +2 -1
- package/lib/email-literal.js +4 -1
- package/lib/formats-source.js +39 -40
- package/lib/interpreter-core.js +17 -9
- package/lib/ir/emit-deps.mjs +131 -0
- package/lib/ir/emit.mjs +560 -0
- package/lib/ir/front.mjs +834 -0
- package/lib/ir/index.mjs +10 -0
- package/lib/ir/refs.mjs +108 -0
- package/lib/ir/run.mjs +520 -0
- package/lib/ir/table.mjs +233 -0
- package/lib/js-compiler-errors.js +9 -4
- package/lib/kernels/collapse.mjs +114 -0
- package/lib/kernels/custom.mjs +70 -0
- package/lib/kernels/fill.mjs +6 -0
- package/lib/kernels/formats-small.mjs +40 -0
- package/lib/kernels/formats.mjs +29 -0
- package/lib/kernels/index.mjs +11 -0
- package/lib/kernels/kernels.mjs +285 -0
- package/lib/kernels/leaves.mjs +28 -0
- package/lib/kernels/order.mjs +25 -0
- package/lib/kernels/plan-rich.mjs +17 -0
- package/lib/kernels/plan.d.ts +9 -0
- package/lib/kernels/plan.mjs +128 -0
- package/lib/kernels/preprocess.mjs +223 -0
- package/lib/kernels/rank.mjs +97 -0
- package/lib/kernels/rt.mjs +249 -0
- package/lib/kernels/table.mjs +182 -0
- package/lib/kernels/unique.mjs +157 -0
- package/lib/native-positions.js +1 -1
- package/lib/openapi.d.ts +2 -0
- package/lib/openapi.js +128 -0
- package/lib/plan-compiler.js +84 -7
- package/lib/public-api.js +7 -2
- package/lib/rejections.js +3 -1
- package/lib/schema-helpers.js +12 -0
- package/lib/schema-helpers.mjs +20 -0
- package/lib/schema-order.js +4 -2
- package/lib/standard-schema.js +1 -1
- package/lib/validator-core.js +35 -30
- package/lib/version.js +1 -1
- package/lite.js +7 -2
- package/package.json +26 -8
package/lib/ir/emit.mjs
ADDED
|
@@ -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
|
+
}
|