# sx std-library gaps surfaced by m3te

m3te is authored entirely in sx. While building it, a handful of *general-purpose*
utilities had to be hand-rolled because the sx standard library / library modules
don't provide them. This report enumerates those candidates — the ones that
arguably belong in sx `std` (or a library module) — and, for each, records an
explicit cross-check against the real sx library so the list contains **true gaps
only**.

- **sx library checked:** `/Users/agra/projects/sx/library/modules/` —
  `std.sx`, `math/` (`math.sx`, `vector2.sx`, `matrix44.sx`), `std/`
  (`objc.sx`, `cli.sx`, `hash.sx`, `json.sx`, `uikit.sx`, `objc_block.sx`),
  `process.sx`, `fs.sx`, `test.sx`, `ui/` (`types.sx`, `animation.sx`,
  `image.sx`, …), `gpu/`, `platform/`.
- **m3te source read:** `*.sx` (`main.sx`, `board.sx`, `board_view.sx`,
  `board_anim.sx`, `board_fx.sx`, `board_layout.sx`, `gem_anim.sx`, `audio.sx`,
  `swipe.sx`, `build.sx`), `tests/test.sx`, `tools/`.

Highest-value candidates first. Borderline items are flagged. A separate
"Already provided" section lists helpers m3te *also* hand-rolled but which the
library **does** ship (so they are honestly **not** gaps) — included because
the step asks for an honest accounting.

---

## Category: Numerics / parsing

### 1. String → number parsing (`parse_s64`, `parse_f32`)
- **m3te location:** `main.sx:156` (`parse_s64`), `main.sx:122` (`parse_f32`).
- **What it does:** decimal ASCII `string` → `s64` / `f32` (sign + integer +
  optional fractional part).
- **Why general-purpose:** the exact inverse of the formatters everyone already
  uses; needed by any program that reads numbers from argv, env, config, or a
  text file. Nothing game-specific about it.
- **Suggested home:** `modules/std` (alongside `int_to_string` /
  `float_to_string`), or a dedicated `modules/strings`.
- **Not already in sx library (checked):** `std.sx` ships only the *forward*
  direction — `int_to_string` (`std.sx:68`), `uint_to_string`,
  `float_to_string` (`std.sx:121`), `int_to_hex_string` — and **no** parse.
  No `atoi`/`strtol`/`strtod`/`string_to_int` anywhere under
  `library/modules` (grepped). `std/json.sx` has a `Parser.parse_number`
  (`json.sx:547`) but it is a private method consuming a JSON parser's buffer,
  not a reusable `string → number`.

### 2. Seedable deterministic PRNG (`Rng` / LCG)
- **m3te location:** `board.sx:55` (`Rng` struct: `next_u32` `board.sx:59`,
  `next_range` `board.sx:66`), `board.sx:71` (`rng_seeded`), constants
  `board.sx:51-53`.
- **What it does:** a 32-bit linear-congruential generator carried in `s64`
  and masked to 32 bits — `next_u32()`, `next_range(n)` (uniform-ish `[0,n)`),
  `rng_seeded(seed)`.
- **Why general-purpose:** a seedable, reproducible RNG is a textbook stdlib
  primitive (simulations, procedural generation, shuffles, tests). m3te's copy
  is written to be host-width-independent; only its *consumer* (`pick_gem`) is
  game-specific — the generator itself is not.
- **Suggested home:** `modules/math` (or a new `modules/random`).
- **Not already in sx library (checked):** no `rand`/`rng`/`lcg`/`xorshift`/
  `random`/`seed` generator anywhere under `library/modules` (grepped names and
  the classic LCG constants `1664525` / `1013904223`). `std/hash.sx` is SHA-256
  only (content addressing, not a PRNG). `math/math.sx` has `sin`/`cos`/`sqrt`/
  `clamp`/… but no randomness.

---

## Category: FFI / system ergonomics

### 3. NUL-terminated C string (`*u8`) → sx `string` bridge (`from_cstr`)
- **m3te location:** `main.sx:108-117` (`read_env`, copying form: `strlen` +
  `cstring` + `memcpy`); `audio.sx:144-148` (viewing form over a `getcwd`
  buffer using `c_strlen`). libc `strlen` is re-bound in both `main.sx:28` and
  `audio.sx:30`.
- **What it does:** turn a foreign NUL-terminated byte pointer into an sx
  `string` — either by copying (`strlen` + alloc + `memcpy`) or by building a
  length-bounded view.
- **Why general-purpose:** *every* FFI surface that returns `char*` needs this
  (env vars, `getcwd`, `UTF8String`, libc results, …). It is the single most
  repeated FFI chore in m3te and has no game content.
- **Suggested home:** `modules/std` (e.g. `from_cstr(*u8) -> string` plus a
  zero-copy `cstr_view`), and a public `strlen` binding so callers stop
  re-declaring it.
- **Not already in sx library (checked):** `process.env` (`process.sx:86`)
  performs exactly this copy **inline** but does not expose it as a reusable
  helper, and re-binds `strlen` privately (`process.sx:28`). `std/objc.sx`
  exposes `NSString.UTF8String` returning `[*]u8` (`objc.sx:113`) with **no**
  helper to convert it back to a `string`. No public `from_cstr`/`cstr_to_string`
  exists.

### 4. `getcwd` binding / `cwd()` helper
- **m3te location:** `audio.sx:29` (`getcwd` `#foreign` binding), used in
  `load_system_sound` (`audio.sx:141`) to absolutize a bundle-relative path.
- **What it does:** read the process's current working directory into a string.
- **Why general-purpose:** resolving relative paths to absolute is a routine
  filesystem need, unrelated to match-3.
- **Suggested home:** `modules/fs` (next to `basename`/`dirname`) or
  `modules/process`.
- **Not already in sx library (checked):** `fs.sx` binds `open`/`read`/`mkdir`/
  `rename`/… and ships `basename`/`dirname`/`path_join` but **no** `getcwd`.
  `process.sx` binds `getenv`/`popen`/`system` but not `getcwd`. The platform
  modules bind only `chdir` (`platform/uikit.sx:20`, `platform/sdl3.sx:14`),
  never `getcwd`; `platform/bundle.sx` obtains a cwd by shelling out, not via a
  binding. (grepped `getcwd`/`chdir`/`cwd`.)

---

## Category: Graphics (borderline — partly engine-specific)

### 5. Image file / RGBA buffer → GPU texture (`load_texture`, `upload_rgba`)
- **m3te location:** `board_view.sx:132` (`load_texture`: PNG path → texture
  handle), `board_fx.sx:104` (`upload_rgba`: in-memory RGBA buffer → texture
  handle). Both branch `gpu.create_texture` vs a raw-GL fallback.
- **What it does:** decode an image (via `stbi_load`) or take an RGBA buffer and
  upload it as a `.rgba8` texture, returning the handle.
- **Why general-purpose:** "load a PNG into a texture" is needed by any app that
  draws sprites; the decode + `create_texture` core is reusable.
- **Suggested home:** `modules/ui/image.sx` (a loader to complement the existing
  `ImageView`) or `modules/gpu`.
- **Not already in sx library (checked):** `ui/image.sx` defines `ImageView`
  which takes an **already-uploaded** `texture_id` — it does **not** load from
  disk. `stbi_load` appears only in m3te, never in `library/modules/ui`
  (grepped). The GPU layer exposes `gpu.create_texture` but no file/decode
  front-end.
- **Borderline:** the raw-GL fallback branch is engine-specific; only the
  "decode → `create_texture`" half is cleanly general. Worth shipping as the
  GPU-only path; the GL fallback can stay app-side.

---

## Category: Testing (borderline — a stub exists)

### 6. Failing assertion with source location (`expect`)
- **m3te location:** `tests/test.sx:10` (`expect(cond, msg)` → prints
  `FAIL <file>:<line>: <msg>` and exits non-zero via `process.exit`).
- **What it does:** a test assertion that, on failure, reports the caller's
  `file:line` and **fails the process** so a harness/CI catches it.
- **Why general-purpose:** every sx test suite needs a failing assertion;
  m3te's whole `tests/` gate is built on this one helper.
- **Suggested home:** `modules/test`.
- **Partially exists (checked):** `modules/test.sx` ships only
  `assert(condition)` which prints `"assertion failed\n"` and **does not exit**
  (`test.sx:3-7`) — useless as a gate. `process.assert(cond, msg)`
  (`process.sx:148`) *does* report `file:line` and exit 1, so the capability is
  half-present but split awkwardly: the dedicated test module can't fail a run,
  and the failing variant lives in `process`. A real `modules/test` `expect`/
  `assert` that reports location and fails is the gap.

---

## Already provided by the sx library — NOT gaps (honest accounting)

m3te also re-implemented or could have reused these; the library **does** ship
them, so they are explicitly **not** counted above:

- **`clamp` / `lerp` / `min` / `max` / `abs` / `sign`** — m3te uses these
  throughout (e.g. `gem_anim.sx:45`, `board_layout.sx:20`, `swipe.sx:31`) and
  they all come from `math/math.sx:11-37`. Not hand-rolled; not a gap.
- **`read_env`** (`main.sx:108`) — duplicates `process.env` (`process.sx:86`)
  almost line-for-line. m3te re-declares its own `getenv` (`main.sx:27`) instead
  of importing `process`, but the capability exists. Not a gap (it motivates #3,
  the *reusable* `from_cstr` underneath it).
- **`ease_out_cubic` / `ease_in_quad`** (`board_anim.sx:28-29`) — identical to
  `ui/animation.sx:19` and `ui/animation.sx:13`. Re-implemented only because
  `board_anim` imports `ui/types` but not all of `ui/animation`. Not a gap.
  *Placement note:* these are pure curves and arguably belong in `modules/math`
  too, so non-UI code can reach them without pulling in the UI module.
- **`fx_lerp_u8` / per-channel colour lerp** (`board_fx.sx:97`) — `Color.lerp`
  (`ui/types.sx:124`) already lerps all four channels. Not a gap.
- **NSString / `NSLog` bridging** (used in `audio.sx`) — `std/objc.sx` ships
  `NSLog` (`objc.sx:75`) and an `Into(*NSString) for string` (`objc.sx:119`),
  which m3te uses via `xx`. Not a gap.

---

## Summary (top gaps, ranked)

1. **String → number parsing** (`parse_s64` / `parse_f32`) — clean, universally
   needed, the missing inverse of the existing formatters.
2. **Seedable PRNG** (`Rng` LCG) — textbook stdlib primitive, entirely absent.
3. **`from_cstr` C-string bridge** (+ public `strlen`) — the most-repeated FFI
   chore; `process.env` inlines it but never exposes it.
4. **`getcwd` / `cwd()`** — only `chdir` is bound; the read side is missing.
5. **Image-file → GPU texture loader** — borderline; the decode-and-upload core
   is general, the GL fallback is app-side.
6. **`expect` test assertion** — borderline; capability is split between a
   non-failing `test.assert` stub and `process.assert`.
