code wiki / (root) / nx_scratch.nx

nx_scratch.nx source

↩ module page · 82 lines · 3365 B

1// nx_scratch.nx -- SOVEREIGN SCRATCH ARENA. The god-level fix for the unfreed-mmap class. 2// 3// THE ROOT DEFECT, stated plainly: NishiLang has no scratch allocator. `sys_mmap` is a raw 4// page-granular syscall with NO allocator and NO free behind it, so every organ that needs a 5// temporary buffer hand-rolls `sys_mmap(N)` -- and every one of them re-derives the same bug. That 6// is 455 occurrences across ~210 files. Fixing them one at a time is 455 chances to get it wrong; 7// fixing the missing primitive is one chance to get it right, inherited by everything below. 8// 9// MEASURED cost of the status quo (nx_mmapleak_gate): an unpaired scratch mmap leaks a full 10// 4096-byte page per call -- even sys_mmap(4) -- so 2000 calls grow a process by 4000 pages. In a 11// supervised daemon that is unbounded: nx_seed_announce_all reached 8.8 GB committed and drove the 12// build host into swap. 13// 14// MODEL: one process-lifetime arena, bump-allocated, reset at the top of each work iteration 15// (per request, per sweep, per connection). Scratch is by definition dead at the end of an 16// iteration, so a reset is the whole free. 17// 18// ★FAIL-SAFE BY CONSTRUCTION: on overflow nxs_alloc does NOT return null and does NOT truncate -- 19// it falls back to exactly the sys_mmap the caller performed BEFORE adopting this library, and 20// counts it. So adoption can never make a caller worse or introduce a null it does not check, and 21// the arena gets sized from evidence (nxs_overflows) instead of guesswork. Refuse-and-report, never 22// silently shrink. 23// 24// ⚠CONTRACT: memory returned by nxs_alloc is INVALID after the next nxs_reset(). Never hold it 25// across an iteration boundary, and never use it for state that must outlive the request. 26// license_tier: ORIGINAL layer: runtime-core module: nishi-core.runtime.scratch 27import "nx_syscalls.nx" 28 29const NXS_CAP_DEFAULT: i64 = 1048576 30const NXS_ALIGN: i64 = 16 31 32static NXS_BASE: i64 33static NXS_OFF: i64 34static NXS_CAP: i64 35static NXS_HWM: i64 36static NXS_OVF: i64 37 38// Allocate the arena once. Idempotent: a second call is a no-op, so callers may init defensively. 39func nxs_init(cap: i64) -> i64 { 40 if NXS_BASE != 0 { return NXS_BASE } 41 var c: i64 = cap 42 if c <= 0 { c = NXS_CAP_DEFAULT } 43 NXS_BASE = sys_mmap(c) as i64 44 NXS_CAP = c 45 NXS_OFF = 0 46 return NXS_BASE 47} 48 49// End of a work iteration: everything handed out since the last reset becomes reusable. 50func nxs_reset() -> i64 { 51 if NXS_OFF > NXS_HWM { NXS_HWM = NXS_OFF } 52 NXS_OFF = 0 53 return 0 54} 55 56func nxs_alloc(n: i64) -> *u8 { 57 if NXS_BASE == 0 { nxs_init(NXS_CAP_DEFAULT) } 58 var need: i64 = n 59 if need <= 0 { need = 1 } 60 need = ((need + NXS_ALIGN - 1) / NXS_ALIGN) * NXS_ALIGN 61 if NXS_OFF + need > NXS_CAP { 62 NXS_OVF = NXS_OVF + 1 63 return sys_mmap(need) 64 } 65 let p: i64 = NXS_BASE + NXS_OFF 66 NXS_OFF = NXS_OFF + need 67 if NXS_OFF > NXS_HWM { NXS_HWM = NXS_OFF } 68 return p as *u8 69} 70 71// Zeroed variant, for callers that relied on fresh mmap pages already being zero. 72func nxs_alloc0(n: i64) -> *u8 { 73 let p: *u8 = nxs_alloc(n) 74 var i: i64 = 0 75 while i < n { p[i] = 0 as u8; i = i + 1 } 76 return p 77} 78 79func nxs_used() -> i64 { return NXS_OFF } 80func nxs_cap_bytes() -> i64 { return NXS_CAP } 81func nxs_hwm() -> i64 { return NXS_HWM } 82func nxs_overflows() -> i64 { return NXS_OVF }