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.
- checksums.yaml +4 -4
- data/README.md +6 -4
- data/ext/schemurai_native/Cargo.lock +55 -1
- data/ext/schemurai_native/Cargo.toml +4 -1
- data/ext/schemurai_native/extconf.rb +1 -1
- data/ext/schemurai_native/glue.c +1679 -62
- data/ext/schemurai_native/protocol.def +45 -0
- data/ext/schemurai_native/src/budget.rs +49 -0
- data/ext/schemurai_native/src/container.rs +460 -0
- data/ext/schemurai_native/src/content.rs +74 -27
- data/ext/schemurai_native/src/diagnostics/array.rs +392 -0
- data/ext/schemurai_native/src/diagnostics/object.rs +218 -0
- data/ext/schemurai_native/src/diagnostics.rs +1087 -307
- data/ext/schemurai_native/src/enumeration.rs +6 -1
- data/ext/schemurai_native/src/evaluation.rs +49 -20
- data/ext/schemurai_native/src/format.rs +126 -14
- data/ext/schemurai_native/src/graph.rs +37 -0
- data/ext/schemurai_native/src/instance_graph.rs +90 -0
- data/ext/schemurai_native/src/integer_scan.rs +132 -0
- data/ext/schemurai_native/src/lib.rs +917 -238
- data/ext/schemurai_native/src/memo.rs +11 -38
- data/ext/schemurai_native/src/number/arbitrary.rs +154 -0
- data/ext/schemurai_native/src/number.rs +250 -59
- data/ext/schemurai_native/src/pattern.rs +147 -109
- data/ext/schemurai_native/src/pointer.rs +97 -0
- data/ext/schemurai_native/src/runtime/properties.rs +162 -0
- data/ext/schemurai_native/src/runtime/reports.rs +266 -0
- data/ext/schemurai_native/src/runtime/scopes.rs +430 -0
- data/ext/schemurai_native/src/runtime.rs +1020 -0
- data/ext/schemurai_native/src/scope.rs +60 -16
- data/ext/schemurai_native/src/string.rs +68 -0
- data/ext/schemurai_native/src/tracking.rs +244 -174
- data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
- data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
- data/lib/schemurai/container_checks.rb +534 -0
- data/lib/schemurai/decimal.rb +11 -0
- data/lib/schemurai/dialect.rb +0 -12
- data/lib/schemurai/ecma_regexp.rb +46 -0
- data/lib/schemurai/evaluation_pool.rb +66 -0
- data/lib/schemurai/evaluator.rb +9 -48
- data/lib/schemurai/json_pointer.rb +11 -0
- data/lib/schemurai/native/diagnostics.rb +4 -3
- data/lib/schemurai/native/evaluator.rb +38 -10
- data/lib/schemurai/native/null_numeric.rb +23 -0
- data/lib/schemurai/number_checks.rb +128 -0
- data/lib/schemurai/string_checks.rb +166 -0
- data/lib/schemurai/type_checks.rb +59 -0
- data/lib/schemurai/value_equality.rb +72 -0
- data/lib/schemurai/version.rb +1 -1
- data/lib/schemurai/vm/compiler.rb +18 -26
- data/lib/schemurai/vm/evaluator.rb +87 -545
- data/lib/schemurai/vm/limited_evaluator.rb +7 -22
- 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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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(
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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()
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
}
|