schemurai 2.2.3 → 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 +81 -7
- data/ext/schemurai_native/Cargo.lock +99 -0
- data/ext/schemurai_native/Cargo.toml +21 -0
- data/ext/schemurai_native/build.rs +35 -0
- data/ext/schemurai_native/extconf.rb +63 -0
- data/ext/schemurai_native/glue.c +780 -0
- data/ext/schemurai_native/protocol.def +63 -0
- data/ext/schemurai_native/protocol.h +10 -0
- data/ext/schemurai_native/src/budget.rs +101 -0
- data/ext/schemurai_native/src/content.rs +574 -0
- data/ext/schemurai_native/src/diagnostics.rs +1135 -0
- data/ext/schemurai_native/src/enumeration.rs +154 -0
- data/ext/schemurai_native/src/evaluation.rs +219 -0
- data/ext/schemurai_native/src/format.rs +357 -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 +3139 -0
- 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 +1054 -0
- data/ext/schemurai_native/src/pattern.rs +431 -0
- data/ext/schemurai_native/src/scope.rs +420 -0
- data/ext/schemurai_native/src/structural.rs +86 -0
- data/ext/schemurai_native/src/tracking.rs +687 -0
- data/lib/schemurai/backend.rb +3 -1
- 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/diagnostics.rb +52 -0
- data/lib/schemurai/native/evaluator.rb +59 -0
- 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 +81 -33
- data/sig/schemurai.rbs +21 -13
- metadata +36 -6
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
//! Byte parsers matching the built-in Ruby formats. They borrow original string
|
|
2
|
+
//! storage and allocate neither Ruby objects nor Rust instance representations.
|
|
3
|
+
use super::*;
|
|
4
|
+
|
|
5
|
+
#[derive(Debug)]
|
|
6
|
+
pub(super) enum Format {
|
|
7
|
+
Date,
|
|
8
|
+
Time,
|
|
9
|
+
DateTime,
|
|
10
|
+
Duration,
|
|
11
|
+
Ipv4,
|
|
12
|
+
Ipv6,
|
|
13
|
+
Uuid,
|
|
14
|
+
Pointer,
|
|
15
|
+
RelativePointer,
|
|
16
|
+
Unsupported,
|
|
17
|
+
}
|
|
18
|
+
impl Format {
|
|
19
|
+
pub(super) fn compile(name: Value) -> Self {
|
|
20
|
+
if unsafe { sn_kind(name) } != TYPE_STRING {
|
|
21
|
+
return Self::Unsupported;
|
|
22
|
+
}
|
|
23
|
+
let bytes = unsafe { string_bytes(name) };
|
|
24
|
+
match bytes {
|
|
25
|
+
b"date" => Self::Date,
|
|
26
|
+
b"time" => Self::Time,
|
|
27
|
+
b"date-time" => Self::DateTime,
|
|
28
|
+
b"duration" => Self::Duration,
|
|
29
|
+
b"ipv4" => Self::Ipv4,
|
|
30
|
+
b"ipv6" => Self::Ipv6,
|
|
31
|
+
b"uuid" => Self::Uuid,
|
|
32
|
+
b"json-pointer" => Self::Pointer,
|
|
33
|
+
b"relative-json-pointer" => Self::RelativePointer,
|
|
34
|
+
_ => Self::Unsupported,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
pub(super) fn eval(&self, value: Value) -> Verdict {
|
|
38
|
+
self.eval_bytes(unsafe { string_bytes(value) })
|
|
39
|
+
}
|
|
40
|
+
pub(super) fn eval_bytes(&self, bytes: &[u8]) -> Verdict {
|
|
41
|
+
verdict(match self {
|
|
42
|
+
Self::Date => date_exact(bytes),
|
|
43
|
+
Self::Time => time(bytes),
|
|
44
|
+
Self::DateTime => date_time(bytes),
|
|
45
|
+
Self::Duration => duration(bytes),
|
|
46
|
+
Self::Ipv4 => ipv4(bytes),
|
|
47
|
+
Self::Ipv6 => ipv6(bytes),
|
|
48
|
+
Self::Uuid => uuid(bytes),
|
|
49
|
+
Self::Pointer => pointer(bytes),
|
|
50
|
+
Self::RelativePointer => relative_pointer(bytes),
|
|
51
|
+
Self::Unsupported => return Unsupported,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
pub(super) fn eval_items(&self, required: i32, values: &[Value]) -> Verdict {
|
|
55
|
+
self.visit_invalid_items(required, values, |_, _| Invalid)
|
|
56
|
+
}
|
|
57
|
+
pub(super) fn visit_invalid_items(
|
|
58
|
+
&self,
|
|
59
|
+
required: i32,
|
|
60
|
+
values: &[Value],
|
|
61
|
+
invalid: impl FnMut(usize, Value) -> Verdict,
|
|
62
|
+
) -> Verdict {
|
|
63
|
+
// Each arm monomorphizes the scan with its parser selected once.
|
|
64
|
+
macro_rules! scan {
|
|
65
|
+
($parser:ident) => {
|
|
66
|
+
scan_items(required, values, invalid, |bytes| verdict($parser(bytes)))
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
match self {
|
|
70
|
+
Self::Date => scan!(date_exact),
|
|
71
|
+
Self::Time => scan!(time),
|
|
72
|
+
Self::DateTime => scan!(date_time),
|
|
73
|
+
Self::Duration => scan!(duration),
|
|
74
|
+
Self::Ipv4 => scan!(ipv4),
|
|
75
|
+
Self::Ipv6 => scan!(ipv6),
|
|
76
|
+
Self::Uuid => scan!(uuid),
|
|
77
|
+
Self::Pointer => scan!(pointer),
|
|
78
|
+
Self::RelativePointer => scan!(relative_pointer),
|
|
79
|
+
Self::Unsupported => scan_items(required, values, invalid, |_| Unsupported),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn scan_items(
|
|
85
|
+
required: i32,
|
|
86
|
+
values: &[Value],
|
|
87
|
+
mut invalid: impl FnMut(usize, Value) -> Verdict,
|
|
88
|
+
mut matches: impl FnMut(&[u8]) -> Verdict,
|
|
89
|
+
) -> Verdict {
|
|
90
|
+
for (index, &value) in values.iter().enumerate() {
|
|
91
|
+
let mut data = std::ptr::null();
|
|
92
|
+
let mut len = 0;
|
|
93
|
+
let kind = unsafe { sn_format_item_view(value, &mut data, &mut len) };
|
|
94
|
+
let result = if kind == -TYPE_UNSUPPORTED {
|
|
95
|
+
Unsupported
|
|
96
|
+
} else if kind < 0 {
|
|
97
|
+
verdict(required == 0)
|
|
98
|
+
} else {
|
|
99
|
+
matches(unsafe { byte_view(data, len) })
|
|
100
|
+
};
|
|
101
|
+
match result {
|
|
102
|
+
Unsupported => return Unsupported,
|
|
103
|
+
Invalid => {
|
|
104
|
+
let status = invalid(index, value);
|
|
105
|
+
if status != Valid {
|
|
106
|
+
return status;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
Valid => {}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
Valid
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn date_exact(bytes: &[u8]) -> bool {
|
|
116
|
+
bytes.len() == 10 && date(bytes)
|
|
117
|
+
}
|
|
118
|
+
fn date_time(bytes: &[u8]) -> bool {
|
|
119
|
+
bytes.len() >= 20 && date(bytes) && matches!(bytes[10], b'T' | b't') && time(&bytes[11..])
|
|
120
|
+
}
|
|
121
|
+
fn uuid(bytes: &[u8]) -> bool {
|
|
122
|
+
bytes.len() == 36
|
|
123
|
+
&& bytes.iter().enumerate().all(|(index, byte)| {
|
|
124
|
+
if matches!(index, 8 | 13 | 18 | 23) {
|
|
125
|
+
*byte == b'-'
|
|
126
|
+
} else {
|
|
127
|
+
byte.is_ascii_hexdigit()
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
fn relative_pointer(bytes: &[u8]) -> bool {
|
|
132
|
+
let Some(end) = digits_end(bytes, 0) else {
|
|
133
|
+
return false;
|
|
134
|
+
};
|
|
135
|
+
if bytes[0] == b'0' && end > 1 {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
end == bytes.len() || (bytes[end] == b'#' && end + 1 == bytes.len()) || pointer(&bytes[end..])
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fn two(bytes: &[u8], index: usize) -> Option<u32> {
|
|
142
|
+
let tens = bytes.get(index)?.checked_sub(b'0')?;
|
|
143
|
+
let ones = bytes.get(index + 1)?.checked_sub(b'0')?;
|
|
144
|
+
(tens <= 9 && ones <= 9).then_some(u32::from(tens) * 10 + u32::from(ones))
|
|
145
|
+
}
|
|
146
|
+
fn date(bytes: &[u8]) -> bool {
|
|
147
|
+
if bytes.get(4) != Some(&b'-') || bytes.get(7) != Some(&b'-') {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
let (Some(century), Some(year), Some(month), Some(day)) =
|
|
151
|
+
(two(bytes, 0), two(bytes, 2), two(bytes, 5), two(bytes, 8))
|
|
152
|
+
else {
|
|
153
|
+
return false;
|
|
154
|
+
};
|
|
155
|
+
let year = century * 100 + year;
|
|
156
|
+
let last = match month {
|
|
157
|
+
2 => {
|
|
158
|
+
if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
|
|
159
|
+
29
|
|
160
|
+
} else {
|
|
161
|
+
28
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
4 | 6 | 9 | 11 => 30,
|
|
165
|
+
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
|
|
166
|
+
_ => return false,
|
|
167
|
+
};
|
|
168
|
+
(1..=last).contains(&day)
|
|
169
|
+
}
|
|
170
|
+
fn time(bytes: &[u8]) -> bool {
|
|
171
|
+
if bytes.len() < 9 || bytes[2] != b':' || bytes[5] != b':' {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
let (Some(hour), Some(minute), Some(second)) = (two(bytes, 0), two(bytes, 3), two(bytes, 6))
|
|
175
|
+
else {
|
|
176
|
+
return false;
|
|
177
|
+
};
|
|
178
|
+
if hour > 23 || minute > 59 || second > 60 {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
let mut at = 8;
|
|
182
|
+
if bytes[at] == b'.' {
|
|
183
|
+
let Some(end) = digits_end(bytes, at + 1) else {
|
|
184
|
+
return false;
|
|
185
|
+
};
|
|
186
|
+
at = end;
|
|
187
|
+
}
|
|
188
|
+
let offset = match bytes.get(at) {
|
|
189
|
+
Some(b'Z' | b'z') if at + 1 == bytes.len() => 0,
|
|
190
|
+
Some(b'+' | b'-') if at + 6 == bytes.len() && bytes[at + 3] == b':' => {
|
|
191
|
+
let (Some(hour), Some(minute)) = (two(bytes, at + 1), two(bytes, at + 4)) else {
|
|
192
|
+
return false;
|
|
193
|
+
};
|
|
194
|
+
if hour > 23 || minute > 59 {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
let offset = (hour * 60 + minute) as i32;
|
|
198
|
+
if bytes[at] == b'-' { -offset } else { offset }
|
|
199
|
+
}
|
|
200
|
+
_ => return false,
|
|
201
|
+
};
|
|
202
|
+
second != 60 || ((hour * 60 + minute) as i32 - offset).rem_euclid(1440) == 1439
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fn digits_end(bytes: &[u8], start: usize) -> Option<usize> {
|
|
206
|
+
let mut at = start;
|
|
207
|
+
while bytes.get(at).is_some_and(u8::is_ascii_digit) {
|
|
208
|
+
at += 1;
|
|
209
|
+
}
|
|
210
|
+
(at > start).then_some(at)
|
|
211
|
+
}
|
|
212
|
+
fn units(bytes: &[u8], start: usize, units: &[u8]) -> Option<usize> {
|
|
213
|
+
let end = digits_end(bytes, start)?;
|
|
214
|
+
let first = units.iter().position(|unit| Some(unit) == bytes.get(end))?;
|
|
215
|
+
let mut at = end + 1;
|
|
216
|
+
for unit in &units[first + 1..] {
|
|
217
|
+
let Some(end) = digits_end(bytes, at) else {
|
|
218
|
+
break;
|
|
219
|
+
};
|
|
220
|
+
if bytes.get(end) != Some(unit) {
|
|
221
|
+
return None;
|
|
222
|
+
}
|
|
223
|
+
at = end + 1;
|
|
224
|
+
}
|
|
225
|
+
Some(at)
|
|
226
|
+
}
|
|
227
|
+
fn duration(bytes: &[u8]) -> bool {
|
|
228
|
+
if bytes.first() != Some(&b'P') || bytes.len() <= 1 {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
if bytes[1] == b'T' {
|
|
232
|
+
return units(bytes, 2, b"HMS") == Some(bytes.len());
|
|
233
|
+
}
|
|
234
|
+
let Some(end) = digits_end(bytes, 1) else {
|
|
235
|
+
return false;
|
|
236
|
+
};
|
|
237
|
+
if bytes.get(end) == Some(&b'W') {
|
|
238
|
+
return end + 1 == bytes.len();
|
|
239
|
+
}
|
|
240
|
+
let Some(at) = units(bytes, 1, b"YMD") else {
|
|
241
|
+
return false;
|
|
242
|
+
};
|
|
243
|
+
at == bytes.len()
|
|
244
|
+
|| (bytes.get(at) == Some(&b'T') && units(bytes, at + 1, b"HMS") == Some(bytes.len()))
|
|
245
|
+
}
|
|
246
|
+
fn pointer(bytes: &[u8]) -> bool {
|
|
247
|
+
if bytes.is_empty() {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
if bytes[0] != b'/' {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
// The scalar scan avoids search dispatch overhead on short pointers.
|
|
254
|
+
if bytes.len() <= 32 {
|
|
255
|
+
let mut at = 1;
|
|
256
|
+
while at < bytes.len() {
|
|
257
|
+
if bytes[at] == b'~' {
|
|
258
|
+
at += 1;
|
|
259
|
+
if !matches!(bytes.get(at), Some(b'0' | b'1')) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
at += 1;
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
// Only tilde escapes need inspection. Search the original byte slice;
|
|
268
|
+
// neither tokens nor decoded pointer strings are materialized.
|
|
269
|
+
memchr::memchr_iter(b'~', &bytes[1..]).all(|at| matches!(bytes.get(at + 2), Some(b'0' | b'1')))
|
|
270
|
+
}
|
|
271
|
+
fn ipv4(bytes: &[u8]) -> bool {
|
|
272
|
+
if bytes.len() > 15 {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
let mut at = 0;
|
|
276
|
+
for octet in 0..4 {
|
|
277
|
+
let Some(end) = digits_end(bytes, at) else {
|
|
278
|
+
return false;
|
|
279
|
+
};
|
|
280
|
+
let count = end - at;
|
|
281
|
+
if count > 3 || (count > 1 && bytes[at] == b'0') {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
let number = bytes[at..end]
|
|
285
|
+
.iter()
|
|
286
|
+
.fold(0_u32, |n, b| n * 10 + u32::from(b - b'0'));
|
|
287
|
+
if number > 255 {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
if octet == 3 {
|
|
291
|
+
return end == bytes.len();
|
|
292
|
+
}
|
|
293
|
+
if bytes.get(end) != Some(&b'.') {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
at = end + 1;
|
|
297
|
+
}
|
|
298
|
+
unreachable!()
|
|
299
|
+
}
|
|
300
|
+
fn ipv6(bytes: &[u8]) -> bool {
|
|
301
|
+
if bytes.is_empty() || bytes.len() > 45 {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
let mut at = 0;
|
|
305
|
+
let mut groups = 0;
|
|
306
|
+
let mut compressed = false;
|
|
307
|
+
if bytes[0] == b':' {
|
|
308
|
+
if bytes.get(1) != Some(&b':') {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
at = 2;
|
|
312
|
+
compressed = true;
|
|
313
|
+
if at == bytes.len() {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
while at < bytes.len() {
|
|
318
|
+
let start = at;
|
|
319
|
+
while bytes.get(at).is_some_and(u8::is_ascii_hexdigit) {
|
|
320
|
+
at += 1;
|
|
321
|
+
}
|
|
322
|
+
if at == start || at - start > 4 {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
if bytes.get(at) == Some(&b'.') {
|
|
326
|
+
if groups > 6 || !ipv4(&bytes[start..]) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
groups += 2;
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
groups += 1;
|
|
333
|
+
if groups > 8 {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
if at == bytes.len() {
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
if bytes[at] != b':' {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
at += 1;
|
|
343
|
+
if bytes.get(at) == Some(&b':') {
|
|
344
|
+
if compressed {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
compressed = true;
|
|
348
|
+
at += 1;
|
|
349
|
+
if at == bytes.len() {
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
} else if at == bytes.len() {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if compressed { groups < 8 } else { groups == 8 }
|
|
357
|
+
}
|
|
@@ -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
|
+
}
|