nx_hygiene_prims.nx source
↩ module page · 351 lines · 13149 B
1// nx_hygiene_prims.nx -- foundational primitives for the
2// NISHI_CODE_HYGIENE_STANDARD.md (nishi-silicon commit f4cc144).
3//
4// Provides the building blocks new code MUST use to satisfy
5// hygiene classes M1 (null safety), M2 (overflow safety), M4
6// (alloc safety), M5 (index safety). Once this ships, every
7// commit going forward starts born-clean per the §4.1 MUST set.
8//
9// WINNER-TIER: WINNER-A (preserves the cardinal); paired with
10// the standards docs that establish the bar
11// INCUMBENTS: Rust std::option::Option, Rust checked_arithmetic,
12// Ada SPARK contracts, MISRA C INT30-C rule, CERT
13// INT32-C, libstd's bounds-checked indexer
14// NUMBERS: V1 ships the primitives; benchmark vs Rust's
15// checked_mul + alloc + Option pending paired bench
16// GAP: Rust's Option<T> is language-built-in; this is a
17// substrate-level convention. Future M3 milestone
18// lifts NxOpt into a language-level type.
19// PLAN: M-next: per-file remediation using these primitives;
20// ratchet metric tracks count of null-creation +
21// unchecked-alloc + raw-multiply sites going down per
22// commit
23// EXEMPTION REASON: n/a; provisional pending measurement
24//
25// Status: SEED v0.1.0. 2026-05-27.
26
27import "nx_syscalls.nx"
28
29// ===== Sealed verdict surface (mirrors NISHI_CODE_HYGIENE_STANDARD §6) =================================================
30const NX_HYG_OK: i64 = 0
31const NX_HYG_OVERFLOW: i64 = 600
32const NX_HYG_LOOP_BUDGET_EXCEEDED: i64 = 601
33const NX_HYG_BAD_INDEX: i64 = 602
34const NX_HYG_ALLOC_FAILED: i64 = 603
35const NX_HYG_NOT_IMPLEMENTED: i64 = 604
36const NX_HYG_BAD_TRUNCATION: i64 = 605
37const NX_HYG_USE_AFTER_RELEASE: i64 = 606
38const NX_HYG_DOUBLE_RELEASE: i64 = 607
39const NX_HYG_NULL_DEREF: i64 = 608
40const NX_HYG_UNCHECKED_RETURN: i64 = 609
41
42// ===== Named integer limits (M7: no magic numbers) =================================================
43const NX_INT64_MAX: i64 = 9223372036854775807
44const NX_INT64_MIN: i64 = 0x8000000000000000
45const NX_BYTE_MAX: i64 = 255
46const NX_U16_MAX: i64 = 65535
47const NX_U32_MAX: i64 = 4294967295
48
49// Tuneable substrate-wide allocation policy. Sized to be sane for
50// substrate use (256 MiB single-alloc cap). Operator may raise via
51// PATCH bump once measurement shows need.
52const NX_MAX_SINGLE_ALLOC_BYTES: i64 = 268435456 // 256 MiB
53
54// ===== Named struct sizes (M7: no `sys_mmap(64)` magic) =================================================
55//
56// Use these in place of bare numeric struct sizes. Each is
57// hand-validated against the struct definition at the cited file.
58// Sizes are conservative upper bounds; over-alloc is harmless for
59// caller-allocated structs.
60
61const NX_SIZEOF_NXOPT_T: i64 = 16 // discriminant + value pointer
62const NX_SIZEOF_SAFE_INT_OUT: i64 = 8 // single i64 out-param
63
64// ===== M1 — NxOpt<T> sealed Option-like wrapper =================================================
65//
66// Anti-pattern this deprecates:
67// let p: *Foo = (0 as i64) as *Foo
68// if (p as i64) == 0 { ... handle absent ... }
69//
70// Replaces with:
71// let opt: NxOptPtr = nx_opt_none()
72// if nx_opt_is_some(&opt) == 1 { ... use nx_opt_unwrap(&opt) ... }
73//
74// V1 uses generic *u8 pointer payload; caller casts to specific *T.
75// V2 introduces per-type NxOpt<T> via codegen (when NishiLang
76// language-level generics ship per M3 milestone of the superiority
77// bar).
78//
79// Discriminant encoding (sealed):
80// has_value == 0 -- absent (None)
81// has_value == 1 -- present (Some); value field meaningful
82
83struct NxOptPtr {
84 has_value: i64
85 value: *u8 // generic pointer payload; caller casts
86}
87
88func nx_opt_none(opt: *NxOptPtr) -> i64 {
89 if (opt as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
90 opt.has_value = 0
91 opt.value = (0 as i64) as *u8 // safe: caller MUST check has_value
92 return NX_HYG_OK
93}
94
95func nx_opt_some(opt: *NxOptPtr, ptr: *u8) -> i64 {
96 if (opt as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
97 if (ptr as i64) == 0 { return 0 - NX_HYG_NULL_DEREF } // can't wrap a null
98 opt.has_value = 1
99 opt.value = ptr
100 return NX_HYG_OK
101}
102
103func nx_opt_is_some(opt: *NxOptPtr) -> i64 {
104 if (opt as i64) == 0 { return 0 } // defensive
105 if opt.has_value == 1 { return 1 }
106 return 0
107}
108
109// Unwrap: returns the wrapped pointer if present; returns null if
110// absent (per V1 limitation, can't panic from NishiLang substrate
111// w/o substrate panic primitives; future M3 lifts to compile-time
112// totality check).
113func nx_opt_unwrap(opt: *NxOptPtr) -> *u8 {
114 if (opt as i64) == 0 { return (0 as i64) as *u8 }
115 if opt.has_value != 1 { return (0 as i64) as *u8 }
116 return opt.value
117}
118
119// Map: applies a transformation iff present. V2 generic; V1
120// returns OK/verdict, with new pointer via out-param.
121func nx_opt_map(opt_in: *NxOptPtr, transform_fn: i64,
122 opt_out: *NxOptPtr) -> i64 {
123 if (opt_in as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
124 if (opt_out as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
125 if opt_in.has_value != 1 {
126 return nx_opt_none(opt_out)
127 }
128 // V1: transform_fn is opaque; future commit wires actual fn-pointer
129 // dispatch. For now, mark not implemented honestly.
130 return 0 - NX_HYG_NOT_IMPLEMENTED
131}
132
133// ===== M2 — Safe arithmetic =================================================
134//
135// Anti-pattern: let total: i64 = a * b (silent wrap on overflow)
136//
137// Replaces with:
138// let out: *i64 = ...
139// let rc: i64 = nx_safe_mul(a, b, out)
140// if rc != NX_HYG_OK { return rc }
141// let total: i64 = out[0]
142//
143// All three operators (add/sub/mul) provided. Pattern matches
144// Rust's checked_add/checked_sub/checked_mul.
145
146func nx_safe_add(a: i64, b: i64, out: *i64) -> i64 {
147 if (out as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
148 // Overflow check: if a > 0 && b > 0, a + b overflows iff b > MAX - a
149 if a > 0 {
150 if b > 0 {
151 if a > (NX_INT64_MAX - b) { return 0 - NX_HYG_OVERFLOW }
152 }
153 }
154 // Underflow check: if a < 0 && b < 0, a + b underflows iff b < MIN - a
155 if a < 0 {
156 if b < 0 {
157 if a < (NX_INT64_MIN - b) { return 0 - NX_HYG_OVERFLOW }
158 }
159 }
160 out[0] = a + b
161 return NX_HYG_OK
162}
163
164func nx_safe_sub(a: i64, b: i64, out: *i64) -> i64 {
165 if (out as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
166 // a - b overflows iff a + (-b) overflows; reduce to nx_safe_add.
167 // Edge: b == INT_MIN cannot be negated safely; handle directly.
168 if b == NX_INT64_MIN {
169 if a >= 0 { return 0 - NX_HYG_OVERFLOW }
170 // a < 0; a - INT_MIN = a + INT_MAX + 1; might fit
171 out[0] = (a + NX_INT64_MAX) + 1
172 return NX_HYG_OK
173 }
174 return nx_safe_add(a, 0 - b, out)
175}
176
177func nx_safe_mul(a: i64, b: i64, out: *i64) -> i64 {
178 if (out as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
179 if a == 0 { out[0] = 0; return NX_HYG_OK }
180 if b == 0 { out[0] = 0; return NX_HYG_OK }
181 // Compute |a| and |b|; if |a| > MAX/|b|, overflow.
182 // V-LANGEXT M2: if-expression natively supported (no more
183 // var+if-statement workaround pattern).
184 let abs_a: i64 = if a < 0 then 0 - a else a
185 let abs_b: i64 = if b < 0 then 0 - b else b
186 // INT_MIN negated overflows; treat specially.
187 if a == NX_INT64_MIN { return 0 - NX_HYG_OVERFLOW }
188 if b == NX_INT64_MIN { return 0 - NX_HYG_OVERFLOW }
189 if abs_a > (NX_INT64_MAX / abs_b) { return 0 - NX_HYG_OVERFLOW }
190 out[0] = a * b
191 return NX_HYG_OK
192}
193
194// ===== M4 — Safe allocation =================================================
195//
196// Anti-pattern: let buf: *u8 = sys_mmap(8 * cap)
197// (multiplication could overflow; sys_mmap could return null)
198//
199// Replaces with:
200// let size_out: *i64 = ...
201// let rc1: i64 = nx_safe_mul(8, cap, size_out)
202// if rc1 != NX_HYG_OK { return rc1 }
203// let opt: NxOptPtr = ...
204// let rc2: i64 = nx_alloc_checked(size_out[0], &opt)
205// if rc2 != NX_HYG_OK { return rc2 }
206// let buf: *u8 = nx_opt_unwrap(&opt)
207//
208// V1: validates size + invokes sys_mmap + checks for null + wraps
209// in NxOpt. V2: integrates with future nx_hal alloc + lifecycle
210// tracking.
211
212func nx_alloc_checked(size: i64, out_opt: *NxOptPtr) -> i64 {
213 if (out_opt as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
214 if size <= 0 { return 0 - NX_HYG_ALLOC_FAILED }
215 if size > NX_MAX_SINGLE_ALLOC_BYTES { return 0 - NX_HYG_ALLOC_FAILED }
216 let raw: *u8 = sys_mmap(size)
217 if (raw as i64) == 0 {
218 nx_opt_none(out_opt)
219 return 0 - NX_HYG_ALLOC_FAILED
220 }
221 return nx_opt_some(out_opt, raw)
222}
223
224// ===== M5 — Safe array indexing =================================================
225//
226// Anti-pattern: let val: i64 = arr[idx] (idx unchecked)
227//
228// Replaces with:
229// let out: *i64 = ...
230// let rc: i64 = nx_arr_get_i64(arr, cap, idx, out)
231// if rc != NX_HYG_OK { return rc }
232// let val: i64 = out[0]
233//
234// Hot-path code can still use direct arr[idx] IF the bound is
235// established earlier in the function with an explicit
236// `if idx < 0 { return ... } if idx >= cap { return ... }` check.
237// The safe-helper is for cold paths + new code where the bound
238// isn't already established at the call site.
239
240func nx_arr_get_i64(arr: *i64, cap: i64, idx: i64, out: *i64) -> i64 {
241 if (arr as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
242 if (out as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
243 if idx < 0 { return 0 - NX_HYG_BAD_INDEX }
244 if idx >= cap { return 0 - NX_HYG_BAD_INDEX }
245 out[0] = arr[idx]
246 return NX_HYG_OK
247}
248
249func nx_arr_set_i64(arr: *i64, cap: i64, idx: i64, value: i64) -> i64 {
250 if (arr as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
251 if idx < 0 { return 0 - NX_HYG_BAD_INDEX }
252 if idx >= cap { return 0 - NX_HYG_BAD_INDEX }
253 arr[idx] = value
254 return NX_HYG_OK
255}
256
257func nx_arr_get_u8(arr: *u8, cap: i64, idx: i64, out: *i64) -> i64 {
258 if (arr as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
259 if (out as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
260 if idx < 0 { return 0 - NX_HYG_BAD_INDEX }
261 if idx >= cap { return 0 - NX_HYG_BAD_INDEX }
262 out[0] = arr[idx] as i64
263 return NX_HYG_OK
264}
265
266func nx_arr_set_u8(arr: *u8, cap: i64, idx: i64, value: i64) -> i64 {
267 if (arr as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
268 if idx < 0 { return 0 - NX_HYG_BAD_INDEX }
269 if idx >= cap { return 0 - NX_HYG_BAD_INDEX }
270 if value < 0 { return 0 - NX_HYG_BAD_TRUNCATION }
271 if value > NX_BYTE_MAX { return 0 - NX_HYG_BAD_TRUNCATION }
272 arr[idx] = value as u8
273 return NX_HYG_OK
274}
275
276// ===== M9 — Safe type narrowing =================================================
277//
278// Anti-pattern: let small: i64 = (large & 0xff) (silent loss)
279//
280// Replaces with:
281// let out: *i64 = ...
282// let rc: i64 = nx_truncate_to_u8(large, out)
283// if rc != NX_HYG_OK { return rc }
284// let small: i64 = out[0]
285//
286// Use nx_extract_byte (V2) when intentionally extracting a low
287// byte without claiming the high bits should be zero.
288
289func nx_truncate_to_u8(v: i64, out: *i64) -> i64 {
290 if (out as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
291 if v < 0 { return 0 - NX_HYG_BAD_TRUNCATION }
292 if v > NX_BYTE_MAX { return 0 - NX_HYG_BAD_TRUNCATION }
293 out[0] = v & 0xff
294 return NX_HYG_OK
295}
296
297func nx_truncate_to_u16(v: i64, out: *i64) -> i64 {
298 if (out as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
299 if v < 0 { return 0 - NX_HYG_BAD_TRUNCATION }
300 if v > NX_U16_MAX { return 0 - NX_HYG_BAD_TRUNCATION }
301 out[0] = v & 0xffff
302 return NX_HYG_OK
303}
304
305func nx_truncate_to_u32(v: i64, out: *i64) -> i64 {
306 if (out as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
307 if v < 0 { return 0 - NX_HYG_BAD_TRUNCATION }
308 if v > NX_U32_MAX { return 0 - NX_HYG_BAD_TRUNCATION }
309 out[0] = v & 0xffffffff
310 return NX_HYG_OK
311}
312
313// ===== M3 — Bounded loop helper (advisory) =================================================
314//
315// Pattern enforcement: every `while` MUST declare a bound. This
316// helper is for cold paths where the operator wants explicit
317// budget tracking with shared semantics; hot paths inline the
318// bound directly.
319//
320// Usage:
321// let budget: *NxLoopBudget = ...
322// nx_loop_budget_init(budget, NX_MAX_ITER_FOO)
323// while condition() {
324// let rc: i64 = nx_loop_budget_step(budget)
325// if rc != NX_HYG_OK { return rc }
326// // ... body ...
327// }
328
329struct NxLoopBudget {
330 max_iter: i64
331 cur_iter: i64
332 valid: i64
333}
334
335func nx_loop_budget_init(budget: *NxLoopBudget, max_iter: i64) -> i64 {
336 if (budget as i64) == 0 { return 0 - NX_HYG_NULL_DEREF }
337 if max_iter <= 0 { return 0 - NX_HYG_BAD_INDEX }
338 budget.max_iter = max_iter
339 budget.cur_iter = 0
340 budget.valid = 1
341 return NX_HYG_OK
342}
343
344func nx_loop_budget_step(budget: *NxLoopBudget) -> i64 {
345 if budget.valid != 1 { return 0 - NX_HYG_NULL_DEREF }
346 if budget.cur_iter >= budget.max_iter {
347 return 0 - NX_HYG_LOOP_BUDGET_EXCEEDED
348 }
349 budget.cur_iter = budget.cur_iter + 1
350 return NX_HYG_OK
351}