pub mod murmur3;
pub mod rapid;
pub mod siphash;
use crate::lang::protocol::HashType;
pub const DEFAULT_HASH: HashType = HashType::Rapid;
pub trait JavaHash {
fn java_hash(&self, hash_type: HashType) -> i64;
}
impl JavaHash for bool {
fn java_hash(&self, _: HashType) -> i64 {
hash_bool(*self) as i64
}
}
impl JavaHash for char {
fn java_hash(&self, _: HashType) -> i64 {
hash_char(*self) as i64
}
}
impl JavaHash for i64 {
fn java_hash(&self, _: HashType) -> i64 {
hash_long(*self) as i64
}
}
impl JavaHash for i32 {
fn java_hash(&self, _: HashType) -> i64 {
hash_long(*self as i64) as i64
}
}
impl JavaHash for usize {
fn java_hash(&self, _: HashType) -> i64 {
hash_long(*self as i64) as i64
}
}
impl JavaHash for u64 {
fn java_hash(&self, _: HashType) -> i64 {
hash_long(*self as i64) as i64
}
}
impl JavaHash for f64 {
fn java_hash(&self, _: HashType) -> i64 {
hash_double(*self) as i64
}
}
impl JavaHash for String {
fn java_hash(&self, _: HashType) -> i64 {
java_string_hash(self) as i64
}
}
impl JavaHash for &str {
fn java_hash(&self, _: HashType) -> i64 {
java_string_hash(self) as i64
}
}
pub fn java_string_hash(s: &str) -> i32 {
let mut h = 0i32;
for unit in s.encode_utf16() {
h = h.wrapping_mul(31).wrapping_add(unit as i32);
}
h
}
pub fn hash_seed(obj_name: &str) -> i32 {
java_string_hash(&format!("::{obj_name}"))
}
pub fn hash_bool(b: bool) -> i32 {
if b {
1231
} else {
1237
}
}
pub fn hash_char(c: char) -> i32 {
c as i32
}
pub fn hash_bytes(bytes: &[u8]) -> i32 {
let mut h = 1i32;
for b in bytes {
h = h.wrapping_mul(31).wrapping_add(*b as i8 as i32);
}
h
}
pub fn hash_long(n: i64) -> i32 {
hash_long_placement(n)
}
pub fn hash_long_placement(n: i64) -> i32 {
let text = n.to_string();
let Some((signum, digits, scale)) = parse_decimal(&text) else {
return java_string_hash(&text);
};
bigdecimal_hash(&digits_to_words_be(&digits), signum, scale as i32)
}
pub fn hash_double(d: f64) -> i32 {
assert!(d.is_finite(), "non-finite number");
if d == 0.0 {
return 0;
}
canonical_decimal_str_hash(&format!("{d}"))
}
pub fn canonical_decimal_str_hash(s: &str) -> i32 {
match parse_decimal(s) {
Some((signum, digits, scale)) => canonical_decimal_hash(signum, digits, scale),
None => java_string_hash(s),
}
}
pub fn hash_string_type(hash_type: HashType, hashed: &str) -> i64 {
match hash_type {
HashType::System => java_string_hash(hashed) as i64,
HashType::Rapid => rapid::hash(hashed.as_bytes()) as i64,
HashType::Murmur3 => murmur3::hash_chars(hashed) as i64,
HashType::Sip => -1,
}
}
pub fn compose_ordered(obj_name: &str, items: impl IntoIterator<Item = i64>) -> i64 {
let mut acc = hash_seed(obj_name) as i64;
for h in items {
acc = acc.wrapping_mul(31).wrapping_add(h);
}
acc
}
pub fn compose_unordered(obj_name: &str, items: impl IntoIterator<Item = i64>) -> i64 {
let mut acc = hash_seed(obj_name) as i64;
for h in items {
acc = acc.wrapping_add(h);
}
acc
}
pub fn compose_entry(key_hash: i64, value_hash: i64) -> i64 {
compose_ordered("SEQUENTIAL", [key_hash, value_hash])
}
fn biginteger_hash(words_be: &[u32], signum: i32) -> i32 {
let mut h = 0i32;
for w in words_be {
h = h.wrapping_mul(31).wrapping_add(*w as i32);
}
h.wrapping_mul(signum)
}
fn bigdecimal_hash(words_be: &[u32], signum: i32, scale: i32) -> i32 {
if signum == 0 {
return scale;
}
let mag: u128 = words_be
.iter()
.fold(0u128, |acc, w| (acc << 32) | (*w as u128));
if mag < (1u128 << 63) {
let val2 = mag as u64;
let temp = ((((val2 >> 32) as u32) as i32).wrapping_mul(31) as i64
+ (val2 & 0xffff_ffff) as i64) as i32;
let signed = if signum < 0 {
temp.wrapping_neg()
} else {
temp
};
31i32.wrapping_mul(signed).wrapping_add(scale)
} else {
31i32
.wrapping_mul(biginteger_hash(words_be, signum))
.wrapping_add(scale)
}
}
fn parse_decimal(s: &str) -> Option<(i32, Vec<u8>, i64)> {
let b = s.as_bytes();
let mut i = 0usize;
let mut neg = false;
if i < b.len() && (b[i] == b'-' || b[i] == b'+') {
neg = b[i] == b'-';
i += 1;
}
let mut digits: Vec<u8> = Vec::new();
let mut seen_dot = false;
let mut scale: i64 = 0;
let mut any_digit = false;
while i < b.len() {
match b[i] {
c @ b'0'..=b'9' => {
digits.push(c - b'0');
if seen_dot {
scale += 1;
}
any_digit = true;
}
b'.' if !seen_dot => seen_dot = true,
b'e' | b'E' => {
i += 1;
let mut eneg = false;
if i < b.len() && (b[i] == b'-' || b[i] == b'+') {
eneg = b[i] == b'-';
i += 1;
}
let mut exp: i64 = 0;
let mut any_exp = false;
while i < b.len() && b[i].is_ascii_digit() {
exp = exp.saturating_mul(10).saturating_add((b[i] - b'0') as i64);
any_exp = true;
i += 1;
}
if !any_exp {
return None;
}
scale -= if eneg { -exp } else { exp };
break;
}
_ => return None,
}
i += 1;
}
if !any_digit {
return None;
}
match digits.iter().position(|d| *d != 0) {
None => Some((0, vec![0], 0)),
Some(first) => Some((if neg { -1 } else { 1 }, digits[first..].to_vec(), scale)),
}
}
fn canonical_decimal_hash(signum: i32, mut digits: Vec<u8>, mut scale: i64) -> i32 {
if signum == 0 {
return 0;
}
while digits.last() == Some(&0) {
digits.pop();
scale -= 1;
}
let words = digits_to_words_be(&digits);
bigdecimal_hash(&words, signum, scale as i32)
}
fn digits_to_words_be(digits: &[u8]) -> Vec<u32> {
let mut le: Vec<u32> = vec![0];
for d in digits {
let mut carry = *d as u64;
for w in le.iter_mut() {
let v = (*w as u64) * 10 + carry;
*w = v as u32;
carry = v >> 32;
}
while carry > 0 {
le.push(carry as u32);
carry >>= 32;
}
}
while le.len() > 1 && le.last() == Some(&0) {
le.pop();
}
le.iter().rev().copied().collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Value;
use crate::kernel::parser::parse_forms;
use crate::kernel::Form;
use crate::lang::data::{
Keyword, Map as PMap, MapEntry as PMapEntry, Set as PSet, Symbol, Tuple as PTuple,
};
use crate::lang::protocol::{IDisplay, IHash, IObjType};
fn corpus_path(relative: &str) -> Option<std::path::PathBuf> {
crate::spec_registry::resolve(relative).filter(|candidate| candidate.is_file())
}
fn field<'a>(case: &'a Form, key: &str) -> &'a Form {
match case {
Form::Map(entries) => entries
.iter()
.find(|(k, _)| matches!(k, Form::Keyword(kw) if kw == key))
.map(|(_, v)| v)
.unwrap_or_else(|| panic!("case missing :{key}: {case}")),
other => panic!("case is not a map: {other}"),
}
}
fn kw_of(form: &Form) -> &str {
match form {
Form::Keyword(s) => s,
other => panic!("expected keyword, got {other}"),
}
}
fn num_of(form: &Form) -> i64 {
match form {
Form::Number(n) => *n,
other => panic!("expected number, got {other}"),
}
}
fn str_of(form: &Form) -> &str {
match form {
Form::String(s) => s,
other => panic!("expected string, got {other}"),
}
}
fn hash_type(id: &str) -> HashType {
match id {
"system" => HashType::System,
"rapid" => HashType::Rapid,
"murmur3" => HashType::Murmur3,
"sip" => HashType::Sip,
other => panic!("unknown hash type: {other}"),
}
}
fn element_value(form: &Form) -> Value {
match form {
Form::Nil => Value::Nil,
Form::Bool(b) => Value::Bool(*b),
Form::Number(n) => Value::Number(*n),
Form::Float(f) => Value::Float(*f),
Form::String(s) => Value::String(s.clone().into()),
Form::Vector(items) => Value::Vector(items.iter().map(element_value).collect()),
Form::List(items) => Value::List(items.iter().map(element_value).collect()),
Form::Map(pairs) => Value::Map(
pairs
.iter()
.map(|(k, v)| (element_value(k), element_value(v)))
.collect::<PMap<Value, Value>>(),
),
Form::Set(items) => {
Value::Set(items.iter().map(element_value).collect::<PSet<Value>>())
}
other => panic!("unsupported collection element: {other}"),
}
}
fn collection_value(structure: &str, input: &Form) -> Value {
match structure {
"vector" | "list" | "map" | "set" => element_value(input),
"queue" => match input {
Form::Vector(items) => {
Value::Queue(Box::new(items.iter().map(element_value).collect()))
}
other => panic!("queue input must be a vector: {other}"),
},
"compact-vector2" => match input {
Form::Vector(items) if items.len() == 2 => Value::Tuple(Box::new(
PTuple::from_values(items.iter().map(element_value).collect()).unwrap(),
)),
other => panic!("compact-vector2 input must be a 2-vector: {other}"),
},
"map-entry" => match input {
Form::Vector(items) if items.len() == 2 => Value::MapEntry(Box::new(
PMapEntry::new(element_value(&items[0]), element_value(&items[1])),
)),
other => panic!("map-entry input must be a 2-vector: {other}"),
},
other => panic!("unknown collection structure: {other}"),
}
}
fn eval_case(case: &Form) -> i64 {
let hash = kw_of(field(case, "hash"));
let kind = kw_of(field(case, "kind"));
let input = field(case, "input");
match kind {
"string" => {
let s = str_of(input);
match hash {
"rapid" => rapid::hash(s.as_bytes()) as i64,
"murmur3" => murmur3::hash_chars(s) as i64,
"sip" => siphash::hash(&siphash::HARA, s.as_bytes()) as i64,
"system" => java_string_hash(s) as i64,
other => panic!("unknown string hash type: {other}"),
}
}
"int" => murmur3::hash_int(num_of(input) as i32) as i64,
"long" => match hash {
"murmur3" => murmur3::hash_long(num_of(input)) as i64,
"system" => hash_long(num_of(input)) as i64,
other => panic!("unknown long hash type: {other}"),
},
"double" => match input {
Form::Float(f) => hash_double(*f) as i64,
other => panic!("double input must be a float: {other}"),
},
"bigint" => canonical_decimal_str_hash(str_of(input)) as i64,
"bool" => match input {
Form::Bool(b) => hash_bool(*b) as i64,
other => panic!("bool input must be a boolean: {other}"),
},
"char" => match input {
Form::Character(c) => hash_char(*c) as i64,
other => panic!("char input must be a character: {other}"),
},
"bytes" => match input {
Form::Vector(items) => {
let bytes: Vec<u8> = items.iter().map(|f| num_of(f) as i8 as u8).collect();
hash_bytes(&bytes) as i64
}
other => panic!("bytes input must be a vector: {other}"),
},
"nil" => 0,
"seed" => hash_seed(str_of(input)) as i64,
"keyword" => match input {
Form::Keyword(s) => Keyword::parse(s).unwrap().hash_calc(hash_type(hash)) as i64,
other => panic!("keyword input must be a keyword: {other}"),
},
"symbol" => match input {
Form::Symbol(s) => Symbol::parse(s).hash_calc(hash_type(hash)) as i64,
other => panic!("symbol input must be a symbol: {other}"),
},
"collection" => {
let structure = kw_of(field(case, "structure"));
let value = collection_value(structure, input);
match hash {
"rapid" => value.stable_hash() as i64,
"murmur3" => value.java_hash(HashType::Murmur3),
other => panic!("unknown collection hash type: {other}"),
}
}
other => panic!("unknown case kind: {other}"),
}
}
#[test]
fn java_parity_fixture() {
let Some(path) =
corpus_path("01-lang/020-data-structures/draft/conformance/hash-parity.edn")
else {
eprintln!(
"skipping hash-parity corpus: specs checkout not found from {}",
env!("CARGO_MANIFEST_DIR")
);
return;
};
let source = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
let forms = parse_forms(&source).expect("hash-parity corpus must parse");
assert_eq!(forms.len(), 1, "corpus must be a single map form");
let Form::Vector(cases) = field(&forms[0], "cases") else {
panic!("corpus :cases must be a vector");
};
let mut failures: Vec<String> = Vec::new();
for case in cases {
if kw_of(field(case, "kind")) == "decimal" {
continue;
}
let id = kw_of(field(case, "id")).to_string();
let expected = num_of(field(case, "expect"));
let actual = eval_case(case);
if actual != expected {
failures.push(format!(":{id}: expected {expected}, got {actual}"));
}
}
assert!(
cases.len() >= 270,
"only {} hash-parity cases found",
cases.len()
);
if !failures.is_empty() {
panic!(
"{} of {} hash-parity cases failed:\n{}",
failures.len(),
cases.len(),
failures.join("\n")
);
}
}
#[test]
fn cross_type_numeric_equality() {
assert_eq!(hash_double(1.0), canonical_decimal_str_hash("1"));
assert_eq!(hash_double(1.0), canonical_decimal_str_hash("1.0"));
assert_eq!(hash_double(2.5), canonical_decimal_str_hash("2.50"));
assert_eq!(hash_double(100.0), canonical_decimal_str_hash("100"));
assert_eq!(hash_double(100.0), canonical_decimal_str_hash("100.0"));
}
#[test]
fn subnormal_double_known_divergence() {
assert_eq!(hash_double(f64::from_bits(1)), 479);
assert_eq!(hash_double(5e-324), 479);
}
#[test]
fn keyword_display_form_deviation() {
let kw = Keyword::create(None, "a").unwrap();
assert_eq!(
kw.hash_calc(HashType::Rapid) as i64,
rapid::hash("::KEYWORD|:a".as_bytes()) as i64
);
let sym = Symbol::create(None, "a");
assert_eq!(
sym.hash_calc(HashType::Rapid) as i64,
rapid::hash("::SYMBOL|hara.lang.data.Symbol<a>".as_bytes()) as i64
);
assert_eq!(kw.hash_calc(HashType::Sip) as i64, -1);
assert_eq!(kw.display(), ":a");
assert_eq!(sym.display(), "a");
assert_eq!(kw.hash_seed(), "::KEYWORD");
assert_eq!(sym.hash_seed(), "::SYMBOL");
}
}