code wiki / (root) / nx_string_ops.nx

nx_string_ops.nx source

↩ module page · 313 lines · 9592 B

1// nx_string_ops.nx -- byte-buffer string primitives (Python/JS stdlib analog). 2// 3// Every program ends up needing: substring search, slicing, length, 4// trim, parse-int, equality, hash, split. Without a substrate-level 5// home for these, every caller hand-rolls a while-loop scanning bytes 6// -- the exact kind of duplicated bullshit the user named in the 7// "loops within loops" feedback. 8// 9// PRIMITIVES (16 total): 10// Family 1: scan + length 11// nx_str_len(buf) -- length until first 0 byte 12// nx_str_index_of(buf, n, ch) -- first index of byte ch, -1 if none 13// nx_str_last_index_of(buf, n, ch) -- last index of ch, -1 if none 14// 15// Family 2: equality / prefix / suffix 16// nx_str_equals(a, na, b, nb) 17// nx_str_starts_with(buf, n, prefix, np) 18// nx_str_ends_with(buf, n, suffix, ns) 19// nx_str_find(haystack, nh, needle, nn) -- first occurrence, -1 if none 20// 21// Family 3: slice + copy 22// nx_str_slice_copy(src, lo, hi, dst, cap) -- copy [lo, hi) into dst 23// nx_str_trim_left(buf, n) -- returns new start offset 24// nx_str_trim_right(buf, n) -- returns new end offset (exclusive) 25// 26// Family 4: parse + format 27// nx_str_parse_int(buf, n) -- parse signed decimal 28// nx_str_format_int(v, dst, cap) -- write decimal; returns length 29// 30// Family 5: classify 31// nx_str_is_empty(buf, n) 32// nx_str_is_all_digits(buf, n) 33// nx_str_is_all_whitespace(buf, n) 34// 35// Family 6: hash 36// nx_str_hash_fnv1a(buf, n) -- 64-bit FNV-1a, well-distributed 37// 38// DESIGN PRINCIPLES: 39// - All take explicit length n; no null-terminator assumption (bytes 40// in buffers may contain 0) 41// - All bounds-check on entry; out-of-range inputs return sentinel 42// (-1 / 0) rather than SEGV 43// - All <= 8 args (safe under nxc2 calling convention, no stack args) 44// - Patent-clean (these are all textbook 1960s-1990s algorithms) 45// 46// genealogy_id: c_stdlib_string_h + python_str_methods + js_string_prototype + 47// fowler_noll_vo_1991_hash 48// lineage_id: nx_string_ops_v1 49 50// nx_safety_envelope: 51// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 52// sil_target: SIL1 53// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 54// verdict: NOT_YET_EVALUATED 55 56import "nx_syscalls.nx" 57import "nx_tier.nx" 58import "nx_essentials.nx" 59 60// ===== Family 1: scan + length =================================== 61 62// Strlen for null-terminated buffers. For length-known buffers prefer 63// just passing the known length. 64func nx_str_len(buf: *u8) -> nx_int { 65 var i: nx_int = 0 66 while buf[i] != 0 { i = i + 1 } 67 return i 68} 69 70// First index of byte ch in buf[0..n). Returns -1 if not found. 71func nx_str_index_of(buf: *u8, n: nx_int, ch: nx_int) -> nx_int { 72 if n <= 0 { return 0 - 1 } 73 var i: nx_int = 0 74 while i < n { 75 if buf[i] == ch { return i } 76 i = i + 1 77 } 78 return 0 - 1 79} 80 81// Last index of byte ch in buf[0..n). Returns -1 if not found. 82func nx_str_last_index_of(buf: *u8, n: nx_int, ch: nx_int) -> nx_int { 83 if n <= 0 { return 0 - 1 } 84 var i: nx_int = n - 1 85 while i >= 0 { 86 if buf[i] == ch { return i } 87 i = i - 1 88 } 89 return 0 - 1 90} 91 92// ===== Family 2: equality / prefix / suffix / find ============== 93 94func nx_str_equals(a: *u8, na: nx_int, b: *u8, nb: nx_int) -> nx_int { 95 if na != nb { return 0 } 96 if na <= 0 { return 1 } 97 var i: nx_int = 0 98 while i < na { 99 if a[i] != b[i] { return 0 } 100 i = i + 1 101 } 102 return 1 103} 104 105func nx_str_starts_with(buf: *u8, n: nx_int, prefix: *u8, np: nx_int) -> nx_int { 106 if np <= 0 { return 1 } // empty prefix matches all 107 if n < np { return 0 } 108 var i: nx_int = 0 109 while i < np { 110 if buf[i] != prefix[i] { return 0 } 111 i = i + 1 112 } 113 return 1 114} 115 116func nx_str_ends_with(buf: *u8, n: nx_int, suffix: *u8, ns: nx_int) -> nx_int { 117 if ns <= 0 { return 1 } 118 if n < ns { return 0 } 119 let start: nx_int = n - ns 120 var i: nx_int = 0 121 while i < ns { 122 if buf[start + i] != suffix[i] { return 0 } 123 i = i + 1 124 } 125 return 1 126} 127 128// First occurrence of needle in haystack. Returns starting index, or 129// -1 if none. Naive O(n*m) -- substrate uses it for short needles in 130// short configs; for long-text indexing use a sketch or suffix array. 131func nx_str_find(haystack: *u8, nh: nx_int, needle: *u8, nn: nx_int) -> nx_int { 132 if nn <= 0 { return 0 } // empty needle matches at 0 133 if nh < nn { return 0 - 1 } 134 let last_start: nx_int = nh - nn 135 var i: nx_int = 0 136 while i <= last_start { 137 var j: nx_int = 0 138 var ok: nx_int = 1 139 while j < nn { 140 if haystack[i + j] != needle[j] { 141 ok = 0 142 j = nn // break 143 } 144 j = j + 1 145 } 146 if ok == 1 { return i } 147 i = i + 1 148 } 149 return 0 - 1 150} 151 152// ===== Family 3: slice + copy + trim ============================= 153 154// Copy src[lo..hi) into dst[0..(hi-lo)). Returns bytes copied; 0 if 155// invalid range or dst capacity too small. 156func nx_str_slice_copy(src: *u8, lo: nx_int, hi: nx_int, dst: *u8, cap: nx_int) -> nx_int { 157 if lo < 0 { return 0 } 158 if hi < lo { return 0 } 159 let want: nx_int = hi - lo 160 if want > cap { return 0 } 161 var i: nx_int = 0 162 while i < want { 163 dst[i] = src[lo + i] 164 i = i + 1 165 } 166 return want 167} 168 169// Return offset of first non-whitespace byte; n if buffer is all 170// whitespace. Whitespace = space/tab/newline/cr. 171func nx_str_trim_left(buf: *u8, n: nx_int) -> nx_int { 172 if n <= 0 { return 0 } 173 var i: nx_int = 0 174 while i < n { 175 if nx_char_is_whitespace(buf[i]) != 1 { return i } 176 i = i + 1 177 } 178 return n 179} 180 181// Return one-past-last non-whitespace byte; 0 if buffer is all 182// whitespace. 183func nx_str_trim_right(buf: *u8, n: nx_int) -> nx_int { 184 if n <= 0 { return 0 } 185 var i: nx_int = n - 1 186 while i >= 0 { 187 if nx_char_is_whitespace(buf[i]) != 1 { return i + 1 } 188 i = i - 1 189 } 190 return 0 191} 192 193// ===== Family 4: parse + format ================================== 194 195// Parse signed decimal from buf[0..n). Allows leading '-' or '+'; 196// returns 0 on empty / non-digit content. No overflow detection 197// (i64 caller's responsibility -- substrate primitive, not config 198// frontend). 199func nx_str_parse_int(buf: *u8, n: nx_int) -> nx_int { 200 if n <= 0 { return 0 } 201 var i: nx_int = 0 202 var sign: nx_int = 1 203 if buf[0] == 45 { // '-' 204 sign = 0 - 1 205 i = 1 206 } 207 if buf[0] == 43 { // '+' 208 i = 1 209 } 210 var v: nx_int = 0 211 while i < n { 212 if nx_char_is_digit(buf[i]) != 1 { return sign * v } 213 v = v * 10 + (buf[i] - 48) 214 i = i + 1 215 } 216 return sign * v 217} 218 219// Write signed decimal of v into dst, up to cap bytes. Returns the 220// number of bytes written, or 0 if cap is too small. Does NOT 221// null-terminate. 222func nx_str_format_int(v: nx_int, dst: *u8, cap: nx_int) -> nx_int { 223 if cap <= 0 { return 0 } 224 var n: nx_int = v 225 var neg: nx_int = 0 226 if n < 0 { 227 neg = 1 228 n = 0 - n 229 } 230 // Count digits first. 231 var digit_count: nx_int = 0 232 var tmp: nx_int = n 233 if tmp == 0 { digit_count = 1 } 234 while tmp > 0 { 235 digit_count = digit_count + 1 236 tmp = tmp / 10 237 } 238 let total: nx_int = digit_count + neg 239 if total > cap { return 0 } 240 // Write minus sign. 241 if neg == 1 { dst[0] = 45 } // '-' 242 // Write digits high-to-low into positions [neg..total). 243 var pos: nx_int = total - 1 244 if n == 0 { 245 dst[neg] = 48 246 } else { 247 var nn: nx_int = n 248 while nn > 0 { 249 let d: nx_int = nn - (nn / 10) * 10 250 dst[pos] = 48 + d 251 nn = nn / 10 252 pos = pos - 1 253 } 254 } 255 return total 256} 257 258// ===== Family 5: classify ======================================== 259 260func nx_str_is_empty(buf: *u8, n: nx_int) -> nx_int { 261 if n <= 0 { return 1 } 262 return 0 263} 264 265func nx_str_is_all_digits(buf: *u8, n: nx_int) -> nx_int { 266 if n <= 0 { return 0 } 267 var i: nx_int = 0 268 while i < n { 269 if nx_char_is_digit(buf[i]) != 1 { return 0 } 270 i = i + 1 271 } 272 return 1 273} 274 275func nx_str_is_all_whitespace(buf: *u8, n: nx_int) -> nx_int { 276 if n <= 0 { return 1 } 277 var i: nx_int = 0 278 while i < n { 279 if nx_char_is_whitespace(buf[i]) != 1 { return 0 } 280 i = i + 1 281 } 282 return 1 283} 284 285// ===== Family 6: hash ============================================ 286 287// 64-bit FNV-1a hash. Constants per Fowler-Noll-Vo 1991 (public 288// domain). Well-distributed; not cryptographic. Used by 289// dedup tables, prompt-fingerprinting, content-addressing. 290// 291// The canonical 64-bit offset basis 0xcbf29ce484222325 is unsigned; 292// reinterpreted as signed i64 it's -3750763034362895579. NishiLang 293// constants don't accept compile-time arithmetic in initialisers, 294// so the basis is built in a getter and bit-XOR/multiply work on the 295// two's-complement representation regardless of sign. 296const NX_FNV1A_PRIME: nx_int = 1099511628211 // 0x100000001b3 297 298// Offset basis = -3750763034362895579 (= 0xcbf29ce484222325 as i64). 299func nx_fnv1a_offset_basis() -> nx_int { 300 return 0 - 3750763034362895579 301} 302 303func nx_str_hash_fnv1a(buf: *u8, n: nx_int) -> nx_int { 304 var h: nx_int = nx_fnv1a_offset_basis() 305 if n <= 0 { return h } 306 var i: nx_int = 0 307 while i < n { 308 h = h ^ buf[i] 309 h = h * NX_FNV1A_PRIME 310 i = i + 1 311 } 312 return h 313}