schemurai 3.0.1 → 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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -4
  3. data/ext/schemurai_native/Cargo.lock +48 -1
  4. data/ext/schemurai_native/Cargo.toml +3 -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 +56 -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 +8 -6
  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/lib.rs +910 -237
  20. data/ext/schemurai_native/src/memo.rs +11 -38
  21. data/ext/schemurai_native/src/number/arbitrary.rs +154 -0
  22. data/ext/schemurai_native/src/number.rs +250 -59
  23. data/ext/schemurai_native/src/pattern.rs +113 -101
  24. data/ext/schemurai_native/src/pointer.rs +97 -0
  25. data/ext/schemurai_native/src/runtime/properties.rs +162 -0
  26. data/ext/schemurai_native/src/runtime/reports.rs +266 -0
  27. data/ext/schemurai_native/src/runtime/scopes.rs +430 -0
  28. data/ext/schemurai_native/src/runtime.rs +1020 -0
  29. data/ext/schemurai_native/src/scope.rs +60 -16
  30. data/ext/schemurai_native/src/string.rs +68 -0
  31. data/ext/schemurai_native/src/tracking.rs +244 -174
  32. data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
  33. data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
  34. data/lib/schemurai/container_checks.rb +534 -0
  35. data/lib/schemurai/decimal.rb +11 -0
  36. data/lib/schemurai/dialect.rb +0 -12
  37. data/lib/schemurai/ecma_regexp.rb +46 -0
  38. data/lib/schemurai/evaluation_pool.rb +66 -0
  39. data/lib/schemurai/evaluator.rb +9 -48
  40. data/lib/schemurai/json_pointer.rb +11 -0
  41. data/lib/schemurai/native/diagnostics.rb +4 -3
  42. data/lib/schemurai/native/evaluator.rb +38 -10
  43. data/lib/schemurai/native/null_numeric.rb +23 -0
  44. data/lib/schemurai/number_checks.rb +128 -0
  45. data/lib/schemurai/string_checks.rb +166 -0
  46. data/lib/schemurai/type_checks.rb +59 -0
  47. data/lib/schemurai/value_equality.rb +72 -0
  48. data/lib/schemurai/version.rb +1 -1
  49. data/lib/schemurai/vm/compiler.rb +18 -26
  50. data/lib/schemurai/vm/evaluator.rb +87 -545
  51. data/lib/schemurai/vm/limited_evaluator.rb +7 -22
  52. metadata +24 -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) })
@@ -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
+ }