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
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:
parent
a38195128f
commit
2002e977d9
27 changed files with 14007 additions and 12105 deletions
9
docs/language/hash/_section.md
Normal file
9
docs/language/hash/_section.md
Normal 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.
|
||||
30
docs/language/hash/hash-combine.md
Normal file
30
docs/language/hash/hash-combine.md
Normal 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
|
||||
}
|
||||
}
|
||||
```
|
||||
28
docs/language/hash/hash-crc32.md
Normal file
28
docs/language/hash/hash-crc32.md
Normal 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
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/hash/hash-fnv1a.md
Normal file
26
docs/language/hash/hash-fnv1a.md
Normal 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"))
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/hash/hash-fnv1a_64.md
Normal file
26
docs/language/hash/hash-fnv1a_64.md
Normal 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"))
|
||||
}
|
||||
}
|
||||
```
|
||||
29
docs/language/hash/hash-mix.md
Normal file
29
docs/language/hash/hash-mix.md
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
27
docs/language/hash/hash-mix64.md
Normal file
27
docs/language/hash/hash-mix64.md
Normal 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
|
||||
}
|
||||
}
|
||||
```
|
||||
28
docs/language/hash/hash-of.md
Normal file
28
docs/language/hash/hash-of.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
28
docs/language/hash/hash-of64.md
Normal file
28
docs/language/hash/hash-of64.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
21
docs/language/types/type-long.md
Normal file
21
docs/language/types/type-long.md
Normal 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
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue