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
| Type | Method and description |
|---|---|
| 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 patternof h is byte-for-byte identical to the canonical u64 FNV-1astate. |
| djb2Bytes(s : string) : i64 djb2 — Daniel J. Bernstein's hash. Seed 5381, multiplier 33. |
| hashCombine(a : i64, b : i64) : i64 Boost's hash_combine — mixes two i64 hashes into one.h = a ^ (b + 0x9e3779b9 + (a << 6) + (a >> 2)) |
| __rotl64(x : i64, n : i64) : i64 Rotate x left by n, 0 < n < 64. Undefined at n == 0 — everycall site here passes a literal in {13, 16, 17, 21, 32}. |
| hashStringSeeded(s : string, k0 : i64, k1 : i64) : i64 SwissTable's HashMap<string, V> hash backend — SipHash-1-3, akeyed 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 abijection, so hash(s1) == hash(s2) held exactly whenfnv(k0, s1) == fnv(k0, s2): every one of the finaliser's rounds ranafter 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 parityagree 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 per8-byte block and d = 3 finalisation rounds is the fast variant, andthe 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 andthe 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. |
| hashString(s : string) : i64 Deterministic public hash entry-points. hashString(s) is thekey-0 keyed mix (stable across runs — usable as a content-hash by callers like strmap_intensive). hashI64(v) is SplitMix64with seed 0 (the same mix axle_codegen::builtins::hash inlinesat 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 seededvariant ( hash_key builtin routes through hashStringSeededwith @axle_hash_seed) and is HashDoS-safe ; user code thathand-rolls a hash table on top of this entry point must wrap it with its own seed. |
| hashI64(v : i64) : i64 SplitMix64 finalizer over an i64 key (seed 0). The two multiplierconstants and the shift sequence are the canonical SplitMix64 mix — canonical including the shift kind: >>> (zero-fill), because thealgorithm is specified over u64. Reading them as >> would be adifferent, unanalysed function. |
Method detail
#fnv1aBytes
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.
s bytes to hash#djb2Bytes
djb2 — Daniel J. Bernstein's hash. Seed 5381, multiplier 33.
s bytes to hash#hashCombine
Boost's hash_combine — mixes two i64 hashes into one.
h = a ^ (b + 0x9e3779b9 + (a << 6) + (a >> 2))
a accumulator / first hash to mixb second hash to fold into a#__rotl64
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}.
x value to rotaten rotation distance, strictly between 0 and 64#hashStringSeeded
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 whenfnv(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 exactlyk0_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.
s key bytes to hashk0 low key wordk1 high key word#hashString
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.
s bytes to hash deterministically (key 0)#hashI64
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.
v integer key to finalize through the SplitMix64 mixC 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
| Type | Method 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
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.
k0 low key wordk1 high key wordMethods
| Type | Method and description |
|---|---|
| round(mut self) : void |
| absorb(mut self, m : i64) : void |
| finish(mut self) : i64 |