schemurai 2.3.0 → 3.0.1

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +67 -4
  3. data/ext/schemurai_native/Cargo.lock +8 -1
  4. data/ext/schemurai_native/Cargo.toml +2 -1
  5. data/ext/schemurai_native/glue.c +155 -14
  6. data/ext/schemurai_native/src/budget.rs +101 -0
  7. data/ext/schemurai_native/src/content.rs +18 -0
  8. data/ext/schemurai_native/src/diagnostics.rs +395 -225
  9. data/ext/schemurai_native/src/enumeration.rs +154 -0
  10. data/ext/schemurai_native/src/evaluation.rs +219 -0
  11. data/ext/schemurai_native/src/format.rs +118 -8
  12. data/ext/schemurai_native/src/graph.rs +50 -0
  13. data/ext/schemurai_native/src/instance_graph.rs +84 -0
  14. data/ext/schemurai_native/src/integer_scan.rs +132 -0
  15. data/ext/schemurai_native/src/lib.rs +954 -185
  16. data/ext/schemurai_native/src/memo.rs +1149 -0
  17. data/ext/schemurai_native/src/memo_scope.rs +240 -0
  18. data/ext/schemurai_native/src/number.rs +7 -8
  19. data/ext/schemurai_native/src/pattern.rs +191 -22
  20. data/ext/schemurai_native/src/scope.rs +243 -33
  21. data/ext/schemurai_native/src/structural.rs +86 -0
  22. data/ext/schemurai_native/src/tracking.rs +245 -90
  23. data/lib/schemurai/backend.rb +1 -0
  24. data/lib/schemurai/evaluator.rb +45 -3
  25. data/lib/schemurai/limit_budget.rb +78 -0
  26. data/lib/schemurai/limit_exceeded.rb +18 -0
  27. data/lib/schemurai/limited_evaluator.rb +100 -0
  28. data/lib/schemurai/native/evaluator.rb +32 -5
  29. data/lib/schemurai/schema_domain.rb +12 -7
  30. data/lib/schemurai/schema_graph.rb +10 -1
  31. data/lib/schemurai/unique_items.rb +2 -0
  32. data/lib/schemurai/version.rb +1 -1
  33. data/lib/schemurai/vm/compiler.rb +7 -3
  34. data/lib/schemurai/vm/evaluator.rb +50 -3
  35. data/lib/schemurai/vm/limited_evaluator.rb +97 -0
  36. data/lib/schemurai.rb +74 -27
  37. data/sig/schemurai.rbs +20 -12
  38. metadata +17 -4
@@ -10,6 +10,85 @@ pub(super) struct Metadata {
10
10
  pub dynamic_anchor: Option<Value>,
11
11
  pub primary_binding: Option<usize>,
12
12
  }
13
+
14
+ #[cfg(test)]
15
+ mod tests {
16
+ use super::*;
17
+
18
+ #[test]
19
+ fn requires_guard_histories_for_cycles_reached_through_dynamic_overrides() {
20
+ let reference = |target, kind| Op::Ref(Reference { target, kind });
21
+ let mut engine = Engine {
22
+ nodes: vec![
23
+ vec![Op::Boolean(true)],
24
+ vec![reference(0, ReferenceKind::Dynamic(0))],
25
+ vec![reference(1, ReferenceKind::Static)],
26
+ vec![reference(1, ReferenceKind::Static)],
27
+ vec![reference(1, ReferenceKind::Static)],
28
+ ],
29
+ anchor_tables: vec![vec![(100, 0), (101, 1)]],
30
+ ..Engine::default()
31
+ };
32
+ engine.compile_memoization();
33
+ assert!(engine.memoizable[1]);
34
+ assert!(engine.memo_guarded[1]);
35
+ engine.nodes.pop();
36
+ engine.compile_memoization();
37
+ assert!(!engine.memoizable[1]);
38
+ assert!(engine.memo_guard_sources[1]);
39
+ engine.nodes.push(vec![reference(1, ReferenceKind::Static)]);
40
+ engine.nodes[0] = vec![Op::Unsupported];
41
+ engine.compile_memoization();
42
+ assert!(!engine.memoizable[1]);
43
+ }
44
+
45
+ #[test]
46
+ fn removes_scope_keys_only_for_proven_invariant_dynamic_targets() {
47
+ let reference = |target, kind| Op::Ref(Reference { target, kind });
48
+ let mut engine = Engine {
49
+ nodes: vec![
50
+ vec![Op::Boolean(true)],
51
+ vec![reference(0, ReferenceKind::Dynamic(0))],
52
+ vec![reference(0, ReferenceKind::Dynamic(0))],
53
+ vec![Op::All(vec![1, 2])],
54
+ vec![reference(3, ReferenceKind::Static)],
55
+ vec![reference(3, ReferenceKind::Static)],
56
+ vec![reference(0, ReferenceKind::Dynamic(1))],
57
+ vec![reference(0, ReferenceKind::Dynamic(2))],
58
+ vec![reference(0, ReferenceKind::Dynamic(3))],
59
+ vec![reference(0, ReferenceKind::Recursive)],
60
+ ],
61
+ anchor_tables: vec![
62
+ vec![(100, 0), (101, 0)],
63
+ vec![(100, 0), (101, 3)],
64
+ vec![(100, 3)],
65
+ vec![],
66
+ ],
67
+ ..Engine::default()
68
+ };
69
+ engine.compile_memoization();
70
+ assert!(engine.memoizable[3]);
71
+ assert!(engine.memo_contextual[3]);
72
+ engine.simplify_dynamic_references();
73
+ engine.compile_memoization();
74
+ assert!(engine.memoizable[3]);
75
+ assert!(!engine.memo_contextual[3]);
76
+ for node in [1, 2] {
77
+ let Op::Ref(reference) = engine.nodes[node][0] else {
78
+ panic!()
79
+ };
80
+ assert_eq!(reference.static_target(), Some(0));
81
+ }
82
+ // Changing bindings, fallback mismatches, absent bindings and
83
+ // recursive references retain context-sensitive resolution.
84
+ for node in [6, 7, 8, 9] {
85
+ let Op::Ref(reference) = engine.nodes[node][0] else {
86
+ panic!()
87
+ };
88
+ assert_eq!(reference.static_target(), None);
89
+ }
90
+ }
91
+ }
13
92
  #[derive(Clone, Copy, Debug)]
14
93
  enum ReferenceKind {
15
94
  Static,
@@ -31,7 +110,50 @@ pub(super) struct ScopeFrame<'a> {
31
110
  pub parent: Option<&'a ScopeFrame<'a>>,
32
111
  pub primary_binding: Option<usize>,
33
112
  }
113
+ impl Reference {
114
+ pub(super) fn dynamic_target(self) -> Option<(usize, usize)> {
115
+ match self.kind {
116
+ ReferenceKind::Dynamic(name) => Some((name, self.target)),
117
+ _ => None,
118
+ }
119
+ }
120
+ pub(super) fn recursive_target(self) -> Option<usize> {
121
+ matches!(self.kind, ReferenceKind::Recursive).then_some(self.target)
122
+ }
123
+ pub(super) fn static_target(self) -> Option<usize> {
124
+ matches!(self.kind, ReferenceKind::Static).then_some(self.target)
125
+ }
126
+ }
34
127
  impl Engine {
128
+ pub(super) fn simplify_dynamic_references(&mut self) {
129
+ // Anchor binding has reached its fixed point, including resources
130
+ // compiled while discovering overrides. A name whose every binding
131
+ // equals the fallback target cannot change under any native scope.
132
+ // Preserve the source opcode for diagnostic paths, while allowing
133
+ // ordinary acyclic static-target memoization for the shared graph.
134
+ let invariant: Vec<_> = self
135
+ .anchor_tables
136
+ .iter()
137
+ .map(|bindings| {
138
+ let &(_, target) = bindings.first()?;
139
+ bindings
140
+ .iter()
141
+ .all(|&(_, candidate)| candidate == target)
142
+ .then_some(target)
143
+ })
144
+ .collect();
145
+ for ops in &mut self.nodes {
146
+ for op in ops {
147
+ if let Op::Ref(reference) = op
148
+ && let ReferenceKind::Dynamic(name) = reference.kind
149
+ && invariant[name] == Some(reference.target)
150
+ {
151
+ reference.kind = ReferenceKind::Static;
152
+ }
153
+ }
154
+ }
155
+ }
156
+
35
157
  pub(super) fn compile_scope(
36
158
  &mut self,
37
159
  program: Value,
@@ -86,15 +208,14 @@ impl Engine {
86
208
  && unsafe { sn_string_equal(fragment, anchor) } != 0 =>
87
209
  {
88
210
  let name = JsonKey::new(fragment);
89
- let index = self
90
- .anchor_names
91
- .iter()
92
- .position(|&candidate| candidate == name)
93
- .unwrap_or_else(|| {
94
- let index = self.anchor_names.len();
95
- self.anchor_names.push(name);
96
- index
97
- });
211
+ let index = if let Some(&index) = self.anchor_ids.get(&name) {
212
+ index
213
+ } else {
214
+ let index = self.anchor_names.len();
215
+ self.anchor_names.push(name);
216
+ self.anchor_ids.insert(name, index);
217
+ index
218
+ };
98
219
  ReferenceKind::Dynamic(index)
99
220
  }
100
221
  _ => ReferenceKind::Static,
@@ -105,35 +226,127 @@ impl Engine {
105
226
  Ok(Op::Ref(Reference { target, kind }))
106
227
  }
107
228
  pub(super) fn bind_dynamic_anchors(&mut self) -> Result<(), Value> {
229
+ // Names append during compilation. A resolved prefix per resource
230
+ // records negative lookups without retaining their Cartesian product.
231
+ // Snapshot only pending rows; compiling a binding can add resources
232
+ // and names, which the next pass must resolve too.
233
+ let mut resolved = HashMap::new();
108
234
  loop {
235
+ let names = self.anchor_names.len();
236
+ if names > 1 {
237
+ return self.bind_indexed_dynamic_anchors();
238
+ }
109
239
  let pending: Vec<_> = self
110
240
  .resources
111
241
  .keys()
112
- .flat_map(|&resource| {
113
- (0..self.anchor_names.len()).map(move |name| (resource, name))
242
+ .filter_map(|&resource| {
243
+ let first = resolved.get(&resource).copied().unwrap_or(0);
244
+ (first < names).then_some((resource, first..names))
114
245
  })
115
- .filter(|key| !self.dynamic_bindings.contains_key(key))
116
246
  .collect();
117
247
  if pending.is_empty() {
118
- self.anchor_tables = vec![vec![]; self.anchor_names.len()];
119
- for (&(resource, name), &target) in &self.dynamic_bindings {
120
- if let Some(target) = target {
121
- self.anchor_tables[name].push((resource, target));
248
+ self.finish_dynamic_bindings();
249
+ return Ok(());
250
+ }
251
+ for (resource, names) in pending {
252
+ let end = names.end;
253
+ for name in names {
254
+ let mut target = 0;
255
+ if unsafe {
256
+ sn_anchor_program(
257
+ self.compiler,
258
+ resource,
259
+ self.anchor_names[name].0,
260
+ &mut target,
261
+ )
262
+ } != 0
263
+ {
264
+ return Err(target);
265
+ }
266
+ if unsafe { sn_same(target, sn_nil()) } == 0 {
267
+ self.retain(target);
268
+ let target = self.compile(target)?;
269
+ self.dynamic_bindings.insert((resource, name), target);
122
270
  }
123
271
  }
124
- if self.anchor_tables.len() == 1 {
125
- for metadata in &mut self.metadata {
126
- metadata.primary_binding =
127
- self.anchor_tables[0]
128
- .iter()
129
- .find_map(|&(resource, target)| {
130
- (resource == metadata.resource).then_some(target)
131
- });
272
+ resolved.insert(resource, end);
273
+ }
274
+ }
275
+ }
276
+ fn finish_dynamic_bindings(&mut self) {
277
+ self.anchor_tables = vec![vec![]; self.anchor_names.len()];
278
+ for (&(resource, name), &target) in &self.dynamic_bindings {
279
+ self.anchor_tables[name].push((resource, target));
280
+ }
281
+ if self.anchor_names.len() == 1 {
282
+ for metadata in &mut self.metadata {
283
+ // Use the existing resource/name index instead of an
284
+ // O(binding count) scan for every compiled node.
285
+ metadata.primary_binding =
286
+ self.dynamic_bindings.get(&(metadata.resource, 0)).copied();
287
+ }
288
+ }
289
+ // Compiler-only name indexes cannot be reused at runtime. Drop
290
+ // their allocations and temporary GC roots after the fixed point.
291
+ drop(std::mem::take(&mut self.anchor_ids));
292
+ drop(std::mem::take(
293
+ &mut unsafe { &mut *self.roots }.binding_names,
294
+ ));
295
+ }
296
+
297
+ fn bind_indexed_dynamic_anchors(&mut self) -> Result<(), Value> {
298
+ // Join registered names with resources that actually bind them. Resource
299
+ // tables append names, including when external indexing augments an
300
+ // existing resource. An indexed prefix admits only newly added names.
301
+ let mut available: HashMap<JsonKey, Vec<Value>, BuildHasherDefault<BucketHasher>> =
302
+ HashMap::default();
303
+ let mut resolved_names = 0;
304
+ let mut indexed = HashMap::new();
305
+ loop {
306
+ let mut pending = vec![];
307
+ for &resource in self.resources.keys() {
308
+ let first = indexed.get(&resource).copied().unwrap_or(0);
309
+ if unsafe { sn_anchor_count(resource) } == first {
310
+ continue;
311
+ }
312
+ let mut names = 0;
313
+ if unsafe { sn_anchor_names(self.compiler, resource, &mut names) } != 0 {
314
+ return Err(names);
315
+ }
316
+ // Copy handles before any Ruby callback. The snapshot Array
317
+ // is never borrowed across allocation, and temporary roots
318
+ // pin its names while the reverse index remains live.
319
+ let count = unsafe { sn_len(names) };
320
+ for i in first..count {
321
+ let name = unsafe { sn_entry(names, i) };
322
+ unsafe { &mut *self.roots }.binding_names.push(name);
323
+ let name = JsonKey::new(name);
324
+ available.entry(name).or_default().push(resource);
325
+ if let Some(&id) = self.anchor_ids.get(&name)
326
+ && id < resolved_names
327
+ {
328
+ pending.push((resource, id));
132
329
  }
133
330
  }
331
+ indexed.insert(resource, count);
332
+ }
333
+ let names = self.anchor_names.len();
334
+ for id in resolved_names..names {
335
+ if let Some(resources) = available.get(&self.anchor_names[id]) {
336
+ pending.extend(resources.iter().map(|&resource| (resource, id)));
337
+ }
338
+ }
339
+ resolved_names = names;
340
+ if pending.is_empty() {
341
+ self.finish_dynamic_bindings();
134
342
  return Ok(());
135
343
  }
136
344
  for (resource, name) in pending {
345
+ // Switching from single-name discovery can revisit a positive
346
+ // pair already compiled. It must not invoke compilation twice.
347
+ if self.dynamic_bindings.contains_key(&(resource, name)) {
348
+ continue;
349
+ }
137
350
  let mut target = 0;
138
351
  if unsafe {
139
352
  sn_anchor_program(
@@ -146,13 +359,11 @@ impl Engine {
146
359
  {
147
360
  return Err(target);
148
361
  }
149
- self.retain(target);
150
- let target = if unsafe { sn_same(target, sn_nil()) } != 0 {
151
- None
152
- } else {
153
- Some(self.compile(target)?)
154
- };
155
- self.dynamic_bindings.insert((resource, name), target);
362
+ if unsafe { sn_same(target, sn_nil()) } == 0 {
363
+ self.retain(target);
364
+ let target = self.compile(target)?;
365
+ self.dynamic_bindings.insert((resource, name), target);
366
+ }
156
367
  }
157
368
  }
158
369
  }
@@ -197,8 +408,7 @@ impl Engine {
197
408
  }
198
409
  }
199
410
  ReferenceKind::Dynamic(name) => {
200
- if let Some(Some(binding)) = self.dynamic_bindings.get(&(frame.resource, name))
201
- {
411
+ if let Some(binding) = self.dynamic_bindings.get(&(frame.resource, name)) {
202
412
  target = *binding;
203
413
  }
204
414
  }
@@ -0,0 +1,86 @@
1
+ //! One comparison/hash owns its cache. Ruby values cannot mutate or move
2
+ //! during these GVL-held traversals, which never call back into Ruby.
3
+ use std::collections::{HashMap, VecDeque};
4
+ use std::hash::Hash;
5
+
6
+ const MAX_ENTRIES: usize = 4096;
7
+
8
+ pub(super) struct Cache<K, T> {
9
+ start_depth: usize,
10
+ shallow_width: usize,
11
+ results: HashMap<K, T>,
12
+ order: VecDeque<K>,
13
+ }
14
+ impl<K, T> Cache<K, T> {
15
+ // A batch can share wide containers below distinct element roots. Keep
16
+ // small shallow trees cheap; four expanded levels are still a constant
17
+ // bound before depth-based reuse handles thin shared graphs.
18
+ pub(super) fn batch(depth: usize) -> Self {
19
+ let mut cache = Self::new(depth);
20
+ cache.shallow_width = 16;
21
+ cache
22
+ }
23
+ pub(super) fn new(depth: usize) -> Self {
24
+ Self {
25
+ start_depth: depth.saturating_add(4),
26
+ shallow_width: usize::MAX,
27
+ results: HashMap::new(),
28
+ order: VecDeque::new(),
29
+ }
30
+ }
31
+ // Shallow ordinary records/trees avoid table bookkeeping. At most four
32
+ // levels are expanded before reuse begins, independently of graph depth.
33
+ pub(super) fn eligible(&self, depth: usize) -> bool {
34
+ depth >= self.start_depth
35
+ }
36
+ pub(super) fn eligible_container(&self, depth: usize, width: usize) -> bool {
37
+ self.eligible(depth) || width >= self.shallow_width
38
+ }
39
+ }
40
+ impl<K: Copy + Eq + Hash, T: Copy> Cache<K, T> {
41
+ pub(super) fn get(&self, key: K) -> Option<T> {
42
+ self.results.get(&key).copied()
43
+ }
44
+ pub(super) fn record(&mut self, key: K, result: T) {
45
+ if self.results.contains_key(&key) {
46
+ return;
47
+ }
48
+ if self.results.len() == MAX_ENTRIES {
49
+ self.results.remove(&self.order.pop_front().unwrap());
50
+ }
51
+ self.results.insert(key, result);
52
+ self.order.push_back(key);
53
+ }
54
+ }
55
+
56
+ #[cfg(test)]
57
+ mod tests {
58
+ use super::*;
59
+
60
+ #[test]
61
+ fn batches_admit_wide_containers_without_charging_small_shallow_trees() {
62
+ let cache = Cache::<usize, usize>::batch(7);
63
+ assert!(!cache.eligible_container(7, 2));
64
+ assert!(!cache.eligible_container(10, 15));
65
+ assert!(cache.eligible_container(8, 16));
66
+ assert!(cache.eligible_container(11, 0));
67
+ assert!(!Cache::<usize, usize>::new(7).eligible_container(8, 1024));
68
+ }
69
+
70
+ #[test]
71
+ fn separates_depth_and_admits_later_shared_values() {
72
+ let mut cache = Cache::new(0);
73
+ for value in 0..MAX_ENTRIES * 3 {
74
+ cache.record((value, 0), Some(value));
75
+ cache.record((value, 0), None);
76
+ }
77
+ assert_eq!(cache.results.len(), MAX_ENTRIES);
78
+ assert_eq!(cache.order.len(), MAX_ENTRIES);
79
+ assert_eq!(cache.get((0, 0)), None);
80
+ assert_eq!(
81
+ cache.get((MAX_ENTRIES * 3 - 1, 0)),
82
+ Some(Some(MAX_ENTRIES * 3 - 1))
83
+ );
84
+ assert_eq!(cache.get((MAX_ENTRIES * 3 - 1, 1)), None);
85
+ }
86
+ }