code wiki / (root) / nx_str_canonical.nx

nx_str_canonical.nx source

↩ module page · 311 lines · 8418 B

1// str.nx -- first-class string / byte-slice type for NishiLang. 2// 3// Carries (ptr, length) by pointer. Fixes the null-terminator 4// problem: `str` can store embedded zeros, and length is O(1). 5// 6// Eventually generalizes to `Slice<T>` once A4 (generics) lands; 7// for now we specialize to bytes because that's 99% of the use 8// case (HTTP parsing, markdown, file contents, console I/O). 9// 10// Usage pattern: 11// let hello: *str = str_from_cstr("Hello, World\n") 12// sys_write(1, hello.ptr, hello.len) 13// 14// Helper `str_from_cstr(ptr)` computes length by walking to NUL. 15// For true runtime-constructed slices (e.g. a substring), use 16// `str_new(ptr, len)` which stores as-given. 17 18struct str { 19 ptr: *u8, 20 len: i64, 21} 22 23// Shared syscalls + sys_mmap now live in syscalls.nx so every runtime 24// module stops copy-pasting them. str_sys_mmap stays here as a 25// back-compat wrapper until callers migrate. 26// nx_safety_envelope: 27// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 28// sil_target: SIL1 29// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 30// verdict: NOT_YET_EVALUATED 31 32import "nx_syscalls.nx" 33 34func str_sys_mmap(size: i64) -> *u8 { 35 return sys_mmap(size) 36} 37 38// ---- constructors ---- 39 40// Wrap a null-terminated C-style pointer into a str. 41// Walks to NUL to determine length. O(n) but called once. 42func str_from_cstr(p: *u8) -> *str { 43 var n: i64 = 0 44 while p[n] != 0 { n = n + 1 } 45 let raw: *u8 = str_sys_mmap(16) 46 let s: *str = raw as *str 47 s.ptr = p 48 s.len = n 49 return s 50} 51 52// Allocate a str with (ptr, len) given directly. ptr must stay 53// valid for the lifetime of the str (caller responsibility). 54func str_new(p: *u8, len: i64) -> *str { 55 let raw: *u8 = str_sys_mmap(16) 56 let s: *str = raw as *str 57 s.ptr = p 58 s.len = len 59 return s 60} 61 62// Empty str -- ptr=0, len=0. Safe to compare lengths, don't 63// dereference ptr. 64func str_empty() -> *str { 65 return str_new(0 as *u8, 0) 66} 67 68// ---- basic accessors ---- 69 70func str_len(s: *str) -> i64 { return s.len } 71func str_is_empty(s: *str) -> i64 { 72 if s.len == 0 { return 1 } 73 return 0 74} 75 76// Unchecked byte access. In production mode we'd bounds-check; 77// for now caller checks. 78func str_at(s: *str, i: i64) -> i64 { 79 let p: *u8 = s.ptr 80 return p[i] 81} 82 83// ---- comparison ---- 84 85func str_eq(a: *str, b: *str) -> i64 { 86 if a.len != b.len { return 0 } 87 let ap: *u8 = a.ptr 88 let bp: *u8 = b.ptr 89 var i: i64 = 0 90 while i < a.len { 91 if ap[i] != bp[i] { return 0 } 92 i = i + 1 93 } 94 return 1 95} 96 97// Compare a str to a C-style literal. Convenient for matching 98// HTTP verbs, header names, etc. 99func str_eq_cstr(a: *str, cstr: *u8) -> i64 { 100 var i: i64 = 0 101 while i < a.len { 102 if cstr[i] == 0 { return 0 } // literal shorter 103 let ap: *u8 = a.ptr 104 if ap[i] != cstr[i] { return 0 } 105 i = i + 1 106 } 107 if cstr[i] != 0 { return 0 } // literal longer 108 return 1 109} 110 111// ---- sub-slicing ---- 112 113// s[begin..end) -- new str referring to the same backing bytes. 114// Caller ensures 0 <= begin <= end <= s.len. 115func str_substr(s: *str, begin: i64, end: i64) -> *str { 116 let p: *u8 = s.ptr 117 let base: i64 = p as i64 118 return str_new((base + begin) as *u8, end - begin) 119} 120 121// Does `s` start with `prefix`? 122func str_starts_with(s: *str, prefix: *str) -> i64 { 123 if s.len < prefix.len { return 0 } 124 let sp: *u8 = s.ptr 125 let pp: *u8 = prefix.ptr 126 var i: i64 = 0 127 while i < prefix.len { 128 if sp[i] != pp[i] { return 0 } 129 i = i + 1 130 } 131 return 1 132} 133 134// Index of first occurrence of `c`, or -1. 135func str_index_of(s: *str, c: i64) -> i64 { 136 let p: *u8 = s.ptr 137 var i: i64 = 0 138 while i < s.len { 139 if p[i] == c { return i } 140 i = i + 1 141 } 142 return -1 143} 144 145// Split s at the first occurrence of byte `c`. Returns the 146// prefix (before c). `remainder` (after c) is written back 147// through `rest_out`, which must be non-null. If `c` is not 148// present, returns s and writes empty string to *rest_out. 149func str_split_once(s: *str, c: i64, rest_out: *i64) -> *str { 150 let idx: i64 = str_index_of(s, c) 151 if idx < 0 { 152 *rest_out = str_empty() as i64 153 return s 154 } 155 let prefix: *str = str_substr(s, 0, idx) 156 let rest: *str = str_substr(s, idx + 1, s.len) 157 *rest_out = rest as i64 158 return prefix 159} 160 161// Trim ASCII whitespace from both ends. 162func str_trim(s: *str) -> *str { 163 let p: *u8 = s.ptr 164 var start: i64 = 0 165 while start < s.len { 166 let c: i64 = p[start] 167 if c != 0x20 { if c != 0x09 { if c != 0x0A { if c != 0x0D { break } } } } 168 start = start + 1 169 } 170 var end: i64 = s.len 171 while end > start { 172 let c: i64 = p[end - 1] 173 if c != 0x20 { if c != 0x09 { if c != 0x0A { if c != 0x0D { break } } } } 174 end = end - 1 175 } 176 return str_substr(s, start, end) 177} 178 179// ---- integer parsing ---- 180 181// Parse a decimal integer from the start of s. Stops at first 182// non-digit. Negative sign allowed. Returns parsed value; writes 183// the number of bytes consumed through `consumed_out`. 184func str_parse_int(s: *str, consumed_out: *i64) -> i64 { 185 let p: *u8 = s.ptr 186 var i: i64 = 0 187 var sign: i64 = 1 188 if i < s.len { 189 if p[0] == 0x2D { 190 sign = -1 191 i = 1 192 } 193 } 194 var v: i64 = 0 195 while i < s.len { 196 let c: i64 = p[i] 197 if c < 0x30 { break } 198 if c > 0x39 { break } 199 v = v * 10 + (c - 0x30) 200 i = i + 1 201 } 202 *consumed_out = i 203 return v * sign 204} 205 206// ---- writing ---- 207 208// Write a str to an fd. Returns bytes written. 209func str_write(fd: i64, s: *str) -> i64 { 210 return __syscall(64, fd, s.ptr, s.len) 211} 212 213// ---- F6 string manipulation (backlog task #88) --------------------- 214 215// Suffix check. Returns 1 if s ends with suffix, else 0. 216func str_ends_with(s: *str, suffix: *str) -> i64 { 217 if suffix.len > s.len { return 0 } 218 let off: i64 = s.len - suffix.len 219 var i: i64 = 0 220 while i < suffix.len { 221 if s.ptr[off + i] != suffix.ptr[i] { return 0 } 222 i = i + 1 223 } 224 return 1 225} 226 227// Substring search. Returns the starting index of `needle` in s, or 228// -1 if not found. Naive O(n*m); fine for the <= MTU strings we 229// typically process (HTTP headers, config lines). 230func str_contains(s: *str, needle: *str) -> i64 { 231 if needle.len == 0 { return 0 } 232 if needle.len > s.len { return -1 } 233 let limit: i64 = s.len - needle.len 234 var i: i64 = 0 235 while i <= limit { 236 var j: i64 = 0 237 var ok: i64 = 1 238 while j < needle.len { 239 if s.ptr[i + j] != needle.ptr[j] { ok = 0; j = needle.len } 240 j = j + 1 241 } 242 if ok == 1 { return i } 243 i = i + 1 244 } 245 return -1 246} 247 248// ASCII lowercase transform into a fresh buffer; non-alpha bytes 249// pass through. Returns a new *str with freshly mmap'd storage. 250func str_to_lower(s: *str) -> *str { 251 let buf: *u8 = sys_mmap(s.len + 8) 252 var i: i64 = 0 253 while i < s.len { 254 var c: i64 = s.ptr[i] 255 if c >= 0x41 { 256 if c <= 0x5A { c = c + 0x20 } 257 } 258 buf[i] = c 259 i = i + 1 260 } 261 return str_new(buf, s.len) 262} 263 264// ASCII uppercase transform, dual of str_to_lower. 265func str_to_upper(s: *str) -> *str { 266 let buf: *u8 = sys_mmap(s.len + 8) 267 var i: i64 = 0 268 while i < s.len { 269 var c: i64 = s.ptr[i] 270 if c >= 0x61 { 271 if c <= 0x7A { c = c - 0x20 } 272 } 273 buf[i] = c 274 i = i + 1 275 } 276 return str_new(buf, s.len) 277} 278 279// Format a signed i64 as a decimal str. Allocates 24 bytes (covers 280// the widest i64 plus sign). Parallels str_parse_int. 281func str_format_int(n: i64) -> *str { 282 let buf: *u8 = sys_mmap(32) 283 if n == 0 { 284 buf[0] = 0x30 285 return str_new(buf, 1) 286 } 287 var v: i64 = n 288 var neg: i64 = 0 289 if v < 0 { 290 neg = 1 291 v = 0 - v 292 } 293 // Write digits into scratch, reversed. 294 let scratch: *u8 = sys_mmap(32) 295 var sp: i64 = 0 296 while v > 0 { 297 scratch[sp] = 0x30 + (v % 10) 298 v = v / 10 299 sp = sp + 1 300 } 301 var k: i64 = 0 302 if neg == 1 { buf[0] = 0x2D; k = 1 } 303 while sp > 0 { 304 sp = sp - 1 305 buf[k] = scratch[sp] 306 k = k + 1 307 } 308 return str_new(buf, k) 309} 310 311// Library only; self-test lives in str_test.nx.