Files
ww/lib/strings/strings.ww

926 lines
26 KiB
Plaintext

// 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);
};