makepad/libs/relax/compiler/std/check/tests/hash.rs
Admin b22a714840 makepad run: the Rust JIT (libs/relax) and makepad-remote
The JIT that runs makepad apps from source: lexer, front end, type checker,
lazy borrow checker (Gandalf; `--check-first` opts in), codegen, session and
warm/cold caches keyed by compiler stamp, provided crates bound late,
startup images, live patching. User-facing name is `makepad run` (binary
`makepad`, MAKEPAD_RUN_* env, ~/.makepad-run); libs/relax stays the internal
library name. `makepad check` reports rustc-style diagnostics with rustc
error codes and checks a whole workspace (`--workspace`). Runs on macOS,
Linux, Windows and Android. makepad-remote: one tool schema for apps and the
WM (app/1): grabs, captures, traces, frame profiles and tweaks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:26:21 +02:00

283 lines
11 KiB
Rust

use relax_std_check::collections::{HashMap, HashSet};
use std::collections::{HashMap as RealMap, HashSet as RealSet};
use std::hash::{BuildHasher, Hasher};
use std::{marker, mem, ptr, slice};
use relax_std_check::panicking;
mod heap {
pub use std::alloc::{dealloc, Layout};
pub fn alloc_or_abort(layout: Layout) -> *mut u8 {
let p = unsafe { std::alloc::alloc(layout) };
if p.is_null() { std::alloc::handle_alloc_error(layout); }
p
}
}
#[path = "../../core/collections/raw_table.rs"]
mod raw_table;
use raw_table::RawTable;
#[derive(Clone)]
struct Builder(u8);
struct Cheap { mode: u8, value: u64 }
impl BuildHasher for Builder {
type Hasher = Cheap;
fn build_hasher(&self) -> Cheap { Cheap { mode: self.0, value: 0 } }
}
impl Hasher for Cheap {
fn finish(&self) -> u64 {
match self.mode { 0 => self.value.wrapping_mul(0x9e3779b97f4a7c15), 1 => 0, 2 => (self.value & 1) << 57, 3 => self.value, _ => self.value.wrapping_mul(0x517cc1b727220a95) }
}
fn write(&mut self, bytes: &[u8]) {
for b in bytes { self.value = self.value.wrapping_mul(257).wrapping_add(*b as u64); }
}
fn write_u64(&mut self, value: u64) { self.value = value; }
}
fn random(seed: &mut u64) -> u64 {
*seed ^= *seed << 13;
*seed ^= *seed >> 7;
*seed ^= *seed << 17;
*seed
}
fn contents(m: &HashMap<u64, u64, Builder>, real: &RealMap<u64, u64, Builder>) {
assert_eq!(m.len(), real.len());
assert!(m.capacity() >= m.len());
let mut a: Vec<_> = m.iter().map(|(k,v)| (*k,*v)).collect();
let mut b: Vec<_> = real.iter().map(|(k,v)| (*k,*v)).collect();
a.sort_unstable(); b.sort_unstable(); assert_eq!(a,b);
}
#[test]
fn randomized_maps_and_sets_200k_each_hasher() {
for mode in 0..3 {
let mut m = HashMap::with_hasher(Builder(mode));
let mut real = RealMap::with_hasher(Builder(mode));
let mut set = HashSet::with_hasher(Builder(mode));
let mut real_set = RealSet::with_hasher(Builder(mode));
let mut seed = 0x4321fedc9876abcd;
for step in 0..200_000 {
let r = random(&mut seed);
let k = (r >> 8) % 512;
let v = r >> 32;
match r % 100 {
0..=39 => { assert_eq!(m.insert(k,v),real.insert(k,v)); }
40..=59 => { assert_eq!(m.remove(&k),real.remove(&k)); }
60..=79 => { assert_eq!(m.get(&k),real.get(&k)); }
80..=94 => {
*m.entry(k).or_insert(v) += 1;
*real.entry(k).or_insert(v) += 1;
}
95 => { m.reserve(32); real.reserve(32); }
96 => { m.shrink_to_fit(); real.shrink_to_fit(); }
97 => {
m.retain(|key,val| { *val ^= 7; key % 19 != k % 19 });
real.retain(|key,val| { *val ^= 7; key % 19 != k % 19 });
}
98 => { contents(&m.clone(), &real.clone()); }
_ => {
if step % 251 == 0 {
let mut a: Vec<_> = m.drain().collect();
let mut b: Vec<_> = real.drain().collect();
a.sort_unstable(); b.sort_unstable(); assert_eq!(a,b);
}
}
}
if r & 1 == 0 { assert_eq!(set.insert(k), real_set.insert(k)); }
else { assert_eq!(set.remove(&k), real_set.remove(&k)); }
assert_eq!(set.contains(&k),real_set.contains(&k));
assert!(m.capacity() >= m.len());
assert!(set.capacity() >= set.len());
if step % 257 == 0 { contents(&m,&real); }
}
contents(&m,&real);
let mut a: Vec<_> = set.iter().copied().collect();
let mut b: Vec<_> = real_set.iter().copied().collect();
a.sort_unstable(); b.sort_unstable(); assert_eq!(a,b);
}
}
#[test]
fn reserve_keeps_bucket_pointer_and_rebuilds_in_place() {
for cap in [1, 3, 4, 7, 8, 15, 28, 100, 1000] {
let mut t = RawTable::with_capacity(cap);
let hash = |v: &u64| *v;
t.insert(0, 0, &hash);
t.reserve(cap, &hash);
let allocation = t.ctrl().as_ptr();
let bucket = t.slot_ptr(t.find(0, |v| *v == 0).unwrap());
for i in 1..=cap as u64 {
t.insert(i,i,&hash);
assert_eq!(t.ctrl().as_ptr(),allocation);
assert_eq!(t.slot_ptr(t.find(0,|v| *v == 0).unwrap()),bucket);
assert!(t.capacity() >= t.len());
}
}
let mut t = RawTable::with_capacity(112);
for i in 0..112 { t.insert(0,i,&|_| 0); }
let allocation = t.ctrl().as_ptr();
for i in 0..100 {
let slot = t.find(0,|v| *v == i).unwrap();
assert_eq!(t.remove(slot),i);
}
let additional = t.capacity() - t.len() + 1;
assert!(t.len() + additional <= 56, "must trigger in-place branch");
t.reserve(additional,&|_| 0);
assert_eq!(t.ctrl().as_ptr(),allocation);
assert_eq!(t.capacity(),112);
for i in 100..112 { assert!(t.find(0,|v| *v == i).is_some()); }
}
#[test]
fn group_wraparound_small_tables_and_alignment() {
#[derive(Clone)]
#[repr(align(128))]
struct Aligned(u64);
for cap in [1,3,4,7,8,14,28] {
for start in 0..64u64 {
let mut t = RawTable::with_capacity(cap);
let limit = t.capacity();
for i in 0..limit { t.insert(start,Aligned(i as u64),&|_| start); }
for i in 0..limit {
let slot = t.find(start,|v| v.0 == i as u64).unwrap();
assert_eq!(t.slot_ptr(slot) as usize % 128,0);
}
assert!(t.find(start,|v| v.0 == 1000).is_none());
let clone = t.clone_table();
for i in 0..limit {
let slot = t.find(start,|v| v.0 == i as u64).unwrap();
assert_eq!(t.remove(slot).0,i as u64);
assert!(clone.find(start,|v| v.0 == i as u64).is_some());
}
}
}
let mut zst = RawTable::new();
for _ in 0..100 { zst.insert(0,(),&|_| 0); }
assert_eq!(zst.len(),100);
zst.clear(); zst.shrink_to(0,&|_| 0);
assert_eq!(zst.buckets(),0);
}
#[test]
fn panic_during_resize_clone_and_rehash_drops_once() {
use std::cell::Cell;
use std::rc::Rc;
use std::panic::{catch_unwind, AssertUnwindSafe};
struct Value { id: usize, drops: Rc<Cell<usize>>, panic_clone: bool }
impl Drop for Value { fn drop(&mut self) { self.drops.set(self.drops.get()+1); } }
impl Clone for Value {
fn clone(&self) -> Self {
if self.panic_clone && self.id == 50 { panic!("clone"); }
Self { id: self.id, drops: self.drops.clone(), panic_clone: self.panic_clone }
}
}
let drops = Rc::new(Cell::new(0));
let mut t = RawTable::with_capacity(112);
for id in 0..112 { t.insert(0,Value { id,drops:drops.clone(),panic_clone:true },&|_| 0); }
assert!(catch_unwind(AssertUnwindSafe(|| { t.reserve(1000,&|v| { if v.id == 50 { panic!("hash"); } 0 }); })).is_err());
assert_eq!(drops.get(),0);
assert_eq!(t.len(),112);
for id in 0..112 { assert!(t.find(0,|v| v.id == id).is_some()); }
assert!(catch_unwind(AssertUnwindSafe(|| { t.clone_table(); })).is_err());
let clone_drops = drops.get();
assert!(clone_drops > 0 && clone_drops < 112);
for id in 0..100 { let slot = t.find(0,|v| v.id == id).unwrap(); drop(t.remove(slot)); }
let additional = t.capacity() - t.len() + 1;
let calls = Cell::new(0);
assert!(catch_unwind(AssertUnwindSafe(|| t.reserve(additional,&|_| {
calls.set(calls.get()+1); if calls.get() == 4 { panic!("rehash"); } 0
}))).is_err());
assert_eq!(t.len(),3);
assert_eq!(t.capacity(),112);
drop(t);
assert_eq!(drops.get(),112 + clone_drops);
}
// Opt-in timing only; does not invoke the shared std/bench lane.
#[test]
#[ignore]
fn quick_timing() {
use std::hint::black_box;
use std::time::{Duration, Instant};
let mut seed = 0x4321fedc9876abcd;
let keys: Vec<u64> = (0..1_000_000).map(|_| random(&mut seed)).collect();
for mode in [3,4] {
let mut ported = Duration::MAX;
let mut real = Duration::MAX;
for _ in 0..5 {
let mut a = HashMap::with_hasher(Builder(mode));
let start = Instant::now();
for &key in &keys { a.insert(key,key); }
for key in &keys { black_box(a.get(black_box(key))); }
ported = ported.min(start.elapsed());
black_box(a);
let mut b = RealMap::with_hasher(Builder(mode));
let start = Instant::now();
for &key in &keys { b.insert(key,key); }
for key in &keys { black_box(b.get(black_box(key))); }
real = real.min(start.elapsed());
black_box(b);
}
println!("hash mode {mode}: ported {ported:?}, real {real:?}, ratio {:.3}",ported.as_secs_f64()/real.as_secs_f64());
}
}
#[test]
fn raw_iteration_mutation_and_drain() {
let mut t = RawTable::with_capacity(100);
for i in 0..100u64 { t.insert(i, i, &|v| *v); }
t.retain(|v| *v % 3 == 0);
let mut i = raw_table::next_full(t.ctrl(), 0);
let mut count = 0;
while i < t.buckets() {
unsafe { *t.get_mut(i) += 1; }
assert_eq!(unsafe { t.take_for_iter(i) } % 3, 1);
count += 1;
i = raw_table::next_full(t.ctrl(), i + 1);
}
assert_eq!(count, 34);
t.reset_after_drain();
assert_eq!(t.len(), 0);
assert_eq!(t.capacity(), 112);
}
#[test]
fn size_classes_mirrors_and_wrap_deletion() {
let width = if cfg!(all(relax_check, not(target_arch = "x86_64"))) { 8 } else { 16 };
// Requested capacities cover every allocated size class through 64 buckets,
// plus growth through both SIMD widths. Start probes at the final bucket.
for requested in 0..=56 {
for one_bit in [false, true] {
let mut t = RawTable::with_capacity(requested);
let mut real = RealMap::new();
let hash = |k: &u64| 63 | (if one_bit { (k & 1) << 57 } else { 0 });
let mut seed = 0x3141592653589793;
for step in 0..512u64 {
let k = if step < 64 { step } else { random(&mut seed) % 96 };
let found = t.find(hash(&k), |v: &u64| *v == k);
if step >= 64 && random(&mut seed) & 1 == 0 {
let got = found.map(|i| t.remove(i));
assert_eq!(got, real.remove(&k));
} else if found.is_none() {
t.insert(hash(&k), k, &hash);
real.insert(k, k);
}
if step % 31 == 0 {
t.reserve(3, &hash);
t.shrink_to(0, &hash);
}
assert_eq!(t.len(), real.len());
for key in 0..96u64 {
assert_eq!(t.find(hash(&key), |v: &u64| *v == key).is_some(), real.contains_key(&key));
}
let buckets = t.buckets();
if buckets != 0 {
for i in 0..buckets.min(width) {
let mirror = (i.wrapping_sub(width) & (buckets - 1)) + width;
assert_eq!(t.ctrl()[i], unsafe { *t.ctrl().as_ptr().add(mirror) });
}
for i in buckets..width {
assert_eq!(unsafe { *t.ctrl().as_ptr().add(i) }, 0xff);
}
}
}
}
}
}