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}