use std::hash::{Hash, Hasher};
use crate::GcRef;
use crate::descriptor::{DynamicHasher, TypeDescriptor};
#[derive(Clone, Copy)]
pub struct DynamicKey {
value: GcRef,
descriptor: &'static TypeDescriptor,
}
impl DynamicKey {
#[must_use]
pub fn new(value: GcRef) -> Self {
let descriptor = value.descriptor();
Self { value, descriptor }
}
#[inline]
#[must_use]
pub fn value(&self) -> GcRef {
self.value
}
#[inline]
#[must_use]
pub fn descriptor(&self) -> &'static TypeDescriptor {
self.descriptor
}
}
impl PartialEq for DynamicKey {
fn eq(&self, other: &Self) -> bool {
if !std::ptr::eq(self.descriptor, other.descriptor) {
return false;
}
if self.value == other.value {
return true;
}
let Some(equals) = self.descriptor.equals else {
return false;
};
unsafe {
let a = self.value.payload::<u8>() as *const u8;
let b = other.value.payload::<u8>() as *const u8;
equals(a, b)
}
}
}
impl Eq for DynamicKey {}
impl std::fmt::Debug for DynamicKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = String::new();
let payload = self.value.payload::<u8>() as *const u8;
unsafe {
(self.descriptor.format)(payload, &mut crate::FormatSink::debug(&mut s));
}
write!(f, "DynamicKey({}:{})", self.descriptor.name, s)
}
}
impl Hash for DynamicKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.descriptor.id().hash(state);
match self.descriptor.hash {
Some(hash_fn) => {
let payload = self.value.payload::<u8>() as *const u8;
let mut shim = HasherShim(state);
unsafe { hash_fn(payload, &mut shim) };
}
None => {
}
}
}
}
struct HasherShim<'a, H: Hasher + ?Sized>(&'a mut H);
impl<H: Hasher + ?Sized> DynamicHasher for HasherShim<'_, H> {
fn write_bytes(&mut self, bytes: &[u8]) {
self.0.write(bytes);
}
fn finish(&self) -> u64 {
self.0.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::abi::praxis_alloc_int;
use crate::context::{Runtime, RuntimeContext};
use crate::descriptor::TypeDescriptor;
use crate::{Heap, Tracer};
unsafe fn test_trace(_: *mut u8, _: &mut dyn Tracer) {}
unsafe fn test_drop(_: *mut u8) {}
unsafe fn test_format(payload: *const u8, out: &mut crate::FormatSink<'_>) {
use std::fmt::Write as _;
let value = unsafe { *(payload as *const i64) };
let _ = write!(out, "{value}");
}
unsafe fn test_equals(a: *const u8, b: *const u8) -> bool {
unsafe { *(a as *const i64) == *(b as *const i64) }
}
unsafe fn test_format_u8(payload: *const u8, out: &mut crate::FormatSink<'_>) {
use std::fmt::Write as _;
let value = unsafe { *payload };
let _ = write!(out, "{value}");
}
unsafe fn test_equals_u8(a: *const u8, b: *const u8) -> bool {
unsafe { *a == *b }
}
static LOGICAL_A: TypeDescriptor = TypeDescriptor::for_test::<i64>(
10,
"LogicalA",
test_trace,
test_drop,
test_format,
Some(test_equals),
None,
None,
);
static LOGICAL_B: TypeDescriptor = TypeDescriptor::for_test::<i64>(
11,
"LogicalB",
test_trace,
test_drop,
test_format,
Some(test_equals),
None,
None,
);
static LOGICAL_C: TypeDescriptor = TypeDescriptor::for_test::<u8>(
12,
"LogicalC",
test_trace,
test_drop,
test_format_u8,
Some(test_equals_u8),
None,
None,
);
static A_PAYLOAD: crate::descriptor::Payload<i64> = crate::descriptor::Payload::new(&LOGICAL_A);
static B_PAYLOAD: crate::descriptor::Payload<i64> = crate::descriptor::Payload::new(&LOGICAL_B);
static C_PAYLOAD: crate::descriptor::Payload<u8> = crate::descriptor::Payload::new(&LOGICAL_C);
fn wired_ctx(rt: &mut Runtime) -> *mut RuntimeContext {
let ctx = Box::leak(Box::new(rt.context()));
ctx as *mut RuntimeContext
}
const UNINTERNED: i64 = crate::small_int::SMALL_INT_MAX + 1;
#[test]
fn dynamic_key_equal_for_identical_scalar_values() {
let mut rt = Runtime::new();
let ctx = wired_ctx(&mut rt);
let a = unsafe { praxis_alloc_int(ctx, UNINTERNED) };
let b = unsafe { praxis_alloc_int(ctx, UNINTERNED) };
assert_ne!(a, b, "distinct allocations");
let ka = DynamicKey::new(a);
let kb = DynamicKey::new(b);
assert_eq!(ka, kb, "equal Ints are equal keys structurally");
}
#[test]
fn dynamic_key_equal_for_the_same_interned_scalar() {
let mut rt = Runtime::new();
let ctx = wired_ctx(&mut rt);
let a = unsafe { praxis_alloc_int(ctx, 5) };
let b = unsafe { praxis_alloc_int(ctx, 5) };
assert_eq!(a.as_ptr(), b.as_ptr(), "a small Int is interned");
assert_eq!(DynamicKey::new(a), DynamicKey::new(b));
let c = unsafe { praxis_alloc_int(ctx, 6) };
let d = unsafe { praxis_alloc_int(ctx, UNINTERNED) };
assert_ne!(DynamicKey::new(a), DynamicKey::new(c));
assert_ne!(DynamicKey::new(a), DynamicKey::new(d));
}
#[test]
fn dynamic_key_unequal_for_different_scalar_values() {
let mut rt = Runtime::new();
let ctx = wired_ctx(&mut rt);
let a = unsafe { praxis_alloc_int(ctx, 5) };
let b = unsafe { praxis_alloc_int(ctx, 7) };
assert_ne!(DynamicKey::new(a), DynamicKey::new(b));
}
#[test]
fn dynamic_key_hash_matches_for_equal_values() {
let mut rt = Runtime::new();
let ctx = wired_ctx(&mut rt);
let a = unsafe { praxis_alloc_int(ctx, UNINTERNED) };
let b = unsafe { praxis_alloc_int(ctx, UNINTERNED) };
let ha = {
let mut h = std::collections::hash_map::DefaultHasher::new();
DynamicKey::new(a).hash(&mut h);
h.finish()
};
let hb = {
let mut h = std::collections::hash_map::DefaultHasher::new();
DynamicKey::new(b).hash(&mut h);
h.finish()
};
assert_eq!(ha, hb, "equal keys hash equal");
}
#[test]
fn dynamic_keys_with_different_descriptors_are_never_equal() {
let heap = Heap::new();
let a = heap.alloc_unpaced(A_PAYLOAD, 7_i64);
let b = heap.alloc_unpaced(B_PAYLOAD, 7_i64);
assert_ne!(
DynamicKey::new(a),
DynamicKey::new(b),
"runtime type identity is part of structural key equality"
);
}
#[test]
fn a_mismatched_key_never_dispatches_the_equality_callback() {
let heap = Heap::new();
let wide = heap.alloc_unpaced(A_PAYLOAD, 7_i64);
let narrow = heap.alloc_unpaced(C_PAYLOAD, 7_u8);
assert_ne!(DynamicKey::new(wide), DynamicKey::new(narrow));
assert_ne!(DynamicKey::new(narrow), DynamicKey::new(wide));
}
#[test]
fn keys_of_different_types_are_unequal_in_a_real_hash_set() {
use std::collections::HashSet;
let heap = Heap::new();
let a = heap.alloc_unpaced(A_PAYLOAD, 7_i64);
let b = heap.alloc_unpaced(B_PAYLOAD, 7_i64);
let mut set = HashSet::new();
assert!(set.insert(DynamicKey::new(a)));
assert!(
set.insert(DynamicKey::new(b)),
"a same-valued key of another type is a distinct entry"
);
assert_eq!(set.len(), 2);
}
#[test]
fn a_structural_key_hashes_by_contents_so_mutating_it_moves_its_bucket() {
use std::collections::HashSet;
use std::collections::hash_map::RandomState;
use std::hash::BuildHasher;
let rt = Runtime::new();
let state = RandomState::new();
let hash_of = |k: &DynamicKey| state.hash_one(*k);
let key = rt.alloc_vec(&crate::scalars::INT, Vec::new());
let wrapped = DynamicKey::new(key);
let before = hash_of(&wrapped);
let twin = DynamicKey::new(rt.alloc_vec(&crate::scalars::INT, Vec::new()));
assert_eq!(hash_of(&twin), before, "the hash is over the contents");
let mut set = HashSet::new();
assert!(set.insert(wrapped));
let item = rt.alloc_int(1);
unsafe {
(*key.payload::<crate::collections::VecPayload>())
.items
.push(item);
}
assert_ne!(
hash_of(&wrapped),
before,
"a mutated key hashes elsewhere — which is exactly why the type \
checker refuses one (D4, Y014)"
);
assert_eq!(set.len(), 1);
}
}