#[unsafe(no_mangle)]pub unsafe extern "C" fn nova_rt_str_hash(s: *const NovaStr) -> i64Expand description
Seeded, finalized hash of a Nova string’s bytes, as an i64.
std/core’s impl Hash for String reaches this through the str_hash
builtin. An earlier version of this comment justified that by saying Nova
cannot walk a string’s bytes itself, on the grounds that String has no
length, indexing or iteration and is not FFI-safe. The FFI-safety half is
true. The byte-walking half was true when written and is now stale,
by two separate routes rather than the one an earlier draft of this
paragraph named. The byte and char BUILTINS – str_chars,
bytes_from_string_intrinsic, bytes_at – are Builtin::STD_ONLY
members, and the resolver seeds every member of that array into every std
module’s scope, which is how std/core comes to walk a string at its own
impl Debug for String. Separately, std/bytes and std/strings are
STD_MODULES entries whose public items reach other modules through
import_std_module. STD_ONLY holds builtins, not modules; conflating
the two is what that earlier draft did.
The reason that survives does not rest on impossibility: the hash lives in
one place, and reimplementing FNV-1a in Nova would pin a second copy of it
free to diverge from this one. ADR 0005’s 2026-08-27 amendment records the
same correction against the same sentence.
Seeded FNV-1a over the bytes, then splitmix64’s finalizer. The seed is
per-process (see str_hash_seed) and the finalizer is what makes the seed
reach the low bits Map selects buckets from; the reasoning and the
measurements are at splitmix64_finalize. This used to be unseeded FNV-1a
alone, described here as “adequate for a hash map’s bucket selection” –
true of an honest workload and false of an adversarial one, which is why it
changed.
STILL NOT COLLISION-RESISTANT, and still not for anything
security-sensitive. What the seed and finalizer buy is resistance to a
PRECOMPUTED collision set: the seed is drawn per process, so a colliding key
set cannot be built offline against a process that has not started yet, and
recovering the seed means first obtaining a hash from the running process.
This paragraph used to close “mix64 – the Int, Bool and Char impls
– is not seeded at all.” That is no longer true: those impls XOR
nova_rt_int_hash_seed’s separate per-process draw into mix64’s input,
so they carry the same precomputation resistance and the same recoverable
seed as this one does. mix64 itself is still unseeded, which is a
different statement – the seeding is in the impls that call it.
An adversary who can observe the process and adapt is OUT OF SCOPE, and the
route is concrete rather than a category: one string hash read out of this
process recovers the seed, because ("").hash() is the finalizer applied to
the seed alone (see str_hash_seed), so an attacker who can obtain any
String hash from a running process can construct collisions for it. The
channel is ADR 0005’s one-shot fn hash(self) -> Int returning a whole
64-bit result in one call, which predates the seeding: seeding did not open
the channel, it made the value behind it worth recovering. Precomputation
resistance is the half that survives.
The u64 -> i64 reinterpretation at the end means the result may be
negative, so a caller selecting buckets must mask (hash & (cap - 1),
which is non-negative for any i64) rather than take a remainder.
§Safety
s must point to a valid NovaStr.