// Hare port of the in-tree subset; see ref/hare/bytes/. // // Documented divergences from Hare: // - peektoken dispatches index/rindex by branching on `reverse` // rather than a function-pointer `ifunc` (ref/hare/bytes/tokenize.ha:97). // ww has no fn pointers in scope yet — same pattern as lib/strings // `move`. Outwardly identical. // - tokenize / rtokenize zero the `delim` field on the constructed // tokenizer when `in` is empty, rather than mutating the variadic // param before the struct write (ref/hare/bytes/tokenize.ha:26-28). // Semantically identical; the variadic param is borrowed and // captured-by-value into the struct, so mutating either side // yields the same observable state. package bytes; import types; // done — iteration sentinel returned by nexttoken / peektoken at // end-of-input. ref/hare/bytes/tokenize.ha uses the built-in `done` // token; ww spells it per-package the same way lib/encoding/utf8 does // (utf8.ww:36). Plain `void` (not `!void`): continuation signal. export type done = void; // Layout mirrors ref/hare/bytes/tokenize.ha:6-10. `p` is the cached // peek-position; I64_MAX (forward) / I64_MIN (reverse) are the // unprimed sentinels. p < 0 also identifies a reverse-direction // iterator. export type tokenizer = struct { in: []u8, delim: []u8, p: i64, }; // ref/hare/bytes/equal.ha:9. export fn equal(a: []u8, b: []u8) bool = { if (a.len != b.len) { return false; }; let i: i32 = 0; for (i < a.len) { if (a[i] != b[i]) { return false; }; i += 1; }; return true; }; // Maximal suffix and period for the Crochemore-Perrin two-way search. // The signed -1 sentinel is the i32 translation of Hare's SIZE_MAX in // ref/hare/bytes/two_way.ha:68. fn indexmaxsuf(x: []u8, inv: bool) (i32, i32) = { let i: i32 = -1; let j: i32 = 0; let k: i32 = 1; let p: i32 = 1; for (j + k < x.len) { let a: u8 = x[j + k]; let b: u8 = x[i + k]; if (a == b) { if (k == p) { j += p; k = 1; } else { k += 1; }; } else if ((a < b) != inv) { j += k; k = 1; p = j - i; } else { i = j; j += 1; k = 1; p = 1; }; }; return (i, p); }; // Allocation-free O(n+m) forward search. This follows // ref/hare/bytes/two_way.ha, with the critical-period test stated directly on // the needle: periodicity is a property of the needle, never the haystack. fn indextwoway(haystack: []u8, needle: []u8) (i32 | void) = { let n: i32 = haystack.len; let m: i32 = needle.len; if (n < m) { return; }; if (n == m) { if (equal(haystack, needle)) { return 0; }; return; }; let (i0, p0) = indexmaxsuf(needle, false); let (i1, p1) = indexmaxsuf(needle, true); let ms: i32 = i0; let per: i32 = p0; if (i0 < i1) { ms = i1; per = p1; }; let periodic: bool = per + ms + 1 <= m; let q: i32 = 0; for (periodic && q <= ms) { if (needle[q] != needle[per + q]) { periodic = false; }; q += 1; }; let mem0: i32 = 0; if (periodic) { mem0 = m - per; } else { let left: i32 = ms + 1; let right: i32 = m - ms - 1; if (left > right) { per = left + 1; } else { per = right + 1; }; }; let off: i32 = 0; let mem: i32 = 0; for (off <= n - m) { let i: i32 = ms + 1; if (mem > i) { i = mem; }; for (i < m && needle[i] == haystack[off + i]) { i += 1; }; if (i < m) { off += i - ms; mem = 0; continue; }; i = ms + 1; for (i > mem && needle[i - 1] == haystack[off + i - 1]) { i -= 1; }; if (i <= mem) { return off; }; off += per; mem = mem0; }; return; }; // ref/hare/bytes/index.ha:6. export fn index(s: []u8, needle: (u8 | []u8)) (i32 | void) = { match (needle) { case let c: u8 => { let i: i32 = 0; for (i < s.len) { if (s[i] == c) { return i; }; i += 1; }; return; }; case let sub: []u8 => { if (sub.len == 0) { return 0; }; if (sub.len == 1) { return index(s, sub[0]); }; return indextwoway(s, sub); }; }; return; }; // Empty []u8 needle returns s.len (ref/hare/bytes/index.ha:103 — // Hare's loop yields r-0 at i=0). ref/hare/bytes/index.ha:86. export fn rindex(s: []u8, needle: (u8 | []u8)) (i32 | void) = { match (needle) { case let c: u8 => { let i: i32 = s.len - 1; for (i >= 0) { if (s[i] == c) { return i; }; i -= 1; }; return; }; case let sub: []u8 => { if (sub.len == 0) { return s.len; }; if (sub.len > s.len) { return; }; let i: i32 = s.len - sub.len; for (i >= 0) { let j: i32 = 0; let ok: bool = true; for (j < sub.len) { if (s[i + j] != sub[j]) { ok = false; j = sub.len; } else { j += 1; }; }; if (ok) { return i; }; i -= 1; }; return; }; }; return; }; // ref/hare/bytes/contains.ha:6. export fn contains(s: []u8, needles: (u8 | []u8)...) bool = { let i: i32 = 0; for (i < needles.len) { match (needles[i]) { case let b: u8 => { match (index(s, b)) { case let bo: i32 => return true; case void => void; }; }; case let n: []u8 => { match (index(s, n)) { case let bo: i32 => return true; case void => void; }; }; }; i += 1; }; return false; }; // ref/hare/bytes/trim.ha:7. Borrowed view of `in` — caller must not free. export fn ltrim(in: []u8, trim: u8...) []u8 = { assert(trim.len > 0, "bytes.ltrim called with empty trim set"); let i: i32 = 0; for (i < in.len && contains(trim, in[i])) { i += 1; }; let r: []u8; r.ptr = in.ptr + (i: u64); r.len = in.len - i; r.cap = r.len; return r; }; // ref/hare/bytes/trim.ha:17. Borrowed view of `in`. Hare's loop uses // `size` underflow at i==0 to terminate; ww indices are signed i32, so // the equivalent termination is spelled `i >= 0` explicitly. export fn rtrim(in: []u8, trim: u8...) []u8 = { assert(trim.len > 0, "bytes.rtrim called with empty trim set"); let i: i32 = in.len - 1; for (i >= 0 && contains(trim, in[i])) { i -= 1; }; let r: []u8; r.ptr = in.ptr; r.len = i + 1; r.cap = r.len; return r; }; // ref/hare/bytes/trim.ha:27. Borrowed view of `in`. export fn trim(in: []u8, trim: u8...) []u8 = { return ltrim(rtrim(in, trim...), trim...); }; // ref/hare/bytes/contains.ha:21. export fn hasprefix(s: []u8, pre: []u8) bool = { if (pre.len > s.len) { return false; }; let i: i32 = 0; for (i < pre.len) { if (s[i] != pre[i]) { return false; }; i += 1; }; return true; }; // ref/hare/bytes/contains.ha:35. export fn hassuffix(s: []u8, suf: []u8) bool = { if (suf.len > s.len) { return false; }; let off: i32 = s.len - suf.len; let i: i32 = 0; for (i < suf.len) { if (s[off + i] != suf[i]) { return false; }; i += 1; }; return true; }; // ref/hare/bytes/reverse.ha:5. export fn reverse(s: []u8) void = { let i: i32 = 0; let j: i32 = s.len - 1; for (i < j) { let t: u8 = s[i]; s[i] = s[j]; s[j] = t; i += 1; j -= 1; }; }; // ref/hare/bytes/zero.ha:5. export fn zero(s: []u8) void = { let i: i32 = 0; for (i < s.len) { s[i] = 0u8; i += 1; }; }; // `delim` is borrowed; caller keeps it valid for the tokenizer's // lifetime. ref/hare/bytes/tokenize.ha:22. export fn tokenize(in: []u8, delim: u8...) tokenizer = { assert(delim.len > 0, "bytes.tokenize called with empty slice"); let t: tokenizer; t.in = in; t.delim = delim; if (in.len == 0) { t.delim.len = 0; t.delim.cap = 0; }; t.p = types.I64_MAX; return t; }; // ref/hare/bytes/tokenize.ha:40. export fn rtokenize(in: []u8, delim: u8...) tokenizer = { assert(delim.len > 0, "bytes.rtokenize called with empty slice"); let t: tokenizer; t.in = in; t.delim = delim; if (in.len == 0) { t.delim.len = 0; t.delim.cap = 0; }; t.p = types.I64_MIN; return t; }; // Returns done once `s.delim` has been zeroed by a prior past-end // nexttoken. ref/hare/bytes/tokenize.ha:91. export fn peektoken(s: *tokenizer) ([]u8 | done) = { if (s.delim.len == 0) { let d: done; return d; }; let reverse: bool = s.p < 0i64; let known: bool = false; if (reverse) { if (s.p != types.I64_MIN) { known = true; }; } else { if (s.p != types.I64_MAX) { known = true; }; }; if (!known) { let i: i64 = types.I64_MAX; if (reverse) { i = types.I64_MIN; }; let dlen: i64 = 0i64; let slen: i64 = s.in.len: i64; let k: i32 = 0; for (k < s.delim.len) { let d: u8 = s.delim[k]; let ix_found: bool = false; let ix_val: i32 = 0; if (reverse) { match (rindex(s.in, d)) { case let v: i32 => { ix_found = true; ix_val = v; }; case void => void; }; } else { match (index(s.in, d)) { case let v: i32 => { ix_found = true; ix_val = v; }; case void => void; }; }; if (ix_found) { if (!reverse) { if ((ix_val: i64) < i) { i = ix_val: i64; dlen = 1i64; }; } else { if ((ix_val: i64) > i) { i = ix_val: i64; dlen = 1i64; }; }; } else { if (!reverse) { if (slen < i) { i = slen; }; } else { if (0i64 > i) { i = 0i64; }; }; }; k += 1; }; if (reverse) { if (i == slen) { s.p = -(slen + 1i64); } else { s.p = i + dlen - slen - 1i64; }; } else { s.p = i; }; }; let r: []u8; if (reverse) { let start: i32 = (s.in.len: i64 + s.p + 1i64): i32; r.ptr = s.in.ptr + (start: u64); r.len = s.in.len - start; r.cap = r.len; } else { let end: i32 = s.p: i32; r.ptr = s.in.ptr; r.len = end; r.cap = end; }; return r; }; // Once the input is exhausted, returns done and zeros `s.delim` so // subsequent peeks short-circuit. ref/hare/bytes/tokenize.ha:59. export fn nexttoken(s: *tokenizer) ([]u8 | done) = { let b: []u8; match (peektoken(s)) { case let v: []u8 => { b = v; }; case done => { let d: done; return d; }; }; let slen: i64 = s.in.len: i64; let reverse: bool = s.p < 0i64; if (reverse) { if (slen + s.p + 1i64 == 0i64) { s.delim.len = 0; s.delim.cap = 0; s.in.len = 0; s.in.cap = 0; } else { let end: i32 = (slen + s.p + 1i64 - 1i64): i32; s.in.len = end; s.in.cap = end; }; s.p = types.I64_MIN; } else { if (s.p == slen) { s.delim.len = 0; s.delim.cap = 0; s.in.len = 0; s.in.cap = 0; } else { let adv: u64 = (s.p: u64) + 1u64; let adv_i32: i32 = (s.p: i32) + 1; s.in.ptr = s.in.ptr + adv; s.in.len = s.in.len - adv_i32; s.in.cap = s.in.cap - adv_i32; }; s.p = types.I64_MAX; }; return b; }; // ref/hare/bytes/tokenize.ha:145. Read-only borrowed view. export fn remainingtokens(s: *tokenizer) []u8 = { return s.in; }; // The trailing slot (when more than `n - 1` tokens exist) holds the // unconsumed remainder. // // The caller frees the returned slice via // `os.free(r.ptr: *void, (r.cap: u64) * 24u64)`. Element bytes are // borrowed from `in`. // // Hare's `([][]u8 | nomem)` collapses to `[][]u8` here: ww os.alloc // has no recoverable failure path. Same precedent as // shlex.split / getopt.tryparse. // // ref/hare/bytes/tokenize.ha:156. export fn splitn(in: []u8, delim: []u8, n: i32) [][]u8 = { assert(delim.len > 0, "bytes.splitn must not be called with an empty delimiter"); assert(n >= 0, "bytes.splitn: token limit must not be negative"); let toks: [][]u8; toks.ptr = nil: *[]u8; 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: []u8 => { append(toks, s); }; case done => { return toks; }; }; i += 1; }; match (peektoken(&tok)) { case done => void; case let pk: []u8 => { let r: []u8 = 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/bytes/tokenize.ha:186. export fn rsplitn(in: []u8, delim: []u8, n: i32) [][]u8 = { assert(delim.len > 0, "bytes.rsplitn called with empty delimiter"); assert(n >= 0, "bytes.rsplitn: token limit must not be negative"); let toks: [][]u8; toks.ptr = nil: *[]u8; 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: []u8 => { append(toks, s); }; case done => { break; }; }; i += 1; }; match (peektoken(&tok)) { case done => void; case let pk: []u8 => { let r: []u8 = remainingtokens(&tok); append(toks, r); }; }; // In-place reverse so callers see argv-order, matching Hare // (ref/hare/bytes/tokenize.ha:207). Element copy is field-wise // through `*[]u8` because `toks[i] = toks[j]` (full 24B slice // store) lands in the multi-word-store gap noted at // cmd/w6c/cgen.c:6515-6523. let a: i32 = 0; let b: i32 = toks.len - 1; for (a < b) { let pa: *[]u8 = &toks.ptr[a]; let pb: *[]u8 = &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/bytes/tokenize.ha:225. export fn split(in: []u8, delim: []u8) [][]u8 = { return splitn(in, delim, types.I32_MAX); }; // When `delim` is absent, the whole input is the first half and the // second is empty. Both halves are borrowed views — caller must not // free them. ref/hare/bytes/tokenize.ha:392. // // Delim is spelled (u8 | []u8) to match index/rindex (bytes.ww:57/91); // the tagged union is an unordered set, so this is the same type as // Hare's ([]u8 | u8), not a divergence. export fn cut(in: []u8, delim: (u8 | []u8)) ([]u8, []u8) = { let ln: i32 = match (delim) { case let c: u8 => yield 1i32; case let sub: []u8 => { assert(sub.len > 0, "bytes.cut called with empty delimiter"); yield sub.len; }; }; match (index(in, delim)) { case let i: i32 => { let lo: i32 = i + ln; return (in[0:i], in[lo:in.len]); }; case void => { let empty: []u8; empty.ptr = nil; empty.len = 0; empty.cap = 0; return (in, empty); }; }; }; // ref/hare/bytes/tokenize.ha:413. export fn rcut(in: []u8, delim: (u8 | []u8)) ([]u8, []u8) = { let ln: i32 = match (delim) { case let c: u8 => yield 1i32; case let sub: []u8 => { assert(sub.len > 0, "bytes.rcut called with empty delimiter"); yield sub.len; }; }; match (rindex(in, delim)) { case let i: i32 => { let lo: i32 = i + ln; return (in[0:i], in[lo:in.len]); }; case void => { let empty: []u8; empty.ptr = nil; empty.len = 0; empty.cap = 0; return (in, empty); }; }; };