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.
- 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 +148 -70
- package/lib/ir/emit-deps.mjs +131 -0
- package/lib/ir/emit.mjs +707 -0
- package/lib/ir/front.mjs +865 -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 +113 -14
- 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/shape-classifier.js +13 -1
- package/lib/standard-schema.js +1 -1
- package/lib/tier0.js +38 -8
- package/lib/validator-core.js +59 -36
- 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,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
|
+
}
|