schemurai 2.3.0 → 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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +67 -4
  3. data/ext/schemurai_native/Cargo.lock +1 -1
  4. data/ext/schemurai_native/Cargo.toml +1 -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/diagnostics.rs +395 -225
  8. data/ext/schemurai_native/src/enumeration.rs +154 -0
  9. data/ext/schemurai_native/src/evaluation.rs +219 -0
  10. data/ext/schemurai_native/src/graph.rs +50 -0
  11. data/ext/schemurai_native/src/instance_graph.rs +84 -0
  12. data/ext/schemurai_native/src/lib.rs +947 -184
  13. data/ext/schemurai_native/src/memo.rs +1149 -0
  14. data/ext/schemurai_native/src/memo_scope.rs +240 -0
  15. data/ext/schemurai_native/src/number.rs +7 -8
  16. data/ext/schemurai_native/src/pattern.rs +157 -14
  17. data/ext/schemurai_native/src/scope.rs +243 -33
  18. data/ext/schemurai_native/src/structural.rs +86 -0
  19. data/ext/schemurai_native/src/tracking.rs +245 -90
  20. data/lib/schemurai/backend.rb +1 -0
  21. data/lib/schemurai/evaluator.rb +45 -3
  22. data/lib/schemurai/limit_budget.rb +78 -0
  23. data/lib/schemurai/limit_exceeded.rb +18 -0
  24. data/lib/schemurai/limited_evaluator.rb +100 -0
  25. data/lib/schemurai/native/evaluator.rb +32 -5
  26. data/lib/schemurai/schema_domain.rb +12 -7
  27. data/lib/schemurai/schema_graph.rb +10 -1
  28. data/lib/schemurai/unique_items.rb +2 -0
  29. data/lib/schemurai/version.rb +1 -1
  30. data/lib/schemurai/vm/compiler.rb +7 -3
  31. data/lib/schemurai/vm/evaluator.rb +50 -3
  32. data/lib/schemurai/vm/limited_evaluator.rb +97 -0
  33. data/lib/schemurai.rb +74 -27
  34. data/sig/schemurai.rbs +20 -12
  35. metadata +16 -4
@@ -24,18 +24,139 @@ impl Hash for JsonKey {
24
24
  }
25
25
  pub(super) type KeySet = HashSet<JsonKey, BuildHasherDefault<BucketHasher>>;
26
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)]
27
138
  pub(super) struct Tracked {
28
139
  pub status: Verdict,
29
140
  properties: KeySet,
30
- known_properties: Vec<u64>,
141
+ known_properties: PropertyBits,
31
142
  items: Vec<u64>,
32
143
  }
33
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
+ }
34
155
  pub(super) fn new(status: Verdict) -> Self {
35
156
  Self {
36
157
  status,
37
158
  properties: KeySet::default(),
38
- known_properties: vec![],
159
+ known_properties: PropertyBits::default(),
39
160
  items: vec![],
40
161
  }
41
162
  }
@@ -52,24 +173,13 @@ impl Tracked {
52
173
  .is_some_and(|word| word & (1_u64 << (index % 64)) != 0)
53
174
  }
54
175
  fn known_property(&mut self, index: usize) {
55
- let word = index / 64;
56
- if self.known_properties.len() <= word {
57
- self.known_properties.resize(word + 1, 0);
58
- }
59
- self.known_properties[word] |= 1_u64 << (index % 64);
176
+ self.known_properties.insert(index);
60
177
  }
61
178
  fn contains_known_property(&self, index: usize) -> bool {
62
- self.known_properties
63
- .get(index / 64)
64
- .is_some_and(|word| word & (1_u64 << (index % 64)) != 0)
179
+ self.known_properties.contains(index)
65
180
  }
66
181
  pub(super) fn property_count(&self) -> usize {
67
- self.properties.len()
68
- + self
69
- .known_properties
70
- .iter()
71
- .map(|word| word.count_ones() as usize)
72
- .sum::<usize>()
182
+ self.properties.len() + self.known_properties.count
73
183
  }
74
184
  pub(super) fn item_count(&self) -> usize {
75
185
  self.items
@@ -86,7 +196,7 @@ impl Tracked {
86
196
  } else {
87
197
  self.properties.extend(other.properties);
88
198
  }
89
- merge_bits(&mut self.known_properties, other.known_properties);
199
+ self.known_properties.merge(other.known_properties);
90
200
  merge_bits(&mut self.items, other.items);
91
201
  Valid
92
202
  }
@@ -132,7 +242,7 @@ fn merge_bits(target: &mut Vec<u64>, source: Vec<u64>) {
132
242
  }
133
243
  }
134
244
  }
135
- impl Engine {
245
+ impl Execution<'_> {
136
246
  pub(super) fn eval_tracked(
137
247
  &self,
138
248
  node: usize,
@@ -143,6 +253,9 @@ impl Engine {
143
253
  if depth > MAX_EVALUATION_DEPTH {
144
254
  return Tracked::new(Unsupported);
145
255
  }
256
+ if !self.budget.checkpoint() {
257
+ return Tracked::new(Unsupported);
258
+ }
146
259
  let kind = if value & self.fixnum_flag != 0 {
147
260
  TYPE_INTEGRAL_NUMBER
148
261
  } else {
@@ -160,6 +273,36 @@ impl Engine {
160
273
  depth: usize,
161
274
  active: scope::Context<'_>,
162
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,
163
306
  ) -> Tracked {
164
307
  let frame = scope::ScopeFrame {
165
308
  resource: self.metadata[node].resource,
@@ -178,6 +321,9 @@ impl Engine {
178
321
  }
179
322
  let mut evaluation = Tracked::new(Valid);
180
323
  for op in &self.nodes[node] {
324
+ if !self.budget.checkpoint() {
325
+ return Tracked::new(Unsupported);
326
+ }
181
327
  let result = match op {
182
328
  Op::Ref(target) => {
183
329
  if RefFrame::contains(active.active, node, value) {
@@ -203,6 +349,9 @@ impl Engine {
203
349
  Op::All(nodes) => {
204
350
  let mut result = Valid;
205
351
  for &child in nodes {
352
+ if !self.budget.checkpoint() {
353
+ return Tracked::new(Unsupported);
354
+ }
206
355
  result = evaluation.merge(self.eval_tracked_kind(
207
356
  child,
208
357
  value,
@@ -217,26 +366,19 @@ impl Engine {
217
366
  result
218
367
  }
219
368
  Op::Any(nodes) | Op::One(nodes) => {
220
- let mut matched = Tracked::new(Valid);
221
- let mut count = 0;
222
- let mut result = Valid;
223
- for &(child, multiplicity) in nodes {
224
- let branch = self.eval_tracked_kind(child, value, depth + 1, active, kind);
225
- if branch.status == Unsupported {
226
- result = Unsupported;
227
- break;
228
- }
229
- if branch.status == Valid {
230
- count += multiplicity;
231
- matched.merge(branch);
232
- }
233
- }
234
- if result != Valid {
235
- result
236
- } else if count == 0 || (matches!(op, Op::One(_)) && count != 1) {
237
- Invalid
369
+ let branch_kind = if matches!(op, Op::Any(_)) {
370
+ evaluation::BranchKind::Any
238
371
  } else {
239
- evaluation.merge(matched)
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),
240
382
  }
241
383
  }
242
384
  Op::Not(child) => match self.eval(*child, value, depth + 1, active) {
@@ -304,17 +446,23 @@ impl Engine {
304
446
  active: scope::Context<'_>,
305
447
  evaluation: &mut Tracked,
306
448
  ) -> Verdict {
449
+ if !self.budget.checkpoint() {
450
+ return Unsupported;
451
+ }
307
452
  let len = unsafe { sn_len(value) };
308
453
  if bounds(len, r.min, r.max) == Invalid {
309
454
  return Invalid;
310
455
  }
311
456
  if r.unique {
312
- let result = unique(value, depth, self.fixnum_flag);
457
+ let result = unique(value, depth, self.fixnum_flag, &self.budget);
313
458
  if result != Valid {
314
459
  return result;
315
460
  }
316
461
  }
317
462
  for (index, &child) in r.prefix.iter().take(len).enumerate() {
463
+ if !self.budget.checkpoint() {
464
+ return Unsupported;
465
+ }
318
466
  let result = self.eval(child, unsafe { sn_entry(value, index) }, depth, active);
319
467
  if result != Valid {
320
468
  return result;
@@ -322,6 +470,9 @@ impl Engine {
322
470
  evaluation.item(index);
323
471
  }
324
472
  for index in 0..len {
473
+ if !self.budget.checkpoint() {
474
+ return Unsupported;
475
+ }
325
476
  let child = if r.list {
326
477
  r.items.get(index).copied().or(r.additional)
327
478
  } else if index >= r.prefix.len() {
@@ -338,17 +489,14 @@ impl Engine {
338
489
  }
339
490
  }
340
491
  if let Some(child) = r.contains {
341
- let mut count = 0;
342
- for index in 0..len {
343
- match self.eval(child, unsafe { sn_entry(value, index) }, depth, active) {
344
- Valid => {
345
- count += 1;
346
- evaluation.item(index);
347
- }
348
- Invalid => {}
349
- Unsupported => return Unsupported,
350
- }
351
- }
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
+ };
352
500
  if bounds(count, Some(r.min_contains), r.max_contains) == Invalid {
353
501
  return Invalid;
354
502
  }
@@ -358,6 +506,9 @@ impl Engine {
358
506
  return Invalid;
359
507
  }
360
508
  for index in 0..len {
509
+ if !self.budget.checkpoint() {
510
+ return Unsupported;
511
+ }
361
512
  if evaluation.contains_item(index) {
362
513
  continue;
363
514
  }
@@ -378,47 +529,47 @@ impl Engine {
378
529
  active: scope::Context<'_>,
379
530
  evaluation: &mut Tracked,
380
531
  ) -> Verdict {
532
+ if !self.budget.checkpoint() {
533
+ return Unsupported;
534
+ }
381
535
  let len = unsafe { sn_len(value) };
382
536
  if bounds(len, r.min, r.max) == Invalid {
383
537
  return Invalid;
384
538
  }
385
- for &key in &r.required {
386
- if missing(value, key, self.undefined) {
387
- return Invalid;
388
- }
539
+ let status = r.check_required(value, self.undefined, &self.budget);
540
+ if status != Valid {
541
+ return status;
389
542
  }
390
- let mut found = 0;
391
- for (index, &(key, child)) in r.properties.iter().enumerate() {
392
- let item = unsafe { sn_lookup(value, key) };
393
- if item == self.undefined {
394
- if r.required_properties[index] {
395
- return Invalid;
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]);
396
552
  }
397
- continue;
398
- }
399
- found += 1;
400
- let result = self.record_value(child, item, depth, active);
401
- if result != Valid {
402
- return result;
403
- }
404
- evaluation.known_property(r.property_indices[index]);
405
- }
553
+ result
554
+ },
555
+ ) {
556
+ Ok(found) => found,
557
+ Err(status) => return status,
558
+ };
406
559
  if r.names.is_some() || !r.patterns.is_empty() || (r.additional.is_some() && found < len) {
407
560
  let result = self.tracked_object_walk(r, value, depth, active, evaluation, false);
408
561
  if result != Valid {
409
562
  return result;
410
563
  }
411
564
  }
412
- for (name, required) in &r.dependent_required {
413
- if !missing(value, *name, self.undefined) {
414
- for &key in required {
415
- if missing(value, key, self.undefined) {
416
- return Invalid;
417
- }
418
- }
419
- }
565
+ let status = r.check_dependent_required(value, self.undefined, &self.budget);
566
+ if status != Valid {
567
+ return status;
420
568
  }
421
569
  for &(key, child) in &r.dependent_schemas {
570
+ if !self.budget.checkpoint() {
571
+ return Unsupported;
572
+ }
422
573
  if !missing(value, key, self.undefined) {
423
574
  let result = evaluation.merge(self.eval_tracked(child, value, depth, active));
424
575
  if result != Valid {
@@ -464,7 +615,7 @@ impl Engine {
464
615
  }
465
616
  }
466
617
  struct TrackingContext<'a> {
467
- engine: &'a Engine,
618
+ engine: &'a Execution<'a>,
468
619
  rules: &'a Object,
469
620
  depth: usize,
470
621
  active: scope::Context<'a>,
@@ -474,6 +625,9 @@ struct TrackingContext<'a> {
474
625
  unsafe extern "C" fn tracking_entry(context: *mut c_void, key: Value, value: Value) -> i32 {
475
626
  catch_unwind(AssertUnwindSafe(|| {
476
627
  let c = unsafe { &mut *context.cast::<TrackingContext>() };
628
+ if !c.engine.budget.checkpoint() {
629
+ return Unsupported as i32;
630
+ }
477
631
  if unsafe { sn_kind(key) } != TYPE_STRING {
478
632
  return Unsupported as i32;
479
633
  }
@@ -494,26 +648,27 @@ unsafe extern "C" fn tracking_entry(context: *mut c_void, key: Value, value: Val
494
648
  return result as i32;
495
649
  }
496
650
  }
497
- let mut matched = c.rules.property_keys.contains(&key_location);
498
- for (pattern, child) in &c.rules.patterns {
499
- match pattern.eval(key) {
500
- Unsupported => return Unsupported as i32,
501
- Invalid => {}
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) {
502
657
  Valid => {
503
- matched = true;
504
- let result = c.engine.eval(*child, value, c.depth, c.active);
505
- if result != Valid {
506
- return result as i32;
507
- }
508
658
  if let Some(index) = known {
509
659
  c.evaluation.known_property(index);
510
660
  } else {
511
661
  c.evaluation.properties.insert(key_location);
512
662
  }
663
+ Ok(())
513
664
  }
514
- }
515
- }
516
- if matched { None } else { c.rules.additional }
665
+ status => Err(status),
666
+ },
667
+ );
668
+ return match result {
669
+ Ok(_) => Valid as i32,
670
+ Err(status) => status as i32,
671
+ };
517
672
  };
518
673
  if let Some(child) = child {
519
674
  let result = c.engine.eval(child, value, c.depth, c.active);
@@ -28,6 +28,7 @@ module Schemurai
28
28
 
29
29
  module_function def load_vm!
30
30
  require_relative VM_FEATURE
31
+ require_relative "vm/limited_evaluator"
31
32
  end
32
33
  end
33
34
  end