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}