Split the flat 38-file selfhost/ into concern-based subdirectories:
frontend/ lex, parse, parse_game, ast
support/ str, buf, io
backend/ core IR + expression/statement lowering
backend/game/ ECS/scene/event/world lowering
backend/stdlib/ the namespaced Math.*/Text.*/Crypto.*/… intrinsics
and split the three oversized emitters at responsibility boundaries so
no file mixes concerns:
emit_game.ludic -> + emit_world.ludic (reflection world table,
tick helpers, @main synthesis)
emit_expr.ludic -> + emit_call.ludic (namespaced builtins, call
lowering, expr dispatch)
emit_text.ludic -> + emit_text_prelude.ludic (emitted string-builder runtime)
FRAGS in tools/x/selfhost.ludic is updated to the new paths with the link
order preserved, and the Python doc/vocabulary tooling is updated to walk
the new layout. Because the build is a plain in-order concatenation and
every split lands on a blank-line boundary, the regenerated seed is
byte-identical: `x reseed` leaves selfhost/ludicc.seed.ll unchanged,
`x bootstrap-cfree` still reaches its fixed point, and both `x test` (56)
and `x selfhost-test` (29, incl. golden renders) stay green.
Closes #29
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
195 lines
8.1 KiB
Text
195 lines
8.1 KiB
Text
# emit_hash.ludic — the Hash.* namespace: fast, non-cryptographic hashing for
|
|
# map keys, content IDs, deterministic seeds and checksums. Everything is plain
|
|
# 32-bit integer IR with defined constants and byte order, so a given input
|
|
# hashes to the same value on every platform and every run — which is what makes
|
|
# it safe for procedural generation and lockstep networking. NOT for passwords
|
|
# or tamper-proofing; point users at the Crypto library for that.
|
|
#
|
|
# Hash.of(s) fast default string hash (currently FNV-1a 32)
|
|
# Hash.fnv1a(s) FNV-1a 32-bit, named explicitly
|
|
# Hash.crc32(s) CRC-32 (IEEE 802.3) checksum, for corruption detection
|
|
# Hash.mix(x) fmix32 avalanche of a single int (turn a counter into a seed)
|
|
# Hash.combine(...) fold several ints into one (e.g. world_seed, cx, cy)
|
|
|
|
function is_hash_ns(meth: pointer) -> bool {
|
|
if (meth == "of") or (meth == "fnv1a") or (meth == "crc32") { return true }
|
|
if (meth == "mix") or (meth == "combine") { return true }
|
|
if (meth == "of64") or (meth == "fnv1a_64") or (meth == "mix64") { return true }
|
|
return false
|
|
}
|
|
|
|
# fmix32 (MurmurHash3 finalizer) of a single i32 -> code of an i32. A strong
|
|
# avalanche: flips ~half the output bits for any one input bit. Used on its own
|
|
# (Hash.mix) and nowhere else — combine has its own mixing step.
|
|
function hash_mix_code(x: pointer) -> pointer {
|
|
let a = emit_bind(`lshr i32 {x}, 16`)
|
|
let b = emit_bind(`xor i32 {x}, {a}`)
|
|
let c = emit_bind(`mul i32 {b}, -2048144789`) # * 0x85ebca6b
|
|
let d = emit_bind(`lshr i32 {c}, 13`)
|
|
let e = emit_bind(`xor i32 {c}, {d}`)
|
|
let f = emit_bind(`mul i32 {e}, -1028477387`) # * 0xc2b2ae35
|
|
let g = emit_bind(`lshr i32 {f}, 16`)
|
|
return emit_bind(`xor i32 {f}, {g}`)
|
|
}
|
|
|
|
# fmix64 (MurmurHash3 64-bit finalizer) of a single i64 -> code of an i64. The
|
|
# 64-bit twin of hash_mix_code: shift by 33 and multiply by the two 64-bit
|
|
# constants. Backs Hash.mix64.
|
|
function hash_mix64_code(x: pointer) -> pointer {
|
|
let a = emit_bind(`lshr i64 {x}, 33`)
|
|
let b = emit_bind(`xor i64 {x}, {a}`)
|
|
let c = emit_bind(`mul i64 {b}, -49064778989728563`) # * 0xff51afd7ed558ccd
|
|
let d = emit_bind(`lshr i64 {c}, 33`)
|
|
let e = emit_bind(`xor i64 {c}, {d}`)
|
|
let f = emit_bind(`mul i64 {e}, -4265267296055464877`) # * 0xc4ceb9fe1a85ec53
|
|
let g = emit_bind(`lshr i64 {f}, 33`)
|
|
return emit_bind(`xor i64 {f}, {g}`)
|
|
}
|
|
|
|
function emit_hash_ns(meth: pointer, e: Node) -> Val {
|
|
if (meth == "of") or (meth == "fnv1a") { # FNV-1a 32-bit over the bytes
|
|
g_uses_hashrt = true
|
|
let s = emit_expr(e.kids[0])
|
|
return val(emit_bind(`call i32 @fn_hash_fnv1a(ptr {s.code})`), "int")
|
|
}
|
|
if (meth == "of64") or (meth == "fnv1a_64") { # FNV-1a 64-bit -> a `long`
|
|
g_uses_hashrt = true
|
|
let s = emit_expr(e.kids[0])
|
|
return val(emit_bind(`call i64 @fn_hash_fnv1a_64(ptr {s.code})`), "long")
|
|
}
|
|
if (meth == "mix64") { # fmix64 avalanche of one long
|
|
let x = emit_expr(e.kids[0])
|
|
return val(hash_mix64_code(to_long(x)), "long")
|
|
}
|
|
if (meth == "crc32") { # CRC-32 (IEEE) checksum
|
|
g_uses_hashrt = true
|
|
let s = emit_expr(e.kids[0])
|
|
return val(emit_bind(`call i32 @fn_hash_crc32(ptr {s.code})`), "int")
|
|
}
|
|
if (meth == "mix") { # fmix32 avalanche of one int
|
|
let x = emit_expr(e.kids[0])
|
|
return val(hash_mix_code(x.code), "int")
|
|
}
|
|
# combine(a, b, ...) -> fold the boost hash_combine step over every argument:
|
|
# seed = seed ^ (v + 0x9e3779b9 + (seed << 6) + (seed >> 2))
|
|
# order-sensitive and deterministic; seed starts at 0 so a single argument is
|
|
# still well-mixed with the golden-ratio constant.
|
|
var seed: pointer = "0"
|
|
var i = 0
|
|
while i < len(e.kids) {
|
|
let v = emit_expr(e.kids[i])
|
|
let g = emit_bind(`add i32 {v.code}, -1640531527`) # + 0x9e3779b9
|
|
let sl = emit_bind(`shl i32 {seed}, 6`)
|
|
let sr = emit_bind(`lshr i32 {seed}, 2`)
|
|
let t1 = emit_bind(`add i32 {g}, {sl}`)
|
|
let t2 = emit_bind(`add i32 {t1}, {sr}`)
|
|
seed = emit_bind(`xor i32 {seed}, {t2}`)
|
|
i = i + 1
|
|
}
|
|
return val(seed, "int")
|
|
}
|
|
|
|
# emit_hash_prelude — the byte-stream hashers, emitted once per program that uses
|
|
# Hash.of/fnv1a/crc32 (g_uses_hashrt). Both walk a null-terminated string a byte
|
|
# at a time with pure integer IR: FNV-1a with the standard 32-bit offset basis /
|
|
# prime, and a bitwise CRC-32 with the reflected poly 0xEDB88320. No libc, no
|
|
# allocation, bit-identical on every target.
|
|
function emit_hash_prelude() -> void {
|
|
emith("define i32 @fn_hash_fnv1a(ptr %s) {\n")
|
|
emith("entry:\n")
|
|
emith(" %hp = alloca i32\n")
|
|
emith(" store i32 -2128831035, ptr %hp\n") # 0x811c9dc5 offset basis
|
|
emith(" %ip = alloca i64\n")
|
|
emith(" store i64 0, ptr %ip\n")
|
|
emith(" br label %cond\n")
|
|
emith("cond:\n")
|
|
emith(" %i = load i64, ptr %ip\n")
|
|
emith(" %p = getelementptr i8, ptr %s, i64 %i\n")
|
|
emith(" %c = load i8, ptr %p\n")
|
|
emith(" %z = icmp eq i8 %c, 0\n")
|
|
emith(" br i1 %z, label %done, label %body\n")
|
|
emith("body:\n")
|
|
emith(" %ce = zext i8 %c to i32\n")
|
|
emith(" %h = load i32, ptr %hp\n")
|
|
emith(" %x = xor i32 %h, %ce\n")
|
|
emith(" %m = mul i32 %x, 16777619\n") # * 0x01000193 prime
|
|
emith(" store i32 %m, ptr %hp\n")
|
|
emith(" %i1 = add i64 %i, 1\n")
|
|
emith(" store i64 %i1, ptr %ip\n")
|
|
emith(" br label %cond\n")
|
|
emith("done:\n")
|
|
emith(" %hr = load i32, ptr %hp\n")
|
|
emith(" ret i32 %hr\n")
|
|
emith("}\n")
|
|
emith("define i64 @fn_hash_fnv1a_64(ptr %s) {\n") # 64-bit FNV-1a, same shape, i64
|
|
emith("entry:\n")
|
|
emith(" %hp = alloca i64\n")
|
|
emith(" store i64 -3750763034362895579, ptr %hp\n") # 0xcbf29ce484222325 offset basis
|
|
emith(" %ip = alloca i64\n")
|
|
emith(" store i64 0, ptr %ip\n")
|
|
emith(" br label %cond\n")
|
|
emith("cond:\n")
|
|
emith(" %i = load i64, ptr %ip\n")
|
|
emith(" %p = getelementptr i8, ptr %s, i64 %i\n")
|
|
emith(" %c = load i8, ptr %p\n")
|
|
emith(" %z = icmp eq i8 %c, 0\n")
|
|
emith(" br i1 %z, label %done, label %body\n")
|
|
emith("body:\n")
|
|
emith(" %ce = zext i8 %c to i64\n")
|
|
emith(" %h = load i64, ptr %hp\n")
|
|
emith(" %x = xor i64 %h, %ce\n")
|
|
emith(" %m = mul i64 %x, 1099511628211\n") # * 0x100000001b3 prime
|
|
emith(" store i64 %m, ptr %hp\n")
|
|
emith(" %i1 = add i64 %i, 1\n")
|
|
emith(" store i64 %i1, ptr %ip\n")
|
|
emith(" br label %cond\n")
|
|
emith("done:\n")
|
|
emith(" %hr = load i64, ptr %hp\n")
|
|
emith(" ret i64 %hr\n")
|
|
emith("}\n")
|
|
emith("define i32 @fn_hash_crc32(ptr %s) {\n")
|
|
emith("entry:\n")
|
|
emith(" %cp = alloca i32\n")
|
|
emith(" store i32 -1, ptr %cp\n") # init 0xFFFFFFFF
|
|
emith(" %ip = alloca i64\n")
|
|
emith(" store i64 0, ptr %ip\n")
|
|
emith(" %kp = alloca i32\n")
|
|
emith(" br label %cond\n")
|
|
emith("cond:\n")
|
|
emith(" %i = load i64, ptr %ip\n")
|
|
emith(" %p = getelementptr i8, ptr %s, i64 %i\n")
|
|
emith(" %ch = load i8, ptr %p\n")
|
|
emith(" %z = icmp eq i8 %ch, 0\n")
|
|
emith(" br i1 %z, label %done, label %body\n")
|
|
emith("body:\n")
|
|
emith(" %ce = zext i8 %ch to i32\n")
|
|
emith(" %c0 = load i32, ptr %cp\n")
|
|
emith(" %cx = xor i32 %c0, %ce\n") # fold byte into low 8 bits
|
|
emith(" store i32 %cx, ptr %cp\n")
|
|
emith(" store i32 0, ptr %kp\n")
|
|
emith(" br label %bit\n")
|
|
emith("bit:\n")
|
|
emith(" %k = load i32, ptr %kp\n")
|
|
emith(" %kd = icmp slt i32 %k, 8\n")
|
|
emith(" br i1 %kd, label %bitbody, label %bitdone\n")
|
|
emith("bitbody:\n")
|
|
emith(" %c1 = load i32, ptr %cp\n")
|
|
emith(" %lb = and i32 %c1, 1\n")
|
|
emith(" %ls = lshr i32 %c1, 1\n")
|
|
emith(" %ism = icmp eq i32 %lb, 1\n")
|
|
emith(" %px = select i1 %ism, i32 -306674912, i32 0\n") # ^ 0xEDB88320 when LSB set
|
|
emith(" %c2 = xor i32 %ls, %px\n")
|
|
emith(" store i32 %c2, ptr %cp\n")
|
|
emith(" %k1 = add i32 %k, 1\n")
|
|
emith(" store i32 %k1, ptr %kp\n")
|
|
emith(" br label %bit\n")
|
|
emith("bitdone:\n")
|
|
emith(" %i1 = add i64 %i, 1\n")
|
|
emith(" store i64 %i1, ptr %ip\n")
|
|
emith(" br label %cond\n")
|
|
emith("done:\n")
|
|
emith(" %cf = load i32, ptr %cp\n")
|
|
emith(" %r = xor i32 %cf, -1\n") # final XOR 0xFFFFFFFF
|
|
emith(" ret i32 %r\n")
|
|
emith("}\n")
|
|
}
|