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.