From dd27ce3339723c10c4c0e296000464604bea4f8f Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 19 May 2026 23:11:01 +0900 Subject: [PATCH] lib/strings+test: re-port index str-arm to dual-iterator rune walk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lib/strings/strings.ww | 68 ++++++++++++++++++++-------- lib/strings/stringstest.ww | 50 ++++++++++++++++++++ selfhost/cmd/w6c/main.combined.ww | 68 ++++++++++++++++++++-------- selfhost/cmd/wwdump/main.combined.ww | 68 ++++++++++++++++++++-------- selfhost/test/smoke.combined.ww | 68 ++++++++++++++++++++-------- 5 files changed, 246 insertions(+), 76 deletions(-) diff --git a/lib/strings/strings.ww b/lib/strings/strings.ww index 1ddec4bb..74727070 100644 --- a/lib/strings/strings.ww +++ b/lib/strings/strings.ww @@ -269,29 +269,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; diff --git a/lib/strings/stringstest.ww b/lib/strings/stringstest.ww index 4b672aa5..54a58110 100644 --- a/lib/strings/stringstest.ww +++ b/lib/strings/stringstest.ww @@ -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 ----------------------------------------------------------- diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index c46cc84a..b01a81ff 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -2106,29 +2106,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; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index f8a0ea17..250e98cd 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -2106,29 +2106,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; diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index 4f3b364f..d4cb7ed0 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -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;