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,687 @@
1
+ //! Evaluated locations stay in native buffers, scoped to one instance location.
2
+ //! Keys remain borrowed VALUEs; no Ruby objects or instance trees are allocated.
3
+ use super::*;
4
+ use std::collections::HashSet;
5
+ use std::hash::Hash;
6
+
7
+ #[derive(Clone, Copy, Debug)]
8
+ pub(super) struct JsonKey(pub Value, u64);
9
+ impl JsonKey {
10
+ pub(super) fn new(value: Value) -> Self {
11
+ Self(value, string_fingerprint(value))
12
+ }
13
+ }
14
+ impl PartialEq for JsonKey {
15
+ fn eq(&self, other: &Self) -> bool {
16
+ self.0 == other.0 || unsafe { sn_string_equal(self.0, other.0) != 0 }
17
+ }
18
+ }
19
+ impl Eq for JsonKey {}
20
+ impl Hash for JsonKey {
21
+ fn hash<H: Hasher>(&self, state: &mut H) {
22
+ state.write_u64(self.1);
23
+ }
24
+ }
25
+ pub(super) type KeySet = HashSet<JsonKey, BuildHasherDefault<BucketHasher>>;
26
+
27
+ // Store only nonempty words. The common single-word case needs no allocation,
28
+ // even when its property IDs are near the end of a large compiled engine.
29
+ #[derive(Clone, Default)]
30
+ enum PropertyWords {
31
+ #[default]
32
+ Empty,
33
+ Single(usize, u64),
34
+ Dense(usize, Vec<u64>),
35
+ Many(HashMap<usize, u64>),
36
+ }
37
+ #[derive(Clone, Default)]
38
+ struct PropertyBits {
39
+ words: PropertyWords,
40
+ count: usize,
41
+ }
42
+ impl PropertyBits {
43
+ fn word(&self, index: usize) -> u64 {
44
+ match &self.words {
45
+ PropertyWords::Empty => 0,
46
+ PropertyWords::Single(at, bits) => {
47
+ if *at == index {
48
+ *bits
49
+ } else {
50
+ 0
51
+ }
52
+ }
53
+ PropertyWords::Dense(first, words) => index
54
+ .checked_sub(*first)
55
+ .and_then(|offset| words.get(offset))
56
+ .copied()
57
+ .unwrap_or(0),
58
+ PropertyWords::Many(words) => words.get(&index).copied().unwrap_or(0),
59
+ }
60
+ }
61
+ fn insert_word(&mut self, index: usize, bits: u64) {
62
+ let previous = self.word(index);
63
+ self.count += (bits & !previous).count_ones() as usize;
64
+ match &mut self.words {
65
+ PropertyWords::Empty => self.words = PropertyWords::Single(index, bits),
66
+ PropertyWords::Single(at, existing) if *at == index => *existing |= bits,
67
+ PropertyWords::Single(at, existing) if at.abs_diff(index) < 128 => {
68
+ let first = (*at).min(index);
69
+ let mut words = vec![0; at.abs_diff(index) + 1];
70
+ words[*at - first] = *existing;
71
+ words[index - first] = bits;
72
+ self.words = PropertyWords::Dense(first, words);
73
+ }
74
+ PropertyWords::Single(at, existing) => {
75
+ self.words = PropertyWords::Many(HashMap::from([(*at, *existing), (index, bits)]));
76
+ }
77
+ PropertyWords::Dense(first, words)
78
+ if index >= *first && index - *first < words.len() =>
79
+ {
80
+ words[index - *first] |= bits;
81
+ }
82
+ PropertyWords::Dense(first, words)
83
+ if index.max(*first + words.len() - 1) - index.min(*first) < 128 =>
84
+ {
85
+ if index < *first {
86
+ let offset = *first - index;
87
+ words.splice(0..0, std::iter::repeat_n(0, offset));
88
+ *first = index;
89
+ } else {
90
+ words.resize(index - *first + 1, 0);
91
+ }
92
+ words[index - *first] |= bits;
93
+ }
94
+ PropertyWords::Dense(first, words) => {
95
+ let mut sparse: HashMap<_, _> = words
96
+ .iter()
97
+ .enumerate()
98
+ .filter(|&(_, &word)| word != 0)
99
+ .map(|(offset, &word)| (*first + offset, word))
100
+ .collect();
101
+ sparse.insert(index, bits);
102
+ self.words = PropertyWords::Many(sparse);
103
+ }
104
+ PropertyWords::Many(words) => *words.entry(index).or_default() |= bits,
105
+ }
106
+ }
107
+ fn insert(&mut self, index: usize) {
108
+ self.insert_word(index / 64, 1_u64 << (index % 64));
109
+ }
110
+ fn contains(&self, index: usize) -> bool {
111
+ self.word(index / 64) & (1_u64 << (index % 64)) != 0
112
+ }
113
+ fn merge(&mut self, other: Self) {
114
+ if matches!(self.words, PropertyWords::Empty) {
115
+ *self = other;
116
+ return;
117
+ }
118
+ match other.words {
119
+ PropertyWords::Empty => {}
120
+ PropertyWords::Single(index, bits) => self.insert_word(index, bits),
121
+ PropertyWords::Dense(first, words) => {
122
+ for (offset, bits) in words.into_iter().enumerate() {
123
+ if bits != 0 {
124
+ self.insert_word(first + offset, bits);
125
+ }
126
+ }
127
+ }
128
+ PropertyWords::Many(words) => {
129
+ for (index, bits) in words {
130
+ self.insert_word(index, bits);
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ #[derive(Clone)]
138
+ pub(super) struct Tracked {
139
+ pub status: Verdict,
140
+ properties: KeySet,
141
+ known_properties: PropertyBits,
142
+ items: Vec<u64>,
143
+ }
144
+ impl Tracked {
145
+ // Bound retained annotation slots independently of the number of results.
146
+ pub(super) fn memo_slots(&self) -> usize {
147
+ self.properties.capacity()
148
+ + self.items.capacity()
149
+ + match &self.known_properties.words {
150
+ PropertyWords::Empty | PropertyWords::Single(..) => 0,
151
+ PropertyWords::Dense(_, words) => words.capacity(),
152
+ PropertyWords::Many(words) => words.capacity(),
153
+ }
154
+ }
155
+ pub(super) fn new(status: Verdict) -> Self {
156
+ Self {
157
+ status,
158
+ properties: KeySet::default(),
159
+ known_properties: PropertyBits::default(),
160
+ items: vec![],
161
+ }
162
+ }
163
+ pub(super) fn item(&mut self, index: usize) {
164
+ let word = index / 64;
165
+ if self.items.len() <= word {
166
+ self.items.resize(word + 1, 0);
167
+ }
168
+ self.items[word] |= 1_u64 << (index % 64);
169
+ }
170
+ pub(super) fn contains_item(&self, index: usize) -> bool {
171
+ self.items
172
+ .get(index / 64)
173
+ .is_some_and(|word| word & (1_u64 << (index % 64)) != 0)
174
+ }
175
+ fn known_property(&mut self, index: usize) {
176
+ self.known_properties.insert(index);
177
+ }
178
+ fn contains_known_property(&self, index: usize) -> bool {
179
+ self.known_properties.contains(index)
180
+ }
181
+ pub(super) fn property_count(&self) -> usize {
182
+ self.properties.len() + self.known_properties.count
183
+ }
184
+ pub(super) fn item_count(&self) -> usize {
185
+ self.items
186
+ .iter()
187
+ .map(|word| word.count_ones() as usize)
188
+ .sum()
189
+ }
190
+ pub(super) fn merge(&mut self, other: Self) -> Verdict {
191
+ if other.status != Valid {
192
+ return other.status;
193
+ }
194
+ if self.properties.is_empty() {
195
+ self.properties = other.properties;
196
+ } else {
197
+ self.properties.extend(other.properties);
198
+ }
199
+ self.known_properties.merge(other.known_properties);
200
+ merge_bits(&mut self.items, other.items);
201
+ Valid
202
+ }
203
+ pub(super) fn merge_diagnostics(&mut self, other: Self) -> Result<(), Verdict> {
204
+ if other.status == Unsupported {
205
+ return Err(Unsupported);
206
+ }
207
+ if self.status != Valid || other.status != Valid {
208
+ *self = Self::new(Invalid);
209
+ } else {
210
+ self.merge(other);
211
+ }
212
+ Ok(())
213
+ }
214
+ pub(super) fn property(&mut self, engine: &Engine, value: Value) {
215
+ let key = JsonKey::new(value);
216
+ if let Some(&index) = engine.property_ids.get(&key) {
217
+ self.known_property(index);
218
+ } else {
219
+ self.properties.insert(key);
220
+ }
221
+ }
222
+ pub(super) fn contains_property(&self, engine: &Engine, value: Value) -> bool {
223
+ let key = JsonKey::new(value);
224
+ engine
225
+ .property_ids
226
+ .get(&key)
227
+ .is_some_and(|&index| self.contains_known_property(index))
228
+ || self.properties.contains(&key)
229
+ }
230
+ }
231
+ fn merge_bits(target: &mut Vec<u64>, source: Vec<u64>) {
232
+ // Branch annotations own native buffers of location bits. Transfer an
233
+ // existing buffer into an empty accumulator instead of allocating a copy.
234
+ if target.is_empty() {
235
+ *target = source;
236
+ } else {
237
+ if target.len() < source.len() {
238
+ target.resize(source.len(), 0);
239
+ }
240
+ for (target, source) in target.iter_mut().zip(source) {
241
+ *target |= source;
242
+ }
243
+ }
244
+ }
245
+ impl Execution<'_> {
246
+ pub(super) fn eval_tracked(
247
+ &self,
248
+ node: usize,
249
+ value: Value,
250
+ depth: usize,
251
+ active: scope::Context<'_>,
252
+ ) -> Tracked {
253
+ if depth > MAX_EVALUATION_DEPTH {
254
+ return Tracked::new(Unsupported);
255
+ }
256
+ if !self.budget.checkpoint() {
257
+ return Tracked::new(Unsupported);
258
+ }
259
+ let kind = if value & self.fixnum_flag != 0 {
260
+ TYPE_INTEGRAL_NUMBER
261
+ } else {
262
+ unsafe { sn_kind(value) }
263
+ };
264
+ self.eval_tracked_kind(node, value, depth, active, kind)
265
+ }
266
+ // Applicator branches evaluate the same borrowed instance under the GVL.
267
+ // No Ruby callback can mutate it, so its guarded kind can be reused while
268
+ // each branch still enters its own resource scope and checks depth.
269
+ pub(super) fn eval_tracked_kind(
270
+ &self,
271
+ node: usize,
272
+ value: Value,
273
+ depth: usize,
274
+ active: scope::Context<'_>,
275
+ kind: i32,
276
+ ) -> Tracked {
277
+ if !self.budget.checkpoint() {
278
+ return Tracked::new(Unsupported);
279
+ }
280
+ let Some(key) = self
281
+ .memo
282
+ .key(self, node, value, depth, active, &self.budget)
283
+ else {
284
+ if self.budget.hit.get().0 != 0 {
285
+ return Tracked::new(Unsupported);
286
+ }
287
+ return self.eval_tracked_uncached(node, value, depth, active, kind);
288
+ };
289
+ if self.budget.hit.get().0 != 0 || !self.budget.checkpoint() {
290
+ return Tracked::new(Unsupported);
291
+ }
292
+ if let Some(result) = self.memo.tracked(key) {
293
+ return result;
294
+ }
295
+ let result = self.eval_tracked_uncached(node, value, depth, active, kind);
296
+ self.memo.record_tracked(key, &result);
297
+ result
298
+ }
299
+ fn eval_tracked_uncached(
300
+ &self,
301
+ node: usize,
302
+ value: Value,
303
+ depth: usize,
304
+ active: scope::Context<'_>,
305
+ kind: i32,
306
+ ) -> Tracked {
307
+ let frame = scope::ScopeFrame {
308
+ resource: self.metadata[node].resource,
309
+ parent: active.scope,
310
+ primary_binding: active
311
+ .scope
312
+ .and_then(|frame| frame.primary_binding)
313
+ .or(self.metadata[node].primary_binding),
314
+ };
315
+ let active = self.enter_scope(node, active, &frame);
316
+ if depth > MAX_EVALUATION_DEPTH {
317
+ return Tracked::new(Unsupported);
318
+ }
319
+ if kind == TYPE_UNSUPPORTED {
320
+ return Tracked::new(Unsupported);
321
+ }
322
+ let mut evaluation = Tracked::new(Valid);
323
+ for op in &self.nodes[node] {
324
+ if !self.budget.checkpoint() {
325
+ return Tracked::new(Unsupported);
326
+ }
327
+ let result = match op {
328
+ Op::Ref(target) => {
329
+ if RefFrame::contains(active.active, node, value) {
330
+ Valid
331
+ } else {
332
+ let frame = RefFrame {
333
+ source: node,
334
+ value,
335
+ parent: active.active,
336
+ };
337
+ evaluation.merge(self.eval_tracked_kind(
338
+ self.reference_target(*target, active),
339
+ value,
340
+ depth + 1,
341
+ scope::Context {
342
+ active: Some(&frame),
343
+ ..active
344
+ },
345
+ kind,
346
+ ))
347
+ }
348
+ }
349
+ Op::All(nodes) => {
350
+ let mut result = Valid;
351
+ for &child in nodes {
352
+ if !self.budget.checkpoint() {
353
+ return Tracked::new(Unsupported);
354
+ }
355
+ result = evaluation.merge(self.eval_tracked_kind(
356
+ child,
357
+ value,
358
+ depth + 1,
359
+ active,
360
+ kind,
361
+ ));
362
+ if result != Valid {
363
+ break;
364
+ }
365
+ }
366
+ result
367
+ }
368
+ Op::Any(nodes) | Op::One(nodes) => {
369
+ let branch_kind = if matches!(op, Op::Any(_)) {
370
+ evaluation::BranchKind::Any
371
+ } else {
372
+ evaluation::BranchKind::One
373
+ };
374
+ match evaluation::scan_branches(
375
+ nodes,
376
+ evaluation::BranchPolicy::Complete,
377
+ |child| self.eval_tracked_kind(child, value, depth + 1, active, kind),
378
+ ) {
379
+ Err(status) => status,
380
+ Ok(result) if !branch_kind.accepts(result.matches) => Invalid,
381
+ Ok(result) => evaluation.merge(result.coverage),
382
+ }
383
+ }
384
+ Op::Not(child) => match self.eval(*child, value, depth + 1, active) {
385
+ Valid => Invalid,
386
+ Invalid => Valid,
387
+ Unsupported => Unsupported,
388
+ },
389
+ Op::Conditional(condition, yes, no) => {
390
+ let condition =
391
+ self.eval_tracked_kind(*condition, value, depth + 1, active, kind);
392
+ let status = condition.status;
393
+ if status == Unsupported {
394
+ Unsupported
395
+ } else {
396
+ let branch = if status == Valid {
397
+ evaluation.merge(condition);
398
+ yes
399
+ } else {
400
+ no
401
+ };
402
+ if let Some(child) = branch {
403
+ evaluation.merge(self.eval_tracked_kind(
404
+ *child,
405
+ value,
406
+ depth + 1,
407
+ active,
408
+ kind,
409
+ ))
410
+ } else {
411
+ Valid
412
+ }
413
+ }
414
+ }
415
+ Op::Array(required, rules) => {
416
+ if *required != 0 && kind != TYPE_ARRAY {
417
+ Invalid
418
+ } else if kind != TYPE_ARRAY {
419
+ Valid
420
+ } else {
421
+ self.array_tracked(rules, value, depth + 1, active, &mut evaluation)
422
+ }
423
+ }
424
+ Op::Object(required, rules) => {
425
+ if *required != 0 && kind != TYPE_OBJECT {
426
+ Invalid
427
+ } else if kind != TYPE_OBJECT {
428
+ Valid
429
+ } else {
430
+ self.object_tracked(rules, value, depth + 1, active, &mut evaluation)
431
+ }
432
+ }
433
+ _ => self.leaf(op, kind, value, depth),
434
+ };
435
+ if result != Valid {
436
+ return Tracked::new(result);
437
+ }
438
+ }
439
+ evaluation
440
+ }
441
+ fn array_tracked(
442
+ &self,
443
+ r: &Array,
444
+ value: Value,
445
+ depth: usize,
446
+ active: scope::Context<'_>,
447
+ evaluation: &mut Tracked,
448
+ ) -> Verdict {
449
+ if !self.budget.checkpoint() {
450
+ return Unsupported;
451
+ }
452
+ let len = unsafe { sn_len(value) };
453
+ if bounds(len, r.min, r.max) == Invalid {
454
+ return Invalid;
455
+ }
456
+ if r.unique {
457
+ let result = unique(value, depth, self.fixnum_flag, &self.budget);
458
+ if result != Valid {
459
+ return result;
460
+ }
461
+ }
462
+ for (index, &child) in r.prefix.iter().take(len).enumerate() {
463
+ if !self.budget.checkpoint() {
464
+ return Unsupported;
465
+ }
466
+ let result = self.eval(child, unsafe { sn_entry(value, index) }, depth, active);
467
+ if result != Valid {
468
+ return result;
469
+ }
470
+ evaluation.item(index);
471
+ }
472
+ for index in 0..len {
473
+ if !self.budget.checkpoint() {
474
+ return Unsupported;
475
+ }
476
+ let child = if r.list {
477
+ r.items.get(index).copied().or(r.additional)
478
+ } else if index >= r.prefix.len() {
479
+ r.items.first().copied()
480
+ } else {
481
+ None
482
+ };
483
+ if let Some(child) = child {
484
+ let result = self.eval(child, unsafe { sn_entry(value, index) }, depth, active);
485
+ if result != Valid {
486
+ return result;
487
+ }
488
+ evaluation.item(index);
489
+ }
490
+ }
491
+ if let Some(child) = r.contains {
492
+ let count = match evaluation::scan_contains(
493
+ len,
494
+ |index| self.eval(child, unsafe { sn_entry(value, index) }, depth, active),
495
+ |index| evaluation.item(index),
496
+ ) {
497
+ Ok(count) => count,
498
+ Err(status) => return status,
499
+ };
500
+ if bounds(count, Some(r.min_contains), r.max_contains) == Invalid {
501
+ return Invalid;
502
+ }
503
+ }
504
+ if let Some(child) = r.unevaluated.filter(|_| evaluation.item_count() < len) {
505
+ if matches!(self.nodes[child].as_slice(), [Op::Boolean(false)]) {
506
+ return Invalid;
507
+ }
508
+ for index in 0..len {
509
+ if !self.budget.checkpoint() {
510
+ return Unsupported;
511
+ }
512
+ if evaluation.contains_item(index) {
513
+ continue;
514
+ }
515
+ let result = self.eval(child, unsafe { sn_entry(value, index) }, depth, active);
516
+ if result != Valid {
517
+ return result;
518
+ }
519
+ evaluation.item(index);
520
+ }
521
+ }
522
+ Valid
523
+ }
524
+ fn object_tracked(
525
+ &self,
526
+ r: &Object,
527
+ value: Value,
528
+ depth: usize,
529
+ active: scope::Context<'_>,
530
+ evaluation: &mut Tracked,
531
+ ) -> Verdict {
532
+ if !self.budget.checkpoint() {
533
+ return Unsupported;
534
+ }
535
+ let len = unsafe { sn_len(value) };
536
+ if bounds(len, r.min, r.max) == Invalid {
537
+ return Invalid;
538
+ }
539
+ let status = r.check_required(value, self.undefined, &self.budget);
540
+ if status != Valid {
541
+ return status;
542
+ }
543
+ let found = match r.visit_properties(
544
+ value,
545
+ len,
546
+ self.undefined,
547
+ &self.budget,
548
+ |index, child, item| {
549
+ let result = self.record_value(child, item, depth, active);
550
+ if result == Valid {
551
+ evaluation.known_property(r.property_indices[index]);
552
+ }
553
+ result
554
+ },
555
+ ) {
556
+ Ok(found) => found,
557
+ Err(status) => return status,
558
+ };
559
+ if r.names.is_some() || !r.patterns.is_empty() || (r.additional.is_some() && found < len) {
560
+ let result = self.tracked_object_walk(r, value, depth, active, evaluation, false);
561
+ if result != Valid {
562
+ return result;
563
+ }
564
+ }
565
+ let status = r.check_dependent_required(value, self.undefined, &self.budget);
566
+ if status != Valid {
567
+ return status;
568
+ }
569
+ for &(key, child) in &r.dependent_schemas {
570
+ if !self.budget.checkpoint() {
571
+ return Unsupported;
572
+ }
573
+ if !missing(value, key, self.undefined) {
574
+ let result = evaluation.merge(self.eval_tracked(child, value, depth, active));
575
+ if result != Valid {
576
+ return result;
577
+ }
578
+ }
579
+ }
580
+ if let Some(child) = r.unevaluated.filter(|_| evaluation.property_count() < len) {
581
+ if matches!(self.nodes[child].as_slice(), [Op::Boolean(false)]) {
582
+ return Invalid;
583
+ }
584
+ let result = self.tracked_object_walk(r, value, depth, active, evaluation, true);
585
+ if result != Valid {
586
+ return result;
587
+ }
588
+ }
589
+ Valid
590
+ }
591
+ fn tracked_object_walk(
592
+ &self,
593
+ rules: &Object,
594
+ value: Value,
595
+ depth: usize,
596
+ active: scope::Context<'_>,
597
+ evaluation: &mut Tracked,
598
+ unevaluated: bool,
599
+ ) -> Verdict {
600
+ let mut c = TrackingContext {
601
+ engine: self,
602
+ rules,
603
+ depth,
604
+ active,
605
+ evaluation,
606
+ unevaluated,
607
+ };
608
+ decode(unsafe {
609
+ sn_hash_each(
610
+ value,
611
+ (&mut c as *mut TrackingContext).cast(),
612
+ tracking_entry,
613
+ )
614
+ })
615
+ }
616
+ }
617
+ struct TrackingContext<'a> {
618
+ engine: &'a Execution<'a>,
619
+ rules: &'a Object,
620
+ depth: usize,
621
+ active: scope::Context<'a>,
622
+ evaluation: &'a mut Tracked,
623
+ unevaluated: bool,
624
+ }
625
+ unsafe extern "C" fn tracking_entry(context: *mut c_void, key: Value, value: Value) -> i32 {
626
+ catch_unwind(AssertUnwindSafe(|| {
627
+ let c = unsafe { &mut *context.cast::<TrackingContext>() };
628
+ if !c.engine.budget.checkpoint() {
629
+ return Unsupported as i32;
630
+ }
631
+ if unsafe { sn_kind(key) } != TYPE_STRING {
632
+ return Unsupported as i32;
633
+ }
634
+ let key_location = JsonKey::new(key);
635
+ let known = c.engine.property_ids.get(&key_location).copied();
636
+ let child = if c.unevaluated {
637
+ if known.map_or_else(
638
+ || c.evaluation.properties.contains(&key_location),
639
+ |index| c.evaluation.contains_known_property(index),
640
+ ) {
641
+ return Valid as i32;
642
+ }
643
+ c.rules.unevaluated
644
+ } else {
645
+ if let Some(child) = c.rules.names {
646
+ let result = c.engine.eval(child, key, c.depth, c.active);
647
+ if result != Valid {
648
+ return result as i32;
649
+ }
650
+ }
651
+ let result = c.rules.visit_value_rules::<false>(
652
+ c.engine.caches,
653
+ &c.engine.budget,
654
+ key,
655
+ Some(key_location),
656
+ |_, _, child| match c.engine.eval(child, value, c.depth, c.active) {
657
+ Valid => {
658
+ if let Some(index) = known {
659
+ c.evaluation.known_property(index);
660
+ } else {
661
+ c.evaluation.properties.insert(key_location);
662
+ }
663
+ Ok(())
664
+ }
665
+ status => Err(status),
666
+ },
667
+ );
668
+ return match result {
669
+ Ok(_) => Valid as i32,
670
+ Err(status) => status as i32,
671
+ };
672
+ };
673
+ if let Some(child) = child {
674
+ let result = c.engine.eval(child, value, c.depth, c.active);
675
+ if result != Valid {
676
+ return result as i32;
677
+ }
678
+ if let Some(index) = known {
679
+ c.evaluation.known_property(index);
680
+ } else {
681
+ c.evaluation.properties.insert(key_location);
682
+ }
683
+ }
684
+ Valid as i32
685
+ }))
686
+ .unwrap_or(Unsupported as i32)
687
+ }
@@ -3,7 +3,7 @@
3
3
  module Schemurai
4
4
  module Backend
5
5
  VM_FEATURE = "vm/evaluator"
6
- CHOICES = %i[default ruby vm].freeze
6
+ CHOICES = %i[default ruby vm native].freeze
7
7
 
8
8
  module_function def requested
9
9
  value = ENV.fetch("SCHEMURAI_BACKEND", "default").to_sym
@@ -18,6 +18,7 @@ module Schemurai
18
18
 
19
19
  selection = production_default if selection == :default
20
20
  load_vm! if selection == :vm
21
+ require_relative "native/evaluator" if selection == :native
21
22
  selection
22
23
  end
23
24
 
@@ -27,6 +28,7 @@ module Schemurai
27
28
 
28
29
  module_function def load_vm!
29
30
  require_relative VM_FEATURE
31
+ require_relative "vm/limited_evaluator"
30
32
  end
31
33
  end
32
34
  end