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
@@ -5,9 +5,17 @@ use std::collections::HashMap;
5
5
  use std::ffi::{c_char, c_void};
6
6
  use std::hash::{BuildHasherDefault, Hasher};
7
7
  use std::panic::{AssertUnwindSafe, catch_unwind};
8
+ mod budget;
8
9
  mod content;
9
10
  mod diagnostics;
11
+ mod enumeration;
12
+ mod evaluation;
10
13
  mod format;
14
+ mod graph;
15
+ mod instance_graph;
16
+ mod integer_scan;
17
+ mod memo;
18
+ mod memo_scope;
11
19
  mod number;
12
20
  mod pattern;
13
21
  // Some shared constants are consumed only by C or Ruby.
@@ -17,6 +25,7 @@ mod protocol {
17
25
  }
18
26
  use protocol::*;
19
27
  mod scope;
28
+ mod structural;
20
29
  mod tracking;
21
30
  type Value = usize;
22
31
  unsafe extern "C" {
@@ -55,7 +64,6 @@ unsafe extern "C" {
55
64
  fn sn_big_equal(big: Value, other: Value) -> i32;
56
65
  fn sn_big_order(left: Value, right: Value) -> i32;
57
66
  fn sn_wide_integer_compare(value: Value, bound: Value) -> i32;
58
- fn sn_integer_bytes(value: Value) -> usize;
59
67
  fn sn_float(value: Value) -> f64;
60
68
  fn sn_is_float(value: Value) -> i32;
61
69
  fn sn_is_rational(value: Value) -> i32;
@@ -80,6 +88,8 @@ unsafe extern "C" {
80
88
  fn sn_resolve(compiler: Value, program: Value, reference: Value, result: *mut Value) -> i32;
81
89
  fn sn_resolution_error(error: Value) -> i32;
82
90
  fn sn_resource_program(compiler: Value, resource: Value, result: *mut Value) -> i32;
91
+ fn sn_anchor_count(resource: Value) -> usize;
92
+ fn sn_anchor_names(compiler: Value, resource: Value, result: *mut Value) -> i32;
83
93
  fn sn_anchor_program(compiler: Value, resource: Value, name: Value, result: *mut Value) -> i32;
84
94
  fn sn_hash_each(
85
95
  value: Value,
@@ -114,7 +124,7 @@ enum Op {
114
124
  Ref(scope::Reference),
115
125
  Type(i32),
116
126
  Const(Value),
117
- Enum(Vec<Value>),
127
+ Enum(Box<enumeration::Enumeration>),
118
128
  All(Vec<usize>),
119
129
  Any(Vec<(usize, usize)>),
120
130
  One(Vec<(usize, usize)>),
@@ -190,7 +200,7 @@ impl StringRules {
190
200
  fn eval_length_guarded(&self, required: i32, value: Value) -> Verdict {
191
201
  length_guarded(value, required, self.min, self.max)
192
202
  }
193
- fn eval_guarded(&self, required: i32, value: Value) -> Verdict {
203
+ fn eval_guarded(&self, caches: &pattern::Caches, required: i32, value: Value) -> Verdict {
194
204
  let mut data = std::ptr::null();
195
205
  let mut len = 0;
196
206
  let encoding = unsafe { sn_pattern_item_view(value, &mut data, &mut len) };
@@ -200,14 +210,15 @@ impl StringRules {
200
210
  verdict(required == 0)
201
211
  } else {
202
212
  let bytes = unsafe { byte_view(data, len) };
203
- self.eval_with_view::<true>(value, Some((bytes, encoding)))
213
+ self.eval_with_view::<true>(caches, value, Some((bytes, encoding)))
204
214
  }
205
215
  }
206
- fn eval(&self, value: Value) -> Verdict {
207
- self.eval_with_view::<false>(value, None)
216
+ fn eval(&self, caches: &pattern::Caches, value: Value) -> Verdict {
217
+ self.eval_with_view::<false>(caches, value, None)
208
218
  }
209
219
  fn eval_with_view<const BORROWED: bool>(
210
220
  &self,
221
+ caches: &pattern::Caches,
211
222
  value: Value,
212
223
  view: Option<(&[u8], i32)>,
213
224
  ) -> Verdict {
@@ -221,8 +232,8 @@ impl StringRules {
221
232
  }
222
233
  if let Some(pattern) = &self.pattern {
223
234
  let result = match view {
224
- Some((bytes, encoding)) => pattern.eval_bytes(bytes, encoding),
225
- None => pattern.eval(value),
235
+ Some((bytes, encoding)) => pattern.eval_bytes(caches, bytes, encoding),
236
+ None => pattern.eval(caches, value),
226
237
  };
227
238
  if result != Valid {
228
239
  return result;
@@ -248,6 +259,12 @@ enum DiagnosticDependency {
248
259
  Required(Value, Vec<Value>, &'static str),
249
260
  Schema(Value, usize, &'static str),
250
261
  }
262
+ #[derive(Clone, Copy, Debug)]
263
+ struct PropertyTarget {
264
+ name: Value,
265
+ child: usize,
266
+ index: usize,
267
+ }
251
268
  #[derive(Debug)]
252
269
  struct Object {
253
270
  record: Option<RecordObject>,
@@ -256,10 +273,12 @@ struct Object {
256
273
  required: Vec<Value>,
257
274
  required_order: Vec<Value>,
258
275
  properties: Vec<(Value, usize)>,
259
- property_targets: HashMap<tracking::JsonKey, (Value, usize), BuildHasherDefault<BucketHasher>>,
276
+ property_targets: HashMap<tracking::JsonKey, PropertyTarget, BuildHasherDefault<BucketHasher>>,
260
277
  patterns: Vec<(pattern::Pattern, usize)>,
261
278
  pattern_names: Vec<Value>,
279
+ pattern_group: Option<pattern::PatternGroup>,
262
280
  required_properties: Vec<bool>,
281
+ required_property_indices: Vec<usize>,
263
282
  property_keys: tracking::KeySet,
264
283
  property_indices: Vec<usize>,
265
284
  additional: Option<usize>,
@@ -331,6 +350,203 @@ impl RecordShortIndex {
331
350
  }
332
351
  }
333
352
  impl Object {
353
+ // Keep these calls at their original evaluation positions: property
354
+ // failures and unsupported children must retain their ordering.
355
+ #[inline]
356
+ fn check_required(&self, value: Value, undefined: Value, budget: &budget::Budget) -> Verdict {
357
+ Self::check_keys(&self.required, value, undefined, budget)
358
+ }
359
+ #[inline]
360
+ fn check_keys(
361
+ keys: &[Value],
362
+ value: Value,
363
+ undefined: Value,
364
+ budget: &budget::Budget,
365
+ ) -> Verdict {
366
+ for &key in keys {
367
+ if !budget.checkpoint() {
368
+ return Unsupported;
369
+ }
370
+ if missing(value, key, undefined) {
371
+ return Invalid;
372
+ }
373
+ }
374
+ Valid
375
+ }
376
+ #[inline]
377
+ fn check_dependent_required(
378
+ &self,
379
+ value: Value,
380
+ undefined: Value,
381
+ budget: &budget::Budget,
382
+ ) -> Verdict {
383
+ for (name, required) in &self.dependent_required {
384
+ if !budget.checkpoint() {
385
+ return Unsupported;
386
+ }
387
+ if !missing(value, *name, undefined) {
388
+ let status = Self::check_keys(required, value, undefined, budget);
389
+ if status != Valid {
390
+ return status;
391
+ }
392
+ }
393
+ }
394
+ Valid
395
+ }
396
+ fn visit_properties(
397
+ &self,
398
+ value: Value,
399
+ len: usize,
400
+ undefined: Value,
401
+ budget: &budget::Budget,
402
+ mut visit: impl FnMut(usize, usize, Value) -> Verdict,
403
+ ) -> Result<usize, Verdict> {
404
+ let mut found = 0;
405
+ if self.properties.len() > len.saturating_mul(4).max(8)
406
+ && self.property_targets.len() == self.properties.len()
407
+ {
408
+ // Discover only present schema fields, then retain schema order for
409
+ // validity, including required failures and unsupported children.
410
+ let mut candidates = PropertyCandidates {
411
+ rules: self,
412
+ budget,
413
+ indices: Vec::with_capacity(len.min(self.properties.len())),
414
+ };
415
+ let status = decode(unsafe {
416
+ sn_hash_each(
417
+ value,
418
+ (&mut candidates as *mut PropertyCandidates).cast(),
419
+ property_candidate,
420
+ )
421
+ });
422
+ if status != Valid {
423
+ return Err(status);
424
+ }
425
+ candidates.indices.sort_unstable();
426
+ candidates.indices.dedup();
427
+ let mut missing_required = None;
428
+ for &index in &self.required_property_indices {
429
+ if !budget.checkpoint() {
430
+ return Err(Unsupported);
431
+ }
432
+ if missing(value, self.properties[index].0, undefined) {
433
+ missing_required = Some(index);
434
+ break;
435
+ }
436
+ }
437
+ for index in candidates.indices {
438
+ if !budget.checkpoint() {
439
+ return Err(Unsupported);
440
+ }
441
+ if missing_required.is_some_and(|missing| missing <= index) {
442
+ return Err(Invalid);
443
+ }
444
+ let (key, child) = self.properties[index];
445
+ // Use the schema handle for the final lookup to retain identity
446
+ // Hash semantics, even when an equal input key found the index.
447
+ let item = unsafe { sn_lookup(value, key) };
448
+ if item == undefined {
449
+ continue;
450
+ }
451
+ found += 1;
452
+ match visit(index, child, item) {
453
+ Valid => {}
454
+ status => return Err(status),
455
+ }
456
+ }
457
+ if missing_required.is_some() {
458
+ return Err(Invalid);
459
+ }
460
+ } else {
461
+ for (index, &(key, child)) in self.properties.iter().enumerate() {
462
+ if !budget.checkpoint() {
463
+ return Err(Unsupported);
464
+ }
465
+ let item = unsafe { sn_lookup(value, key) };
466
+ if item == undefined {
467
+ if self.required_properties[index] {
468
+ return Err(Invalid);
469
+ }
470
+ continue;
471
+ }
472
+ found += 1;
473
+ match visit(index, child, item) {
474
+ Valid => {}
475
+ status => return Err(status),
476
+ }
477
+ }
478
+ }
479
+ Ok(found)
480
+ }
481
+
482
+ // All execution policies use the same property/pattern/additional routing.
483
+ // Schema-order validity scans have already visited declared properties;
484
+ // input-order diagnostic scans visit them here as well.
485
+ fn visit_value_rules<const PROPERTIES: bool>(
486
+ &self,
487
+ caches: &pattern::Caches,
488
+ budget: &budget::Budget,
489
+ key: Value,
490
+ location: Option<tracking::JsonKey>,
491
+ mut visit: impl FnMut(&'static str, Option<Value>, usize) -> Result<(), Verdict>,
492
+ ) -> Result<bool, Verdict> {
493
+ // Tracked scans already fingerprinted this key for location lookup.
494
+ // Reuse that result, and skip hashing for pattern-only object rules.
495
+ let mut matched = if self.properties.is_empty() {
496
+ false
497
+ } else if PROPERTIES {
498
+ if let Some(target) = self
499
+ .property_targets
500
+ .get(&location.unwrap_or_else(|| tracking::JsonKey::new(key)))
501
+ {
502
+ visit("properties", Some(target.name), target.child)?;
503
+ true
504
+ } else {
505
+ false
506
+ }
507
+ } else {
508
+ self.property_keys
509
+ .contains(&location.unwrap_or_else(|| tracking::JsonKey::new(key)))
510
+ };
511
+ if let Some(mut matches) = self
512
+ .pattern_group
513
+ .as_ref()
514
+ .and_then(|group| group.matches(key))
515
+ {
516
+ while matches != 0 {
517
+ if !budget.checkpoint() {
518
+ return Err(Unsupported);
519
+ }
520
+ let index = matches.trailing_zeros() as usize;
521
+ matches &= matches - 1;
522
+ matched = true;
523
+ visit(
524
+ "patternProperties",
525
+ Some(self.pattern_names[index]),
526
+ self.patterns[index].1,
527
+ )?;
528
+ }
529
+ } else {
530
+ for ((pattern, child), &name) in self.patterns.iter().zip(&self.pattern_names) {
531
+ if !budget.checkpoint() {
532
+ return Err(Unsupported);
533
+ }
534
+ match pattern.eval(caches, key) {
535
+ Unsupported => return Err(Unsupported),
536
+ Invalid => {}
537
+ Valid => {
538
+ matched = true;
539
+ visit("patternProperties", Some(name), *child)?;
540
+ }
541
+ }
542
+ }
543
+ }
544
+ if !matched && let Some(child) = self.additional {
545
+ visit("additionalProperties", None, child)?;
546
+ }
547
+ Ok(matched || self.additional.is_some())
548
+ }
549
+
334
550
  fn with_record(mut self) -> Self {
335
551
  if self.required_order.len() > 64
336
552
  || !self.patterns.is_empty()
@@ -394,16 +610,27 @@ impl Object {
394
610
  #[derive(Default)]
395
611
  pub struct Roots {
396
612
  values: Vec<Value>,
613
+ binding_names: Vec<Value>,
397
614
  }
398
615
  #[derive(Default)]
399
616
  pub struct Engine {
400
617
  nodes: Vec<Vec<Op>>,
618
+ pattern_cache_count: usize,
401
619
  error_operands: Vec<Vec<(i32, Value)>>,
402
620
  tracks: Vec<bool>,
621
+ memoizable: Vec<bool>,
622
+ memo_contextual: Vec<bool>,
623
+ memo_guarded: Vec<bool>,
624
+ memo_guard_sources: Vec<bool>,
625
+ memo_guard_same_sources: Vec<bool>,
626
+ memo_guard_scalar_sources: Vec<bool>,
627
+ memo_guard_scalar_amplification: Vec<bool>,
628
+ memo_scope_projection: memo_scope::Projection,
403
629
  metadata: Vec<scope::Metadata>,
404
630
  resources: HashMap<Value, usize>,
405
- dynamic_bindings: HashMap<(Value, usize), Option<usize>>,
631
+ dynamic_bindings: HashMap<(Value, usize), usize>,
406
632
  anchor_names: Vec<tracking::JsonKey>,
633
+ anchor_ids: HashMap<tracking::JsonKey, usize, BuildHasherDefault<BucketHasher>>,
407
634
  anchor_tables: Vec<Vec<(Value, usize)>>,
408
635
  property_ids: HashMap<tracking::JsonKey, usize, BuildHasherDefault<BucketHasher>>,
409
636
  indices: HashMap<Value, usize>,
@@ -580,7 +807,10 @@ impl Engine {
580
807
  Op::Type(unsafe { sn_integer(mask) } as i32)
581
808
  }
582
809
  OP_CONST => Op::Const(operand),
583
- OP_ENUM => Op::Enum(self.values(operand)),
810
+ OP_ENUM => Op::Enum(Box::new(enumeration::Enumeration::compile(
811
+ self.values(operand),
812
+ self.fixnum_flag,
813
+ ))),
584
814
  OP_ALL_OF => Op::All(self.children(operand)?),
585
815
  OP_ANY_OF => Op::Any(self.branches(operand)?),
586
816
  OP_ONE_OF => Op::One(self.branches(operand)?),
@@ -652,7 +882,10 @@ impl Engine {
652
882
  pattern: if unsafe { sn_same(pattern, sn_nil()) } != 0 {
653
883
  None
654
884
  } else {
655
- Some(pattern::Pattern::compile(pattern))
885
+ Some(pattern::Pattern::compile(
886
+ pattern,
887
+ &mut self.pattern_cache_count,
888
+ ))
656
889
  },
657
890
  }),
658
891
  )
@@ -698,11 +931,18 @@ impl Engine {
698
931
  let dependencies = self.field(operand, b"dependencies\0")?;
699
932
  {
700
933
  let patterns = self.compile_map(patterns)?;
701
- let pattern_names = patterns.iter().map(|&(key, _)| key).collect();
702
- let patterns = patterns
934
+ let pattern_names: Vec<_> = patterns.iter().map(|&(key, _)| key).collect();
935
+ let patterns: Vec<_> = patterns
703
936
  .into_iter()
704
- .map(|(key, child)| (pattern::Pattern::compile(key), child))
937
+ .map(|(key, child)| {
938
+ (
939
+ pattern::Pattern::compile(key, &mut self.pattern_cache_count),
940
+ child,
941
+ )
942
+ })
705
943
  .collect();
944
+ let pattern_group =
945
+ pattern::PatternGroup::compile(&pattern_names, &patterns);
706
946
  let required = self.field(operand, b"required\0")?;
707
947
  let required = self.values(required);
708
948
  let required_order = required.clone();
@@ -716,10 +956,17 @@ impl Engine {
716
956
  ops.push(Op::Unsupported);
717
957
  continue;
718
958
  }
719
- let property_targets = properties
720
- .iter()
721
- .map(|&(key, child)| (tracking::JsonKey::new(key), (key, child)))
722
- .collect();
959
+ let property_targets: HashMap<_, _, BuildHasherDefault<BucketHasher>> =
960
+ properties
961
+ .iter()
962
+ .enumerate()
963
+ .map(|(index, &(name, child))| {
964
+ (
965
+ tracking::JsonKey::new(name),
966
+ PropertyTarget { name, child, index },
967
+ )
968
+ })
969
+ .collect();
723
970
  let property_locations: Vec<_> = properties
724
971
  .iter()
725
972
  .map(|&(key, _)| tracking::JsonKey::new(key))
@@ -732,20 +979,43 @@ impl Engine {
732
979
  *self.property_ids.entry(key).or_insert(next)
733
980
  })
734
981
  .collect();
735
- let required_properties = properties
982
+ // Keep tiny schemas allocation-cheap; larger joins use
983
+ // content-keyed sets and the already compiled property index.
984
+ let indexed_required = properties.len().saturating_mul(required.len()) > 64;
985
+ let required_keys: Option<tracking::KeySet> = indexed_required.then(|| {
986
+ required
987
+ .iter()
988
+ .map(|&name| tracking::JsonKey::new(name))
989
+ .collect()
990
+ });
991
+ let required_properties: Vec<bool> = property_locations
736
992
  .iter()
737
- .map(|&(key, _)| {
738
- required
739
- .iter()
740
- .any(|&name| unsafe { sn_string_equal(key, name) } != 0)
993
+ .map(|key| {
994
+ required_keys.as_ref().map_or_else(
995
+ || {
996
+ required.iter().any(
997
+ |&name| unsafe { sn_string_equal(key.0, name) } != 0,
998
+ )
999
+ },
1000
+ |names| names.contains(key),
1001
+ )
741
1002
  })
742
1003
  .collect();
1004
+ let required_property_indices = required_properties
1005
+ .iter()
1006
+ .enumerate()
1007
+ .filter_map(|(index, &required)| required.then_some(index))
1008
+ .collect();
743
1009
  let required = required
744
1010
  .into_iter()
745
1011
  .filter(|&name| {
746
- !properties
747
- .iter()
748
- .any(|&(key, _)| unsafe { sn_string_equal(key, name) } != 0)
1012
+ if indexed_required {
1013
+ !property_targets.contains_key(&tracking::JsonKey::new(name))
1014
+ } else {
1015
+ !properties
1016
+ .iter()
1017
+ .any(|&(key, _)| unsafe { sn_string_equal(key, name) } != 0)
1018
+ }
749
1019
  })
750
1020
  .collect();
751
1021
  let dependent = self.field(operand, b"dependent_required\0")?;
@@ -811,7 +1081,9 @@ impl Engine {
811
1081
  property_targets,
812
1082
  patterns,
813
1083
  pattern_names,
1084
+ pattern_group,
814
1085
  required_properties,
1086
+ required_property_indices,
815
1087
  property_keys,
816
1088
  property_indices,
817
1089
  additional: self.child(operand, b"additional\0")?,
@@ -894,7 +1166,65 @@ impl Engine {
894
1166
  self.error_operands[index] = error_operands;
895
1167
  Ok(index)
896
1168
  }
1169
+ }
1170
+
1171
+ // A view of immutable compiled data and validator-owned regex search caches.
1172
+ struct Execution<'a> {
1173
+ engine: &'a Engine,
1174
+ caches: &'a pattern::Caches,
1175
+ memo: memo::Memo,
1176
+ budget: budget::Budget,
1177
+ }
1178
+ impl std::ops::Deref for Execution<'_> {
1179
+ type Target = Engine;
1180
+ fn deref(&self) -> &Engine {
1181
+ self.engine
1182
+ }
1183
+ }
1184
+ impl Execution<'_> {
897
1185
  fn eval(&self, node: usize, value: Value, depth: usize, active: scope::Context<'_>) -> Verdict {
1186
+ self.eval_with_kind(node, value, depth, active, None)
1187
+ }
1188
+ // Same-location applicators borrow an immutable instance under the GVL.
1189
+ // A full key guard can be reused; descendants at other locations recheck.
1190
+ fn eval_with_kind(
1191
+ &self,
1192
+ node: usize,
1193
+ value: Value,
1194
+ depth: usize,
1195
+ active: scope::Context<'_>,
1196
+ known_kind: Option<i32>,
1197
+ ) -> Verdict {
1198
+ if !self.budget.checkpoint() {
1199
+ return Unsupported;
1200
+ }
1201
+ let Some(key) = self
1202
+ .memo
1203
+ .key(self, node, value, depth, active, &self.budget)
1204
+ else {
1205
+ if self.budget.hit.get().0 != 0 {
1206
+ return Unsupported;
1207
+ }
1208
+ return self.eval_uncached(node, value, depth, active, known_kind);
1209
+ };
1210
+ if self.budget.hit.get().0 != 0 {
1211
+ return Unsupported;
1212
+ }
1213
+ if let Some(status) = self.memo.verdict(key) {
1214
+ return status;
1215
+ }
1216
+ let status = self.eval_uncached(node, value, depth, active, known_kind);
1217
+ self.memo.record_verdict(key, status);
1218
+ status
1219
+ }
1220
+ fn eval_uncached(
1221
+ &self,
1222
+ node: usize,
1223
+ value: Value,
1224
+ depth: usize,
1225
+ active: scope::Context<'_>,
1226
+ known_kind: Option<i32>,
1227
+ ) -> Verdict {
898
1228
  let frame = scope::ScopeFrame {
899
1229
  resource: self.metadata[node].resource,
900
1230
  parent: active.scope,
@@ -905,7 +1235,13 @@ impl Engine {
905
1235
  };
906
1236
  let active = self.enter_scope(node, active, &frame);
907
1237
  if self.tracks[node] {
908
- return self.eval_tracked(node, value, depth, active).status;
1238
+ return match known_kind {
1239
+ Some(kind) => {
1240
+ self.eval_tracked_kind(node, value, depth, active, kind)
1241
+ .status
1242
+ }
1243
+ None => self.eval_tracked(node, value, depth, active).status,
1244
+ };
909
1245
  }
910
1246
  if depth > MAX_EVALUATION_DEPTH {
911
1247
  return Unsupported;
@@ -913,17 +1249,22 @@ impl Engine {
913
1249
  if let [Op::String(required, rules)] = self.nodes[node].as_slice()
914
1250
  && rules.pattern.is_some()
915
1251
  {
916
- return rules.eval_guarded(*required, value);
1252
+ return rules.eval_guarded(self.caches, *required, value);
917
1253
  }
918
- let kind = if value & self.fixnum_flag != 0 {
919
- TYPE_INTEGRAL_NUMBER
920
- } else {
921
- self.instance_kind(node, value)
922
- };
1254
+ let kind = known_kind.unwrap_or_else(|| {
1255
+ if value & self.fixnum_flag != 0 {
1256
+ TYPE_INTEGRAL_NUMBER
1257
+ } else {
1258
+ self.instance_kind(node, value)
1259
+ }
1260
+ });
923
1261
  if kind == TYPE_UNSUPPORTED {
924
1262
  return Unsupported;
925
1263
  }
926
1264
  for op in &self.nodes[node] {
1265
+ if !self.budget.checkpoint() {
1266
+ return Unsupported;
1267
+ }
927
1268
  let outcome = match op {
928
1269
  Op::Ref(target) => {
929
1270
  if RefFrame::contains(active.active, node, value) {
@@ -934,7 +1275,7 @@ impl Engine {
934
1275
  value,
935
1276
  parent: active.active,
936
1277
  };
937
- self.eval(
1278
+ self.eval_with_kind(
938
1279
  self.reference_target(*target, active),
939
1280
  value,
940
1281
  depth + 1,
@@ -942,13 +1283,17 @@ impl Engine {
942
1283
  active: Some(&frame),
943
1284
  ..active
944
1285
  },
1286
+ Some(kind),
945
1287
  )
946
1288
  }
947
1289
  }
948
1290
  Op::All(nodes) => {
949
1291
  let mut outcome = Valid;
950
1292
  for &n in nodes {
951
- outcome = self.eval(n, value, depth + 1, active);
1293
+ if !self.budget.checkpoint() {
1294
+ return Unsupported;
1295
+ }
1296
+ outcome = self.eval_with_kind(n, value, depth + 1, active, Some(kind));
952
1297
  if outcome != Valid {
953
1298
  break;
954
1299
  }
@@ -956,46 +1301,36 @@ impl Engine {
956
1301
  outcome
957
1302
  }
958
1303
  Op::Any(nodes) | Op::One(nodes) => {
959
- let mut matches = 0;
960
- let mut unknown = false;
961
- for &(n, multiplicity) in nodes {
962
- match self.eval(n, value, depth + 1, active) {
963
- Valid => matches += multiplicity,
964
- Unsupported => {
965
- unknown = true;
966
- break;
967
- }
968
- Invalid => {}
969
- }
970
- if matches > 0 && matches!(op, Op::Any(_)) {
971
- break;
972
- }
973
- if matches > 1 && matches!(op, Op::One(_)) {
974
- break;
975
- }
976
- }
977
- if unknown {
978
- Unsupported
1304
+ let branch_kind = if matches!(op, Op::Any(_)) {
1305
+ evaluation::BranchKind::Any
979
1306
  } else {
980
- verdict(if matches!(op, Op::Any(_)) {
981
- matches > 0
982
- } else {
983
- matches == 1
984
- })
1307
+ evaluation::BranchKind::One
1308
+ };
1309
+ match evaluation::scan_branches(
1310
+ nodes,
1311
+ evaluation::BranchPolicy::Validity(branch_kind),
1312
+ |child| self.eval_with_kind(child, value, depth + 1, active, Some(kind)),
1313
+ ) {
1314
+ Ok(result) => verdict(branch_kind.accepts(result.matches)),
1315
+ Err(status) => status,
985
1316
  }
986
1317
  }
987
- Op::Not(n) => match self.eval(*n, value, depth + 1, active) {
1318
+ Op::Not(n) => match self.eval_with_kind(*n, value, depth + 1, active, Some(kind)) {
988
1319
  Valid => Invalid,
989
1320
  Invalid => Valid,
990
1321
  Unsupported => Unsupported,
991
1322
  },
992
- Op::Conditional(c, yes, no) => match self.eval(*c, value, depth + 1, active) {
993
- Unsupported => Unsupported,
994
- result => match if result == Valid { yes } else { no } {
995
- Some(n) => self.eval(*n, value, depth + 1, active),
996
- None => Valid,
997
- },
998
- },
1323
+ Op::Conditional(c, yes, no) => {
1324
+ match self.eval_with_kind(*c, value, depth + 1, active, Some(kind)) {
1325
+ Unsupported => Unsupported,
1326
+ result => match if result == Valid { yes } else { no } {
1327
+ Some(n) => {
1328
+ self.eval_with_kind(*n, value, depth + 1, active, Some(kind))
1329
+ }
1330
+ None => Valid,
1331
+ },
1332
+ }
1333
+ }
999
1334
  Op::Array(required, rules) => {
1000
1335
  if *required != 0 && kind != TYPE_ARRAY {
1001
1336
  Invalid
@@ -1027,17 +1362,8 @@ impl Engine {
1027
1362
  Op::Unsupported => Unsupported,
1028
1363
  Op::Boolean(v) => verdict(*v),
1029
1364
  Op::Type(mask) => verdict(kind & mask != 0),
1030
- Op::Const(v) => equal(*v, value, depth),
1031
- Op::Enum(values) => {
1032
- let mut outcome = Invalid;
1033
- for &v in values {
1034
- outcome = equal(v, value, depth);
1035
- if outcome != Invalid {
1036
- break;
1037
- }
1038
- }
1039
- outcome
1040
- }
1365
+ Op::Const(v) => equal_budgeted(*v, value, depth, &self.budget),
1366
+ Op::Enum(values) => values.eval(value, kind, depth, self.fixnum_flag, &self.budget),
1041
1367
  Op::Number(required, rules) => {
1042
1368
  if *required != 0 && kind & required == 0 {
1043
1369
  Invalid
@@ -1053,24 +1379,30 @@ impl Engine {
1053
1379
  } else if kind != TYPE_STRING {
1054
1380
  Valid
1055
1381
  } else {
1056
- rules.eval(value)
1382
+ rules.eval(self.caches, value)
1057
1383
  }
1058
1384
  }
1059
1385
  _ => unreachable!("applicators must be evaluated with their context"),
1060
1386
  }
1061
1387
  }
1062
1388
  fn array(&self, r: &Array, value: Value, depth: usize, active: scope::Context<'_>) -> Verdict {
1389
+ if !self.budget.checkpoint() {
1390
+ return Unsupported;
1391
+ }
1063
1392
  let len = unsafe { sn_len(value) };
1064
1393
  if bounds(len, r.min, r.max) == Invalid {
1065
1394
  return Invalid;
1066
1395
  }
1067
1396
  if r.unique {
1068
- let result = unique(value, depth, self.fixnum_flag);
1397
+ let result = unique(value, depth, self.fixnum_flag, &self.budget);
1069
1398
  if result != Valid {
1070
1399
  return result;
1071
1400
  }
1072
1401
  }
1073
1402
  for (i, &child) in r.prefix.iter().take(len).enumerate() {
1403
+ if !self.budget.checkpoint() {
1404
+ return Unsupported;
1405
+ }
1074
1406
  let result = self.eval(child, unsafe { sn_entry(value, i) }, depth, active);
1075
1407
  if result != Valid {
1076
1408
  return result;
@@ -1078,6 +1410,9 @@ impl Engine {
1078
1410
  }
1079
1411
  if r.list {
1080
1412
  for (i, &child) in r.items.iter().take(len).enumerate() {
1413
+ if !self.budget.checkpoint() {
1414
+ return Unsupported;
1415
+ }
1081
1416
  let result = self.eval(child, unsafe { sn_entry(value, i) }, depth, active);
1082
1417
  if result != Valid {
1083
1418
  return result;
@@ -1085,6 +1420,9 @@ impl Engine {
1085
1420
  }
1086
1421
  if let Some(child) = r.additional {
1087
1422
  for i in r.items.len()..len {
1423
+ if !self.budget.checkpoint() {
1424
+ return Unsupported;
1425
+ }
1088
1426
  let result = self.eval(child, unsafe { sn_entry(value, i) }, depth, active);
1089
1427
  if result != Valid {
1090
1428
  return result;
@@ -1142,16 +1480,29 @@ impl Engine {
1142
1480
  };
1143
1481
  let object_active = self.enter_scope(child, active, &frame);
1144
1482
  let pattern = string.and_then(|(required, rules)| {
1145
- rules.pattern_only().map(|pattern| (required, pattern))
1483
+ rules
1484
+ .pattern_only()
1485
+ .map(|pattern| (required, pattern))
1486
+ .filter(|_| !self.budget.timed())
1487
+ });
1488
+ let format = string.and_then(|(required, rules)| {
1489
+ rules
1490
+ .format_only()
1491
+ .map(|format| (required, format))
1492
+ .filter(|_| !self.budget.timed())
1146
1493
  });
1147
- let format = string
1148
- .and_then(|(required, rules)| rules.format_only().map(|format| (required, format)));
1149
1494
  let content = string.and_then(|(required, rules)| {
1150
- rules.content_only().map(|content| (required, content))
1495
+ rules
1496
+ .content_only()
1497
+ .map(|content| (required, content))
1498
+ .filter(|_| !self.budget.timed())
1151
1499
  });
1152
1500
  if let Some((required, pattern)) = pattern {
1153
- let result =
1154
- pattern.eval_items(required, values.get(r.prefix.len()..).unwrap_or(&[]));
1501
+ let result = pattern.eval_items(
1502
+ self.caches,
1503
+ required,
1504
+ values.get(r.prefix.len()..).unwrap_or(&[]),
1505
+ );
1155
1506
  if result != Valid {
1156
1507
  return result;
1157
1508
  }
@@ -1191,11 +1542,14 @@ impl Engine {
1191
1542
  }
1192
1543
  } else {
1193
1544
  for &item in values.iter().skip(r.prefix.len()) {
1545
+ if !self.budget.checkpoint() {
1546
+ return Unsupported;
1547
+ }
1194
1548
  let result = if let Some((required, rules)) = number {
1195
1549
  rules.eval_guarded(required, item)
1196
1550
  } else if let Some((required, rules)) = string {
1197
1551
  if rules.pattern.is_some() {
1198
- rules.eval_guarded(required, item)
1552
+ rules.eval_guarded(self.caches, required, item)
1199
1553
  } else if rules.length_only() {
1200
1554
  rules.eval_length_guarded(required, item)
1201
1555
  } else {
@@ -1205,7 +1559,7 @@ impl Engine {
1205
1559
  } else if kind != TYPE_STRING {
1206
1560
  verdict(required == 0)
1207
1561
  } else {
1208
- rules.eval(item)
1562
+ rules.eval(self.caches, item)
1209
1563
  }
1210
1564
  }
1211
1565
  } else {
@@ -1218,20 +1572,64 @@ impl Engine {
1218
1572
  }
1219
1573
  }
1220
1574
  if let Some(child) = r.contains {
1221
- let mut matches = 0;
1222
- for i in 0..len {
1223
- match self.eval(child, unsafe { sn_entry(value, i) }, depth, active) {
1224
- Valid => matches += 1,
1225
- Unsupported => return Unsupported,
1226
- Invalid => {}
1227
- }
1575
+ if let Some(status) = self.fixnum_contains(r, child, value, len, depth) {
1576
+ return status;
1228
1577
  }
1578
+ let matches = match evaluation::scan_contains(
1579
+ len,
1580
+ |index| self.eval(child, unsafe { sn_entry(value, index) }, depth, active),
1581
+ |_| {},
1582
+ ) {
1583
+ Ok(matches) => matches,
1584
+ Err(status) => return status,
1585
+ };
1229
1586
  if bounds(matches, Some(r.min_contains), r.max_contains) == Invalid {
1230
1587
  return Invalid;
1231
1588
  }
1232
1589
  }
1233
1590
  Valid
1234
1591
  }
1592
+ fn fixnum_contains(
1593
+ &self,
1594
+ rules: &Array,
1595
+ child: usize,
1596
+ value: Value,
1597
+ len: usize,
1598
+ depth: usize,
1599
+ ) -> Option<Verdict> {
1600
+ if depth > MAX_EVALUATION_DEPTH || self.tracks[child] || self.budget.timed() {
1601
+ return None;
1602
+ }
1603
+ let [op] = self.nodes[child].as_slice() else {
1604
+ return None;
1605
+ };
1606
+ let total = match op {
1607
+ Op::Type(_) | Op::Boolean(_) => true,
1608
+ Op::Const(expected) => expected & self.fixnum_flag != 0,
1609
+ Op::Number(_, rules) => rules.fast_limits.is_some(),
1610
+ _ => false,
1611
+ };
1612
+ if !total {
1613
+ return None;
1614
+ }
1615
+ let values = if len == 0 {
1616
+ &[]
1617
+ } else {
1618
+ unsafe { std::slice::from_raw_parts(sn_array_data(value), len) }
1619
+ };
1620
+ // Preserve later Unsupported/fallback results: no predicate work may
1621
+ // be skipped until all values are proven to be supported Fixnums.
1622
+ // This guard stays linear, but is far cheaper than full node evaluation.
1623
+ if !values.iter().all(|&item| item & self.fixnum_flag != 0) {
1624
+ return None;
1625
+ }
1626
+ Some(evaluation::contains_validity(
1627
+ len,
1628
+ rules.min_contains,
1629
+ rules.max_contains,
1630
+ |index| self.leaf(op, TYPE_INTEGRAL_NUMBER, values[index], depth) == Valid,
1631
+ ))
1632
+ }
1235
1633
  #[inline(never)]
1236
1634
  fn integer_items(
1237
1635
  &self,
@@ -1261,7 +1659,15 @@ impl Engine {
1261
1659
  }
1262
1660
  _ => None,
1263
1661
  };
1264
- for &value in values {
1662
+ let prefix = if self.budget.timed() {
1663
+ 0
1664
+ } else {
1665
+ integer_scan::valid_prefix(values, min, max, self.fixnum_flag)
1666
+ };
1667
+ for &value in &values[prefix..] {
1668
+ if !self.budget.checkpoint() {
1669
+ return Unsupported;
1670
+ }
1265
1671
  let result = if value & self.fixnum_flag != 0 {
1266
1672
  let n = immediate_integer(value);
1267
1673
  verdict(n >= min && n <= max)
@@ -1286,6 +1692,9 @@ impl Engine {
1286
1692
  active: scope::Context<'_>,
1287
1693
  ) -> Verdict {
1288
1694
  for &value in values {
1695
+ if !self.budget.checkpoint() {
1696
+ return Unsupported;
1697
+ }
1289
1698
  let kind = if value & self.fixnum_flag != 0 {
1290
1699
  TYPE_INTEGRAL_NUMBER
1291
1700
  } else {
@@ -1311,6 +1720,9 @@ impl Engine {
1311
1720
  depth: usize,
1312
1721
  active: scope::Context<'_>,
1313
1722
  ) -> Verdict {
1723
+ if !self.budget.checkpoint() {
1724
+ return Unsupported;
1725
+ }
1314
1726
  if let Some(record) = &r.record {
1315
1727
  // C initializes only the written key/value pairs. The untouched
1316
1728
  // tail is never read, including empty and unsupported records.
@@ -1357,6 +1769,9 @@ impl Engine {
1357
1769
  status,
1358
1770
  };
1359
1771
  for entry in entries.as_chunks::<2>().0 {
1772
+ if !self.budget.checkpoint() {
1773
+ return Unsupported;
1774
+ }
1360
1775
  if record_entry(&mut context, entry[0], entry[1]) != Valid {
1361
1776
  return Unsupported;
1362
1777
  }
@@ -1390,26 +1805,20 @@ impl Engine {
1390
1805
  if bounds(len, r.min, r.max) == Invalid {
1391
1806
  return Invalid;
1392
1807
  }
1393
- for &key in &r.required {
1394
- if missing(value, key, self.undefined) {
1395
- return Invalid;
1396
- }
1397
- }
1398
- let mut found = 0;
1399
- for (index, &(key, child)) in r.properties.iter().enumerate() {
1400
- let item = unsafe { sn_lookup(value, key) };
1401
- if item == self.undefined {
1402
- if r.required_properties[index] {
1403
- return Invalid;
1404
- }
1405
- continue;
1406
- }
1407
- found += 1;
1408
- let result = self.eval(child, item, depth, active);
1409
- if result != Valid {
1410
- return result;
1411
- }
1412
- }
1808
+ let status = r.check_required(value, self.undefined, &self.budget);
1809
+ if status != Valid {
1810
+ return status;
1811
+ }
1812
+ let found = match r.visit_properties(
1813
+ value,
1814
+ len,
1815
+ self.undefined,
1816
+ &self.budget,
1817
+ |_, child, item| self.eval(child, item, depth, active),
1818
+ ) {
1819
+ Ok(found) => found,
1820
+ Err(status) => return status,
1821
+ };
1413
1822
  if r.names.is_some() || !r.patterns.is_empty() || (r.additional.is_some() && found < len) {
1414
1823
  let mut context = ObjectContext {
1415
1824
  engine: self,
@@ -1423,16 +1832,14 @@ impl Engine {
1423
1832
  return if result == 0 { Invalid } else { Unsupported };
1424
1833
  }
1425
1834
  }
1426
- for (name, required) in &r.dependent_required {
1427
- if !missing(value, *name, self.undefined) {
1428
- for &key in required {
1429
- if missing(value, key, self.undefined) {
1430
- return Invalid;
1431
- }
1432
- }
1433
- }
1835
+ let status = r.check_dependent_required(value, self.undefined, &self.budget);
1836
+ if status != Valid {
1837
+ return status;
1434
1838
  }
1435
1839
  for &(name, child) in &r.dependent_schemas {
1840
+ if !self.budget.checkpoint() {
1841
+ return Unsupported;
1842
+ }
1436
1843
  if !missing(value, name, self.undefined) {
1437
1844
  let result = self.eval(child, value, depth, active);
1438
1845
  if result != Valid {
@@ -1456,6 +1863,9 @@ impl Engine {
1456
1863
  depth: usize,
1457
1864
  active: scope::Context<'_>,
1458
1865
  ) -> Verdict {
1866
+ if !self.budget.checkpoint() {
1867
+ return Unsupported;
1868
+ }
1459
1869
  match self.nodes[node].as_slice() {
1460
1870
  [Op::String(required, rules)]
1461
1871
  if depth <= MAX_EVALUATION_DEPTH && rules.length_only() =>
@@ -1465,7 +1875,7 @@ impl Engine {
1465
1875
  [Op::String(required, rules)]
1466
1876
  if depth <= MAX_EVALUATION_DEPTH && rules.pattern.is_some() =>
1467
1877
  {
1468
- rules.eval_guarded(*required, value)
1878
+ rules.eval_guarded(self.caches, *required, value)
1469
1879
  }
1470
1880
  [
1471
1881
  op @ (Op::Type(_)
@@ -1491,7 +1901,7 @@ impl Engine {
1491
1901
  }
1492
1902
  }
1493
1903
  struct RecordContext<'a, 'b> {
1494
- engine: &'a Engine,
1904
+ engine: &'a Execution<'a>,
1495
1905
  rules: &'a Object,
1496
1906
  record: &'a RecordObject,
1497
1907
  indexed: bool,
@@ -1614,7 +2024,19 @@ fn length_guarded(value: Value, required: i32, min: Option<usize>, max: Option<u
1614
2024
  fn immediate_integer(value: Value) -> i64 {
1615
2025
  (value as isize >> 1) as i64
1616
2026
  }
1617
- fn equal(a: Value, b: Value, depth: usize) -> Verdict {
2027
+ fn equal_budgeted(a: Value, b: Value, depth: usize, budget: &budget::Budget) -> Verdict {
2028
+ equal_cached(a, b, depth, &mut structural::Cache::new(depth), budget)
2029
+ }
2030
+ fn equal_cached(
2031
+ a: Value,
2032
+ b: Value,
2033
+ depth: usize,
2034
+ memo: &mut structural::Cache<(Value, Value, usize), Verdict>,
2035
+ budget: &budget::Budget,
2036
+ ) -> Verdict {
2037
+ if !budget.checkpoint() {
2038
+ return Unsupported;
2039
+ }
1618
2040
  if depth > MAX_EVALUATION_DEPTH {
1619
2041
  return Unsupported;
1620
2042
  }
@@ -1677,7 +2099,15 @@ fn equal(a: Value, b: Value, depth: usize) -> Verdict {
1677
2099
  if unsafe { sn_same_class(a, b) } == 0 {
1678
2100
  return Invalid;
1679
2101
  }
1680
- match ka {
2102
+ let container = ka == TYPE_ARRAY || ka == TYPE_OBJECT;
2103
+ let key = (a, b, depth);
2104
+ if container
2105
+ && memo.eligible(depth)
2106
+ && let Some(result) = memo.get(key)
2107
+ {
2108
+ return result;
2109
+ }
2110
+ let result = match ka {
1681
2111
  TYPE_STRING => verdict(unsafe { sn_string_equal(a, b) } != 0),
1682
2112
  TYPE_ARRAY => {
1683
2113
  let len = unsafe { sn_len(a) };
@@ -1685,10 +2115,12 @@ fn equal(a: Value, b: Value, depth: usize) -> Verdict {
1685
2115
  return Invalid;
1686
2116
  }
1687
2117
  for i in 0..len {
1688
- let result = equal(
2118
+ let result = equal_cached(
1689
2119
  unsafe { sn_entry(a, i) },
1690
2120
  unsafe { sn_entry(b, i) },
1691
2121
  depth + 1,
2122
+ memo,
2123
+ budget,
1692
2124
  );
1693
2125
  if result != Valid {
1694
2126
  return result;
@@ -1703,18 +2135,24 @@ fn equal(a: Value, b: Value, depth: usize) -> Verdict {
1703
2135
  let mut context = EqualityContext {
1704
2136
  other: b,
1705
2137
  depth: depth + 1,
2138
+ memo,
2139
+ budget,
1706
2140
  };
1707
2141
  decode(unsafe {
1708
2142
  sn_hash_each(
1709
2143
  a,
1710
- (&mut context as *mut EqualityContext).cast(),
2144
+ (&mut context as *mut EqualityContext<'_>).cast(),
1711
2145
  equal_entry,
1712
2146
  )
1713
2147
  })
1714
2148
  }
1715
2149
  TYPE_UNSUPPORTED => Unsupported,
1716
2150
  _ => Invalid,
2151
+ };
2152
+ if container && memo.eligible(depth) {
2153
+ memo.record(key, result);
1717
2154
  }
2155
+ result
1718
2156
  }
1719
2157
  fn decode(result: i32) -> Verdict {
1720
2158
  match result {
@@ -1723,21 +2161,26 @@ fn decode(result: i32) -> Verdict {
1723
2161
  _ => Unsupported,
1724
2162
  }
1725
2163
  }
1726
- struct EqualityContext {
2164
+ struct EqualityContext<'a> {
1727
2165
  other: Value,
1728
2166
  depth: usize,
2167
+ memo: &'a mut structural::Cache<(Value, Value, usize), Verdict>,
2168
+ budget: &'a budget::Budget,
1729
2169
  }
1730
2170
  unsafe extern "C" fn equal_entry(context: *mut c_void, key: Value, value: Value) -> i32 {
1731
2171
  catch_unwind(AssertUnwindSafe(|| {
1732
- let c = unsafe { &*context.cast::<EqualityContext>() };
2172
+ let c = unsafe { &mut *context.cast::<EqualityContext<'_>>() };
1733
2173
  if unsafe { sn_kind(key) } != TYPE_STRING {
1734
2174
  return Unsupported as i32;
1735
2175
  }
2176
+ if !c.budget.checkpoint() {
2177
+ return Unsupported as i32;
2178
+ }
1736
2179
  let other = unsafe { sn_lookup(c.other, key) };
1737
2180
  if unsafe { sn_missing(other) } != 0 {
1738
2181
  Invalid as i32
1739
2182
  } else {
1740
- equal(value, other, c.depth) as i32
2183
+ equal_cached(value, other, c.depth, c.memo, c.budget) as i32
1741
2184
  }
1742
2185
  }))
1743
2186
  .unwrap_or(Unsupported as i32)
@@ -1816,7 +2259,33 @@ fn bytes_fingerprint(bytes: &[u8]) -> u64 {
1816
2259
  }
1817
2260
  mix(result ^ TYPE_STRING as u64)
1818
2261
  }
1819
- fn fingerprint(value: Value, depth: usize, fixnum_flag: Value, big_hint: bool) -> Option<u64> {
2262
+ fn fingerprint_budgeted(
2263
+ value: Value,
2264
+ depth: usize,
2265
+ fixnum_flag: Value,
2266
+ big_hint: bool,
2267
+ budget: &budget::Budget,
2268
+ ) -> Option<u64> {
2269
+ fingerprint_cached(
2270
+ value,
2271
+ depth,
2272
+ fixnum_flag,
2273
+ big_hint,
2274
+ &mut structural::Cache::new(depth),
2275
+ budget,
2276
+ )
2277
+ }
2278
+ fn fingerprint_cached(
2279
+ value: Value,
2280
+ depth: usize,
2281
+ fixnum_flag: Value,
2282
+ big_hint: bool,
2283
+ memo: &mut structural::Cache<(Value, usize), Option<u64>>,
2284
+ budget: &budget::Budget,
2285
+ ) -> Option<u64> {
2286
+ if !budget.checkpoint() {
2287
+ return None;
2288
+ }
1820
2289
  if depth > MAX_EVALUATION_DEPTH {
1821
2290
  return None;
1822
2291
  }
@@ -1846,7 +2315,18 @@ fn fingerprint(value: Value, depth: usize, fixnum_flag: Value, big_hint: bool) -
1846
2315
  }
1847
2316
  return Some(mix(n.to_bits() ^ TYPE_NUMBER as u64));
1848
2317
  }
1849
- match kind {
2318
+ let container = kind == TYPE_ARRAY || kind == TYPE_OBJECT;
2319
+ // Batch reuse pays for wide shared containers immediately. Small shallow
2320
+ // records keep the original cheap traversal, expanding at most four levels
2321
+ // before the depth-based cache handles deeper shared graphs.
2322
+ let cacheable = container && memo.eligible_container(depth, unsafe { len.assume_init() });
2323
+ let key = (value, depth);
2324
+ if cacheable
2325
+ && let Some(result) = memo.get(key)
2326
+ {
2327
+ return result;
2328
+ }
2329
+ let result = match kind {
1850
2330
  TYPE_INTEGRAL_NUMBER => Some(number::integer_fingerprint(value)),
1851
2331
  TYPE_NULL => Some(mix(TYPE_NULL as u64)),
1852
2332
  TYPE_BOOLEAN => Some(mix(if unsafe { len.assume_init() } != 0 {
@@ -1868,7 +2348,9 @@ fn fingerprint(value: Value, depth: usize, fixnum_flag: Value, big_hint: bool) -
1868
2348
  // All descendants are read under the GVL without allocating Ruby
1869
2349
  // objects or invoking Ruby callbacks, so the borrowed view is stable.
1870
2350
  for &item in values {
1871
- hash = mix(hash ^ fingerprint(item, depth + 1, fixnum_flag, false)?);
2351
+ hash =
2352
+ mix(hash
2353
+ ^ fingerprint_cached(item, depth + 1, fixnum_flag, false, memo, budget)?);
1872
2354
  }
1873
2355
  Some(hash)
1874
2356
  }
@@ -1878,23 +2360,31 @@ fn fingerprint(value: Value, depth: usize, fixnum_flag: Value, big_hint: bool) -
1878
2360
  hash: 0,
1879
2361
  depth: depth + 1,
1880
2362
  fixnum_flag,
2363
+ memo,
2364
+ budget,
1881
2365
  };
1882
2366
  let status = unsafe {
1883
2367
  sn_hash_each_key_view(
1884
2368
  value,
1885
- (&mut c as *mut FingerprintContext).cast(),
2369
+ (&mut c as *mut FingerprintContext<'_>).cast(),
1886
2370
  fingerprint_entry,
1887
2371
  )
1888
2372
  };
1889
2373
  (status == VERDICT_VALID).then(|| mix(c.hash ^ len as u64 ^ TYPE_OBJECT as u64))
1890
2374
  }
1891
2375
  _ => None,
2376
+ };
2377
+ if cacheable {
2378
+ memo.record(key, result);
1892
2379
  }
2380
+ result
1893
2381
  }
1894
- struct FingerprintContext {
2382
+ struct FingerprintContext<'a> {
1895
2383
  hash: u64,
1896
2384
  depth: usize,
1897
2385
  fixnum_flag: Value,
2386
+ memo: &'a mut structural::Cache<(Value, usize), Option<u64>>,
2387
+ budget: &'a budget::Budget,
1898
2388
  }
1899
2389
  unsafe extern "C" fn fingerprint_entry(
1900
2390
  context: *mut c_void,
@@ -1903,8 +2393,12 @@ unsafe extern "C" fn fingerprint_entry(
1903
2393
  value: Value,
1904
2394
  ) -> i32 {
1905
2395
  catch_unwind(AssertUnwindSafe(|| {
1906
- let c = unsafe { &mut *context.cast::<FingerprintContext>() };
1907
- let Some(hash) = fingerprint(value, c.depth, c.fixnum_flag, false) else {
2396
+ let c = unsafe { &mut *context.cast::<FingerprintContext<'_>>() };
2397
+ if !c.budget.checkpoint() {
2398
+ return Unsupported as i32;
2399
+ }
2400
+ let Some(hash) = fingerprint_cached(value, c.depth, c.fixnum_flag, false, c.memo, c.budget)
2401
+ else {
1908
2402
  return Unsupported as i32;
1909
2403
  };
1910
2404
  c.hash ^= mix(bytes_fingerprint(unsafe { byte_view(data, len) }) ^ hash.rotate_left(17));
@@ -1929,15 +2423,19 @@ impl Hasher for BucketHasher {
1929
2423
  }
1930
2424
  }
1931
2425
  }
1932
- fn unique(value: Value, depth: usize, fixnum_flag: Value) -> Verdict {
2426
+ fn unique(value: Value, depth: usize, fixnum_flag: Value, budget: &budget::Budget) -> Verdict {
1933
2427
  let len = unsafe { sn_len(value) };
2428
+ if !budget.checkpoint() {
2429
+ return Unsupported;
2430
+ }
1934
2431
  if len <= 16 {
1935
2432
  for i in 0..len {
1936
2433
  for previous in 0..i {
1937
- match equal(
2434
+ match equal_budgeted(
1938
2435
  unsafe { sn_entry(value, previous) },
1939
2436
  unsafe { sn_entry(value, i) },
1940
2437
  depth,
2438
+ budget,
1941
2439
  ) {
1942
2440
  Valid => return Invalid,
1943
2441
  Unsupported => return Unsupported,
@@ -1951,30 +2449,35 @@ fn unique(value: Value, depth: usize, fixnum_flag: Value) -> Verdict {
1951
2449
  // This is a checked optimization hint, not an instance shape assumption.
1952
2450
  let big_hint = unsafe { sn_is_bignum(values[0]) } != 0;
1953
2451
  if big_hint
2452
+ && !budget.timed()
1954
2453
  && depth <= MAX_EVALUATION_DEPTH
1955
2454
  && let Some(result) = number::ordered_big_unique(values)
1956
2455
  {
1957
2456
  return result;
1958
2457
  }
1959
2458
  if big_hint && depth <= MAX_EVALUATION_DEPTH {
1960
- let result = unordered_big_unique(values);
2459
+ let result = unordered_big_unique(values, budget);
1961
2460
  if result != Unsupported {
1962
2461
  return result;
1963
2462
  }
1964
2463
  }
2464
+ let mut fingerprints = structural::Cache::batch(depth);
1965
2465
  indexed_unique(
1966
2466
  values,
1967
- |item| fingerprint(item, depth, fixnum_flag, big_hint),
1968
- |a, b| equal(a, b, depth),
2467
+ |item| fingerprint_cached(item, depth, fixnum_flag, big_hint, &mut fingerprints, budget),
2468
+ |a, b| equal_budgeted(a, b, depth, budget),
1969
2469
  )
1970
2470
  }
1971
2471
  // Keep Big-only sampling and retry state out of the common scalar path.
1972
2472
  #[inline(never)]
1973
- fn unordered_big_unique(values: &[Value]) -> Verdict {
2473
+ fn unordered_big_unique(values: &[Value], budget: &budget::Budget) -> Verdict {
1974
2474
  let mut all_big = true;
1975
2475
  let result = indexed_unique_short(
1976
2476
  values,
1977
2477
  |item| {
2478
+ if !budget.checkpoint() {
2479
+ return None;
2480
+ }
1978
2481
  let hash = number::big_low_fingerprint(item);
1979
2482
  if hash.is_none() {
1980
2483
  all_big = false;
@@ -1982,6 +2485,9 @@ fn unordered_big_unique(values: &[Value]) -> Verdict {
1982
2485
  hash
1983
2486
  },
1984
2487
  |a, b| {
2488
+ if !budget.checkpoint() {
2489
+ return Unsupported;
2490
+ }
1985
2491
  // Distinct integers sharing the short sample need the wider pass.
1986
2492
  if unsafe { sn_big_equal(a, b) } != 0 {
1987
2493
  Valid
@@ -1994,15 +2500,41 @@ fn unordered_big_unique(values: &[Value]) -> Verdict {
1994
2500
  return result;
1995
2501
  }
1996
2502
  if all_big {
1997
- let result = indexed_unique_impl::<true>(values, number::big_low64_fingerprint, |a, b| {
1998
- verdict(unsafe { sn_big_equal(a, b) } != 0)
1999
- });
2503
+ let result = indexed_unique_impl::<true>(
2504
+ values,
2505
+ |item| {
2506
+ if !budget.checkpoint() {
2507
+ None
2508
+ } else {
2509
+ number::big_low64_fingerprint(item)
2510
+ }
2511
+ },
2512
+ |a, b| {
2513
+ if !budget.checkpoint() {
2514
+ return Unsupported;
2515
+ }
2516
+ verdict(unsafe { sn_big_equal(a, b) } != 0)
2517
+ },
2518
+ );
2000
2519
  if result != Unsupported {
2001
2520
  return result;
2002
2521
  }
2003
- let result = indexed_unique(values, number::big_only_fingerprint, |a, b| {
2004
- verdict(unsafe { sn_big_equal(a, b) } != 0)
2005
- });
2522
+ let result = indexed_unique(
2523
+ values,
2524
+ |item| {
2525
+ if !budget.checkpoint() {
2526
+ None
2527
+ } else {
2528
+ number::big_only_fingerprint(item)
2529
+ }
2530
+ },
2531
+ |a, b| {
2532
+ if !budget.checkpoint() {
2533
+ return Unsupported;
2534
+ }
2535
+ verdict(unsafe { sn_big_equal(a, b) } != 0)
2536
+ },
2537
+ );
2006
2538
  if result != Unsupported {
2007
2539
  return result;
2008
2540
  }
@@ -2157,13 +2689,16 @@ fn indexed_unique_slots<S: UniqueSlot, const BOUNDED: bool>(
2157
2689
  slots[at] = S::new(hash, i);
2158
2690
  break;
2159
2691
  };
2160
- if BOUNDED {
2161
- // Bound total bucket probing as well as equal fingerprints.
2162
- // A poor sampled-word distribution restarts with full hashes.
2163
- probes += 1;
2164
- if probes > values.len().saturating_mul(2) {
2692
+ // A full fingerprint can still have adversarial low table bits.
2693
+ // Bound probing independently of exact fingerprint collisions.
2694
+ probes += 1;
2695
+ if probes > values.len().saturating_mul(2) {
2696
+ if BOUNDED {
2165
2697
  return Unsupported;
2166
2698
  }
2699
+ // Release the hash table before allocating the flat index.
2700
+ drop(slots);
2701
+ return sorted_unique(values, fingerprint, equal);
2167
2702
  }
2168
2703
  if slot.matches(hash) {
2169
2704
  match equal(values[previous], item) {
@@ -2184,6 +2719,56 @@ fn indexed_unique_slots<S: UniqueSlot, const BOUNDED: bool>(
2184
2719
  }
2185
2720
  Valid
2186
2721
  }
2722
+ fn sorted_unique(
2723
+ values: &[Value],
2724
+ mut fingerprint: impl FnMut(Value) -> Option<u64>,
2725
+ mut equal: impl FnMut(Value, Value) -> Verdict,
2726
+ ) -> Verdict {
2727
+ let Some(bytes) = values
2728
+ .len()
2729
+ .checked_mul(std::mem::size_of::<(u64, usize)>())
2730
+ else {
2731
+ return Unsupported;
2732
+ };
2733
+ if bytes > MAX_UNIQUE_INDEX_BYTES {
2734
+ return Unsupported;
2735
+ }
2736
+ let mut entries = Vec::new();
2737
+ if entries.try_reserve_exact(values.len()).is_err() {
2738
+ return Unsupported;
2739
+ }
2740
+ for (index, &value) in values.iter().enumerate() {
2741
+ let Some(hash) = fingerprint(value) else {
2742
+ break;
2743
+ };
2744
+ entries.push((hash, index));
2745
+ }
2746
+ let complete = entries.len() == values.len();
2747
+ // O(n log n) sorting replaces quadratic slot probing. Exact equality is
2748
+ // needed only within identical full fingerprints, in original array order.
2749
+ entries.sort_unstable();
2750
+ let mut decisive: Option<(usize, Verdict)> = None;
2751
+ for group in entries.chunk_by(|a, b| a.0 == b.0) {
2752
+ for at in 1..group.len() {
2753
+ let index = group[at].1;
2754
+ if decisive.is_some_and(|(earlier, _)| earlier <= index) {
2755
+ break;
2756
+ }
2757
+ for &(_, previous) in &group[..at] {
2758
+ let status = equal(values[previous], values[index]);
2759
+ if status != Invalid {
2760
+ decisive = Some((index, status));
2761
+ break;
2762
+ }
2763
+ }
2764
+ }
2765
+ }
2766
+ // A duplicate in the successfully fingerprinted prefix must precede a
2767
+ // later unavailable fingerprint, just as it does in the streaming index.
2768
+ decisive.map_or(if complete { Valid } else { Unsupported }, |(_, status)| {
2769
+ if status == Valid { Invalid } else { status }
2770
+ })
2771
+ }
2187
2772
  fn allocate_unique_slots<S: UniqueSlot>(capacity: usize) -> Option<Vec<S>> {
2188
2773
  let bytes = capacity.checked_mul(std::mem::size_of::<S>())?;
2189
2774
  if bytes > MAX_UNIQUE_INDEX_BYTES {
@@ -2194,8 +2779,30 @@ fn allocate_unique_slots<S: UniqueSlot>(capacity: usize) -> Option<Vec<S>> {
2194
2779
  slots.resize(capacity, S::EMPTY);
2195
2780
  Some(slots)
2196
2781
  }
2782
+ struct PropertyCandidates<'a> {
2783
+ rules: &'a Object,
2784
+ indices: Vec<usize>,
2785
+ budget: &'a budget::Budget,
2786
+ }
2787
+ unsafe extern "C" fn property_candidate(context: *mut c_void, key: Value, _: Value) -> i32 {
2788
+ catch_unwind(AssertUnwindSafe(|| {
2789
+ let c = unsafe { &mut *context.cast::<PropertyCandidates<'_>>() };
2790
+ if !c.budget.checkpoint() {
2791
+ return Unsupported as i32;
2792
+ }
2793
+ if unsafe { sn_kind(key) } != TYPE_STRING {
2794
+ return Unsupported as i32;
2795
+ }
2796
+ if let Some(target) = c.rules.property_targets.get(&tracking::JsonKey::new(key)) {
2797
+ c.indices.push(target.index);
2798
+ }
2799
+ Valid as i32
2800
+ }))
2801
+ .unwrap_or(Unsupported as i32)
2802
+ }
2803
+
2197
2804
  struct ObjectContext<'a> {
2198
- engine: &'a Engine,
2805
+ engine: &'a Execution<'a>,
2199
2806
  rules: &'a Object,
2200
2807
  depth: usize,
2201
2808
  active: scope::Context<'a>,
@@ -2207,6 +2814,9 @@ struct ObjectContext<'a> {
2207
2814
  pub unsafe extern "C" fn sn_object_entry(context: *const c_void, key: Value, value: Value) -> i32 {
2208
2815
  catch_unwind(AssertUnwindSafe(|| {
2209
2816
  let c = unsafe { &*context.cast::<ObjectContext>() };
2817
+ if !c.engine.budget.checkpoint() {
2818
+ return Unsupported as i32;
2819
+ }
2210
2820
  if unsafe { sn_kind(key) } != TYPE_STRING {
2211
2821
  return Unsupported as i32;
2212
2822
  }
@@ -2216,24 +2826,19 @@ pub unsafe extern "C" fn sn_object_entry(context: *const c_void, key: Value, val
2216
2826
  return result as i32;
2217
2827
  }
2218
2828
  }
2219
- let mut matched = c.rules.property_keys.contains(&tracking::JsonKey::new(key));
2220
- for (pattern, child) in &c.rules.patterns {
2221
- match pattern.eval(key) {
2222
- Unsupported => return Unsupported as i32,
2223
- Invalid => {}
2224
- Valid => {
2225
- matched = true;
2226
- let result = c.engine.eval(*child, value, c.depth, c.active);
2227
- if result != Valid {
2228
- return result as i32;
2229
- }
2230
- }
2231
- }
2829
+ match c.rules.visit_value_rules::<false>(
2830
+ c.engine.caches,
2831
+ &c.engine.budget,
2832
+ key,
2833
+ None,
2834
+ |_, _, child| match c.engine.eval(child, value, c.depth, c.active) {
2835
+ Valid => Ok(()),
2836
+ status => Err(status),
2837
+ },
2838
+ ) {
2839
+ Ok(_) => Valid as i32,
2840
+ Err(status) => status as i32,
2232
2841
  }
2233
- if !matched && let Some(child) = c.rules.additional {
2234
- return c.engine.eval(child, value, c.depth, c.active) as i32;
2235
- }
2236
- Valid as i32
2237
2842
  }))
2238
2843
  .unwrap_or(Unsupported as i32)
2239
2844
  }
@@ -2247,7 +2852,8 @@ pub extern "C" fn sn_new() -> *mut Engine {
2247
2852
  }))
2248
2853
  }
2249
2854
  /// # Safety
2250
- /// Engine and error must be valid writable pointers; program must be a live VM Program under the GVL.
2855
+ /// Engine/error must be writable. Program must be a live VM Program (or Array
2856
+ /// of Programs when multiple is set), and compiler must be live under the GVL.
2251
2857
  #[unsafe(no_mangle)]
2252
2858
  pub unsafe extern "C" fn sn_compile(
2253
2859
  engine: *mut Engine,
@@ -2255,6 +2861,7 @@ pub unsafe extern "C" fn sn_compile(
2255
2861
  compiler: Value,
2256
2862
  format: i32,
2257
2863
  content: i32,
2864
+ multiple: i32,
2258
2865
  error: *mut Value,
2259
2866
  ) -> i32 {
2260
2867
  match catch_unwind(AssertUnwindSafe(|| {
@@ -2262,8 +2869,16 @@ pub unsafe extern "C" fn sn_compile(
2262
2869
  e.validate_format = format != 0;
2263
2870
  e.validate_content = content != 0;
2264
2871
  e.compiler = e.retain(compiler);
2265
- e.root = e.compile(program)?;
2872
+ if multiple != 0 {
2873
+ for program in e.values(program) {
2874
+ e.compile(program)?;
2875
+ }
2876
+ } else {
2877
+ e.root = e.compile(program)?;
2878
+ }
2266
2879
  e.bind_dynamic_anchors()?;
2880
+ e.simplify_dynamic_references();
2881
+ e.compile_memoization();
2267
2882
  Ok::<_, Value>(())
2268
2883
  })) {
2269
2884
  Ok(Ok(())) => VERDICT_VALID,
@@ -2275,16 +2890,96 @@ pub unsafe extern "C" fn sn_compile(
2275
2890
  }
2276
2891
  }
2277
2892
  /// # Safety
2278
- /// Engine must be initialized and value must be a live Ruby VALUE under the GVL.
2893
+ /// Engine must be fully initialized and program must be live.
2894
+ #[unsafe(no_mangle)]
2895
+ pub unsafe extern "C" fn sn_node_index(engine: *const Engine, program: Value) -> usize {
2896
+ unsafe { &*engine }
2897
+ .indices
2898
+ .get(&program)
2899
+ .copied()
2900
+ .unwrap_or(usize::MAX)
2901
+ }
2902
+ /// # Safety
2903
+ /// Engine must be initialized.
2279
2904
  #[unsafe(no_mangle)]
2280
- pub unsafe extern "C" fn sn_valid(engine: *const Engine, value: Value) -> i32 {
2905
+ pub unsafe extern "C" fn sn_root_index(engine: *const Engine) -> usize {
2906
+ unsafe { &*engine }.root
2907
+ }
2908
+ /// # Safety
2909
+ /// Roots must be live and compilation must have finished.
2910
+ #[unsafe(no_mangle)]
2911
+ pub unsafe extern "C" fn sn_root_count(roots: *const Roots) -> usize {
2912
+ unsafe { &*roots }.values.len()
2913
+ }
2914
+ /// # Safety
2915
+ /// Roots must be live and index in bounds, outside compilation.
2916
+ #[unsafe(no_mangle)]
2917
+ pub unsafe extern "C" fn sn_root_at(roots: *const Roots, index: usize) -> Value {
2918
+ unsafe { &*roots }.values[index]
2919
+ }
2920
+ /// # Safety
2921
+ /// Engine must be initialized, root in bounds and value live under the GVL.
2922
+ /// Caches must be null or live, owned by this validator and not in use.
2923
+ #[unsafe(no_mangle)]
2924
+ pub unsafe extern "C" fn sn_valid(
2925
+ engine: *const Engine,
2926
+ caches: *const pattern::Caches,
2927
+ root: usize,
2928
+ value: Value,
2929
+ ) -> i32 {
2281
2930
  catch_unwind(AssertUnwindSafe(|| {
2282
2931
  let e = unsafe { &*engine };
2283
- e.eval(e.root, value, 0, scope::Context::default()) as i32
2932
+ let local = pattern::Caches::default();
2933
+ let caches = unsafe { caches.as_ref() }.unwrap_or(&local);
2934
+ Execution {
2935
+ engine: e,
2936
+ caches,
2937
+ memo: memo::Memo::default(),
2938
+ budget: budget::Budget::default(),
2939
+ }
2940
+ .eval(root, value, 0, scope::Context::default()) as i32
2284
2941
  }))
2285
2942
  .unwrap_or(STATUS_PANIC)
2286
2943
  }
2287
2944
  /// # Safety
2945
+ /// Same requirements as sn_valid; all limit/output pointers must be live.
2946
+ #[unsafe(no_mangle)]
2947
+ pub unsafe extern "C" fn sn_valid_with_limits(
2948
+ engine: *const Engine,
2949
+ caches: *const pattern::Caches,
2950
+ root: usize,
2951
+ value: Value,
2952
+ limits: *const budget::Limits,
2953
+ outcome: *mut budget::Outcome,
2954
+ ) -> i32 {
2955
+ let budget = budget::Budget::new(unsafe { *limits });
2956
+ let result = catch_unwind(AssertUnwindSafe(|| {
2957
+ let local = pattern::Caches::default();
2958
+ let caches = unsafe { caches.as_ref() }.unwrap_or(&local);
2959
+ let execution = Execution {
2960
+ engine: unsafe { &*engine },
2961
+ caches,
2962
+ memo: memo::Memo::default(),
2963
+ budget,
2964
+ };
2965
+ let result = if execution.budget.check_deadline() {
2966
+ execution.eval(root, value, 0, scope::Context::default())
2967
+ } else {
2968
+ Unsupported
2969
+ };
2970
+ let result = if execution.budget.check_deadline() {
2971
+ result
2972
+ } else {
2973
+ Unsupported
2974
+ };
2975
+ unsafe {
2976
+ ((*outcome).kind, (*outcome).attempted) = execution.budget.hit.get();
2977
+ }
2978
+ result as i32
2979
+ }));
2980
+ result.unwrap_or(STATUS_PANIC)
2981
+ }
2982
+ /// # Safety
2288
2983
  /// Engine must be an owned pointer returned by sn_new, freed exactly once.
2289
2984
  #[unsafe(no_mangle)]
2290
2985
  pub unsafe extern "C" fn sn_free(engine: *mut Engine) {
@@ -2303,7 +2998,11 @@ pub unsafe extern "C" fn sn_roots(engine: *const Engine) -> *const Roots {
2303
2998
  /// No mutable root borrow can be live across a Ruby allocation/callback.
2304
2999
  #[unsafe(no_mangle)]
2305
3000
  pub unsafe extern "C" fn sn_roots_mark(roots: *const Roots) {
2306
- for &value in &unsafe { &*roots }.values {
3001
+ for &value in unsafe { &*roots }
3002
+ .values
3003
+ .iter()
3004
+ .chain(&unsafe { &*roots }.binding_names)
3005
+ {
2307
3006
  unsafe { sn_pin(value) };
2308
3007
  }
2309
3008
  }
@@ -2312,7 +3011,8 @@ pub unsafe extern "C" fn sn_roots_mark(roots: *const Roots) {
2312
3011
  #[unsafe(no_mangle)]
2313
3012
  pub unsafe extern "C" fn sn_roots_size(roots: *const Roots) -> usize {
2314
3013
  std::mem::size_of::<Roots>()
2315
- + unsafe { &*roots }.values.capacity() * std::mem::size_of::<Value>()
3014
+ + (unsafe { &*roots }.values.capacity() + unsafe { &*roots }.binding_names.capacity())
3015
+ * std::mem::size_of::<Value>()
2316
3016
  }
2317
3017
  #[unsafe(no_mangle)]
2318
3018
  pub extern "C" fn sn_engine_header_size() -> usize {
@@ -2324,7 +3024,22 @@ pub extern "C" fn sn_engine_header_size() -> usize {
2324
3024
  pub unsafe extern "C" fn sn_size(engine: *const Engine) -> usize {
2325
3025
  let nodes = unsafe { &*std::ptr::addr_of!((*engine).nodes) };
2326
3026
  let operands = unsafe { &*std::ptr::addr_of!((*engine).error_operands) };
3027
+ let contextual = unsafe { &*std::ptr::addr_of!((*engine).memo_contextual) };
3028
+ let guarded = unsafe { &*std::ptr::addr_of!((*engine).memo_guarded) };
3029
+ let sources = unsafe { &*std::ptr::addr_of!((*engine).memo_guard_sources) };
3030
+ let same = unsafe { &*std::ptr::addr_of!((*engine).memo_guard_same_sources) };
3031
+ let scalar = unsafe { &*std::ptr::addr_of!((*engine).memo_guard_scalar_sources) };
3032
+ let amplification = unsafe { &*std::ptr::addr_of!((*engine).memo_guard_scalar_amplification) };
3033
+ let projection = unsafe { &*std::ptr::addr_of!((*engine).memo_scope_projection) };
2327
3034
  std::mem::size_of::<Engine>()
3035
+ + projection.memory_usage()
3036
+ + (contextual.capacity()
3037
+ + guarded.capacity()
3038
+ + sources.capacity()
3039
+ + same.capacity()
3040
+ + scalar.capacity()
3041
+ + amplification.capacity())
3042
+ * std::mem::size_of::<bool>()
2328
3043
  + nodes.capacity() * std::mem::size_of::<Vec<Op>>()
2329
3044
  + operands.capacity() * std::mem::size_of::<Vec<(i32, Value)>>()
2330
3045
  + operands
@@ -2339,6 +3054,7 @@ pub unsafe extern "C" fn sn_size(engine: *const Engine) -> usize {
2339
3054
  .iter()
2340
3055
  .flatten()
2341
3056
  .map(|op| match op {
3057
+ Op::Enum(values) => values.memory_usage(),
2342
3058
  Op::String(_, rules) => {
2343
3059
  std::mem::size_of::<StringRules>()
2344
3060
  + rules
@@ -2347,7 +3063,12 @@ pub unsafe extern "C" fn sn_size(engine: *const Engine) -> usize {
2347
3063
  .map_or(0, pattern::Pattern::memory_usage)
2348
3064
  }
2349
3065
  Op::Object(_, rules) => {
2350
- rules.patterns.capacity() * std::mem::size_of::<(pattern::Pattern, usize)>()
3066
+ rules
3067
+ .pattern_group
3068
+ .as_ref()
3069
+ .map_or(0, pattern::PatternGroup::memory_usage)
3070
+ + rules.patterns.capacity()
3071
+ * std::mem::size_of::<(pattern::Pattern, usize)>()
2351
3072
  + rules
2352
3073
  .patterns
2353
3074
  .iter()
@@ -2363,6 +3084,54 @@ pub unsafe extern "C" fn sn_size(engine: *const Engine) -> usize {
2363
3084
  mod tests {
2364
3085
  use super::*;
2365
3086
 
3087
+ #[test]
3088
+ fn falls_back_for_clustered_slots_without_comparing_distinct_fingerprints() {
3089
+ let values: Vec<_> = (1..=4096).collect();
3090
+ let mut hashes = 0;
3091
+ assert_eq!(
3092
+ indexed_unique(
3093
+ &values,
3094
+ |value| {
3095
+ hashes += 1;
3096
+ Some((value as u64) << 32)
3097
+ },
3098
+ |_, _| panic!("distinct full fingerprints cannot require equality"),
3099
+ ),
3100
+ Valid
3101
+ );
3102
+ assert!(hashes > values.len());
3103
+ assert!(hashes <= values.len() * 2);
3104
+ }
3105
+
3106
+ #[test]
3107
+ fn sorted_collisions_preserve_exact_equality_and_early_results() {
3108
+ let hash = |value| (value != 99).then_some(value as u64 % 2);
3109
+ let equal = |a, b| verdict(a == b);
3110
+ assert_eq!(sorted_unique(&[0, 2, 4, 1, 3, 5], hash, equal), Valid);
3111
+ assert_eq!(sorted_unique(&[0, 2, 0, 99], hash, equal), Invalid);
3112
+ assert_eq!(sorted_unique(&[0, 2, 99, 0], hash, equal), Unsupported);
3113
+ assert_eq!(
3114
+ sorted_unique(&[0, 1, 1, 2], hash, |a, b| {
3115
+ if (a, b) == (0, 2) {
3116
+ Unsupported
3117
+ } else {
3118
+ equal(a, b)
3119
+ }
3120
+ }),
3121
+ Invalid
3122
+ );
3123
+ assert_eq!(
3124
+ sorted_unique(&[0, 2, 1, 1], hash, |a, b| {
3125
+ if (a, b) == (0, 2) {
3126
+ Unsupported
3127
+ } else {
3128
+ equal(a, b)
3129
+ }
3130
+ }),
3131
+ Unsupported
3132
+ );
3133
+ }
3134
+
2366
3135
  #[test]
2367
3136
  fn bounds_unique_index_allocation() {
2368
3137
  assert_eq!(