Files
ww/lib/strings/strings.ww

609 lines
18 KiB
Plaintext

// strings — operations over str ({ptr,len}). Hare port; see
// ref/hare/strings/.
//
// Documented divergences from Hare:
//
// - `trim` / `ltrim` / `rtrim` 0-arg returns the input unchanged.
// Hare strips ASCII whitespace via `bytes::ltrim(input,
// whitespace...)`; that needs `lib/bytes` variadic graduation
// (future commit).
// - `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;
// 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 each element of `strs` in
// order. 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 total: i32 = 0;
let i: i32 = 0;
for (i < strs.len) { total += strs[i].len; i += 1; };
let r: str;
r.ptr = nil;
r.len = 0;
if (total == 0) { return r; };
let buf: *u8 = os.alloc(total: u64): *u8;
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;
};
r.ptr = buf;
r.len = total;
return r;
};
// join — fresh allocation with `delim` placed between each element of
// `strs`. 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 total: i32 = 0;
let i: i32 = 0;
for (i < strs.len) {
total += strs[i].len;
if (i + 1 < strs.len) { total += delim.len; };
i += 1;
};
let r: str;
r.ptr = nil;
r.len = 0;
if (total == 0) { return r; };
let buf: *u8 = os.alloc(total: u64): *u8;
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;
};
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);
};
// index — rune-wise offset of `needle`'s first occurrence in
// `haystack`, or void if absent. ref/hare/strings/index.ha:10. The
// str-arm reuses `byteindex` for the anchor byte offset and then
// walks `iter` forward to convert byte→rune index; 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 => {
match (byteindex(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;
for (true) {
match (next(&it)) {
case let n: rune => {
if (n == r) { return i; };
i += 1;
};
case utf8.done => return;
};
};
};
};
return;
};
// rindex — rune-wise offset of `needle`'s last occurrence in
// `haystack`, or void if absent. 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;
};
// contains — true iff any of `needles` occurs in `haystack`.
// 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;
};
// 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 leading runes that occur in `trim`. Borrowed view.
// Empty `trim` returns input unchanged (Hare's no-rune branch strips
// ASCII whitespace via `bytes::ltrim`; needs lib/bytes variadic
// graduation). ref/hare/strings/trim.ha:11.
export fn ltrim(input: str, trim: rune...) str = {
if (trim.len == 0) { return input; };
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);
};
// rtrim — strip trailing runes that occur in `trim`. Borrowed view.
// ref/hare/strings/trim.ha:32.
export fn rtrim(input: str, trim: rune...) str = {
if (trim.len == 0) { return input; };
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);
};
// trim — strip from both ends. ref/hare/strings/trim.ha:54.
export fn trim(input: str, trim: rune...) str = {
return ltrim(rtrim(input, trim...), trim...);
};
// 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);
};
// slice — borrowed substring between two iterator positions.
// 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;
let e: utf8.decoder;
e.src = end.src;
e.offs = end.offs;
return fromutf8_unsafe(utf8.slice(&b, &e));
};
// 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;
};
// tokenizer — re-export of bytes.tokenizer. 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;
// tokenize — yield substrings of `s` split on any byte in `delim`.
// Leading / trailing / adjacent delims yield empty tokens. `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) {
os.assert((d[i] & 0x80u8) == 0u8,
"strings.tokenize cannot tokenize on non-ASCII delimiters");
i += 1;
};
return bytes.tokenize(toutf8(s), d...);
};
// rtokenize — reverse-direction counterpart to [[tokenize]]. First
// next_token yields the last token, last yields the first.
// 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) {
os.assert((d[i] & 0x80u8) == 0u8,
"strings.rtokenize cannot tokenize on non-ASCII delimiters");
i += 1;
};
return bytes.rtokenize(toutf8(s), d...);
};
// next_token — current token, advancing the cursor.
// ref/hare/strings/tokenize.ha:62.
export fn next_token(s: *tokenizer) (str | bytes.done) = {
let b: *bytes.tokenizer = s: *bytes.tokenizer;
match (bytes.next_token(b)) {
case let v: []u8 => return fromutf8_unsafe(v);
case bytes.done => { let d: bytes.done; return d; };
};
};
// peek_token — current token without advancing.
// ref/hare/strings/tokenize.ha:71.
export fn peek_token(s: *tokenizer) (str | bytes.done) = {
let b: *bytes.tokenizer = s: *bytes.tokenizer;
match (bytes.peek_token(b)) {
case let v: []u8 => return fromutf8_unsafe(v);
case bytes.done => { let d: bytes.done; return d; };
};
};
// remaining_tokens — unconsumed portion of the input ahead of the
// cursor. ref/hare/strings/tokenize.ha:79.
export fn remaining_tokens(s: *tokenizer) str = {
let b: *bytes.tokenizer = s: *bytes.tokenizer;
return fromutf8_unsafe(bytes.remaining_tokens(b));
};