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
@@ -123,14 +123,16 @@ pub(super) struct Memo {
123
123
  impl Memo {
124
124
  pub(super) fn key(
125
125
  &self,
126
- engine: &Engine,
126
+ engine: &Execution<'_>,
127
127
  node: usize,
128
128
  value: Value,
129
129
  depth: usize,
130
130
  context: scope::Context<'_>,
131
131
  budget: &budget::Budget,
132
132
  ) -> Option<Key> {
133
- if !engine.memoizable[node] {
133
+ // Dynamic Ruby predicates may be stateful. Replayed observations already
134
+ // preserve reached calls; result memoization must not skip later calls.
135
+ if engine.caches.dynamic_calls() || !engine.memoizable[node] {
134
136
  return None;
135
137
  }
136
138
  let mut object = false;
@@ -968,29 +970,9 @@ impl Engine {
968
970
  // Cyclic candidates can reuse results only under an exact guard/scope
969
971
  // context. Propagate unsupported instructions and nested iteration
970
972
  // through reverse edges without a per-candidate graph walk.
971
- let mut parents = vec![vec![]; edges.len()];
972
- for (parent, children) in edges.iter().enumerate() {
973
- for &child in children {
974
- parents[child].push(parent);
975
- }
976
- }
977
- fn propagate(flags: &mut [bool], parents: &[Vec<usize>]) {
978
- let mut queue: std::collections::VecDeque<_> = flags
979
- .iter()
980
- .enumerate()
981
- .filter_map(|(node, &set)| set.then_some(node))
982
- .collect();
983
- while let Some(node) = queue.pop_front() {
984
- for &parent in &parents[node] {
985
- if !flags[parent] {
986
- flags[parent] = true;
987
- queue.push_back(parent);
988
- }
989
- }
990
- }
991
- }
992
- propagate(&mut unsupported, &parents);
993
- propagate(&mut repeated_subtree, &parents);
973
+ let parents = graph::parents(&edges);
974
+ graph::propagate(&mut unsupported, &parents);
975
+ graph::propagate(&mut repeated_subtree, &parents);
994
976
  // Iterative Kosaraju analysis identifies the only active sources
995
977
  // whose guards can affect descendants: sources lying on a schema cycle.
996
978
  fn cycle_nodes(edges: &[Vec<usize>], parents: &[Vec<usize>]) -> Vec<bool> {
@@ -1072,19 +1054,10 @@ impl Engine {
1072
1054
  }
1073
1055
  }
1074
1056
  }
1075
- fn reverse(edges: &[Vec<usize>]) -> Vec<Vec<usize>> {
1076
- let mut parents = vec![vec![]; edges.len()];
1077
- for (parent, children) in edges.iter().enumerate() {
1078
- for &child in children {
1079
- parents[child].push(parent);
1080
- }
1081
- }
1082
- parents
1083
- }
1084
- let scalar_parents = reverse(&same_edges);
1057
+ let scalar_parents = graph::parents(&same_edges);
1085
1058
  self.memo_guard_scalar_sources =
1086
1059
  cycle_nodes(&same_edges, &scalar_parents)[..count].to_vec();
1087
- propagate(&mut scalar_refs, &scalar_parents);
1060
+ graph::propagate(&mut scalar_refs, &scalar_parents);
1088
1061
  let mut scalar_amplification = vec![false; edges.len()];
1089
1062
  for (node, ops) in self.nodes.iter().enumerate() {
1090
1063
  for op in ops {
@@ -1106,7 +1079,7 @@ impl Engine {
1106
1079
  scalar_amplification[node] |= references > 1;
1107
1080
  }
1108
1081
  }
1109
- propagate(&mut scalar_amplification, &scalar_parents);
1082
+ graph::propagate(&mut scalar_amplification, &scalar_parents);
1110
1083
  self.memo_guard_scalar_amplification = scalar_amplification[..count].to_vec();
1111
1084
  // Object dependencies keep the same value; all other container child
1112
1085
  // edges change positions. Scalar/array inputs cannot take dependencies.
@@ -1119,7 +1092,7 @@ impl Engine {
1119
1092
  }
1120
1093
  }
1121
1094
  self.memo_guard_same_sources =
1122
- cycle_nodes(&same_edges, &reverse(&same_edges))[..count].to_vec();
1095
+ cycle_nodes(&same_edges, &graph::parents(&same_edges))[..count].to_vec();
1123
1096
  self.memo_guard_sources = cyclic[..count].to_vec();
1124
1097
  self.memo_guarded = vec![false; count];
1125
1098
  for node in 0..count {
@@ -0,0 +1,154 @@
1
+ //! Cold arithmetic for magnitudes outside the fixed-width scalar paths.
2
+ //! Only ordinary numeric scalar operands are read; no Ruby allocation or
3
+ //! method dispatch can invalidate the evaluator's borrowed instance storage.
4
+ use super::*;
5
+ use num_bigint::{BigInt, Sign};
6
+ use std::borrow::Cow;
7
+
8
+ fn integer(value: Value) -> BigInt {
9
+ if unsafe { sn_fixnum(value) } != 0 {
10
+ return BigInt::from(unsafe { sn_integer(value) });
11
+ }
12
+ let (words, sign) = unbounded_integer_words(value);
13
+ let digits = words
14
+ .into_iter()
15
+ .flat_map(|word| [word as u32, (word >> 32) as u32])
16
+ .collect();
17
+ BigInt::new(if sign < 0 { Sign::Minus } else { Sign::Plus }, digits)
18
+ }
19
+
20
+ pub(super) fn fraction(numerator: Value, denominator: Option<Value>) -> BigRational {
21
+ BigRational::new(
22
+ integer(numerator),
23
+ denominator.map_or_else(|| BigInt::from(1), integer),
24
+ )
25
+ }
26
+
27
+ fn decimal(value: f64) -> Option<BigRational> {
28
+ let Decimal { coefficient, power } = decimal_parts(value)?;
29
+ let scale = BigInt::from(10).pow(power.unsigned_abs());
30
+ Some(if power >= 0 {
31
+ BigRational::from_integer(BigInt::from(coefficient) * scale)
32
+ } else {
33
+ BigRational::new(BigInt::from(coefficient), scale)
34
+ })
35
+ }
36
+
37
+ impl Limit {
38
+ fn fraction(&self) -> Cow<'_, BigRational> {
39
+ if let Some(fraction) = &self.arbitrary {
40
+ Cow::Borrowed(fraction)
41
+ } else {
42
+ let fraction = self.exact.expect("compiled numeric fraction");
43
+ Cow::Owned(BigRational::new_raw(
44
+ BigInt::from(fraction.numerator),
45
+ BigInt::from(fraction.denominator),
46
+ ))
47
+ }
48
+ }
49
+
50
+ pub(super) fn arbitrary_memory_usage(&self) -> usize {
51
+ self.arbitrary.as_ref().map_or(0, |fraction| {
52
+ // num-bigint exposes used digits rather than allocation capacity.
53
+ std::mem::size_of::<BigRational>()
54
+ + (fraction.numer().bits().div_ceil(64) + fraction.denom().bits().div_ceil(64))
55
+ as usize
56
+ * 8
57
+ })
58
+ }
59
+ }
60
+
61
+ impl Rules {
62
+ pub(super) fn arbitrary_check(
63
+ &self,
64
+ caches: &runtime::Caches,
65
+ value: Value,
66
+ budget: &budget::Budget,
67
+ mut failure: impl FnMut(i32) -> bool,
68
+ ) -> Result<i32, Verdict> {
69
+ let float = unsafe { sn_is_float(value) } != 0;
70
+ let n = if float {
71
+ unsafe { sn_float(value) }
72
+ } else {
73
+ 0.0
74
+ };
75
+ let actual = if float {
76
+ if self.multiple.is_some() {
77
+ if !n.is_finite() {
78
+ budget.failure.set(if n.is_nan() {
79
+ STATUS_DECIMAL_NAN
80
+ } else if n.is_sign_negative() {
81
+ STATUS_DECIMAL_NEGATIVE_INFINITY
82
+ } else {
83
+ STATUS_DECIMAL_POSITIVE_INFINITY
84
+ });
85
+ return Err(Unsupported);
86
+ }
87
+ Some(decimal(n).ok_or(Unsupported)?)
88
+ } else {
89
+ None
90
+ }
91
+ } else {
92
+ if unsafe { sn_kind(value) } & TYPE_INTEGER == 0 {
93
+ return Err(Unsupported);
94
+ }
95
+ Some(BigRational::from_integer(integer(value)))
96
+ };
97
+ // Without multipleOf, Ruby compares a Float to Integer bounds exactly
98
+ // in binary, but rounds Rational bounds to Float. With multipleOf,
99
+ // Ruby first converts the instance's decimal spelling to a Rational.
100
+ let binary = if actual.is_none() && self.limits.iter().any(|(_, limit)| limit.integer) {
101
+ BigRational::from_float(n)
102
+ } else {
103
+ None
104
+ };
105
+ let mut bits = 0;
106
+ for (bit, limit) in &self.limits {
107
+ if !budget.checkpoint() {
108
+ return Err(Unsupported);
109
+ }
110
+ let violates = limit.violation(caches, value, *bit, self.multiple.is_some(), || {
111
+ let order = if let Some(actual) = &actual {
112
+ Some(actual.cmp(limit.fraction().as_ref()))
113
+ } else if !limit.integer {
114
+ n.partial_cmp(&limit.rounded)
115
+ } else if let Some(binary) = &binary {
116
+ Some(binary.cmp(limit.fraction().as_ref()))
117
+ } else if n.is_nan() {
118
+ None
119
+ } else {
120
+ Some(if n.is_sign_negative() {
121
+ Ordering::Less
122
+ } else {
123
+ Ordering::Greater
124
+ })
125
+ };
126
+ violates(*bit, order)
127
+ })?;
128
+ if violates {
129
+ bits |= bit;
130
+ if failure(*bit) {
131
+ return Ok(bits);
132
+ }
133
+ }
134
+ }
135
+ if let Some(limit) = &self.multiple {
136
+ if !budget.checkpoint() {
137
+ return Err(Unsupported);
138
+ }
139
+ let violates = limit.violation(caches, value, MULTIPLE_OF, true, || {
140
+ let divisor = limit.fraction();
141
+ let actual = actual.as_ref().expect("multipleOf decimal operand");
142
+ let valid = divisor.numer().sign() == Sign::Plus
143
+ && (actual.numer() % divisor.numer()).sign() == Sign::NoSign
144
+ && (divisor.denom() % actual.denom()).sign() == Sign::NoSign;
145
+ !valid
146
+ })?;
147
+ if violates {
148
+ bits |= MULTIPLE_OF;
149
+ failure(MULTIPLE_OF);
150
+ }
151
+ }
152
+ Ok(bits)
153
+ }
154
+ }