code wiki / (root) / nx_hygiene_prims.nx

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}