lib/strings+test: add index/rindex Hare sum-type dispatch (#11)

index(haystack: str, needle: (str|rune)) (i32|void) per
ref/hare/strings/index.ha:10. rindex symmetric per :22.

Returns RUNE-index (not byte-index) per Hare contract. str-arm reuses
byteindex/rbyteindex for the anchor byte offset, then walks iter
forward counting runes until position(&it) >= bo. rune-arm forward-
iterates with next(), counts rune positions.

rindex_rune divergence from ref/hare/strings/index.ha:45: Hare's
rindex_rune walks i = len(s) - 1 by 1 per step (byte-len-1 minus
decrement count) — neither pure-byte nor pure-rune for multibyte
input, contradicts its own docstring's rune-wise claim. ww honors
the docstring contract: forward iter + last-match-index. Cited
inline at strings.ww.

Unblocked by #13 (d2c64bc) — pre-#13 the test rows
"strings.index" + "bytes.index" would collide on
*.index_match_next_1 labels in combined.s.

index_cases + rindex_cases per-row inline-match (sister convention of
byteindex_str/rune_cases at stringstest.ww:147-231; sum-type-needle
single-struct table awkward). Bisect via signalled = 1400+i / 1500+i.
Rune-vs-byte pin rows: "こんにちは"+"ちは" → 3 (byte 9),
"またあったね"+"た" rindex → 4 (byte 12). Void miss + 4-byte rune
multibyte coverage.

make test 125/125; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.
This commit is contained in:
2026-05-19 04:12:42 +09:00
parent 5ed6293330
commit 77e62e5ed2
5 changed files with 467 additions and 0 deletions

View File

@@ -208,6 +208,91 @@ export fn rbyteindex(haystack: str, needle: (str | rune)) (i32 | void) = {
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 `needle` occurs in `haystack`.
// ref/hare/strings/contains.ha:9 (subset: Hare's variadic form
// `(needles: (str | rune)...)` blocks on task #16).