Axle v0.14.1
Package

std/text/hashing

std/hashing — non-cryptographic hashes for HashMap-style use.

All hash functions below are pure Axle. Byte iteration goes
through the __string_byte_at / __string_byte_len compiler
builtins (crates/3-codegen/axle_codegen/.../builtins/strings.rs), which
lower to an inline gep i8 + load i8 and an O(1) read of the
string's i64 length header. Reserved-prefix names so user code
can't reach them by bare name — s.byteAt(i) / the s.length
property stay on their FFI shims because those carry
codepoint-aware semantics (UTF-8 counting, bounds-checked -1 on
OOB) the inline lowering doesn't reproduce; the byte-level form
here is specifically for hash hot loops where every byte counts
and the iteration bound already enforces validity.

The length header spans the whole buffer, so these hashes cover
every byte a string holds — an interior NUL is hashed like any
other byte rather than ending the digest.

Free functions

TypeMethod and description
i64
fnv1aBytes(s : string) : i64 FNV-1a 64-bit hash. Constants from the RFC draft :
offset = 14695981039346656037 = 0xcbf29ce484222325
prime = 1099511628211 = 0x100000001b3

Signed-i64 multiplication wraps modulo 2^64, so the bit pattern
of h is byte-for-byte identical to the canonical u64 FNV-1a
state.
i64
djb2Bytes(s : string) : i64 djb2 — Daniel J. Bernstein's hash. Seed 5381, multiplier 33.
i64
hashCombine(a : i64, b : i64) : i64 Boost's hash_combine — mixes two i64 hashes into one.
h = a ^ (b + 0x9e3779b9 + (a << 6) + (a >> 2))
i64
__rotl64(x : i64, n : i64) : i64 Rotate x left by n, 0 < n < 64. Undefined at n == 0 — every
call site here passes a literal in {13, 16, 17, 21, 32}.
i64
hashStringSeeded(s : string, k0 : i64, k1 : i64) : i64 SwissTable's HashMap<string, V> hash backend — SipHash-1-3, a
keyed pseudo-random function, over the 128-bit process-random key
(k0, k1) drawn once at main's prologue from the OS CSPRNG
(@axle_hash_seed_lo/_hi, the same key the inline integer mix uses).

── Why a PRF and not a keyed FNV ──────────────────────────────
HashDoS resistance is a statement about collisions under an
unknown key, and a strong finaliser cannot supply it. Under the
previous construction the digest was F(fnv(k0, s) ^ k1) with F a
bijection, so hash(s1) == hash(s2) held exactly when
fnv(k0, s1) == fnv(k0, s2): every one of the finaliser's rounds ran
after the state had already collapsed, and contributed nothing an
attacker had to defeat. What it did buy — that an observed digest
cannot be inverted back to the key — is key-recovery resistance, a
different property. The mixing it protected also carried key-
independent structure: bit 0 of the FNV state is exactly
k0_0 XOR parity(bytes), so any two messages of equal byte parity
agree there for every key.

SipHash keys every round instead of only the ends, which is what puts
the collision question behind the key. c = 1 compression round per
8-byte block and d = 3 finalisation rounds is the fast variant, and
the one Rust's RandomState uses.

The cost is real and was measured, best-of-12 interleaved runs:
word_freq_topk +27%, hashmap_string +17%, strmap_intensive and
the integer-keyed hashset_intensive control both inside a ±2.5%
noise floor. A table that hashes untrusted keys is worth that; a
caller that wants the old speed on trusted input has fnv1aBytes.
i64
hashString(s : string) : i64 Deterministic public hash entry-points. hashString(s) is the
key-0 keyed mix (stable across runs — usable as a content-hash by
callers like strmap_intensive). hashI64(v) is SplitMix64
with seed 0 (the same mix axle_codegen::builtins::hash inlines
at HashMap<i64, V> sites, exposed here for explicit calls).

── HashDoS warning ────────────────────────────────────────────
Seed = 0 means the digest is FULLY PREDICTABLE. Safe for content
hashing (checksums, dedup, content-addressed caches) but UNSAFE
for hashing untrusted key material into a flat table — an
attacker can pre-compute colliding inputs and drive the table
into O(n²). The HashMap<string, V> backend uses the seeded
variant (hash_key builtin routes through hashStringSeeded
with @axle_hash_seed) and is HashDoS-safe ; user code that
hand-rolls a hash table on top of this entry point must wrap it
with its own seed.
i64
hashI64(v : i64) : i64 SplitMix64 finalizer over an i64 key (seed 0). The two multiplier
constants and the shift sequence are the canonical SplitMix64 mix —
canonical including the shift kind: >>> (zero-fill), because the
algorithm is specified over u64. Reading them as >> would be a
different, unanalysed function.

Method detail

#fnv1aBytes

fnv1aBytes(s : string) : i64

FNV-1a 64-bit hash. Constants from the RFC draft :
offset = 14695981039346656037 = 0xcbf29ce484222325
prime = 1099511628211 = 0x100000001b3

Signed-i64 multiplication wraps modulo 2^64, so the bit pattern
of h is byte-for-byte identical to the canonical u64 FNV-1a
state.

Parameters
s bytes to hash

#djb2Bytes

djb2Bytes(s : string) : i64

djb2 — Daniel J. Bernstein's hash. Seed 5381, multiplier 33.

Parameters
s bytes to hash

#hashCombine

hashCombine(a : i64, b : i64) : i64

Boost's hash_combine — mixes two i64 hashes into one.
h = a ^ (b + 0x9e3779b9 + (a << 6) + (a >> 2))

Parameters
a accumulator / first hash to mix
b second hash to fold into a

#__rotl64

__rotl64(x : i64, n : i64) : i64

Rotate x left by n, 0 < n < 64. Undefined at n == 0 — every
call site here passes a literal in {13, 16, 17, 21, 32}.

Parameters
x value to rotate
n rotation distance, strictly between 0 and 64

#hashStringSeeded

hashStringSeeded(s : string, k0 : i64, k1 : i64) : i64

SwissTable's HashMap<string, V> hash backend — SipHash-1-3, a
keyed pseudo-random function, over the 128-bit process-random key
(k0, k1) drawn once at main's prologue from the OS CSPRNG
(@axle_hash_seed_lo/_hi, the same key the inline integer mix uses).

── Why a PRF and not a keyed FNV ──────────────────────────────
HashDoS resistance is a statement about collisions under an
unknown key, and a strong finaliser cannot supply it. Under the
previous construction the digest was F(fnv(k0, s) ^ k1) with F a
bijection, so hash(s1) == hash(s2) held exactly when
fnv(k0, s1) == fnv(k0, s2): every one of the finaliser's rounds ran
after the state had already collapsed, and contributed nothing an
attacker had to defeat. What it did buy — that an observed digest
cannot be inverted back to the key — is key-recovery resistance, a
different property. The mixing it protected also carried key-
independent structure: bit 0 of the FNV state is exactly
k0_0 XOR parity(bytes), so any two messages of equal byte parity
agree there for every key.

SipHash keys every round instead of only the ends, which is what puts
the collision question behind the key. c = 1 compression round per
8-byte block and d = 3 finalisation rounds is the fast variant, and
the one Rust's RandomState uses.

The cost is real and was measured, best-of-12 interleaved runs:
word_freq_topk +27%, hashmap_string +17%, strmap_intensive and
the integer-keyed hashset_intensive control both inside a ±2.5%
noise floor. A table that hashes untrusted keys is worth that; a
caller that wants the old speed on trusted input has fnv1aBytes.

Parameters
s key bytes to hash
k0 low key word
k1 high key word

#hashString

hashString(s : string) : i64

Deterministic public hash entry-points. hashString(s) is the
key-0 keyed mix (stable across runs — usable as a content-hash by
callers like strmap_intensive). hashI64(v) is SplitMix64
with seed 0 (the same mix axle_codegen::builtins::hash inlines
at HashMap<i64, V> sites, exposed here for explicit calls).

── HashDoS warning ────────────────────────────────────────────
Seed = 0 means the digest is FULLY PREDICTABLE. Safe for content
hashing (checksums, dedup, content-addressed caches) but UNSAFE
for hashing untrusted key material into a flat table — an
attacker can pre-compute colliding inputs and drive the table
into O(n²). The HashMap<string, V> backend uses the seeded
variant (hash_key builtin routes through hashStringSeeded
with @axle_hash_seed) and is HashDoS-safe ; user code that
hand-rolls a hash table on top of this entry point must wrap it
with its own seed.

Parameters
s bytes to hash deterministically (key 0)

#hashI64

hashI64(v : i64) : i64

SplitMix64 finalizer over an i64 key (seed 0). The two multiplier
constants and the shift sequence are the canonical SplitMix64 mix —
canonical including the shift kind: >>> (zero-fill), because the
algorithm is specified over u64. Reading them as >> would be a
different, unanalysed function.

Parameters
v integer key to finalize through the SplitMix64 mix

C class SipState

SipHash-1-3 round state. Four 64-bit words, one SIPROUND method.

A class rather than four threaded locals because SIPROUND runs five
times per digest (once per message block, once for the tail, three
times to finalise) and Axle has no macro; a stack object is the
readable equivalent, and SROA dissolves it into registers exactly as
it does StringBuilder.

Constructors

TypeMethod and description
constructor(k0 : i64, k1 : i64) Seed the state from the 128-bit key. The four constants are the
ASCII of "somepseudorandomlygeneratedbytes", split into four
little-endian words, exactly as the specification gives them.

Method detail

#constructor

constructor(k0 : i64, k1 : i64)

Seed the state from the 128-bit key. The four constants are the
ASCII of "somepseudorandomlygeneratedbytes", split into four
little-endian words, exactly as the specification gives them.

Parameters
k0 low key word
k1 high key word

Methods

Method detail

#round

round(mut self) : void

#absorb

absorb(mut self, m : i64) : void

#finish

finish(mut self) : i64