diff --git a/lib/strings/strings.ww b/lib/strings/strings.ww index 72dcf611..e66adeaa 100644 --- a/lib/strings/strings.ww +++ b/lib/strings/strings.ww @@ -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). diff --git a/lib/strings/stringstest.ww b/lib/strings/stringstest.ww index f0bb553a..bf1e2d1d 100644 --- a/lib/strings/stringstest.ww +++ b/lib/strings/stringstest.ww @@ -230,6 +230,131 @@ fn streq(a: str, b: str) bool = { }; }; +// ---- index ------------------------------------------------------------ +// ref/hare/strings/index.ha:108. Rune-wise offset, NOT byte-wise — the +// multibyte rows pin that distinction (Hare doc at index.ha:7). + +@test fn index_cases() void = { + // str-arm: ASCII haystack/needle, mid-string match. + signalled = 1400; + match (strings.index("hello", "ll")) { + case let i: i32 => { if (i != 2) { fail(); }; }; + case void => { fail(); }; + }; + // str-arm: absent needle. + signalled = 1401; + match (strings.index("hello", "world")) { + case let i: i32 => { fail(); }; + case void => void; + }; + // Hare vectors at ref/hare/strings/index.ha:113-119. + signalled = 1402; + match (strings.index("hello world!", "hello")) { + case let i: i32 => { if (i != 0) { fail(); }; }; + case void => { fail(); }; + }; + signalled = 1403; + match (strings.index("hello world!", "world")) { + case let i: i32 => { if (i != 6) { fail(); }; }; + case void => { fail(); }; + }; + signalled = 1404; + match (strings.index("hello world!", "orld!")) { + case let i: i32 => { if (i != 7) { fail(); }; }; + case void => { fail(); }; + }; + // Multibyte haystack + str needle: "ちは" at rune index 3 + // in "こんにちは" (byteindex returns 9, rune-index is 3). + signalled = 1405; + match (strings.index("こんにちは", "ちは")) { + case let i: i32 => { if (i != 3) { fail(); }; }; + case void => { fail(); }; + }; + // rune-arm. + signalled = 1406; + match (strings.index("hello", 'l')) { + case let i: i32 => { if (i != 2) { fail(); }; }; + case void => { fail(); }; + }; + signalled = 1407; + match (strings.index("hello world", 'w')) { + case let i: i32 => { if (i != 6) { fail(); }; }; + case void => { fail(); }; + }; + // Multibyte rune: 'é' U+00E9 at rune 1 in "héllo" — pins + // rune-index vs byte-index (byteindex returns 1; rune-index is + // 1 also — but the str-arm's 1405 row covers the distinction). + signalled = 1408; + match (strings.index("héllo", 0xE9u32: rune)) { + case let i: i32 => { if (i != 1) { fail(); }; }; + case void => { fail(); }; + }; + // Multibyte rune in multibyte haystack: 'ち' U+3061 at rune 3 + // in "こんにちは" (byteindex returns 9, rune-index is 3). + signalled = 1409; + match (strings.index("こんにちは", 0x3061u32: rune)) { + case let i: i32 => { if (i != 3) { fail(); }; }; + case void => { fail(); }; + }; + signalled = 1410; + match (strings.index("こんにちは", 'q')) { + case let i: i32 => { fail(); }; + case void => void; + }; +}; + +// ---- rindex ----------------------------------------------------------- +// ref/hare/strings/index.ha:22. Symmetric: last-occurrence rune index. + +@test fn rindex_cases() void = { + // str-arm. + signalled = 1500; + match (strings.rindex("hello", "lo")) { + case let i: i32 => { if (i != 3) { fail(); }; }; + case void => { fail(); }; + }; + // Hare vector at ref/hare/strings/index.ha:122. + signalled = 1501; + match (strings.rindex("hello world!", "o")) { + case let i: i32 => { if (i != 7) { fail(); }; }; + case void => { fail(); }; + }; + signalled = 1502; + match (strings.rindex("hello", "world")) { + case let i: i32 => { fail(); }; + case void => void; + }; + // Multibyte: last "た" in "またあったね" — rbyteindex returns + // 12, rune-index is 4 (ま=0 た=1 あ=2 っ=3 た=4 ね=5). + signalled = 1503; + match (strings.rindex("またあったね", "た")) { + case let i: i32 => { if (i != 4) { fail(); }; }; + case void => { fail(); }; + }; + // rune-arm. + signalled = 1504; + match (strings.rindex("hello", 'l')) { + case let i: i32 => { if (i != 3) { fail(); }; }; + case void => { fail(); }; + }; + signalled = 1505; + match (strings.rindex("aaaaa", 'a')) { + case let i: i32 => { if (i != 4) { fail(); }; }; + case void => { fail(); }; + }; + // Multibyte rune: 'に' U+306B at rune 2 in "こんにちは". + signalled = 1506; + match (strings.rindex("こんにちは", 0x306Bu32: rune)) { + case let i: i32 => { if (i != 2) { fail(); }; }; + case void => { fail(); }; + }; + signalled = 1507; + match (strings.rindex("hello", 'z')) { + case let i: i32 => { fail(); }; + case void => void; + }; +}; + // ---- trimprefix / trimsuffix ------------------------------------------ // ref/hare/strings/trim.ha:99-107. @@ -709,6 +834,8 @@ export fn main() i32 = { signalled = 6; byteindex_str_cases(); signalled = 7; byteindex_rune_cases(); signalled = 8; rbyteindex_cases(); + signalled = 28; index_cases(); + signalled = 29; rindex_cases(); signalled = 9; trimprefix_cases(); signalled = 10; trimsuffix_cases(); signalled = 11; ltrim_cases(); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 3c9c03b0..b2d2c275 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -1650,6 +1650,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). diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index f1b38fe3..dece3f48 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -1650,6 +1650,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). diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index efa16c24..ed442b72 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -1541,6 +1541,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).