schemurai 3.0.0 → 3.0.2

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -4
  3. data/ext/schemurai_native/Cargo.lock +55 -1
  4. data/ext/schemurai_native/Cargo.toml +4 -1
  5. data/ext/schemurai_native/extconf.rb +1 -1
  6. data/ext/schemurai_native/glue.c +1679 -62
  7. data/ext/schemurai_native/protocol.def +45 -0
  8. data/ext/schemurai_native/src/budget.rs +49 -0
  9. data/ext/schemurai_native/src/container.rs +460 -0
  10. data/ext/schemurai_native/src/content.rs +74 -27
  11. data/ext/schemurai_native/src/diagnostics/array.rs +392 -0
  12. data/ext/schemurai_native/src/diagnostics/object.rs +218 -0
  13. data/ext/schemurai_native/src/diagnostics.rs +1087 -307
  14. data/ext/schemurai_native/src/enumeration.rs +6 -1
  15. data/ext/schemurai_native/src/evaluation.rs +49 -20
  16. data/ext/schemurai_native/src/format.rs +126 -14
  17. data/ext/schemurai_native/src/graph.rs +37 -0
  18. data/ext/schemurai_native/src/instance_graph.rs +90 -0
  19. data/ext/schemurai_native/src/integer_scan.rs +132 -0
  20. data/ext/schemurai_native/src/lib.rs +917 -238
  21. data/ext/schemurai_native/src/memo.rs +11 -38
  22. data/ext/schemurai_native/src/number/arbitrary.rs +154 -0
  23. data/ext/schemurai_native/src/number.rs +250 -59
  24. data/ext/schemurai_native/src/pattern.rs +147 -109
  25. data/ext/schemurai_native/src/pointer.rs +97 -0
  26. data/ext/schemurai_native/src/runtime/properties.rs +162 -0
  27. data/ext/schemurai_native/src/runtime/reports.rs +266 -0
  28. data/ext/schemurai_native/src/runtime/scopes.rs +430 -0
  29. data/ext/schemurai_native/src/runtime.rs +1020 -0
  30. data/ext/schemurai_native/src/scope.rs +60 -16
  31. data/ext/schemurai_native/src/string.rs +68 -0
  32. data/ext/schemurai_native/src/tracking.rs +244 -174
  33. data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
  34. data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
  35. data/lib/schemurai/container_checks.rb +534 -0
  36. data/lib/schemurai/decimal.rb +11 -0
  37. data/lib/schemurai/dialect.rb +0 -12
  38. data/lib/schemurai/ecma_regexp.rb +46 -0
  39. data/lib/schemurai/evaluation_pool.rb +66 -0
  40. data/lib/schemurai/evaluator.rb +9 -48
  41. data/lib/schemurai/json_pointer.rb +11 -0
  42. data/lib/schemurai/native/diagnostics.rb +4 -3
  43. data/lib/schemurai/native/evaluator.rb +38 -10
  44. data/lib/schemurai/native/null_numeric.rb +23 -0
  45. data/lib/schemurai/number_checks.rb +128 -0
  46. data/lib/schemurai/string_checks.rb +166 -0
  47. data/lib/schemurai/type_checks.rb +59 -0
  48. data/lib/schemurai/value_equality.rb +72 -0
  49. data/lib/schemurai/version.rb +1 -1
  50. data/lib/schemurai/vm/compiler.rb +18 -26
  51. data/lib/schemurai/vm/evaluator.rb +87 -545
  52. data/lib/schemurai/vm/limited_evaluator.rb +7 -22
  53. metadata +25 -2
@@ -7,13 +7,14 @@ const MAX_INDEX_BYTES: usize = 64 * 1024 * 1024;
7
7
 
8
8
  #[derive(Debug)]
9
9
  pub(super) struct Enumeration {
10
+ source: Value,
10
11
  values: Vec<Value>,
11
12
  index: Option<Index>,
12
13
  kinds: i32,
13
14
  }
14
15
 
15
16
  impl Enumeration {
16
- pub(super) fn compile(values: Vec<Value>, fixnum_flag: Value) -> Self {
17
+ pub(super) fn compile(source: Value, values: Vec<Value>, fixnum_flag: Value) -> Self {
17
18
  let mut fingerprints = structural::Cache::batch(0);
18
19
  let budget = budget::Budget::default();
19
20
  let index = (values.len() > LINEAR_LIMIT)
@@ -31,12 +32,16 @@ impl Enumeration {
31
32
  0
32
33
  };
33
34
  Self {
35
+ source,
34
36
  values,
35
37
  index,
36
38
  kinds,
37
39
  }
38
40
  }
39
41
 
42
+ pub(super) fn runtime(&self, caches: &runtime::Caches, value: Value) -> Verdict {
43
+ caches.equality(self.source, value, RUNTIME_ENUM)
44
+ }
40
45
  pub(super) fn eval(
41
46
  &self,
42
47
  value: Value,
@@ -42,7 +42,6 @@ impl BranchPolicy {
42
42
  pub(super) trait BranchResult {
43
43
  fn valid() -> Self;
44
44
  fn status(&self) -> Verdict;
45
- fn merge(&mut self, branch: Self);
46
45
  }
47
46
  impl BranchResult for Verdict {
48
47
  fn valid() -> Self {
@@ -51,7 +50,6 @@ impl BranchResult for Verdict {
51
50
  fn status(&self) -> Verdict {
52
51
  *self
53
52
  }
54
- fn merge(&mut self, _: Self) {}
55
53
  }
56
54
  impl BranchResult for Tracked {
57
55
  fn valid() -> Self {
@@ -60,9 +58,6 @@ impl BranchResult for Tracked {
60
58
  fn status(&self) -> Verdict {
61
59
  self.status
62
60
  }
63
- fn merge(&mut self, branch: Self) {
64
- self.merge(branch);
65
- }
66
61
  }
67
62
 
68
63
  pub(super) struct BranchMatches<R> {
@@ -72,33 +67,44 @@ pub(super) struct BranchMatches<R> {
72
67
  pub(super) fn scan_branches<R: BranchResult>(
73
68
  nodes: &[(usize, usize)],
74
69
  policy: BranchPolicy,
70
+ separate_calls: bool,
75
71
  mut evaluate: impl FnMut(usize) -> R,
72
+ mut merge: impl FnMut(&mut R, R) -> Result<(), Verdict>,
76
73
  ) -> Result<BranchMatches<R>, Verdict> {
77
74
  let mut result = BranchMatches {
78
75
  matches: 0,
79
76
  coverage: R::valid(),
80
77
  };
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);
78
+ 'groups: for &(node, multiplicity) in nodes {
79
+ for _ in 0..if separate_calls { multiplicity } else { 1 } {
80
+ let branch = evaluate(node);
81
+ match branch.status() {
82
+ Unsupported => return Err(Unsupported),
83
+ Invalid => {}
84
+ Valid => {
85
+ result.matches += if separate_calls { 1 } else { multiplicity };
86
+ merge(&mut result.coverage, branch)?;
87
+ }
88
+ }
89
+ if policy.finished(result.matches) {
90
+ break 'groups;
89
91
  }
90
- }
91
- if policy.finished(result.matches) {
92
- break;
93
92
  }
94
93
  }
95
94
  Ok(result)
96
95
  }
97
96
 
98
- // Diagnostic counts and annotations need every match. Keep the same complete
99
- // traversal for ordinary validity so unsupported later items retain fallback.
97
+ #[derive(Clone, Copy)]
98
+ pub(super) enum ContainsPolicy {
99
+ FirstMatch,
100
+ AllMatches,
101
+ }
102
+
103
+ // Older dialects use any? for untracked validity; counted validity,
104
+ // diagnostic counts and annotations need every match.
100
105
  pub(super) fn scan_contains(
101
106
  len: usize,
107
+ policy: ContainsPolicy,
102
108
  mut evaluate: impl FnMut(usize) -> Verdict,
103
109
  mut record: impl FnMut(usize),
104
110
  ) -> Result<usize, Verdict> {
@@ -110,6 +116,9 @@ pub(super) fn scan_contains(
110
116
  Valid => {
111
117
  matches += 1;
112
118
  record(index);
119
+ if matches!(policy, ContainsPolicy::FirstMatch) {
120
+ break;
121
+ }
113
122
  }
114
123
  }
115
124
  }
@@ -184,7 +193,14 @@ mod tests {
184
193
  let nodes = [(0, 2), (1, 1)];
185
194
  let evaluate = |node| if node == 0 { Valid } else { Unsupported };
186
195
  for kind in [BranchKind::Any, BranchKind::One] {
187
- let result = scan_branches(&nodes, BranchPolicy::Validity(kind), evaluate).unwrap();
196
+ let result = scan_branches(
197
+ &nodes,
198
+ BranchPolicy::Validity(kind),
199
+ false,
200
+ evaluate,
201
+ |_, _| Ok(()),
202
+ )
203
+ .unwrap();
188
204
  assert_eq!(result.matches, 2);
189
205
  assert_eq!(
190
206
  kind.accepts(result.matches),
@@ -192,7 +208,9 @@ mod tests {
192
208
  );
193
209
  }
194
210
  assert!(matches!(
195
- scan_branches(&nodes, BranchPolicy::Complete, evaluate),
211
+ scan_branches(&nodes, BranchPolicy::Complete, false, evaluate, |_, _| Ok(
212
+ ()
213
+ )),
196
214
  Err(Unsupported)
197
215
  ));
198
216
  }
@@ -202,6 +220,7 @@ mod tests {
202
220
  let mut recorded = vec![];
203
221
  let result = scan_contains(
204
222
  4,
223
+ ContainsPolicy::AllMatches,
205
224
  |index| verdict(index % 2 == 0),
206
225
  |index| recorded.push(index),
207
226
  );
@@ -210,6 +229,16 @@ mod tests {
210
229
  assert_eq!(
211
230
  scan_contains(
212
231
  2,
232
+ ContainsPolicy::FirstMatch,
233
+ |index| if index == 0 { Valid } else { Unsupported },
234
+ |_| {}
235
+ ),
236
+ Ok(1)
237
+ );
238
+ assert_eq!(
239
+ scan_contains(
240
+ 2,
241
+ ContainsPolicy::AllMatches,
213
242
  |index| if index == 0 { Valid } else { Unsupported },
214
243
  |_| {}
215
244
  ),
@@ -51,9 +51,6 @@ impl Format {
51
51
  Self::Unsupported => return Unsupported,
52
52
  })
53
53
  }
54
- pub(super) fn eval_items(&self, required: i32, values: &[Value]) -> Verdict {
55
- self.visit_invalid_items(required, values, |_, _| Invalid)
56
- }
57
54
  pub(super) fn visit_invalid_items(
58
55
  &self,
59
56
  required: i32,
@@ -91,9 +88,14 @@ fn scan_items(
91
88
  let mut data = std::ptr::null();
92
89
  let mut len = 0;
93
90
  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 {
91
+ if kind == -TYPE_UNSUPPORTED {
92
+ let status = invalid(index, value);
93
+ if status != Valid {
94
+ return status;
95
+ }
96
+ continue;
97
+ }
98
+ let result = if kind < 0 {
97
99
  verdict(required == 0)
98
100
  } else {
99
101
  matches(unsafe { byte_view(data, len) })
@@ -118,16 +120,100 @@ fn date_exact(bytes: &[u8]) -> bool {
118
120
  fn date_time(bytes: &[u8]) -> bool {
119
121
  bytes.len() >= 20 && date(bytes) && matches!(bytes[10], b'T' | b't') && time(&bytes[11..])
120
122
  }
123
+ // Keep the SIMD body out of the shared format dispatchers. Local x64 benchmarks
124
+ // and disassembly showed that inlining it shifted the unchanged JSON Pointer
125
+ // loop into a slower instruction layout; outlining retains the UUID SIMD gain.
126
+ #[inline(never)]
121
127
  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
- })
128
+ if bytes.len() != 36 || [8, 13, 18, 23].iter().any(|&at| bytes[at] != b'-') {
129
+ return false;
130
+ }
131
+ #[cfg(any(
132
+ all(target_arch = "x86_64", target_feature = "sse2"),
133
+ all(
134
+ target_arch = "aarch64",
135
+ target_feature = "neon",
136
+ target_endian = "little"
137
+ )
138
+ ))]
139
+ {
140
+ // The first two vectors contain all four separators. Their exact
141
+ // positions were checked above; only those lanes may bypass hex checks.
142
+ hex_block(&bytes[..16], (1 << 8) | (1 << 13))
143
+ && hex_block(&bytes[16..32], (1 << 2) | (1 << 7))
144
+ && bytes[32..].iter().all(u8::is_ascii_hexdigit)
145
+ }
146
+ #[cfg(not(any(
147
+ all(target_arch = "x86_64", target_feature = "sse2"),
148
+ all(
149
+ target_arch = "aarch64",
150
+ target_feature = "neon",
151
+ target_endian = "little"
152
+ )
153
+ )))]
154
+ bytes
155
+ .iter()
156
+ .enumerate()
157
+ .all(|(index, byte)| matches!(index, 8 | 13 | 18 | 23) || byte.is_ascii_hexdigit())
158
+ }
159
+
160
+ #[cfg(all(target_arch = "x86_64", target_feature = "sse2"))]
161
+ fn hex_block(bytes: &[u8], separators: u16) -> bool {
162
+ use core::arch::x86_64::{
163
+ _mm_cmpeq_epi8, _mm_loadu_si128, _mm_movemask_epi8, _mm_or_si128, _mm_set1_epi8,
164
+ _mm_setzero_si128, _mm_sub_epi8, _mm_subs_epu8,
165
+ };
166
+ // The caller supplies sixteen readable bytes. SSE2 is a target baseline;
167
+ // unsigned wrapping subtraction rejects bytes below the ASCII ranges too.
168
+ let valid = unsafe {
169
+ let word = _mm_loadu_si128(bytes.as_ptr().cast());
170
+ let digits = _mm_sub_epi8(word, _mm_set1_epi8(b'0' as i8));
171
+ let letters = _mm_sub_epi8(
172
+ _mm_or_si128(word, _mm_set1_epi8(32)),
173
+ _mm_set1_epi8(b'a' as i8),
174
+ );
175
+ let digits = _mm_cmpeq_epi8(_mm_subs_epu8(digits, _mm_set1_epi8(9)), _mm_setzero_si128());
176
+ let letters = _mm_cmpeq_epi8(
177
+ _mm_subs_epu8(letters, _mm_set1_epi8(5)),
178
+ _mm_setzero_si128(),
179
+ );
180
+ _mm_movemask_epi8(_mm_or_si128(digits, letters)) as u16
181
+ };
182
+ valid | separators == u16::MAX
130
183
  }
184
+
185
+ #[cfg(all(
186
+ target_arch = "aarch64",
187
+ target_feature = "neon",
188
+ target_endian = "little"
189
+ ))]
190
+ fn hex_block(bytes: &[u8], separators: u16) -> bool {
191
+ use core::arch::aarch64::{
192
+ vcleq_u8, vdupq_n_u8, vget_lane_u64, vld1q_u8, vorrq_u8, vreinterpret_u64_u8,
193
+ vreinterpretq_u16_u8, vshrn_n_u16, vsubq_u8,
194
+ };
195
+ // Each load remains within the sixteen-byte block. Narrowing packs two
196
+ // four-bit flags per byte in little-endian lane order.
197
+ let valid = unsafe {
198
+ let word = vld1q_u8(bytes.as_ptr());
199
+ let digits = vcleq_u8(vsubq_u8(word, vdupq_n_u8(b'0')), vdupq_n_u8(9));
200
+ let letters = vcleq_u8(
201
+ vsubq_u8(vorrq_u8(word, vdupq_n_u8(32)), vdupq_n_u8(b'a')),
202
+ vdupq_n_u8(5),
203
+ );
204
+ let flags = vorrq_u8(digits, letters);
205
+ let packed = vshrn_n_u16::<4>(vreinterpretq_u16_u8(flags));
206
+ vget_lane_u64::<0>(vreinterpret_u64_u8(packed))
207
+ };
208
+ let mut ignored = 0_u64;
209
+ for lane in 0..16 {
210
+ if separators & (1 << lane) != 0 {
211
+ ignored |= 0xf << (lane * 4);
212
+ }
213
+ }
214
+ valid | ignored == u64::MAX
215
+ }
216
+
131
217
  fn relative_pointer(bytes: &[u8]) -> bool {
132
218
  let Some(end) = digits_end(bytes, 0) else {
133
219
  return false;
@@ -355,3 +441,29 @@ fn ipv6(bytes: &[u8]) -> bool {
355
441
  }
356
442
  if compressed { groups < 8 } else { groups == 8 }
357
443
  }
444
+
445
+ #[cfg(test)]
446
+ mod tests {
447
+ use super::*;
448
+
449
+ #[test]
450
+ fn uuid_checks_every_byte_and_separator_without_accepting_non_ascii() {
451
+ let original = *b"123e4567-e89b-12d3-a456-426614174000";
452
+ for at in 0..36 {
453
+ for byte in 0..=255 {
454
+ let mut input = original;
455
+ input[at] = byte;
456
+ let expected = if matches!(at, 8 | 13 | 18 | 23) {
457
+ byte == b'-'
458
+ } else {
459
+ byte.is_ascii_hexdigit()
460
+ };
461
+ assert_eq!(uuid(&input), expected, "position {at}, byte {byte}");
462
+ }
463
+ }
464
+ assert!(!uuid(&original[..35]));
465
+ let mut longer = original.to_vec();
466
+ longer.push(b'0');
467
+ assert!(!uuid(&longer));
468
+ }
469
+ }
@@ -26,6 +26,34 @@ pub(super) fn postorder(edges: &[Vec<usize>], seen: &mut [bool]) -> Vec<usize> {
26
26
  order
27
27
  }
28
28
 
29
+ pub(super) fn parents(edges: &[Vec<usize>]) -> Vec<Vec<usize>> {
30
+ let mut parents = vec![vec![]; edges.len()];
31
+ for (parent, children) in edges.iter().enumerate() {
32
+ for &child in children {
33
+ parents[child].push(parent);
34
+ }
35
+ }
36
+ parents
37
+ }
38
+
39
+ // Reachability flags flow from children to parents, including through cycles.
40
+ // Each newly marked node enters the queue once.
41
+ pub(super) fn propagate(flags: &mut [bool], parents: &[Vec<usize>]) {
42
+ let mut queue: std::collections::VecDeque<_> = flags
43
+ .iter()
44
+ .enumerate()
45
+ .filter_map(|(node, &set)| set.then_some(node))
46
+ .collect();
47
+ while let Some(node) = queue.pop_front() {
48
+ for &parent in &parents[node] {
49
+ if !flags[parent] {
50
+ flags[parent] = true;
51
+ queue.push_back(parent);
52
+ }
53
+ }
54
+ }
55
+ }
56
+
29
57
  #[cfg(test)]
30
58
  mod tests {
31
59
  use super::*;
@@ -39,6 +67,15 @@ mod tests {
39
67
  assert!(postorder(&[], &mut []).is_empty());
40
68
  }
41
69
 
70
+ #[test]
71
+ fn flags_propagate_through_cycles_without_marking_disconnected_nodes() {
72
+ let parents = vec![vec![2], vec![0], vec![1, 3], vec![], vec![4]];
73
+ let mut flags = vec![false, true, false, false, false];
74
+ propagate(&mut flags, &parents);
75
+ assert_eq!(flags, [true, true, true, true, false]);
76
+ propagate(&mut [], &[]);
77
+ }
78
+
42
79
  #[test]
43
80
  fn deep_chains_use_an_explicit_stack() {
44
81
  let count = 100_000;
@@ -82,3 +82,93 @@ unsafe extern "C" fn entry(context: *mut c_void, _key: Value, value: Value) -> i
82
82
  }))
83
83
  .unwrap_or(VERDICT_INVALID)
84
84
  }
85
+
86
+ enum Work {
87
+ Visit(Value),
88
+ Finish(Value),
89
+ }
90
+
91
+ pub(super) enum Safety {
92
+ Ordinary,
93
+ Cyclic,
94
+ Unsupported,
95
+ }
96
+
97
+ // A failed fingerprint may indicate either an optimization bound or Ruby
98
+ // behavior that native equality must not skip. Classify every reachable value
99
+ // before substituting direct comparisons or a stack error for hashing. Continue
100
+ // after detecting a cycle: any special value still requires Ruby's behavior.
101
+ // Unlike the memoization hint above, this rare path needs no depth/visit cap.
102
+ pub(super) fn ordinary_graph(value: Value, budget: &budget::Budget) -> Safety {
103
+ let mut work = vec![Work::Visit(value)];
104
+ let mut states = HashMap::new();
105
+ let mut cyclic = false;
106
+ while let Some(task) = work.pop() {
107
+ if !budget.checkpoint() {
108
+ return Safety::Unsupported;
109
+ }
110
+ match task {
111
+ Work::Finish(value) => {
112
+ states.insert(value, true);
113
+ }
114
+ Work::Visit(value) => {
115
+ if let Some(&done) = states.get(&value) {
116
+ if !done {
117
+ cyclic = true;
118
+ }
119
+ continue;
120
+ }
121
+ match unsafe { sn_kind(value) } {
122
+ TYPE_UNSUPPORTED => return Safety::Unsupported,
123
+ TYPE_ARRAY => {
124
+ states.insert(value, false);
125
+ work.push(Work::Finish(value));
126
+ for i in (0..unsafe { sn_len(value) }).rev() {
127
+ work.push(Work::Visit(unsafe { sn_entry(value, i) }));
128
+ }
129
+ }
130
+ TYPE_OBJECT => {
131
+ states.insert(value, false);
132
+ work.push(Work::Finish(value));
133
+ let mut context = OrdinaryWalk {
134
+ work: &mut work,
135
+ budget,
136
+ };
137
+ if unsafe {
138
+ sn_hash_each(
139
+ value,
140
+ (&mut context as *mut OrdinaryWalk<'_>).cast(),
141
+ ordinary_entry,
142
+ )
143
+ } != VERDICT_VALID
144
+ {
145
+ return Safety::Unsupported;
146
+ }
147
+ }
148
+ _ => {}
149
+ }
150
+ }
151
+ }
152
+ }
153
+ if cyclic {
154
+ Safety::Cyclic
155
+ } else {
156
+ Safety::Ordinary
157
+ }
158
+ }
159
+
160
+ struct OrdinaryWalk<'a> {
161
+ work: &'a mut Vec<Work>,
162
+ budget: &'a budget::Budget,
163
+ }
164
+ unsafe extern "C" fn ordinary_entry(context: *mut c_void, _: Value, value: Value) -> i32 {
165
+ catch_unwind(AssertUnwindSafe(|| {
166
+ let context = unsafe { &mut *context.cast::<OrdinaryWalk<'_>>() };
167
+ if !context.budget.checkpoint() {
168
+ return VERDICT_UNSUPPORTED;
169
+ }
170
+ context.work.push(Work::Visit(value));
171
+ VERDICT_VALID
172
+ }))
173
+ .unwrap_or(VERDICT_UNSUPPORTED)
174
+ }
@@ -0,0 +1,132 @@
1
+ //! Validate complete blocks of borrowed, header-verified immediate integers.
2
+ //! Stop before any uncertain block so the ordinary evaluator retains ordering.
3
+ use super::Value;
4
+
5
+ pub(super) fn valid_prefix(values: &[Value], min: i64, max: i64, fixnum_flag: Value) -> usize {
6
+ if fixnum_flag != 1 || values.len() < 16 {
7
+ return 0;
8
+ }
9
+ // Comparing tagged values preserves signed integer order. Clamp the
10
+ // interval before tagging, since some schema limits exceed Fixnum range.
11
+ let min = min.max(-(1_i64 << 62));
12
+ let max = max.min((1_i64 << 62) - 1);
13
+ if min > max {
14
+ return 0;
15
+ }
16
+ let min = (min << 1) | 1;
17
+ let max = (max << 1) | 1;
18
+ #[cfg(target_arch = "x86_64")]
19
+ if std::is_x86_feature_detected!("avx2") {
20
+ // Detection includes OS support. The kernel only loads full blocks.
21
+ return unsafe { avx2_prefix(values, min, max) };
22
+ }
23
+ #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
24
+ return neon_prefix(values, min, max);
25
+ #[allow(unreachable_code)]
26
+ {
27
+ let _ = (min, max);
28
+ 0
29
+ }
30
+ }
31
+
32
+ #[cfg(target_arch = "x86_64")]
33
+ #[target_feature(enable = "avx2")]
34
+ unsafe fn avx2_prefix(values: &[Value], min: i64, max: i64) -> usize {
35
+ use core::arch::x86_64::{
36
+ _mm256_and_si256, _mm256_andnot_si256, _mm256_cmpeq_epi64, _mm256_cmpgt_epi64,
37
+ _mm256_loadu_si256, _mm256_movemask_epi8, _mm256_or_si256, _mm256_set1_epi64x,
38
+ };
39
+ let lower = _mm256_set1_epi64x(min);
40
+ let upper = _mm256_set1_epi64x(max);
41
+ let tag = _mm256_set1_epi64x(1);
42
+ let mut consumed = 0;
43
+ for block in values.as_chunks::<4>().0 {
44
+ // Four VALUE words provide all 32 readable bytes. No Ruby object is
45
+ // dereferenced; the header-validated low bit identifies each Fixnum.
46
+ let valid = unsafe {
47
+ let word = _mm256_loadu_si256(block.as_ptr().cast());
48
+ let tagged = _mm256_cmpeq_epi64(_mm256_and_si256(word, tag), tag);
49
+ let outside = _mm256_or_si256(
50
+ _mm256_cmpgt_epi64(lower, word),
51
+ _mm256_cmpgt_epi64(word, upper),
52
+ );
53
+ _mm256_movemask_epi8(_mm256_andnot_si256(outside, tagged)) == -1
54
+ };
55
+ if !valid {
56
+ break;
57
+ }
58
+ consumed += 4;
59
+ }
60
+ consumed
61
+ }
62
+
63
+ #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
64
+ fn neon_prefix(values: &[Value], min: i64, max: i64) -> usize {
65
+ use core::arch::aarch64::{
66
+ vandq_u64, vceqq_u64, vcgeq_s64, vcleq_s64, vdupq_n_s64, vdupq_n_u64, vgetq_lane_u64,
67
+ vld1q_u64, vreinterpretq_s64_u64,
68
+ };
69
+ // This function is compiled only when the target guarantees NEON.
70
+ let (lower, upper, tag) = unsafe { (vdupq_n_s64(min), vdupq_n_s64(max), vdupq_n_u64(1)) };
71
+ let mut consumed = 0;
72
+ for block in values.as_chunks::<2>().0 {
73
+ // Baseline NEON; the unaligned load stays within two borrowed VALUEs.
74
+ let valid = unsafe {
75
+ let word = vld1q_u64(block.as_ptr().cast());
76
+ let tagged = vceqq_u64(vandq_u64(word, tag), tag);
77
+ let signed = vreinterpretq_s64_u64(word);
78
+ let bounds = vandq_u64(vcgeq_s64(signed, lower), vcleq_s64(signed, upper));
79
+ let valid = vandq_u64(tagged, bounds);
80
+ vgetq_lane_u64::<0>(valid) & vgetq_lane_u64::<1>(valid) == u64::MAX
81
+ };
82
+ if !valid {
83
+ break;
84
+ }
85
+ consumed += 2;
86
+ }
87
+ consumed
88
+ }
89
+
90
+ #[cfg(test)]
91
+ mod tests {
92
+ use super::*;
93
+
94
+ fn tag(value: i64) -> Value {
95
+ ((value << 1) | 1) as Value
96
+ }
97
+
98
+ #[test]
99
+ fn skips_only_valid_fixnums_before_the_first_uncertain_block() {
100
+ for len in 0..65 {
101
+ let original: Vec<_> = (0..len).map(|at| tag((at % 33) as i64 - 16)).collect();
102
+ for at in 0..len {
103
+ for value in [0, 2, tag(-17), tag(17), tag(-(1 << 62)), tag((1 << 62) - 1)] {
104
+ let mut values = original.clone();
105
+ values[at] = value;
106
+ let prefix = valid_prefix(&values, -16, 16, 1);
107
+ assert!(prefix <= at, "length {len}, position {at}, prefix {prefix}");
108
+ assert_eq!(&values[..prefix], &original[..prefix]);
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ #[test]
115
+ fn clamps_schema_limits_without_overflow_and_obeys_disabled_hints() {
116
+ let values = [tag(-(1 << 62)), tag((1 << 62) - 1)].repeat(32);
117
+ let prefix = valid_prefix(&values, i64::MIN, i64::MAX, 1);
118
+ #[cfg(target_arch = "x86_64")]
119
+ if std::is_x86_feature_detected!("avx2") {
120
+ assert_eq!(prefix, values.len());
121
+ }
122
+ #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
123
+ assert_eq!(prefix, values.len());
124
+ assert!(prefix <= values.len());
125
+ assert_eq!(valid_prefix(&values, i64::MIN, i64::MAX, 0), 0);
126
+ assert_eq!(valid_prefix(&values, 1 << 62, i64::MAX, 1), 0);
127
+ assert_eq!(valid_prefix(&values, i64::MIN, -(1 << 62) - 1, 1), 0);
128
+ assert_eq!(valid_prefix(&values, 1, 0, 1), 0);
129
+ assert_eq!(valid_prefix(&values, -(1 << 62) + 1, i64::MAX, 1), 0);
130
+ assert_eq!(valid_prefix(&values, i64::MIN, (1 << 62) - 2, 1), 0);
131
+ }
132
+ }