code wiki / (root) / nx_arena.nx

nx_arena.nx source

↩ module page · 125 lines · 5171 B

1// nx_arena.nx -- bump-pointer arena allocator. 2// 3// Today every "give me a buffer" inside the runtime calls sys_mmap. 4// On Linux that mmaps a fresh page (4 KiB) per allocation -- fine 5// for a handful of structs, terrible when, e.g., the parser allocs 6// thousands of small Token / IR-value records. Page fragmentation 7// + syscall overhead silently dominate. 8// 9// nx_arena solves the small-allocation problem the right way for a 10// bottom-up systems language: pre-mmap N pages, then hand out 11// aligned chunks via a bump pointer. Reset frees the whole arena 12// at once (perfect for "scope-bound" allocations like a parse pass). 13// 14// This file is the CONVENIENCE layer: nx_arena_new() does the 15// sys_mmap so callers that don't care about syscall ABI portability 16// can just call it. Callers that need to compose nx_arena with a 17// DIFFERENT syscall layer (e.g., nx_syscalls_x86_64.nx for native 18// exec) import nx_arena_types.nx directly + provide their own 19// backing buffer via nx_arena_init. See cardinal user-owns-every-bit 20// + four-pillar fix 2026-05-16. 21// 22// Why now (decade-horizon framing): 23// * Determinism -- predictable allocation pattern means the F6 24// manifest stays stable across alloc-pattern changes upstream. 25// * Bug surface -- one allocator instead of N call sites means 26// one place to add poisoning, guard pages, alloc-tracing, 27// fuzz instrumentation. 28// * Migration target -- when MemCap (Phase B) lands, every 29// arena_alloc returns a MemCap with bounds prefilled. All 30// existing code transparently gains capability bounds-check. 31// 32// Not yet (deferred): 33// * Multiple chunks (grow when full). v0.0.1 single chunk. 34// * Free-list or per-size pool. v0.0.1 bump only. 35// * Guard pages between large allocs. Add when first reproed 36// OOB hits arena. 37// * Threading. v0.0.1 single producer. 38 39// nx_safety_envelope: 40// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 41// sil_target: SIL1 42// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 43// verdict: NOT_YET_EVALUATED 44 45import "nx_arena_types.nx" 46import "nx_syscalls.nx" 47import "nx_assert.nx" 48const K_MAGIC_1024: i64 = 1024 49 50// --- construction (syscall-using convenience) ---------------------- 51 52// Convenience constructor: mmaps the NxArena struct + cap bytes. 53// Callers that want a different syscall ABI (e.g., x86_64 native exec) 54// should import nx_arena_types.nx directly + allocate the bytes 55// themselves + call nx_arena_init(a, base, cap). 56func nx_arena_new(cap: i64) -> *NxArena { 57 nx_assert(cap > 0, "nx_arena_new: zero cap" as *u8) 58 let raw: *u8 = sys_mmap(NX_ARENA_BYTES) 59 let a: *NxArena = raw as *NxArena 60 let base: *u8 = sys_mmap(cap) 61 nx_arena_init(a, base, cap) 62 return a 63} 64 65// --- self-test ------------------------------------------------------ 66 67func main() -> i64 { 68 let a: *NxArena = nx_arena_new(256) 69 if nx_arena_used(a) != 0 { return __syscall(93, 10, 0, 0, 0, 0, 0) } 70 71 // Three small aligned allocs. 72 let p1: *u8 = nx_arena_alloc(a, 16, 8) 73 if p1 == (0 as *u8) { return __syscall(93, 11, 0, 0, 0, 0, 0) } 74 if nx_arena_used(a) != 16 { return __syscall(93, 12, 0, 0, 0, 0, 0) } 75 76 let p2: *u8 = nx_arena_alloc(a, 9, 8) 77 if p2 == (0 as *u8) { return __syscall(93, 13, 0, 0, 0, 0, 0) } 78 if nx_arena_used(a) != 25 { return __syscall(93, 14, 0, 0, 0, 0, 0) } 79 80 // Next align-8 alloc rounds up from 25 -> 32 before adding 8. 81 let p3: *u8 = nx_arena_alloc(a, 8, 8) 82 if p3 == (0 as *u8) { return __syscall(93, 15, 0, 0, 0, 0, 0) } 83 if nx_arena_used(a) != 40 { return __syscall(93, 16, 0, 0, 0, 0, 0) } 84 85 // Pointers must be distinct + non-overlapping. 86 if p1 == p2 { return __syscall(93, 17, 0, 0, 0, 0, 0) } 87 if p2 == p3 { return __syscall(93, 18, 0, 0, 0, 0, 0) } 88 89 // Write through them -- no SEGV expected. 90 p1[0] = 0x41 91 p1[15] = 0x42 92 p2[0] = 0x43 93 p2[8] = 0x44 94 p3[0] = 0x45 95 p3[7] = 0x46 96 97 // Zero-alloc verifies bytes are zero. 98 let p4: *u8 = nx_arena_alloc_zero(a, 32, 8) 99 if p4 == (0 as *u8) { return __syscall(93, 20, 0, 0, 0, 0, 0) } 100 var i: i64 = 0 101 while i < 32 { 102 if p4[i] != 0 { return __syscall(93, 21, 0, 0, 0, 0, 0) } 103 i = i + 1 104 } 105 106 // OOM behaviour. 107 let used_before: i64 = nx_arena_used(a) 108 let huge: *u8 = nx_arena_alloc(a, K_MAGIC_1024, 8) 109 if huge != (0 as *u8) { return __syscall(93, 30, 0, 0, 0, 0, 0) } 110 if nx_arena_oom_count(a) != 1 { return __syscall(93, 31, 0, 0, 0, 0, 0) } 111 // OOM does not advance the bump pointer. 112 if nx_arena_used(a) != used_before { return __syscall(93, 32, 0, 0, 0, 0, 0) } 113 114 // Reset frees everything in O(1). 115 nx_arena_reset(a) 116 if nx_arena_used(a) != 0 { return __syscall(93, 40, 0, 0, 0, 0, 0) } 117 let p5: *u8 = nx_arena_alloc(a, 8, 8) 118 if p5 == (0 as *u8) { return __syscall(93, 41, 0, 0, 0, 0, 0) } 119 if nx_arena_used(a) != 8 { return __syscall(93, 42, 0, 0, 0, 0, 0) } 120 121 // Diagnostic counters retained across reset (lifetime totals). 122 if nx_arena_alloc_count(a) < 5 { return __syscall(93, 50, 0, 0, 0, 0, 0) } 123 124 return 0 125}