corvus_json_schema 0.1.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.
- checksums.yaml +7 -0
- data/Cargo.lock +390 -0
- data/Cargo.toml +8 -0
- data/LICENSE +201 -0
- data/README.md +111 -0
- data/VERSIONHISTORY.md +7 -0
- data/ext/corvus_json_schema/Cargo.toml +18 -0
- data/ext/corvus_json_schema/extconf.rb +6 -0
- data/ext/corvus_json_schema/rustfmt.toml +2 -0
- data/ext/corvus_json_schema/src/lib.rs +643 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/Cargo.toml +35 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/LICENSE +201 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/README.md +139 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/compiler.rs +1118 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/dialect.rs +108 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/document.rs +905 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval/plan/fused.rs +1187 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval/plan.rs +1728 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval.rs +1451 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/formats.rs +1021 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/instance.rs +228 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/lib.rs +189 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/loader.rs +403 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/metaschemas.rs +29 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/node.rs +417 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/numbers.rs +244 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/options.rs +108 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/pattern.rs +1591 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/results.rs +316 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/uri.rs +274 -0
- data/lib/corvus_json_schema/version.rb +5 -0
- data/lib/corvus_json_schema.rb +65 -0
- metadata +94 -0
|
@@ -0,0 +1,1187 @@
|
|
|
1
|
+
//! The fused object plan: one pass over an object's properties for a schema whose object semantics are spread over
|
|
2
|
+
//! in-place applicators (`$ref`, `allOf`, `if`/`then`/`else`, dependencies, required-only `oneOf`/`anyOf`) and
|
|
3
|
+
//! possibly finished by `unevaluatedProperties`. A port of the C# evaluator's `FusedObjects`.
|
|
4
|
+
//!
|
|
5
|
+
//! The general path evaluates each applicator branch as a separate pass over the instance, each with its own
|
|
6
|
+
//! property lookups, and then walks the instance once more for `unevaluatedProperties`. The fused plan resolves every
|
|
7
|
+
//! property name known to any branch at compile time to the list of child schemas that apply to it (a branch's own
|
|
8
|
+
//! property schema, its matching pattern-property schemas, or its `additionalProperties` when neither matched), so
|
|
9
|
+
//! evaluation is one lookup per instance property, and it tracks which properties some branch covered so that the
|
|
10
|
+
//! unevaluated check needs no second analysis.
|
|
11
|
+
//!
|
|
12
|
+
//! Branches under `then`/`else` (or a dependency's schema) apply only when their condition holds; the plan supports
|
|
13
|
+
//! conditions the pass itself decides (required names, property values tested against constants or a pattern, and
|
|
14
|
+
//! names no property may match),
|
|
15
|
+
//! and defers those branches' applications to a second step over the properties they touch.
|
|
16
|
+
//!
|
|
17
|
+
//! Fail-fast evaluation only. The fused pass does not enter its contributors as nodes (and so would not push their
|
|
18
|
+
//! resources on the dynamic scope): below a live dynamic reference, only contributors in the node's own resource fuse.
|
|
19
|
+
|
|
20
|
+
use std::sync::Arc;
|
|
21
|
+
|
|
22
|
+
use serde_json::Value;
|
|
23
|
+
|
|
24
|
+
use super::{Child, Evaluator, LOOKUP_NAMES, NO_CHILD, Names, ObjectPlan, Program, Visit};
|
|
25
|
+
use crate::eval::{json_equal, matches_type};
|
|
26
|
+
use crate::instance::{Instance, ObjectView, View};
|
|
27
|
+
use crate::node::*;
|
|
28
|
+
use crate::numbers::Num;
|
|
29
|
+
use crate::pattern::Pattern;
|
|
30
|
+
|
|
31
|
+
const MAX_NAMES: usize = 256;
|
|
32
|
+
const MAX_CONDITIONS: usize = 64;
|
|
33
|
+
const MAX_CONTRIBUTORS: usize = 64;
|
|
34
|
+
const MAX_ALT_GROUPS: usize = 8;
|
|
35
|
+
|
|
36
|
+
pub(crate) struct FusedObject {
|
|
37
|
+
/// The branches merge into one strict object loop: every one applies unconditionally with declared properties and
|
|
38
|
+
/// required names only (and count bounds), and each name resolves to one schema.
|
|
39
|
+
flat: Option<Box<ObjectPlan>>,
|
|
40
|
+
/// Every property name any branch or condition knows, to its entry.
|
|
41
|
+
names: Names,
|
|
42
|
+
entries: Box<[Entry]>,
|
|
43
|
+
/// The branches, the node itself first, then the required lists of dependencies.
|
|
44
|
+
contributors: Box<[Contributor]>,
|
|
45
|
+
conditions: Box<[Condition]>,
|
|
46
|
+
/// Required-only `oneOf`/`anyOf` keywords, decided from the seen names after the pass.
|
|
47
|
+
alternatives: Box<[Alternative]>,
|
|
48
|
+
/// `oneOf`/`anyOf` groups whose branches carry object keywords (each branch is a contributor).
|
|
49
|
+
alt_groups: Box<[AltGroup]>,
|
|
50
|
+
/// `not: {required: [...]}`: names that must not all be present, under a condition.
|
|
51
|
+
forbidden: Box<[(Gate, Box<[u16]>)]>,
|
|
52
|
+
/// Conditions that fail when some property name matches a pattern (an `if` with `patternProperties: {P: false}`),
|
|
53
|
+
/// for names no entry knows; a known name that matches carries a test that never holds.
|
|
54
|
+
absent: Box<[(u16, Arc<Pattern>)]>,
|
|
55
|
+
/// Some branch has pattern properties or additional properties, or some condition absent patterns, so names no
|
|
56
|
+
/// entry knows need resolving.
|
|
57
|
+
resolves_unknown: bool,
|
|
58
|
+
has_count_bounds: bool,
|
|
59
|
+
unevaluated: Option<Child>,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
struct Entry {
|
|
63
|
+
apps: Box<[App]>,
|
|
64
|
+
/// The conditions' tests on this property's value.
|
|
65
|
+
tests: Box<[ValueTest]>,
|
|
66
|
+
/// The constant tests merged (the C# MergeValueTests): one lookup decides them all.
|
|
67
|
+
merged: Option<Merged>,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Each constant any of an entry's constant tests allows, with the mask of the tests (by index in `tests`) that allow
|
|
71
|
+
/// it. Strings are looked up by name; other constants compared in turn.
|
|
72
|
+
struct Merged {
|
|
73
|
+
strings: Names,
|
|
74
|
+
string_masks: Box<[u64]>,
|
|
75
|
+
others: Box<[(Value, u64)]>,
|
|
76
|
+
/// The tests that are constant sets; the others (patterns) are tested one by one.
|
|
77
|
+
keyed: u64,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
impl Merged {
|
|
81
|
+
/// The mask of the constant tests that allow the value.
|
|
82
|
+
#[inline]
|
|
83
|
+
fn allowed<'x, I: Instance<'x>>(&self, v: I) -> u64 {
|
|
84
|
+
match v.view() {
|
|
85
|
+
View::String(s) => self.strings.contains_at(s).map_or(0, |i| self.string_masks[i]),
|
|
86
|
+
_ => self.others.iter().find(|(m, _)| json_equal(m, v)).map_or(0, |&(_, mask)| mask),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// One child schema applying to a property on behalf of a contributor (`None`: `true`, which covers without a test).
|
|
92
|
+
struct App {
|
|
93
|
+
contributor: u16,
|
|
94
|
+
child: Option<Child>,
|
|
95
|
+
/// Other contributors whose resolution of the property is the same test: applied once when any is active.
|
|
96
|
+
others: Box<[u16]>,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
struct Contributor {
|
|
100
|
+
/// The condition and the polarity under which it applies (`None`: always).
|
|
101
|
+
condition: Option<(u16, bool)>,
|
|
102
|
+
/// The alternative group and the branch in it.
|
|
103
|
+
alt: Option<(u16, u16)>,
|
|
104
|
+
patterns: Box<[(Arc<Pattern>, Option<Child>)]>,
|
|
105
|
+
/// additionalProperties (`Some(None)`: `true`).
|
|
106
|
+
additional: Option<Option<Child>>,
|
|
107
|
+
required: Box<[u16]>,
|
|
108
|
+
min: Option<u64>,
|
|
109
|
+
max: Option<u64>,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// An `if` the pass decides (required names and value tests), or the presence of a dependency's property.
|
|
113
|
+
struct Condition {
|
|
114
|
+
required: Box<[u16]>,
|
|
115
|
+
/// The enclosing condition and the polarity under which this one is reached.
|
|
116
|
+
gate: Option<(u16, bool)>,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// A condition's test on a property's value: absent passes, present must hold.
|
|
120
|
+
struct ValueTest {
|
|
121
|
+
condition: u16,
|
|
122
|
+
kind: TestKind,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
enum TestKind {
|
|
126
|
+
/// One of these constants (strings, integers, booleans, null); none: the property must be absent.
|
|
127
|
+
Allowed(Box<[Value]>),
|
|
128
|
+
Pattern {
|
|
129
|
+
pattern: Arc<Pattern>,
|
|
130
|
+
requires_string: bool,
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
impl ValueTest {
|
|
135
|
+
fn holds<'x, I: Instance<'x>>(&self, v: I) -> bool {
|
|
136
|
+
match &self.kind {
|
|
137
|
+
TestKind::Allowed(values) => values.iter().any(|a| json_equal(a, v)),
|
|
138
|
+
TestKind::Pattern { pattern, requires_string } => match v.view() {
|
|
139
|
+
View::String(s) => pattern.is_match(s),
|
|
140
|
+
_ => !requires_string,
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
struct Alternative {
|
|
147
|
+
condition: Option<(u16, bool)>,
|
|
148
|
+
exactly_one: bool,
|
|
149
|
+
branches: Box<[Box<[u16]>]>,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
struct AltGroup {
|
|
153
|
+
exactly_one: bool,
|
|
154
|
+
count: u32,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
158
|
+
// Construction
|
|
159
|
+
|
|
160
|
+
/// A condition and the polarity under which something applies.
|
|
161
|
+
type Gate = Option<(u16, bool)>;
|
|
162
|
+
|
|
163
|
+
/// A contributor as collected: the branch, its condition, its alternative group and branch.
|
|
164
|
+
type Collected = (NodeId, Gate, Option<(u16, u16)>);
|
|
165
|
+
|
|
166
|
+
/// What a collection gathers.
|
|
167
|
+
struct Collect<'a> {
|
|
168
|
+
p: &'a Program,
|
|
169
|
+
contributors: Vec<Collected>,
|
|
170
|
+
conditions: Vec<Pending>,
|
|
171
|
+
/// Dependencies' required names: (condition, names).
|
|
172
|
+
extras: Vec<(u16, Vec<String>)>,
|
|
173
|
+
alternatives: Vec<(Gate, bool, Vec<Vec<String>>)>,
|
|
174
|
+
alt_groups: Vec<(bool, u32)>,
|
|
175
|
+
forbidden: Vec<(Gate, Vec<String>)>,
|
|
176
|
+
/// Alternative groups with object keywords only where coverage is not tracked (a failed branch must not cover).
|
|
177
|
+
allow_alt_groups: bool,
|
|
178
|
+
/// Every contributor must belong to this resource (below a live dynamic reference, where the general path would
|
|
179
|
+
/// push a contributor's own resource on the dynamic scope and the fused pass does not enter contributors).
|
|
180
|
+
resource: Option<u32>,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
struct Pending {
|
|
184
|
+
/// The `if` schema, or the dependency's property name.
|
|
185
|
+
test: Result<NodeId, String>,
|
|
186
|
+
gate: Option<(u16, bool)>,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
impl<'a> Collect<'a> {
|
|
190
|
+
fn node(&self, id: NodeId) -> &'a SchemaNode {
|
|
191
|
+
&self.p.nodes[id as usize]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn target(&self, id: NodeId) -> NodeId {
|
|
195
|
+
self.p.fast_target[id as usize]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// Walks the in-place applicators of a branch, adding every object branch with its condition. Fails when a branch
|
|
199
|
+
/// cannot be fused.
|
|
200
|
+
fn collect(&mut self, id: NodeId, condition: Option<(u16, bool)>) -> bool {
|
|
201
|
+
let n = self.node(id);
|
|
202
|
+
if n.always_true {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
if n.always_false || n.in_place_cycle || !is_object_branch(self.p, n, self.contributors.is_empty()) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
if self.contributors.len() >= MAX_CONTRIBUTORS || self.resource.is_some_and(|r| r != n.resource_id) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
self.contributors.push((id, condition, None));
|
|
212
|
+
if let Some(not) = n.not {
|
|
213
|
+
self.forbidden.push((condition, forbidden_names(self.p, not).unwrap().to_vec()));
|
|
214
|
+
}
|
|
215
|
+
for r in [n.ref_, n.static_dynamic_ref].into_iter().flatten() {
|
|
216
|
+
if !self.collect(self.target(r), condition) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for &c in n.all_of.iter().flatten() {
|
|
221
|
+
if !self.collect(self.target(c), condition) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if let Some(list) = &n.one_of {
|
|
226
|
+
let discriminated = n.one_of_discriminator.is_some() || self.type_union(list);
|
|
227
|
+
if !self.collect_alternative(list, condition, true, discriminated) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if let Some(list) = &n.any_of {
|
|
232
|
+
let discriminated = n.any_of_discriminator.is_some() || self.type_union(list);
|
|
233
|
+
if !self.collect_alternative(list, condition, false, discriminated) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
for d in n.dependencies.iter().flatten() {
|
|
238
|
+
if self.conditions.len() >= MAX_CONDITIONS {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
let at = self.conditions.len() as u16;
|
|
242
|
+
self.conditions.push(Pending { test: Err(d.name.clone()), gate: condition });
|
|
243
|
+
if let Some(required) = &d.required
|
|
244
|
+
&& !required.is_empty()
|
|
245
|
+
{
|
|
246
|
+
self.extras.push((at, required.clone()));
|
|
247
|
+
}
|
|
248
|
+
if let Some(schema) = d.schema
|
|
249
|
+
&& !self.collect(self.target(schema), Some((at, true)))
|
|
250
|
+
{
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if let Some(cond) = n.if_ {
|
|
255
|
+
let test_id = self.target(cond);
|
|
256
|
+
let test = self.node(test_id);
|
|
257
|
+
let (then, else_) = (n.then.map(|t| self.target(t)), n.else_.map(|e| self.target(e)));
|
|
258
|
+
if test.always_true {
|
|
259
|
+
return then.is_none_or(|t| self.collect(t, condition));
|
|
260
|
+
}
|
|
261
|
+
if test.always_false {
|
|
262
|
+
return else_.is_none_or(|e| self.collect(e, condition));
|
|
263
|
+
}
|
|
264
|
+
if !self.supported_condition(test) || self.conditions.len() >= MAX_CONDITIONS {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
let at = self.conditions.len() as u16;
|
|
268
|
+
self.conditions.push(Pending { test: Ok(test_id), gate: condition });
|
|
269
|
+
// When the condition holds, the if schema's own properties count as evaluated, so it contributes under
|
|
270
|
+
// the same condition as then.
|
|
271
|
+
if test.properties.is_some() && !self.collect(test_id, Some((at, true))) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
if let Some(t) = then
|
|
275
|
+
&& !self.collect(t, Some((at, true)))
|
|
276
|
+
{
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
if let Some(e) = else_
|
|
280
|
+
&& !self.collect(e, Some((at, false)))
|
|
281
|
+
{
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
true
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/// Every branch only tests the type (the general path decides the keyword by one mask test).
|
|
289
|
+
fn type_union(&self, list: &[NodeId]) -> bool {
|
|
290
|
+
list.iter().all(|&c| {
|
|
291
|
+
let b = self.node(self.target(c));
|
|
292
|
+
b.has_type && is_type_only(b)
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/// A `oneOf`/`anyOf` fuses when every branch is a plain `required` list (decided from the seen names after the
|
|
297
|
+
/// pass), or as an alternative group of object branches where coverage is not tracked.
|
|
298
|
+
fn collect_alternative(
|
|
299
|
+
&mut self,
|
|
300
|
+
list: &[NodeId],
|
|
301
|
+
condition: Option<(u16, bool)>,
|
|
302
|
+
exactly_one: bool,
|
|
303
|
+
discriminated: bool,
|
|
304
|
+
) -> bool {
|
|
305
|
+
let branches: Vec<NodeId> = list.iter().map(|&c| self.target(c)).collect();
|
|
306
|
+
let required_only: Option<Vec<Vec<String>>> = branches
|
|
307
|
+
.iter()
|
|
308
|
+
.map(|&b| {
|
|
309
|
+
let n = self.node(b);
|
|
310
|
+
n.required.clone().filter(|r| !r.is_empty() && is_required_list_only(n))
|
|
311
|
+
})
|
|
312
|
+
.collect();
|
|
313
|
+
if let Some(required) = required_only {
|
|
314
|
+
self.alternatives.push((condition, exactly_one, required));
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
// Branches with object keywords: each a contributor whose failure marks the branch rather than the object.
|
|
318
|
+
// The pass applies every branch that knows a name to that property, where the general path stops at the
|
|
319
|
+
// first branch that passes, so a name with an expensive child in more than one branch is refused; and a
|
|
320
|
+
// keyword the general path decides by discriminator or type stays with it.
|
|
321
|
+
if !self.allow_alt_groups
|
|
322
|
+
|| discriminated
|
|
323
|
+
|| condition.is_some()
|
|
324
|
+
|| self.alt_groups.len() >= MAX_ALT_GROUPS
|
|
325
|
+
|| branches.len() > 64
|
|
326
|
+
|| !self.expensive_children_disjoint(&branches)
|
|
327
|
+
{
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
let group = self.alt_groups.len() as u16;
|
|
331
|
+
for (i, &b) in branches.iter().enumerate() {
|
|
332
|
+
let n = self.node(b);
|
|
333
|
+
if n.always_true
|
|
334
|
+
|| n.always_false
|
|
335
|
+
|| n.in_place_cycle
|
|
336
|
+
|| n.has_in_place_applicators()
|
|
337
|
+
|| n.dependencies.is_some()
|
|
338
|
+
|| n.not.is_some()
|
|
339
|
+
|| !is_object_branch(self.p, n, false)
|
|
340
|
+
|| self.resource.is_some_and(|r| r != n.resource_id)
|
|
341
|
+
|| self.contributors.len() >= MAX_CONTRIBUTORS
|
|
342
|
+
{
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
self.contributors.push((b, None, Some((group, i as u16))));
|
|
346
|
+
}
|
|
347
|
+
self.alt_groups.push((exactly_one, branches.len() as u32));
|
|
348
|
+
true
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/// Whether no property name gets an expensive child (anything but a leaf or a boolean schema) from more than one
|
|
352
|
+
/// branch; pattern and additional properties count as every name.
|
|
353
|
+
fn expensive_children_disjoint(&self, branches: &[NodeId]) -> bool {
|
|
354
|
+
let cheap = |id: NodeId| {
|
|
355
|
+
let n = self.node(self.target(id));
|
|
356
|
+
n.always_true || n.always_false || is_leaf(n) || self.is_simple_array(n)
|
|
357
|
+
};
|
|
358
|
+
let mut expensive: Vec<&str> = Vec::new();
|
|
359
|
+
let mut wildcards = 0;
|
|
360
|
+
for &b in branches {
|
|
361
|
+
let n = self.node(b);
|
|
362
|
+
for (name, c) in n.properties.iter().flatten() {
|
|
363
|
+
if !cheap(*c) {
|
|
364
|
+
if expensive.contains(&name.as_str()) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
expensive.push(name);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
let wildcard = n.pattern_properties.iter().flatten().any(|pp| !cheap(pp.node))
|
|
371
|
+
|| n.additional_properties.is_some_and(|a| !cheap(a));
|
|
372
|
+
if wildcard {
|
|
373
|
+
wildcards += 1;
|
|
374
|
+
if wildcards > 1 {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
wildcards == 0 || expensive.is_empty()
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/// An array of leaf items with at most size bounds (C#'s IsSimpleArray): cheap to apply more than once.
|
|
383
|
+
fn is_simple_array(&self, n: &SchemaNode) -> bool {
|
|
384
|
+
n.has_array_keywords()
|
|
385
|
+
&& !n.has_object_keywords()
|
|
386
|
+
&& !n.has_in_place_applicators()
|
|
387
|
+
&& n.dynamic_ref.is_none()
|
|
388
|
+
&& n.const_value.is_none()
|
|
389
|
+
&& n.enum_values.is_none()
|
|
390
|
+
&& n.prefix_items.is_none()
|
|
391
|
+
&& n.contains.is_none()
|
|
392
|
+
&& !n.unique_items
|
|
393
|
+
&& n.unevaluated_items.is_none()
|
|
394
|
+
&& n.items.is_some_and(|i| {
|
|
395
|
+
let item = self.node(self.target(i));
|
|
396
|
+
item.always_true || is_leaf(item)
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/// A condition the pass decides alone: required names, and properties whose schemas are value tests, optionally
|
|
401
|
+
/// with `type: object` (which the object plan has established). At least one of the two.
|
|
402
|
+
fn supported_condition(&self, test: &SchemaNode) -> bool {
|
|
403
|
+
let mut any = test.required.as_ref().is_some_and(|r| !r.is_empty());
|
|
404
|
+
if test.const_value.is_some()
|
|
405
|
+
|| test.enum_values.is_some()
|
|
406
|
+
|| test.has_number_keywords()
|
|
407
|
+
|| test.has_string_keywords()
|
|
408
|
+
|| test.has_array_keywords()
|
|
409
|
+
|| test.has_in_place_applicators()
|
|
410
|
+
|| test.dynamic_ref.is_some()
|
|
411
|
+
|| (test.has_type && test.type_mask & type_mask::OBJECT == 0)
|
|
412
|
+
|| test.additional_properties.is_some()
|
|
413
|
+
|| test.property_names.is_some()
|
|
414
|
+
|| test.unevaluated_properties.is_some()
|
|
415
|
+
|| test.dependencies.is_some()
|
|
416
|
+
|| test.min_properties.is_some()
|
|
417
|
+
|| test.max_properties.is_some()
|
|
418
|
+
{
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
for (_, c) in test.properties.iter().flatten() {
|
|
422
|
+
if value_test(self.node(self.target(*c))).is_none() {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
any = true;
|
|
426
|
+
}
|
|
427
|
+
// `patternProperties: {P: false}`: no property name may match P.
|
|
428
|
+
for pp in test.pattern_properties.iter().flatten() {
|
|
429
|
+
if !self.node(self.target(pp.node)).always_false {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
any = true;
|
|
433
|
+
}
|
|
434
|
+
any
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/// A branch whose only effect on an object instance is through object keywords and fusable in-place applicators.
|
|
439
|
+
fn is_object_branch(p: &Program, n: &SchemaNode, allow_unevaluated: bool) -> bool {
|
|
440
|
+
!(n.const_value.is_some()
|
|
441
|
+
|| n.enum_values.is_some()
|
|
442
|
+
|| n.has_number_keywords()
|
|
443
|
+
|| n.has_string_keywords()
|
|
444
|
+
|| (n.has_type && n.type_mask & type_mask::OBJECT == 0)
|
|
445
|
+
|| n.not.is_some_and(|t| forbidden_names(p, t).is_none())
|
|
446
|
+
|| n.property_names.is_some()
|
|
447
|
+
|| n.dynamic_ref.is_some()
|
|
448
|
+
|| (!allow_unevaluated && n.unevaluated_properties.is_some()))
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
fn is_type_only(n: &SchemaNode) -> bool {
|
|
452
|
+
is_leaf(n)
|
|
453
|
+
&& n.const_value.is_none()
|
|
454
|
+
&& n.enum_values.is_none()
|
|
455
|
+
&& !n.has_number_keywords()
|
|
456
|
+
&& !n.has_string_keywords()
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/// Only local keywords (type, const, enum, number and string keywords).
|
|
460
|
+
fn is_leaf(n: &SchemaNode) -> bool {
|
|
461
|
+
!n.always_true
|
|
462
|
+
&& !n.always_false
|
|
463
|
+
&& !n.has_object_keywords()
|
|
464
|
+
&& !n.has_array_keywords()
|
|
465
|
+
&& !n.has_in_place_applicators()
|
|
466
|
+
&& n.dynamic_ref.is_none()
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/// The names of a `not` whose schema is a non-empty `required` list: an object fails when all are present.
|
|
470
|
+
fn forbidden_names(p: &Program, not: NodeId) -> Option<&[String]> {
|
|
471
|
+
let n = &p.nodes[p.fast_target[not as usize] as usize];
|
|
472
|
+
if n.always_true || n.always_false || !is_required_list_only(n) {
|
|
473
|
+
return None;
|
|
474
|
+
}
|
|
475
|
+
n.required.as_deref().filter(|r| !r.is_empty())
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
fn is_required_list_only(n: &SchemaNode) -> bool {
|
|
479
|
+
!(n.const_value.is_some()
|
|
480
|
+
|| n.enum_values.is_some()
|
|
481
|
+
|| n.has_number_keywords()
|
|
482
|
+
|| n.has_string_keywords()
|
|
483
|
+
|| n.has_array_keywords()
|
|
484
|
+
|| n.has_in_place_applicators()
|
|
485
|
+
|| n.dynamic_ref.is_some()
|
|
486
|
+
|| n.unevaluated_properties.is_some()
|
|
487
|
+
|| n.unevaluated_items.is_some()
|
|
488
|
+
|| (n.has_type && n.type_mask & type_mask::OBJECT == 0)
|
|
489
|
+
|| n.pattern_properties.is_some()
|
|
490
|
+
|| n.additional_properties.is_some()
|
|
491
|
+
|| n.property_names.is_some()
|
|
492
|
+
|| n.dependencies.is_some()
|
|
493
|
+
|| n.min_properties.is_some()
|
|
494
|
+
|| n.max_properties.is_some()
|
|
495
|
+
|| n.properties.as_ref().is_some_and(|p| !p.is_empty()))
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/// The value test for a property schema inside an `if`: `const`/`enum` of scalars (the values the schema's `type`
|
|
499
|
+
/// admits), or a `pattern` with at most `type: string`; `None` for anything else.
|
|
500
|
+
fn value_test(n: &SchemaNode) -> Option<TestKind> {
|
|
501
|
+
if n.always_true
|
|
502
|
+
|| n.always_false
|
|
503
|
+
|| n.has_number_keywords()
|
|
504
|
+
|| n.has_object_keywords()
|
|
505
|
+
|| n.has_array_keywords()
|
|
506
|
+
|| n.has_in_place_applicators()
|
|
507
|
+
|| n.dynamic_ref.is_some()
|
|
508
|
+
{
|
|
509
|
+
return None;
|
|
510
|
+
}
|
|
511
|
+
if let Some(pattern) = &n.pattern {
|
|
512
|
+
let only_pattern = n.const_value.is_none()
|
|
513
|
+
&& n.enum_values.is_none()
|
|
514
|
+
&& n.min_length.is_none()
|
|
515
|
+
&& n.max_length.is_none()
|
|
516
|
+
&& !(n.assert_format && n.format.is_some())
|
|
517
|
+
&& !n.assert_content
|
|
518
|
+
&& (!n.has_type || n.type_mask == type_mask::STRING);
|
|
519
|
+
return only_pattern.then(|| TestKind::Pattern { pattern: pattern.clone(), requires_string: n.has_type });
|
|
520
|
+
}
|
|
521
|
+
if n.has_string_keywords() {
|
|
522
|
+
return None;
|
|
523
|
+
}
|
|
524
|
+
let values: Vec<Value> = match (&n.const_value, &n.enum_values) {
|
|
525
|
+
(Some(c), _) => vec![c.clone()],
|
|
526
|
+
(None, Some(e)) if !e.is_empty() => e.clone(),
|
|
527
|
+
_ => return None,
|
|
528
|
+
};
|
|
529
|
+
let scalar = |v: &Value| match v {
|
|
530
|
+
Value::Number(x) => matches!(Num::of(x), Num::I(_)),
|
|
531
|
+
Value::String(_) | Value::Bool(_) | Value::Null => true,
|
|
532
|
+
_ => false,
|
|
533
|
+
};
|
|
534
|
+
if !values.iter().all(scalar) {
|
|
535
|
+
return None;
|
|
536
|
+
}
|
|
537
|
+
// A value the schema's type rejects never passes, so it is not allowed.
|
|
538
|
+
let allowed = values.into_iter().filter(|v| !n.has_type || matches_type(n.type_mask, v)).collect();
|
|
539
|
+
Some(TestKind::Allowed(allowed))
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/// Builds the fused plan for a node, or `None` when it cannot be fused or fusing does not pay.
|
|
543
|
+
pub(super) fn try_fuse(
|
|
544
|
+
p: &Program,
|
|
545
|
+
id: NodeId,
|
|
546
|
+
same_resource: bool,
|
|
547
|
+
child: &dyn Fn(NodeId) -> Child,
|
|
548
|
+
) -> Option<FusedObject> {
|
|
549
|
+
let n = &p.nodes[id as usize];
|
|
550
|
+
if !is_object_branch(p, n, true) || n.in_place_cycle || n.always_true || n.always_false {
|
|
551
|
+
return None;
|
|
552
|
+
}
|
|
553
|
+
let mut ctx = Collect {
|
|
554
|
+
p,
|
|
555
|
+
contributors: Vec::new(),
|
|
556
|
+
conditions: Vec::new(),
|
|
557
|
+
extras: Vec::new(),
|
|
558
|
+
alternatives: Vec::new(),
|
|
559
|
+
alt_groups: Vec::new(),
|
|
560
|
+
forbidden: Vec::new(),
|
|
561
|
+
allow_alt_groups: n.unevaluated_properties.is_none(),
|
|
562
|
+
resource: same_resource.then_some(n.resource_id),
|
|
563
|
+
};
|
|
564
|
+
if !ctx.collect(id, None) {
|
|
565
|
+
return None;
|
|
566
|
+
}
|
|
567
|
+
if ctx.contributors.len() + ctx.extras.len() > MAX_CONTRIBUTORS || ctx.conditions.len() > MAX_CONDITIONS {
|
|
568
|
+
return None;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Fusing pays when a pass over every property is unavoidable (unevaluatedProperties) or when it replaces several
|
|
572
|
+
// passes: two or more branches with object keywords, an if the seen names decide, or alternatives. A node whose
|
|
573
|
+
// object keywords are all its own keeps its object plan.
|
|
574
|
+
let has_if = ctx.conditions.iter().any(|c| c.test.is_ok());
|
|
575
|
+
if n.unevaluated_properties.is_none() && !has_if && ctx.alternatives.is_empty() && ctx.alt_groups.is_empty() {
|
|
576
|
+
let effective = ctx
|
|
577
|
+
.contributors
|
|
578
|
+
.iter()
|
|
579
|
+
.filter(|&&(b, ..)| {
|
|
580
|
+
let b = &p.nodes[b as usize];
|
|
581
|
+
b.properties.is_some()
|
|
582
|
+
|| b.pattern_properties.is_some()
|
|
583
|
+
|| b.additional_properties.is_some()
|
|
584
|
+
|| b.required.as_ref().is_some_and(|r| !r.is_empty())
|
|
585
|
+
|| b.min_properties.is_some()
|
|
586
|
+
|| b.max_properties.is_some()
|
|
587
|
+
})
|
|
588
|
+
.count();
|
|
589
|
+
if effective < 2 {
|
|
590
|
+
return None;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Every name any branch or condition knows gets an index.
|
|
595
|
+
let mut names: Vec<String> = Vec::new();
|
|
596
|
+
let mut bit = |name: &str| -> u16 {
|
|
597
|
+
match names.iter().position(|n| n == name) {
|
|
598
|
+
Some(i) => i as u16,
|
|
599
|
+
None => {
|
|
600
|
+
names.push(name.to_string());
|
|
601
|
+
(names.len() - 1) as u16
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
for &(b, ..) in &ctx.contributors {
|
|
606
|
+
let b = &p.nodes[b as usize];
|
|
607
|
+
for (name, _) in b.properties.iter().flatten() {
|
|
608
|
+
bit(name);
|
|
609
|
+
}
|
|
610
|
+
for name in b.required.iter().flatten() {
|
|
611
|
+
bit(name);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
let mut conditions = Vec::new();
|
|
615
|
+
let mut tests_by_entry: Vec<(u16, ValueTest)> = Vec::new();
|
|
616
|
+
let mut absent: Vec<(u16, Arc<Pattern>)> = Vec::new();
|
|
617
|
+
for (i, pending) in ctx.conditions.iter().enumerate() {
|
|
618
|
+
let required: Vec<u16> = match &pending.test {
|
|
619
|
+
Ok(test) => {
|
|
620
|
+
let test = &p.nodes[*test as usize];
|
|
621
|
+
absent.extend(test.pattern_properties.iter().flatten().map(|pp| (i as u16, pp.pattern.clone())));
|
|
622
|
+
for (name, c) in test.properties.iter().flatten() {
|
|
623
|
+
let kind = value_test(&p.nodes[p.fast_target[*c as usize] as usize]).unwrap();
|
|
624
|
+
tests_by_entry.push((bit(name), ValueTest { condition: i as u16, kind }));
|
|
625
|
+
}
|
|
626
|
+
test.required.iter().flatten().map(|r| bit(r)).collect()
|
|
627
|
+
}
|
|
628
|
+
Err(dependency) => vec![bit(dependency)],
|
|
629
|
+
};
|
|
630
|
+
conditions.push(Condition { required: required.into_boxed_slice(), gate: pending.gate });
|
|
631
|
+
}
|
|
632
|
+
let forbidden: Vec<(Gate, Box<[u16]>)> =
|
|
633
|
+
ctx.forbidden.iter().map(|(gate, names)| (*gate, names.iter().map(|n| bit(n)).collect())).collect();
|
|
634
|
+
let extras: Vec<(u16, Vec<u16>)> =
|
|
635
|
+
ctx.extras.iter().map(|(c, names)| (*c, names.iter().map(|n| bit(n)).collect())).collect();
|
|
636
|
+
let alternatives: Vec<Alternative> = ctx
|
|
637
|
+
.alternatives
|
|
638
|
+
.iter()
|
|
639
|
+
.map(|(condition, exactly_one, branches)| Alternative {
|
|
640
|
+
condition: *condition,
|
|
641
|
+
exactly_one: *exactly_one,
|
|
642
|
+
branches: branches.iter().map(|b| b.iter().map(|n| bit(n)).collect()).collect(),
|
|
643
|
+
})
|
|
644
|
+
.collect();
|
|
645
|
+
if names.len() > MAX_NAMES {
|
|
646
|
+
return None;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
let app_child = |c: NodeId| -> Option<Child> {
|
|
650
|
+
let t = p.fast_target[c as usize];
|
|
651
|
+
if p.nodes[t as usize].always_true { None } else { Some(child(t)) }
|
|
652
|
+
};
|
|
653
|
+
let mut contributors: Vec<Contributor> = ctx
|
|
654
|
+
.contributors
|
|
655
|
+
.iter()
|
|
656
|
+
.map(|&(b, condition, alt)| {
|
|
657
|
+
let b = &p.nodes[b as usize];
|
|
658
|
+
Contributor {
|
|
659
|
+
condition,
|
|
660
|
+
alt,
|
|
661
|
+
patterns: b
|
|
662
|
+
.pattern_properties
|
|
663
|
+
.iter()
|
|
664
|
+
.flatten()
|
|
665
|
+
.map(|pp| (pp.pattern.clone(), app_child(pp.node)))
|
|
666
|
+
.collect(),
|
|
667
|
+
additional: b.additional_properties.map(app_child),
|
|
668
|
+
required: b
|
|
669
|
+
.required
|
|
670
|
+
.iter()
|
|
671
|
+
.flatten()
|
|
672
|
+
.map(|r| names.iter().position(|n| n == r).unwrap() as u16)
|
|
673
|
+
.collect(),
|
|
674
|
+
min: b.min_properties,
|
|
675
|
+
max: b.max_properties,
|
|
676
|
+
}
|
|
677
|
+
})
|
|
678
|
+
.collect();
|
|
679
|
+
let branch_count = contributors.len();
|
|
680
|
+
for (condition, required) in extras {
|
|
681
|
+
contributors.push(Contributor {
|
|
682
|
+
condition: Some((condition, true)),
|
|
683
|
+
alt: None,
|
|
684
|
+
patterns: Box::new([]),
|
|
685
|
+
additional: None,
|
|
686
|
+
required: required.into_boxed_slice(),
|
|
687
|
+
min: None,
|
|
688
|
+
max: None,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
let mut tests: Vec<Vec<ValueTest>> = (0..names.len()).map(|_| Vec::new()).collect();
|
|
693
|
+
for (e, test) in tests_by_entry {
|
|
694
|
+
tests[e as usize].push(test);
|
|
695
|
+
}
|
|
696
|
+
// A known name matching an absent pattern fails its condition whatever its value: no constant is allowed.
|
|
697
|
+
for (e, name) in names.iter().enumerate() {
|
|
698
|
+
for (condition, pattern) in &absent {
|
|
699
|
+
if pattern.is_match(name) {
|
|
700
|
+
tests[e].push(ValueTest { condition: *condition, kind: TestKind::Allowed(Box::new([])) });
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Resolve every known name against every branch now.
|
|
706
|
+
let mut entries = Vec::with_capacity(names.len());
|
|
707
|
+
for (i, name) in names.iter().enumerate() {
|
|
708
|
+
let mut apps: Vec<(u16, Option<Child>, Option<NodeId>)> = Vec::new();
|
|
709
|
+
for (c, &(b, ..)) in ctx.contributors.iter().enumerate() {
|
|
710
|
+
let b = &p.nodes[b as usize];
|
|
711
|
+
let mut matched = false;
|
|
712
|
+
if let Some(&(_, schema)) = b.properties.iter().flatten().find(|(k, _)| k == name) {
|
|
713
|
+
matched = true;
|
|
714
|
+
apps.push((c as u16, app_child(schema), Some(p.fast_target[schema as usize])));
|
|
715
|
+
}
|
|
716
|
+
for pp in b.pattern_properties.iter().flatten() {
|
|
717
|
+
if pp.pattern.is_match(name) {
|
|
718
|
+
matched = true;
|
|
719
|
+
apps.push((c as u16, app_child(pp.node), Some(p.fast_target[pp.node as usize])));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if !matched && let Some(a) = b.additional_properties {
|
|
723
|
+
apps.push((c as u16, app_child(a), Some(p.fast_target[a as usize])));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
let apps = coalesce(apps, &contributors);
|
|
727
|
+
let entry_tests = std::mem::take(&mut tests[i]);
|
|
728
|
+
let merged = merge_value_tests(&entry_tests);
|
|
729
|
+
entries.push(Entry { apps: apps.into_boxed_slice(), tests: entry_tests.into_boxed_slice(), merged });
|
|
730
|
+
}
|
|
731
|
+
debug_assert_eq!(branch_count + ctx.extras.len(), contributors.len());
|
|
732
|
+
|
|
733
|
+
let flat = (conditions.is_empty()
|
|
734
|
+
&& alternatives.is_empty()
|
|
735
|
+
&& ctx.alt_groups.is_empty()
|
|
736
|
+
&& forbidden.is_empty()
|
|
737
|
+
&& n.unevaluated_properties.is_none()
|
|
738
|
+
&& names.len() <= 64
|
|
739
|
+
&& contributors.iter().all(|c| c.condition.is_none() && c.patterns.is_empty() && c.additional.is_none())
|
|
740
|
+
&& entries.iter().all(|e| e.apps.len() <= 1 && e.tests.is_empty()))
|
|
741
|
+
.then(|| {
|
|
742
|
+
let names: Names = Names::new(names.iter().map(|n| n.as_str().into()).collect());
|
|
743
|
+
let required_mask = contributors.iter().flat_map(|c| c.required.iter()).fold(0u64, |m, &i| m | 1 << i);
|
|
744
|
+
Box::new(ObjectPlan {
|
|
745
|
+
min: contributors.iter().filter_map(|c| c.min).max().unwrap_or(0),
|
|
746
|
+
max: contributors.iter().filter_map(|c| c.max).min().unwrap_or(u64::MAX),
|
|
747
|
+
visit: Visit::Names,
|
|
748
|
+
declared: entries.len(),
|
|
749
|
+
children: entries.iter().map(|e| e.apps.first().and_then(|a| a.child).unwrap_or(NO_CHILD)).collect(),
|
|
750
|
+
required_mask,
|
|
751
|
+
required: Box::new([]),
|
|
752
|
+
patterns: Box::new([]),
|
|
753
|
+
name_patterns: entries.iter().map(|_| Box::default()).collect(),
|
|
754
|
+
additional: None,
|
|
755
|
+
property_names: None,
|
|
756
|
+
dependencies: Box::new([]),
|
|
757
|
+
rest_free: true,
|
|
758
|
+
strict: true,
|
|
759
|
+
// No contributor has additionalProperties (`flat` requires it).
|
|
760
|
+
lookup: names.map.names.len() <= LOOKUP_NAMES,
|
|
761
|
+
names,
|
|
762
|
+
})
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
Some(FusedObject {
|
|
766
|
+
flat,
|
|
767
|
+
names: Names::new(names.into_iter().map(Into::into).collect()),
|
|
768
|
+
entries: entries.into_boxed_slice(),
|
|
769
|
+
resolves_unknown: !absent.is_empty()
|
|
770
|
+
|| contributors.iter().any(|c| !c.patterns.is_empty() || c.additional.is_some()),
|
|
771
|
+
has_count_bounds: contributors.iter().any(|c| c.min.is_some() || c.max.is_some()),
|
|
772
|
+
contributors: contributors.into_boxed_slice(),
|
|
773
|
+
conditions: conditions.into_boxed_slice(),
|
|
774
|
+
alternatives: alternatives.into_boxed_slice(),
|
|
775
|
+
alt_groups: ctx.alt_groups.iter().map(|&(exactly_one, count)| AltGroup { exactly_one, count }).collect(),
|
|
776
|
+
forbidden: forbidden.into_boxed_slice(),
|
|
777
|
+
absent: absent.into_boxed_slice(),
|
|
778
|
+
unevaluated: n.unevaluated_properties.map(|u| child(p.fast_target[u as usize])),
|
|
779
|
+
})
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/// The merged constants of an entry's value tests, when some (of at most 64) are constant sets and there is more than
|
|
783
|
+
/// one constant to look for.
|
|
784
|
+
fn merge_value_tests(tests: &[ValueTest]) -> Option<Merged> {
|
|
785
|
+
let constants: usize =
|
|
786
|
+
tests.iter().map(|t| if let TestKind::Allowed(values) = &t.kind { values.len() } else { 0 }).sum();
|
|
787
|
+
if tests.len() > 64 || constants < 2 {
|
|
788
|
+
return None;
|
|
789
|
+
}
|
|
790
|
+
let (mut strings, mut string_masks): (Vec<Box<str>>, Vec<u64>) = (Vec::new(), Vec::new());
|
|
791
|
+
let mut others: Vec<(Value, u64)> = Vec::new();
|
|
792
|
+
let mut keyed = 0;
|
|
793
|
+
for (t, test) in tests.iter().enumerate() {
|
|
794
|
+
let TestKind::Allowed(values) = &test.kind else { continue };
|
|
795
|
+
keyed |= 1 << t;
|
|
796
|
+
for v in values.iter() {
|
|
797
|
+
if let Value::String(s) = v {
|
|
798
|
+
match strings.iter().position(|m| **m == **s) {
|
|
799
|
+
Some(i) => string_masks[i] |= 1 << t,
|
|
800
|
+
None => {
|
|
801
|
+
strings.push(s.as_str().into());
|
|
802
|
+
string_masks.push(1 << t);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
} else {
|
|
806
|
+
match others.iter_mut().find(|(m, _)| json_equal(m, v)) {
|
|
807
|
+
Some((_, mask)) => *mask |= 1 << t,
|
|
808
|
+
None => others.push((v.clone(), 1 << t)),
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
Some(Merged { strings: Names::new(strings), string_masks: string_masks.into(), others: others.into(), keyed })
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/// Identical resolutions of a property from several branches (the same child, or both `true`) become one application
|
|
817
|
+
/// listing every branch, applied once when any of them is active. Branches of an alternative group merge only within
|
|
818
|
+
/// the same branch. The primary contributor is an unconditional one when there is one, so the pass applies it at
|
|
819
|
+
/// once.
|
|
820
|
+
fn coalesce(apps: Vec<(u16, Option<Child>, Option<NodeId>)>, contributors: &[Contributor]) -> Vec<App> {
|
|
821
|
+
let same = |a: &(u16, Option<Child>, Option<NodeId>), b: &(u16, Option<Child>, Option<NodeId>)| match (&a.1, &b.1) {
|
|
822
|
+
(None, None) => true,
|
|
823
|
+
(Some(x), Some(y)) => a.2 == b.2 || (x.trivial() && y.trivial() && x.types == y.types),
|
|
824
|
+
_ => false,
|
|
825
|
+
};
|
|
826
|
+
let mut used = vec![false; apps.len()];
|
|
827
|
+
let mut out = Vec::new();
|
|
828
|
+
for i in 0..apps.len() {
|
|
829
|
+
if used[i] {
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
let mut primary = i;
|
|
833
|
+
let mut others = Vec::new();
|
|
834
|
+
for j in i + 1..apps.len() {
|
|
835
|
+
let (a, b) = (&contributors[apps[primary].0 as usize], &contributors[apps[j].0 as usize]);
|
|
836
|
+
if used[j] || !same(&apps[primary], &apps[j]) || a.alt != b.alt {
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
used[j] = true;
|
|
840
|
+
if a.condition.is_some() && b.condition.is_none() {
|
|
841
|
+
others.push(apps[primary].0);
|
|
842
|
+
primary = j;
|
|
843
|
+
} else {
|
|
844
|
+
others.push(apps[j].0);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
out.push(App { contributor: apps[primary].0, child: apps[primary].1, others: others.into_boxed_slice() });
|
|
848
|
+
}
|
|
849
|
+
out
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
853
|
+
// Evaluation
|
|
854
|
+
|
|
855
|
+
/// Set bits over at most `MAX_NAMES` entries.
|
|
856
|
+
#[derive(Default)]
|
|
857
|
+
struct Seen([u64; MAX_NAMES / 64]);
|
|
858
|
+
|
|
859
|
+
impl Seen {
|
|
860
|
+
#[inline]
|
|
861
|
+
fn set(&mut self, i: u16) {
|
|
862
|
+
self.0[i as usize >> 6] |= 1 << (i & 63);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
#[inline]
|
|
866
|
+
fn all(&self, bits: &[u16]) -> bool {
|
|
867
|
+
bits.iter().all(|&i| self.0[i as usize >> 6] & (1 << (i & 63)) != 0)
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/// Covered property ordinals (for unevaluatedProperties), inline up to 256 properties.
|
|
872
|
+
enum Covered {
|
|
873
|
+
None,
|
|
874
|
+
Inline([u64; 4]),
|
|
875
|
+
Heap(Vec<u64>),
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
impl Covered {
|
|
879
|
+
fn new(track: bool, len: usize) -> Covered {
|
|
880
|
+
match (track, len) {
|
|
881
|
+
(false, _) => Covered::None,
|
|
882
|
+
(true, 0..=256) => Covered::Inline([0; 4]),
|
|
883
|
+
(true, _) => Covered::Heap(vec![0; len.div_ceil(64)]),
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
#[inline]
|
|
888
|
+
fn set(&mut self, i: usize) {
|
|
889
|
+
match self {
|
|
890
|
+
Covered::None => {}
|
|
891
|
+
Covered::Inline(w) => w[i >> 6] |= 1 << (i & 63),
|
|
892
|
+
Covered::Heap(w) => w[i >> 6] |= 1 << (i & 63),
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
fn get(&self, i: usize) -> bool {
|
|
897
|
+
match self {
|
|
898
|
+
Covered::None => true,
|
|
899
|
+
Covered::Inline(w) => w[i >> 6] & (1 << (i & 63)) != 0,
|
|
900
|
+
Covered::Heap(w) => w[i >> 6] & (1 << (i & 63)) != 0,
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/// The per-evaluation state of the pass.
|
|
906
|
+
struct Pass {
|
|
907
|
+
seen: Seen,
|
|
908
|
+
/// Bit `i`: condition `i`'s value test failed.
|
|
909
|
+
failed: u64,
|
|
910
|
+
alt_failed: [u64; MAX_ALT_GROUPS],
|
|
911
|
+
holds: u64,
|
|
912
|
+
gate_ok: u64,
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
impl Pass {
|
|
916
|
+
#[inline]
|
|
917
|
+
fn active(&self, condition: Option<(u16, bool)>) -> bool {
|
|
918
|
+
match condition {
|
|
919
|
+
None => true,
|
|
920
|
+
Some((c, polarity)) => self.gate_ok & (1 << c) != 0 && (self.holds & (1 << c) != 0) == polarity,
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
enum Outcome {
|
|
926
|
+
Failed,
|
|
927
|
+
/// Whether some branch covered the property, and whether conditional applications are pending.
|
|
928
|
+
Done {
|
|
929
|
+
cover: bool,
|
|
930
|
+
defer: bool,
|
|
931
|
+
},
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
impl Evaluator<'_, '_> {
|
|
935
|
+
#[inline]
|
|
936
|
+
fn apply<'x, I: Instance<'x>>(&mut self, child: Option<Child>, v: I) -> bool {
|
|
937
|
+
child.is_none_or(|c| self.run_child(c, v))
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/// Resolves a name no entry knows against one branch's pattern and additional properties; returns whether it
|
|
941
|
+
/// matched (the property is covered), or `None` when the application failed.
|
|
942
|
+
fn resolve_unknown<'x, I: Instance<'x>>(&mut self, c: &Contributor, name: &str, v: I) -> Option<bool> {
|
|
943
|
+
let mut matched = false;
|
|
944
|
+
for (pattern, child) in c.patterns.iter() {
|
|
945
|
+
if pattern.is_match(name) {
|
|
946
|
+
matched = true;
|
|
947
|
+
if !self.apply(*child, v) {
|
|
948
|
+
return None;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if !matched && let Some(child) = c.additional {
|
|
953
|
+
matched = true;
|
|
954
|
+
if !self.apply(child, v) {
|
|
955
|
+
return None;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
Some(matched)
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
fn fused_entry<'x, I: Instance<'x>>(&mut self, f: &FusedObject, e: usize, v: I, pass: &mut Pass) -> Outcome {
|
|
962
|
+
let entry = &f.entries[e];
|
|
963
|
+
pass.seen.set(e as u16);
|
|
964
|
+
match &entry.merged {
|
|
965
|
+
Some(merged) => {
|
|
966
|
+
let allowed = merged.allowed(v);
|
|
967
|
+
for (t, test) in entry.tests.iter().enumerate() {
|
|
968
|
+
let holds = if merged.keyed & (1 << t) != 0 { allowed & (1 << t) != 0 } else { test.holds(v) };
|
|
969
|
+
if !holds {
|
|
970
|
+
pass.failed |= 1 << test.condition;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
None => {
|
|
975
|
+
for test in entry.tests.iter() {
|
|
976
|
+
if !test.holds(v) {
|
|
977
|
+
pass.failed |= 1 << test.condition;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
let (mut cover, mut defer) = (false, false);
|
|
983
|
+
for app in entry.apps.iter() {
|
|
984
|
+
let c = &f.contributors[app.contributor as usize];
|
|
985
|
+
if c.condition.is_some() {
|
|
986
|
+
defer = true;
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
if !self.apply(app.child, v) {
|
|
990
|
+
match c.alt {
|
|
991
|
+
None => return Outcome::Failed,
|
|
992
|
+
Some((group, branch)) => pass.alt_failed[group as usize] |= 1 << branch,
|
|
993
|
+
}
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
cover = true;
|
|
997
|
+
}
|
|
998
|
+
Outcome::Done { cover, defer }
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
fn fused_unknown<'x, I: Instance<'x>>(&mut self, f: &FusedObject, name: &str, v: I, pass: &mut Pass) -> Outcome {
|
|
1002
|
+
let (mut cover, mut defer) = (false, false);
|
|
1003
|
+
if !f.resolves_unknown {
|
|
1004
|
+
return Outcome::Done { cover, defer };
|
|
1005
|
+
}
|
|
1006
|
+
for (c, pattern) in f.absent.iter() {
|
|
1007
|
+
if pass.failed & (1 << c) == 0 && pattern.is_match(name) {
|
|
1008
|
+
pass.failed |= 1 << c;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
for c in f.contributors.iter() {
|
|
1012
|
+
if c.condition.is_some() {
|
|
1013
|
+
defer |= !c.patterns.is_empty() || c.additional.is_some();
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
match self.resolve_unknown(c, name, v) {
|
|
1017
|
+
Some(matched) => cover |= matched,
|
|
1018
|
+
None => match c.alt {
|
|
1019
|
+
None => return Outcome::Failed,
|
|
1020
|
+
Some((group, branch)) => pass.alt_failed[group as usize] |= 1 << branch,
|
|
1021
|
+
},
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
Outcome::Done { cover, defer }
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
pub(super) fn run_fused<'x, I: Instance<'x>>(&mut self, f: &FusedObject, o: I::Object) -> bool {
|
|
1028
|
+
if let Some(plan) = &f.flat {
|
|
1029
|
+
return self.run_strict_object::<I>(plan, o);
|
|
1030
|
+
}
|
|
1031
|
+
let count = o.len() as u64;
|
|
1032
|
+
if f.has_count_bounds {
|
|
1033
|
+
for c in f.contributors.iter() {
|
|
1034
|
+
if c.condition.is_none() && c.alt.is_none() && !count_ok(c, count) {
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
let mut pass = Pass { seen: Seen::default(), failed: 0, alt_failed: [0; MAX_ALT_GROUPS], holds: 0, gate_ok: 0 };
|
|
1040
|
+
let mut covered = Covered::new(f.unevaluated.is_some(), o.len());
|
|
1041
|
+
// The ordinals of the properties with conditional applications pending: a mask for the first 64, a vector
|
|
1042
|
+
// (allocated only for objects that large) for the rest. A vector of the properties themselves cost an
|
|
1043
|
+
// allocation and a free per evaluation.
|
|
1044
|
+
let mut deferred = 0u64;
|
|
1045
|
+
let mut deferred_beyond: Vec<usize> = Vec::new();
|
|
1046
|
+
let mut hint = 0;
|
|
1047
|
+
for (ordinal, (k, v)) in o.iter().enumerate() {
|
|
1048
|
+
let e = f.names.find_from(k, &mut hint);
|
|
1049
|
+
let outcome = match e {
|
|
1050
|
+
Some(e) => self.fused_entry(f, e, v, &mut pass),
|
|
1051
|
+
None => self.fused_unknown(f, k, v, &mut pass),
|
|
1052
|
+
};
|
|
1053
|
+
match outcome {
|
|
1054
|
+
Outcome::Failed => return false,
|
|
1055
|
+
Outcome::Done { cover, defer } => {
|
|
1056
|
+
if cover {
|
|
1057
|
+
covered.set(ordinal);
|
|
1058
|
+
}
|
|
1059
|
+
if defer {
|
|
1060
|
+
if ordinal < 64 {
|
|
1061
|
+
deferred |= 1 << ordinal;
|
|
1062
|
+
} else {
|
|
1063
|
+
deferred_beyond.push(ordinal);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Decide the conditions, then which apply along their gates (a gate precedes the conditions under it).
|
|
1071
|
+
for (i, c) in f.conditions.iter().enumerate() {
|
|
1072
|
+
if pass.failed & (1 << i) == 0 && pass.seen.all(&c.required) {
|
|
1073
|
+
pass.holds |= 1 << i;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
for (i, c) in f.conditions.iter().enumerate() {
|
|
1077
|
+
if pass.active(c.gate) {
|
|
1078
|
+
pass.gate_ok |= 1 << i;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
let mut pending = deferred.count_ones() as usize + deferred_beyond.len();
|
|
1083
|
+
for (ordinal, (name, v)) in o.iter().enumerate() {
|
|
1084
|
+
if pending == 0 {
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
let is_deferred = if ordinal < 64 {
|
|
1088
|
+
deferred & (1 << ordinal) != 0
|
|
1089
|
+
} else {
|
|
1090
|
+
deferred_beyond.binary_search(&ordinal).is_ok()
|
|
1091
|
+
};
|
|
1092
|
+
if !is_deferred {
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
pending -= 1;
|
|
1096
|
+
let e = f.names.find(name);
|
|
1097
|
+
let mut cover = false;
|
|
1098
|
+
match e {
|
|
1099
|
+
Some(e) => {
|
|
1100
|
+
for app in f.entries[e].apps.iter() {
|
|
1101
|
+
let c = &f.contributors[app.contributor as usize];
|
|
1102
|
+
if c.condition.is_none() {
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
let active = pass.active(c.condition)
|
|
1106
|
+
|| app.others.iter().any(|&o| pass.active(f.contributors[o as usize].condition));
|
|
1107
|
+
if active {
|
|
1108
|
+
if !self.apply(app.child, v) {
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1111
|
+
cover = true;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
None => {
|
|
1116
|
+
for c in f.contributors.iter() {
|
|
1117
|
+
if c.condition.is_some() && pass.active(c.condition) {
|
|
1118
|
+
match self.resolve_unknown(c, name, v) {
|
|
1119
|
+
Some(matched) => cover |= matched,
|
|
1120
|
+
None => return false,
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if cover {
|
|
1127
|
+
covered.set(ordinal);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
for c in f.contributors.iter() {
|
|
1132
|
+
if c.condition.is_some() {
|
|
1133
|
+
if !pass.active(c.condition) {
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
if !count_ok(c, count) {
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
let satisfied = (c.alt.is_none() || count_ok(c, count)) && pass.seen.all(&c.required);
|
|
1141
|
+
if !satisfied {
|
|
1142
|
+
match c.alt {
|
|
1143
|
+
None => return false,
|
|
1144
|
+
Some((group, branch)) => pass.alt_failed[group as usize] |= 1 << branch,
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
for (g, group) in f.alt_groups.iter().enumerate() {
|
|
1150
|
+
let all = if group.count == 64 { u64::MAX } else { (1u64 << group.count) - 1 };
|
|
1151
|
+
let survivors = !pass.alt_failed[g] & all;
|
|
1152
|
+
if survivors == 0 || (group.exactly_one && survivors & (survivors - 1) != 0) {
|
|
1153
|
+
return false;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
for (gate, names) in f.forbidden.iter() {
|
|
1158
|
+
if pass.active(*gate) && pass.seen.all(names) {
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
for a in f.alternatives.iter() {
|
|
1164
|
+
if a.condition.is_some() && !pass.active(a.condition) {
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
let matches = a.branches.iter().filter(|b| pass.seen.all(b)).count();
|
|
1168
|
+
if matches == 0 || (a.exactly_one && matches != 1) {
|
|
1169
|
+
return false;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if let Some(u) = f.unevaluated {
|
|
1174
|
+
for (ordinal, v) in o.values().enumerate() {
|
|
1175
|
+
if !covered.get(ordinal) && !self.run_child(u, v) {
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
true
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
#[inline]
|
|
1185
|
+
fn count_ok(c: &Contributor, count: u64) -> bool {
|
|
1186
|
+
c.min.is_none_or(|m| count >= m) && c.max.is_none_or(|m| count <= m)
|
|
1187
|
+
}
|