Port four of the five c3 strings functions per ref/hare/strings/iter.ha; strings.slice deferred behind task #34 (wwstage fnretlookup same-name cross-module phantom return-ABI fixup, sub-bug of #4e). - prev ref/hare/strings/iter.ha:49 - riter ref/hare/strings/iter.ha:32 - iterstr ref/hare/strings/iter.ha:63 - position ref/hare/strings/iter.ha:82 Also adds a private move() helper (ref/hare/strings/iter.ha:51) shared by next/prev. Hare's move picks the utf8 function via a fn-pointer (`let fun = if (forward) &utf8::next else &utf8::prev`); ww has no fn-pointers in scope yet, so move branches on `forward` and calls utf8.next or utf8.prev directly at each site. strings.next is updated to dispatch via move(!it.reverse, it) — c2's implementation always called utf8.next regardless of iter.reverse, which was correct for forward iter() but would walk forward on riter()-produced iterators too. With riter landed in this commit, next() now correctly walks backward on reverse iterators per Hare's ref/hare/strings/iter.ha:45. No in-tree consumer regression: only stringstest constructs iterators today. strings.iterstr uses ww's `[lo:hi]` slice syntax instead of Hare's `[lo..hi]`; same semantics (borrowed []u8 view). strings.slice deferred — Hare's body is `fromutf8_unsafe(utf8::slice(begin, end))` (ref/hare/strings/iter.ha:76). That delegation form triggers a wwstage fnretlookup miscompile when the caller module has a function of the same name as the callee (here: both strings::slice and utf8::slice exist), causing wwstage to emit the wrong return-ABI fixup (MOVQ DX, BX, str's AX/DX→AX/BX shim) after the cross-module CALL. Cstage handles the collision correctly; wwstage routes through the same-module function's return type and the byte-id checks (993_ww_ww + 995_self_rebuild) trip. Filed as task #34 with minimal repro; will land strings.slice when the fnretlookup graduate-by-module sub-bug in #34 is fixed. Top-of-file divergence note lists slice in the deferred set; the landing site keeps a comment-only stub. The public strings c3 surface ships 4-of-5 in this commit; slice + #34 land together in a follow-up. Tests: 5 new @test fns in stringstest.ww (signalled 22-26): iter_prev_at_start_cases (prev at offs=0 → done), iter_prev_ascii_cases (round-trip on forward iter), iter_full_cases (mirror of ref/hare/strings/iter.ha:84-108 — iter "こんにちは" with mid-walk iterstr + prev + next, then s = riter(...) sret-into-existing-slot for the reverse iterator pass), iter_position_cases (position tracks offs through a multibyte walk), iter_iterstr_reverse_cases (riter iterstr is bytes BEFORE the cursor, dual to forward iter's bytes-AFTER). The Hare @test fn iter body uses `s = riter("にちは")` mid-test to swap the iterator's direction (ref/hare/strings/iter.ha:101); ww's sret-into-existing-slot path handles that fine (probed pre-port). struct-copy let-from-ident (task #32) is sidestepped because no test creates a duplicate iterator via `let dup = it;`. 117/117 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id). 993_ww_ww + 994_w6c_ww also green (cstage/wwstage byte-identical on every corpus input including selfhost/cmd/wwdump/main.combined.ww).
383 lines
12 KiB
Plaintext
383 lines
12 KiB
Plaintext
// 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;
|
|
};
|