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