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,240 @@
1
+ //! Exact reachable scope dependencies, shared across schema components.
2
+ use super::*;
3
+ use std::borrow::Cow;
4
+ use std::collections::HashSet;
5
+ use std::sync::Arc;
6
+
7
+ const MAX_NAMES: usize = 65536;
8
+
9
+ #[derive(Default)]
10
+ pub(super) struct Projection {
11
+ names: Vec<Option<Arc<[usize]>>>,
12
+ // If materialized closures exceed the budget, retain the linear dependency
13
+ // graph and compute those exact projections on demand, never all names.
14
+ graph: Option<Vec<Vec<usize>>>,
15
+ }
16
+ impl Projection {
17
+ pub(super) fn compile(edges: &[Vec<usize>], parents: &[Vec<usize>], count: usize) -> Self {
18
+ Self::bounded(edges, parents, count, MAX_NAMES)
19
+ }
20
+ fn bounded(edges: &[Vec<usize>], parents: &[Vec<usize>], count: usize, limit: usize) -> Self {
21
+ let mut seen = vec![false; edges.len()];
22
+ let order = graph::postorder(edges, &mut seen);
23
+ let mut component = vec![usize::MAX; edges.len()];
24
+ let mut members = vec![];
25
+ for root in order.into_iter().rev() {
26
+ if component[root] != usize::MAX {
27
+ continue;
28
+ }
29
+ let id = members.len();
30
+ let mut nodes = vec![];
31
+ let mut stack = vec![root];
32
+ component[root] = id;
33
+ while let Some(node) = stack.pop() {
34
+ nodes.push(node);
35
+ for &parent in &parents[node] {
36
+ if component[parent] == usize::MAX {
37
+ component[parent] = id;
38
+ stack.push(parent);
39
+ }
40
+ }
41
+ }
42
+ members.push(nodes);
43
+ }
44
+ // Kosaraju components are ordered from sources to sinks. Every member
45
+ // of a cycle has the same reachable reference names; solve it once.
46
+ let mut closures: Vec<Option<Arc<[usize]>>> = vec![None; members.len()];
47
+ let mut interned: HashSet<Arc<[usize]>> = HashSet::new();
48
+ let mut slots = 0;
49
+ for (id, nodes) in members.iter().enumerate().rev() {
50
+ let mut names = vec![];
51
+ let mut children = HashSet::new();
52
+ let mut dependencies = HashSet::new();
53
+ let mut complete = true;
54
+ for &node in nodes {
55
+ if node >= count {
56
+ names.push(node - count);
57
+ }
58
+ for &child in &edges[node] {
59
+ let child = component[child];
60
+ if child != id && children.insert(child) {
61
+ match &closures[child] {
62
+ Some(dependency) => {
63
+ if dependencies.insert(dependency.as_ptr()) {
64
+ names.extend(dependency.iter().copied());
65
+ }
66
+ }
67
+ None => complete = false,
68
+ }
69
+ if names.len() > limit {
70
+ names.sort_unstable();
71
+ names.dedup();
72
+ if names.len() > limit {
73
+ complete = false;
74
+ break;
75
+ }
76
+ }
77
+ }
78
+ }
79
+ // Bound temporary concatenation too. Deduplicate overlapping
80
+ // dependencies before choosing the on-demand representation.
81
+ if names.len() > limit {
82
+ names.sort_unstable();
83
+ names.dedup();
84
+ if names.len() > limit {
85
+ complete = false;
86
+ break;
87
+ }
88
+ }
89
+ }
90
+ if !complete {
91
+ continue;
92
+ }
93
+ names.sort_unstable();
94
+ names.dedup();
95
+ if let Some(existing) = interned.get(names.as_slice()) {
96
+ closures[id] = Some(existing.clone());
97
+ } else if names.len() <= limit - slots {
98
+ slots += names.len();
99
+ let names: Arc<[usize]> = names.into();
100
+ interned.insert(names.clone());
101
+ closures[id] = Some(names);
102
+ }
103
+ }
104
+ let names: Vec<_> = component[..count]
105
+ .iter()
106
+ .map(|&id| closures[id].clone())
107
+ .collect();
108
+ let graph = names.iter().any(Option::is_none).then(|| edges.to_vec());
109
+ Self { names, graph }
110
+ }
111
+ pub(super) fn names(
112
+ &self,
113
+ node: usize,
114
+ count: usize,
115
+ anchors: usize,
116
+ guarded: bool,
117
+ budget: &budget::Budget,
118
+ ) -> Option<Cow<'_, [usize]>> {
119
+ if self.names.is_empty() {
120
+ // Hand-built engines in key tests have no compiled dependency graph.
121
+ return Some(Cow::Owned((0..anchors + usize::from(guarded)).collect()));
122
+ }
123
+ if let Some(names) = &self.names[node] {
124
+ return Some(Cow::Borrowed(names));
125
+ }
126
+ let graph = self.graph.as_ref()?;
127
+ let mut seen = vec![false; graph.len()];
128
+ let mut stack = vec![node];
129
+ let mut names = vec![];
130
+ seen[node] = true;
131
+ while let Some(node) = stack.pop() {
132
+ if !budget.checkpoint() {
133
+ return None;
134
+ }
135
+ if node >= count {
136
+ names.push(node - count);
137
+ }
138
+ for &child in &graph[node] {
139
+ if !seen[child] {
140
+ seen[child] = true;
141
+ stack.push(child);
142
+ }
143
+ }
144
+ }
145
+ names.sort_unstable();
146
+ Some(Cow::Owned(names))
147
+ }
148
+ pub(super) fn memory_usage(&self) -> usize {
149
+ let mut seen = HashSet::new();
150
+ self.names.capacity() * std::mem::size_of::<Option<Arc<[usize]>>>()
151
+ + self
152
+ .names
153
+ .iter()
154
+ .flatten()
155
+ .filter(|names| seen.insert(names.as_ptr()))
156
+ .map(|names| (names.len() + 2) * std::mem::size_of::<usize>())
157
+ .sum::<usize>()
158
+ + self.graph.as_ref().map_or(0, |graph| {
159
+ graph.capacity() * std::mem::size_of::<Vec<usize>>()
160
+ + graph
161
+ .iter()
162
+ .map(|row| row.capacity() * std::mem::size_of::<usize>())
163
+ .sum::<usize>()
164
+ })
165
+ }
166
+ }
167
+
168
+ #[cfg(test)]
169
+ mod tests {
170
+ use super::*;
171
+
172
+ fn parents(edges: &[Vec<usize>]) -> Vec<Vec<usize>> {
173
+ let mut parents = vec![vec![]; edges.len()];
174
+ for (node, children) in edges.iter().enumerate() {
175
+ for &child in children {
176
+ parents[child].push(node);
177
+ }
178
+ }
179
+ parents
180
+ }
181
+ #[test]
182
+ fn projects_cycles_and_transitive_dynamic_dispatch_targets() {
183
+ // Four nodes, two dynamic names and the recursive dispatch vertex.
184
+ let edges = vec![
185
+ vec![1],
186
+ vec![0, 4],
187
+ vec![5],
188
+ vec![],
189
+ vec![2],
190
+ vec![3],
191
+ vec![0],
192
+ ];
193
+ for limit in [0, 1, MAX_NAMES] {
194
+ let projection = Projection::bounded(&edges, &parents(&edges), 4, limit);
195
+ let names = |node| {
196
+ projection
197
+ .names(node, 4, 2, true, &budget::Budget::default())
198
+ .unwrap()
199
+ .into_owned()
200
+ };
201
+ assert_eq!(names(0), [0, 1]);
202
+ assert_eq!(names(1), [0, 1]);
203
+ assert_eq!(names(2), [1]);
204
+ assert!(names(3).is_empty());
205
+ }
206
+ }
207
+ #[test]
208
+ fn retains_recursive_dispatch_only_where_reachable() {
209
+ let edges = vec![vec![1], vec![4], vec![3], vec![], vec![0]];
210
+ let projection = Projection::compile(&edges, &parents(&edges), 3);
211
+ let names = |node| {
212
+ projection
213
+ .names(node, 3, 1, true, &budget::Budget::default())
214
+ .unwrap()
215
+ .into_owned()
216
+ };
217
+ assert_eq!(names(0), [1]);
218
+ assert_eq!(names(1), [1]);
219
+ assert_eq!(names(2), [0]);
220
+ }
221
+
222
+ #[test]
223
+ fn shares_equal_closures_and_retains_exact_projection_after_budget_exhaustion() {
224
+ let edges = vec![vec![1, 2], vec![3], vec![3], vec![4], vec![], vec![]];
225
+ let projection = Projection::bounded(&edges, &parents(&edges), 4, MAX_NAMES);
226
+ assert!(projection.graph.is_none());
227
+ assert!(Arc::ptr_eq(
228
+ projection.names[0].as_ref().unwrap(),
229
+ projection.names[3].as_ref().unwrap()
230
+ ));
231
+ let projection = Projection::bounded(&edges, &parents(&edges), 4, 0);
232
+ assert!(projection.graph.is_some());
233
+ assert_eq!(
234
+ &*projection
235
+ .names(0, 4, 1, false, &budget::Budget::default())
236
+ .unwrap(),
237
+ &[0]
238
+ );
239
+ }
240
+ }
@@ -96,14 +96,13 @@ pub(super) fn integer_fingerprint(integer: Value) -> u64 {
96
96
  let mut words = [0_u64; 16];
97
97
  let sign = unsafe { sn_integer1024(integer, words.as_mut_ptr()) };
98
98
  if sign.abs() > 1 {
99
- // No finite Float can equal this magnitude. Mix bounded low words,
100
- // sign, and magnitude size; full equality resolves all collisions,
101
- // including integers that differ only in unsampled high digits.
102
- let len = words
103
- .iter()
104
- .rposition(|&word| word != 0)
105
- .map_or(0, |index| index + 1);
106
- return integer_bucket(&words[..len], sign, unsafe { sn_integer_bytes(integer) });
99
+ // No finite Float can equal this magnitude, so the full native digit
100
+ // hash needs no Float normalization. Sampling only low digits makes
101
+ // mixed/nested uniqueItems quadratic for differences above bit 1024
102
+ // and collapses large enum indexes into a single candidate bucket.
103
+ // The adapter hashes the original Bignum without Ruby dispatch or
104
+ // allocation; exact equality still decides every hash collision.
105
+ return mix(big_only_fingerprint(integer).unwrap() ^ TYPE_NUMBER as u64);
107
106
  }
108
107
  exact_integer_fingerprint(&words, sign)
109
108
  }
@@ -4,29 +4,147 @@
4
4
  use super::*;
5
5
  use regex_automata::{
6
6
  Anchored, Input, MatchKind,
7
- dfa::{Automaton, StartKind, dense},
8
- meta::Regex,
7
+ dfa::{Automaton, OverlappingState, StartKind, dense},
8
+ meta::{Cache, Regex},
9
9
  nfa::thompson,
10
10
  };
11
11
  use regex_syntax::ast::{Ast, RepetitionKind, RepetitionRange};
12
12
 
13
+ // Each validator owns its search caches. Compiled regex/DFA data stays shared.
14
+ #[derive(Default)]
15
+ pub struct Caches(std::cell::RefCell<Vec<Option<Box<Cache>>>>);
16
+ impl Caches {
17
+ fn is_match(&self, index: usize, regex: &Regex, bytes: &[u8]) -> bool {
18
+ // Each runner is tied to one engine, whose regex indices are fixed at
19
+ // compilation. Allocate only the caches this validator actually uses.
20
+ let mut caches = self.0.borrow_mut();
21
+ if caches.len() <= index {
22
+ caches.resize_with(index + 1, || None);
23
+ }
24
+ let cache = caches[index].get_or_insert_with(|| Box::new(regex.create_cache()));
25
+ regex
26
+ .search_half_with(cache, &Input::new(bytes).earliest(true))
27
+ .is_some()
28
+ }
29
+ }
30
+ #[unsafe(no_mangle)]
31
+ pub extern "C" fn sn_caches_new() -> *mut Caches {
32
+ Box::into_raw(Box::new(Caches::default()))
33
+ }
34
+ /// # Safety
35
+ /// Caches must be owned, returned by sn_caches_new, and freed exactly once.
36
+ #[unsafe(no_mangle)]
37
+ pub unsafe extern "C" fn sn_caches_free(caches: *mut Caches) {
38
+ drop(unsafe { Box::from_raw(caches) });
39
+ }
40
+ /// # Safety
41
+ /// Caches must be live; call under the GVL or while all Ractors are stopped.
42
+ #[unsafe(no_mangle)]
43
+ pub unsafe extern "C" fn sn_caches_size(caches: *const Caches) -> usize {
44
+ // GC may inspect a stopped runner while its search has borrowed a cache.
45
+ let Ok(caches) = (unsafe { &*caches }).0.try_borrow() else {
46
+ return std::mem::size_of::<Caches>();
47
+ };
48
+ std::mem::size_of::<Caches>()
49
+ + caches.capacity() * std::mem::size_of::<Option<Box<Cache>>>()
50
+ + caches
51
+ .iter()
52
+ .flatten()
53
+ .map(|cache| std::mem::size_of::<Cache>() + cache.memory_usage())
54
+ .sum::<usize>()
55
+ }
56
+
13
57
  #[derive(Debug)]
14
58
  pub(super) enum Pattern {
15
59
  Unsupported,
16
60
  Dfa(Box<dense::DFA<Vec<u32>>>),
17
- Compiled(Regex),
61
+ Compiled { regex: Regex, index: usize },
62
+ }
63
+
64
+ // Preserve every overlapping match while scanning a key with one automaton.
65
+ // Keep individual matchers for unsupported compilation/search configurations.
66
+ #[derive(Debug)]
67
+ pub(super) struct PatternGroup {
68
+ dfa: dense::DFA<Vec<u32>>,
69
+ all: u64,
70
+ }
71
+ impl PatternGroup {
72
+ pub(super) fn compile(names: &[Value], patterns: &[(Pattern, usize)]) -> Option<Self> {
73
+ if !(2..=64).contains(&patterns.len())
74
+ || !patterns
75
+ .iter()
76
+ .all(|(pattern, _)| matches!(pattern, Pattern::Dfa(_)))
77
+ {
78
+ return None;
79
+ }
80
+ // Existing DFA compilation already proved syntax, encoding, compatibility
81
+ // and full start anchoring. Reuse exactly the same translation.
82
+ let sources: Option<Vec<_>> = names
83
+ .iter()
84
+ .map(|&name| {
85
+ std::str::from_utf8(unsafe { string_bytes(name) })
86
+ .ok()
87
+ .and_then(translate)
88
+ })
89
+ .collect();
90
+ let dfa = dense::Builder::new()
91
+ .configure(
92
+ dense::Config::new()
93
+ .match_kind(MatchKind::All)
94
+ .start_kind(StartKind::Anchored)
95
+ .dfa_size_limit(Some(1 << 20))
96
+ .determinize_size_limit(Some(1 << 22)),
97
+ )
98
+ .thompson(thompson::Config::new().nfa_size_limit(Some(1 << 20)))
99
+ .build_many(&sources?)
100
+ .ok()?;
101
+ let all = u64::MAX >> (64 - patterns.len());
102
+ Some(Self { dfa, all })
103
+ }
104
+ pub(super) fn matches(&self, value: Value) -> Option<u64> {
105
+ let mut data = std::ptr::null();
106
+ let mut len = 0;
107
+ let encoding = unsafe { sn_pattern_view(value, &mut data, &mut len) };
108
+ if encoding == ENCODING_OTHER {
109
+ return None;
110
+ }
111
+ let bytes = unsafe { byte_view(data, len) };
112
+ if !valid_encoding(bytes, encoding) {
113
+ return None;
114
+ }
115
+ let input = Input::new(bytes).anchored(Anchored::Yes);
116
+ let mut state = OverlappingState::start();
117
+ let mut matches = 0;
118
+ loop {
119
+ self.dfa
120
+ .try_search_overlapping_fwd(&input, &mut state)
121
+ .ok()?;
122
+ let Some(found) = state.get_match() else {
123
+ break;
124
+ };
125
+ matches |= 1_u64 << found.pattern().as_usize();
126
+ if matches == self.all {
127
+ break;
128
+ }
129
+ }
130
+ Some(matches)
131
+ }
132
+ pub(super) fn memory_usage(&self) -> usize {
133
+ self.dfa.memory_usage()
134
+ }
18
135
  }
19
136
 
20
137
  impl Pattern {
21
138
  // Select the crate matcher once for a homogeneous array. The borrowed
22
139
  // Ruby slices are used synchronously; classification and encoding are
23
140
  // still checked on every element, including non-string instances.
24
- pub(super) fn eval_items(&self, required: i32, values: &[Value]) -> Verdict {
25
- self.visit_invalid_items(required, values, |_, _| Invalid)
141
+ pub(super) fn eval_items(&self, caches: &Caches, required: i32, values: &[Value]) -> Verdict {
142
+ self.visit_invalid_items(caches, required, values, |_, _| Invalid)
26
143
  }
27
144
 
28
145
  pub(super) fn visit_invalid_items(
29
146
  &self,
147
+ caches: &Caches,
30
148
  required: i32,
31
149
  values: &[Value],
32
150
  invalid: impl FnMut(usize, Value) -> Verdict,
@@ -39,8 +157,8 @@ impl Pattern {
39
157
  Err(_) => Unsupported,
40
158
  }
41
159
  }),
42
- Self::Compiled(regex) => scan_items(required, values, invalid, |bytes| {
43
- verdict(regex.is_match(bytes))
160
+ Self::Compiled { regex, index } => scan_items(required, values, invalid, |bytes| {
161
+ verdict(caches.is_match(*index, regex, bytes))
44
162
  }),
45
163
  Self::Unsupported => scan_items(required, values, invalid, |_| Unsupported),
46
164
  }
@@ -49,12 +167,12 @@ impl Pattern {
49
167
  pub(super) fn memory_usage(&self) -> usize {
50
168
  match self {
51
169
  Self::Dfa(dfa) => std::mem::size_of_val(&**dfa) + dfa.memory_usage(),
52
- Self::Compiled(regex) => regex.memory_usage(),
170
+ Self::Compiled { regex, .. } => regex.memory_usage(),
53
171
  Self::Unsupported => 0,
54
172
  }
55
173
  }
56
174
 
57
- pub(super) fn compile(value: Value) -> Self {
175
+ pub(super) fn compile(value: Value, cache_count: &mut usize) -> Self {
58
176
  if unsafe { sn_kind(value) } != TYPE_STRING {
59
177
  return Self::Unsupported;
60
178
  }
@@ -105,11 +223,15 @@ impl Pattern {
105
223
  .hybrid_cache_capacity(1 << 20),
106
224
  )
107
225
  .build(&source)
108
- .map(Self::Compiled)
226
+ .map(|regex| {
227
+ let index = *cache_count;
228
+ *cache_count += 1;
229
+ Self::Compiled { regex, index }
230
+ })
109
231
  .unwrap_or(Self::Unsupported)
110
232
  }
111
233
 
112
- pub(super) fn eval(&self, value: Value) -> Verdict {
234
+ pub(super) fn eval(&self, caches: &Caches, value: Value) -> Verdict {
113
235
  if matches!(self, Self::Unsupported) {
114
236
  return Unsupported;
115
237
  }
@@ -120,17 +242,14 @@ impl Pattern {
120
242
  return Unsupported;
121
243
  }
122
244
  let bytes = unsafe { std::slice::from_raw_parts(data, len) };
123
- self.eval_bytes(bytes, encoding)
245
+ self.eval_bytes(caches, bytes, encoding)
124
246
  }
125
247
 
126
- pub(super) fn eval_bytes(&self, bytes: &[u8], encoding: i32) -> Verdict {
248
+ pub(super) fn eval_bytes(&self, caches: &Caches, bytes: &[u8], encoding: i32) -> Verdict {
127
249
  if encoding == ENCODING_OTHER || matches!(self, Self::Unsupported) {
128
250
  return Unsupported;
129
251
  }
130
- if encoding != ENCODING_SEVEN_BIT
131
- && ((encoding == ENCODING_ASCII && !bytes.is_ascii())
132
- || std::str::from_utf8(bytes).is_err())
133
- {
252
+ if !valid_encoding(bytes, encoding) {
134
253
  return Unsupported;
135
254
  }
136
255
  let matched = match self {
@@ -140,13 +259,27 @@ impl Pattern {
140
259
  Ok(matched) => matched.is_some(),
141
260
  Err(_) => return Unsupported,
142
261
  },
143
- Self::Compiled(regex) => regex.is_match(bytes),
262
+ Self::Compiled { regex, index } => caches.is_match(*index, regex, bytes),
144
263
  Self::Unsupported => return Unsupported,
145
264
  };
146
265
  if matched { Valid } else { Invalid }
147
266
  }
148
267
  }
149
268
 
269
+ // Keep short checks in std and skip scanning when Ruby already proved ASCII.
270
+ // The compatibility SIMD API retains early rejection of malformed UTF-8.
271
+ fn valid_encoding(bytes: &[u8], encoding: i32) -> bool {
272
+ if encoding == ENCODING_SEVEN_BIT {
273
+ true
274
+ } else if encoding == ENCODING_ASCII {
275
+ bytes.is_ascii()
276
+ } else if bytes.len() < 64 {
277
+ std::str::from_utf8(bytes).is_ok()
278
+ } else {
279
+ simdutf8::compat::from_utf8(bytes).is_ok()
280
+ }
281
+ }
282
+
150
283
  fn scan_items(
151
284
  required: i32,
152
285
  values: &[Value],
@@ -173,10 +306,7 @@ fn scan_items(
173
306
  return Unsupported;
174
307
  }
175
308
  let bytes = unsafe { std::slice::from_raw_parts(data, len) };
176
- if encoding != ENCODING_SEVEN_BIT
177
- && ((encoding == ENCODING_ASCII && !bytes.is_ascii())
178
- || std::str::from_utf8(bytes).is_err())
179
- {
309
+ if !valid_encoding(bytes, encoding) {
180
310
  return Unsupported;
181
311
  }
182
312
  let result = matches(bytes);
@@ -286,3 +416,42 @@ fn compatible(ast: &Ast) -> bool {
286
416
  _ => true,
287
417
  }
288
418
  }
419
+
420
+ #[cfg(test)]
421
+ mod tests {
422
+ use super::*;
423
+
424
+ #[test]
425
+ fn encoding_validation_matches_std_across_simd_blocks_and_tails() {
426
+ let original = "日𐀀é".repeat(32).into_bytes();
427
+ for length in [63, 64, 65, 127, 128, 129, original.len()] {
428
+ for at in 0..length {
429
+ for byte in 0..=255 {
430
+ let mut input = original[..length].to_vec();
431
+ input[at] = byte;
432
+ assert_eq!(
433
+ valid_encoding(&input, ENCODING_UTF8),
434
+ std::str::from_utf8(&input).is_ok(),
435
+ "length {length}, position {at}, byte {byte}"
436
+ );
437
+ assert_eq!(valid_encoding(&input, ENCODING_ASCII), input.is_ascii());
438
+ }
439
+ }
440
+ }
441
+ assert!(valid_encoding(&original, ENCODING_UTF8));
442
+ assert!(valid_encoding(b"already classified", ENCODING_SEVEN_BIT));
443
+ }
444
+
445
+ #[test]
446
+ fn reports_memory_when_a_search_cache_is_borrowed() {
447
+ let caches = Caches::default();
448
+ let regex = Regex::new("value[0-9]+").unwrap();
449
+ assert!(caches.is_match(0, &regex, b"value1"));
450
+ assert!(unsafe { sn_caches_size(&caches) } > std::mem::size_of::<Caches>());
451
+ let _search = caches.0.borrow_mut();
452
+ assert_eq!(
453
+ unsafe { sn_caches_size(&caches) },
454
+ std::mem::size_of::<Caches>()
455
+ );
456
+ }
457
+ }