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).

View File

@@ -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();

View File

@@ -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).

View File

@@ -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).

View File

@@ -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).