schemurai 2.2.3 → 3.0.0

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -7
  3. data/ext/schemurai_native/Cargo.lock +99 -0
  4. data/ext/schemurai_native/Cargo.toml +21 -0
  5. data/ext/schemurai_native/build.rs +35 -0
  6. data/ext/schemurai_native/extconf.rb +63 -0
  7. data/ext/schemurai_native/glue.c +780 -0
  8. data/ext/schemurai_native/protocol.def +63 -0
  9. data/ext/schemurai_native/protocol.h +10 -0
  10. data/ext/schemurai_native/src/budget.rs +101 -0
  11. data/ext/schemurai_native/src/content.rs +574 -0
  12. data/ext/schemurai_native/src/diagnostics.rs +1135 -0
  13. data/ext/schemurai_native/src/enumeration.rs +154 -0
  14. data/ext/schemurai_native/src/evaluation.rs +219 -0
  15. data/ext/schemurai_native/src/format.rs +357 -0
  16. data/ext/schemurai_native/src/graph.rs +50 -0
  17. data/ext/schemurai_native/src/instance_graph.rs +84 -0
  18. data/ext/schemurai_native/src/lib.rs +3139 -0
  19. data/ext/schemurai_native/src/memo.rs +1149 -0
  20. data/ext/schemurai_native/src/memo_scope.rs +240 -0
  21. data/ext/schemurai_native/src/number.rs +1054 -0
  22. data/ext/schemurai_native/src/pattern.rs +431 -0
  23. data/ext/schemurai_native/src/scope.rs +420 -0
  24. data/ext/schemurai_native/src/structural.rs +86 -0
  25. data/ext/schemurai_native/src/tracking.rs +687 -0
  26. data/lib/schemurai/backend.rb +3 -1
  27. data/lib/schemurai/evaluator.rb +45 -3
  28. data/lib/schemurai/limit_budget.rb +78 -0
  29. data/lib/schemurai/limit_exceeded.rb +18 -0
  30. data/lib/schemurai/limited_evaluator.rb +100 -0
  31. data/lib/schemurai/native/diagnostics.rb +52 -0
  32. data/lib/schemurai/native/evaluator.rb +59 -0
  33. data/lib/schemurai/schema_domain.rb +12 -7
  34. data/lib/schemurai/schema_graph.rb +10 -1
  35. data/lib/schemurai/unique_items.rb +2 -0
  36. data/lib/schemurai/version.rb +1 -1
  37. data/lib/schemurai/vm/compiler.rb +7 -3
  38. data/lib/schemurai/vm/evaluator.rb +50 -3
  39. data/lib/schemurai/vm/limited_evaluator.rb +97 -0
  40. data/lib/schemurai.rb +81 -33
  41. data/sig/schemurai.rbs +21 -13
  42. metadata +36 -6
@@ -0,0 +1,154 @@
1
+ //! Large enums retain a compiled fingerprint index over pinned schema values.
2
+ //! Hashes only select candidates; JSON equality still decides every match.
3
+ use super::*;
4
+
5
+ const LINEAR_LIMIT: usize = 16;
6
+ const MAX_INDEX_BYTES: usize = 64 * 1024 * 1024;
7
+
8
+ #[derive(Debug)]
9
+ pub(super) struct Enumeration {
10
+ values: Vec<Value>,
11
+ index: Option<Index>,
12
+ kinds: i32,
13
+ }
14
+
15
+ impl Enumeration {
16
+ pub(super) fn compile(values: Vec<Value>, fixnum_flag: Value) -> Self {
17
+ let mut fingerprints = structural::Cache::batch(0);
18
+ let budget = budget::Budget::default();
19
+ let index = (values.len() > LINEAR_LIMIT)
20
+ .then(|| {
21
+ Index::compile(&values, |value| {
22
+ fingerprint_cached(value, 0, fixnum_flag, false, &mut fingerprints, &budget)
23
+ })
24
+ })
25
+ .flatten();
26
+ let kinds = if index.is_some() {
27
+ values
28
+ .iter()
29
+ .fold(0, |mask, &value| mask | unsafe { sn_shallow_kind(value) })
30
+ } else {
31
+ 0
32
+ };
33
+ Self {
34
+ values,
35
+ index,
36
+ kinds,
37
+ }
38
+ }
39
+
40
+ pub(super) fn eval(
41
+ &self,
42
+ value: Value,
43
+ kind: i32,
44
+ depth: usize,
45
+ fixnum_flag: Value,
46
+ budget: &budget::Budget,
47
+ ) -> Verdict {
48
+ if let Some(index) = &self.index {
49
+ // All indexed schema values are supported. A disjoint root type
50
+ // cannot match, even if the input contains unsupported descendants.
51
+ // Avoid fingerprinting large containers for scalar-only enums.
52
+ if kind & self.kinds == 0 {
53
+ return Invalid;
54
+ }
55
+ // Keep the best-case linear lookup cheap. This also avoids hashing
56
+ // a whole input container when the first candidate already matches.
57
+ let first = equal_budgeted(self.values[0], value, depth, budget);
58
+ if first != Invalid {
59
+ return first;
60
+ }
61
+ if let Some(hash) = fingerprint_budgeted(value, depth, fixnum_flag, false, budget) {
62
+ return self.compare(
63
+ index.candidates(hash).filter(|&index| index != 0),
64
+ value,
65
+ depth,
66
+ budget,
67
+ );
68
+ }
69
+ return self.compare(1..self.values.len(), value, depth, budget);
70
+ }
71
+ // An unavailable fingerprint is not an unsupported comparison. Linear
72
+ // equality can reject a different shape before reaching that value.
73
+ self.compare(0..self.values.len(), value, depth, budget)
74
+ }
75
+
76
+ fn compare(
77
+ &self,
78
+ candidates: impl Iterator<Item = usize>,
79
+ value: Value,
80
+ depth: usize,
81
+ budget: &budget::Budget,
82
+ ) -> Verdict {
83
+ for index in candidates {
84
+ let result = equal_budgeted(self.values[index], value, depth, budget);
85
+ if result != Invalid {
86
+ return result;
87
+ }
88
+ }
89
+ Invalid
90
+ }
91
+
92
+ pub(super) fn memory_usage(&self) -> usize {
93
+ std::mem::size_of::<Self>()
94
+ + self.values.capacity() * std::mem::size_of::<Value>()
95
+ + self.index.as_ref().map_or(0, |index| {
96
+ index.entries.capacity() * std::mem::size_of::<(u64, usize)>()
97
+ })
98
+ }
99
+ }
100
+
101
+ #[derive(Debug)]
102
+ struct Index {
103
+ entries: Vec<(u64, usize)>,
104
+ }
105
+
106
+ impl Index {
107
+ fn compile(values: &[Value], mut hash: impl FnMut(Value) -> Option<u64>) -> Option<Self> {
108
+ if values
109
+ .len()
110
+ .checked_mul(std::mem::size_of::<(u64, usize)>())?
111
+ > MAX_INDEX_BYTES
112
+ {
113
+ return None;
114
+ }
115
+ let mut entries = Vec::new();
116
+ entries.try_reserve_exact(values.len()).ok()?;
117
+ for (index, &value) in values.iter().enumerate() {
118
+ entries.push((hash(value)?, index));
119
+ }
120
+ // A sorted flat index bounds memory and compilation to O(E log E),
121
+ // even for colliding fingerprints. Equal hashes retain schema order.
122
+ entries.sort_unstable();
123
+ Some(Self { entries })
124
+ }
125
+
126
+ fn candidates(&self, hash: u64) -> impl Iterator<Item = usize> + '_ {
127
+ let start = self
128
+ .entries
129
+ .partition_point(|&(candidate, _)| candidate < hash);
130
+ self.entries[start..]
131
+ .iter()
132
+ .take_while(move |&&(candidate, _)| candidate == hash)
133
+ .map(|&(_, index)| index)
134
+ }
135
+ }
136
+
137
+ #[cfg(test)]
138
+ mod tests {
139
+ use super::*;
140
+
141
+ #[test]
142
+ fn selects_all_collisions_in_schema_order() {
143
+ let index = Index::compile(&[3, 1, 3, 2, 3], |value| Some(value as u64)).unwrap();
144
+ assert_eq!(index.candidates(3).collect::<Vec<_>>(), [0, 2, 4]);
145
+ assert_eq!(index.candidates(1).collect::<Vec<_>>(), [1]);
146
+ assert_eq!(index.candidates(0).count(), 0);
147
+ assert_eq!(index.candidates(4).count(), 0);
148
+ }
149
+
150
+ #[test]
151
+ fn unavailable_fingerprint_discards_the_index() {
152
+ assert!(Index::compile(&[1, 2, 3], |value| (value != 2).then_some(value as u64)).is_none());
153
+ }
154
+ }
@@ -0,0 +1,219 @@
1
+ //! Shared branch and contains scans with separate result and termination policies.
2
+ use super::*;
3
+ use tracking::Tracked;
4
+
5
+ #[derive(Clone, Copy)]
6
+ pub(super) enum BranchKind {
7
+ Any,
8
+ One,
9
+ }
10
+ impl BranchKind {
11
+ pub(super) fn accepts(self, matches: usize) -> bool {
12
+ match self {
13
+ Self::Any => matches > 0,
14
+ Self::One => matches == 1,
15
+ }
16
+ }
17
+ pub(super) fn keyword(self) -> &'static str {
18
+ match self {
19
+ Self::Any => "anyOf",
20
+ Self::One => "oneOf",
21
+ }
22
+ }
23
+ }
24
+
25
+ #[derive(Clone, Copy)]
26
+ pub(super) enum BranchPolicy {
27
+ Validity(BranchKind),
28
+ // Annotations, exact diagnostic counts and compatibility exceptions can
29
+ // require later branches even after validity alone is conclusive.
30
+ Complete,
31
+ }
32
+ impl BranchPolicy {
33
+ fn finished(self, matches: usize) -> bool {
34
+ match self {
35
+ Self::Validity(BranchKind::Any) => matches > 0,
36
+ Self::Validity(BranchKind::One) => matches > 1,
37
+ Self::Complete => false,
38
+ }
39
+ }
40
+ }
41
+
42
+ pub(super) trait BranchResult {
43
+ fn valid() -> Self;
44
+ fn status(&self) -> Verdict;
45
+ fn merge(&mut self, branch: Self);
46
+ }
47
+ impl BranchResult for Verdict {
48
+ fn valid() -> Self {
49
+ Valid
50
+ }
51
+ fn status(&self) -> Verdict {
52
+ *self
53
+ }
54
+ fn merge(&mut self, _: Self) {}
55
+ }
56
+ impl BranchResult for Tracked {
57
+ fn valid() -> Self {
58
+ Self::new(Valid)
59
+ }
60
+ fn status(&self) -> Verdict {
61
+ self.status
62
+ }
63
+ fn merge(&mut self, branch: Self) {
64
+ self.merge(branch);
65
+ }
66
+ }
67
+
68
+ pub(super) struct BranchMatches<R> {
69
+ pub matches: usize,
70
+ pub coverage: R,
71
+ }
72
+ pub(super) fn scan_branches<R: BranchResult>(
73
+ nodes: &[(usize, usize)],
74
+ policy: BranchPolicy,
75
+ mut evaluate: impl FnMut(usize) -> R,
76
+ ) -> Result<BranchMatches<R>, Verdict> {
77
+ let mut result = BranchMatches {
78
+ matches: 0,
79
+ coverage: R::valid(),
80
+ };
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);
89
+ }
90
+ }
91
+ if policy.finished(result.matches) {
92
+ break;
93
+ }
94
+ }
95
+ Ok(result)
96
+ }
97
+
98
+ // Diagnostic counts and annotations need every match. Keep the same complete
99
+ // traversal for ordinary validity so unsupported later items retain fallback.
100
+ pub(super) fn scan_contains(
101
+ len: usize,
102
+ mut evaluate: impl FnMut(usize) -> Verdict,
103
+ mut record: impl FnMut(usize),
104
+ ) -> Result<usize, Verdict> {
105
+ let mut matches = 0;
106
+ for index in 0..len {
107
+ match evaluate(index) {
108
+ Unsupported => return Err(Unsupported),
109
+ Invalid => {}
110
+ Valid => {
111
+ matches += 1;
112
+ record(index);
113
+ }
114
+ }
115
+ }
116
+ Ok(matches)
117
+ }
118
+
119
+ // Use only after proving every remaining predicate evaluation is supported.
120
+ // Annotation and diagnostic policies continue using the complete scan above.
121
+ pub(super) fn contains_validity(
122
+ len: usize,
123
+ min: usize,
124
+ max: Option<usize>,
125
+ mut evaluate: impl FnMut(usize) -> bool,
126
+ ) -> Verdict {
127
+ if len < min || max.is_some_and(|max| max < min) {
128
+ return Invalid;
129
+ }
130
+ // Infinity and any bound at least as large as the array cannot constrain
131
+ // its match count. The VM represents an absent maxContains as +Infinity.
132
+ let max = max.filter(|&max| max < len);
133
+ if min == 0 && max.is_none() {
134
+ return Valid;
135
+ }
136
+ let mut matches = 0;
137
+ for index in 0..len {
138
+ matches += usize::from(evaluate(index));
139
+ if max.is_some_and(|max| matches > max) {
140
+ return Invalid;
141
+ }
142
+ if max.is_none() && matches >= min {
143
+ return Valid;
144
+ }
145
+ if matches + (len - index - 1) < min {
146
+ return Invalid;
147
+ }
148
+ }
149
+ bounds(matches, Some(min), max)
150
+ }
151
+
152
+ #[cfg(test)]
153
+ mod tests {
154
+ use super::*;
155
+
156
+ #[test]
157
+ fn contains_termination_matches_complete_counts() {
158
+ for len in 0..8 {
159
+ for mask in 0..(1 << len) {
160
+ let count = (mask as u32).count_ones() as usize;
161
+ for min in 0..9 {
162
+ for max in [None, Some(0), Some(1), Some(4), Some(8)] {
163
+ assert_eq!(
164
+ contains_validity(len, min, max, |i| mask & (1 << i) != 0),
165
+ bounds(count, Some(min), max)
166
+ );
167
+ }
168
+ }
169
+ }
170
+ }
171
+ let mut calls = 0;
172
+ assert_eq!(
173
+ contains_validity(1000, 1, None, |_| {
174
+ calls += 1;
175
+ true
176
+ }),
177
+ Valid
178
+ );
179
+ assert_eq!(calls, 1);
180
+ }
181
+
182
+ #[test]
183
+ fn termination_preserves_multiplicity_and_complete_scan_errors() {
184
+ let nodes = [(0, 2), (1, 1)];
185
+ let evaluate = |node| if node == 0 { Valid } else { Unsupported };
186
+ for kind in [BranchKind::Any, BranchKind::One] {
187
+ let result = scan_branches(&nodes, BranchPolicy::Validity(kind), evaluate).unwrap();
188
+ assert_eq!(result.matches, 2);
189
+ assert_eq!(
190
+ kind.accepts(result.matches),
191
+ matches!(kind, BranchKind::Any)
192
+ );
193
+ }
194
+ assert!(matches!(
195
+ scan_branches(&nodes, BranchPolicy::Complete, evaluate),
196
+ Err(Unsupported)
197
+ ));
198
+ }
199
+
200
+ #[test]
201
+ fn contains_records_only_matches_and_retains_later_unsupported_results() {
202
+ let mut recorded = vec![];
203
+ let result = scan_contains(
204
+ 4,
205
+ |index| verdict(index % 2 == 0),
206
+ |index| recorded.push(index),
207
+ );
208
+ assert_eq!(result, Ok(2));
209
+ assert_eq!(recorded, [0, 2]);
210
+ assert_eq!(
211
+ scan_contains(
212
+ 2,
213
+ |index| if index == 0 { Valid } else { Unsupported },
214
+ |_| {}
215
+ ),
216
+ Err(Unsupported)
217
+ );
218
+ }
219
+ }