feat(lang,stdlib): 64-bit long type + 64-bit Hash variants (issue #17)
Some checks are pending
docs / build-and-deploy (push) Waiting to run

Add `long`, a 64-bit signed integer primitive (i64), threaded through
codegen: llty; int<->long coercion (coerce_code/to_long) at let/assign/
return/call-args; i64 arithmetic + comparison promotion in emit_bin;
unary negate/~; print via %lld and str()/interpolation via @fn_long_str.
Editor vocabulary (syntax header, TextMate grammar, formatter, LSP)
synced; check-vocabulary green. Numeric literals stay i32 — build large
values by widening (documented on the type page).

Complete the Hash.* namespace (issue #17) with both 32- and 64-bit
algorithms: Hash.of/fnv1a/crc32/mix/combine (32-bit) and
Hash.of64/fnv1a_64/mix64 (64-bit, returning long). Deterministic and
C-free; CRC-32 (poly 0xEDB88320) and FNV vectors verified against
reference implementations.

Tests: selfhost/tests/{hash,long}.ludic. Docs: docs/language/hash/*,
type-long.md. Seed reseeded; C-free bootstrap fixpoint holds; 23
selfhost + 45 regression + 29 tooling checks green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-30 01:01:06 +03:00
parent a38195128f
commit 2002e977d9
27 changed files with 14007 additions and 12105 deletions

View file

@ -24,6 +24,8 @@ var g_uses_strslice: bool = false # `s[a..b]` was emitted -> emit the substring
var g_uses_mathrt: bool = false # Math.sqrt/sin/cos/tan was emitted -> emit the math runtime prelude
var g_uses_textrt: bool = false # Text.upper/lower/trim/repeat/pad was emitted -> emit the text builders
var g_uses_textrt2: bool = false # Text.split/join/replace was emitted -> emit the string/slice builders
var g_uses_hashrt: bool = false # Hash.of/fnv1a/crc32 was emitted -> emit the byte-stream hashers
var g_uses_longstr: bool = false # str(long) / interpolating a long was emitted -> emit fn_long_str
# loop targets for break/continue (innermost last)
var brk_lbl: []ptr
@ -70,6 +72,7 @@ fn lbl(pfx: ptr) -> ptr { let r = (pfx + itoa(ll_lbl)); ll_lbl = ll_lbl + 1; r
# slice) is a pointer; void is void.
fn llty(t: ptr) -> ptr {
if (t == "int") or (t == "bool") or (t == "fixed") or (t == "entity") { return "i32" } # entity = an i32 handle (self())
if (t == "long") { return "i64" } # a 64-bit signed integer
if (t == "byte") { return "i8" } # a single byte (p[i] on a raw ptr)
if (t == "words") or (t == "fixeds") or (t == "ptrs") { return "ptr" } # typed buffers
if (t == "void") { return "void" }

View file

@ -98,10 +98,12 @@ fn emit_program() -> void {
if g_uses_loopback { emit_loopback() } # built-in transport, after all net_send/net_poll uses are seen
if g_uses_str { emit_str_prelude() } # @fn_str_eq / @fn_str_concat, after all uses are seen
if g_uses_intstr { emit_int_str() } # @fn_int_str, for str(int) in interpolation
if g_uses_longstr { emit_long_str() } # @fn_long_str, for str(long) / long interpolation
if g_uses_strslice { emit_str_slice() } # @fn_str_slice, for s[a..b]
if g_uses_mathrt { emit_math_prelude() } # @fn_fx_sqrt / @fn_fx_sin + the sine table
if g_uses_textrt { emit_text_prelude() } # @fn_str_upper/lower/trim/repeat/pad builders
if g_uses_textrt2 { emit_text2_prelude() } # @fn_str_replace/join/split builders
if g_uses_hashrt { emit_hash_prelude() } # @fn_hash_fnv1a / @fn_hash_crc32 byte hashers
}
# Flush the emitted IR. With a null path it goes to stdout (the pipe the shell

View file

@ -56,6 +56,24 @@ fn to_fixed(v: Val) -> ptr {
return emit_bind(`shl i32 {v.code}, 16`)
}
# coerce a value's code to the LLVM type of `target`, for the only cross-width
# pair the language has: int (i32) <-> long (i64). int widens with sext, long
# narrows with trunc; everything else (same width, or ptr) passes through.
fn coerce_code(v: Val, target: ptr) -> ptr {
let lt = llty(target)
let vt = llty(v.ty)
if (lt == vt) { return v.code }
if (lt == "i64") and (vt == "i32") { return emit_bind(`sext i32 {v.code} to i64`) }
if (lt == "i32") and (vt == "i64") { return emit_bind(`trunc i64 {v.code} to i32`) }
return v.code
}
# widen an int value to i64 (a long passes through) — the long analogue of to_fixed
fn to_long(v: Val) -> ptr {
if (llty(v.ty) == "i64") { return v.code }
return emit_bind(`sext i32 {v.code} to i64`)
}
# string operators: `a + b` concatenates, `a == b` / `a != b` compare by content.
# Both call the @fn_str_* prelude (emitted once per program that uses them).
fn emit_str_op(op: ptr, a: Val, b: Val) -> Val {
@ -85,14 +103,22 @@ fn emit_bin(e: Node) -> Val {
if ((e.s == ("==")) or (e.s == ("!="))) and not isnull { return emit_str_op(e.s, a, b) }
}
let fx = (a.ty == "fixed") or (b.ty == "fixed")
# a 64-bit operand (and no fixed/ptr involved) promotes the whole expression to
# i64: the other side widens with sext, and the result stays `long`.
let lng = ((llty(a.ty) == "i64") or (llty(b.ty) == "i64")) and not fx and not ptrish
if is_cmp(e.s) {
var ac = a.code; var bc = b.code
var ct = "i32"
if fx { ac = to_fixed(a); bc = to_fixed(b) }
else { if (llty(a.ty) == "ptr") or (llty(b.ty) == "ptr") { ct = "ptr" } } # `p == null`, str/record identity
else { if lng { ct = "i64"; ac = to_long(a); bc = to_long(b) }
else { if (llty(a.ty) == "ptr") or (llty(b.ty) == "ptr") { ct = "ptr" } } } # `p == null`, str/record identity
let c = emit_bind(`icmp {cmp_code(e.s)} {ct} {ac}, {bc}`)
return val(emit_bind(`zext i1 {c} to i32`), "bool")
}
if lng {
let al = to_long(a); let bl = to_long(b)
return val(emit_bind(`{arith_code(e.s)} i64 {al}, {bl}`), "long")
}
if fx {
let af = to_fixed(a); let bf = to_fixed(b)
if (e.s == ("*")) {
@ -154,6 +180,13 @@ fn param_labels(fn: Node) -> []ptr {
return out
}
fn param_types(fn: Node) -> []ptr {
let out = new []ptr
var i = 0
while i < len(fn.kids) { if fn.kids[i].kind == N_PARAM { push(out, fn.kids[i].ty) }; i = i + 1 }
return out
}
# ---- namespaced builtins: Screen.* / Random.* / Input.* --------------------
# The game-facing API reads as `subject.action(...)`. Each method maps to a bare
# runtime builtin plus the parameter labels callers may use as named arguments;
@ -194,6 +227,10 @@ fn emit_ns_call(ns: ptr, meth: ptr, e: Node) -> Val {
if is_time_ns(meth) { return emit_time_ns(meth, e) }
perr(`unknown builtin Time.{meth}`)
}
if (ns == "Hash") {
if is_hash_ns(meth) { return emit_hash_ns(meth, e) }
perr(`unknown builtin Hash.{meth}`)
}
var bare: ptr = null
let labels = new []ptr
if (ns == "Screen") {
@ -374,16 +411,18 @@ fn emit_call(e: Node) -> Val {
}
if (name == "len") { return emit_len(e) }
if (name == "push") { return emit_push(e) }
if (name == "str") { # str(x): int/bool/fixed -> text, a string passes through
if (name == "str") { # str(x): int/bool/fixed/long -> text, a string passes through
let a = emit_expr(e.kids[0])
if (llty(a.ty) == "ptr") { return a }
if (llty(a.ty) == "i64") { g_uses_longstr = true; return val(emit_bind(`call ptr @fn_long_str(i64 {a.code})`), "str") }
g_uses_intstr = true
return val(emit_bind(`call ptr @fn_int_str(i32 {a.code})`), "str")
}
if (name == "print") { # print(x): a value + newline (int or string)
if (name == "print") { # print(x): a value + newline (string, long, or int)
let a = emit_expr(e.kids[0])
if (llty(a.ty) == "ptr") { emit(" call i32 (ptr, ...) @printf(ptr @.fmt_line, ptr " + `{a.code})\n`) }
else { emit(" call i32 (ptr, ...) @printf(ptr @.fmt_int, i32 " + `{a.code})\n`) }
else { if (llty(a.ty) == "i64") { emit(" call i32 (ptr, ...) @printf(ptr @.fmt_long, i64 " + `{a.code})\n`) }
else { emit(" call i32 (ptr, ...) @printf(ptr @.fmt_int, i32 " + `{a.code})\n`) } }
return val("0", "void")
}
if (name == "bytes") { # bytes(n): allocate n bytes -> a byte buffer
@ -459,11 +498,18 @@ fn emit_call(e: Node) -> Val {
cname = rtname
}
reorder_named(e, param_labels(fn2))
# evaluate args first (their IR is emitted before the call instruction)
# evaluate args first (their IR is emitted before the call instruction), coercing
# each to the parameter's declared type so an int passed for a `long` widens.
let ptys = param_types(fn2)
let args = new []ptr
let atys = new []ptr
var i = 0
while i < len(e.kids) { let v = emit_expr(e.kids[i]); push(args, v.code); push(atys, v.ty); i = i + 1 }
while i < len(e.kids) {
let v = emit_expr(e.kids[i])
var pty = v.ty
if (i < len(ptys)) { pty = ptys[i] }
push(args, coerce_code(v, pty)); push(atys, pty); i = i + 1
}
let rl = llty(fn2.ty)
emit(" ")
var rreg = "0"
@ -540,6 +586,10 @@ fn emit_expr(e: Node) -> Val {
if e.kind == E_BIN { return emit_bin(e) }
if e.kind == E_UN {
let a = emit_expr(e.a)
if (llty(a.ty) == "i64") { # negate / bit-flip a long, staying 64-bit
if (e.s == ("-")) { return val(emit_bind(`sub i64 0, {a.code}`), "long") }
if (e.s == "~") { return val(emit_bind(`xor i64 {a.code}, -1`), "long") }
}
if (e.s == ("-")) { return val(emit_bind(`sub i32 0, {a.code}`), "int") }
if (e.s == "~") { return val(emit_bind(`xor i32 {a.code}, -1`), "int") }
let c = emit_bind(`icmp eq i32 {a.code}, 0`)

195
selfhost/emit_hash.ludic Normal file
View file

@ -0,0 +1,195 @@
# 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)
fn is_hash_ns(meth: ptr) -> 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.
fn hash_mix_code(x: ptr) -> ptr {
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.
fn hash_mix64_code(x: ptr) -> ptr {
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}`)
}
fn emit_hash_ns(meth: ptr, 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: ptr = "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.
fn 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")
}

View file

@ -66,6 +66,7 @@ fn emit_header() -> void {
emith("@__stderrp = external global ptr\n")
emith("@__stdoutp = external global ptr\n")
emith("@.fmt_int = private unnamed_addr constant [4 x i8] c\"%d\\0A\\00\"\n")
emith("@.fmt_long = private unnamed_addr constant [6 x i8] c\"%lld\\0A\\00\"\n")
emith("@.fmt_line = private unnamed_addr constant [4 x i8] c\"%s\\0A\\00\"\n")
emith("@L_argc = internal global i32 0\n")
emith("@L_argv = internal global ptr null\n")
@ -192,6 +193,29 @@ fn emit_int_str() -> void {
emith(" %rpos = add i32 %fpos, 1\n %res = getelementptr inbounds i8, ptr %buf, i32 %rpos\n ret ptr %res\n}\n")
}
# the i64 twin of fn_int_str: a signed 64-bit integer -> decimal text. Emitted
# once per program that stringifies a `long` (g_uses_longstr). A 64-bit value is
# at most 20 digits plus sign and NUL, so the 24-byte scratch buffer still fits.
fn emit_long_str() -> void {
emith("define ptr @fn_long_str(i64 %n0) {\n")
emith("entry:\n %buf = call ptr @malloc(i64 24)\n")
emith(" %isneg = icmp slt i64 %n0, 0\n %neg = sub i64 0, %n0\n")
emith(" %n = select i1 %isneg, i64 %neg, i64 %n0\n")
emith(" %term = getelementptr inbounds i8, ptr %buf, i32 23\n store i8 0, ptr %term\n")
emith(" %iszero = icmp eq i64 %n0, 0\n br i1 %iszero, label %zc, label %dl\n")
emith("zc:\n %zp = getelementptr inbounds i8, ptr %buf, i32 22\n store i8 48, ptr %zp\n ret ptr %zp\n")
emith("dl:\n br label %dloop\n")
emith("dloop:\n %pos = phi i32 [ 22, %dl ], [ %pos2, %dbody ]\n %cur = phi i64 [ %n, %dl ], [ %cur2, %dbody ]\n")
emith(" %done = icmp eq i64 %cur, 0\n br i1 %done, label %sign, label %dbody\n")
emith("dbody:\n %d = urem i64 %cur, 10\n %d32 = trunc i64 %d to i32\n %ch = add i32 %d32, 48\n %ch8 = trunc i32 %ch to i8\n")
emith(" %pp = getelementptr inbounds i8, ptr %buf, i32 %pos\n store i8 %ch8, ptr %pp\n")
emith(" %cur2 = udiv i64 %cur, 10\n %pos2 = sub i32 %pos, 1\n br label %dloop\n")
emith("sign:\n br i1 %isneg, label %addneg, label %fin\n")
emith("addneg:\n %sp = getelementptr inbounds i8, ptr %buf, i32 %pos\n store i8 45, ptr %sp\n %posn = sub i32 %pos, 1\n br label %fin\n")
emith("fin:\n %fpos = phi i32 [ %pos, %sign ], [ %posn, %addneg ]\n")
emith(" %rpos = add i32 %fpos, 1\n %res = getelementptr inbounds i8, ptr %buf, i32 %rpos\n ret ptr %res\n}\n")
}
# s[a..b] -> a fresh NUL-terminated copy of the bytes [a, b), emitted (once) into
# any program that slices a string. Mallocs (b-a)+1, copies, terminates.
fn emit_str_slice() -> void {

View file

@ -40,16 +40,16 @@ fn emit_assign(st: Node) -> void {
}
let lt = llty(ty)
let rv = emit_expr(st.b)
var v = rv.code
var v = coerce_code(rv, ty)
if (lt == "i8") { v = emit_bind(`trunc i32 {v} to i8`) } # narrow to a byte for p[i] = v
if not (st.s == "=") {
let cur = emit_bind(`load {lt}, ptr {addr}`)
var opc = "add"
if (st.s == ("-=")) { opc = "sub" }
if (st.s == ("*=")) { opc = "mul" }
if (st.s == ("/=")) { opc = "sdiv" }
v = emit_bind(`{opc} i32 {cur}, {v}`)
v = emit_bind(`{opc} {lt} {cur}, {v}`)
}
if (lt == "i8") { v = emit_bind(`trunc i32 {v} to i8`) } # narrow to a byte for p[i] = v
store_at(lt, v, addr)
}
@ -115,7 +115,7 @@ fn emit_for(st: Node) -> void {
fn emit_return(st: Node) -> void {
if (st.a != null) {
let v = emit_expr(st.a)
store_at(llty(ret_ty), v.code, "%retval")
store_at(llty(ret_ty), coerce_code(v, ret_ty), "%retval")
}
emit(" br label %ret\n")
g_term = true
@ -231,7 +231,7 @@ fn emit_stmt(st: Node) -> void {
var z = "0"
if (llty(ty) == "ptr") { z = "null" }
store_at(llty(ty), z, slot)
} else { let v = emit_expr(st.a); store_at(llty(ty), v.code, slot) }
} else { let v = emit_expr(st.a); store_at(llty(ty), coerce_code(v, ty), slot) }
loc_push(st.s, slot, ty)
loc_set_mut(st.ival)
return

File diff suppressed because it is too large Load diff

35
selfhost/tests/hash.ludic Normal file
View file

@ -0,0 +1,35 @@
program T {
entry {
# ---- Hash.* — deterministic non-cryptographic hashing ----
# FNV-1a 32-bit (printed as a signed i32).
print(Hash.fnv1a("")) # 0x811c9dc5 -> -2128831035
print(Hash.fnv1a("a")) # -468965076
print(Hash.fnv1a("grass_tile")) # 114400290
print(Hash.of("grass_tile")) # Hash.of is the FNV-1a default -> same
# CRC-32 (IEEE) — known-answer check vectors.
print(Hash.crc32("")) # 0
print(Hash.crc32("123456789")) # 0xCBF43926 -> -873187034
print(Hash.crc32("The quick brown fox jumps over the lazy dog")) # 1095738169
# fmix32 avalanche of a single int.
print(Hash.mix(0)) # 0
print(Hash.mix(1)) # 1364076727
print(Hash.mix(-1)) # -2114883783
# combine folds several ints into one seed: deterministic + order-sensitive.
print(Hash.combine(123, 456)) # -845898438
print(Hash.combine(123, 456)) # same input -> same hash
print(Hash.combine(1, 2, 3)) # -78065325
print(Hash.combine(3, 2, 1)) # -78057399 (order matters)
# ---- 64-bit variants (return `long`) ----
print(Hash.fnv1a_64("")) # 0xcbf29ce484222325 -> -3750763034362895579
print(Hash.fnv1a_64("a")) # 0xaf63dc4c8601ec8c -> -5808556873153909620
print(Hash.fnv1a_64("grass_tile")) # -4100651535478758590
print(Hash.of64("grass_tile")) # Hash.of64 is the 64-bit default -> same
print(Hash.mix64(0)) # 0
print(Hash.mix64(1)) # -5451962507482445012
print(Hash.mix64(-1)) # 7256831767414464289
}
}

23
selfhost/tests/long.ludic Normal file
View file

@ -0,0 +1,23 @@
program T {
fn wide(a: long, b: int) -> long { return a * b } # int arg widens to i64
entry {
let big: long = 1000000 # int literal widens to a long
let sq = big * big # i64 multiply -> 10^12, no overflow
print(sq) # 1000000000000
print(sq + 1) # 1000000000001
var acc: long = 0
acc += sq
acc += 5 # compound assign in i64
print(acc) # 1000000000005
print(wide(big, 3000000)) # 3000000000000 (> i32 max)
let x: long = big * 3
let y = big * 2 # inferred long
print(x > y) # 1
print(y < x) # 1
print(x == x) # 1
print(0 - sq) # -1000000000000 (unary negate stays 64-bit)
print(big) # 1000000
print(Text.length(str(sq))) # 13 — str(long) renders all 13 digits
}
}