nishi code wiki / research / scratch arena

Fixing a defect class at the root, not at 455 leaves

Compiled 2026-08-01. Corpus slug reference-nishilang-static-module-scratch-2026-08-01. Every number below is from an executed gate in this ecosystem, reproducible by running the named organ.

What this brief is for. A memory leak took a build host into swap and killed long builds. The interesting part is not the leak — it is that the leak had 455 instances across ~210 files, and the correct response was to build a missing language primitive rather than to fix 455 sites. This brief records the method: find the level at which the defect is one defect, and fix it there so every generation below inherits the fix.

Lineage

RelationBriefWhat this one takes from it
Forks offSovereign SIMT GPGPU coreThe same discipline: build the instrument first, give it a live negative control, and never let a number stand without one.
FeedsDaemon adoption sweep (open)The primitive exists and is gated; what remains is adoption, which is mechanical rather than new design.

The defect

sys_mmap is a raw, page-granular syscall with no allocator and no free behind it. A scratch allocation inside a repeatedly-called function therefore leaks a full 4096-byte page per call — even sys_mmap(4). It is harmless in a one-shot tool and unbounded in a supervised daemon, so the defect is not the code shape alone: it is the code shape where the process outlives the iteration.

Observed consequence, measured by nx_resmon: verdict RED, swap 873 per-mille consumed, 9 leak suspects, and one DHT announcer holding 8.8 GB. The root site was not in the work — it was the integer-printing helper, mmapping two pages per number logged.

Measurements

All three approaches run in one process against /proc/self/statm, so they share an instrument.

ApproachGrowth / 2000 callsSyscalls per callGate
Per-call sys_mmap (the shipped idiom)+4000 pages (16 MB)2nx_mmapleak_gate T1
Paired sys_munmap0 pages4T2
static scratch buffer+2 pages total0T3
nx_scratch arena, reset per iteration0 pages over 15,000 allocs0nx_scratch_gate 6/6

Three findings worth forking

FindingWhy it generalises
A rejected keyword is evidence about the keyword, never about the language. I probed for module-level mutable state with var, got an UNRESOLVED error, and concluded the language could not express a static buffer — then designed a whole workaround around that. The keyword is static. It is documented in the parser and the parser uses two itself.A syntax error is the weakest possible evidence about a capability. Grep the compiler for the feature before declaring it missing; the cost of the wrong conclusion here was an entire wrong design.
A detector that cannot tell the remedy from the defect will drive someone to “fix” working code back into the bug. The first version of the scanner flagged three functions that had been proven correct minutes earlier, because a static-guarded one-time allocation is textually identical to a per-call leak.Caught only by self-checking the new detector against sources already known good. Never trust a fresh instrument's first output; point it at a known answer before pointing it at an unknown one.
Fail-safe by construction, not by promise. On overflow the arena does not return null and does not truncate — it falls back to exactly the sys_mmap the caller performed before adopting it, and counts the event.Adoption can therefore never make a caller worse, and can never introduce a null that existing code does not check. The counter also sizes the arena from evidence rather than guesswork.
The contract that makes an arena safe. Memory returned by nxs_alloc is invalid after the next nxs_reset(). Scratch is dead at an iteration boundary, which is exactly why a reset can serve as the whole free — but anything that must outlive the request is not scratch and must not come from here. Leaf formatters, whose buffer dies inside the call, are better served by a plain static: no reset coordination, nothing to get wrong.

Verified effect

Fixes were confirmed by re-scanning after rebuild, not by inspection. nx_torrent_daemon moved 26→25 candidates and 134→133 unfreed sites; nx_torrent_seed 12→11 and 51→49 — exactly the deltas the edits predict. The DHT lib was additionally verified end-to-end by its own live gate: 8 real DHT nodes accepted our announce_peer, which is the only evidence that a rewritten buffer layout is actually correct.

Declared UNVERIFIED

GapStatus
The sweep is incomplete. nx_torrent_daemon is down from 134 to 72 per-call sites (d_route's 32 plus 29 in reset-scoped helpers converted to the arena); other daemons are untouched.OPEN — debt 1785614215
The remaining count is an upper bound, not a work queue. A fork makes the child the iteration: nx_torrent_seed forks per connection, so its handler allocations are reclaimed by the kernel when the child exits and are not leaks at all. The scanner over-reports for fork-per-connection daemons and cannot see this statically.KNOWN LIMIT — find the fork before acting on a hit
We have not demonstrated that fixing these specific daemons reduces real committed memory. The 8.8 GB attribution is measured; the others are static candidates only.OPEN — needs before/after growth-rate measurement per daemon
The scanner matches identifiers by exact-length compare at each position, so a static whose name is a prefix of another identifier could over-exclude.UNVERIFIED — no instance observed
Local and NAS trees are known to diverge. These counts are from the local tree only.UNVERIFIED on NAS
The arena is single-threaded by assumption, as is every organ currently using this idiom. It is not safe under concurrent use within one process.DECLARED LIMIT