schemurai 2.3.0 → 3.0.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +67 -4
  3. data/ext/schemurai_native/Cargo.lock +8 -1
  4. data/ext/schemurai_native/Cargo.toml +2 -1
  5. data/ext/schemurai_native/glue.c +155 -14
  6. data/ext/schemurai_native/src/budget.rs +101 -0
  7. data/ext/schemurai_native/src/content.rs +18 -0
  8. data/ext/schemurai_native/src/diagnostics.rs +395 -225
  9. data/ext/schemurai_native/src/enumeration.rs +154 -0
  10. data/ext/schemurai_native/src/evaluation.rs +219 -0
  11. data/ext/schemurai_native/src/format.rs +118 -8
  12. data/ext/schemurai_native/src/graph.rs +50 -0
  13. data/ext/schemurai_native/src/instance_graph.rs +84 -0
  14. data/ext/schemurai_native/src/integer_scan.rs +132 -0
  15. data/ext/schemurai_native/src/lib.rs +954 -185
  16. data/ext/schemurai_native/src/memo.rs +1149 -0
  17. data/ext/schemurai_native/src/memo_scope.rs +240 -0
  18. data/ext/schemurai_native/src/number.rs +7 -8
  19. data/ext/schemurai_native/src/pattern.rs +191 -22
  20. data/ext/schemurai_native/src/scope.rs +243 -33
  21. data/ext/schemurai_native/src/structural.rs +86 -0
  22. data/ext/schemurai_native/src/tracking.rs +245 -90
  23. data/lib/schemurai/backend.rb +1 -0
  24. data/lib/schemurai/evaluator.rb +45 -3
  25. data/lib/schemurai/limit_budget.rb +78 -0
  26. data/lib/schemurai/limit_exceeded.rb +18 -0
  27. data/lib/schemurai/limited_evaluator.rb +100 -0
  28. data/lib/schemurai/native/evaluator.rb +32 -5
  29. data/lib/schemurai/schema_domain.rb +12 -7
  30. data/lib/schemurai/schema_graph.rb +10 -1
  31. data/lib/schemurai/unique_items.rb +2 -0
  32. data/lib/schemurai/version.rb +1 -1
  33. data/lib/schemurai/vm/compiler.rb +7 -3
  34. data/lib/schemurai/vm/evaluator.rb +50 -3
  35. data/lib/schemurai/vm/limited_evaluator.rb +97 -0
  36. data/lib/schemurai.rb +74 -27
  37. data/sig/schemurai.rbs +20 -12
  38. metadata +17 -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
+ }
@@ -118,16 +118,100 @@ fn date_exact(bytes: &[u8]) -> bool {
118
118
  fn date_time(bytes: &[u8]) -> bool {
119
119
  bytes.len() >= 20 && date(bytes) && matches!(bytes[10], b'T' | b't') && time(&bytes[11..])
120
120
  }
121
+ // Keep the SIMD body out of the shared format dispatchers. Local x64 benchmarks
122
+ // and disassembly showed that inlining it shifted the unchanged JSON Pointer
123
+ // loop into a slower instruction layout; outlining retains the UUID SIMD gain.
124
+ #[inline(never)]
121
125
  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
- })
126
+ if bytes.len() != 36 || [8, 13, 18, 23].iter().any(|&at| bytes[at] != b'-') {
127
+ return false;
128
+ }
129
+ #[cfg(any(
130
+ all(target_arch = "x86_64", target_feature = "sse2"),
131
+ all(
132
+ target_arch = "aarch64",
133
+ target_feature = "neon",
134
+ target_endian = "little"
135
+ )
136
+ ))]
137
+ {
138
+ // The first two vectors contain all four separators. Their exact
139
+ // positions were checked above; only those lanes may bypass hex checks.
140
+ hex_block(&bytes[..16], (1 << 8) | (1 << 13))
141
+ && hex_block(&bytes[16..32], (1 << 2) | (1 << 7))
142
+ && bytes[32..].iter().all(u8::is_ascii_hexdigit)
143
+ }
144
+ #[cfg(not(any(
145
+ all(target_arch = "x86_64", target_feature = "sse2"),
146
+ all(
147
+ target_arch = "aarch64",
148
+ target_feature = "neon",
149
+ target_endian = "little"
150
+ )
151
+ )))]
152
+ bytes
153
+ .iter()
154
+ .enumerate()
155
+ .all(|(index, byte)| matches!(index, 8 | 13 | 18 | 23) || byte.is_ascii_hexdigit())
130
156
  }
157
+
158
+ #[cfg(all(target_arch = "x86_64", target_feature = "sse2"))]
159
+ fn hex_block(bytes: &[u8], separators: u16) -> bool {
160
+ use core::arch::x86_64::{
161
+ _mm_cmpeq_epi8, _mm_loadu_si128, _mm_movemask_epi8, _mm_or_si128, _mm_set1_epi8,
162
+ _mm_setzero_si128, _mm_sub_epi8, _mm_subs_epu8,
163
+ };
164
+ // The caller supplies sixteen readable bytes. SSE2 is a target baseline;
165
+ // unsigned wrapping subtraction rejects bytes below the ASCII ranges too.
166
+ let valid = unsafe {
167
+ let word = _mm_loadu_si128(bytes.as_ptr().cast());
168
+ let digits = _mm_sub_epi8(word, _mm_set1_epi8(b'0' as i8));
169
+ let letters = _mm_sub_epi8(
170
+ _mm_or_si128(word, _mm_set1_epi8(32)),
171
+ _mm_set1_epi8(b'a' as i8),
172
+ );
173
+ let digits = _mm_cmpeq_epi8(_mm_subs_epu8(digits, _mm_set1_epi8(9)), _mm_setzero_si128());
174
+ let letters = _mm_cmpeq_epi8(
175
+ _mm_subs_epu8(letters, _mm_set1_epi8(5)),
176
+ _mm_setzero_si128(),
177
+ );
178
+ _mm_movemask_epi8(_mm_or_si128(digits, letters)) as u16
179
+ };
180
+ valid | separators == u16::MAX
181
+ }
182
+
183
+ #[cfg(all(
184
+ target_arch = "aarch64",
185
+ target_feature = "neon",
186
+ target_endian = "little"
187
+ ))]
188
+ fn hex_block(bytes: &[u8], separators: u16) -> bool {
189
+ use core::arch::aarch64::{
190
+ vcleq_u8, vdupq_n_u8, vget_lane_u64, vld1q_u8, vorrq_u8, vreinterpret_u64_u8,
191
+ vreinterpretq_u16_u8, vshrn_n_u16, vsubq_u8,
192
+ };
193
+ // Each load remains within the sixteen-byte block. Narrowing packs two
194
+ // four-bit flags per byte in little-endian lane order.
195
+ let valid = unsafe {
196
+ let word = vld1q_u8(bytes.as_ptr());
197
+ let digits = vcleq_u8(vsubq_u8(word, vdupq_n_u8(b'0')), vdupq_n_u8(9));
198
+ let letters = vcleq_u8(
199
+ vsubq_u8(vorrq_u8(word, vdupq_n_u8(32)), vdupq_n_u8(b'a')),
200
+ vdupq_n_u8(5),
201
+ );
202
+ let flags = vorrq_u8(digits, letters);
203
+ let packed = vshrn_n_u16::<4>(vreinterpretq_u16_u8(flags));
204
+ vget_lane_u64::<0>(vreinterpret_u64_u8(packed))
205
+ };
206
+ let mut ignored = 0_u64;
207
+ for lane in 0..16 {
208
+ if separators & (1 << lane) != 0 {
209
+ ignored |= 0xf << (lane * 4);
210
+ }
211
+ }
212
+ valid | ignored == u64::MAX
213
+ }
214
+
131
215
  fn relative_pointer(bytes: &[u8]) -> bool {
132
216
  let Some(end) = digits_end(bytes, 0) else {
133
217
  return false;
@@ -355,3 +439,29 @@ fn ipv6(bytes: &[u8]) -> bool {
355
439
  }
356
440
  if compressed { groups < 8 } else { groups == 8 }
357
441
  }
442
+
443
+ #[cfg(test)]
444
+ mod tests {
445
+ use super::*;
446
+
447
+ #[test]
448
+ fn uuid_checks_every_byte_and_separator_without_accepting_non_ascii() {
449
+ let original = *b"123e4567-e89b-12d3-a456-426614174000";
450
+ for at in 0..36 {
451
+ for byte in 0..=255 {
452
+ let mut input = original;
453
+ input[at] = byte;
454
+ let expected = if matches!(at, 8 | 13 | 18 | 23) {
455
+ byte == b'-'
456
+ } else {
457
+ byte.is_ascii_hexdigit()
458
+ };
459
+ assert_eq!(uuid(&input), expected, "position {at}, byte {byte}");
460
+ }
461
+ }
462
+ assert!(!uuid(&original[..35]));
463
+ let mut longer = original.to_vec();
464
+ longer.push(b'0');
465
+ assert!(!uuid(&longer));
466
+ }
467
+ }
@@ -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
+ }