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}