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,420 @@
1
+ //! Resource/anchor lookup is compiled once. Evaluation only follows native
2
+ //! tables and stack frames, including when speculative branches collect annotations.
3
+ use super::*;
4
+ use tracking::JsonKey;
5
+ #[derive(Clone, Copy, Default)]
6
+ pub(super) struct Metadata {
7
+ pub resource: Value,
8
+ pub enters_scope: bool,
9
+ pub recursive_anchor: bool,
10
+ pub dynamic_anchor: Option<Value>,
11
+ pub primary_binding: Option<usize>,
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
+ }
92
+ #[derive(Clone, Copy, Debug)]
93
+ enum ReferenceKind {
94
+ Static,
95
+ Recursive,
96
+ Dynamic(usize),
97
+ }
98
+ #[derive(Clone, Copy, Debug)]
99
+ pub(super) struct Reference {
100
+ target: usize,
101
+ kind: ReferenceKind,
102
+ }
103
+ #[derive(Clone, Copy, Default)]
104
+ pub(super) struct Context<'a> {
105
+ pub active: Option<&'a RefFrame<'a>>,
106
+ pub scope: Option<&'a ScopeFrame<'a>>,
107
+ }
108
+ pub(super) struct ScopeFrame<'a> {
109
+ pub resource: Value,
110
+ pub parent: Option<&'a ScopeFrame<'a>>,
111
+ pub primary_binding: Option<usize>,
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
+ }
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
+
157
+ pub(super) fn compile_scope(
158
+ &mut self,
159
+ program: Value,
160
+ index: usize,
161
+ flags: i64,
162
+ ) -> Result<(), Value> {
163
+ let node = self.field(program, b"node\0")?;
164
+ let resource = self.field(node, b"resource\0")?;
165
+ let recursive = self.field(program, b"recursive_anchor?\0")?;
166
+ let dynamic = self.field(program, b"dynamic_anchor\0")?;
167
+ self.metadata[index] = Metadata {
168
+ resource,
169
+ enters_scope: flags & i64::from(TRACKS_DYNAMIC_SCOPE) != 0,
170
+ recursive_anchor: unsafe { sn_true(recursive) } != 0,
171
+ primary_binding: None,
172
+ dynamic_anchor: (unsafe { sn_same(dynamic, sn_nil()) } == 0).then_some(dynamic),
173
+ };
174
+ if !self.resources.contains_key(&resource) {
175
+ // Reserve the resource before compiling its root: roots can reach
176
+ // the current child through a recursive or dynamic reference.
177
+ self.resources.insert(resource, usize::MAX);
178
+ let mut root = 0;
179
+ if unsafe { sn_resource_program(self.compiler, resource, &mut root) } != 0 {
180
+ return Err(root);
181
+ }
182
+ self.retain(root);
183
+ let root = self.compile(root)?;
184
+ self.resources.insert(resource, root);
185
+ }
186
+ Ok(())
187
+ }
188
+ pub(super) fn compile_reference(
189
+ &mut self,
190
+ operand: Value,
191
+ opcode: i32,
192
+ target: usize,
193
+ ) -> Result<Op, Value> {
194
+ let fragment = self.field(operand, b"fragment\0")?;
195
+ let kind = if opcode == OP_RECURSIVE_REF
196
+ && unsafe { sn_same(fragment, sn_nil()) } == 0
197
+ && unsafe { sn_string_bytes(fragment) } == 0
198
+ && self.metadata[target].recursive_anchor
199
+ {
200
+ ReferenceKind::Recursive
201
+ } else if opcode == OP_DYNAMIC_REF {
202
+ let anchor = self.metadata[target].dynamic_anchor;
203
+ match anchor {
204
+ Some(anchor)
205
+ if unsafe { sn_same(fragment, sn_nil()) } == 0
206
+ && unsafe { sn_string_bytes(fragment) } != 0
207
+ && unsafe { *sn_string_data(fragment) } != b'/'
208
+ && unsafe { sn_string_equal(fragment, anchor) } != 0 =>
209
+ {
210
+ let name = JsonKey::new(fragment);
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
+ };
219
+ ReferenceKind::Dynamic(index)
220
+ }
221
+ _ => ReferenceKind::Static,
222
+ }
223
+ } else {
224
+ ReferenceKind::Static
225
+ };
226
+ Ok(Op::Ref(Reference { target, kind }))
227
+ }
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();
234
+ loop {
235
+ let names = self.anchor_names.len();
236
+ if names > 1 {
237
+ return self.bind_indexed_dynamic_anchors();
238
+ }
239
+ let pending: Vec<_> = self
240
+ .resources
241
+ .keys()
242
+ .filter_map(|&resource| {
243
+ let first = resolved.get(&resource).copied().unwrap_or(0);
244
+ (first < names).then_some((resource, first..names))
245
+ })
246
+ .collect();
247
+ if pending.is_empty() {
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);
270
+ }
271
+ }
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));
329
+ }
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();
342
+ return Ok(());
343
+ }
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
+ }
350
+ let mut target = 0;
351
+ if unsafe {
352
+ sn_anchor_program(
353
+ self.compiler,
354
+ resource,
355
+ self.anchor_names[name].0,
356
+ &mut target,
357
+ )
358
+ } != 0
359
+ {
360
+ return Err(target);
361
+ }
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
+ }
367
+ }
368
+ }
369
+ }
370
+ pub(super) fn enter_scope<'a>(
371
+ &self,
372
+ node: usize,
373
+ active: Context<'a>,
374
+ frame: &'a ScopeFrame<'a>,
375
+ ) -> Context<'a> {
376
+ if self.metadata[node].enters_scope
377
+ && active
378
+ .scope
379
+ .is_none_or(|scope| scope.resource != frame.resource)
380
+ {
381
+ Context {
382
+ scope: Some(frame),
383
+ ..active
384
+ }
385
+ } else {
386
+ active
387
+ }
388
+ }
389
+ pub(super) fn reference_target(&self, reference: Reference, context: Context<'_>) -> usize {
390
+ if matches!(reference.kind, ReferenceKind::Dynamic(0)) && self.anchor_tables.len() == 1 {
391
+ return context
392
+ .scope
393
+ .and_then(|frame| frame.primary_binding)
394
+ .unwrap_or(reference.target);
395
+ }
396
+ let mut target = reference.target;
397
+ let mut current = context.scope;
398
+ // Linked frames walk inner to outer. Keep replacing a match so the
399
+ // outermost matching resource wins, just as the VM's scope.each does.
400
+ while let Some(frame) = current {
401
+ match reference.kind {
402
+ ReferenceKind::Static => break,
403
+ ReferenceKind::Recursive => {
404
+ if let Some(&root) = self.resources.get(&frame.resource)
405
+ && self.metadata[root].recursive_anchor
406
+ {
407
+ target = root;
408
+ }
409
+ }
410
+ ReferenceKind::Dynamic(name) => {
411
+ if let Some(binding) = self.dynamic_bindings.get(&(frame.resource, name)) {
412
+ target = *binding;
413
+ }
414
+ }
415
+ }
416
+ current = frame.parent;
417
+ }
418
+ target
419
+ }
420
+ }
@@ -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
+ }