// Hare port; see ref/hare/strings/. // // Documented divergences from Hare: // // - `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`/`slice` copy `offs`/`src` into a // local `utf8.decoder` for the call (and `next`/`prev` 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. package strings; import bytes; import encoding.utf8; import os; import types; // 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; }; // Pure borrowed reinterpret per CLAUDE.md rule 9 carve-out; // ref/hare/strings/utf8.ha:10. Mutating the view is valid only when the // caller owns mutable backing storage. export fn frombytes(in: []u8) str = { let r: str; r.ptr = in.ptr; r.len = in.len; r.cap = in.len; return r; }; // Return is a sign (neg/zero/pos), not an index, so it tracks Hare's // `int` rather than the str-index i32 (#8). ref/hare/strings/compare.ha:12. export fn compare(a: str, b: str) int = { 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]: int) - (b[i]: int); }; i += 1; }; return (a.len: int) - (b.len: int); }; // 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; r.cap = 0; if (s.len == 0) { return r; }; let buf: []u8 = alloc([], s.len: u64)!; let i: i32 = 0; for (i < s.len) { buf[i] = s[i]; i += 1; }; buf.len = s.len; return frombytes(buf); }; // Caller releases via [[freeall]]. ref/hare/strings/dup.ha:26 (#6). // // Hare gates the per-element dup behind `?` and rolls back via // `defer if (!ok) freeall(newsl)`. ww has no `defer if`; more // importantly, ww's [[dup]] is still unchecked (returns plain `str`, // aborts via os.alloc on OOM — see top-of-file divergence note), // so the only nomem propagation point is the initial slice alloc. // With no inner failure path, the rollback is structurally a no-op // and is omitted; it returns once dup graduates to `(str | nomem)` // (#46). The pre-allocated slice has `cap == s.len`, so append's // rt_ensure call never reaches the grow branch. // // Empty input bypasses the alloc: rt_malloc(0) is an mmap of 0 bytes // which returns -EINVAL, and the alloc-slice `?` shortcut routes // that through nomem — Hare's heap allocator hands back a sentinel // instead (#47). Return `{nil, 0, 0}` directly so callers get the // Hare-observable shape (len==0, freeall is a no-op via cap==0). export fn dupall(s: []str) ([]str | nomem) = { if (s.len == 0) { let r: []str; r.ptr = nil: *str; r.len = 0; r.cap = 0; return r; }; let newsl: []str = alloc([], s.len)?; let i: i32 = 0; for (i < s.len) { append(newsl, dup(s[i])); i += 1; }; return newsl; }; // 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 * size(str)` — the literal // would drift under #1's str-layout bump, so route through the // typ.ww SSoT. 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) * size(str): u64); }; }; // Caller releases with `os.free(r.ptr, r.len: u64)`. // ref/hare/strings/concat.ha:5. Hare's `nomem` return is dropped: // `os.alloc` aborts on OOM. export fn concat(strs: str...) str = { let total64: i64 = 0; let i: i32 = 0; for (i < strs.len) { total64 += strs[i].len: i64; assert(total64 <= types.I32_MAX: i64, "strings.concat: result exceeds maximum string length"); i += 1; }; let total: i32 = total64: i32; let r: str; r.ptr = nil; r.len = 0; r.cap = 0; if (total == 0) { return r; }; let buf: []u8 = alloc([], total: u64)!; let off: i32 = 0; i = 0; for (i < strs.len) { let j: i32 = 0; for (j < strs[i].len) { buf[off + j] = strs[i][j]; j += 1; }; off += strs[i].len; i += 1; }; buf.len = total; return frombytes(buf); }; // Caller releases with `os.free(r.ptr, r.len: u64)`. // ref/hare/strings/concat.ha:46. Hare's `nomem` return is dropped: // `os.alloc` aborts on OOM. export fn join(delim: str, strs: str...) str = { let total64: i64 = 0; let i: i32 = 0; for (i < strs.len) { total64 += strs[i].len: i64; if (i + 1 < strs.len) { total64 += delim.len: i64; }; assert(total64 <= types.I32_MAX: i64, "strings.join: result exceeds maximum string length"); i += 1; }; let total: i32 = total64: i32; let r: str; r.ptr = nil; r.len = 0; r.cap = 0; if (total == 0) { return r; }; let buf: []u8 = alloc([], total: u64)!; let off: i32 = 0; i = 0; for (i < strs.len) { let j: i32 = 0; for (j < strs[i].len) { buf[off + j] = strs[i][j]; j += 1; }; off += strs[i].len; if (i + 1 < strs.len) { j = 0; for (j < delim.len) { buf[off + j] = delim[j]; j += 1; }; off += delim.len; }; i += 1; }; buf.len = total; return frombytes(buf); }; // ref/hare/strings/sub.ha:10. Aborts on short input per Hare's // contract for the rune-wise [[sub]]. fn utf8bytelenbounded(it: *iterator, end: i32) i32 = { let i: i32 = 0; for (i < end) { match (next(it)) { case let r: rune => void; case utf8.done => abort("strings.sub: index exceeds string length"); }; i += 1; }; return it.offs; }; // Borrowed substring; start/end are rune indices; byte-indexed // counterpart [[bytesub]]. // ref/hare/strings/sub.ha:30. Hare's 2-arg `sub(s, start)` // defaulting end=END is omitted: ww has no default-parameter syntax // (filed as #37). export fn sub(s: str, start: i32, end: i32) str = { assert(start >= 0 && end >= 0, "strings.sub: indexes must not be negative"); assert(start <= end, "strings.sub: start is higher than end"); let it: iterator = iter(s); let starti: i32 = utf8bytelenbounded(&it, start); let endi: i32 = utf8bytelenbounded(&it, end - start); let r: str; r.ptr = s.ptr + (starti: u64); r.len = endi - starti; r.cap = r.len; return r; }; // Borrowed substring; start/end are byte offsets, unlike the // rune-wise [[sub]]. // ref/hare/strings/sub.ha:59 (#7). Returns `utf8.invalid` if // either endpoint lands on a continuation byte (would split a // codepoint); the equivalent Hare predicate is `s[i] & 0xc0 == 0x80` // at ref/hare/strings/sub.ha:72-73. export fn bytesub(s: str, start: i32, end: i32) (str | utf8.invalid) = { assert(start >= 0 && end >= 0, "strings.bytesub: indexes must not be negative"); assert(start <= end, "strings.bytesub: start is higher than end"); assert(end <= s.len, "strings.bytesub: end exceeds string length"); if (start < s.len && (s[start] & 0xC0u8) == 0x80u8) { let e: utf8.invalid; return e; }; if (end < s.len && (s[end] & 0xC0u8) == 0x80u8) { let e: utf8.invalid; return e; }; let r: str; r.ptr = s.ptr + (start: u64); r.len = end - start; r.cap = r.len; return r; }; // `scratch` must hold 4 bytes; the return borrows it. 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; }; // 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); }; // 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); }; // 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); }; // 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); }; // Mirrors ref/hare/strings/index.ha:59 (#10). Hare copies // `rest_iter = s_iter` directly via struct assignment; ww re-seats // `rest_iter` field-wise because the let-init struct-copy form // diverges between cstage and wwstage on this iterator type // (993_ww_ww + 995_self_rebuild fail, filed as #41) and rule #10 // (CLAUDE.md) forbids stage asymmetry. fn indexstring(haystack: str, needle: str) (i32 | void) = { let s_iter: iterator = iter(haystack); let i: i32 = 0; for (true) { let rest_iter: iterator; rest_iter.src = s_iter.src; rest_iter.offs = s_iter.offs; rest_iter.reverse = s_iter.reverse; let needle_iter: iterator = iter(needle); let matched: bool = false; for (true) { let rest_done: bool = false; let rest_r: rune; match (next(&rest_iter)) { case let r: rune => rest_r = r; case utf8.done => rest_done = true; }; let needle_done: bool = false; let needle_r: rune; match (next(&needle_iter)) { case let r: rune => needle_r = r; case utf8.done => needle_done = true; }; if (rest_done && !needle_done) { break; }; if (needle_done) { matched = true; break; }; if (rest_r != needle_r) { break; }; }; if (matched) { return i; }; match (next(&s_iter)) { case let r: rune => i += 1; case utf8.done => return; }; }; return; }; // Rune-wise offset, not byte-wise ([[byteindex]]). // ref/hare/strings/index.ha:10. The str-arm delegates to // [[indexstring]] (per Hare's `index_string`, #10); the rune-arm // mirrors Hare's `index_rune` (ref/hare/strings/index.ha:31). export fn index(haystack: str, needle: (str | rune)) (i32 | void) = { match (needle) { case let s: str => return indexstring(haystack, s); case let r: rune => { let it: iterator = iter(haystack); let i: i32 = 0; for (true) { match (next(&it)) { case let n: rune => { if (n == r) { return i; }; i += 1; }; case utf8.done => return; }; }; }; }; return; }; // Rune-wise offset. ref/hare/strings/index.ha:22. The // str-arm reuses `rbyteindex`; the rune-arm walks forward tracking // the most recent matching rune index (Hare's `rindex_rune` with // `riter` returns a byte-offset value for multibyte strings, which // disagrees with the rune-wise docstring; we keep the docstring's // contract). export fn rindex(haystack: str, needle: (str | rune)) (i32 | void) = { match (needle) { case let s: str => { match (rbyteindex(haystack, s)) { case void => return; case let bo: i32 => { let it: iterator = iter(haystack); let i: i32 = 0; for (position(&it) < bo) { match (next(&it)) { case let r: rune => i += 1; case utf8.done => break; }; }; return i; }; }; }; case let r: rune => { let it: iterator = iter(haystack); let i: i32 = 0; let last: i32 = -1; for (true) { match (next(&it)) { case let n: rune => { if (n == r) { last = i; }; i += 1; }; case utf8.done => break; }; }; if (last < 0) { return; }; return last; }; }; return; }; // ref/hare/strings/contains.ha:9. export fn contains(haystack: str, needles: (str | rune)...) bool = { let i: i32 = 0; for (i < needles.len) { match (needles[i]) { case let s: str => { match (byteindex(haystack, s)) { case let bo: i32 => return true; case void => void; }; }; case let r: rune => { match (byteindex(haystack, r)) { case let bo: i32 => return true; case void => void; }; }; }; i += 1; }; return false; }; // ref/hare/strings/trim.ha:60. Borrowed view. 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; r.cap = r.len; return r; }; // 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; r.cap = r.len; return r; }; // ASCII set for the 0-arg ltrim/rtrim/trim branches (#9). // ref/hare/strings/trim.ha:6. let whitespace: [4]u8 = [0x20u8, 0x0Au8, 0x09u8, 0x0Du8]; // Borrowed view. 0-arg strips ASCII whitespace via [[bytes.ltrim]] (#9). // ref/hare/strings/trim.ha:11. The spread expression is inlined // because `let ws: []u8 = whitespace[0:4]` produces a slice whose // ptr doesn't track the module-level array storage (filed as #40); // `b.flush = flushdefault[0:1]` in lib/bufio is the same shape via // the working field-assign path. export fn ltrim(input: str, trim: rune...) str = { if (trim.len == 0) { return frombytes(bytes.ltrim(toutf8(input), whitespace[0:4]...)); }; let it: iterator = iter(input); for (true) { match (next(&it)) { case let r: rune => { let j: i32 = 0; let found: bool = false; for (j < trim.len) { if (r == trim[j]) { found = true; j = trim.len; } else { j += 1; }; }; if (!found) { match (prev(&it)) { case let r2: rune => void; case utf8.done => void; }; break; }; }; case utf8.done => break; }; }; return iterstr(&it); }; // 0-arg strips ASCII whitespace via [[bytes.rtrim]] (#9). Spread is // Borrowed view; inlined to dodge #40 — see [[ltrim]]. // ref/hare/strings/trim.ha:32. export fn rtrim(input: str, trim: rune...) str = { if (trim.len == 0) { return frombytes(bytes.rtrim(toutf8(input), whitespace[0:4]...)); }; let it: iterator = riter(input); for (true) { match (next(&it)) { case let r: rune => { let j: i32 = 0; let found: bool = false; for (j < trim.len) { if (r == trim[j]) { found = true; j = trim.len; } else { j += 1; }; }; if (!found) { match (prev(&it)) { case let r2: rune => void; case utf8.done => void; }; break; }; }; case utf8.done => break; }; }; return iterstr(&it); }; // ref/hare/strings/trim.ha:54. export fn trim(input: str, trim: rune...) str = { return ltrim(rtrim(input, trim...), trim...); }; // 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, }; // 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; }; // 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; }; // 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; // utf8.decoder.offs is `size` (#70); the iterator carries i32. The // rune-return path keeps offs in [0, len), so the narrowing cast back // is safe. d.offs = it.offs: size; if (forward) { match (utf8.next(&d)) { case let r: rune => { it.offs = d.offs: i32; 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: i32; 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"); }; }; }; // ref/hare/strings/iter.ha:45. export fn next(it: *iterator) (rune | utf8.done) = { return move(!it.reverse, it); }; // ref/hare/strings/iter.ha:49. export fn prev(it: *iterator) (rune | utf8.done) = { return move(it.reverse, it); }; // Borrowed view. 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 frombytes(r); }; // ref/hare/strings/iter.ha:75. Hare passes `*iterator` directly where // `*utf8::decoder` is expected via anonymous-embed coercion; ww has // no anonymous embed, so we reconstruct a local utf8.decoder for each // endpoint and forward — same pattern as `move` above. export fn slice(begin: *iterator, end: *iterator) str = { let b: utf8.decoder; b.src = begin.src; b.offs = begin.offs: size; // decoder.offs is size (#70) let e: utf8.decoder; e.src = end.src; e.offs = end.offs: size; return frombytes(utf8.slice(&b, &e)); }; // ref/hare/strings/iter.ha:82. export fn position(it: *iterator) i32 = { return it.offs; }; // ref/hare/strings/tokenize.ha:7. // First cross-module type alias in tree; needs #22's transitive // alias-chain unwrap (cstage type_chase_named + wwstage // structlookupchain) to walk struct fields through the chain. export type tokenizer = bytes.tokenizer; // `s` and `delim` are borrowed; caller keeps them live for the // tokenizer's lifetime. ref/hare/strings/tokenize.ha:32. ASCII-only delim // asserted per Hare lines 35-37: a multibyte rune in delim would // split on a single continuation byte and yield invalid UTF-8. export fn tokenize(s: str, delim: str) tokenizer = { let d: []u8 = toutf8(delim); let i: i32 = 0; for (i < d.len) { assert((d[i] & 0x80u8) == 0u8, "strings.tokenize cannot tokenize on non-ASCII delimiters"); i += 1; }; return bytes.tokenize(toutf8(s), d...); }; // ref/hare/strings/tokenize.ha:44. export fn rtokenize(s: str, delim: str) tokenizer = { let d: []u8 = toutf8(delim); let i: i32 = 0; for (i < d.len) { assert((d[i] & 0x80u8) == 0u8, "strings.rtokenize cannot tokenize on non-ASCII delimiters"); i += 1; }; return bytes.rtokenize(toutf8(s), d...); }; // ref/hare/strings/tokenize.ha:62. export fn nexttoken(s: *tokenizer) (str | bytes.done) = { let b: *bytes.tokenizer = s: *bytes.tokenizer; match (bytes.nexttoken(b)) { case let v: []u8 => return frombytes(v); case bytes.done => { let d: bytes.done; return d; }; }; }; // ref/hare/strings/tokenize.ha:71. export fn peektoken(s: *tokenizer) (str | bytes.done) = { let b: *bytes.tokenizer = s: *bytes.tokenizer; match (bytes.peektoken(b)) { case let v: []u8 => return frombytes(v); case bytes.done => { let d: bytes.done; return d; }; }; }; // ref/hare/strings/tokenize.ha:79. export fn remainingtokens(s: *tokenizer) str = { let b: *bytes.tokenizer = s: *bytes.tokenizer; return frombytes(bytes.remainingtokens(b)); }; // When `delim` is absent the whole input is the first half and the // second is empty. Both halves are borrowed from `in`; caller // ensures `delim` is non-empty. ref/hare/strings/tokenize.ha:288. export fn cut(in: str, delim: str) (str, str) = { let (a, b) = bytes.cut(toutf8(in), toutf8(delim)); return (frombytes(a), frombytes(b)); }; // ref/hare/strings/tokenize.ha:302. export fn rcut(in: str, delim: str) (str, str) = { let (a, b) = bytes.rcut(toutf8(in), toutf8(delim)); return (frombytes(a), frombytes(b)); }; // The trailing slot (when more than `n - 1` tokens exist) holds the // unconsumed remainder. Strings within the result are borrowed // from `in`. // // The caller frees the returned slice via // `os.free(r.ptr: *void, (r.cap: u64) * size(str): u64)`. // // Hare's `([]str | nomem)` collapses to `[]str` here: ww os.alloc // has no recoverable failure path. Same precedent as // shlex.split / bytes.splitn. // // ref/hare/strings/tokenize.ha:172. export fn splitn(in: str, delim: str, n: i32) []str = { assert(n >= 0, "strings.splitn: token limit must not be negative"); let toks: []str; toks.ptr = nil: *str; toks.len = 0; toks.cap = 0; if (n == 0) { return toks; }; let tok: tokenizer = tokenize(in, delim); let i: i32 = 0; for (i < n - 1) { match (nexttoken(&tok)) { case let s: str => { append(toks, s); }; case bytes.done => { return toks; }; }; i += 1; }; match (peektoken(&tok)) { case bytes.done => void; case let pk: str => { let r: str = remainingtokens(&tok); append(toks, r); }; }; return toks; }; // The trailing slot holds the unconsumed prefix (everything before // the n-th-from-last delim hit). // // ref/hare/strings/tokenize.ha:200. export fn rsplitn(in: str, delim: str, n: i32) []str = { assert(n >= 0, "strings.rsplitn: token limit must not be negative"); let toks: []str; toks.ptr = nil: *str; toks.len = 0; toks.cap = 0; if (n == 0) { return toks; }; let tok: tokenizer = rtokenize(in, delim); let i: i32 = 0; for (i < n - 1) { match (nexttoken(&tok)) { case let s: str => { append(toks, s); }; case bytes.done => { break; }; }; i += 1; }; match (peektoken(&tok)) { case bytes.done => void; case let pk: str => { let r: str = remainingtokens(&tok); append(toks, r); }; }; // In-place reverse so callers see argv-order, matching Hare // (ref/hare/strings/tokenize.ha:220). Element copy is field-wise // through `*str` because `toks[i] = toks[j]` (full str-header store) // lands in the multi-word-store gap noted at cmd/w6c/cgen.c:6515. let a: i32 = 0; let b: i32 = toks.len - 1; for (a < b) { let pa: *str = &toks.ptr[a]; let pb: *str = &toks.ptr[b]; let tp: *u8 = pa.ptr; let tl: i32 = pa.len; let tc: i32 = pa.cap; pa.ptr = pb.ptr; pa.len = pb.len; pa.cap = pb.cap; pb.ptr = tp; pb.len = tl; pb.cap = tc; a += 1; b -= 1; }; return toks; }; // Mirrors `splitn(in, delim, types::SIZE_MAX)`. ww uses `types.I32_MAX` // because the index type is i32 (lib/CLAUDE.md). // // ref/hare/strings/tokenize.ha:242. export fn split(in: str, delim: str) []str = { return splitn(in, delim, types.I32_MAX); }; // Length is measured in bytes, matching Hare's `len(s) >= maxlen` at // ref/hare/strings/pad.ha:9. The exact byte target must hold a whole number of // pad runes; otherwise no valid UTF-8 result of that length exists. Caller // releases with `os.free(r.ptr, r.len: u64)`. export fn lpad(s: str, p: rune, maxlen: i32) str = { assert(maxlen >= 0, "strings.lpad: length must not be negative"); if (s.len >= maxlen) { return dup(s); }; let scratch: [4]u8; let pad: []u8 = runebytes(scratch[0:4], p); let gap: i32 = maxlen - s.len; assert(gap % pad.len == 0, "strings.lpad: byte length would split the pad rune"); let buf: []u8 = alloc([], maxlen: u64)!; let off: i32 = 0; for (off < gap) { buf[off] = pad.ptr[off % pad.len]; off += 1; }; let k: i32 = 0; for (k < s.len) { buf[off + k] = s[k]; k += 1; }; buf.len = maxlen; return frombytes(buf); }; // Replaces every non-overlapping occurrence of `needle`. Caller // releases with `os.free(r.ptr, r.len: u64)`. // ref/hare/strings/replace.ha:8 (#4). // // Hare delegates to [[multireplace]] with a single pair; ww has no // `(str, str)` variadic shape today (#39), so this is a standalone // two-pass implementation: pass 1 counts matches to size the result, // pass 2 copies chunks and `target` into a single fresh buffer. // Single nomem path (the `alloc([], total)?`) preserves Hare's // signature without a per-write `append(...)?` (ww's append builtin // aborts on OOM, #11). Empty `needle` would hasprefix-match every // position with a zero stride, so it is rejected as an explicit // precondition rather than hanging. export fn replace(s: str, needle: str, target: str) (str | nomem) = { assert(needle.len > 0, "strings.replace: needle must not be empty"); let sb: []u8 = toutf8(s); let nb: []u8 = toutf8(needle); let tb: []u8 = toutf8(target); let count: i32 = 0; let i: i32 = 0; for (i < sb.len) { if (bytes.hasprefix(sb[i:sb.len], nb)) { count += 1; i += nb.len; } else { i += 1; }; }; let total64: i64 = (sb.len: i64) + (count: i64) * ((tb.len: i64) - (nb.len: i64)); if (total64 > types.I32_MAX: i64) { let e: nomem; return e; }; let total: i32 = total64: i32; if (total == 0) { let r: str; r.ptr = nil; r.len = 0; r.cap = 0; return r; }; let res: []u8 = alloc([], total)?; let off: i32 = 0; i = 0; for (i < sb.len) { if (bytes.hasprefix(sb[i:sb.len], nb)) { let j: i32 = 0; for (j < tb.len) { res.ptr[off + j] = tb.ptr[j]; j += 1; }; off += tb.len; i += nb.len; } else { res.ptr[off] = sb.ptr[i]; off += 1; i += 1; }; }; res.len = total; return frombytes(res); }; // Symmetric with [[lpad]]. ref/hare/strings/pad.ha:39. export fn rpad(s: str, p: rune, maxlen: i32) str = { assert(maxlen >= 0, "strings.rpad: length must not be negative"); if (s.len >= maxlen) { return dup(s); }; let scratch: [4]u8; let pad: []u8 = runebytes(scratch[0:4], p); let gap: i32 = maxlen - s.len; assert(gap % pad.len == 0, "strings.rpad: byte length would split the pad rune"); let buf: []u8 = alloc([], maxlen: u64)!; let k: i32 = 0; for (k < s.len) { buf[k] = s[k]; k += 1; }; let i: i32 = 0; for (i < gap) { buf[s.len + i] = pad.ptr[i % pad.len]; i += 1; }; buf.len = maxlen; return frombytes(buf); };