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