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

@@ -371,6 +371,56 @@ fn streq(a: str, b: str) bool = {
case let i: i32 => { fail(); };
case void => void;
};
// str-arm: rune-index ≠ byte-index again, mid-string match.
// "あった" starts at rune 2 (byte 6) in "またあったね"
// (each kana is 3 bytes; ref/hare/strings/index.ha:60). Pins
// the dual-iterator walk against the discarded byteindex-and-walk
// shape (#10).
signalled = 1411;
match (strings.index("またあったね", "あった")) {
case let i: i32 => { if (i != 2) { fail(); }; };
case void => { fail(); };
};
// str-arm: tail-anchored multibyte needle. "は" is at rune 4
// (byte 12) in "こんにちは".
signalled = 1412;
match (strings.index("こんにちは", "は")) {
case let i: i32 => { if (i != 4) { fail(); }; };
case void => { fail(); };
};
// Empty needle hits at rune 0 — Hare's `index_string` falls into
// the `needle_rune is done` branch on the very first inner step
// (ref/hare/strings/index.ha:70).
signalled = 1413;
match (strings.index("hello", "")) {
case let i: i32 => { if (i != 0) { fail(); }; };
case void => { fail(); };
};
signalled = 1414;
match (strings.index("", "")) {
case let i: i32 => { if (i != 0) { fail(); }; };
case void => { fail(); };
};
// Empty haystack, non-empty needle — absent.
signalled = 1415;
match (strings.index("", "x")) {
case let i: i32 => { fail(); };
case void => void;
};
// Multibyte haystack, multibyte absent needle — exercises the
// inner-loop mismatch-break across runes (#10, Hare row
// ref/hare/strings/index.ha:119).
signalled = 1416;
match (strings.index("こんにちは", "きょうは")) {
case let i: i32 => { fail(); };
case void => void;
};
// Self-match: haystack == needle, Hare row index.ha:113.
signalled = 1417;
match (strings.index("hello", "hello")) {
case let i: i32 => { if (i != 0) { fail(); }; };
case void => { fail(); };
};
};
// ---- rindex -----------------------------------------------------------