// strings — operations over str ({ptr,len}). Hare port; see // ref/hare/strings/. // // Documented divergences from Hare: // // - `concat(a, b)` is 2-arg. Hare ships `concat(strs: str...)` // (ref/hare/strings/concat.ha:5). Blocks on task #16 (cstage // variadic-pack drops .len of multi-field element type). Cite // reverts on fix. // - `trim` / `ltrim` / `rtrim` take a single rune. Hare's are // `(exclude: rune...)` (ref/hare/strings/trim.ha:54). Same // blocker as concat. Hare's no-rune branch (strip whitespace) // is also dropped — depends on a rune set. // - `contains` is non-variadic. Hare's is // `contains(haystack, needles: (str | rune)...)` // (ref/hare/strings/contains.ha:9). Same blocker. // - `byteindex` / `rbyteindex` rune arms encode via // `utf8.encoderune`; the legacy impls scanned for `r: u8` (an // undocumented ASCII-only restriction that silently dropped // to the wrong byte for U+80..U+7FF and higher). // - `dup(s: str) str` — Hare returns `(str | nomem)`. ww's // `os.alloc` aborts on OOM (no `nomem` type), so we return plain // `str`. Empty input returns `{nil, 0}`; Hare returns the static // empty string — same observable result. // - `iterator` is flattened (`offs`, `src`, `reverse` fields). // Hare uses anonymous-embedded `utf8::decoder` // (ref/hare/strings/iter.ha:6-9); ww has no anonymous-embed // syntax, so `next`/`prev` copy `offs`/`src` into a local // `utf8.decoder` for the call, then write `offs` back. // - Hare's private `move()` helper dispatches on a `forward: bool` // using a function-pointer `let fun = if (forward) &utf8::next // else &utf8::prev`. ww has no fn-pointers in scope yet, so the // dispatch is a branch on `forward` selecting the call site. // - `slice` deferred — Hare's `fromutf8_unsafe(utf8::slice(begin, // end))` (ref/hare/strings/iter.ha:76) hits wwstage fnretlookup // same-name cross-module phantom return-ABI fixup (task #34). // Lands once #34 clears. package strings; import bytes; import encoding.utf8; import os; // toutf8 — borrowed []u8 view of `s`. ref/hare/strings/utf8.ha:29. // `cap` equals `len`; the slice does not own a separate allocation. export fn toutf8(s: str) []u8 = { let r: []u8; r.ptr = s.ptr; r.len = s.len; r.cap = s.len; return r; }; // fromutf8_unsafe — borrowed str view of `in`. Does not validate. // ref/hare/strings/utf8.ha:10. export fn fromutf8_unsafe(in: []u8) str = { let r: str; r.ptr = in.ptr; r.len = in.len; return r; }; // compare — three-way bytewise codepoint-order comparison. // ref/hare/strings/compare.ha:12. export fn compare(a: str, b: str) i32 = { let n: i32 = a.len; if (b.len < n) { n = b.len; }; let i: i32 = 0; for (i < n) { if (a[i] != b[i]) { return (a[i]: i32) - (b[i]: i32); }; i += 1; }; return a.len - b.len; }; // dup — allocate a fresh copy of `s`. Caller releases with // `os.free(r.ptr, r.len: u64)`. ref/hare/strings/dup.ha:7. export fn dup(s: str) str = { let r: str; r.ptr = nil; r.len = 0; if (s.len == 0) { return r; }; let buf: *u8 = os.alloc(s.len: u64): *u8; let i: i32 = 0; for (i < s.len) { buf[i] = s[i]; i += 1; }; r.ptr = buf; r.len = s.len; return r; }; // freeall — release each element + the slice header. The natural // disposer for any `[]str` of dup'd elements (e.g. shlex.split). // ref/hare/strings/dup.ha:38. // // Empty elements (`{nil, 0}` from a zero-length dup) are skipped: // os.free on a nil pointer at len 0 tickles the rt_free guard. The // slice header itself is freed at `cap * 16` (one str = 16B); a // never-grown slice (cap == 0) skips the header free. export fn freeall(s: []str) void = { let i: i32 = 0; for (i < s.len) { if (s[i].len > 0) { os.free(s[i].ptr: *void, s[i].len: u64); }; i += 1; }; if (s.cap > 0) { os.free(s.ptr: *void, (s.cap: u64) * 16u64); }; }; // concat — fresh allocation containing `a` then `b`. Caller releases // with `os.free(r.ptr, r.len: u64)`. ref/hare/strings/concat.ha:5 // (subset: Hare's `(strs: str...)` blocks on task #16). export fn concat(a: str, b: str) str = { let total: i32 = a.len + b.len; let buf: *u8 = os.alloc(total: u64): *u8; let i: i32 = 0; for (i < a.len) { buf[i] = a[i]; i += 1; }; let j: i32 = 0; for (j < b.len) { buf[a.len + j] = b[j]; j += 1; }; let r: str; r.ptr = buf; r.len = total; return r; }; // sub — borrowed `s[start..end]`. ref/hare/strings/sub.ha:30 is // rune-wise; this ww form is byte-wise (no rune iterator yet, planned // for commit 2). Clamps out-of-range silently where Hare aborts — // retained for the existing getopt caller; will graduate when the // rune-wise form lands. export fn sub(s: str, start: i32, end: i32) str = { let lo: i32 = start; let hi: i32 = end; if (lo < 0) { lo = 0; }; if (hi > s.len) { hi = s.len; }; if (hi < lo) { hi = lo; }; let r: str; r.ptr = s.ptr + (lo: u64); r.len = hi - lo; return r; }; // runebytes — encode `r` into caller's `scratch` (must hold 4 bytes) // and return the borrowed slice trimmed to the encoded length. Hare // inlines the same shape at ref/hare/strings/index.ha:132. fn runebytes(scratch: []u8, r: rune) []u8 = { let n: i32 = utf8.encoderune(scratch, r); let s: []u8; s.ptr = scratch.ptr; s.len = n; s.cap = n; return s; }; // hasprefix — true iff `in` begins with `prefix`. // ref/hare/strings/suffix.ha:8. export fn hasprefix(in: str, prefix: (str | rune)) bool = { let scratch: [4]u8; let p: []u8 = match (prefix) { case let s: str => yield toutf8(s); case let r: rune => yield runebytes(scratch[0:4], r); }; return bytes.hasprefix(toutf8(in), p); }; // hassuffix — true iff `in` ends with `suff`. // ref/hare/strings/suffix.ha:26. export fn hassuffix(in: str, suff: (str | rune)) bool = { let scratch: [4]u8; let s: []u8 = match (suff) { case let v: str => yield toutf8(v); case let r: rune => yield runebytes(scratch[0:4], r); }; return bytes.hassuffix(toutf8(in), s); }; // byteindex — byte-wise offset of `needle` in `haystack`, or void if // absent. ref/hare/strings/index.ha:127. Rune arm encodes via // utf8.encoderune (Hare passes the encoded slice straight to // bytes::index). export fn byteindex(haystack: str, needle: (str | rune)) (i32 | void) = { let scratch: [4]u8; let n: []u8 = match (needle) { case let s: str => yield toutf8(s); case let r: rune => yield runebytes(scratch[0:4], r); }; return bytes.index(toutf8(haystack), n); }; // rbyteindex — byte-wise offset of the last `needle` in `haystack`. // ref/hare/strings/index.ha:138. export fn rbyteindex(haystack: str, needle: (str | rune)) (i32 | void) = { let scratch: [4]u8; let n: []u8 = match (needle) { case let s: str => yield toutf8(s); case let r: rune => yield runebytes(scratch[0:4], r); }; return bytes.rindex(toutf8(haystack), n); }; // contains — true iff `needle` occurs in `haystack`. // ref/hare/strings/contains.ha:9 (subset: Hare's variadic form // `(needles: (str | rune)...)` blocks on task #16). export fn contains(haystack: str, needle: (str | rune)) bool = { match (byteindex(haystack, needle)) { case let i: i32 => return true; case void => return false; }; return false; }; // trimprefix — `s` with `prefix` stripped from the front, or `s` // unchanged if it doesn't start with `prefix`. Borrowed view. // ref/hare/strings/trim.ha:60. export fn trimprefix(input: str, prefix: str) str = { if (!hasprefix(input, prefix)) { return input; }; let r: str; r.ptr = input.ptr + (prefix.len: u64); r.len = input.len - prefix.len; return r; }; // trimsuffix — symmetric. ref/hare/strings/trim.ha:69. export fn trimsuffix(input: str, suffix: str) str = { if (!hassuffix(input, suffix)) { return input; }; let r: str; r.ptr = input.ptr; r.len = input.len - suffix.len; return r; }; // ltrim — strip occurrences of `exclude` (encoded as UTF-8) from the // front. Borrowed view. ref/hare/strings/trim.ha:11 (subset: single // rune; Hare's `(trim: rune...)` blocks on task #16). The no-rune // strip-whitespace branch is omitted for the same reason. export fn ltrim(input: str, exclude: rune) str = { let scratch: [4]u8; let pat: []u8 = runebytes(scratch[0:4], exclude); let i: i32 = 0; for (i + pat.len <= input.len) { let j: i32 = 0; let ok: bool = true; for (j < pat.len) { if (input[i + j] != pat[j]) { ok = false; j = pat.len; } else { j += 1; }; }; if (!ok) { break; }; i += pat.len; }; let r: str; r.ptr = input.ptr + (i: u64); r.len = input.len - i; return r; }; // rtrim — strip occurrences of `exclude` from the end. Borrowed view. // ref/hare/strings/trim.ha:32 (same subset note). export fn rtrim(input: str, exclude: rune) str = { let scratch: [4]u8; let pat: []u8 = runebytes(scratch[0:4], exclude); let n: i32 = input.len; for (n >= pat.len) { let off: i32 = n - pat.len; let j: i32 = 0; let ok: bool = true; for (j < pat.len) { if (input[off + j] != pat[j]) { ok = false; j = pat.len; } else { j += 1; }; }; if (!ok) { break; }; n -= pat.len; }; let r: str; r.ptr = input.ptr; r.len = n; return r; }; // trim — strip from both ends. ref/hare/strings/trim.ha:54. export fn trim(input: str, exclude: rune) str = { return ltrim(rtrim(input, exclude), exclude); }; // iterator — UTF-8 rune cursor over a `str`. Layout flattens Hare's // anonymous-embedded `utf8::decoder` (ref/hare/strings/iter.ha:6-9) to // explicit fields. `reverse` selects walk direction: forward iterators // (`iter`) advance through utf8.next; reverse iterators (`riter`) advance // through utf8.prev. May be copied to save state. export type iterator = struct { offs: i32, src: []u8, reverse: bool, }; // iter — initialize a forward iterator at the start of `src`. // ref/hare/strings/iter.ha:24. export fn iter(src: str) iterator = { let r: iterator; r.src = toutf8(src); r.offs = 0; r.reverse = false; return r; }; // riter — initialize a reverse iterator at the end of `src`. `next` // on a reverse iterator walks back through the string. // ref/hare/strings/iter.ha:32. export fn riter(src: str) iterator = { let r: iterator; r.src = toutf8(src); r.offs = src.len; r.reverse = true; return r; }; // move — private dispatch shared by next/prev. `forward` selects // utf8.next vs utf8.prev. Aborts on more/invalid per Hare's // ref/hare/strings/iter.ha:51-58 ("Invalid UTF-8 string (this should // not happen)"). Hare picks the utf8 function via a fn-pointer; ww // branches on `forward` at each call site instead. fn move(forward: bool, it: *iterator) (rune | utf8.done) = { let d: utf8.decoder; d.src = it.src; d.offs = it.offs; if (forward) { match (utf8.next(&d)) { case let r: rune => { it.offs = d.offs; return r; }; case let dn: utf8.done => return dn; case let m: utf8.more => abort("strings.move: invalid UTF-8"); case let e: utf8.invalid => abort("strings.move: invalid UTF-8"); }; } else { match (utf8.prev(&d)) { case let r: rune => { it.offs = d.offs; return r; }; case let dn: utf8.done => return dn; case let m: utf8.more => abort("strings.move: invalid UTF-8"); case let e: utf8.invalid => abort("strings.move: invalid UTF-8"); }; }; }; // next — advance the iterator one rune. Forward iterators step // through utf8.next; reverse iterators (riter) step backward through // utf8.prev. Returns utf8.done at end-of-walk. ref/hare/strings/iter.ha:45. export fn next(it: *iterator) (rune | utf8.done) = { return move(!it.reverse, it); }; // prev — step back one rune. Dual to next: on a forward iterator // this walks utf8.prev; on a reverse iterator (riter) it walks // utf8.next. ref/hare/strings/iter.ha:49. export fn prev(it: *iterator) (rune | utf8.done) = { return move(it.reverse, it); }; // iterstr — borrowed view of the bytes remaining in the iterator's // walk direction. Forward iter: bytes from offs to end; reverse iter: // bytes from start to offs. ref/hare/strings/iter.ha:63. export fn iterstr(it: *iterator) str = { let r: []u8; if (it.reverse) { r = it.src[0:it.offs]; } else { r = it.src[it.offs:it.src.len]; }; return fromutf8_unsafe(r); }; // strings.slice deferred — Hare's delegation form // `fromutf8_unsafe(utf8::slice(begin, end))` (ref/hare/strings/iter.ha:76) // triggers wwstage fnretlookup same-name cross-module phantom return-ABI // fixup (task #34, Class A wwstage UNDER, sub-bug of #4e). Inlining the // body would diverge from Hare at API level (rule 9). Land after #34. // position — byte-wise offset of the iterator in its source. // ref/hare/strings/iter.ha:82. export fn position(it: *iterator) i32 = { return it.offs; };