code wiki / (root) / nx_assert.nx

nx_assert.nx source

↩ module page · 170 lines · 5507 B

1// nx_assert.nx -- runtime self-check primitives. 2// 3// Phase 1 of the detection stack (feedback-three-disciplines.md and 4// the bootstrap_proof debug session of 2026-04-24 shipped the first 5// ad-hoc guards; this file factors them out so any runtime code can 6// assert cheaply). Pattern mirrors ASAN's FAIL_FAST: 7// 8// if (!INVARIANT) { sys_write(2, "tag: msg\n", N); exit(200); } 9// 10// Exit code 200 is "assertion failed". Distinct from 0 (success), 11// 1-127 (typed failures), 124/137/139 (qemu timeout/SIGKILL/SIGSEGV) 12// so debug harnesses can grep for it. 13// 14// Tradeoffs: 15// * Every assert is a branch + call. Negligible perf on the 16// success path -- branch predictor pins the not-taken leg and 17// there are zero data-dependent loads. 18// * Error messages emitted directly via sys_write so the assert 19// fires even when a heap allocator is the thing that broke. 20// 21// Later: a release-mode compile-time flag elides the checks. The 22// release build is expected to run the check code only in debug 23// CI / bootstrap_proof diagnostic passes. 24 25// nx_safety_envelope: 26// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 27// sil_target: SIL1 28// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 29// verdict: NOT_YET_EVALUATED 30 31// Canonical nx_-prefixed import per family convention. Was 32// "syscalls.nx" which duplicated sys_* symbols against 33// nx_syscalls.nx that the rest of the self-host chain uses -- 34// link-time "sys_ioctl already defined" on every nxc.elf rebuild. 35// Fixed 2026-05-16. 36import "nx_syscalls.nx" 37 38// The single exit code convention for assert failures. External 39// scripts (verify.sh, f6_gate.sh) match this. 40const NX_ASSERT_EXIT: i64 = 200 41 42// --- message helpers ------------------------------------------------ 43// 44// Tiny print helpers for building assert messages without pulling in 45// a full printf. Each writes to stderr (fd 2). 46 47// Emit a NUL-terminated C string to stderr. 48func nx_puts_err(s: *u8) -> i64 { 49 var n: i64 = 0 50 while s[n] != 0 { n = n + 1 } 51 sys_write(2, s, n) 52 return 0 53} 54 55// Emit a signed i64 as ASCII decimal + '\n'. 56func nx_puti_err(v: i64) -> i64 { 57 let buf: *u8 = sys_mmap(32) 58 var n: i64 = v 59 var i: i64 = 0 60 var neg: i64 = 0 61 if n < 0 { neg = 1; n = 0 - n } 62 if n == 0 { buf[0] = 0x30; i = 1 } 63 while n > 0 { 64 buf[i] = 0x30 + (n - (n / 10) * 10) 65 n = n / 10 66 i = i + 1 67 } 68 if neg == 1 { buf[i] = 0x2D; i = i + 1 } 69 // reverse 70 var j: i64 = 0 71 var k: i64 = i - 1 72 while j < k { 73 let tmp: i64 = buf[j] 74 buf[j] = buf[k] 75 buf[k] = tmp 76 j = j + 1 77 k = k - 1 78 } 79 buf[i] = 0x0A 80 sys_write(2, buf, i + 1) 81 return 0 82} 83 84// Emit an i64 as 16-char hex with a space after. Useful for 85// dumping pointer addresses. 86func nx_putx_err(v: i64) -> i64 { 87 let buf: *u8 = sys_mmap(24) 88 var i: i64 = 0 89 while i < 16 { 90 let nib: i64 = (v >> ((15 - i) * 4)) & 0xF 91 if nib < 10 { buf[i] = 0x30 + nib } 92 if nib >= 10 { buf[i] = 0x61 + (nib - 10) } 93 i = i + 1 94 } 95 buf[16] = 0x20 96 sys_write(2, buf, 17) 97 return 0 98} 99 100// --- core assertion primitive -------------------------------------- 101// 102// If `cond` is 0, emits "nx_assert: <tag>\n" and exits 200. 103// Otherwise returns 0. Minimal overhead on the success path. 104 105func nx_assert(cond: i64, tag: *u8) -> i64 { 106 if cond == 0 { 107 sys_write(2, "\nnx_assert: " as *u8, 12) 108 nx_puts_err(tag) 109 sys_write(2, "\n" as *u8, 1) 110 __syscall(93, NX_ASSERT_EXIT, 0, 0, 0, 0, 0) 111 } 112 return 0 113} 114 115// --- pointer / bounds specialisations ------------------------------- 116 117// Assert a pointer is non-null. Pointer-typed specialisations make 118// the compiler emit tighter checks than going through the generic 119// cond-is-zero form. 120func nx_assert_ptr(p: *u8, tag: *u8) -> i64 { 121 if p == (0 as *u8) { 122 sys_write(2, "\nnx_assert_ptr: NULL at " as *u8, 24) 123 nx_puts_err(tag) 124 sys_write(2, "\n" as *u8, 1) 125 __syscall(93, NX_ASSERT_EXIT, 0, 0, 0, 0, 0) 126 } 127 return 0 128} 129 130// Assert index < cap -- bounds check with tag. If it fails, 131// reports both the out-of-range idx and the cap so the caller 132// doesn't need to re-derive them. 133func nx_assert_lt(idx: i64, cap: i64, tag: *u8) -> i64 { 134 if idx >= cap { 135 sys_write(2, "\nnx_assert_lt: idx=" as *u8, 19) 136 nx_puti_err(idx) 137 sys_write(2, " cap=" as *u8, 5) 138 nx_puti_err(cap) 139 sys_write(2, " tag=" as *u8, 5) 140 nx_puts_err(tag) 141 sys_write(2, "\n" as *u8, 1) 142 __syscall(93, NX_ASSERT_EXIT, 0, 0, 0, 0, 0) 143 } 144 return 0 145} 146 147// Self-test. Exit 0 on success, other codes on mismatch. 148// Assertions that WOULD fire are tested in a separate bench with 149// expected-exit 200. 150func main() -> i64 { 151 // Success path: non-null ptr, in-bounds idx, true cond -- all 152 // return 0 without exit. 153 let p: *u8 = sys_mmap(16) 154 if nx_assert(1, "true-cond" as *u8) != 0 { 155 return __syscall(93, 10, 0, 0, 0, 0, 0) 156 } 157 if nx_assert_ptr(p, "non-null-ptr" as *u8) != 0 { 158 return __syscall(93, 11, 0, 0, 0, 0, 0) 159 } 160 if nx_assert_lt(5, 10, "in-bounds" as *u8) != 0 { 161 return __syscall(93, 12, 0, 0, 0, 0, 0) 162 } 163 164 // Boundary: idx == cap - 1 is the last valid index. 165 if nx_assert_lt(9, 10, "last-valid" as *u8) != 0 { 166 return __syscall(93, 13, 0, 0, 0, 0, 0) 167 } 168 169 return 0 170}