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