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

@ -0,0 +1,9 @@
---
id: hash
title: Hash
order: 6
---
Fast, non-cryptographic hashing for everyday game needs: turning string IDs into integer handles, mixing a few numbers into one deterministic seed, and checksumming data to catch corruption. Every function is plain 32-bit integer arithmetic with a defined byte order and fixed constants, so a given input hashes to exactly the same value on every platform and every run — the guarantee that makes it safe for procedural generation and lockstep networking. Results are 32-bit and print as signed integers. Arguments are positional.
Not for security. These hashes are fast and reversible — never use them for passwords, tokens, or tamper-proofing. For secure hashing reach for the cryptography library instead.

View file

@ -0,0 +1,30 @@
---
id: hash-combine
name: Hash.combine
category: hash
kind: namespace-method
tokens: Hash.combine
sig: Hash.combine(...) -> int
tip: Fold several ints into one deterministic seed.
order: 5
ns: Hash
member: combine
---
Mixes any number of integers into a single 32-bit value. It folds the arguments left to right with the classic hash-combine step (each value is spread with the golden-ratio constant <code>0x9e3779b9</code> and stirred into the running seed), so the result depends on every input and on their order — <code>Hash.combine(1, 2, 3)</code> and <code>Hash.combine(3, 2, 1)</code> differ. The whole point is a per-thing deterministic seed: hash a world seed with a chunk's coordinates to drive procedural generation, so the same chunk always regenerates identically. Pair it with the noise and random libraries, which take a seed.
Parameters:
- `...` — the integers to combine (one or more)
```ludic
program ChunkSeed {
entry {
let world_seed = 1337
let cx = 4
let cy = -2
let seed = Hash.combine(world_seed, cx, cy) # this chunk's stable seed
print(seed)
print(Hash.combine(world_seed, cx, cy) == seed) # 1 — reproducible
}
}
```

View file

@ -0,0 +1,28 @@
---
id: hash-crc32
name: Hash.crc32
category: hash
kind: namespace-method
tokens: Hash.crc32
sig: Hash.crc32(s) -> int
tip: CRC-32 checksum for corruption detection.
order: 3
ns: Hash
member: crc32
---
The <a href="https://en.wikipedia.org/wiki/Cyclic_redundancy_check">CRC-32</a> checksum of a string's bytes — the standard IEEE 802.3 variant (reflected polynomial <code>0xEDB88320</code>), the same one used by zip, gzip and PNG. Its well-known check value: <code>Hash.crc32("123456789")</code> is <code>0xCBF43926</code>. Use it to catch accidental corruption in a save file, downloaded asset, or config blob: store the checksum alongside the data and compare on load. It detects damage, not tampering — an attacker can trivially forge a matching CRC, so it is not a security check (see the section note).
Parameters:
- `s` — the string (bytes) to checksum
```ludic
program Checksum {
entry {
let data = "save:level=3;coins=120"
let ck = Hash.crc32(data) # store this next to the save; recompute on load
print(ck)
print(Hash.crc32(data) == ck) # 1 — unchanged data checks out
}
}
```

View file

@ -0,0 +1,26 @@
---
id: hash-fnv1a
name: Hash.fnv1a
category: hash
kind: namespace-method
tokens: Hash.fnv1a
sig: Hash.fnv1a(s) -> int
tip: FNV-1a 32-bit, named explicitly.
order: 2
ns: Hash
member: fnv1a
---
The <a href="https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function">FNV-1a</a> hash of a string's bytes, as an explicit 32-bit algorithm. It starts from the standard offset basis <code>0x811c9dc5</code> and, for each byte, XORs the byte in and multiplies by the FNV prime <code>0x01000193</code>. Small, allocation-free, and well-distributed for short strings — ideal for map keys and content ids. This is the same function <a href="hash-of"><code>Hash.of</code></a> currently delegates to; call <code>Hash.fnv1a</code> when you want to pin the algorithm by name so the value stays fixed even if the default changes.
Parameters:
- `s` — the string to hash
```ludic
program Fnv {
entry {
print(Hash.fnv1a("")) # the empty string -> the offset basis
print(Hash.fnv1a("grass_tile"))
}
}
```

View file

@ -0,0 +1,26 @@
---
id: hash-fnv1a_64
name: Hash.fnv1a_64
category: hash
kind: namespace-method
tokens: Hash.fnv1a_64
sig: Hash.fnv1a_64(s) -> long
tip: FNV-1a 64-bit, named explicitly.
order: 7
ns: Hash
member: fnv1a_64
---
The 64-bit <a href="https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function">FNV-1a</a> hash of a string's bytes, returned as a <code>long</code>. It uses the standard 64-bit offset basis <code>0xcbf29ce484222325</code> and prime <code>0x100000001b3</code>: for each byte, XOR it in and multiply by the prime. Its wider output makes accidental collisions vanishingly unlikely for the id and registry use cases games hit. This is the function <a href="hash-of64"><code>Hash.of64</code></a> delegates to; call <code>Hash.fnv1a_64</code> when you want the algorithm pinned by name.
Parameters:
- `s` — the string to hash
```ludic
program Fnv64 {
entry {
print(Hash.fnv1a_64("")) # the empty string -> the 64-bit offset basis
print(Hash.fnv1a_64("grass_tile"))
}
}
```

View file

@ -0,0 +1,29 @@
---
id: hash-mix
name: Hash.mix
category: hash
kind: namespace-method
tokens: Hash.mix
sig: Hash.mix(x) -> int
tip: Avalanche one integer into a well-scrambled value.
order: 4
ns: Hash
member: mix
---
Scrambles a single integer so its bits are thoroughly mixed — the <a href="https://en.wikipedia.org/wiki/MurmurHash">MurmurHash3</a> <code>fmix32</code> finalizer (shift/multiply/shift). Nearby inputs like <code>0</code>, <code>1</code>, <code>2</code> map to values that look unrelated, which is exactly what you want when turning a counter, entity id, or frame number into a random-looking seed. It is deterministic and self-inverse-free: the same input always gives the same output. <code>Hash.mix(0)</code> is <code>0</code>. For folding several numbers together use <a href="hash-combine"><code>Hash.combine</code></a>.
Parameters:
- `x` — the integer to avalanche
```ludic
program Seeds {
entry {
var i = 0
while i < 4 {
print(Hash.mix(i)) # consecutive counters -> unrelated-looking seeds
i = i + 1
}
}
}
```

View file

@ -0,0 +1,27 @@
---
id: hash-mix64
name: Hash.mix64
category: hash
kind: namespace-method
tokens: Hash.mix64
sig: Hash.mix64(x) -> long
tip: Avalanche one 64-bit integer into a well-scrambled value.
order: 8
ns: Hash
member: mix64
---
The 64-bit counterpart of <a href="hash-mix"><code>Hash.mix</code></a>: the <a href="https://en.wikipedia.org/wiki/MurmurHash">MurmurHash3</a> <code>fmix64</code> finalizer (shift by 33, multiply, repeat). It thoroughly scrambles a <code>long</code>, so consecutive or structured inputs map to unrelated-looking outputs — ideal for turning a large counter, packed coordinate, or 64-bit id into a random-looking seed. An <code>int</code> argument widens to 64 bits first. Deterministic; <code>Hash.mix64(0)</code> is <code>0</code>.
Parameters:
- `x` — the integer to avalanche (widened to `long`)
```ludic
program Seeds64 {
entry {
let base: long = Hash.of64("chunk")
print(Hash.mix64(base))
print(Hash.mix64(base + 1)) # a neighbouring input -> an unrelated seed
}
}
```

View file

@ -0,0 +1,28 @@
---
id: hash-of
name: Hash.of
category: hash
kind: namespace-method
tokens: Hash.of
sig: Hash.of(s) -> int
tip: The fast default string hash.
order: 1
ns: Hash
member: of
---
The go-to hash for a string: turns text like <code>"grass_tile"</code> into a fast, stable 32-bit integer handle. <code>Hash.of</code> is the default when you don't care which algorithm is used — currently it is <a href="hash-fnv1a"><code>Hash.fnv1a</code></a>, FNV-1a over the bytes. Reach for it to key a lookup table by name, give content a compact id, or detect that a config string changed. The result is deterministic: the same string always produces the same value, on every platform and run. It is not for security — see the section note.
Parameters:
- `s` — the string to hash
```ludic
program TileIds {
entry {
let grass = Hash.of("grass_tile") # a stable integer id for this tile name
let water = Hash.of("water_tile")
print(grass)
print(water)
}
}
```

View file

@ -0,0 +1,28 @@
---
id: hash-of64
name: Hash.of64
category: hash
kind: namespace-method
tokens: Hash.of64
sig: Hash.of64(s) -> long
tip: The fast default 64-bit string hash.
order: 6
ns: Hash
member: of64
---
The 64-bit counterpart of <a href="hash-of"><code>Hash.of</code></a>: hashes a string to a <code>long</code> instead of a 32-bit <code>int</code>. Use it when you want far fewer collisions than 32 bits can give — large content-id spaces, asset registries, or network identifiers where two different strings sharing a hash would be a real risk. Currently it is <a href="hash-fnv1a_64"><code>Hash.fnv1a_64</code></a>, FNV-1a over the bytes. Deterministic like every hash here: the same string always gives the same 64-bit value.
Parameters:
- `s` — the string to hash
```ludic
program Ids64 {
entry {
let grass: long = Hash.of64("grass_tile")
let water: long = Hash.of64("water_tile")
print(grass)
print(water)
}
}
```

View file

@ -0,0 +1,21 @@
---
id: type-long
name: long
category: types
kind: type
tokens: long
sig: long
tip: A 64-bit signed integer for values that overflow a 32-bit int.
order: 1
---
`long` is a 64-bit signed integer — reach for it when a value would overflow the 32-bit `int`: a 64-bit hash or content id, an accumulated playtime in milliseconds, a large score or currency total, packed bit flags. Arithmetic (`+ - * / %`), comparison, and the bitwise operators all work on it, and mixing an `int` with a `long` promotes the `int` to 64 bits automatically — so `big * count` where `big` is a `long` computes in 64 bits and does not overflow. `print` and `str` render all its digits.
One limit to know: a numeric *literal* is still parsed as a 32-bit `int`, so build large values by widening — assign a smaller literal into a `long` and compute from there (`let m: long = 1000000` then `m * m`) rather than writing a 10-digit literal directly.
```ludic
let big: long = 1000000
var area: long = big * big # 10^12, computed in 64 bits
var total: long = area + 1
var doubled: long = area * 2
```

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
}
}

View file

@ -52,7 +52,7 @@ object LudicVocabulary {
"enable", "disable", "match", "machine", "state", "become", "where",
"and", "or", "not", "break", "continue", "new", "emit", "cancel"
)
val PRIMITIVES = setOf("int", "fixed", "bool", "entity", "str", "ptr", "byte", "words", "fixeds", "ptrs", "void")
val PRIMITIVES = setOf("int", "long", "fixed", "bool", "entity", "str", "ptr", "byte", "words", "fixeds", "ptrs", "void")
val PHASES = setOf("Start", "Input", "FixedUpdate", "Update", "LateUpdate", "Render")
val WIDGETS = setOf("panel", "col", "row", "label", "button", "image", "spacer")

View file

@ -177,7 +177,7 @@
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|enum|ui|const|var|let|fn|handler|entry|state)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|fixed|bool|entity|str|ptr|byte|words|fixeds|ptrs|void)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|bool|entity|str|ptr|byte|words|fixeds|ptrs|void)\\b" },
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }
]

View file

@ -177,7 +177,7 @@
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|enum|ui|const|var|let|fn|handler|entry|state)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|fixed|bool|entity|str|ptr|byte|words|fixeds|ptrs|void)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|bool|entity|str|ptr|byte|words|fixeds|ptrs|void)\\b" },
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }
]

View file

@ -97,7 +97,7 @@ program LudicFmt {
# ---- vocabulary classifiers ----
fn is_type_word(w: ptr) -> bool {
return (w == "int") or (w == "fixed") or (w == "bool") or (w == "entity") or (w == "str") or (w == "ptr") or (w == "byte") or (w == "words") or (w == "fixeds") or (w == "ptrs") or (w == "void")
return (w == "int") or (w == "long") or (w == "fixed") or (w == "bool") or (w == "entity") or (w == "str") or (w == "ptr") or (w == "byte") or (w == "words") or (w == "fixeds") or (w == "ptrs") or (w == "void")
}
fn is_phase_word(w: ptr) -> bool {
return (w == "Start") or (w == "Input") or (w == "FixedUpdate") or (w == "Update") or (w == "LateUpdate") or (w == "Render")

View file

@ -204,7 +204,7 @@ program LudicLsp {
return c == 43 or c == 45 or c == 42 or c == 47 or c == 37 or c == 60 or c == 62 or c == 61 or c == 40 or c == 41 or c == 123 or c == 125 or c == 91 or c == 93 or c == 44 or c == 58 or c == 46 or c == 33 or c == 64 or c == 59
}
fn is_type_word(w: ptr) -> bool {
return (w == "int") or (w == "fixed") or (w == "bool") or (w == "entity") or (w == "str") or (w == "ptr") or (w == "byte") or (w == "words") or (w == "fixeds") or (w == "ptrs") or (w == "void")
return (w == "int") or (w == "long") or (w == "fixed") or (w == "bool") or (w == "entity") or (w == "str") or (w == "ptr") or (w == "byte") or (w == "words") or (w == "fixeds") or (w == "ptrs") or (w == "void")
}
fn is_phase_word(w: ptr) -> bool {
return (w == "Start") or (w == "Input") or (w == "FixedUpdate") or (w == "Update") or (w == "LateUpdate") or (w == "Render")
@ -1291,6 +1291,16 @@ program LudicLsp {
if (meth == "join") { return "Text.join(parts, sep) -> str" }
if (meth == "replace") { return "Text.replace(s, from, to) -> str" }
}
if (ns == "Hash") {
if (meth == "of") { return "Hash.of(s) -> int" }
if (meth == "fnv1a") { return "Hash.fnv1a(s) -> int" }
if (meth == "crc32") { return "Hash.crc32(s) -> int" }
if (meth == "mix") { return "Hash.mix(x) -> int" }
if (meth == "combine") { return "Hash.combine(...) -> int" }
if (meth == "of64") { return "Hash.of64(s) -> long" }
if (meth == "fnv1a_64") { return "Hash.fnv1a_64(s) -> long" }
if (meth == "mix64") { return "Hash.mix64(x) -> long" }
}
if (ns == "List") {
if (meth == "len") { return "List.len(s) -> int" }
if (meth == "push") { return "List.push(s, v)" }
@ -2276,7 +2286,7 @@ program LudicLsp {
}
}
fn comp_types(o: Buf) -> void {
comp_item(o, "int", 14, "built-in type"); comp_item(o, "fixed", 14, "built-in type"); comp_item(o, "bool", 14, "built-in type")
comp_item(o, "int", 14, "built-in type"); comp_item(o, "long", 14, "built-in type"); comp_item(o, "fixed", 14, "built-in type"); comp_item(o, "bool", 14, "built-in type")
comp_item(o, "entity", 14, "built-in type"); comp_item(o, "str", 14, "built-in type"); comp_item(o, "ptr", 14, "built-in type"); comp_item(o, "void", 14, "built-in type")
}
fn comp_builtins(o: Buf) -> void { var i = 0; while i < len(g_builtins) { comp_item(o, g_builtins[i], 3, g_bsigs[i]); i = i + 1 } }

View file

@ -71,7 +71,7 @@ static const char* LUDIC_KW_STMT[] = {
"and","or","not","break","continue","new","emit","cancel", 0
};
static const char* LUDIC_TYPES[] = {
"int","fixed","bool","entity","str","ptr","byte","words","fixeds","ptrs","void", 0
"int","long","fixed","bool","entity","str","ptr","byte","words","fixeds","ptrs","void", 0
};
static const char* LUDIC_PHASES[] = {
"Start","Input","FixedUpdate","Update","LateUpdate","Render", 0

View file

@ -26,6 +26,7 @@ fn selfhost_frags() -> []ptr {
push(f, "selfhost/emit_intrin2.ludic")
push(f, "selfhost/emit_math.ludic")
push(f, "selfhost/emit_text.ludic")
push(f, "selfhost/emit_hash.ludic")
push(f, "selfhost/emit_list.ludic")
push(f, "selfhost/emit_ease.ludic")
push(f, "selfhost/emit_collide.ludic")

View file

@ -46,6 +46,8 @@ fn cmd_selfhost_test() -> int {
sh_case("memsys", "65 66 0 99 12345 1")
sh_case("math3", "45 90 180 -135 30 60 90")
sh_case("textsplit", "1 1 1 3 1 1 1 1 1 1 1")
sh_case("hash", "-2128831035 -468965076 114400290 114400290 0 -873187034 1095738169 0 1364076727 -2114883783 -845898438 -845898438 -78065325 -78057399 -3750763034362895579 -5808556873153909620 -4100651535478758590 -4100651535478758590 0 -5451962507482445012 7256831767414464289")
sh_case("long", "1000000000000 1000000000001 1000000000005 3000000000000 1 1 1 -1000000000000 1000000 13")
sh_case("color", "16744512 1090486336 1090486336 8355711 8355711 8355711")
print("== the self-host compiler compiles real games (vs golden output) ==")