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 }