feat(stdlib): add Noise.* — deterministic fixed-point procedural noise (#3)
All checks were successful
docs / build-and-deploy (push) Successful in 2s

A Noise.* namespace for procedural generation, implemented entirely in Q16.16
fixed point over an integer permutation hash so a seed reproduces the exact same
field on every platform and run (native/headless/wasm) — the determinism edge
over float noise that drifts across CPUs.

  - value2 / perlin2 / simplex2  — value, gradient, and simplex noise -> [-1,1]
  - fbm2(x,y,seed,octaves)       — fractal Brownian motion (octaves of simplex)
  - cellular2 / cellular2_id     — Worley F1 distance + nearest-cell id
  - unit(n)                      — remap [-1,1] -> [0,1]

Covers issue phases 1–2 fully plus cellular from phase 3; domain warp, ridged/
billow, and sample1/sample3 remain as follow-ups. Pure integer IR, C-free;
cellular/fbm reuse the math prelude's fx_sqrt.

- examples/library/noise.ludic: asserts the invariants a fixed-point generator
  must hold (Perlin == 0 at lattice points, every sampler within [-1,1],
  reproducibility, seed sensitivity, non-negative cellular distance). Wired into
  `x test` (now 52 passed).
- docs: a new Noise section + per-symbol pages; inventory and coverage pass.
- seed regenerated; `x bootstrap-cfree` fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-30 21:50:50 +03:00
parent 2ddf830f0b
commit a4f1494a04
17 changed files with 11720 additions and 10432 deletions

View file

@ -0,0 +1,13 @@
---
id: noise
title: Noise
order: 6
---
Procedural noise — the primitive that terrain, caves, biomes, textures, clouds, wind, and object placement are built on. Every generator is implemented in Q16.16 **fixed point** over an integer permutation hash seeded from an explicit seed, so a given seed reproduces the *exact* same field on every platform and every run: native, headless, and (later) wasm all agree bit-for-bit. That is a real edge over float-based engines, whose worlds can drift subtly across CPUs and break shared-seed multiplayer or replays.
Coordinates are <a href="type-fixed"><code>fixed</code></a> values. The integer part of a coordinate selects a lattice cell and the fraction interpolates within it, so you scale feature size by sampling at a fractional *frequency* (e.g. multiply coordinates by <code>1/64</code>). Outputs are <code>fixed</code> normalised to <code>[-1, 1]</code>; <a href="noise-unit"><code>Noise.unit</code></a> remaps that to <code>[0, 1]</code> when you want a height or a probability.
Pick a generator by feel: <a href="noise-value2"><code>value2</code></a> is cheap and blocky; <a href="noise-perlin2"><code>perlin2</code></a> is the classic gradient noise; <a href="noise-simplex2"><code>simplex2</code></a> is the organic default with fewer directional artifacts; <a href="noise-fbm2"><code>fbm2</code></a> stacks octaves of simplex for natural, detailed fields; and <a href="noise-cellular2"><code>cellular2</code></a> (Worley) gives Voronoi-cell structure for stone, cracks, and biome boundaries. Every sampler is a pure function of <code>(x, y, seed)</code> — no global state, no allocation — so it is safe to call across a whole worldgen pass or a per-pixel fill.
Seed worldgen from its own seed (or a dedicated <a href="ns-Random"><code>Random</code></a> stream), kept separate from gameplay RNG, so generating the world never desyncs the simulation.

View file

@ -0,0 +1,30 @@
---
id: noise-cellular2
name: Noise.cellular2
category: noise
kind: namespace-method
tokens: Noise.cellular2
sig: Noise.cellular2(x, y, seed) -> fixed
tip: Worley (cellular) F1 distance to the nearest cell point.
order: 5
ns: Noise
member: cellular2
---
Samples **cellular (Worley) noise** at <code>(x, y)</code> and returns the F1 distance — the distance to the nearest feature point — as a <a href="type-fixed"><code>fixed</code></a> (roughly <code>[0, 1.5]</code>). Each lattice cell holds one feature point placed by its hash; scanning the 3×3 neighbourhood finds the closest one. The distance field forms Voronoi cells, which are exactly the structure you want for stone and cracked textures, biome or region boundaries, and scattered-feature layouts. Small distances mark cell centres; ridges appear where two cells meet.
Pair it with <a href="noise-cellular2_id"><code>Noise.cellular2_id</code></a> to also know *which* cell you are in. Deterministic in fixed point across platforms and runs.
Parameters:
- `x`, `y` — the sample coordinates (`fixed`)
- `seed` — the field selector
```ludic
program Cellular {
entry {
let seed = 7
let d = Noise.cellular2(fixed(5) / fixed(4), fixed(3) / fixed(4), seed)
print(Math.floor(d * fixed(1000))) # distance to nearest cell point
}
}
```

View file

@ -0,0 +1,31 @@
---
id: noise-cellular2_id
name: Noise.cellular2_id
category: noise
kind: namespace-method
tokens: Noise.cellular2_id
sig: Noise.cellular2_id(x, y, seed) -> int
tip: The id of the nearest Worley cell — stable per cell.
order: 6
ns: Noise
member: cellular2_id
---
Returns the integer **id of the nearest cell** in the same Worley diagram that <a href="noise-cellular2"><code>Noise.cellular2</code></a> measures distance in — a stable hash that is identical for every sample point inside a given cell. Use it to assign something discrete per region: the biome or material of a cell, the variant of a scattered prop, a per-region colour. Combine it with the F1 distance to shade toward cell edges.
The id is a hash, so treat it as an opaque label (mod it into your table of choices); it is deterministic across platforms and runs.
Parameters:
- `x`, `y` — the sample coordinates (`fixed`)
- `seed` — the field selector (must match the `cellular2` call it pairs with)
```ludic
program CellId {
entry {
let seed = 7
let id = Noise.cellular2_id(fixed(5) / fixed(4), fixed(3) / fixed(4), seed)
let biome = Math.posmod(id, 4) # one of 4 biomes for this cell
print(biome)
}
}
```

View file

@ -0,0 +1,31 @@
---
id: noise-fbm2
name: Noise.fbm2
category: noise
kind: namespace-method
tokens: Noise.fbm2
sig: Noise.fbm2(x, y, seed, octaves) -> fixed
tip: Fractal Brownian motion — octaves of simplex, in [-1, 1].
order: 4
ns: Noise
member: fbm2
---
Samples **fractal Brownian motion** at <code>(x, y)</code>: it stacks <code>octaves</code> layers of <a href="noise-simplex2"><code>simplex2</code></a>, each at double the frequency and half the amplitude of the last, and normalises by the total amplitude so the result stays a <a href="type-fixed"><code>fixed</code></a> in <code>[-1, 1]</code>. Layering this way adds fine detail on top of broad shapes — the standard recipe for natural-looking terrain, clouds, and marble. More octaves means more detail (and more cost); 4–6 is typical.
Each octave uses a different derived seed, and the whole thing is deterministic in fixed point across platforms and runs.
Parameters:
- `x`, `y` — the sample coordinates (`fixed`)
- `seed` — the base field selector (each octave derives from it)
- `octaves` — how many layers to sum (higher = more detail)
```ludic
program Fbm {
entry {
let seed = 1337
let h = Noise.fbm2(fixed(2) / fixed(7), fixed(9) / fixed(7), seed, 5)
print(Math.floor(Noise.unit(h) * fixed(100))) # detailed 0..100 height
}
}
```

View file

@ -0,0 +1,31 @@
---
id: noise-perlin2
name: Noise.perlin2
category: noise
kind: namespace-method
tokens: Noise.perlin2
sig: Noise.perlin2(x, y, seed) -> fixed
tip: 2D Perlin gradient noise, deterministic, in [-1, 1].
order: 2
ns: Noise
member: perlin2
---
Samples 2D **Perlin gradient noise** at <code>(x, y)</code> for the given <code>seed</code> and returns a <a href="type-fixed"><code>fixed</code></a> in <code>[-1, 1]</code>. Instead of a random value per lattice point, Perlin places a random gradient at each corner and interpolates their dot products with the offset vectors — which gives smoother, more natural gradients than value noise, the familiar look of classic terrain and cloud fields. By construction the value is exactly <code>0</code> at every integer lattice point.
Deterministic in fixed point across platforms and runs. Scale the coordinates to set feature size, and stack octaves with <a href="noise-fbm2"><code>Noise.fbm2</code></a> for richer detail.
Parameters:
- `x`, `y` — the sample coordinates (`fixed`)
- `seed` — the field selector
```ludic
program Perlin {
entry {
let seed = 1337
if Noise.perlin2(fixed(0), fixed(0), seed) == 0 { print(1) } # zero at the lattice
let n = Noise.perlin2(fixed(10) / fixed(3), fixed(7) / fixed(3), seed)
print(Math.floor(n * fixed(1000)))
}
}
```

View file

@ -0,0 +1,30 @@
---
id: noise-simplex2
name: Noise.simplex2
category: noise
kind: namespace-method
tokens: Noise.simplex2
sig: Noise.simplex2(x, y, seed) -> fixed
tip: 2D simplex noise, the organic default, in [-1, 1].
order: 3
ns: Noise
member: simplex2
---
Samples 2D **simplex noise** at <code>(x, y)</code> for the given <code>seed</code> and returns a <a href="type-fixed"><code>fixed</code></a> in <code>[-1, 1]</code>. Simplex noise sums contributions from the three corners of a skewed triangular cell, which gives it fewer of the axis-aligned directional artifacts that gradient noise can show — making it the recommended default for organic fields like terrain height and biome masks.
Deterministic in fixed point across platforms and runs, and the generator that <a href="noise-fbm2"><code>Noise.fbm2</code></a> layers by default. Scale the coordinates to set feature size.
Parameters:
- `x`, `y` — the sample coordinates (`fixed`)
- `seed` — the field selector
```ludic
program Simplex {
entry {
let seed = 1337
let n = Noise.simplex2(fixed(4) / fixed(9), fixed(6) / fixed(9), seed)
print(Math.floor(Noise.unit(n) * fixed(100)))
}
}
```

View file

@ -0,0 +1,27 @@
---
id: noise-unit
name: Noise.unit
category: noise
kind: namespace-method
tokens: Noise.unit
sig: Noise.unit(n) -> fixed
tip: Remap a [-1,1] noise sample to [0,1].
order: 7
ns: Noise
member: unit
---
Remaps a noise sample from <code>[-1, 1]</code> to <code>[0, 1]</code> — literally <code>n / 2 + 0.5</code> — returning a <a href="type-fixed"><code>fixed</code></a>. The signed range is the natural output of the samplers, but heights, densities, probabilities, and colour ramps usually want the unsigned <code>[0, 1]</code> range; <code>unit</code> is the one-step conversion. It is a plain affine remap, so <code>unit(-1) == 0</code>, <code>unit(0) == 0.5</code>, and <code>unit(1) == 1</code>.
Parameters:
- `n` — a sample in `[-1, 1]` (e.g. from `perlin2`/`simplex2`/`fbm2`)
```ludic
program Unit {
entry {
let n = Noise.simplex2(fixed(1) / fixed(2), fixed(1) / fixed(2), 1337)
let height = Math.floor(Noise.unit(n) * fixed(64)) # 0..64 tiles
print(height)
}
}
```

View file

@ -0,0 +1,30 @@
---
id: noise-value2
name: Noise.value2
category: noise
kind: namespace-method
tokens: Noise.value2
sig: Noise.value2(x, y, seed) -> fixed
tip: 2D value (lattice) noise, deterministic, in [-1, 1].
order: 1
ns: Noise
member: value2
---
Samples 2D **value noise** at <code>(x, y)</code> for the given <code>seed</code> and returns a <a href="type-fixed"><code>fixed</code></a> in <code>[-1, 1]</code>. Value noise assigns a pseudo-random value to each integer lattice point and smoothly interpolates between them with a quintic fade — the cheapest generator here, with a slightly blocky, retro character that suits low-detail height fields, dithering, and per-cell variation.
Because it is a pure function of <code>(x, y, seed)</code> in fixed point, the same arguments always produce the same value on every platform. Sample at a fractional frequency (scale the coordinates) to change feature size.
Parameters:
- `x`, `y` — the sample coordinates (`fixed`); the integer part picks a cell
- `seed` — the field selector; different seeds give independent worlds
```ludic
program Value {
entry {
let seed = 1337
let n = Noise.value2(fixed(3) / fixed(8), fixed(5) / fixed(8), seed)
print(Math.floor(Noise.unit(n) * fixed(100))) # a 0..100 height
}
}
```

View file

@ -0,0 +1,43 @@
# noise.ludic — Noise.* determinism and structural invariants. Fixed-point noise
# can't be checked against a float reference bit-for-bit (that difference is the
# whole point), so we assert the properties that must hold: Perlin is exactly 0
# at integer lattice points, every sampler stays within [-1,1] (±65536 fixed),
# the same (x,y,seed) always reproduces, the seed changes the field, and cellular
# distance is non-negative. Running it prints: 1 2 3 4 5 6 7 8 9 10 11
program Noise {
entry {
let seed = 1337
# gradient noise is exactly 0 at integer lattice points (zero offset vectors)
if Noise.perlin2(fixed(0), fixed(0), seed) == 0 { print(1) }
if Noise.perlin2(fixed(3), fixed(5), seed) == 0 { print(2) }
# determinism: identical inputs -> identical output
let a = Noise.simplex2(20000, 100, seed)
let b = Noise.simplex2(20000, 100, seed)
if a == b { print(3) }
# range: every sampler stays within [-1, 1] (comparisons are in the fixed domain)
let lo = fixed(0) - fixed(1)
let hi = fixed(1)
let p = Noise.perlin2(12345, 54321, seed)
if p >= lo and p <= hi { print(4) }
let s = Noise.simplex2(12345, 54321, seed)
if s >= lo and s <= hi { print(5) }
let v = Noise.value2(12345, 54321, seed)
if v >= lo and v <= hi { print(6) }
let f = Noise.fbm2(12345, 54321, seed, 5)
if f >= lo and f <= hi { print(7) }
# unit() remaps [-1,1] -> [0,1]: endpoints and midpoint (all in the fixed domain)
let half = fixed(1) / fixed(2)
if Noise.unit(fixed(1)) == fixed(1) and Noise.unit(fixed(0) - fixed(1)) == fixed(0) and Noise.unit(fixed(0)) == half { print(8) }
# the seed selects the world: different seeds -> different fields
if Noise.value2(12345, 54321, 1) != Noise.value2(12345, 54321, 2) { print(9) }
# cellular F1 distance is non-negative; the cell id is deterministic
if Noise.cellular2(12345, 54321, seed) >= 0 { print(10) }
if Noise.cellular2_id(12345, 54321, seed) == Noise.cellular2_id(12345, 54321, seed) { print(11) }
}
}

View file

@ -27,6 +27,7 @@ var g_uses_textrt2: bool = false # Text.split/join/replace was emitted -> emit
var g_uses_hashrt: bool = false # Hash.of/fnv1a/crc32 was emitted -> emit the byte-stream hashers var g_uses_hashrt: bool = false # Hash.of/fnv1a/crc32 was emitted -> emit the byte-stream hashers
var g_uses_cryptort: bool = false # Crypto.* was emitted -> emit the SHA-256 / HMAC runtime var g_uses_cryptort: bool = false # Crypto.* was emitted -> emit the SHA-256 / HMAC runtime
var g_uses_uuidrt: bool = false # Uuid.* was emitted -> emit the UUID runtime (needs the crypto CSPRNG) var g_uses_uuidrt: bool = false # Uuid.* was emitted -> emit the UUID runtime (needs the crypto CSPRNG)
var g_uses_noisert: bool = false # Noise.* was emitted -> emit the fixed-point noise runtime
var g_uses_datert: bool = false # Date.*/DateTime.* was emitted -> emit the civil<->epoch conversions var g_uses_datert: bool = false # Date.*/DateTime.* was emitted -> emit the civil<->epoch conversions
var g_uses_longstr: bool = false # string(long) / interpolating a long was emitted -> emit fn_long_str var g_uses_longstr: bool = false # string(long) / interpolating a long was emitted -> emit fn_long_str

View file

@ -106,6 +106,7 @@ function emit_program() -> void {
if g_uses_hashrt { emit_hash_prelude() } # @fn_hash_fnv1a / @fn_hash_crc32 byte hashers if g_uses_hashrt { emit_hash_prelude() } # @fn_hash_fnv1a / @fn_hash_crc32 byte hashers
if g_uses_cryptort { emit_crypto_prelude() } # @fn_sha256_hex / @fn_hmac_sha256_hex + constant-time compare + CSPRNG if g_uses_cryptort { emit_crypto_prelude() } # @fn_sha256_hex / @fn_hmac_sha256_hex + constant-time compare + CSPRNG
if g_uses_uuidrt { emit_uuid_prelude() } # @fn_uuid_v4 / @fn_uuid_v7 / parse / equals (over the crypto CSPRNG) if g_uses_uuidrt { emit_uuid_prelude() } # @fn_uuid_v4 / @fn_uuid_v7 / parse / equals (over the crypto CSPRNG)
if g_uses_noisert { emit_noise_prelude() } # @fn_noise_value2/perlin2/simplex2/fbm2/cellular2 (Q16.16)
if g_uses_datert { emit_datetime_prelude() } # @fn_days_from_civil / @fn_civil_from_days conversions if g_uses_datert { emit_datetime_prelude() } # @fn_days_from_civil / @fn_civil_from_days conversions
} }

View file

@ -239,6 +239,10 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if is_uuid_ns(meth) { return emit_uuid_ns(meth, e) } if is_uuid_ns(meth) { return emit_uuid_ns(meth, e) }
perr(`unknown builtin Uuid.{meth}`) perr(`unknown builtin Uuid.{meth}`)
} }
if (ns == "Noise") {
if is_noise_ns(meth) { return emit_noise_ns(meth, e) }
perr(`unknown builtin Noise.{meth}`)
}
if (ns == "Vector") { if (ns == "Vector") {
if is_vector_ns(meth) { return emit_vector_ns(meth, e) } if is_vector_ns(meth) { return emit_vector_ns(meth, e) }
perr(`unknown builtin Vector.{meth}`) perr(`unknown builtin Vector.{meth}`)

217
selfhost/emit_noise.ludic Normal file
View file

@ -0,0 +1,217 @@
# emit_noise.ludic — the Noise.* namespace: deterministic, fixed-point procedural
# noise for terrain, caves, biomes, textures, clouds, placement — the primitives
# procedural generation is built on. Everything is Q16.16 integer IR over an
# integer permutation hash seeded from an explicit seed, so a seed reproduces the
# EXACT same field on every platform and every run (native, headless, and later
# wasm) — the determinism edge over float engines whose worlds drift across CPUs.
#
# Noise.value2(x, y, seed) value (lattice) noise -> fixed in [-1, 1]
# Noise.perlin2(x, y, seed) classic gradient noise -> fixed in [-1, 1]
# Noise.simplex2(x, y, seed) organic simplex noise -> fixed in [-1, 1]
# Noise.fbm2(x, y, seed, oct) fractal Brownian motion (octaves of simplex)
# -> fixed in [-1, 1]
# Noise.cellular2(x, y, seed) Worley F1 distance to the nearest cell point
# -> fixed, ~[0, 1.5]
# Noise.cellular2_id(x, y, seed) the id (hash) of that nearest cell -> int
# Noise.unit(n) remap a [-1,1] sample to [0,1] -> fixed
#
# Coordinates are `fixed` (Q16.16); the integer part selects a lattice cell and
# the fraction interpolates within it, so sample at a fractional `frequency` to
# scale features. All samplers are pure functions of (x, y, seed): no global
# state, no allocation, safe to call from worldgen or a shader-like fill.
function is_noise_ns(meth: pointer) -> bool {
if (meth == "value2") or (meth == "perlin2") or (meth == "simplex2") { return true }
if (meth == "fbm2") or (meth == "cellular2") or (meth == "cellular2_id") { return true }
if (meth == "unit") { return true }
return false
}
function emit_noise_ns(meth: pointer, e: Node) -> Val {
if (meth == "unit") { # [-1,1] -> [0,1]: n/2 + 0.5
let n = emit_expr(e.kids[0])
let h = emit_bind(`ashr i32 {n.code}, 1`)
return val(emit_bind(`add i32 {h}, 32768`), "fixed")
}
g_uses_noisert = true
if (meth == "value2") {
let x = emit_expr(e.kids[0]); let y = emit_expr(e.kids[1]); let s = emit_expr(e.kids[2])
return val(emit_bind(`call i32 @fn_noise_value2(i32 {x.code}, i32 {y.code}, i32 {s.code})`), "fixed")
}
if (meth == "perlin2") {
let x = emit_expr(e.kids[0]); let y = emit_expr(e.kids[1]); let s = emit_expr(e.kids[2])
return val(emit_bind(`call i32 @fn_noise_perlin2(i32 {x.code}, i32 {y.code}, i32 {s.code})`), "fixed")
}
if (meth == "simplex2") {
let x = emit_expr(e.kids[0]); let y = emit_expr(e.kids[1]); let s = emit_expr(e.kids[2])
return val(emit_bind(`call i32 @fn_noise_simplex2(i32 {x.code}, i32 {y.code}, i32 {s.code})`), "fixed")
}
if (meth == "fbm2") {
g_uses_mathrt = true # simplex path is standalone; fbm needs fx_div only (local)
let x = emit_expr(e.kids[0]); let y = emit_expr(e.kids[1]); let s = emit_expr(e.kids[2]); let o = emit_expr(e.kids[3])
return val(emit_bind(`call i32 @fn_noise_fbm2(i32 {x.code}, i32 {y.code}, i32 {s.code}, i32 {o.code})`), "fixed")
}
if (meth == "cellular2") {
g_uses_mathrt = true # F1 distance needs @fn_fx_sqrt
let x = emit_expr(e.kids[0]); let y = emit_expr(e.kids[1]); let s = emit_expr(e.kids[2])
return val(emit_bind(`call i32 @fn_noise_cellular2(i32 {x.code}, i32 {y.code}, i32 {s.code})`), "fixed")
}
# cellular2_id: the hash id of the nearest feature cell (stable per cell -> use
# it to pick a biome/material). Distances come from cellular2.
let x = emit_expr(e.kids[0]); let y = emit_expr(e.kids[1]); let s = emit_expr(e.kids[2])
return val(emit_bind(`call i32 @fn_noise_cellular2_id(i32 {x.code}, i32 {y.code}, i32 {s.code})`), "int")
}
# emit_noise_prelude — the noise runtime, emitted once per program that uses
# Noise.* (g_uses_noisert). Pure Q16.16 integer IR; cellular/fbm additionally use
# the math prelude (@fn_fx_sqrt), pulled in by setting g_uses_mathrt at the call.
function emit_noise_prelude() -> void {
# Q16.16 helpers (local to noise so the prelude is self-contained for the
# gradient/value paths). fx multiply, divide, lerp, and a [-1,1] clamp.
emith("define i32 @fn_nfx_mul(i32 %a, i32 %b) {\n")
emith(" %a64 = sext i32 %a to i64\n %b64 = sext i32 %b to i64\n %m = mul i64 %a64, %b64\n %s = ashr i64 %m, 16\n %r = trunc i64 %s to i32\n ret i32 %r\n}\n")
emith("define i32 @fn_nfx_div(i32 %a, i32 %b) {\n")
emith(" %z = icmp eq i32 %b, 0\n br i1 %z, label %zero, label %go\n")
emith("zero:\n ret i32 0\n")
emith("go:\n %a64 = sext i32 %a to i64\n %ash = shl i64 %a64, 16\n %b64 = sext i32 %b to i64\n %d = sdiv i64 %ash, %b64\n %r = trunc i64 %d to i32\n ret i32 %r\n}\n")
emith("define i32 @fn_nfx_lerp(i32 %a, i32 %b, i32 %t) {\n")
emith(" %d = sub i32 %b, %a\n %dt = call i32 @fn_nfx_mul(i32 %d, i32 %t)\n %r = add i32 %a, %dt\n ret i32 %r\n}\n")
emith("define i32 @fn_noise_clamp(i32 %v) {\n")
emith(" %hi = icmp sgt i32 %v, 65536\n %v1 = select i1 %hi, i32 65536, i32 %v\n %lo = icmp slt i32 %v1, -65536\n %r = select i1 %lo, i32 -65536, i32 %v1\n ret i32 %r\n}\n")
# integer lattice hash: mix seed + cell coords with large odd constants, then a
# MurmurHash3-style fmix32 finalizer. Deterministic and well-distributed.
emith("define i32 @fn_noise_hash(i32 %seed, i32 %xi, i32 %yi) {\n")
emith(" %a = mul i32 %xi, 374761393\n %b = mul i32 %yi, -1028477387\n %c = add i32 %seed, %a\n %d0 = add i32 %c, %b\n")
emith(" %e = lshr i32 %d0, 16\n %f = xor i32 %d0, %e\n %g = mul i32 %f, -2048144789\n")
emith(" %h = lshr i32 %g, 13\n %i = xor i32 %g, %h\n %j = mul i32 %i, -1028477387\n")
emith(" %k = lshr i32 %j, 16\n %l = xor i32 %j, %k\n ret i32 %l\n}\n")
# quintic fade 6t^5 - 15t^4 + 10t^3 (Q16.16); t in [0,1]
emith("define i32 @fn_noise_fade(i32 %t) {\n")
emith(" %t2 = call i32 @fn_nfx_mul(i32 %t, i32 %t)\n %t3 = call i32 @fn_nfx_mul(i32 %t2, i32 %t)\n")
emith(" %t4 = call i32 @fn_nfx_mul(i32 %t3, i32 %t)\n %t5 = call i32 @fn_nfx_mul(i32 %t4, i32 %t)\n")
emith(" %c6 = mul i32 %t5, 6\n %c15 = mul i32 %t4, 15\n %c10 = mul i32 %t3, 10\n")
emith(" %s1 = sub i32 %c6, %c15\n %r = add i32 %s1, %c10\n ret i32 %r\n}\n")
# value noise: bilinear-interpolate the four corner random values (each mapped
# to [-1,1]) with the faded fractional coordinates.
emith("define i32 @fn_noise_value2(i32 %x, i32 %y, i32 %seed) {\n")
emith(" %xi = ashr i32 %x, 16\n %yi = ashr i32 %y, 16\n %xf = and i32 %x, 65535\n %yf = and i32 %y, 65535\n")
emith(" %u = call i32 @fn_noise_fade(i32 %xf)\n %v = call i32 @fn_noise_fade(i32 %yf)\n")
emith(" %xi1 = add i32 %xi, 1\n %yi1 = add i32 %yi, 1\n")
emith(" %h00 = call i32 @fn_noise_hash(i32 %seed, i32 %xi, i32 %yi)\n %m00 = and i32 %h00, 131071\n %n00 = sub i32 %m00, 65536\n")
emith(" %h10 = call i32 @fn_noise_hash(i32 %seed, i32 %xi1, i32 %yi)\n %m10 = and i32 %h10, 131071\n %n10 = sub i32 %m10, 65536\n")
emith(" %h01 = call i32 @fn_noise_hash(i32 %seed, i32 %xi, i32 %yi1)\n %m01 = and i32 %h01, 131071\n %n01 = sub i32 %m01, 65536\n")
emith(" %h11 = call i32 @fn_noise_hash(i32 %seed, i32 %xi1, i32 %yi1)\n %m11 = and i32 %h11, 131071\n %n11 = sub i32 %m11, 65536\n")
emith(" %a = call i32 @fn_nfx_lerp(i32 %n00, i32 %n10, i32 %u)\n %b = call i32 @fn_nfx_lerp(i32 %n01, i32 %n11, i32 %u)\n")
emith(" %n = call i32 @fn_nfx_lerp(i32 %a, i32 %b, i32 %v)\n %r = call i32 @fn_noise_clamp(i32 %n)\n ret i32 %r\n}\n")
# 8 gradient directions (axis + diagonal, the diagonals scaled by 1/sqrt2), as
# packed (gx, gy) Q16.16 pairs; grad2 dots the selected gradient with (dx, dy).
emith("@noise_grad2 = private unnamed_addr constant [16 x i32] [i32 65536, i32 0, i32 -65536, i32 0, i32 0, i32 65536, i32 0, i32 -65536, i32 46341, i32 46341, i32 -46341, i32 46341, i32 46341, i32 -46341, i32 -46341, i32 -46341]\n")
emith("define i32 @fn_noise_grad2(i32 %hash, i32 %dx, i32 %dy) {\n")
emith(" %h = and i32 %hash, 7\n %idx = shl i32 %h, 1\n %idx64 = sext i32 %idx to i64\n")
emith(" %gxp = getelementptr [16 x i32], ptr @noise_grad2, i64 0, i64 %idx64\n %gx = load i32, ptr %gxp\n")
emith(" %idy = add i32 %idx, 1\n %idy64 = sext i32 %idy to i64\n %gyp = getelementptr [16 x i32], ptr @noise_grad2, i64 0, i64 %idy64\n %gy = load i32, ptr %gyp\n")
emith(" %px = call i32 @fn_nfx_mul(i32 %gx, i32 %dx)\n %py = call i32 @fn_nfx_mul(i32 %gy, i32 %dy)\n %r = add i32 %px, %py\n ret i32 %r\n}\n")
# Perlin gradient noise: interpolate the four corner gradient dots, then scale
# the ~[-0.707,0.707] result by sqrt2 into [-1,1] (and clamp for safety).
emith("define i32 @fn_noise_perlin2(i32 %x, i32 %y, i32 %seed) {\n")
emith(" %xi = ashr i32 %x, 16\n %yi = ashr i32 %y, 16\n %xf = and i32 %x, 65535\n %yf = and i32 %y, 65535\n")
emith(" %xf1 = sub i32 %xf, 65536\n %yf1 = sub i32 %yf, 65536\n")
emith(" %u = call i32 @fn_noise_fade(i32 %xf)\n %v = call i32 @fn_noise_fade(i32 %yf)\n")
emith(" %xi1 = add i32 %xi, 1\n %yi1 = add i32 %yi, 1\n")
emith(" %h00 = call i32 @fn_noise_hash(i32 %seed, i32 %xi, i32 %yi)\n %h10 = call i32 @fn_noise_hash(i32 %seed, i32 %xi1, i32 %yi)\n")
emith(" %h01 = call i32 @fn_noise_hash(i32 %seed, i32 %xi, i32 %yi1)\n %h11 = call i32 @fn_noise_hash(i32 %seed, i32 %xi1, i32 %yi1)\n")
emith(" %g00 = call i32 @fn_noise_grad2(i32 %h00, i32 %xf, i32 %yf)\n %g10 = call i32 @fn_noise_grad2(i32 %h10, i32 %xf1, i32 %yf)\n")
emith(" %g01 = call i32 @fn_noise_grad2(i32 %h01, i32 %xf, i32 %yf1)\n %g11 = call i32 @fn_noise_grad2(i32 %h11, i32 %xf1, i32 %yf1)\n")
emith(" %a = call i32 @fn_nfx_lerp(i32 %g00, i32 %g10, i32 %u)\n %b = call i32 @fn_nfx_lerp(i32 %g01, i32 %g11, i32 %u)\n")
emith(" %n = call i32 @fn_nfx_lerp(i32 %a, i32 %b, i32 %v)\n %sc = call i32 @fn_nfx_mul(i32 %n, i32 92682)\n %r = call i32 @fn_noise_clamp(i32 %sc)\n ret i32 %r\n}\n")
# one simplex corner contribution: t = 0.5 - x^2 - y^2; if t <= 0 -> 0, else
# t^4 * grad(hash, x, y). Kept as a helper so simplex2 reads as three corners.
emith("define i32 @fn_noise_scorner(i32 %hash, i32 %dx, i32 %dy) {\n")
emith(" %xx = call i32 @fn_nfx_mul(i32 %dx, i32 %dx)\n %yy = call i32 @fn_nfx_mul(i32 %dy, i32 %dy)\n")
emith(" %s0 = sub i32 32768, %xx\n %t = sub i32 %s0, %yy\n %neg = icmp sle i32 %t, 0\n br i1 %neg, label %zero, label %go\n")
emith("zero:\n ret i32 0\n")
emith("go:\n %t2 = call i32 @fn_nfx_mul(i32 %t, i32 %t)\n %t4 = call i32 @fn_nfx_mul(i32 %t2, i32 %t2)\n")
emith(" %g = call i32 @fn_noise_grad2(i32 %hash, i32 %dx, i32 %dy)\n %r = call i32 @fn_nfx_mul(i32 %t4, i32 %g)\n ret i32 %r\n}\n")
# 2D simplex noise (skewed triangular lattice). F2 = (sqrt3-1)/2 = 23994,
# G2 = (3-sqrt3)/6 = 13849 in Q16.16. Sum of three corner contributions, scaled
# into [-1,1] and clamped.
emith("define i32 @fn_noise_simplex2(i32 %x, i32 %y, i32 %seed) {\n")
emith(" %sum0 = add i32 %x, %y\n %skew = call i32 @fn_nfx_mul(i32 %sum0, i32 23994)\n")
emith(" %xs = add i32 %x, %skew\n %ys = add i32 %y, %skew\n %i = ashr i32 %xs, 16\n %j = ashr i32 %ys, 16\n")
emith(" %ij = add i32 %i, %j\n %tt = mul i32 %ij, 13849\n") # t = (i+j)*G2, fixed
emith(" %if0 = shl i32 %i, 16\n %jf0 = shl i32 %j, 16\n")
emith(" %X0 = sub i32 %if0, %tt\n %Y0 = sub i32 %jf0, %tt\n %x0 = sub i32 %x, %X0\n %y0 = sub i32 %y, %Y0\n")
emith(" %gt = icmp sgt i32 %x0, %y0\n %i1 = select i1 %gt, i32 1, i32 0\n %j1 = select i1 %gt, i32 0, i32 1\n")
emith(" %i1f = shl i32 %i1, 16\n %j1f = shl i32 %j1, 16\n")
emith(" %x1a = sub i32 %x0, %i1f\n %x1 = add i32 %x1a, 13849\n %y1a = sub i32 %y0, %j1f\n %y1 = add i32 %y1a, 13849\n")
emith(" %x2a = sub i32 %x0, 65536\n %x2 = add i32 %x2a, 27698\n %y2a = sub i32 %y0, 65536\n %y2 = add i32 %y2a, 27698\n")
emith(" %i1p = add i32 %i, %i1\n %j1p = add i32 %j, %j1\n %i2 = add i32 %i, 1\n %j2 = add i32 %j, 1\n")
emith(" %gi0 = call i32 @fn_noise_hash(i32 %seed, i32 %i, i32 %j)\n %gi1 = call i32 @fn_noise_hash(i32 %seed, i32 %i1p, i32 %j1p)\n %gi2 = call i32 @fn_noise_hash(i32 %seed, i32 %i2, i32 %j2)\n")
emith(" %n0 = call i32 @fn_noise_scorner(i32 %gi0, i32 %x0, i32 %y0)\n %n1 = call i32 @fn_noise_scorner(i32 %gi1, i32 %x1, i32 %y1)\n %n2 = call i32 @fn_noise_scorner(i32 %gi2, i32 %x2, i32 %y2)\n")
emith(" %sa = add i32 %n0, %n1\n %sb = add i32 %sa, %n2\n")
emith(" %sc = mul i32 %sb, 45\n %r = call i32 @fn_noise_clamp(i32 %sc)\n ret i32 %r\n}\n")
# fractal Brownian motion: sum `oct` octaves of simplex at rising frequency
# (lacunarity 2.0) and falling amplitude (gain 0.5), normalised by total
# amplitude so the result stays in [-1,1]. seed varies per octave.
emith("define i32 @fn_noise_fbm2(i32 %x, i32 %y, i32 %seed, i32 %oct) {\n")
emith("entry:\n %sump = alloca i32\n %normp = alloca i32\n %ampp = alloca i32\n %freqp = alloca i32\n %op = alloca i32\n")
emith(" store i32 0, ptr %sump\n store i32 0, ptr %normp\n store i32 65536, ptr %ampp\n store i32 65536, ptr %freqp\n store i32 0, ptr %op\n br label %cond\n")
emith("cond:\n %o = load i32, ptr %op\n %lt = icmp slt i32 %o, %oct\n br i1 %lt, label %body, label %done\n")
emith("body:\n %freq = load i32, ptr %freqp\n %amp = load i32, ptr %ampp\n")
emith(" %fx = call i32 @fn_nfx_mul(i32 %x, i32 %freq)\n %fy = call i32 @fn_nfx_mul(i32 %y, i32 %freq)\n")
emith(" %so = add i32 %seed, %o\n %n = call i32 @fn_noise_simplex2(i32 %fx, i32 %fy, i32 %so)\n")
emith(" %na = call i32 @fn_nfx_mul(i32 %n, i32 %amp)\n %sum = load i32, ptr %sump\n %sum2 = add i32 %sum, %na\n store i32 %sum2, ptr %sump\n")
emith(" %norm = load i32, ptr %normp\n %norm2 = add i32 %norm, %amp\n store i32 %norm2, ptr %normp\n")
emith(" %amp2 = call i32 @fn_nfx_mul(i32 %amp, i32 32768)\n store i32 %amp2, ptr %ampp\n")
emith(" %freq2 = call i32 @fn_nfx_mul(i32 %freq, i32 131072)\n store i32 %freq2, ptr %freqp\n")
emith(" %o1 = add i32 %o, 1\n store i32 %o1, ptr %op\n br label %cond\n")
emith("done:\n %fsum = load i32, ptr %sump\n %fnorm = load i32, ptr %normp\n")
emith(" %nz = icmp eq i32 %fnorm, 0\n br i1 %nz, label %z, label %div\n")
emith("z:\n ret i32 0\n")
emith("div:\n %d = call i32 @fn_nfx_div(i32 %fsum, i32 %fnorm)\n %r = call i32 @fn_noise_clamp(i32 %d)\n ret i32 %r\n}\n")
# Worley / cellular noise: scan the 3x3 neighbourhood of cells, each holding one
# feature point placed by its cell hash, and return the distance to (and id of)
# the nearest feature point. cellular2 returns F1 distance; cellular2_id the id.
emit_noise_cellular()
}
# split out so no single function is oversized; shares the hash/helpers above.
function emit_noise_cellular() -> void {
# core scan -> writes the min squared distance to %d2out and the winning id to
# %idout (both caller-allocated), so both public entry points share one loop.
emith("define void @fn_noise_cell_scan(i32 %x, i32 %y, i32 %seed, ptr %d2out, ptr %idout) {\n")
emith("entry:\n %xi = ashr i32 %x, 16\n %yi = ashr i32 %y, 16\n")
emith(" %bestp = alloca i32\n %idp = alloca i32\n %dyp = alloca i32\n %dxp = alloca i32\n")
emith(" store i32 2147483647, ptr %bestp\n store i32 0, ptr %idp\n store i32 -1, ptr %dyp\n br label %yc\n")
emith("yc:\n %oy = load i32, ptr %dyp\n %yok = icmp sle i32 %oy, 1\n br i1 %yok, label %yb, label %ydone\n")
emith("yb:\n store i32 -1, ptr %dxp\n br label %xc\n")
emith("xc:\n %ox = load i32, ptr %dxp\n %xok = icmp sle i32 %ox, 1\n br i1 %xok, label %xb, label %xdone\n")
emith("xb:\n %cx = add i32 %xi, %ox\n %cy = add i32 %yi, %oy\n")
emith(" %h = call i32 @fn_noise_hash(i32 %seed, i32 %cx, i32 %cy)\n")
emith(" %fxr = and i32 %h, 65535\n %hs = lshr i32 %h, 16\n %fyr = and i32 %hs, 65535\n")
emith(" %cxf = shl i32 %cx, 16\n %cyf = shl i32 %cy, 16\n %pxr = add i32 %cxf, %fxr\n %pyr = add i32 %cyf, %fyr\n")
emith(" %ddx = sub i32 %pxr, %x\n %ddy = sub i32 %pyr, %y\n")
emith(" %dxx = call i32 @fn_nfx_mul(i32 %ddx, i32 %ddx)\n %dyy = call i32 @fn_nfx_mul(i32 %ddy, i32 %ddy)\n %d2 = add i32 %dxx, %dyy\n")
emith(" %best = load i32, ptr %bestp\n %less = icmp slt i32 %d2, %best\n br i1 %less, label %upd, label %skip\n")
emith("upd:\n store i32 %d2, ptr %bestp\n store i32 %h, ptr %idp\n br label %skip\n")
emith("skip:\n %ox1 = add i32 %ox, 1\n store i32 %ox1, ptr %dxp\n br label %xc\n")
emith("xdone:\n %oy1 = add i32 %oy, 1\n store i32 %oy1, ptr %dyp\n br label %yc\n")
emith("ydone:\n %fb = load i32, ptr %bestp\n store i32 %fb, ptr %d2out\n %fi = load i32, ptr %idp\n store i32 %fi, ptr %idout\n ret void\n}\n")
emith("define i32 @fn_noise_cellular2(i32 %x, i32 %y, i32 %seed) {\n")
emith(" %d2 = alloca i32\n %id = alloca i32\n call void @fn_noise_cell_scan(i32 %x, i32 %y, i32 %seed, ptr %d2, ptr %id)\n")
emith(" %v = load i32, ptr %d2\n %r = call i32 @fn_fx_sqrt(i32 %v)\n ret i32 %r\n}\n")
emith("define i32 @fn_noise_cellular2_id(i32 %x, i32 %y, i32 %seed) {\n")
emith(" %d2 = alloca i32\n %id = alloca i32\n call void @fn_noise_cell_scan(i32 %x, i32 %y, i32 %seed, ptr %d2, ptr %id)\n")
emith(" %r = load i32, ptr %id\n ret i32 %r\n}\n")
}

File diff suppressed because it is too large Load diff

View file

@ -420,5 +420,14 @@
"uuid-to_text", "uuid-to_text",
"uuid-equals", "uuid-equals",
"uuid-nil" "uuid-nil"
],
"noise": [
"noise-value2",
"noise-perlin2",
"noise-simplex2",
"noise-fbm2",
"noise-cellular2",
"noise-cellular2_id",
"noise-unit"
] ]
} }

View file

@ -30,6 +30,7 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/emit_hash.ludic") push(f, "selfhost/emit_hash.ludic")
push(f, "selfhost/emit_crypto.ludic") push(f, "selfhost/emit_crypto.ludic")
push(f, "selfhost/emit_uuid.ludic") push(f, "selfhost/emit_uuid.ludic")
push(f, "selfhost/emit_noise.ludic")
push(f, "selfhost/emit_list.ludic") push(f, "selfhost/emit_list.ludic")
push(f, "selfhost/emit_ease.ludic") push(f, "selfhost/emit_ease.ludic")
push(f, "selfhost/emit_collide.ludic") push(f, "selfhost/emit_collide.ludic")

View file

@ -101,6 +101,7 @@ function cmd_test() -> int {
# against known-answer vectors (crypto) or structural invariants (uuid/noise). # against known-answer vectors (crypto) or structural invariants (uuid/noise).
feat_case("library/crypto", "", "1 2 3 4 5 6 7 8 9", "crypto.ludic (Crypto SHA-256/HMAC/base64 KAT + CSPRNG shape)") feat_case("library/crypto", "", "1 2 3 4 5 6 7 8 9", "crypto.ludic (Crypto SHA-256/HMAC/base64 KAT + CSPRNG shape)")
feat_case("library/uuid", "", "1 2 3 4 5 6 7 8 9 10", "uuid.ludic (Uuid v4/v7 format, version/variant, parse/equals)") feat_case("library/uuid", "", "1 2 3 4 5 6 7 8 9 10", "uuid.ludic (Uuid v4/v7 format, version/variant, parse/equals)")
feat_case("library/noise", "", "1 2 3 4 5 6 7 8 9 10 11", "noise.ludic (Noise value/perlin/simplex/fbm/cellular determinism + range)")
# issue #9: the Time/Date/Duration/Clock stdlib, driven from its own `entry`. # issue #9: the Time/Date/Duration/Clock stdlib, driven from its own `entry`.
net_case("lang/offline_rewards", "13 650 2026-08-30 0") net_case("lang/offline_rewards", "13 650 2026-08-30 0")