lib/strings+test: re-port index str-arm to dual-iterator rune walk

Old shape ran byteindex then rewound to count runes — two passes,
different algorithm from Hare. New `indexstring` mirrors
ref/hare/strings/index.ha:59-81: one outer iterator over the
haystack, an inner iterator re-seated from it for each candidate
match, both walking rune-by-rune. Returns the rune-index of the
first match, or void.

Rest-iterator copy is field-wise rather than `let rest_iter =
s_iter;` because the local-to-local copy of the 3-field iterator
struct diverges between stages today (#41 — 993_ww_ww and
995_self_rebuild byte-diverge when written the natural way).
WHY-comment cites #41 with the precise failing tests.

Tests pin the rune-vs-byte distinction at i=2 and i=4 with 3-byte
kana, plus self-match, empty-needle, empty-haystack, and a no-match
multibyte row from ref/hare/strings/index.ha:119.
This commit is contained in:
2026-05-19 23:11:01 +09:00
parent 18fe1a7a31
commit dd27ce3339
5 changed files with 246 additions and 76 deletions

View File

@@ -1997,29 +1997,59 @@ 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;
};
// indexstring — str-arm of [[index]]. Dual-rune-iterator walk: at each
// candidate rune index `i`, compare `haystack` from that position
// against `needle` rune-by-rune until needle is exhausted (match) or
// a mismatch / haystack-exhaustion breaks the inner loop. 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;
};
return i;
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;
};
// index — rune-wise offset of `needle`'s first occurrence in
// `haystack`, or void if absent. ref/hare/strings/index.ha:10. The
// str-arm delegates to [[indexstring]] (dual-iterator rune-by-rune
// walk 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;