Hash function & determinism guarantee
Class names produced by tl.create come from a hash function with three properties:
- Deterministic — same input string produces the same 8-character base36 output, byte for byte, on every Node version, every OS, and at runtime.
- Identical between compiler and runtime —
src/compiler/hash.tsandsrc/runtime/index.ts(_fnv32a) implement the same function. Token / theme / keyframe hashing insrc/compiler/tokens.tsis the third copy. - Low collision rate — <50% probability of any collision at 1.5M rules (birthday-paradox bound).
Definition
// src/compiler/hash.ts and src/runtime/index.ts (must match byte-for-byte)
const _H8_SPACE = 36n ** 8n;
function fnv32a(str: string): string {
let a = 0x811c9dc5 >>> 0;
let b = 0x84222325 >>> 0;
for (let i = 0; i < str.length; i++) {
const c = str.charCodeAt(i);
a = Math.imul(a ^ c, 0x01000193) >>> 0;
b = Math.imul(b ^ c, 0x05f5e101) >>> 0;
}
const combined = ((BigInt(a) << 32n) | BigInt(b)) % _H8_SPACE;
return combined.toString(36).padStart(8, "0");
}
Two parallel 32-bit FNV-1a streams (different primes: 0x01000193 and 0x05f5e101), combined to 64 bits via BigInt, reduced mod 36⁸, padded to exactly 8 chars.
Class-name composition
function classFor(prop: string, value: string, selector?: string): string {
return `tl${fnv32a(selector ? `${prop}:${value}:${selector}` : `${prop}:${value}`)}`;
}
So classFor("color", "red") = tl<hash("color:red")>, and classFor("color", "red", ":hover") = tl<hash("color:red::hover")>. The selector string includes the exact selector as written in the variant registry — _hover always means ":hover", both at compile time and runtime.
Why two parallel hashes?
A single 32-bit FNV-1a produces ~4.3B values. The birthday-paradox 50% collision threshold is at √(2 × 4.3B) ≈ 65,500 keys — uncomfortably close to a large project's CSS scale.
Combining two 32-bit hashes with different primes into a 64-bit value gives 1.8 × 10¹⁹ values, with 50% threshold at √(2 × 1.8 × 10¹⁹) ≈ 6 × 10⁹ keys. After mod 36⁸ ≈ 2.8 × 10¹², the threshold drops to ~2.4 × 10⁶ — well above any realistic project. The benchmark in bench/hash-collision.mjs empirically confirms <50% collision rate at 1.5M unique inputs.
Why Math.imul?
JavaScript multiplication is double-precision float. 0x811c9dc5 * 0x01000193 overflows 53-bit precision and produces wrong results. Math.imul(a, b) is "32-bit signed multiplication, low 32 bits" — defined exactly. Without it, the hash diverges between V8 and JavaScriptCore.
Why base36?
- Case-insensitive (CSS class names are case-sensitive in HTML, but we don't want collisions on
tlA0vstla0if a future CSS feature flag normalizes case). - Compact: 36⁸ = 2.8 × 10¹² possible values in 8 chars (vs 16⁸ = 4.3 × 10⁹ for hex).
- All characters are valid CSS identifier characters.
Why padStart to 8?
Without padding, (combined % 36⁸).toString(36) produces a variable-length string (1–8 chars). Two distinct inputs that differ only in leading zeros would collide. padStart(8, "0") makes every output exactly 8 chars.
Test invariants
Pinned by:
test/hash.test.ts— basic determinism and length.test/tokens.test.ts— token name parity between compiler and runtime.test/extractor-swc.test.ts— SWC ↔ legacy parity (both produce identical class names for identical input).
If any of these fail, the runtime fallback will produce different class names than the compiler emitted — broken styles, no error. Do not modify the hash or any built-in variant selector string without updating all three locations and re-running these tests.
Practical implications
- Stable across builds — the same source file produces the same class names today, tomorrow, and after a clean install.
- Stable across runtimes — Node 18 / 20 / 22, browser V8 / SpiderMonkey / JavaScriptCore all produce identical hashes.
- Stable across server/client — server-rendered HTML and client-rendered HTML use the same class names. No hydration mismatch from CSS.
Hash visualization (for debugging)
import { fnv32a } from "traceless-style/compiler/hash";
console.log(fnv32a("color:red")); // "<8-char string>"
console.log(`tl${fnv32a("color:red")}`); // → tl<...>
console.log(`tl${fnv32a("color:red::hover")}`); // → tl<...> (different)
If you want to confirm a class name corresponds to an expected (prop, value, selector) triplet without grepping the CSS file, this is the recipe.