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