Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion proposals/MIGRATION-PLAN.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ Heuristic:
| C2 wave 2a | DONE | Scalar multiply/divide (2026-06-05, Opus). `Mul`/`Div` turned out to be pure *scalar* value-transforms (not memory ops), so they need *no* array ABI — split out of "wave 2" and landed now. *1 kernel* `VmMulDiv` (11 exports) under `proposals/idaptik/migrated/`. Brain = the reversible ancilla multiply (`c := c + a*b`, inverse `c := c - a*b`) + the quotient/remainder divide whose dividend is reconstructable (`q*b + r == a` for all b incl. 0); the intentional-flaw in-place/simple variants migrated as value transforms with no rt==id claim. *Four gates green:* 1/1 compile, *3322/3322 parity* (incl. mul reversibility roundtrip + div reconstruction), G3 n/a (numeric transform), 1/1 assail-clean. *2 new i32 ABI facts:* (a) JS `a*b` loses precision >2^53 → oracle must use `Math.imul` to match `i32.mul`; (b) `i32.div_s` TRAPS on `b==0` and `INT_MIN/-1` (ReScript wraps the latter) → brain guards both (nested-`if`, avoiding the unverified `&&` codegen path) so the wasm is total. Evidence: `migrated/EVIDENCE-C2-wave2a.adoc`. NEXT: C2 wave 2b.
| C2 wave 2b | DONE | Memory/stack/port/control opcodes (2026-06-05, Opus). *The "needs an array/linear-memory ABI" premise was WRONG and re-decomposition overturned it:* reading the sources, the VM's memory/stack/port buffers are not arrays in the brain — `VmState.res` stores every one as string-keyed dict slots (`_mem:N`, `_s:N`, `_pin:port`), i.e. host-side STATE (senses). The integer brains are all scalar. *4 kernels* `VmMemory` (LOAD/STORE), `VmStack` (PUSH/POP), `VmPort` (RECV/SEND), `VmControl` (IF_POS/IF_ZERO/LOOP) covering *9 opcodes*. *Four gates green:* 4/4 compile, *1568/1568 parity* (incl. load/store/sp/recv/send round-trips), G3 n/a (transforms/predicates), 4/4 assail-clean. No-kernel senses (classified, not migrated): Call (orchestration), CoprocessorCall (tombstone — never implemented), State/VmState/VM/SubroutineRegistry/VmStateCoprocessor/InstructionCoprocessor (state containers / bridges). Evidence: `migrated/EVIDENCE-C2-wave2b.adoc`. *No array ABI was required* (the blocker was a triage-bucket artefact). *Cluster C2 COMPLETE.* NEXT: C3.
| C3 | DONE | Coprocessor wiring layer (2026-06-05, Opus) — *classified, no new brains to extract.* C3's 17 `src/shared/` files are the host-side bridge layer that exposes C1's already-migrated wasm brains to the game engine. Verified de-dup (by constant, not name): the integer cores C3 needs (`compute_gate`/`data_limit_for_domain` 513/16/259/1024, `max_concurrent_compute` 10, `is_transient` 503/504/429, policy table, DeviceType/GameEvent ordinals) are *all* already migrated + 4-gate-verified in C1. The 12 `*Coprocessor.res` are pure-FFI passthrough (0 logic lines); `Kernel_Compute`/`RetryPolicy` (src/shared) are async/float wrappers over C1 brains; `DeviceType`/`GameEvent` add string-gated ops (string wall, Phase F). No 4-gate run (nothing new to verify). Bridges stay host-side as the essential wasm-loading glue (ADDITIVE, FeaturePacks-flagged); their ReScript→glue fate is a Phase-Ω cutover decision. Evidence: `migrated/EVIDENCE-C3.adoc`. NEXT: C5.
| C5..C12 | TODO | Remaining leaf-brain clusters per `migration-map.json`: *C5 (11, engine coprocessors — maths/random/navigation/resize/audio-state)* and *C11 (10, "pure integer presentation state" — font scaling/keyboard-nav/popup logic)* are the highest brain-yield; C9 (16, game-loop/AffineTEA/VeriSim types — mixed) and C10 (21, utils/tools/companions/narrative/proven — mixed); C12 (43, render-glue screens/PixiJS bindings — senses-heavy, migrated last). Established scalar/enum/predicate recipe. Genuine compiler gates remain: string wall (52 non-test .res), effect wall (110); unary-`~` codegen bug is a candidate Phase-F fix.
| C5 | DONE | Engine coprocessors (2026-06-05, Opus). *2 integer brains extracted + 9 senses classified.* `Maths` (clamp/lerp-milli/dist_sq — float-wall convention, sqrt host-side) and — the strategic one — `Random`, the deterministic **xmur3 + mulberry32 PRNG**, the multiplayer-reproducibility backbone (host iterates the seed string + does the final u32→[0,1) float div; brain owns the i32 mixing). *Four gates green:* 2/2 compile, *2108/2108 parity* (Random **bit-exact** vs the JS source over the full i32 domain + UTF-16 code units, incl. the `>>>` emulation + signed-i32 multiplier constants), G3 n/a (transforms/mixing), 2/2 assail-clean. Senses (classified, not migrated): 5 `*Coprocessor` FFI bridges, Resize/GetResolution (float+DOM), Audio (Pixi), Navigation (screen-stack orchestration + async asset load, effect-gated), WaitFor (timing). Evidence: `migrated/EVIDENCE-C5.adoc`. NEXT: C11.
| C9..C12 | TODO | Remaining clusters per `migration-map.json`: *C11 (10, "pure integer presentation state" — font scaling/keyboard-nav/popup logic)* next (highest brain-yield); then C9 (16, game-loop/AffineTEA/VeriSim types — mixed) and C10 (21, utils/tools/companions/narrative/proven — mixed); C12 (43, render-glue screens/PixiJS bindings — senses-heavy, migrated last). Established scalar/enum/predicate recipe. Genuine compiler gates remain: string wall (52 non-test .res — note the `[len:i32][utf8]` layout is already in `codegen.ml:375`, only the *ops* are missing), effect wall (110); unary-`~` codegen bug is a candidate Phase-F fix.
| F+ | TODO | Compiler walls (string backend, then effects).
| Ω | TODO (access-gated) | Cutover + ReScript extinction.
|===
Expand Down
94 changes: 94 additions & 0 deletions proposals/idaptik/migrated/EVIDENCE-C5.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
= Cluster C5 (engine coprocessors) — four-gate evidence (captured 2026-06-05)
:toc: macro

[IMPORTANT]
====
*Two integer brains extracted; nine files classified as senses.* C5 is the
engine-coprocessor layer (audio, navigation, resize, resolution, maths, random,
storage). The brains are `Maths` and — the strategically important one —
`Random`, the deterministic xmur3 + mulberry32 PRNG that is the multiplayer
reproducibility backbone. The rest are senses: FFI bridges, DOM-coupled display
math, Pixi audio state, screen-stack orchestration, and timing. Toolchain:
AffineScript compiler `_build/default/bin/main.exe`, Deno 2.8.2.
====

toc::[]

== Summary

[cols="2,2,1,1,2,1",options="header"]
|===
| Kernel | Source | G1 | G2 parity | G3 | G4 assail
| `Maths` | `src/engine/utils/Maths.res` | OK | 1982/1982 | n/a (transform) | clean
| `Random` | `src/engine/utils/Random.res` | OK | 126/126 | n/a (mixing) | clean
| *Total* | | *2/2* | *2108/2108* | *n/a* | *2/2 clean*
|===

== Random — the deterministic PRNG (bit-exact, the headline)

`Random` is the multiplayer-reproducibility backbone: identical seed -> identical
stream, bit-for-bit, on every client. Per the brain/senses split the host
iterates the seed string (feeding UTF-16 code units to `xmur3_step`) and does the
final `u32 -> [0,1)` float division; the brain owns only the i32 mixing. Six
exports, matching the `RandomCoprocessor.res` bridge surface exactly:

* `xmur3_init`, `xmur3_step`, `xmur3_finalise` — the seed hash.
* `mulberry32_advance`, `mulberry32_output`, `mulberry32_draw_raw` — the generator.

*Parity is bit-exact against the ORIGINAL `Random.res`* — each oracle re-derives
the source with JS-native `Math.imul` and `>>>` (the source of truth), over the
full i32 domain plus real UTF-16 code-unit values. 126/126. If `xmur3_step`
matches for every `(h, c)` the folded multi-char hash matches by induction, and
the host loop never leaves i32. Two compiler facts were written around:

* `*` == `i32.mul` == `Math.imul` (32-bit truncating multiply).
* `>>>` is emulated via `lsr(x,n) = (x >> n) & ((1 << (32-n)) - 1)` (per
VmBitwise); the u32 multiplier constants are written as their signed-i32 bit
patterns (`3432918353 = -862048943`, etc.) — identical under `i32.mul`.

This is exactly the kind of core-correctness logic to lock into verified wasm
before the game is handed on: a desync-proof shared RNG, provably matching the
legacy stream so the FeaturePacks flag can flip with zero behavioural drift.

== Maths — integer utilities

`clamp` (integer, swap-if-inverted), `lerp_milli` (milli-unit blend; host
pre-scales `t` by 1000), `dist_sq` (squared Euclidean distance — the integer core
of `getDistance`; the `sqrt` stays host-side, and games compare squared distances
anyway). 1982/1982 over moderate domains chosen so the blend and squared terms
never overflow i32 (these are screen/value magnitudes). The ReScript original is
float throughout; this is the float-wall convention (integer brain, floats stay
host-side).

== Senses (9 files — classified, not migrated)

[cols="2,3",options="header"]
|===
| File | Class
| `ResizeCoprocessor.res`, `GetResolutionCoprocessor.res`, `MathsCoprocessor.res`, `RandomCoprocessor.res`, `StorageCoprocessor.res` | FFI bridges — load the wasm, expose the brain across the boundary in i32. *Senses.*
| `Resize.res` | Canvas-size computation from `window.innerWidth/innerHeight` (DOM reads) + float aspect-ratio/scale math. Display-layout — the player's window size *is* a sense. *Senses / float-gated.*
| `GetResolution.res` | Reads `window.devicePixelRatio` (DOM) + float. *Senses.*
| `Audio.res` | Pixi sound state (BGM/SFX). 2 trivial logic lines, no separable integer brain. *Senses.*
| `Navigation.res` | Screen/popup *stack orchestration* with async asset preloading — manages UI objects with lifecycle methods (prepare/show/hide/...), `Promise`-based loading. Effect-gated orchestration over objects, not a pure-integer brain (51 logic lines are array/option/lifecycle plumbing). *Senses / effect-gated.*
| `WaitFor.res` | Timing (`setTimeout`/clock). *Senses / effect-gated.*
|===

== Gate detail

* *G1 compile* — `main.exe compile <k>.affine -o <k>.wasm` → WASM (Maths 346 B,
Random 507 B).
* *G2 parity* — 2108/2108. Maths over moderate domains; Random bit-exact over the
full i32 domain + UTF-16 code units.
* *G3 boundary* — n/a. Numeric transforms / bit-mixing with no discrete
value↔integer encoding table; the G2 bit-exact round-trip against the
independent JS-source oracle is the faithfulness check.
* *G4 assail* — 0 findings on both. No enum decoders; the PRNG is pure
arithmetic; PA-AFF-001 does not apply.

== Cluster C5 status: DONE

2 brains migrated + 4-gate-verified (incl. the deterministic PRNG backbone);
9 senses classified. NEXT per the completion-drive directive: C11 ("pure integer
presentation state").
36 changes: 36 additions & 0 deletions proposals/idaptik/migrated/Maths/Maths.affine
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// Maths -- the pure-integer math utilities of the idaptik engine, the brain
// extracted from src/engine/utils/Maths.res. The ReScript original is Float
// (getDistance/lerp/clamp); per the float-wall convention the brain is
// integer-native: clamp on integers, lerp in milli-units (the host pre-scales t
// by 1000), and the squared distance dist_sq. The sqrt that finishes
// getDistance stays host-side -- the default wasm backend has no float sqrt, and
// distance comparisons use the square anyway. "The integer IS the magnitude."

fn imin(a: Int, b: Int) -> Int { if a < b { a } else { b } }
fn imax(a: Int, b: Int) -> Int { if a > b { a } else { b } }

// clamp v into [lo, hi]; if the bounds are inverted, swap them first (matching
// the ReScript clamp's min>max guard). Integer-exact, no float.
pub fn clamp(v: Int, lo: Int, hi: Int) -> Int {
let mn = imin(lo, hi);
let mx = imax(lo, hi);
imax(mn, imin(v, mx))
}

// lerp in milli-units: t_milli in [0,1000] represents t in [0,1]. Returns the
// truncated integer blend (1-t)*a + t*b. The host pre-scales t by 1000; integer
// division truncates toward zero, matching ReScript Float.toInt on the blend.
pub fn lerp_milli(a: Int, b: Int, t_milli: Int) -> Int {
((1000 - t_milli) * a + t_milli * b) / 1000
}

// squared Euclidean distance between (ax,ay) and (bx,by) -- the integer core of
// getDistance. The final sqrt is a host op (senses); games compare squared
// distances directly, so the brain rarely needs the root at all.
pub fn dist_sq(ax: Int, ay: Int, bx: Int, by: Int) -> Int {
let dx = bx - ax;
let dy = by - ay;
dx * dx + dy * dy
}
25 changes: 25 additions & 0 deletions proposals/idaptik/migrated/Maths/maths.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MPL-2.0
// hypatia: allow cicd_rules/javascript_detected -- Deno trial component for nextgen-evangelist; production target is Rust/AffineScript (see proposals/nextgen-evangelist/README.adoc)
//
// affine-parity config for Maths.affine (integer math utilities; scalar i32
// ABI). Oracles re-derive the integer core of src/engine/utils/Maths.res
// (clamp; lerp in milli-units; squared distance). Domains kept moderate so the
// lerp blend and the squared terms never overflow i32 (these are screen/value
// magnitudes, not stress cases). i32-normalised both sides.
const C = { values: [-1000, -7, -1, 0, 1, 7, 1000] }; // clamp / lerp operands
const T = { values: [0, 1, 250, 500, 750, 999, 1000] }; // t in milli-units [0,1000]
const K = { values: [-1000, -7, 0, 1, 7, 1000] }; // coordinates for dist_sq
const i32 = (x) => x | 0;
const imul = (a, b) => Math.imul(a, b);

export default {
affine: "Maths.affine",
cases: [
{ name: "clamp v into [lo,hi] (swap if inverted)", export: "clamp", args: [C, C, C],
oracle: (v, lo, hi) => { const mn = Math.min(lo, hi), mx = Math.max(lo, hi); return Math.max(mn, Math.min(v, mx)); } },
{ name: "lerp_milli (1-t)a+tb truncated", export: "lerp_milli", args: [C, C, T],
oracle: (a, b, tm) => Math.trunc(((1000 - tm) * a + tm * b) / 1000) },
{ name: "dist_sq dx*dx+dy*dy", export: "dist_sq", args: [K, K, K, K],
oracle: (ax, ay, bx, by) => { const dx = bx - ax, dy = by - ay; return i32(imul(dx, dx) + imul(dy, dy)); } },
],
};
58 changes: 58 additions & 0 deletions proposals/idaptik/migrated/Random/Random.affine
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// Random -- the deterministic seeded-PRNG brain of the idaptik engine, the pure
// i32 core extracted from src/engine/utils/Random.res (xmur3 seed hash +
// mulberry32 generator). This is the MULTIPLAYER-REPRODUCIBILITY backbone:
// identical seed -> identical stream, bit-for-bit, on every client. The brain
// owns ONLY the integer mixing; the host iterates the seed string (feeding code
// units to xmur3_step) and does the final u32 -> [0,1) float division (the wasm
// backend has no int->float). Surface matches RandomCoprocessor.res exactly.
//
//## Two compiler facts this kernel is written around
// * `*` == i32.mul == JS Math.imul (32-bit truncating multiply), so the
// `imul(...)` calls in the source become plain `*`.
// * `>>>` (unsigned shift-right) is not native; emulated via lsr (per
// VmBitwise): lsr(x,n) = (x >> n) & ((1 << (32-n)) - 1) for 1 <= n <= 31.
// The u32 multiplier constants exceed i32, so they are written as their
// signed-i32 bit patterns (identical under i32.mul):
// 3432918353 = -862048943 2246822507 = -2048144789 3266489909 = -1028477387
// 1779033703 and 0x6d2b79f5 (= 1831565813) fit i32 directly.

fn lsr(x: Int, n: Int) -> Int { (x >> n) & ((1 << (32 - n)) - 1) }

// --- xmur3 string-seed hash (host feeds the string one code unit at a time) ---
// init: h := 1779033703 ^ length
pub fn xmur3_init(len: Int) -> Int { 1779033703 ^ len }

// step: fold one UTF-16 code unit c into the accumulator h
// h := imul(h ^ c, 3432918353); h := (h << 13) | (h >>> 19)
pub fn xmur3_step(h: Int, c: Int) -> Int {
let m = (h ^ c) * (-862048943);
(m << 13) | lsr(m, 19)
}

// finalise: avalanche to the u32 seed bit pattern
// h := imul(h ^ (h>>>16), 2246822507); h := imul(h ^ (h>>>13), 3266489909)
// return h ^ (h>>>16)
pub fn xmur3_finalise(h: Int) -> Int {
let h1 = (h ^ lsr(h, 16)) * (-2048144789);
let h2 = (h1 ^ lsr(h1, 13)) * (-1028477387);
h2 ^ lsr(h2, 16)
}

// --- mulberry32 generator (host threads the state across draws) ---
// advance: a := (a + 0x6d2b79f5) | 0
pub fn mulberry32_advance(a: Int) -> Int { a + 1831565813 }

// output: derive the raw u32 draw from an advanced state
// t := a; t := imul(t ^ (t>>>15), t | 1); t ^= t + imul(t ^ (t>>>7), t | 61)
// return t ^ (t>>>14)
pub fn mulberry32_output(a: Int) -> Int {
let t1 = (a ^ lsr(a, 15)) * (a | 1);
let t2 = t1 ^ (t1 + (t1 ^ lsr(t1, 7)) * (t1 | 61));
t2 ^ lsr(t2, 14)
}

// composite per-draw raw u32: advance THEN output -- the host's inner loop body.
// (The float division to [0,1) is host-side; this returns the raw u32 bits.)
pub fn mulberry32_draw_raw(state: Int) -> Int { mulberry32_output(mulberry32_advance(state)) }
39 changes: 39 additions & 0 deletions proposals/idaptik/migrated/Random/random.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MPL-2.0
// hypatia: allow cicd_rules/javascript_detected -- Deno trial component for nextgen-evangelist; production target is Rust/AffineScript (see proposals/nextgen-evangelist/README.adoc)
//
// affine-parity config for Random.affine (deterministic xmur3 + mulberry32
// PRNG; scalar i32 ABI). Each oracle re-derives the ORIGINAL src/engine/utils/
// Random.res mixing using JS-native Math.imul and >>> -- the source of truth.
// This is the multiplayer-reproducibility backbone, so the sweep is bit-exact
// over the full i32 domain (accumulators are arbitrary i32; code units add
// real UTF-16 values). If xmur3_step matches for every (h, c) the folded
// multi-char hash matches by induction; the host loop never leaves i32.
const I = { values: [-2147483648, -1000000, -7, -1, 0, 1, 7, 1000000, 2147483647] };
const CU = { values: [0, 1, 65, 97, 255, 256, 65535, -1, 2147483647] }; // UTF-16 code units + extremes
const i32 = (x) => x | 0;

const xmur3_finalise = (h) => {
let h1 = Math.imul(h ^ (h >>> 16), 2246822507);
let h2 = Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return i32(h2 ^ (h2 >>> 16));
};
const mulberry32_output = (a) => {
let t1 = Math.imul(a ^ (a >>> 15), a | 1);
let t2 = t1 ^ (t1 + Math.imul(t1 ^ (t1 >>> 7), t1 | 61));
return i32(t2 ^ (t2 >>> 14));
};

export default {
affine: "Random.affine",
cases: [
// --- xmur3 seed hash ---
{ name: "xmur3_init 1779033703^len", export: "xmur3_init", args: [I], oracle: (len) => i32(1779033703 ^ len) },
{ name: "xmur3_step fold one code unit", export: "xmur3_step", args: [I, CU],
oracle: (h, c) => { const m = Math.imul(h ^ c, 3432918353); return i32((m << 13) | (m >>> 19)); } },
{ name: "xmur3_finalise avalanche -> u32 seed", export: "xmur3_finalise", args: [I], oracle: xmur3_finalise },
// --- mulberry32 generator ---
{ name: "mulberry32_advance (a+0x6d2b79f5)|0", export: "mulberry32_advance", args: [I], oracle: (a) => i32(a + 1831565813) },
{ name: "mulberry32_output raw u32 draw", export: "mulberry32_output", args: [I], oracle: mulberry32_output },
{ name: "mulberry32_draw_raw advance+output", export: "mulberry32_draw_raw", args: [I], oracle: (s) => mulberry32_output(i32(s + 1831565813)) },
],
};
11 changes: 10 additions & 1 deletion proposals/idaptik/migration-map.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,16 @@
"id": "C5",
"name": "Engine coprocessors",
"description": "Core engine coprocessors: audio state, navigation, resize, maths, random, storage.",
"status": "TODO",
"status": "DONE",
"done": {
"date": "2026-06-05",
"phase": "G",
"kernels": ["Maths", "Random"],
"note": "2 integer brains + 9 senses. Random = deterministic xmur3+mulberry32 PRNG (multiplayer-reproducibility backbone); host iterates the seed string + does the final u32->[0,1) float div, brain owns the i32 mixing. Maths = clamp/lerp-milli/dist_sq (float-wall convention; sqrt host-side).",
"gates": "2/2 compile; 2108/2108 parity (Random bit-exact vs JS source over full i32 + UTF-16 code units); G3 n/a; 2/2 assail-clean",
"evidence": "proposals/idaptik/migrated/EVIDENCE-C5.adoc",
"senses": ["ResizeCoprocessor", "GetResolutionCoprocessor", "MathsCoprocessor", "RandomCoprocessor", "StorageCoprocessor", "Resize (float+DOM)", "GetResolution (float+DOM)", "Audio (Pixi)", "Navigation (screen-stack orchestration, effect-gated)", "WaitFor (timing)"]
},
"files": [
"src/engine/audio/Audio.res",
"src/engine/navigation/Navigation.res",
Expand Down
Loading