use std::hash::{Hash, Hasher};
use super::{
Term,
bigint_math::BigIntValue,
binary_ref::BinaryRef,
boxed::{BigInt, Closure, Cons, Float, Map, Tuple},
compare,
pid_ref::PidRef,
reference_ref::ReferenceRef,
};
#[derive(Copy, Clone, Debug)]
pub struct EtsKey(Term);
impl EtsKey {
#[must_use]
pub const fn new(term: Term) -> Self {
Self(term)
}
#[must_use]
pub const fn term(self) -> Term {
self.0
}
}
impl PartialEq for EtsKey {
fn eq(&self, other: &Self) -> bool {
compare::partial_eq(&self.0, &other.0)
}
}
impl Eq for EtsKey {}
impl Hash for EtsKey {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u64(term_hash(self.0));
}
}
#[must_use]
pub fn term_hash(term: Term) -> u64 {
let mut state = StableHasher::default();
hash_term(term, &mut state);
state.finish()
}
#[derive(Default)]
struct StableHasher {
hash: u64,
}
impl StableHasher {
const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
fn mix(&mut self, value: u64) {
let mut bytes = value.to_le_bytes();
for byte in &mut bytes {
self.hash ^= u64::from(*byte);
self.hash = self.hash.wrapping_mul(Self::PRIME);
}
}
}
impl Hasher for StableHasher {
fn finish(&self) -> u64 {
if self.hash == 0 {
Self::OFFSET_BASIS
} else {
self.hash
}
}
fn write(&mut self, bytes: &[u8]) {
if self.hash == 0 {
self.hash = Self::OFFSET_BASIS;
}
for byte in bytes {
self.hash ^= u64::from(*byte);
self.hash = self.hash.wrapping_mul(Self::PRIME);
}
}
fn write_u8(&mut self, i: u8) {
self.write(&[i]);
}
fn write_u64(&mut self, i: u64) {
self.mix(i);
}
fn write_usize(&mut self, i: usize) {
self.mix(i as u64);
}
}
#[derive(Copy, Clone)]
enum HashKind {
SmallInt = 1,
Atom = 2,
Pid = 3,
Nil = 4,
Tuple = 5,
Float = 6,
BigInt = 7,
Closure = 8,
Map = 9,
Reference = 10,
Binary = 11,
List = 12,
Other = 13,
}
fn hash_term(term: Term, state: &mut StableHasher) {
if let Some(value) = term.as_small_int() {
hash_kind(HashKind::SmallInt, state);
state.write_u64(value as u64);
} else if let Some(atom) = term.as_atom() {
hash_kind(HashKind::Atom, state);
state.write_u64(u64::from(atom.index()));
} else if let Some(pid) = PidRef::new(term) {
hash_kind(HashKind::Pid, state);
state.write_u8(u8::from(pid.node().is_some()));
state.write_u32(pid.node().map_or(0, |node| node.index()));
state.write_u64(pid.pid_number());
state.write_u64(pid.serial());
} else if term.is_nil() {
hash_kind(HashKind::Nil, state);
} else if let Some(tuple) = Tuple::new(term) {
hash_tuple(tuple, state);
} else if let Some(float) = Float::new(term) {
hash_kind(HashKind::Float, state);
state.write_u64(float.value().to_bits());
} else if let Some(bigint) = BigInt::new(term) {
hash_bigint(bigint, state);
} else if let Some(closure) = Closure::new(term) {
hash_closure(closure, state);
} else if let Some(map) = Map::new(term) {
hash_map(map, state);
} else if let Some(reference) = ReferenceRef::new(term) {
hash_kind(HashKind::Reference, state);
state.write_u8(u8::from(reference.node().is_some()));
state.write_u32(reference.node().map_or(0, |node| node.index()));
state.write_u64(reference.id());
} else if let Some(binary) = BinaryRef::new(term) {
hash_kind(HashKind::Binary, state);
state.write(binary.as_bytes());
} else if term.is_list() {
hash_list(term, state);
} else {
hash_kind(HashKind::Other, state);
state.write_u64(term.raw());
}
}
fn hash_kind(kind: HashKind, state: &mut StableHasher) {
state.write_u8(kind as u8);
}
fn hash_tuple(tuple: Tuple, state: &mut StableHasher) {
hash_kind(HashKind::Tuple, state);
state.write_usize(tuple.arity());
for index in 0..tuple.arity() {
if let Some(element) = tuple.get(index) {
hash_term(element, state);
}
}
}
fn hash_bigint(bigint: BigInt, state: &mut StableHasher) {
let value = BigIntValue::from_bigint(bigint);
if let Some(small) = value.to_small_i64().and_then(Term::try_small_int) {
hash_term(small, state);
return;
}
hash_kind(HashKind::BigInt, state);
state.write_u8(u8::from(value.is_negative()));
state.write_usize(value.limbs().len());
for limb in value.limbs() {
state.write_u64(*limb);
}
}
fn hash_closure(closure: Closure, state: &mut StableHasher) {
hash_kind(HashKind::Closure, state);
match closure.module() {
Some(module) => {
state.write_u8(1);
state.write_u64(u64::from(module.index()));
}
None => state.write_u8(0),
}
state.write_u64(closure.function_index());
state.write_u8(closure.arity());
state.write_u64(closure.generation());
state.write_u64(closure.unique_id());
state.write_usize(closure.num_free());
for index in 0..closure.num_free() {
if let Some(free_var) = closure.free_var(index) {
hash_term(free_var, state);
}
}
}
fn hash_map(map: Map, state: &mut StableHasher) {
hash_kind(HashKind::Map, state);
let mut entries = map_entries(map);
entries.sort_by(|left, right| compare::exact_cmp(left.key, right.key));
state.write_usize(entries.len());
for entry in entries {
hash_term(entry.key, state);
hash_term(entry.value, state);
}
}
fn map_entries(map: Map) -> Vec<MapEntry> {
let mut entries = Vec::with_capacity(map.len());
for index in 0..map.len() {
if let (Some(key), Some(value)) = (map.key(index), map.value(index)) {
entries.push(MapEntry { key, value });
}
}
entries
}
#[derive(Copy, Clone)]
struct MapEntry {
key: Term,
value: Term,
}
fn hash_list(mut term: Term, state: &mut StableHasher) {
hash_kind(HashKind::List, state);
loop {
match Cons::new(term) {
Some(cons) => {
state.write_u8(1);
hash_term(cons.head(), state);
term = cons.tail();
}
None => {
state.write_u8(0);
hash_term(term, state);
break;
}
}
}
}
#[cfg(test)]
mod tests {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use super::*;
use crate::atom::Atom;
use crate::term::{Term, binary::write_binary, boxed};
#[test]
fn equal_atoms_and_integers_hash_equal() {
assert_eq!(
term_hash(Term::atom(Atom::OK)),
term_hash(Term::atom(Atom::OK))
);
assert_eq!(
term_hash(Term::small_int(42)),
term_hash(Term::small_int(42))
);
}
#[test]
fn variety_of_different_terms_hash_differently() {
let mut binary_heap = [0_u64; 3];
let binary = write_binary(&mut binary_heap, b"ok").expect("binary fits");
let terms = [
Term::small_int(42),
Term::small_int(43),
Term::atom(Atom::OK),
Term::atom(Atom::ERROR),
Term::NIL,
binary,
];
for (left_index, left) in terms.iter().copied().enumerate() {
for right in terms[left_index + 1..].iter().copied() {
assert_ne!(term_hash(left), term_hash(right));
}
}
}
#[test]
fn independently_allocated_equal_tuples_lists_and_binaries_hash_equal() {
let mut left_tuple_heap = [0_u64; 3];
let mut right_tuple_heap = [0_u64; 3];
let left_tuple = boxed::write_tuple(
&mut left_tuple_heap,
&[Term::atom(Atom::OK), Term::small_int(1)],
)
.expect("tuple fits");
let right_tuple = boxed::write_tuple(
&mut right_tuple_heap,
&[Term::atom(Atom::OK), Term::small_int(1)],
)
.expect("tuple fits");
assert_eq!(left_tuple, right_tuple);
assert_eq!(term_hash(left_tuple), term_hash(right_tuple));
let mut left_tail_heap = [0_u64; 2];
let mut left_head_heap = [0_u64; 2];
let mut right_tail_heap = [0_u64; 2];
let mut right_head_heap = [0_u64; 2];
let left_tail = boxed::write_cons(&mut left_tail_heap, Term::small_int(2), Term::NIL)
.expect("tail fits");
let left_list = boxed::write_cons(&mut left_head_heap, Term::small_int(1), left_tail)
.expect("head fits");
let right_tail = boxed::write_cons(&mut right_tail_heap, Term::small_int(2), Term::NIL)
.expect("tail fits");
let right_list = boxed::write_cons(&mut right_head_heap, Term::small_int(1), right_tail)
.expect("head fits");
assert_eq!(left_list, right_list);
assert_eq!(term_hash(left_list), term_hash(right_list));
let mut left_binary_heap = [0_u64; 3];
let mut right_binary_heap = [0_u64; 3];
let left_binary = write_binary(&mut left_binary_heap, b"same").expect("binary fits");
let right_binary = write_binary(&mut right_binary_heap, b"same").expect("binary fits");
assert_eq!(left_binary, right_binary);
assert_eq!(term_hash(left_binary), term_hash(right_binary));
}
#[test]
fn ets_key_structural_equality_for_boxed_terms() {
let mut left_heap = [0_u64; 3];
let mut right_heap = [0_u64; 3];
let left = boxed::write_tuple(&mut left_heap, &[Term::atom(Atom::OK), Term::small_int(1)])
.expect("tuple fits");
let right =
boxed::write_tuple(&mut right_heap, &[Term::atom(Atom::OK), Term::small_int(1)])
.expect("tuple fits");
let left_key = EtsKey::new(left);
let right_key = EtsKey::new(right);
assert_ne!(left.raw(), right.raw());
assert_eq!(left_key, right_key);
let mut left_hasher = DefaultHasher::new();
let mut right_hasher = DefaultHasher::new();
left_key.hash(&mut left_hasher);
right_key.hash(&mut right_hasher);
assert_eq!(left_hasher.finish(), right_hasher.finish());
}
#[test]
fn exact_distinct_numeric_terms_hash_differently() {
let mut heap = [0_u64; 2];
let float = boxed::write_float(&mut heap, 1.0).expect("float fits");
assert_ne!(Term::small_int(1), float);
assert_ne!(term_hash(Term::small_int(1)), term_hash(float));
}
#[test]
fn bigint_hash_canonicalizes_small_integer_values() {
let mut small_heap = [0_u64; 4];
let mut large_heap = [0_u64; 5];
let mut different_heap = [0_u64; 5];
let small_bigint = boxed::write_bigint(&mut small_heap, false, &[42]).expect("bigint fits");
let large_bigint =
boxed::write_bigint(&mut large_heap, false, &[0, 1]).expect("bigint fits");
let different_bigint =
boxed::write_bigint(&mut different_heap, false, &[1, 1]).expect("bigint fits");
assert_eq!(term_hash(small_bigint), term_hash(Term::small_int(42)));
assert_ne!(term_hash(large_bigint), 0);
assert_eq!(term_hash(large_bigint), term_hash(large_bigint));
assert_ne!(term_hash(large_bigint), term_hash(different_bigint));
}
#[test]
fn equal_maps_hash_equal_regardless_of_entry_order() {
let mut left_heap = [0_u64; 6];
let mut right_heap = [0_u64; 6];
let left = boxed::write_map(
&mut left_heap,
&[Term::atom(Atom::OK), Term::atom(Atom::ERROR)],
&[Term::small_int(1), Term::small_int(2)],
)
.expect("left map fits");
let right = boxed::write_map(
&mut right_heap,
&[Term::atom(Atom::ERROR), Term::atom(Atom::OK)],
&[Term::small_int(2), Term::small_int(1)],
)
.expect("right map fits");
assert_eq!(left, right);
assert_eq!(term_hash(left), term_hash(right));
}
#[test]
fn improper_list_tail_contributes_to_hash() {
let mut proper_heap = [0_u64; 2];
let mut improper_heap = [0_u64; 2];
let proper = boxed::write_cons(&mut proper_heap, Term::small_int(1), Term::NIL)
.expect("proper list fits");
let improper =
boxed::write_cons(&mut improper_heap, Term::small_int(1), Term::small_int(2))
.expect("improper list fits");
assert_ne!(proper, improper);
assert_ne!(term_hash(proper), term_hash(improper));
}
}