schemurai 2.2.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -7
  3. data/ext/schemurai_native/Cargo.lock +99 -0
  4. data/ext/schemurai_native/Cargo.toml +21 -0
  5. data/ext/schemurai_native/build.rs +35 -0
  6. data/ext/schemurai_native/extconf.rb +63 -0
  7. data/ext/schemurai_native/glue.c +780 -0
  8. data/ext/schemurai_native/protocol.def +63 -0
  9. data/ext/schemurai_native/protocol.h +10 -0
  10. data/ext/schemurai_native/src/budget.rs +101 -0
  11. data/ext/schemurai_native/src/content.rs +574 -0
  12. data/ext/schemurai_native/src/diagnostics.rs +1135 -0
  13. data/ext/schemurai_native/src/enumeration.rs +154 -0
  14. data/ext/schemurai_native/src/evaluation.rs +219 -0
  15. data/ext/schemurai_native/src/format.rs +357 -0
  16. data/ext/schemurai_native/src/graph.rs +50 -0
  17. data/ext/schemurai_native/src/instance_graph.rs +84 -0
  18. data/ext/schemurai_native/src/lib.rs +3139 -0
  19. data/ext/schemurai_native/src/memo.rs +1149 -0
  20. data/ext/schemurai_native/src/memo_scope.rs +240 -0
  21. data/ext/schemurai_native/src/number.rs +1054 -0
  22. data/ext/schemurai_native/src/pattern.rs +431 -0
  23. data/ext/schemurai_native/src/scope.rs +420 -0
  24. data/ext/schemurai_native/src/structural.rs +86 -0
  25. data/ext/schemurai_native/src/tracking.rs +687 -0
  26. data/lib/schemurai/backend.rb +3 -1
  27. data/lib/schemurai/evaluator.rb +45 -3
  28. data/lib/schemurai/limit_budget.rb +78 -0
  29. data/lib/schemurai/limit_exceeded.rb +18 -0
  30. data/lib/schemurai/limited_evaluator.rb +100 -0
  31. data/lib/schemurai/native/diagnostics.rb +52 -0
  32. data/lib/schemurai/native/evaluator.rb +59 -0
  33. data/lib/schemurai/schema_domain.rb +12 -7
  34. data/lib/schemurai/schema_graph.rb +10 -1
  35. data/lib/schemurai/unique_items.rb +2 -0
  36. data/lib/schemurai/version.rb +1 -1
  37. data/lib/schemurai/vm/compiler.rb +7 -3
  38. data/lib/schemurai/vm/evaluator.rb +50 -3
  39. data/lib/schemurai/vm/limited_evaluator.rb +97 -0
  40. data/lib/schemurai.rb +81 -33
  41. data/sig/schemurai.rbs +21 -13
  42. metadata +36 -6
@@ -0,0 +1,3139 @@
1
+ //! A VM over borrowed CRuby VALUE handles. No instance decoding or serialization.
2
+ //! Ruby runs with the GVL held. Evaluation calls only non-allocating C adapters;
3
+ //! Ruby callbacks during compilation are protected on the C side.
4
+ use std::collections::HashMap;
5
+ use std::ffi::{c_char, c_void};
6
+ use std::hash::{BuildHasherDefault, Hasher};
7
+ use std::panic::{AssertUnwindSafe, catch_unwind};
8
+ mod budget;
9
+ mod content;
10
+ mod diagnostics;
11
+ mod enumeration;
12
+ mod evaluation;
13
+ mod format;
14
+ mod graph;
15
+ mod instance_graph;
16
+ mod memo;
17
+ mod memo_scope;
18
+ mod number;
19
+ mod pattern;
20
+ // Some shared constants are consumed only by C or Ruby.
21
+ #[allow(dead_code)]
22
+ mod protocol {
23
+ include!(concat!(env!("OUT_DIR"), "/protocol.rs"));
24
+ }
25
+ use protocol::*;
26
+ mod scope;
27
+ mod structural;
28
+ mod tracking;
29
+ type Value = usize;
30
+ unsafe extern "C" {
31
+ fn sn_pin(value: Value);
32
+ fn sn_nil() -> Value;
33
+ fn sn_undefined() -> Value;
34
+ fn sn_kind(value: Value) -> i32;
35
+ fn sn_shallow_kind(value: Value) -> i32;
36
+ fn sn_fingerprint_view(
37
+ value: Value,
38
+ data: *mut *const c_void,
39
+ length: *mut usize,
40
+ number: *mut f64,
41
+ ) -> i32;
42
+ fn sn_small_hash(value: Value, entries: *mut Value) -> i32;
43
+ fn sn_true(value: Value) -> i32;
44
+ fn sn_fixnum(value: Value) -> i32;
45
+ fn sn_fixnum_flag() -> Value;
46
+ fn sn_array_data(value: Value) -> *const Value;
47
+ fn sn_integer(value: Value) -> i64;
48
+ fn sn_integer128(value: Value, words: *mut u64) -> i32;
49
+ fn sn_integer_bounds128(value: Value, words: *mut u64) -> i32;
50
+ fn sn_number_bounds_view(
51
+ value: Value,
52
+ words: *mut u64,
53
+ kind: *mut i32,
54
+ floating: *mut f64,
55
+ required: i32,
56
+ ) -> i32;
57
+ fn sn_integer1024(value: Value, words: *mut u64) -> i32;
58
+ fn sn_is_bignum(value: Value) -> i32;
59
+ fn sn_big256(value: Value, words: *mut u64) -> i32;
60
+ fn sn_big_hash(value: Value, hash: *mut u64) -> i32;
61
+ fn sn_big_low_word(value: Value) -> u64;
62
+ fn sn_big_low64_word(value: Value, word: *mut u64) -> i32;
63
+ fn sn_big_equal(big: Value, other: Value) -> i32;
64
+ fn sn_big_order(left: Value, right: Value) -> i32;
65
+ fn sn_wide_integer_compare(value: Value, bound: Value) -> i32;
66
+ fn sn_float(value: Value) -> f64;
67
+ fn sn_is_float(value: Value) -> i32;
68
+ fn sn_is_rational(value: Value) -> i32;
69
+ fn sn_len(value: Value) -> usize;
70
+ fn sn_entry(value: Value, index: usize) -> Value;
71
+ fn sn_lookup(hash: Value, key: Value) -> Value;
72
+ fn sn_missing(value: Value) -> i32;
73
+ fn sn_same(a: Value, b: Value) -> i32;
74
+ fn sn_same_class(a: Value, b: Value) -> i32;
75
+ fn sn_string_data(value: Value) -> *const u8;
76
+ fn sn_string_bytes(value: Value) -> usize;
77
+ fn sn_string_equal(a: Value, b: Value) -> i32;
78
+ fn sn_string_length_guarded(value: Value, length: *mut usize) -> i32;
79
+ fn sn_record_key_view(value: Value, data: *mut *const u8, length: *mut usize) -> i32;
80
+ fn sn_string_raw_view(value: Value, data: *mut *const u8, length: *mut usize);
81
+ fn sn_string_view(value: Value, data: *mut *const u8, length: *mut usize) -> i32;
82
+ fn sn_pattern_view(value: Value, data: *mut *const u8, length: *mut usize) -> i32;
83
+ fn sn_pattern_item_view(value: Value, data: *mut *const u8, length: *mut usize) -> i32;
84
+ fn sn_format_item_view(value: Value, data: *mut *const u8, length: *mut usize) -> i32;
85
+ fn sn_json_encoding(value: Value) -> i32;
86
+ fn sn_json_item_view(value: Value, data: *mut *const u8, length: *mut usize) -> i32;
87
+ fn sn_resolve(compiler: Value, program: Value, reference: Value, result: *mut Value) -> i32;
88
+ fn sn_resolution_error(error: Value) -> i32;
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;
92
+ fn sn_anchor_program(compiler: Value, resource: Value, name: Value, result: *mut Value) -> i32;
93
+ fn sn_hash_each(
94
+ value: Value,
95
+ context: *mut c_void,
96
+ callback: unsafe extern "C" fn(*mut c_void, Value, Value) -> i32,
97
+ ) -> i32;
98
+ fn sn_hash_each_key_view(
99
+ value: Value,
100
+ context: *mut c_void,
101
+ callback: unsafe extern "C" fn(*mut c_void, *const u8, usize, Value) -> i32,
102
+ ) -> i32;
103
+ fn sn_field(object: Value, name: *const c_char, value: *mut Value) -> i32;
104
+ fn sn_pairs(hash: Value, value: *mut Value) -> i32;
105
+ fn sn_key_copy(key: Value, value: *mut Value) -> i32;
106
+ fn sn_opcode(symbol: Value) -> i32;
107
+ fn sn_object_each(value: Value, context: *mut c_void) -> i32;
108
+ }
109
+ #[derive(Clone, Copy, Debug, PartialEq)]
110
+ #[repr(i32)]
111
+ enum Verdict {
112
+ Invalid = VERDICT_INVALID,
113
+ Valid = VERDICT_VALID,
114
+ Unsupported = VERDICT_UNSUPPORTED,
115
+ }
116
+ use Verdict::*;
117
+ const MAX_EVALUATION_DEPTH: usize = 256;
118
+ const MAX_UNIQUE_INDEX_BYTES: usize = 64 * 1024 * 1024;
119
+ #[derive(Debug)]
120
+ enum Op {
121
+ Unsupported,
122
+ Boolean(bool),
123
+ Ref(scope::Reference),
124
+ Type(i32),
125
+ Const(Value),
126
+ Enum(Box<enumeration::Enumeration>),
127
+ All(Vec<usize>),
128
+ Any(Vec<(usize, usize)>),
129
+ One(Vec<(usize, usize)>),
130
+ Not(usize),
131
+ Conditional(usize, Option<usize>, Option<usize>),
132
+ Number(i32, Box<number::Rules>),
133
+ String(i32, Box<StringRules>),
134
+ Array(i32, Box<Array>),
135
+ Object(i32, Box<Object>),
136
+ }
137
+ #[derive(Debug)]
138
+ struct Array {
139
+ min: Option<usize>,
140
+ max: Option<usize>,
141
+ unique: bool,
142
+ prefix: Vec<usize>,
143
+ items: Vec<usize>,
144
+ list: bool,
145
+ additional: Option<usize>,
146
+ contains: Option<usize>,
147
+ min_contains: usize,
148
+ max_contains: Option<usize>,
149
+ unevaluated: Option<usize>,
150
+ }
151
+ #[derive(Debug)]
152
+ struct StringRules {
153
+ min: Option<usize>,
154
+ max: Option<usize>,
155
+ pattern: Option<pattern::Pattern>,
156
+ format: Option<format::Format>,
157
+ content: Option<content::Content>,
158
+ }
159
+ impl StringRules {
160
+ fn content_only(&self) -> Option<&content::Content> {
161
+ if self.min.is_none()
162
+ && self.max.is_none()
163
+ && self.pattern.is_none()
164
+ && self.format.is_none()
165
+ {
166
+ self.content.as_ref()
167
+ } else {
168
+ None
169
+ }
170
+ }
171
+ fn format_only(&self) -> Option<&format::Format> {
172
+ if self.min.is_none()
173
+ && self.max.is_none()
174
+ && self.pattern.is_none()
175
+ && self.content.is_none()
176
+ {
177
+ self.format.as_ref()
178
+ } else {
179
+ None
180
+ }
181
+ }
182
+ fn pattern_only(&self) -> Option<&pattern::Pattern> {
183
+ if self.min.is_none()
184
+ && self.max.is_none()
185
+ && self.format.is_none()
186
+ && self.content.is_none()
187
+ {
188
+ self.pattern.as_ref()
189
+ } else {
190
+ None
191
+ }
192
+ }
193
+ fn length_only(&self) -> bool {
194
+ (self.min.is_some() || self.max.is_some())
195
+ && self.pattern.is_none()
196
+ && self.format.is_none()
197
+ && self.content.is_none()
198
+ }
199
+ fn eval_length_guarded(&self, required: i32, value: Value) -> Verdict {
200
+ length_guarded(value, required, self.min, self.max)
201
+ }
202
+ fn eval_guarded(&self, caches: &pattern::Caches, required: i32, value: Value) -> Verdict {
203
+ let mut data = std::ptr::null();
204
+ let mut len = 0;
205
+ let encoding = unsafe { sn_pattern_item_view(value, &mut data, &mut len) };
206
+ if encoding == -TYPE_UNSUPPORTED {
207
+ Unsupported
208
+ } else if encoding < 0 {
209
+ verdict(required == 0)
210
+ } else {
211
+ let bytes = unsafe { byte_view(data, len) };
212
+ self.eval_with_view::<true>(caches, value, Some((bytes, encoding)))
213
+ }
214
+ }
215
+ fn eval(&self, caches: &pattern::Caches, value: Value) -> Verdict {
216
+ self.eval_with_view::<false>(caches, value, None)
217
+ }
218
+ fn eval_with_view<const BORROWED: bool>(
219
+ &self,
220
+ caches: &pattern::Caches,
221
+ value: Value,
222
+ view: Option<(&[u8], i32)>,
223
+ ) -> Verdict {
224
+ // Specialize the standalone path so diagnostics and ordinary string
225
+ // checks do not dispatch through borrowed-view branches at runtime.
226
+ let view = if BORROWED { view } else { None };
227
+ if (self.min.is_some() || self.max.is_some())
228
+ && bounds(unsafe { sn_len(value) }, self.min, self.max) == Invalid
229
+ {
230
+ return Invalid;
231
+ }
232
+ if let Some(pattern) = &self.pattern {
233
+ let result = match view {
234
+ Some((bytes, encoding)) => pattern.eval_bytes(caches, bytes, encoding),
235
+ None => pattern.eval(caches, value),
236
+ };
237
+ if result != Valid {
238
+ return result;
239
+ }
240
+ }
241
+ if let Some(format) = &self.format {
242
+ let result = match view {
243
+ Some((bytes, _)) => format.eval_bytes(bytes),
244
+ None => format.eval(value),
245
+ };
246
+ if result != Valid {
247
+ return result;
248
+ }
249
+ }
250
+ self.content.as_ref().map_or(Valid, |content| match view {
251
+ Some((bytes, encoding)) => content.eval_borrowed(value, bytes, encoding),
252
+ None => content.eval(value),
253
+ })
254
+ }
255
+ }
256
+ #[derive(Debug)]
257
+ enum DiagnosticDependency {
258
+ Required(Value, Vec<Value>, &'static str),
259
+ Schema(Value, usize, &'static str),
260
+ }
261
+ #[derive(Clone, Copy, Debug)]
262
+ struct PropertyTarget {
263
+ name: Value,
264
+ child: usize,
265
+ index: usize,
266
+ }
267
+ #[derive(Debug)]
268
+ struct Object {
269
+ record: Option<RecordObject>,
270
+ min: Option<usize>,
271
+ max: Option<usize>,
272
+ required: Vec<Value>,
273
+ required_order: Vec<Value>,
274
+ properties: Vec<(Value, usize)>,
275
+ property_targets: HashMap<tracking::JsonKey, PropertyTarget, BuildHasherDefault<BucketHasher>>,
276
+ patterns: Vec<(pattern::Pattern, usize)>,
277
+ pattern_names: Vec<Value>,
278
+ pattern_group: Option<pattern::PatternGroup>,
279
+ required_properties: Vec<bool>,
280
+ required_property_indices: Vec<usize>,
281
+ property_keys: tracking::KeySet,
282
+ property_indices: Vec<usize>,
283
+ additional: Option<usize>,
284
+ names: Option<usize>,
285
+ dependent_required: Vec<(Value, Vec<Value>)>,
286
+ dependent_schemas: Vec<(Value, usize)>,
287
+ diagnostic_dependencies: Vec<DiagnosticDependency>,
288
+ unevaluated: Option<usize>,
289
+ }
290
+ #[derive(Clone, Copy, Debug)]
291
+ struct RecordField {
292
+ key: Value,
293
+ name: Value,
294
+ short_key: ShortRecordKey,
295
+ child: Option<usize>,
296
+ bit: u64,
297
+ check: Option<RecordCheck>,
298
+ }
299
+ #[derive(Clone, Copy, Debug)]
300
+ enum RecordCheck {
301
+ IntegerBounds(i64, i64),
302
+ StringLength(i32, Option<usize>, Option<usize>),
303
+ }
304
+ #[derive(Debug)]
305
+ struct RecordObject {
306
+ fields: Vec<RecordField>,
307
+ short_index: Option<RecordShortIndex>,
308
+ // Immutable schema handle, used only to choose a lookup strategy.
309
+ identity_hint: Value,
310
+ required: u64,
311
+ }
312
+ #[derive(Debug)]
313
+ struct RecordShortIndex {
314
+ slots: [u8; 16],
315
+ shift: u8,
316
+ }
317
+ impl RecordShortIndex {
318
+ fn compile(fields: &[RecordField]) -> Option<Self> {
319
+ if !(4..=8).contains(&fields.len()) || fields.iter().any(|field| field.short_key.tag == 0) {
320
+ return None;
321
+ }
322
+ for shift in 0..=60 {
323
+ // Use a four-bit window only if every compiled key owns a distinct
324
+ // slot. Unknown input keys still undergo full equality checks.
325
+ let mut index = Self {
326
+ slots: [0; 16],
327
+ shift,
328
+ };
329
+ let mut unique = true;
330
+ for (at, field) in fields.iter().enumerate() {
331
+ let slot = index.slot(field.short_key);
332
+ if index.slots[slot] != 0 {
333
+ unique = false;
334
+ break;
335
+ }
336
+ index.slots[slot] = at as u8 + 1;
337
+ }
338
+ if unique {
339
+ return Some(index);
340
+ }
341
+ }
342
+ None
343
+ }
344
+ fn slot(&self, key: ShortRecordKey) -> usize {
345
+ // Encoding is excluded: equal ASCII keys may have compatible but
346
+ // different encodings. Exact byte/length/encoding checks follow lookup.
347
+ let word = key.word ^ (key.tag & 15).wrapping_mul(0x9e3779b97f4a7c15);
348
+ ((word >> self.shift) & 15) as usize
349
+ }
350
+ }
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
+
549
+ fn with_record(mut self) -> Self {
550
+ if self.required_order.len() > 64
551
+ || !self.patterns.is_empty()
552
+ || self.names.is_some()
553
+ || !self.diagnostic_dependencies.is_empty()
554
+ || self.unevaluated.is_some()
555
+ {
556
+ return self;
557
+ }
558
+ let mut fields: HashMap<_, _, BuildHasherDefault<BucketHasher>> = self
559
+ .properties
560
+ .iter()
561
+ .map(|&(name, child)| {
562
+ (
563
+ tracking::JsonKey::new(name),
564
+ RecordField {
565
+ key: name,
566
+ name,
567
+ short_key: short_record_key(name),
568
+ child: Some(child),
569
+ bit: 0,
570
+ check: None,
571
+ },
572
+ )
573
+ })
574
+ .collect();
575
+ for &name in &self.required_order {
576
+ fields
577
+ .entry(tracking::JsonKey::new(name))
578
+ .or_insert(RecordField {
579
+ key: name,
580
+ name,
581
+ short_key: short_record_key(name),
582
+ child: None,
583
+ bit: 0,
584
+ check: None,
585
+ });
586
+ }
587
+ if fields.len() > 8 {
588
+ return self;
589
+ }
590
+ for (index, field) in fields.values_mut().enumerate() {
591
+ field.bit = 1_u64 << index;
592
+ }
593
+ // Required names share the field bits already used for duplicate-key
594
+ // detection. Repeated required names therefore need no extra state.
595
+ let required = self.required_order.iter().fold(0, |bits, &name| {
596
+ bits | fields[&tracking::JsonKey::new(name)].bit
597
+ });
598
+ let fields: Vec<_> = fields.into_values().collect();
599
+ let short_index = RecordShortIndex::compile(&fields);
600
+ self.record = Some(RecordObject {
601
+ identity_hint: self.properties.first().map_or(0, |&(key, _)| key),
602
+ fields,
603
+ short_index,
604
+ required,
605
+ });
606
+ self
607
+ }
608
+ }
609
+ #[derive(Default)]
610
+ pub struct Roots {
611
+ values: Vec<Value>,
612
+ binding_names: Vec<Value>,
613
+ }
614
+ #[derive(Default)]
615
+ pub struct Engine {
616
+ nodes: Vec<Vec<Op>>,
617
+ pattern_cache_count: usize,
618
+ error_operands: Vec<Vec<(i32, Value)>>,
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,
628
+ metadata: Vec<scope::Metadata>,
629
+ resources: HashMap<Value, usize>,
630
+ dynamic_bindings: HashMap<(Value, usize), usize>,
631
+ anchor_names: Vec<tracking::JsonKey>,
632
+ anchor_ids: HashMap<tracking::JsonKey, usize, BuildHasherDefault<BucketHasher>>,
633
+ anchor_tables: Vec<Vec<(Value, usize)>>,
634
+ property_ids: HashMap<tracking::JsonKey, usize, BuildHasherDefault<BucketHasher>>,
635
+ indices: HashMap<Value, usize>,
636
+ roots: *mut Roots,
637
+ root: usize,
638
+ fixnum_flag: Value,
639
+ compiler: Value,
640
+ validate_format: bool,
641
+ validate_content: bool,
642
+ undefined: Value,
643
+ }
644
+ impl Engine {
645
+ fn retain(&mut self, value: Value) -> Value {
646
+ // A separate allocation lets GC mark roots without aliasing the
647
+ // compiler's exclusive Engine reference. This borrow ends before any
648
+ // allocating Ruby call; Vec growth uses the Rust allocator only.
649
+ unsafe { &mut *self.roots }.values.push(value);
650
+ value
651
+ }
652
+ fn field(&mut self, value: Value, name: &'static [u8]) -> Result<Value, Value> {
653
+ let mut result = 0;
654
+ if unsafe { sn_field(value, name.as_ptr().cast(), &mut result) } != 0 {
655
+ return Err(result);
656
+ }
657
+ Ok(self.retain(result))
658
+ }
659
+ fn values(&mut self, value: Value) -> Vec<Value> {
660
+ if unsafe { sn_same(value, sn_nil()) } != 0 {
661
+ return vec![];
662
+ }
663
+ (0..unsafe { sn_len(value) })
664
+ .map(|i| {
665
+ let item = unsafe { sn_entry(value, i) };
666
+ self.retain(item)
667
+ })
668
+ .collect()
669
+ }
670
+ fn map(&mut self, value: Value) -> Result<Vec<(Value, Value)>, Value> {
671
+ if unsafe { sn_same(value, sn_nil()) } != 0 {
672
+ return Ok(vec![]);
673
+ }
674
+ let mut pairs = 0;
675
+ if unsafe { sn_pairs(value, &mut pairs) } != 0 {
676
+ return Err(pairs);
677
+ }
678
+ self.retain(pairs);
679
+ Ok(self
680
+ .values(pairs)
681
+ .as_chunks::<2>()
682
+ .0
683
+ .iter()
684
+ .map(|p| (p[0], p[1]))
685
+ .collect())
686
+ }
687
+ fn size_field(&mut self, value: Value, name: &'static [u8]) -> Result<Option<usize>, Value> {
688
+ let n = self.field(value, name)?;
689
+ Ok(if unsafe { sn_same(n, sn_nil()) } != 0 {
690
+ None
691
+ } else if unsafe { sn_fixnum(n) } != 0 {
692
+ Some(unsafe { sn_integer(n) } as usize)
693
+ } else if unsafe { sn_is_float(n) } != 0 {
694
+ // Count keywords allow integral JSON numbers such as 2.0.
695
+ // Saturation is exact for comparisons against representable lengths.
696
+ Some(unsafe { sn_float(n) } as usize)
697
+ } else if unsafe { sn_kind(n) } & TYPE_INTEGER != 0 {
698
+ Some(usize::MAX)
699
+ } else {
700
+ None // The only non-integral bound is maxContains' +Infinity.
701
+ })
702
+ }
703
+ fn child(&mut self, value: Value, name: &'static [u8]) -> Result<Option<usize>, Value> {
704
+ let child = self.field(value, name)?;
705
+ if unsafe { sn_same(child, sn_nil()) } != 0 {
706
+ Ok(None)
707
+ } else {
708
+ self.compile(child).map(Some)
709
+ }
710
+ }
711
+ fn children(&mut self, value: Value) -> Result<Vec<usize>, Value> {
712
+ self.values(value)
713
+ .into_iter()
714
+ .map(|c| self.compile(c))
715
+ .collect()
716
+ }
717
+ fn branches(&mut self, value: Value) -> Result<Vec<(usize, usize)>, Value> {
718
+ let nodes = self.children(value)?;
719
+ let mut groups: Vec<(usize, usize)> = Vec::with_capacity(nodes.len());
720
+ for node in nodes {
721
+ if let Some(group) = groups
722
+ .last_mut()
723
+ .filter(|(prior, _)| *prior == node || self.same_simple_object(*prior, node))
724
+ {
725
+ group.1 += 1;
726
+ } else {
727
+ groups.push((node, 1));
728
+ }
729
+ }
730
+ Ok(groups)
731
+ }
732
+ fn same_simple_object(&self, a: usize, b: usize) -> bool {
733
+ let ([Op::Object(required_a, a)], [Op::Object(required_b, b)]) =
734
+ (self.nodes[a].as_slice(), self.nodes[b].as_slice())
735
+ else {
736
+ return false;
737
+ };
738
+ let simple = |r: &Object| {
739
+ r.patterns.is_empty()
740
+ && r.additional.is_none()
741
+ && r.names.is_none()
742
+ && r.dependent_required.is_empty()
743
+ && r.dependent_schemas.is_empty()
744
+ && r.unevaluated.is_none()
745
+ };
746
+ required_a == required_b
747
+ && simple(a)
748
+ && simple(b)
749
+ && a.min == b.min
750
+ && a.max == b.max
751
+ && a.required == b.required
752
+ && a.required_properties == b.required_properties
753
+ && a.properties.len() == b.properties.len()
754
+ && a.properties
755
+ .iter()
756
+ .zip(&b.properties)
757
+ .all(|(&(ka, ca), &(kb, cb))| {
758
+ // Exact key handles also preserve identity-Hash lookups. Only
759
+ // shape-independent leaves can ignore resource/path metadata.
760
+ ka == kb
761
+ && match (self.nodes[ca].as_slice(), self.nodes[cb].as_slice()) {
762
+ ([Op::Type(a)], [Op::Type(b)]) => a == b,
763
+ ([Op::Boolean(a)], [Op::Boolean(b)]) => a == b,
764
+ _ => false,
765
+ }
766
+ })
767
+ }
768
+ fn compile_map(&mut self, value: Value) -> Result<Vec<(Value, usize)>, Value> {
769
+ self.map(value)?
770
+ .into_iter()
771
+ .map(|(k, c)| Ok((k, self.compile(c)?)))
772
+ .collect()
773
+ }
774
+ fn compile(&mut self, program: Value) -> Result<usize, Value> {
775
+ if let Some(&i) = self.indices.get(&program) {
776
+ return Ok(i);
777
+ }
778
+ self.retain(program);
779
+ let index = self.nodes.len();
780
+ self.indices.insert(program, index);
781
+ self.nodes.push(vec![]);
782
+ self.error_operands.push(vec![]);
783
+ self.tracks.push(false);
784
+ self.metadata.push(scope::Metadata::default());
785
+ let code = self.field(program, b"code\0")?;
786
+ let flags = unsafe { sn_integer(sn_entry(code, 0)) };
787
+ self.tracks[index] = flags & i64::from(TRACKS_EVALUATION) != 0;
788
+ self.compile_scope(program, index, flags)?;
789
+ let mut ops = vec![];
790
+ let mut error_operands = vec![];
791
+ for i in (1..unsafe { sn_len(code) }).step_by(2) {
792
+ let operand = self.retain(unsafe { sn_entry(code, i + 1) });
793
+ let opcode = unsafe { sn_opcode(sn_entry(code, i)) };
794
+ error_operands.push((opcode, operand));
795
+ let op = match opcode {
796
+ OP_BOOLEAN => Op::Boolean(unsafe { sn_true(operand) } != 0),
797
+ OP_TYPE_NULL => Op::Type(TYPE_NULL),
798
+ OP_TYPE_BOOLEAN => Op::Type(TYPE_BOOLEAN),
799
+ OP_TYPE_OBJECT => Op::Type(TYPE_OBJECT),
800
+ OP_TYPE_ARRAY => Op::Type(TYPE_ARRAY),
801
+ OP_TYPE_NUMBER => Op::Type(TYPE_NUMBER),
802
+ OP_TYPE_INTEGER => Op::Type(TYPE_INTEGER),
803
+ OP_TYPE_STRING => Op::Type(TYPE_STRING),
804
+ OP_TYPES => {
805
+ let mask = self.field(operand, b"mask\0")?;
806
+ Op::Type(unsafe { sn_integer(mask) } as i32)
807
+ }
808
+ OP_CONST => Op::Const(operand),
809
+ OP_ENUM => Op::Enum(Box::new(enumeration::Enumeration::compile(
810
+ self.values(operand),
811
+ self.fixnum_flag,
812
+ ))),
813
+ OP_ALL_OF => Op::All(self.children(operand)?),
814
+ OP_ANY_OF => Op::Any(self.branches(operand)?),
815
+ OP_ONE_OF => Op::One(self.branches(operand)?),
816
+ OP_NOT => Op::Not(self.compile(operand)?),
817
+ OP_CONDITIONAL => {
818
+ let condition = self.field(operand, b"condition\0")?;
819
+ Op::Conditional(
820
+ self.compile(condition)?,
821
+ self.child(operand, b"then_branch\0")?,
822
+ self.child(operand, b"else_branch\0")?,
823
+ )
824
+ }
825
+ OP_NUMBER | OP_TYPED_NUMBER | OP_TYPED_INTEGER => {
826
+ if let Some(rules) = self.compile_number(operand)? {
827
+ Op::Number(
828
+ if opcode == OP_NUMBER {
829
+ 0
830
+ } else if opcode == OP_TYPED_NUMBER {
831
+ TYPE_NUMBER
832
+ } else {
833
+ TYPE_INTEGER
834
+ },
835
+ Box::new(rules),
836
+ )
837
+ } else {
838
+ Op::Unsupported
839
+ }
840
+ }
841
+ OP_STRING | OP_TYPED_STRING => {
842
+ let pattern = self.field(operand, b"pattern\0")?;
843
+ let format = self.field(operand, b"format\0")?;
844
+ let base64 = self.field(operand, b"decode_base64\0")?;
845
+ let json = self.field(operand, b"parse_json\0")?;
846
+ {
847
+ let content = if self.validate_content
848
+ && unsafe { sn_true(base64) | sn_true(json) } != 0
849
+ {
850
+ Some(content::Content {
851
+ base64: unsafe { sn_true(base64) } != 0,
852
+ json: unsafe { sn_true(json) } != 0,
853
+ })
854
+ } else {
855
+ None
856
+ };
857
+ let assertion = self.field(operand, b"format_assertion\0")?;
858
+ let format = if (self.validate_format || unsafe { sn_true(assertion) } != 0)
859
+ && unsafe { sn_same(format, sn_nil()) } == 0
860
+ {
861
+ let name = self.field(format, b"name\0")?;
862
+ if unsafe { sn_same(name, sn_nil()) } != 0 {
863
+ None
864
+ } else {
865
+ Some(format::Format::compile(name))
866
+ }
867
+ } else {
868
+ None
869
+ };
870
+ Op::String(
871
+ if opcode == OP_TYPED_STRING {
872
+ TYPE_STRING
873
+ } else {
874
+ 0
875
+ },
876
+ Box::new(StringRules {
877
+ format,
878
+ content,
879
+ min: self.size_field(operand, b"min_length\0")?,
880
+ max: self.size_field(operand, b"max_length\0")?,
881
+ pattern: if unsafe { sn_same(pattern, sn_nil()) } != 0 {
882
+ None
883
+ } else {
884
+ Some(pattern::Pattern::compile(
885
+ pattern,
886
+ &mut self.pattern_cache_count,
887
+ ))
888
+ },
889
+ }),
890
+ )
891
+ }
892
+ }
893
+ OP_ARRAY | OP_TYPED_ARRAY => {
894
+ let unique = self.field(operand, b"unique\0")?;
895
+ let prefix = self.field(operand, b"prefix_items\0")?;
896
+ let prefix = self.children(prefix)?;
897
+ let items = self.field(operand, b"items\0")?;
898
+ let list = self.field(operand, b"items_list\0")?;
899
+ let list = unsafe { sn_true(list) } != 0;
900
+ let items = if list {
901
+ self.children(items)?
902
+ } else if unsafe { sn_same(items, sn_nil()) } != 0 {
903
+ vec![]
904
+ } else {
905
+ vec![self.compile(items)?]
906
+ };
907
+ Op::Array(
908
+ if opcode == OP_TYPED_ARRAY {
909
+ TYPE_ARRAY
910
+ } else {
911
+ 0
912
+ },
913
+ Box::new(Array {
914
+ unique: unsafe { sn_true(unique) } != 0,
915
+ min: self.size_field(operand, b"min_items\0")?,
916
+ max: self.size_field(operand, b"max_items\0")?,
917
+ prefix,
918
+ items,
919
+ list,
920
+ additional: self.child(operand, b"additional\0")?,
921
+ contains: self.child(operand, b"contains\0")?,
922
+ min_contains: self.size_field(operand, b"min_contains\0")?.unwrap_or(1),
923
+ max_contains: self.size_field(operand, b"max_contains\0")?,
924
+ unevaluated: self.child(operand, b"unevaluated\0")?,
925
+ }),
926
+ )
927
+ }
928
+ OP_OBJECT | OP_TYPED_OBJECT => {
929
+ let patterns = self.field(operand, b"patterns\0")?;
930
+ let dependencies = self.field(operand, b"dependencies\0")?;
931
+ {
932
+ let patterns = self.compile_map(patterns)?;
933
+ let pattern_names: Vec<_> = patterns.iter().map(|&(key, _)| key).collect();
934
+ let patterns: Vec<_> = patterns
935
+ .into_iter()
936
+ .map(|(key, child)| {
937
+ (
938
+ pattern::Pattern::compile(key, &mut self.pattern_cache_count),
939
+ child,
940
+ )
941
+ })
942
+ .collect();
943
+ let pattern_group =
944
+ pattern::PatternGroup::compile(&pattern_names, &patterns);
945
+ let required = self.field(operand, b"required\0")?;
946
+ let required = self.values(required);
947
+ let required_order = required.clone();
948
+ let properties = self.field(operand, b"properties\0")?;
949
+ let properties = self.compile_map(properties)?;
950
+ if required
951
+ .iter()
952
+ .chain(properties.iter().map(|(key, _)| key))
953
+ .any(|&key| unsafe { sn_kind(key) } != TYPE_STRING)
954
+ {
955
+ ops.push(Op::Unsupported);
956
+ continue;
957
+ }
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();
969
+ let property_locations: Vec<_> = properties
970
+ .iter()
971
+ .map(|&(key, _)| tracking::JsonKey::new(key))
972
+ .collect();
973
+ let property_keys = property_locations.iter().copied().collect();
974
+ let property_indices = property_locations
975
+ .iter()
976
+ .map(|&key| {
977
+ let next = self.property_ids.len();
978
+ *self.property_ids.entry(key).or_insert(next)
979
+ })
980
+ .collect();
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
991
+ .iter()
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
+ )
1001
+ })
1002
+ .collect();
1003
+ let required_property_indices = required_properties
1004
+ .iter()
1005
+ .enumerate()
1006
+ .filter_map(|(index, &required)| required.then_some(index))
1007
+ .collect();
1008
+ let required = required
1009
+ .into_iter()
1010
+ .filter(|&name| {
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
+ }
1018
+ })
1019
+ .collect();
1020
+ let dependent = self.field(operand, b"dependent_required\0")?;
1021
+ let mut dependent_required = vec![];
1022
+ let mut dependent_schemas = vec![];
1023
+ let mut diagnostic_dependencies = vec![];
1024
+ for pair in self.values(dependencies) {
1025
+ let name = self.retain(unsafe { sn_entry(pair, 0) });
1026
+ let dependency = self.retain(unsafe { sn_entry(pair, 1) });
1027
+ if unsafe { sn_kind(dependency) } == TYPE_ARRAY {
1028
+ let names = self.values(dependency);
1029
+ diagnostic_dependencies.push(DiagnosticDependency::Required(
1030
+ name,
1031
+ names.clone(),
1032
+ "dependencies",
1033
+ ));
1034
+ dependent_required.push((name, names));
1035
+ } else {
1036
+ let child = self.compile(dependency)?;
1037
+ diagnostic_dependencies.push(DiagnosticDependency::Schema(
1038
+ name,
1039
+ child,
1040
+ "dependencies",
1041
+ ));
1042
+ dependent_schemas.push((name, child));
1043
+ }
1044
+ }
1045
+ for pair in self.values(dependent) {
1046
+ let name = self.retain(unsafe { sn_entry(pair, 0) });
1047
+ let names = self.values(unsafe { sn_entry(pair, 1) });
1048
+ diagnostic_dependencies.push(DiagnosticDependency::Required(
1049
+ name,
1050
+ names.clone(),
1051
+ "dependentRequired",
1052
+ ));
1053
+ dependent_required.push((name, names));
1054
+ }
1055
+ let dependent = self.field(operand, b"dependent_schemas\0")?;
1056
+ let schemas = self.compile_map(dependent)?;
1057
+ diagnostic_dependencies.extend(schemas.iter().map(|&(name, child)| {
1058
+ DiagnosticDependency::Schema(name, child, "dependentSchemas")
1059
+ }));
1060
+ dependent_schemas.extend(schemas);
1061
+ if dependent_required.iter().any(|(name, keys)| {
1062
+ (unsafe { sn_kind(*name) }) != TYPE_STRING
1063
+ || keys
1064
+ .iter()
1065
+ .any(|&key| unsafe { sn_kind(key) } != TYPE_STRING)
1066
+ }) || dependent_schemas
1067
+ .iter()
1068
+ .any(|&(key, _)| unsafe { sn_kind(key) } != TYPE_STRING)
1069
+ {
1070
+ ops.push(Op::Unsupported);
1071
+ continue;
1072
+ }
1073
+ let mut object = Object {
1074
+ record: None,
1075
+ min: self.size_field(operand, b"min_properties\0")?,
1076
+ max: self.size_field(operand, b"max_properties\0")?,
1077
+ required,
1078
+ required_order,
1079
+ properties,
1080
+ property_targets,
1081
+ patterns,
1082
+ pattern_names,
1083
+ pattern_group,
1084
+ required_properties,
1085
+ required_property_indices,
1086
+ property_keys,
1087
+ property_indices,
1088
+ additional: self.child(operand, b"additional\0")?,
1089
+ names: self.child(operand, b"property_names\0")?,
1090
+ dependent_required,
1091
+ dependent_schemas,
1092
+ diagnostic_dependencies,
1093
+ unevaluated: self.child(operand, b"unevaluated\0")?,
1094
+ }
1095
+ .with_record();
1096
+ if let Some(record) = &mut object.record {
1097
+ for field in &mut record.fields {
1098
+ field.check = field.child.and_then(|child| {
1099
+ match self.nodes[child].as_slice() {
1100
+ [Op::Number(_, rules)] => {
1101
+ rules.fast_limits.as_ref().map(|limits| {
1102
+ let (min, max) = limits.iter().fold(
1103
+ (i64::MIN, i64::MAX),
1104
+ |(min, max), &(bit, limit)| match bit {
1105
+ MAXIMUM => (min, max.min(limit)),
1106
+ MINIMUM => (min.max(limit), max),
1107
+ EXCLUSIVE_MAXIMUM => {
1108
+ (min, max.min(limit.saturating_sub(1)))
1109
+ }
1110
+ _ => {
1111
+ (min.max(limit.saturating_add(1)), max)
1112
+ }
1113
+ },
1114
+ );
1115
+ RecordCheck::IntegerBounds(min, max)
1116
+ })
1117
+ }
1118
+ [Op::String(required, rules)] if rules.length_only() => {
1119
+ Some(RecordCheck::StringLength(
1120
+ *required, rules.min, rules.max,
1121
+ ))
1122
+ }
1123
+ _ => None,
1124
+ }
1125
+ });
1126
+ let mut key = 0;
1127
+ if unsafe { sn_key_copy(field.name, &mut key) } != 0 {
1128
+ return Err(key);
1129
+ }
1130
+ // Distinct schema-only lookup handles also work
1131
+ // when VM keys and input keys share an fstring.
1132
+ field.name = self.retain(key);
1133
+ }
1134
+ }
1135
+ Op::Object(
1136
+ if opcode == OP_TYPED_OBJECT {
1137
+ TYPE_OBJECT
1138
+ } else {
1139
+ 0
1140
+ },
1141
+ Box::new(object),
1142
+ )
1143
+ }
1144
+ }
1145
+ OP_REF | OP_RECURSIVE_REF | OP_DYNAMIC_REF => {
1146
+ let reference = self.field(operand, b"value\0")?;
1147
+ let mut target = 0;
1148
+ if unsafe { sn_resolve(self.compiler, program, reference, &mut target) } != 0 {
1149
+ if unsafe { sn_resolution_error(target) } != 0 {
1150
+ Op::Unsupported
1151
+ } else {
1152
+ return Err(target);
1153
+ }
1154
+ } else {
1155
+ self.retain(target);
1156
+ let target = self.compile(target)?;
1157
+ self.compile_reference(operand, opcode, target)?
1158
+ }
1159
+ }
1160
+ _ => Op::Unsupported,
1161
+ };
1162
+ ops.push(op);
1163
+ }
1164
+ self.nodes[index] = ops;
1165
+ self.error_operands[index] = error_operands;
1166
+ Ok(index)
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<'_> {
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 {
1227
+ let frame = scope::ScopeFrame {
1228
+ resource: self.metadata[node].resource,
1229
+ parent: active.scope,
1230
+ primary_binding: active
1231
+ .scope
1232
+ .and_then(|frame| frame.primary_binding)
1233
+ .or(self.metadata[node].primary_binding),
1234
+ };
1235
+ let active = self.enter_scope(node, active, &frame);
1236
+ if self.tracks[node] {
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
+ };
1244
+ }
1245
+ if depth > MAX_EVALUATION_DEPTH {
1246
+ return Unsupported;
1247
+ }
1248
+ if let [Op::String(required, rules)] = self.nodes[node].as_slice()
1249
+ && rules.pattern.is_some()
1250
+ {
1251
+ return rules.eval_guarded(self.caches, *required, value);
1252
+ }
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
+ });
1260
+ if kind == TYPE_UNSUPPORTED {
1261
+ return Unsupported;
1262
+ }
1263
+ for op in &self.nodes[node] {
1264
+ if !self.budget.checkpoint() {
1265
+ return Unsupported;
1266
+ }
1267
+ let outcome = match op {
1268
+ Op::Ref(target) => {
1269
+ if RefFrame::contains(active.active, node, value) {
1270
+ Valid
1271
+ } else {
1272
+ let frame = RefFrame {
1273
+ source: node,
1274
+ value,
1275
+ parent: active.active,
1276
+ };
1277
+ self.eval_with_kind(
1278
+ self.reference_target(*target, active),
1279
+ value,
1280
+ depth + 1,
1281
+ scope::Context {
1282
+ active: Some(&frame),
1283
+ ..active
1284
+ },
1285
+ Some(kind),
1286
+ )
1287
+ }
1288
+ }
1289
+ Op::All(nodes) => {
1290
+ let mut outcome = Valid;
1291
+ for &n in nodes {
1292
+ if !self.budget.checkpoint() {
1293
+ return Unsupported;
1294
+ }
1295
+ outcome = self.eval_with_kind(n, value, depth + 1, active, Some(kind));
1296
+ if outcome != Valid {
1297
+ break;
1298
+ }
1299
+ }
1300
+ outcome
1301
+ }
1302
+ Op::Any(nodes) | Op::One(nodes) => {
1303
+ let branch_kind = if matches!(op, Op::Any(_)) {
1304
+ evaluation::BranchKind::Any
1305
+ } else {
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,
1315
+ }
1316
+ }
1317
+ Op::Not(n) => match self.eval_with_kind(*n, value, depth + 1, active, Some(kind)) {
1318
+ Valid => Invalid,
1319
+ Invalid => Valid,
1320
+ Unsupported => Unsupported,
1321
+ },
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
+ }
1333
+ Op::Array(required, rules) => {
1334
+ if *required != 0 && kind != TYPE_ARRAY {
1335
+ Invalid
1336
+ } else if kind != TYPE_ARRAY {
1337
+ Valid
1338
+ } else {
1339
+ self.array(rules, value, depth + 1, active)
1340
+ }
1341
+ }
1342
+ Op::Object(required, rules) => {
1343
+ if *required != 0 && kind != TYPE_OBJECT {
1344
+ Invalid
1345
+ } else if kind != TYPE_OBJECT {
1346
+ Valid
1347
+ } else {
1348
+ self.object(rules, value, depth + 1, active)
1349
+ }
1350
+ }
1351
+ _ => self.leaf(op, kind, value, depth),
1352
+ };
1353
+ if outcome != Valid {
1354
+ return outcome;
1355
+ }
1356
+ }
1357
+ Valid
1358
+ }
1359
+ fn leaf(&self, op: &Op, kind: i32, value: Value, depth: usize) -> Verdict {
1360
+ match op {
1361
+ Op::Unsupported => Unsupported,
1362
+ Op::Boolean(v) => verdict(*v),
1363
+ Op::Type(mask) => verdict(kind & mask != 0),
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),
1366
+ Op::Number(required, rules) => {
1367
+ if *required != 0 && kind & required == 0 {
1368
+ Invalid
1369
+ } else if kind & TYPE_NUMBER == 0 {
1370
+ Valid
1371
+ } else {
1372
+ rules.eval(value)
1373
+ }
1374
+ }
1375
+ Op::String(required, rules) => {
1376
+ if *required != 0 && kind != TYPE_STRING {
1377
+ Invalid
1378
+ } else if kind != TYPE_STRING {
1379
+ Valid
1380
+ } else {
1381
+ rules.eval(self.caches, value)
1382
+ }
1383
+ }
1384
+ _ => unreachable!("applicators must be evaluated with their context"),
1385
+ }
1386
+ }
1387
+ fn array(&self, r: &Array, value: Value, depth: usize, active: scope::Context<'_>) -> Verdict {
1388
+ if !self.budget.checkpoint() {
1389
+ return Unsupported;
1390
+ }
1391
+ let len = unsafe { sn_len(value) };
1392
+ if bounds(len, r.min, r.max) == Invalid {
1393
+ return Invalid;
1394
+ }
1395
+ if r.unique {
1396
+ let result = unique(value, depth, self.fixnum_flag, &self.budget);
1397
+ if result != Valid {
1398
+ return result;
1399
+ }
1400
+ }
1401
+ for (i, &child) in r.prefix.iter().take(len).enumerate() {
1402
+ if !self.budget.checkpoint() {
1403
+ return Unsupported;
1404
+ }
1405
+ let result = self.eval(child, unsafe { sn_entry(value, i) }, depth, active);
1406
+ if result != Valid {
1407
+ return result;
1408
+ }
1409
+ }
1410
+ if r.list {
1411
+ for (i, &child) in r.items.iter().take(len).enumerate() {
1412
+ if !self.budget.checkpoint() {
1413
+ return Unsupported;
1414
+ }
1415
+ let result = self.eval(child, unsafe { sn_entry(value, i) }, depth, active);
1416
+ if result != Valid {
1417
+ return result;
1418
+ }
1419
+ }
1420
+ if let Some(child) = r.additional {
1421
+ for i in r.items.len()..len {
1422
+ if !self.budget.checkpoint() {
1423
+ return Unsupported;
1424
+ }
1425
+ let result = self.eval(child, unsafe { sn_entry(value, i) }, depth, active);
1426
+ if result != Valid {
1427
+ return result;
1428
+ }
1429
+ }
1430
+ }
1431
+ } else if let Some(&child) = r.items.first() {
1432
+ // Borrow CRuby's storage only for this Ruby-allocation-free traversal.
1433
+ // No Ruby callback or Ruby allocation occurs while it is live.
1434
+ let values = if len == 0 {
1435
+ &[]
1436
+ } else {
1437
+ unsafe { std::slice::from_raw_parts(sn_array_data(value), len) }
1438
+ };
1439
+ let primitive = match self.nodes[child].as_slice() {
1440
+ [Op::Type(mask)] if mask & TYPE_NUMBER != 0 || mask & TYPE_INTEGER != 0 => {
1441
+ Some(&[][..])
1442
+ }
1443
+ [Op::Number(_, rules)] => rules.fast_limits.as_deref(),
1444
+ _ => None,
1445
+ };
1446
+ let number = match self.nodes[child].as_slice() {
1447
+ [Op::Number(required, rules)]
1448
+ if depth <= MAX_EVALUATION_DEPTH && !self.tracks[child] =>
1449
+ {
1450
+ Some((*required, rules))
1451
+ }
1452
+ _ => None,
1453
+ };
1454
+ let string = match self.nodes[child].as_slice() {
1455
+ [Op::String(required, rules)] if depth <= MAX_EVALUATION_DEPTH => {
1456
+ Some((*required, rules))
1457
+ }
1458
+ _ => None,
1459
+ };
1460
+ let object = match self.nodes[child].as_slice() {
1461
+ [Op::Object(required, rules)]
1462
+ if depth <= MAX_EVALUATION_DEPTH
1463
+ && !self.tracks[child]
1464
+ && rules.record.is_some() =>
1465
+ {
1466
+ Some((*required, rules))
1467
+ }
1468
+ _ => None,
1469
+ };
1470
+ // Sibling items enter the same schema resource. Reuse the immutable
1471
+ // scope frame while their child evaluations remain synchronous.
1472
+ let frame = scope::ScopeFrame {
1473
+ resource: self.metadata[child].resource,
1474
+ parent: active.scope,
1475
+ primary_binding: active
1476
+ .scope
1477
+ .and_then(|frame| frame.primary_binding)
1478
+ .or(self.metadata[child].primary_binding),
1479
+ };
1480
+ let object_active = self.enter_scope(child, active, &frame);
1481
+ let pattern = string.and_then(|(required, rules)| {
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())
1492
+ });
1493
+ let content = string.and_then(|(required, rules)| {
1494
+ rules
1495
+ .content_only()
1496
+ .map(|content| (required, content))
1497
+ .filter(|_| !self.budget.timed())
1498
+ });
1499
+ if let Some((required, pattern)) = pattern {
1500
+ let result = pattern.eval_items(
1501
+ self.caches,
1502
+ required,
1503
+ values.get(r.prefix.len()..).unwrap_or(&[]),
1504
+ );
1505
+ if result != Valid {
1506
+ return result;
1507
+ }
1508
+ } else if let Some((required, format)) = format {
1509
+ let result =
1510
+ format.eval_items(required, values.get(r.prefix.len()..).unwrap_or(&[]));
1511
+ if result != Valid {
1512
+ return result;
1513
+ }
1514
+ } else if let Some((required, content)) = content {
1515
+ let result =
1516
+ content.eval_items(required, values.get(r.prefix.len()..).unwrap_or(&[]));
1517
+ if result != Valid {
1518
+ return result;
1519
+ }
1520
+ } else if let Some((required, rules)) = object {
1521
+ let result = self.record_items(
1522
+ rules,
1523
+ required,
1524
+ values.get(r.prefix.len()..).unwrap_or(&[]),
1525
+ depth + 1,
1526
+ object_active,
1527
+ );
1528
+ if result != Valid {
1529
+ return result;
1530
+ }
1531
+ } else if let Some(limits) = primitive {
1532
+ let result = self.integer_items(
1533
+ limits,
1534
+ values.get(r.prefix.len()..).unwrap_or(&[]),
1535
+ child,
1536
+ depth,
1537
+ active,
1538
+ );
1539
+ if result != Valid {
1540
+ return result;
1541
+ }
1542
+ } else {
1543
+ for &item in values.iter().skip(r.prefix.len()) {
1544
+ if !self.budget.checkpoint() {
1545
+ return Unsupported;
1546
+ }
1547
+ let result = if let Some((required, rules)) = number {
1548
+ rules.eval_guarded(required, item)
1549
+ } else if let Some((required, rules)) = string {
1550
+ if rules.pattern.is_some() {
1551
+ rules.eval_guarded(self.caches, required, item)
1552
+ } else if rules.length_only() {
1553
+ rules.eval_length_guarded(required, item)
1554
+ } else {
1555
+ let kind = unsafe { sn_kind(item) };
1556
+ if kind == TYPE_UNSUPPORTED {
1557
+ Unsupported
1558
+ } else if kind != TYPE_STRING {
1559
+ verdict(required == 0)
1560
+ } else {
1561
+ rules.eval(self.caches, item)
1562
+ }
1563
+ }
1564
+ } else {
1565
+ self.eval(child, item, depth, active)
1566
+ };
1567
+ if result != Valid {
1568
+ return result;
1569
+ }
1570
+ }
1571
+ }
1572
+ }
1573
+ if let Some(child) = r.contains {
1574
+ if let Some(status) = self.fixnum_contains(r, child, value, len, depth) {
1575
+ return status;
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
+ };
1585
+ if bounds(matches, Some(r.min_contains), r.max_contains) == Invalid {
1586
+ return Invalid;
1587
+ }
1588
+ }
1589
+ Valid
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
+ }
1632
+ #[inline(never)]
1633
+ fn integer_items(
1634
+ &self,
1635
+ limits: &[(i32, i64)],
1636
+ values: &[Value],
1637
+ child: usize,
1638
+ depth: usize,
1639
+ active: scope::Context<'_>,
1640
+ ) -> Verdict {
1641
+ // Interpret exact limits once; only header-validated immediate integers
1642
+ // use this interval. Other values retain the original guarded evaluator.
1643
+ let (min, max) =
1644
+ limits.iter().fold(
1645
+ (i64::MIN, i64::MAX),
1646
+ |(min, max), &(bit, limit)| match bit {
1647
+ MAXIMUM => (min, max.min(limit)),
1648
+ MINIMUM => (min.max(limit), max),
1649
+ EXCLUSIVE_MAXIMUM => (min, max.min(limit.saturating_sub(1))),
1650
+ _ => (min.max(limit.saturating_add(1)), max),
1651
+ },
1652
+ );
1653
+ let number = match self.nodes[child].as_slice() {
1654
+ [Op::Number(required, rules)]
1655
+ if depth <= MAX_EVALUATION_DEPTH && !self.tracks[child] =>
1656
+ {
1657
+ Some((*required, rules))
1658
+ }
1659
+ _ => None,
1660
+ };
1661
+ for &value in values {
1662
+ if !self.budget.checkpoint() {
1663
+ return Unsupported;
1664
+ }
1665
+ let result = if value & self.fixnum_flag != 0 {
1666
+ let n = immediate_integer(value);
1667
+ verdict(n >= min && n <= max)
1668
+ } else if let Some((required, rules)) = number {
1669
+ rules.eval_guarded(required, value)
1670
+ } else {
1671
+ self.eval(child, value, depth, active)
1672
+ };
1673
+ if result != Valid {
1674
+ return result;
1675
+ }
1676
+ }
1677
+ Valid
1678
+ }
1679
+ #[inline(never)]
1680
+ fn record_items(
1681
+ &self,
1682
+ rules: &Object,
1683
+ required: i32,
1684
+ values: &[Value],
1685
+ depth: usize,
1686
+ active: scope::Context<'_>,
1687
+ ) -> Verdict {
1688
+ for &value in values {
1689
+ if !self.budget.checkpoint() {
1690
+ return Unsupported;
1691
+ }
1692
+ let kind = if value & self.fixnum_flag != 0 {
1693
+ TYPE_INTEGRAL_NUMBER
1694
+ } else {
1695
+ unsafe { sn_shallow_kind(value) }
1696
+ };
1697
+ let result = if kind == TYPE_UNSUPPORTED {
1698
+ Unsupported
1699
+ } else if kind != TYPE_OBJECT {
1700
+ verdict(required == 0)
1701
+ } else {
1702
+ self.object(rules, value, depth, active)
1703
+ };
1704
+ if result != Valid {
1705
+ return result;
1706
+ }
1707
+ }
1708
+ Valid
1709
+ }
1710
+ fn object(
1711
+ &self,
1712
+ r: &Object,
1713
+ value: Value,
1714
+ depth: usize,
1715
+ active: scope::Context<'_>,
1716
+ ) -> Verdict {
1717
+ if !self.budget.checkpoint() {
1718
+ return Unsupported;
1719
+ }
1720
+ if let Some(record) = &r.record {
1721
+ // C initializes only the written key/value pairs. The untouched
1722
+ // tail is never read, including empty and unsupported records.
1723
+ let mut buffer = std::mem::MaybeUninit::<[Value; 16]>::uninit();
1724
+ let len = unsafe { sn_small_hash(value, buffer.as_mut_ptr().cast()) };
1725
+ if len == SMALL_HASH_TOO_LARGE {
1726
+ return if unsafe { sn_kind(value) } == TYPE_OBJECT {
1727
+ self.object_general(r, value, depth, active)
1728
+ } else {
1729
+ Unsupported
1730
+ };
1731
+ }
1732
+ if len < 0 {
1733
+ return Unsupported;
1734
+ }
1735
+ let ordinary = len & SMALL_HASH_ORDINARY != 0;
1736
+ let len = len & SMALL_HASH_COUNT_MASK;
1737
+ let status = bounds(len as usize, r.min, r.max);
1738
+ if status != Valid {
1739
+ return status;
1740
+ }
1741
+ // Successful calls wrote exactly len pairs, bounded by eight;
1742
+ // no Ruby callback or allocation can change the Hash during C's walk.
1743
+ let entries = unsafe {
1744
+ std::slice::from_raw_parts(buffer.as_ptr().cast::<Value>(), 2 * len as usize)
1745
+ };
1746
+ let mut context = RecordContext {
1747
+ engine: self,
1748
+ rules: r,
1749
+ record,
1750
+ // Choose for this record only. Both lookup strategies verify
1751
+ // every actual key; mixed layouts cannot change the verdict.
1752
+ indexed: record.short_index.is_some()
1753
+ && entries.first().is_some_and(|key| {
1754
+ record.identity_hint != *key
1755
+ && !record.fields.iter().any(|field| field.key == *key)
1756
+ }),
1757
+ depth,
1758
+ active,
1759
+ visited: 0,
1760
+ // A proven ordinary Hash never uses this lookup handle.
1761
+ // Seed its unused slot to avoid collecting a distinct key.
1762
+ probe: ordinary.then_some(self.undefined),
1763
+ status,
1764
+ };
1765
+ for entry in entries.as_chunks::<2>().0 {
1766
+ if !self.budget.checkpoint() {
1767
+ return Unsupported;
1768
+ }
1769
+ if record_entry(&mut context, entry[0], entry[1]) != Valid {
1770
+ return Unsupported;
1771
+ }
1772
+ }
1773
+ if !ordinary && let Some(name) = context.probe {
1774
+ // All keys have now been checked. A successful lookup by
1775
+ // a distinct equal schema key proves ordinary Hash key
1776
+ // semantics, without calling a Ruby identity predicate.
1777
+ if missing(value, name, self.undefined) {
1778
+ return Unsupported;
1779
+ }
1780
+ } else if !ordinary && context.visited != 0 {
1781
+ return Unsupported;
1782
+ }
1783
+ return if context.status != Valid {
1784
+ context.status
1785
+ } else {
1786
+ verdict(context.visited & record.required == record.required)
1787
+ };
1788
+ }
1789
+ self.object_general(r, value, depth, active)
1790
+ }
1791
+ fn object_general(
1792
+ &self,
1793
+ r: &Object,
1794
+ value: Value,
1795
+ depth: usize,
1796
+ active: scope::Context<'_>,
1797
+ ) -> Verdict {
1798
+ let len = unsafe { sn_len(value) };
1799
+ if bounds(len, r.min, r.max) == Invalid {
1800
+ return Invalid;
1801
+ }
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
+ };
1816
+ if r.names.is_some() || !r.patterns.is_empty() || (r.additional.is_some() && found < len) {
1817
+ let mut context = ObjectContext {
1818
+ engine: self,
1819
+ rules: r,
1820
+ depth,
1821
+ active,
1822
+ };
1823
+ let result =
1824
+ unsafe { sn_object_each(value, (&mut context as *mut ObjectContext).cast()) };
1825
+ if result != VERDICT_VALID {
1826
+ return if result == 0 { Invalid } else { Unsupported };
1827
+ }
1828
+ }
1829
+ let status = r.check_dependent_required(value, self.undefined, &self.budget);
1830
+ if status != Valid {
1831
+ return status;
1832
+ }
1833
+ for &(name, child) in &r.dependent_schemas {
1834
+ if !self.budget.checkpoint() {
1835
+ return Unsupported;
1836
+ }
1837
+ if !missing(value, name, self.undefined) {
1838
+ let result = self.eval(child, value, depth, active);
1839
+ if result != Valid {
1840
+ return result;
1841
+ }
1842
+ }
1843
+ }
1844
+ Valid
1845
+ }
1846
+ fn instance_kind(&self, node: usize, value: Value) -> i32 {
1847
+ if matches!(self.nodes[node].as_slice(), [Op::Object(_, rules)] if rules.record.is_some()) {
1848
+ unsafe { sn_shallow_kind(value) }
1849
+ } else {
1850
+ unsafe { sn_kind(value) }
1851
+ }
1852
+ }
1853
+ fn record_value(
1854
+ &self,
1855
+ node: usize,
1856
+ value: Value,
1857
+ depth: usize,
1858
+ active: scope::Context<'_>,
1859
+ ) -> Verdict {
1860
+ if !self.budget.checkpoint() {
1861
+ return Unsupported;
1862
+ }
1863
+ match self.nodes[node].as_slice() {
1864
+ [Op::String(required, rules)]
1865
+ if depth <= MAX_EVALUATION_DEPTH && rules.length_only() =>
1866
+ {
1867
+ rules.eval_length_guarded(*required, value)
1868
+ }
1869
+ [Op::String(required, rules)]
1870
+ if depth <= MAX_EVALUATION_DEPTH && rules.pattern.is_some() =>
1871
+ {
1872
+ rules.eval_guarded(self.caches, *required, value)
1873
+ }
1874
+ [
1875
+ op @ (Op::Type(_)
1876
+ | Op::Boolean(_)
1877
+ | Op::Number(..)
1878
+ | Op::String(..)
1879
+ | Op::Const(_)
1880
+ | Op::Enum(_)),
1881
+ ] if depth <= MAX_EVALUATION_DEPTH => {
1882
+ let kind = if value & self.fixnum_flag != 0 {
1883
+ TYPE_INTEGRAL_NUMBER
1884
+ } else {
1885
+ unsafe { sn_kind(value) }
1886
+ };
1887
+ if kind == TYPE_UNSUPPORTED {
1888
+ Unsupported
1889
+ } else {
1890
+ self.leaf(op, kind, value, depth)
1891
+ }
1892
+ }
1893
+ _ => self.eval(node, value, depth, active),
1894
+ }
1895
+ }
1896
+ }
1897
+ struct RecordContext<'a, 'b> {
1898
+ engine: &'a Execution<'a>,
1899
+ rules: &'a Object,
1900
+ record: &'a RecordObject,
1901
+ indexed: bool,
1902
+ depth: usize,
1903
+ active: scope::Context<'b>,
1904
+ visited: u64,
1905
+ probe: Option<Value>,
1906
+ status: Verdict,
1907
+ }
1908
+ fn indexed_record_field(record: &RecordObject, key: Value) -> Option<&RecordField> {
1909
+ let index = record.short_index.as_ref()?;
1910
+ let short = short_record_key(key);
1911
+ if short.tag == 0 {
1912
+ return None;
1913
+ }
1914
+ index.slots[index.slot(short)]
1915
+ .checked_sub(1)
1916
+ .and_then(|at| record.fields.get(at as usize))
1917
+ .filter(|field| {
1918
+ field.short_key.tag & 15 == short.tag & 15
1919
+ && field.short_key.word == short.word
1920
+ && (field.short_key.tag == short.tag
1921
+ || unsafe { sn_string_equal(field.key, key) } != 0)
1922
+ })
1923
+ }
1924
+ fn record_entry(c: &mut RecordContext<'_, '_>, key: Value, value: Value) -> Verdict {
1925
+ let field = if c.indexed {
1926
+ indexed_record_field(c.record, key)
1927
+ } else {
1928
+ c.record
1929
+ .fields
1930
+ .iter()
1931
+ .find(|field| field.key == key)
1932
+ .or_else(|| {
1933
+ if c.record.short_index.is_some() {
1934
+ return indexed_record_field(c.record, key);
1935
+ }
1936
+ let short = (c.record.fields.len() >= 4).then(|| short_record_key(key));
1937
+ c.record.fields.iter().find(|field| {
1938
+ if let Some(short) = short.filter(|short| short.tag != 0) {
1939
+ field.short_key.tag & 15 == short.tag & 15
1940
+ && field.short_key.word == short.word
1941
+ && (field.short_key.tag == short.tag
1942
+ || unsafe { sn_string_equal(field.key, key) } != 0)
1943
+ } else {
1944
+ (unsafe { sn_string_equal(field.key, key) }) != 0
1945
+ }
1946
+ })
1947
+ })
1948
+ };
1949
+ let child = if let Some(field) = field {
1950
+ if c.visited & field.bit != 0 {
1951
+ return Unsupported;
1952
+ }
1953
+ c.visited |= field.bit;
1954
+ if c.probe.is_none() && field.name != key {
1955
+ c.probe = Some(field.name);
1956
+ }
1957
+ field.child.or(c.rules.additional)
1958
+ } else {
1959
+ c.rules.additional
1960
+ };
1961
+ if c.status == Valid
1962
+ && let Some(child) = child
1963
+ {
1964
+ c.status = if c.depth <= MAX_EVALUATION_DEPTH {
1965
+ match field.and_then(|field| field.check) {
1966
+ Some(RecordCheck::IntegerBounds(min, max)) if value & c.engine.fixnum_flag != 0 => {
1967
+ let n = immediate_integer(value);
1968
+ verdict(n >= min && n <= max)
1969
+ }
1970
+ Some(RecordCheck::StringLength(required, min, max)) => {
1971
+ length_guarded(value, required, min, max)
1972
+ }
1973
+ _ => c.engine.record_value(child, value, c.depth, c.active),
1974
+ }
1975
+ } else {
1976
+ c.engine.record_value(child, value, c.depth, c.active)
1977
+ };
1978
+ }
1979
+ // Keep recording matches after failure: duplicate equal keys indicate
1980
+ // identity semantics, so their speculative child results cannot be used.
1981
+ Valid
1982
+ }
1983
+ struct RefFrame<'a> {
1984
+ source: usize,
1985
+ value: Value,
1986
+ parent: Option<&'a RefFrame<'a>>,
1987
+ }
1988
+ impl RefFrame<'_> {
1989
+ fn contains(mut active: Option<&Self>, source: usize, value: Value) -> bool {
1990
+ while let Some(frame) = active {
1991
+ if frame.source == source && frame.value == value {
1992
+ return true;
1993
+ }
1994
+ active = frame.parent;
1995
+ }
1996
+ false
1997
+ }
1998
+ }
1999
+ fn missing(hash: Value, key: Value, undefined: Value) -> bool {
2000
+ unsafe { sn_lookup(hash, key) == undefined }
2001
+ }
2002
+ fn verdict(valid: bool) -> Verdict {
2003
+ if valid { Valid } else { Invalid }
2004
+ }
2005
+ fn bounds(len: usize, min: Option<usize>, max: Option<usize>) -> Verdict {
2006
+ verdict(!min.is_some_and(|m| len < m) && !max.is_some_and(|m| len > m))
2007
+ }
2008
+ fn length_guarded(value: Value, required: i32, min: Option<usize>, max: Option<usize>) -> Verdict {
2009
+ let mut len = 0;
2010
+ match unsafe { sn_string_length_guarded(value, &mut len) } {
2011
+ TYPE_UNSUPPORTED => Unsupported,
2012
+ TYPE_STRING => bounds(len, min, max),
2013
+ _ => verdict(required == 0),
2014
+ }
2015
+ }
2016
+ // Call only after value & the header-validated fixnum_flag is nonzero.
2017
+ // Rust's signed shift is arithmetic; other encodings retain the C adapters.
2018
+ fn immediate_integer(value: Value) -> i64 {
2019
+ (value as isize >> 1) as i64
2020
+ }
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
+ }
2034
+ if depth > MAX_EVALUATION_DEPTH {
2035
+ return Unsupported;
2036
+ }
2037
+ let (ka, kb) = unsafe { (sn_kind(a), sn_kind(b)) };
2038
+ if ka == TYPE_UNSUPPORTED || kb == TYPE_UNSUPPORTED {
2039
+ return Unsupported;
2040
+ }
2041
+ if ka & TYPE_NUMBER != 0 && kb & TYPE_NUMBER != 0 {
2042
+ let (ia, ib) = unsafe { (sn_fixnum(a) != 0, sn_fixnum(b) != 0) };
2043
+ let (fa, fb) = unsafe { (sn_is_float(a) != 0, sn_is_float(b) != 0) };
2044
+ if ia && ib {
2045
+ return verdict(unsafe { sn_integer(a) == sn_integer(b) });
2046
+ }
2047
+ if fa && fb {
2048
+ return verdict(unsafe { sn_float(a) == sn_float(b) });
2049
+ }
2050
+ if (ia && fb) || (fa && ib) {
2051
+ let (n, f) = unsafe {
2052
+ if ia {
2053
+ (sn_integer(a), sn_float(b))
2054
+ } else {
2055
+ (sn_integer(b), sn_float(a))
2056
+ }
2057
+ };
2058
+ // Convert the float to an integer, never the integer to a rounded float.
2059
+ // Fixnums are strictly inside i64's bounds on supported CRuby targets.
2060
+ return verdict(
2061
+ f.is_finite()
2062
+ && f.trunc() == f
2063
+ && f >= i64::MIN as f64
2064
+ && f < i64::MAX as f64
2065
+ && f as i64 == n,
2066
+ );
2067
+ }
2068
+ if !fa && !fb {
2069
+ return verdict(
2070
+ unsafe {
2071
+ if ia {
2072
+ sn_big_equal(b, a)
2073
+ } else {
2074
+ sn_big_equal(a, b)
2075
+ }
2076
+ } != 0,
2077
+ );
2078
+ }
2079
+ return if fa {
2080
+ number::integer_float_equal(b, unsafe { sn_float(a) })
2081
+ } else {
2082
+ number::integer_float_equal(a, unsafe { sn_float(b) })
2083
+ };
2084
+ }
2085
+ // JSON container equality visits every value, even for the same object:
2086
+ // nested NaN values are unequal and unsupported values need VM evaluation.
2087
+ if ka != TYPE_ARRAY && ka != TYPE_OBJECT && unsafe { sn_same(a, b) } != 0 {
2088
+ return Valid;
2089
+ }
2090
+ if ka != kb {
2091
+ return Invalid;
2092
+ }
2093
+ if unsafe { sn_same_class(a, b) } == 0 {
2094
+ return Invalid;
2095
+ }
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 {
2105
+ TYPE_STRING => verdict(unsafe { sn_string_equal(a, b) } != 0),
2106
+ TYPE_ARRAY => {
2107
+ let len = unsafe { sn_len(a) };
2108
+ if len != unsafe { sn_len(b) } {
2109
+ return Invalid;
2110
+ }
2111
+ for i in 0..len {
2112
+ let result = equal_cached(
2113
+ unsafe { sn_entry(a, i) },
2114
+ unsafe { sn_entry(b, i) },
2115
+ depth + 1,
2116
+ memo,
2117
+ budget,
2118
+ );
2119
+ if result != Valid {
2120
+ return result;
2121
+ }
2122
+ }
2123
+ Valid
2124
+ }
2125
+ TYPE_OBJECT => {
2126
+ if unsafe { sn_len(a) != sn_len(b) } {
2127
+ return Invalid;
2128
+ }
2129
+ let mut context = EqualityContext {
2130
+ other: b,
2131
+ depth: depth + 1,
2132
+ memo,
2133
+ budget,
2134
+ };
2135
+ decode(unsafe {
2136
+ sn_hash_each(
2137
+ a,
2138
+ (&mut context as *mut EqualityContext<'_>).cast(),
2139
+ equal_entry,
2140
+ )
2141
+ })
2142
+ }
2143
+ TYPE_UNSUPPORTED => Unsupported,
2144
+ _ => Invalid,
2145
+ };
2146
+ if container && memo.eligible(depth) {
2147
+ memo.record(key, result);
2148
+ }
2149
+ result
2150
+ }
2151
+ fn decode(result: i32) -> Verdict {
2152
+ match result {
2153
+ VERDICT_INVALID => Invalid,
2154
+ VERDICT_VALID => Valid,
2155
+ _ => Unsupported,
2156
+ }
2157
+ }
2158
+ struct EqualityContext<'a> {
2159
+ other: Value,
2160
+ depth: usize,
2161
+ memo: &'a mut structural::Cache<(Value, Value, usize), Verdict>,
2162
+ budget: &'a budget::Budget,
2163
+ }
2164
+ unsafe extern "C" fn equal_entry(context: *mut c_void, key: Value, value: Value) -> i32 {
2165
+ catch_unwind(AssertUnwindSafe(|| {
2166
+ let c = unsafe { &mut *context.cast::<EqualityContext<'_>>() };
2167
+ if unsafe { sn_kind(key) } != TYPE_STRING {
2168
+ return Unsupported as i32;
2169
+ }
2170
+ if !c.budget.checkpoint() {
2171
+ return Unsupported as i32;
2172
+ }
2173
+ let other = unsafe { sn_lookup(c.other, key) };
2174
+ if unsafe { sn_missing(other) } != 0 {
2175
+ Invalid as i32
2176
+ } else {
2177
+ equal_cached(value, other, c.depth, c.memo, c.budget) as i32
2178
+ }
2179
+ }))
2180
+ .unwrap_or(Unsupported as i32)
2181
+ }
2182
+ fn mix(n: u64) -> u64 {
2183
+ let n = (n ^ (n >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
2184
+ let n = (n ^ (n >> 27)).wrapping_mul(0x94d049bb133111eb);
2185
+ n ^ (n >> 31)
2186
+ }
2187
+ fn string_fingerprint(value: Value) -> u64 {
2188
+ bytes_fingerprint(unsafe { string_bytes(value) })
2189
+ }
2190
+ #[derive(Clone, Copy, Debug)]
2191
+ struct ShortRecordKey {
2192
+ word: u64,
2193
+ // Low four bits encode length + 1; zero means a longer key. The remaining
2194
+ // bits hold the public CRuby encoding index, not a borrowed pointer.
2195
+ tag: u64,
2196
+ }
2197
+ fn short_record_key(value: Value) -> ShortRecordKey {
2198
+ let mut data = std::ptr::null();
2199
+ let mut len = 0;
2200
+ let encoding = unsafe { sn_record_key_view(value, &mut data, &mut len) };
2201
+ if len > 8 {
2202
+ return ShortRecordKey { word: 0, tag: 0 };
2203
+ }
2204
+ let bytes = unsafe { byte_view(data, len) };
2205
+ ShortRecordKey {
2206
+ word: short_word(bytes),
2207
+ tag: ((encoding as u64) << 4) | (len as u64 + 1),
2208
+ }
2209
+ }
2210
+ // Borrow an already classified String under the GVL. The caller must not
2211
+ // allocate Ruby objects or run Ruby callbacks while the returned view is live.
2212
+ unsafe fn string_bytes<'a>(value: Value) -> &'a [u8] {
2213
+ let mut data = std::ptr::null();
2214
+ let mut len = 0;
2215
+ unsafe { sn_string_raw_view(value, &mut data, &mut len) };
2216
+ unsafe { byte_view(data, len) }
2217
+ }
2218
+ // The caller must keep the original Ruby string alive and unchanged under
2219
+ // the GVL for the entire lifetime of the view, without allocating in Ruby.
2220
+ unsafe fn byte_view<'a>(data: *const u8, len: usize) -> &'a [u8] {
2221
+ if len == 0 {
2222
+ &[]
2223
+ } else {
2224
+ unsafe { std::slice::from_raw_parts(data, len) }
2225
+ }
2226
+ }
2227
+ // For at most eight bytes, the first/last scalar words cover every byte.
2228
+ fn short_word(bytes: &[u8]) -> u64 {
2229
+ let len = bytes.len();
2230
+ match bytes.len() {
2231
+ 0 => 0,
2232
+ 1 => bytes[0] as u64,
2233
+ 2 => bytes[0] as u64 | (bytes[1] as u64) << 8,
2234
+ 3 => bytes[0] as u64 | (bytes[1] as u64) << 8 | (bytes[2] as u64) << 16,
2235
+ _ => {
2236
+ let first = u32::from_le_bytes(bytes[..4].try_into().unwrap()) as u64;
2237
+ let last = u32::from_le_bytes(bytes[len - 4..].try_into().unwrap()) as u64;
2238
+ first | last << 32
2239
+ }
2240
+ }
2241
+ }
2242
+ fn bytes_fingerprint(bytes: &[u8]) -> u64 {
2243
+ // Short keys and values fit in one scalar. Overlapping first/last words
2244
+ // cover every byte without a byte-at-a-time multiplication chain.
2245
+ let len = bytes.len();
2246
+ if len <= 8 {
2247
+ let word = short_word(bytes);
2248
+ return mix(word ^ (len as u64).wrapping_mul(0x100000001b3) ^ TYPE_STRING as u64);
2249
+ }
2250
+ let mut result = 0xcbf29ce484222325_u64;
2251
+ for &byte in bytes {
2252
+ result = (result ^ byte as u64).wrapping_mul(0x100000001b3);
2253
+ }
2254
+ mix(result ^ TYPE_STRING as u64)
2255
+ }
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
+ }
2283
+ if depth > MAX_EVALUATION_DEPTH {
2284
+ return None;
2285
+ }
2286
+ if value & fixnum_flag != 0 {
2287
+ // sn_fixnum_flag enables this hint only after the C headers confirm
2288
+ // the signed immediate encoding. Rust's signed shift is arithmetic.
2289
+ let integer = immediate_integer(value);
2290
+ return Some(mix(integer as u64 ^ TYPE_NUMBER as u64));
2291
+ }
2292
+ if big_hint && let Some(hash) = number::big_fingerprint(value) {
2293
+ return Some(hash);
2294
+ }
2295
+ // The C key-view iterator validates every key without Ruby lookup.
2296
+ // Avoid a separate preliminary walk of the same keys.
2297
+ let mut data = std::mem::MaybeUninit::<*const c_void>::uninit();
2298
+ let mut len = std::mem::MaybeUninit::<usize>::uninit();
2299
+ let mut n = std::mem::MaybeUninit::<f64>::uninit();
2300
+ // Read only the fields the C adapter initialized for the returned kind.
2301
+ let kind =
2302
+ unsafe { sn_fingerprint_view(value, data.as_mut_ptr(), len.as_mut_ptr(), n.as_mut_ptr()) };
2303
+ if kind == TYPE_NUMBER {
2304
+ let n = unsafe { n.assume_init() };
2305
+ // Normalize integral floats without rounding distinct integers. Exact
2306
+ // JSON equality still resolves every collision before declaring duplicates.
2307
+ if n.is_finite() && n.trunc() == n && n >= i64::MIN as f64 && n < i64::MAX as f64 {
2308
+ return Some(mix(n as i64 as u64 ^ TYPE_NUMBER as u64));
2309
+ }
2310
+ return Some(mix(n.to_bits() ^ TYPE_NUMBER as u64));
2311
+ }
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 {
2324
+ TYPE_INTEGRAL_NUMBER => Some(number::integer_fingerprint(value)),
2325
+ TYPE_NULL => Some(mix(TYPE_NULL as u64)),
2326
+ TYPE_BOOLEAN => Some(mix(if unsafe { len.assume_init() } != 0 {
2327
+ 2
2328
+ } else {
2329
+ 3
2330
+ })),
2331
+ TYPE_STRING => Some(bytes_fingerprint(unsafe {
2332
+ byte_view(data.assume_init().cast(), len.assume_init())
2333
+ })),
2334
+ TYPE_ARRAY => {
2335
+ let mut hash = mix(TYPE_ARRAY as u64);
2336
+ let len = unsafe { len.assume_init() };
2337
+ let values = if len == 0 {
2338
+ &[]
2339
+ } else {
2340
+ unsafe { std::slice::from_raw_parts(data.assume_init().cast::<Value>(), len) }
2341
+ };
2342
+ // All descendants are read under the GVL without allocating Ruby
2343
+ // objects or invoking Ruby callbacks, so the borrowed view is stable.
2344
+ for &item in values {
2345
+ hash =
2346
+ mix(hash
2347
+ ^ fingerprint_cached(item, depth + 1, fixnum_flag, false, memo, budget)?);
2348
+ }
2349
+ Some(hash)
2350
+ }
2351
+ TYPE_OBJECT => {
2352
+ let len = unsafe { len.assume_init() };
2353
+ let mut c = FingerprintContext {
2354
+ hash: 0,
2355
+ depth: depth + 1,
2356
+ fixnum_flag,
2357
+ memo,
2358
+ budget,
2359
+ };
2360
+ let status = unsafe {
2361
+ sn_hash_each_key_view(
2362
+ value,
2363
+ (&mut c as *mut FingerprintContext<'_>).cast(),
2364
+ fingerprint_entry,
2365
+ )
2366
+ };
2367
+ (status == VERDICT_VALID).then(|| mix(c.hash ^ len as u64 ^ TYPE_OBJECT as u64))
2368
+ }
2369
+ _ => None,
2370
+ };
2371
+ if cacheable {
2372
+ memo.record(key, result);
2373
+ }
2374
+ result
2375
+ }
2376
+ struct FingerprintContext<'a> {
2377
+ hash: u64,
2378
+ depth: usize,
2379
+ fixnum_flag: Value,
2380
+ memo: &'a mut structural::Cache<(Value, usize), Option<u64>>,
2381
+ budget: &'a budget::Budget,
2382
+ }
2383
+ unsafe extern "C" fn fingerprint_entry(
2384
+ context: *mut c_void,
2385
+ data: *const u8,
2386
+ len: usize,
2387
+ value: Value,
2388
+ ) -> i32 {
2389
+ catch_unwind(AssertUnwindSafe(|| {
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 {
2396
+ return Unsupported as i32;
2397
+ };
2398
+ c.hash ^= mix(bytes_fingerprint(unsafe { byte_view(data, len) }) ^ hash.rotate_left(17));
2399
+ Valid as i32
2400
+ }))
2401
+ .unwrap_or(Unsupported as i32)
2402
+ }
2403
+ // Fingerprints are already mixed uniformly. Rehashing each u64 with SipHash
2404
+ // adds work without changing collision resolution, which always uses JSON equality.
2405
+ #[derive(Default)]
2406
+ struct BucketHasher(u64);
2407
+ impl Hasher for BucketHasher {
2408
+ fn finish(&self) -> u64 {
2409
+ self.0
2410
+ }
2411
+ fn write_u64(&mut self, value: u64) {
2412
+ self.0 = value;
2413
+ }
2414
+ fn write(&mut self, bytes: &[u8]) {
2415
+ for &byte in bytes {
2416
+ self.0 = mix(self.0 ^ byte as u64);
2417
+ }
2418
+ }
2419
+ }
2420
+ fn unique(value: Value, depth: usize, fixnum_flag: Value, budget: &budget::Budget) -> Verdict {
2421
+ let len = unsafe { sn_len(value) };
2422
+ if !budget.checkpoint() {
2423
+ return Unsupported;
2424
+ }
2425
+ if len <= 16 {
2426
+ for i in 0..len {
2427
+ for previous in 0..i {
2428
+ match equal_budgeted(
2429
+ unsafe { sn_entry(value, previous) },
2430
+ unsafe { sn_entry(value, i) },
2431
+ depth,
2432
+ budget,
2433
+ ) {
2434
+ Valid => return Invalid,
2435
+ Unsupported => return Unsupported,
2436
+ Invalid => {}
2437
+ }
2438
+ }
2439
+ }
2440
+ return Valid;
2441
+ }
2442
+ let values = unsafe { std::slice::from_raw_parts(sn_array_data(value), len) };
2443
+ // This is a checked optimization hint, not an instance shape assumption.
2444
+ let big_hint = unsafe { sn_is_bignum(values[0]) } != 0;
2445
+ if big_hint
2446
+ && !budget.timed()
2447
+ && depth <= MAX_EVALUATION_DEPTH
2448
+ && let Some(result) = number::ordered_big_unique(values)
2449
+ {
2450
+ return result;
2451
+ }
2452
+ if big_hint && depth <= MAX_EVALUATION_DEPTH {
2453
+ let result = unordered_big_unique(values, budget);
2454
+ if result != Unsupported {
2455
+ return result;
2456
+ }
2457
+ }
2458
+ let mut fingerprints = structural::Cache::batch(depth);
2459
+ indexed_unique(
2460
+ values,
2461
+ |item| fingerprint_cached(item, depth, fixnum_flag, big_hint, &mut fingerprints, budget),
2462
+ |a, b| equal_budgeted(a, b, depth, budget),
2463
+ )
2464
+ }
2465
+ // Keep Big-only sampling and retry state out of the common scalar path.
2466
+ #[inline(never)]
2467
+ fn unordered_big_unique(values: &[Value], budget: &budget::Budget) -> Verdict {
2468
+ let mut all_big = true;
2469
+ let result = indexed_unique_short(
2470
+ values,
2471
+ |item| {
2472
+ if !budget.checkpoint() {
2473
+ return None;
2474
+ }
2475
+ let hash = number::big_low_fingerprint(item);
2476
+ if hash.is_none() {
2477
+ all_big = false;
2478
+ }
2479
+ hash
2480
+ },
2481
+ |a, b| {
2482
+ if !budget.checkpoint() {
2483
+ return Unsupported;
2484
+ }
2485
+ // Distinct integers sharing the short sample need the wider pass.
2486
+ if unsafe { sn_big_equal(a, b) } != 0 {
2487
+ Valid
2488
+ } else {
2489
+ Unsupported
2490
+ }
2491
+ },
2492
+ );
2493
+ if result != Unsupported {
2494
+ return result;
2495
+ }
2496
+ if all_big {
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
+ );
2513
+ if result != Unsupported {
2514
+ return result;
2515
+ }
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
+ );
2532
+ if result != Unsupported {
2533
+ return result;
2534
+ }
2535
+ }
2536
+ // A non-Bignum invalidates both Big-only hash conventions. Restart
2537
+ // from the original array so equal Integer/Float pairs share a bucket.
2538
+ Unsupported
2539
+ }
2540
+ fn indexed_unique(
2541
+ values: &[Value],
2542
+ fingerprint: impl FnMut(Value) -> Option<u64>,
2543
+ equal: impl FnMut(Value, Value) -> Verdict,
2544
+ ) -> Verdict {
2545
+ indexed_unique_impl::<false>(values, fingerprint, equal)
2546
+ }
2547
+ fn indexed_unique_short(
2548
+ values: &[Value],
2549
+ fingerprint: impl FnMut(Value) -> Option<u64>,
2550
+ equal: impl FnMut(Value, Value) -> Verdict,
2551
+ ) -> Verdict {
2552
+ // Index zero means empty. Choose a larger slot beyond the 16-bit limit
2553
+ // so the short sample remains available without truncating an index.
2554
+ if values.len() > u32::MAX as usize {
2555
+ return Unsupported;
2556
+ }
2557
+ let Some(capacity) = values
2558
+ .len()
2559
+ .checked_mul(2)
2560
+ .and_then(usize::checked_next_power_of_two)
2561
+ else {
2562
+ return Unsupported;
2563
+ };
2564
+ if values.len() <= u16::MAX as usize {
2565
+ indexed_unique_slots::<ShortUniqueSlot, true>(values, capacity, fingerprint, equal)
2566
+ } else {
2567
+ indexed_unique_slots::<ShortWideUniqueSlot, true>(values, capacity, fingerprint, equal)
2568
+ }
2569
+ }
2570
+ fn indexed_unique_impl<const BOUNDED: bool>(
2571
+ values: &[Value],
2572
+ fingerprint: impl FnMut(Value) -> Option<u64>,
2573
+ equal: impl FnMut(Value, Value) -> Verdict,
2574
+ ) -> Verdict {
2575
+ let Some(capacity) = values
2576
+ .len()
2577
+ .checked_mul(2)
2578
+ .and_then(usize::checked_next_power_of_two)
2579
+ else {
2580
+ return Unsupported;
2581
+ };
2582
+ if values.len() <= u16::MAX as usize {
2583
+ indexed_unique_slots::<PackedUniqueSlot, BOUNDED>(values, capacity, fingerprint, equal)
2584
+ } else {
2585
+ indexed_unique_slots::<FullUniqueSlot, BOUNDED>(values, capacity, fingerprint, equal)
2586
+ }
2587
+ }
2588
+ trait UniqueSlot: Copy {
2589
+ const EMPTY: Self;
2590
+ fn new(hash: u64, index: usize) -> Self;
2591
+ fn index(self) -> Option<usize>;
2592
+ fn matches(self, hash: u64) -> bool;
2593
+ }
2594
+ #[derive(Clone, Copy)]
2595
+ struct ShortUniqueSlot(u32);
2596
+ impl UniqueSlot for ShortUniqueSlot {
2597
+ const EMPTY: Self = Self(0);
2598
+ fn new(hash: u64, index: usize) -> Self {
2599
+ debug_assert!(index < u16::MAX as usize);
2600
+ Self((hash as u32 & !0xffff) | (index + 1) as u32)
2601
+ }
2602
+ fn index(self) -> Option<usize> {
2603
+ let index = self.0 & 0xffff;
2604
+ (index != 0).then(|| index as usize - 1)
2605
+ }
2606
+ fn matches(self, hash: u64) -> bool {
2607
+ // Truncated filters only select an exact comparison or wider retry.
2608
+ self.0 & !0xffff == hash as u32 & !0xffff
2609
+ }
2610
+ }
2611
+ #[derive(Clone, Copy)]
2612
+ struct ShortWideUniqueSlot(u64);
2613
+ impl UniqueSlot for ShortWideUniqueSlot {
2614
+ const EMPTY: Self = Self(0);
2615
+ fn new(hash: u64, index: usize) -> Self {
2616
+ debug_assert!(index < u32::MAX as usize);
2617
+ Self((u64::from(hash as u32) << 32) | (index + 1) as u64)
2618
+ }
2619
+ fn index(self) -> Option<usize> {
2620
+ let index = self.0 as u32;
2621
+ (index != 0).then(|| index as usize - 1)
2622
+ }
2623
+ fn matches(self, hash: u64) -> bool {
2624
+ self.0 >> 32 == u64::from(hash as u32)
2625
+ }
2626
+ }
2627
+ #[derive(Clone, Copy)]
2628
+ struct PackedUniqueSlot(u64);
2629
+ impl UniqueSlot for PackedUniqueSlot {
2630
+ const EMPTY: Self = Self(0);
2631
+ fn new(hash: u64, index: usize) -> Self {
2632
+ // Selected only for at most 65535 entries. Zero is the empty marker;
2633
+ // the remaining 48 hash bits only filter exact JSON comparisons.
2634
+ debug_assert!(index < u16::MAX as usize);
2635
+ Self((hash & !0xffff) | (index + 1) as u64)
2636
+ }
2637
+ fn index(self) -> Option<usize> {
2638
+ let index = self.0 & 0xffff;
2639
+ (index != 0).then(|| index as usize - 1)
2640
+ }
2641
+ fn matches(self, hash: u64) -> bool {
2642
+ self.0 & !0xffff == hash & !0xffff
2643
+ }
2644
+ }
2645
+ #[derive(Clone, Copy)]
2646
+ struct FullUniqueSlot(u64, usize);
2647
+ impl UniqueSlot for FullUniqueSlot {
2648
+ const EMPTY: Self = Self(0, usize::MAX);
2649
+ fn new(hash: u64, index: usize) -> Self {
2650
+ Self(hash, index)
2651
+ }
2652
+ fn index(self) -> Option<usize> {
2653
+ (self.1 != usize::MAX).then_some(self.1)
2654
+ }
2655
+ fn matches(self, hash: u64) -> bool {
2656
+ self.0 == hash
2657
+ }
2658
+ }
2659
+ fn indexed_unique_slots<S: UniqueSlot, const BOUNDED: bool>(
2660
+ values: &[Value],
2661
+ capacity: usize,
2662
+ mut fingerprint: impl FnMut(Value) -> Option<u64>,
2663
+ mut equal: impl FnMut(Value, Value) -> Verdict,
2664
+ ) -> Verdict {
2665
+ // Keep at least half the slots empty, so every probe terminates. Slots
2666
+ // retain only a fingerprint and an index into the original Ruby array.
2667
+ // Equal fingerprints continue probing after exact inequality; neither
2668
+ // table-position collisions nor fingerprint collisions imply duplicates.
2669
+ let Some(mut slots) = allocate_unique_slots::<S>(capacity) else {
2670
+ return Unsupported;
2671
+ };
2672
+ let mask = capacity - 1;
2673
+ let mut collisions = 0;
2674
+ let mut probes = 0;
2675
+ for (i, &item) in values.iter().enumerate() {
2676
+ let Some(hash) = fingerprint(item) else {
2677
+ return Unsupported;
2678
+ };
2679
+ let mut at = hash as usize & mask;
2680
+ loop {
2681
+ let slot = slots[at];
2682
+ let Some(previous) = slot.index() else {
2683
+ slots[at] = S::new(hash, i);
2684
+ break;
2685
+ };
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 {
2691
+ return Unsupported;
2692
+ }
2693
+ // Release the hash table before allocating the flat index.
2694
+ drop(slots);
2695
+ return sorted_unique(values, fingerprint, equal);
2696
+ }
2697
+ if slot.matches(hash) {
2698
+ match equal(values[previous], item) {
2699
+ Valid => return Invalid,
2700
+ Unsupported => return Unsupported,
2701
+ Invalid => {
2702
+ if BOUNDED {
2703
+ collisions += 1;
2704
+ if collisions >= 16 {
2705
+ return Unsupported;
2706
+ }
2707
+ }
2708
+ }
2709
+ }
2710
+ }
2711
+ at = (at + 1) & mask;
2712
+ }
2713
+ }
2714
+ Valid
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
+ }
2766
+ fn allocate_unique_slots<S: UniqueSlot>(capacity: usize) -> Option<Vec<S>> {
2767
+ let bytes = capacity.checked_mul(std::mem::size_of::<S>())?;
2768
+ if bytes > MAX_UNIQUE_INDEX_BYTES {
2769
+ return None;
2770
+ }
2771
+ let mut slots = Vec::new();
2772
+ slots.try_reserve_exact(capacity).ok()?;
2773
+ slots.resize(capacity, S::EMPTY);
2774
+ Some(slots)
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
+
2798
+ struct ObjectContext<'a> {
2799
+ engine: &'a Execution<'a>,
2800
+ rules: &'a Object,
2801
+ depth: usize,
2802
+ active: scope::Context<'a>,
2803
+ }
2804
+ /// Called synchronously by the C hash iterator. This never calls allocating Ruby APIs.
2805
+ /// # Safety
2806
+ /// The context must be a live ObjectContext from this engine and all VALUEs must be live under the GVL.
2807
+ #[unsafe(no_mangle)]
2808
+ pub unsafe extern "C" fn sn_object_entry(context: *const c_void, key: Value, value: Value) -> i32 {
2809
+ catch_unwind(AssertUnwindSafe(|| {
2810
+ let c = unsafe { &*context.cast::<ObjectContext>() };
2811
+ if !c.engine.budget.checkpoint() {
2812
+ return Unsupported as i32;
2813
+ }
2814
+ if unsafe { sn_kind(key) } != TYPE_STRING {
2815
+ return Unsupported as i32;
2816
+ }
2817
+ if let Some(child) = c.rules.names {
2818
+ let result = c.engine.eval(child, key, c.depth, c.active);
2819
+ if result != Valid {
2820
+ return result as i32;
2821
+ }
2822
+ }
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,
2835
+ }
2836
+ }))
2837
+ .unwrap_or(Unsupported as i32)
2838
+ }
2839
+ #[unsafe(no_mangle)]
2840
+ pub extern "C" fn sn_new() -> *mut Engine {
2841
+ Box::into_raw(Box::new(Engine {
2842
+ roots: Box::into_raw(Box::new(Roots::default())),
2843
+ fixnum_flag: unsafe { sn_fixnum_flag() },
2844
+ undefined: unsafe { sn_undefined() },
2845
+ ..Engine::default()
2846
+ }))
2847
+ }
2848
+ /// # Safety
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.
2851
+ #[unsafe(no_mangle)]
2852
+ pub unsafe extern "C" fn sn_compile(
2853
+ engine: *mut Engine,
2854
+ program: Value,
2855
+ compiler: Value,
2856
+ format: i32,
2857
+ content: i32,
2858
+ multiple: i32,
2859
+ error: *mut Value,
2860
+ ) -> i32 {
2861
+ match catch_unwind(AssertUnwindSafe(|| {
2862
+ let e = unsafe { &mut *engine };
2863
+ e.validate_format = format != 0;
2864
+ e.validate_content = content != 0;
2865
+ e.compiler = e.retain(compiler);
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
+ }
2873
+ e.bind_dynamic_anchors()?;
2874
+ e.simplify_dynamic_references();
2875
+ e.compile_memoization();
2876
+ Ok::<_, Value>(())
2877
+ })) {
2878
+ Ok(Ok(())) => VERDICT_VALID,
2879
+ Ok(Err(value)) => {
2880
+ unsafe { *error = value };
2881
+ STATUS_EXCEPTION
2882
+ }
2883
+ Err(_) => STATUS_PANIC,
2884
+ }
2885
+ }
2886
+ /// # Safety
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.
2917
+ #[unsafe(no_mangle)]
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 {
2924
+ catch_unwind(AssertUnwindSafe(|| {
2925
+ let e = unsafe { &*engine };
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
2935
+ }))
2936
+ .unwrap_or(STATUS_PANIC)
2937
+ }
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
2977
+ /// Engine must be an owned pointer returned by sn_new, freed exactly once.
2978
+ #[unsafe(no_mangle)]
2979
+ pub unsafe extern "C" fn sn_free(engine: *mut Engine) {
2980
+ let engine = unsafe { Box::from_raw(engine) };
2981
+ drop(unsafe { Box::from_raw(engine.roots) });
2982
+ drop(engine);
2983
+ }
2984
+ /// # Safety
2985
+ /// Engine must be live and not exclusively borrowed by the compiler.
2986
+ #[unsafe(no_mangle)]
2987
+ pub unsafe extern "C" fn sn_roots(engine: *const Engine) -> *const Roots {
2988
+ unsafe { &*engine }.roots
2989
+ }
2990
+ /// # Safety
2991
+ /// Roots must be live; call synchronously from the owning Ruby GC mark callback.
2992
+ /// No mutable root borrow can be live across a Ruby allocation/callback.
2993
+ #[unsafe(no_mangle)]
2994
+ pub unsafe extern "C" fn sn_roots_mark(roots: *const Roots) {
2995
+ for &value in unsafe { &*roots }
2996
+ .values
2997
+ .iter()
2998
+ .chain(&unsafe { &*roots }.binding_names)
2999
+ {
3000
+ unsafe { sn_pin(value) };
3001
+ }
3002
+ }
3003
+ /// # Safety
3004
+ /// Roots must be live with no mutable root borrow active.
3005
+ #[unsafe(no_mangle)]
3006
+ pub unsafe extern "C" fn sn_roots_size(roots: *const Roots) -> usize {
3007
+ std::mem::size_of::<Roots>()
3008
+ + (unsafe { &*roots }.values.capacity() + unsafe { &*roots }.binding_names.capacity())
3009
+ * std::mem::size_of::<Value>()
3010
+ }
3011
+ #[unsafe(no_mangle)]
3012
+ pub extern "C" fn sn_engine_header_size() -> usize {
3013
+ std::mem::size_of::<Engine>()
3014
+ }
3015
+ /// # Safety
3016
+ /// Engine must be live and not exclusively borrowed by the compiler.
3017
+ #[unsafe(no_mangle)]
3018
+ pub unsafe extern "C" fn sn_size(engine: *const Engine) -> usize {
3019
+ let nodes = unsafe { &*std::ptr::addr_of!((*engine).nodes) };
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) };
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>()
3037
+ + nodes.capacity() * std::mem::size_of::<Vec<Op>>()
3038
+ + operands.capacity() * std::mem::size_of::<Vec<(i32, Value)>>()
3039
+ + operands
3040
+ .iter()
3041
+ .map(|ops| ops.capacity() * std::mem::size_of::<(i32, Value)>())
3042
+ .sum::<usize>()
3043
+ + nodes
3044
+ .iter()
3045
+ .map(|n| n.capacity() * std::mem::size_of::<Op>())
3046
+ .sum::<usize>()
3047
+ + nodes
3048
+ .iter()
3049
+ .flatten()
3050
+ .map(|op| match op {
3051
+ Op::Enum(values) => values.memory_usage(),
3052
+ Op::String(_, rules) => {
3053
+ std::mem::size_of::<StringRules>()
3054
+ + rules
3055
+ .pattern
3056
+ .as_ref()
3057
+ .map_or(0, pattern::Pattern::memory_usage)
3058
+ }
3059
+ Op::Object(_, rules) => {
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)>()
3066
+ + rules
3067
+ .patterns
3068
+ .iter()
3069
+ .map(|(pattern, _)| pattern.memory_usage())
3070
+ .sum::<usize>()
3071
+ }
3072
+ _ => 0,
3073
+ })
3074
+ .sum::<usize>()
3075
+ }
3076
+
3077
+ #[cfg(test)]
3078
+ mod tests {
3079
+ use super::*;
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
+
3129
+ #[test]
3130
+ fn bounds_unique_index_allocation() {
3131
+ assert_eq!(
3132
+ allocate_unique_slots::<FullUniqueSlot>(16).unwrap().len(),
3133
+ 16
3134
+ );
3135
+ let over_limit = MAX_UNIQUE_INDEX_BYTES / std::mem::size_of::<FullUniqueSlot>() + 1;
3136
+ assert!(allocate_unique_slots::<FullUniqueSlot>(over_limit).is_none());
3137
+ assert!(allocate_unique_slots::<FullUniqueSlot>(usize::MAX).is_none());
3138
+ }
3139
+ }