schemurai 2.3.0 → 3.0.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 +4 -4
- data/README.md +67 -4
- data/ext/schemurai_native/Cargo.lock +1 -1
- data/ext/schemurai_native/Cargo.toml +1 -1
- data/ext/schemurai_native/glue.c +155 -14
- data/ext/schemurai_native/src/budget.rs +101 -0
- data/ext/schemurai_native/src/diagnostics.rs +395 -225
- data/ext/schemurai_native/src/enumeration.rs +154 -0
- data/ext/schemurai_native/src/evaluation.rs +219 -0
- data/ext/schemurai_native/src/graph.rs +50 -0
- data/ext/schemurai_native/src/instance_graph.rs +84 -0
- data/ext/schemurai_native/src/lib.rs +947 -184
- data/ext/schemurai_native/src/memo.rs +1149 -0
- data/ext/schemurai_native/src/memo_scope.rs +240 -0
- data/ext/schemurai_native/src/number.rs +7 -8
- data/ext/schemurai_native/src/pattern.rs +157 -14
- data/ext/schemurai_native/src/scope.rs +243 -33
- data/ext/schemurai_native/src/structural.rs +86 -0
- data/ext/schemurai_native/src/tracking.rs +245 -90
- data/lib/schemurai/backend.rb +1 -0
- data/lib/schemurai/evaluator.rb +45 -3
- data/lib/schemurai/limit_budget.rb +78 -0
- data/lib/schemurai/limit_exceeded.rb +18 -0
- data/lib/schemurai/limited_evaluator.rb +100 -0
- data/lib/schemurai/native/evaluator.rb +32 -5
- data/lib/schemurai/schema_domain.rb +12 -7
- data/lib/schemurai/schema_graph.rb +10 -1
- data/lib/schemurai/unique_items.rb +2 -0
- data/lib/schemurai/version.rb +1 -1
- data/lib/schemurai/vm/compiler.rb +7 -3
- data/lib/schemurai/vm/evaluator.rb +50 -3
- data/lib/schemurai/vm/limited_evaluator.rb +97 -0
- data/lib/schemurai.rb +74 -27
- data/sig/schemurai.rbs +20 -12
- metadata +16 -4
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
//! Large enums retain a compiled fingerprint index over pinned schema values.
|
|
2
|
+
//! Hashes only select candidates; JSON equality still decides every match.
|
|
3
|
+
use super::*;
|
|
4
|
+
|
|
5
|
+
const LINEAR_LIMIT: usize = 16;
|
|
6
|
+
const MAX_INDEX_BYTES: usize = 64 * 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
#[derive(Debug)]
|
|
9
|
+
pub(super) struct Enumeration {
|
|
10
|
+
values: Vec<Value>,
|
|
11
|
+
index: Option<Index>,
|
|
12
|
+
kinds: i32,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
impl Enumeration {
|
|
16
|
+
pub(super) fn compile(values: Vec<Value>, fixnum_flag: Value) -> Self {
|
|
17
|
+
let mut fingerprints = structural::Cache::batch(0);
|
|
18
|
+
let budget = budget::Budget::default();
|
|
19
|
+
let index = (values.len() > LINEAR_LIMIT)
|
|
20
|
+
.then(|| {
|
|
21
|
+
Index::compile(&values, |value| {
|
|
22
|
+
fingerprint_cached(value, 0, fixnum_flag, false, &mut fingerprints, &budget)
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
.flatten();
|
|
26
|
+
let kinds = if index.is_some() {
|
|
27
|
+
values
|
|
28
|
+
.iter()
|
|
29
|
+
.fold(0, |mask, &value| mask | unsafe { sn_shallow_kind(value) })
|
|
30
|
+
} else {
|
|
31
|
+
0
|
|
32
|
+
};
|
|
33
|
+
Self {
|
|
34
|
+
values,
|
|
35
|
+
index,
|
|
36
|
+
kinds,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub(super) fn eval(
|
|
41
|
+
&self,
|
|
42
|
+
value: Value,
|
|
43
|
+
kind: i32,
|
|
44
|
+
depth: usize,
|
|
45
|
+
fixnum_flag: Value,
|
|
46
|
+
budget: &budget::Budget,
|
|
47
|
+
) -> Verdict {
|
|
48
|
+
if let Some(index) = &self.index {
|
|
49
|
+
// All indexed schema values are supported. A disjoint root type
|
|
50
|
+
// cannot match, even if the input contains unsupported descendants.
|
|
51
|
+
// Avoid fingerprinting large containers for scalar-only enums.
|
|
52
|
+
if kind & self.kinds == 0 {
|
|
53
|
+
return Invalid;
|
|
54
|
+
}
|
|
55
|
+
// Keep the best-case linear lookup cheap. This also avoids hashing
|
|
56
|
+
// a whole input container when the first candidate already matches.
|
|
57
|
+
let first = equal_budgeted(self.values[0], value, depth, budget);
|
|
58
|
+
if first != Invalid {
|
|
59
|
+
return first;
|
|
60
|
+
}
|
|
61
|
+
if let Some(hash) = fingerprint_budgeted(value, depth, fixnum_flag, false, budget) {
|
|
62
|
+
return self.compare(
|
|
63
|
+
index.candidates(hash).filter(|&index| index != 0),
|
|
64
|
+
value,
|
|
65
|
+
depth,
|
|
66
|
+
budget,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return self.compare(1..self.values.len(), value, depth, budget);
|
|
70
|
+
}
|
|
71
|
+
// An unavailable fingerprint is not an unsupported comparison. Linear
|
|
72
|
+
// equality can reject a different shape before reaching that value.
|
|
73
|
+
self.compare(0..self.values.len(), value, depth, budget)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fn compare(
|
|
77
|
+
&self,
|
|
78
|
+
candidates: impl Iterator<Item = usize>,
|
|
79
|
+
value: Value,
|
|
80
|
+
depth: usize,
|
|
81
|
+
budget: &budget::Budget,
|
|
82
|
+
) -> Verdict {
|
|
83
|
+
for index in candidates {
|
|
84
|
+
let result = equal_budgeted(self.values[index], value, depth, budget);
|
|
85
|
+
if result != Invalid {
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
Invalid
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pub(super) fn memory_usage(&self) -> usize {
|
|
93
|
+
std::mem::size_of::<Self>()
|
|
94
|
+
+ self.values.capacity() * std::mem::size_of::<Value>()
|
|
95
|
+
+ self.index.as_ref().map_or(0, |index| {
|
|
96
|
+
index.entries.capacity() * std::mem::size_of::<(u64, usize)>()
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[derive(Debug)]
|
|
102
|
+
struct Index {
|
|
103
|
+
entries: Vec<(u64, usize)>,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
impl Index {
|
|
107
|
+
fn compile(values: &[Value], mut hash: impl FnMut(Value) -> Option<u64>) -> Option<Self> {
|
|
108
|
+
if values
|
|
109
|
+
.len()
|
|
110
|
+
.checked_mul(std::mem::size_of::<(u64, usize)>())?
|
|
111
|
+
> MAX_INDEX_BYTES
|
|
112
|
+
{
|
|
113
|
+
return None;
|
|
114
|
+
}
|
|
115
|
+
let mut entries = Vec::new();
|
|
116
|
+
entries.try_reserve_exact(values.len()).ok()?;
|
|
117
|
+
for (index, &value) in values.iter().enumerate() {
|
|
118
|
+
entries.push((hash(value)?, index));
|
|
119
|
+
}
|
|
120
|
+
// A sorted flat index bounds memory and compilation to O(E log E),
|
|
121
|
+
// even for colliding fingerprints. Equal hashes retain schema order.
|
|
122
|
+
entries.sort_unstable();
|
|
123
|
+
Some(Self { entries })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn candidates(&self, hash: u64) -> impl Iterator<Item = usize> + '_ {
|
|
127
|
+
let start = self
|
|
128
|
+
.entries
|
|
129
|
+
.partition_point(|&(candidate, _)| candidate < hash);
|
|
130
|
+
self.entries[start..]
|
|
131
|
+
.iter()
|
|
132
|
+
.take_while(move |&&(candidate, _)| candidate == hash)
|
|
133
|
+
.map(|&(_, index)| index)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[cfg(test)]
|
|
138
|
+
mod tests {
|
|
139
|
+
use super::*;
|
|
140
|
+
|
|
141
|
+
#[test]
|
|
142
|
+
fn selects_all_collisions_in_schema_order() {
|
|
143
|
+
let index = Index::compile(&[3, 1, 3, 2, 3], |value| Some(value as u64)).unwrap();
|
|
144
|
+
assert_eq!(index.candidates(3).collect::<Vec<_>>(), [0, 2, 4]);
|
|
145
|
+
assert_eq!(index.candidates(1).collect::<Vec<_>>(), [1]);
|
|
146
|
+
assert_eq!(index.candidates(0).count(), 0);
|
|
147
|
+
assert_eq!(index.candidates(4).count(), 0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[test]
|
|
151
|
+
fn unavailable_fingerprint_discards_the_index() {
|
|
152
|
+
assert!(Index::compile(&[1, 2, 3], |value| (value != 2).then_some(value as u64)).is_none());
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
//! Shared branch and contains scans with separate result and termination policies.
|
|
2
|
+
use super::*;
|
|
3
|
+
use tracking::Tracked;
|
|
4
|
+
|
|
5
|
+
#[derive(Clone, Copy)]
|
|
6
|
+
pub(super) enum BranchKind {
|
|
7
|
+
Any,
|
|
8
|
+
One,
|
|
9
|
+
}
|
|
10
|
+
impl BranchKind {
|
|
11
|
+
pub(super) fn accepts(self, matches: usize) -> bool {
|
|
12
|
+
match self {
|
|
13
|
+
Self::Any => matches > 0,
|
|
14
|
+
Self::One => matches == 1,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
pub(super) fn keyword(self) -> &'static str {
|
|
18
|
+
match self {
|
|
19
|
+
Self::Any => "anyOf",
|
|
20
|
+
Self::One => "oneOf",
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[derive(Clone, Copy)]
|
|
26
|
+
pub(super) enum BranchPolicy {
|
|
27
|
+
Validity(BranchKind),
|
|
28
|
+
// Annotations, exact diagnostic counts and compatibility exceptions can
|
|
29
|
+
// require later branches even after validity alone is conclusive.
|
|
30
|
+
Complete,
|
|
31
|
+
}
|
|
32
|
+
impl BranchPolicy {
|
|
33
|
+
fn finished(self, matches: usize) -> bool {
|
|
34
|
+
match self {
|
|
35
|
+
Self::Validity(BranchKind::Any) => matches > 0,
|
|
36
|
+
Self::Validity(BranchKind::One) => matches > 1,
|
|
37
|
+
Self::Complete => false,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub(super) trait BranchResult {
|
|
43
|
+
fn valid() -> Self;
|
|
44
|
+
fn status(&self) -> Verdict;
|
|
45
|
+
fn merge(&mut self, branch: Self);
|
|
46
|
+
}
|
|
47
|
+
impl BranchResult for Verdict {
|
|
48
|
+
fn valid() -> Self {
|
|
49
|
+
Valid
|
|
50
|
+
}
|
|
51
|
+
fn status(&self) -> Verdict {
|
|
52
|
+
*self
|
|
53
|
+
}
|
|
54
|
+
fn merge(&mut self, _: Self) {}
|
|
55
|
+
}
|
|
56
|
+
impl BranchResult for Tracked {
|
|
57
|
+
fn valid() -> Self {
|
|
58
|
+
Self::new(Valid)
|
|
59
|
+
}
|
|
60
|
+
fn status(&self) -> Verdict {
|
|
61
|
+
self.status
|
|
62
|
+
}
|
|
63
|
+
fn merge(&mut self, branch: Self) {
|
|
64
|
+
self.merge(branch);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
pub(super) struct BranchMatches<R> {
|
|
69
|
+
pub matches: usize,
|
|
70
|
+
pub coverage: R,
|
|
71
|
+
}
|
|
72
|
+
pub(super) fn scan_branches<R: BranchResult>(
|
|
73
|
+
nodes: &[(usize, usize)],
|
|
74
|
+
policy: BranchPolicy,
|
|
75
|
+
mut evaluate: impl FnMut(usize) -> R,
|
|
76
|
+
) -> Result<BranchMatches<R>, Verdict> {
|
|
77
|
+
let mut result = BranchMatches {
|
|
78
|
+
matches: 0,
|
|
79
|
+
coverage: R::valid(),
|
|
80
|
+
};
|
|
81
|
+
for &(node, multiplicity) in nodes {
|
|
82
|
+
let branch = evaluate(node);
|
|
83
|
+
match branch.status() {
|
|
84
|
+
Unsupported => return Err(Unsupported),
|
|
85
|
+
Invalid => {}
|
|
86
|
+
Valid => {
|
|
87
|
+
result.matches += multiplicity;
|
|
88
|
+
result.coverage.merge(branch);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if policy.finished(result.matches) {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
Ok(result)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Diagnostic counts and annotations need every match. Keep the same complete
|
|
99
|
+
// traversal for ordinary validity so unsupported later items retain fallback.
|
|
100
|
+
pub(super) fn scan_contains(
|
|
101
|
+
len: usize,
|
|
102
|
+
mut evaluate: impl FnMut(usize) -> Verdict,
|
|
103
|
+
mut record: impl FnMut(usize),
|
|
104
|
+
) -> Result<usize, Verdict> {
|
|
105
|
+
let mut matches = 0;
|
|
106
|
+
for index in 0..len {
|
|
107
|
+
match evaluate(index) {
|
|
108
|
+
Unsupported => return Err(Unsupported),
|
|
109
|
+
Invalid => {}
|
|
110
|
+
Valid => {
|
|
111
|
+
matches += 1;
|
|
112
|
+
record(index);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
Ok(matches)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Use only after proving every remaining predicate evaluation is supported.
|
|
120
|
+
// Annotation and diagnostic policies continue using the complete scan above.
|
|
121
|
+
pub(super) fn contains_validity(
|
|
122
|
+
len: usize,
|
|
123
|
+
min: usize,
|
|
124
|
+
max: Option<usize>,
|
|
125
|
+
mut evaluate: impl FnMut(usize) -> bool,
|
|
126
|
+
) -> Verdict {
|
|
127
|
+
if len < min || max.is_some_and(|max| max < min) {
|
|
128
|
+
return Invalid;
|
|
129
|
+
}
|
|
130
|
+
// Infinity and any bound at least as large as the array cannot constrain
|
|
131
|
+
// its match count. The VM represents an absent maxContains as +Infinity.
|
|
132
|
+
let max = max.filter(|&max| max < len);
|
|
133
|
+
if min == 0 && max.is_none() {
|
|
134
|
+
return Valid;
|
|
135
|
+
}
|
|
136
|
+
let mut matches = 0;
|
|
137
|
+
for index in 0..len {
|
|
138
|
+
matches += usize::from(evaluate(index));
|
|
139
|
+
if max.is_some_and(|max| matches > max) {
|
|
140
|
+
return Invalid;
|
|
141
|
+
}
|
|
142
|
+
if max.is_none() && matches >= min {
|
|
143
|
+
return Valid;
|
|
144
|
+
}
|
|
145
|
+
if matches + (len - index - 1) < min {
|
|
146
|
+
return Invalid;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
bounds(matches, Some(min), max)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[cfg(test)]
|
|
153
|
+
mod tests {
|
|
154
|
+
use super::*;
|
|
155
|
+
|
|
156
|
+
#[test]
|
|
157
|
+
fn contains_termination_matches_complete_counts() {
|
|
158
|
+
for len in 0..8 {
|
|
159
|
+
for mask in 0..(1 << len) {
|
|
160
|
+
let count = (mask as u32).count_ones() as usize;
|
|
161
|
+
for min in 0..9 {
|
|
162
|
+
for max in [None, Some(0), Some(1), Some(4), Some(8)] {
|
|
163
|
+
assert_eq!(
|
|
164
|
+
contains_validity(len, min, max, |i| mask & (1 << i) != 0),
|
|
165
|
+
bounds(count, Some(min), max)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
let mut calls = 0;
|
|
172
|
+
assert_eq!(
|
|
173
|
+
contains_validity(1000, 1, None, |_| {
|
|
174
|
+
calls += 1;
|
|
175
|
+
true
|
|
176
|
+
}),
|
|
177
|
+
Valid
|
|
178
|
+
);
|
|
179
|
+
assert_eq!(calls, 1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[test]
|
|
183
|
+
fn termination_preserves_multiplicity_and_complete_scan_errors() {
|
|
184
|
+
let nodes = [(0, 2), (1, 1)];
|
|
185
|
+
let evaluate = |node| if node == 0 { Valid } else { Unsupported };
|
|
186
|
+
for kind in [BranchKind::Any, BranchKind::One] {
|
|
187
|
+
let result = scan_branches(&nodes, BranchPolicy::Validity(kind), evaluate).unwrap();
|
|
188
|
+
assert_eq!(result.matches, 2);
|
|
189
|
+
assert_eq!(
|
|
190
|
+
kind.accepts(result.matches),
|
|
191
|
+
matches!(kind, BranchKind::Any)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
assert!(matches!(
|
|
195
|
+
scan_branches(&nodes, BranchPolicy::Complete, evaluate),
|
|
196
|
+
Err(Unsupported)
|
|
197
|
+
));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#[test]
|
|
201
|
+
fn contains_records_only_matches_and_retains_later_unsupported_results() {
|
|
202
|
+
let mut recorded = vec![];
|
|
203
|
+
let result = scan_contains(
|
|
204
|
+
4,
|
|
205
|
+
|index| verdict(index % 2 == 0),
|
|
206
|
+
|index| recorded.push(index),
|
|
207
|
+
);
|
|
208
|
+
assert_eq!(result, Ok(2));
|
|
209
|
+
assert_eq!(recorded, [0, 2]);
|
|
210
|
+
assert_eq!(
|
|
211
|
+
scan_contains(
|
|
212
|
+
2,
|
|
213
|
+
|index| if index == 0 { Valid } else { Unsupported },
|
|
214
|
+
|_| {}
|
|
215
|
+
),
|
|
216
|
+
Err(Unsupported)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//! Iterative schema graph traversal used by compilation analyses.
|
|
2
|
+
|
|
3
|
+
// Callers own the visited buffer so cycle analysis can reuse it in the
|
|
4
|
+
// reverse pass. Keep discovery and child order identical for both analyses.
|
|
5
|
+
pub(super) fn postorder(edges: &[Vec<usize>], seen: &mut [bool]) -> Vec<usize> {
|
|
6
|
+
let mut order = vec![];
|
|
7
|
+
for root in 0..edges.len() {
|
|
8
|
+
if seen[root] {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
seen[root] = true;
|
|
12
|
+
let mut stack = vec![(root, 0)];
|
|
13
|
+
while let Some((node, next)) = stack.last_mut() {
|
|
14
|
+
if let Some(&child) = edges[*node].get(*next) {
|
|
15
|
+
*next += 1;
|
|
16
|
+
if !seen[child] {
|
|
17
|
+
seen[child] = true;
|
|
18
|
+
stack.push((child, 0));
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
order.push(*node);
|
|
22
|
+
stack.pop();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
order
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[cfg(test)]
|
|
30
|
+
mod tests {
|
|
31
|
+
use super::*;
|
|
32
|
+
|
|
33
|
+
#[test]
|
|
34
|
+
fn orders_disconnected_cycles_and_shared_children() {
|
|
35
|
+
let edges = vec![vec![1, 2], vec![2], vec![0], vec![3], vec![]];
|
|
36
|
+
let mut seen = vec![false; edges.len()];
|
|
37
|
+
assert_eq!(postorder(&edges, &mut seen), [2, 1, 0, 3, 4]);
|
|
38
|
+
assert!(seen.iter().all(|&visited| visited));
|
|
39
|
+
assert!(postorder(&[], &mut []).is_empty());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[test]
|
|
43
|
+
fn deep_chains_use_an_explicit_stack() {
|
|
44
|
+
let count = 100_000;
|
|
45
|
+
let mut edges: Vec<_> = (1..count).map(|child| vec![child]).collect();
|
|
46
|
+
edges.push(vec![]);
|
|
47
|
+
let order = postorder(&edges, &mut vec![false; count]);
|
|
48
|
+
assert!(order.into_iter().eq((0..count).rev()));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
//! Bounded, invocation-local proof that container values cannot revisit an
|
|
2
|
+
//! ancestor instance. Read under the GVL, without Ruby allocations/callbacks.
|
|
3
|
+
use super::*;
|
|
4
|
+
|
|
5
|
+
const MAX_VISITS: usize = 4096;
|
|
6
|
+
|
|
7
|
+
pub(super) struct Graph {
|
|
8
|
+
proven: structural::Cache<Value, bool>,
|
|
9
|
+
}
|
|
10
|
+
impl Default for Graph {
|
|
11
|
+
fn default() -> Self {
|
|
12
|
+
Self {
|
|
13
|
+
proven: structural::Cache::new(0),
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
impl Graph {
|
|
18
|
+
pub(super) fn acyclic(&mut self, value: Value, budget: &budget::Budget) -> bool {
|
|
19
|
+
self.visit(value, 0, &mut HashMap::new(), budget)
|
|
20
|
+
}
|
|
21
|
+
fn visit(
|
|
22
|
+
&mut self,
|
|
23
|
+
value: Value,
|
|
24
|
+
depth: usize,
|
|
25
|
+
states: &mut HashMap<Value, bool>,
|
|
26
|
+
budget: &budget::Budget,
|
|
27
|
+
) -> bool {
|
|
28
|
+
if !budget.checkpoint() {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
let kind = unsafe { sn_shallow_kind(value) };
|
|
32
|
+
if kind != TYPE_ARRAY && kind != TYPE_OBJECT {
|
|
33
|
+
return kind != TYPE_UNSUPPORTED;
|
|
34
|
+
}
|
|
35
|
+
if self.proven.get(value) == Some(true) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if let Some(&done) = states.get(&value) {
|
|
39
|
+
return done;
|
|
40
|
+
}
|
|
41
|
+
if depth > MAX_EVALUATION_DEPTH || states.len() == MAX_VISITS {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
states.insert(value, false);
|
|
45
|
+
let complete = if kind == TYPE_ARRAY {
|
|
46
|
+
let len = unsafe { sn_len(value) };
|
|
47
|
+
if !budget.checkpoint() {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
(0..len).all(|i| self.visit(unsafe { sn_entry(value, i) }, depth + 1, states, budget))
|
|
51
|
+
} else {
|
|
52
|
+
let mut context = Walk {
|
|
53
|
+
graph: self,
|
|
54
|
+
states,
|
|
55
|
+
depth: depth + 1,
|
|
56
|
+
budget,
|
|
57
|
+
};
|
|
58
|
+
(unsafe { sn_hash_each(value, (&mut context as *mut Walk<'_>).cast(), entry) })
|
|
59
|
+
== VERDICT_VALID
|
|
60
|
+
};
|
|
61
|
+
if complete {
|
|
62
|
+
states.insert(value, true);
|
|
63
|
+
self.proven.record(value, true);
|
|
64
|
+
}
|
|
65
|
+
complete
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
struct Walk<'a> {
|
|
69
|
+
graph: &'a mut Graph,
|
|
70
|
+
states: &'a mut HashMap<Value, bool>,
|
|
71
|
+
depth: usize,
|
|
72
|
+
budget: &'a budget::Budget,
|
|
73
|
+
}
|
|
74
|
+
unsafe extern "C" fn entry(context: *mut c_void, _key: Value, value: Value) -> i32 {
|
|
75
|
+
catch_unwind(AssertUnwindSafe(|| {
|
|
76
|
+
let context = unsafe { &mut *context.cast::<Walk<'_>>() };
|
|
77
|
+
verdict(
|
|
78
|
+
context
|
|
79
|
+
.graph
|
|
80
|
+
.visit(value, context.depth, context.states, context.budget),
|
|
81
|
+
) as i32
|
|
82
|
+
}))
|
|
83
|
+
.unwrap_or(VERDICT_INVALID)
|
|
84
|
+
}
|