// stringstest — exercises lib/strings. Run with // `out/bin/ww run lib/strings/stringstest.ww`. // A failing row aborts via the assert/abort builtin (task #5 @test // conversion). Per-row exit-code pinpoint is intentionally dropped: an // abort reports the file, not the row (drew-t2-conversion-spec sec.5; // Hare-equivalent, which reports file:line not loop index). // // Vectors mirror ref/hare/strings/{dup,concat,trim,contains,index, // suffix,compare}.ha where ww can express them. // `_test` suffix: Go external-test-package idiom (sanctioned Go-over-Hare // departure for lib/ tests) — self-import is hard-rejected, so the test // lives in package strings_test and pulls the real package via import. // See CLAUDE.md rule-5/9 carve-out; task #16. package strings_test; import strings; import encoding.utf8; import os; fn streq(a: str, b: str) bool = { if (a.len != b.len) { return false; }; let i: i32 = 0; for (i < a.len) { if (a[i] != b[i]) { return false; }; i += 1; }; return true; }; // ---- dup -------------------------------------------------------------- // ref/hare/strings/dup.ha:45. @test fn dup_cases() void = { let e: str = strings.dup(""); assert(!(!streq(e, ""))); assert(!(e.len != 0)); let h: str = strings.dup("hello"); assert(!(!streq(h, "hello"))); defer os.free(h.ptr: *void, h.len: u64); // multi-byte UTF-8: dup must copy raw bytes, not aliased view. let m: str = strings.dup("こんにちは"); assert(!(m.len != 15)); assert(!(!streq(m, "こんにちは"))); assert(!(m.ptr == "こんにちは".ptr)); // fresh alloc defer os.free(m.ptr: *void, m.len: u64); }; // ---- dupall ----------------------------------------------------------- // ref/hare/strings/dup.ha:55 (#6). Element reads go through // `&toks.ptr[i]: *str` // per the splitn cases (16B element copy gap, cgen.c:6515). @test fn dupall_cases() void = { // Empty input — empty result, mirrors Hare's `payload = []`. let empty: []str; empty.ptr = nil: *str; empty.len = 0; empty.cap = 0; match (strings.dupall(empty)) { case let r: []str => { assert(!(r.len != 0)); strings.freeall(r); }; case nomem => { abort(); }; }; // Two-element ASCII — each output element is a fresh allocation // independent of the input (ptr differs from the borrowed source). let in2: [2]str; in2[0] = "hello"; in2[1] = "world"; let src2: []str; src2.ptr = &in2[0]; src2.len = 2; src2.cap = 2; match (strings.dupall(src2)) { case let r: []str => { assert(!(r.len != 2)); expect_str(r, 0, "hello"); expect_str(r, 1, "world"); let p0: *str = &r.ptr[0]; assert(!(p0.ptr == in2[0].ptr)); let p1: *str = &r.ptr[1]; assert(!(p1.ptr == in2[1].ptr)); strings.freeall(r); }; case nomem => { abort(); }; }; // Singleton — `only` rune-equivalent of Hare's `["only"]`. let in1: [1]str; in1[0] = "only"; let src1: []str; src1.ptr = &in1[0]; src1.len = 1; src1.cap = 1; match (strings.dupall(src1)) { case let r: []str => { assert(!(r.len != 1)); expect_str(r, 0, "only"); strings.freeall(r); }; case nomem => { abort(); }; }; // Multibyte — UTF-8 bytes (5-byte and 15-byte) round-trip. let inm: [2]str; inm[0] = "héllo"; inm[1] = "こんにちは"; let srcm: []str; srcm.ptr = &inm[0]; srcm.len = 2; srcm.cap = 2; match (strings.dupall(srcm)) { case let r: []str => { assert(!(r.len != 2)); expect_str(r, 0, "héllo"); expect_str(r, 1, "こんにちは"); let p0: *str = &r.ptr[0]; assert(!(p0.len != 6)); // é is 2 bytes assert(!(p0.ptr == inm[0].ptr)); let p1: *str = &r.ptr[1]; assert(!(p1.len != 15)); // each kana is 3 bytes assert(!(p1.ptr == inm[1].ptr)); strings.freeall(r); }; case nomem => { abort(); }; }; }; // ---- concat ----------------------------------------------------------- // ref/hare/strings/concat.ha:18. Rows mirror Hare's vectors (0/1/2/3-arg, // empty-mid, 2-empty) plus empty-first / empty-last / multibyte. @test fn concat_cases() void = { let pool: [17]str; pool[0] = "hello"; pool[1] = "hello "; pool[2] = "world"; pool[3] = "hello"; pool[4] = " "; pool[5] = "world"; pool[6] = "hello"; pool[7] = ""; pool[8] = "world"; pool[9] = ""; pool[10] = ""; pool[11] = ""; pool[12] = "world"; pool[13] = "hello"; pool[14] = ""; pool[15] = "こん"; pool[16] = "にちは"; let argo: [9]i32; let argn: [9]i32; let want: [9]str; let labels: [9]str; argo[0]=0; argn[0]=0; want[0]=""; labels[0]="0-arg"; argo[1]=0; argn[1]=1; want[1]="hello"; labels[1]="1-arg"; argo[2]=1; argn[2]=2; want[2]="hello world"; labels[2]="2-arg"; argo[3]=3; argn[3]=3; want[3]="hello world"; labels[3]="3-arg"; argo[4]=6; argn[4]=3; want[4]="helloworld"; labels[4]="empty-mid"; argo[5]=9; argn[5]=2; want[5]=""; labels[5]="2-empty"; argo[6]=11; argn[6]=2; want[6]="world"; labels[6]="empty-first"; argo[7]=13; argn[7]=2; want[7]="hello"; labels[7]="empty-last"; argo[8]=15; argn[8]=2; want[8]="こんにちは"; labels[8]="multibyte"; let i: i32 = 0; for (i < 9) { let argv: []str; argv.ptr = &pool[argo[i]]; argv.len = argn[i]; argv.cap = argn[i]; let got: str = strings.concat(argv...); assert(!(!streq(got, want[i]))); if (got.len > 0) { os.free(got.ptr: *void, got.len: u64); }; i += 1; }; }; // ---- join ------------------------------------------------------------- // ref/hare/strings/concat.ha:64. Rows mirror Hare's @test fn join // (0-arg, 1-arg, empty-sep, 3-arg.) plus 2-arg, all-empties, long sep, // multibyte, empty-mid (delim still inserted around the empty slot). @test fn join_cases() void = { let pool: [19]str; pool[0] = "hello"; pool[1] = "a"; pool[2] = "b"; pool[3] = "a"; pool[4] = "b"; pool[5] = "c"; pool[6] = "a"; pool[7] = "b"; pool[8] = "c"; pool[9] = ""; pool[10] = ""; pool[11] = "a"; pool[12] = "b"; pool[13] = "c"; pool[14] = "こん"; pool[15] = "にちは"; pool[16] = "a"; pool[17] = ""; pool[18] = "b"; let argo: [9]i32; let argn: [9]i32; let seps: [9]str; let want: [9]str; argo[0]=0; argn[0]=0; seps[0]="."; want[0]=""; argo[1]=0; argn[1]=1; seps[1]="."; want[1]="hello"; argo[2]=1; argn[2]=2; seps[2]=", "; want[2]="a, b"; argo[3]=3; argn[3]=3; seps[3]="."; want[3]="a.b.c"; argo[4]=6; argn[4]=3; seps[4]=""; want[4]="abc"; argo[5]=9; argn[5]=2; seps[5]=","; want[5]=","; argo[6]=11; argn[6]=3; seps[6]=" :: "; want[6]="a :: b :: c"; argo[7]=14; argn[7]=2; seps[7]="・"; want[7]="こん・にちは"; argo[8]=16; argn[8]=3; seps[8]="-"; want[8]="a--b"; let i: i32 = 0; for (i < 9) { let argv: []str; argv.ptr = &pool[argo[i]]; argv.len = argn[i]; argv.cap = argn[i]; let got: str = strings.join(seps[i], argv...); assert(!(!streq(got, want[i]))); if (got.len > 0) { os.free(got.ptr: *void, got.len: u64); }; i += 1; }; }; // ---- hasprefix -------------------------------------------------------- // ref/hare/strings/suffix.ha:18. @test fn hasprefix_cases() void = { assert(!(!strings.hasprefix("hello world", "hello"))); assert(!(!strings.hasprefix("hello world", 'h'))); assert(!( strings.hasprefix("hello world", "world"))); assert(!( strings.hasprefix("hello world", 'q'))); assert(!(!strings.hasprefix("hello", "hello"))); // equal-len assert(!(!strings.hasprefix("anything", ""))); // empty prefix assert(!( strings.hasprefix("", "x"))); // multibyte rune prefix — '\'é\'' literal blocked by single-byte // lexrune (lib/ww/lex/lex.ww:659); pass codepoint directly. assert(!(!strings.hasprefix("éclat", 0xE9u32: rune))); assert(!(!strings.hasprefix("🦀rust", 0x1F980u32: rune))); }; // ---- hassuffix -------------------------------------------------------- // ref/hare/strings/suffix.ha:36. @test fn hassuffix_cases() void = { assert(!(!strings.hassuffix("hello world", "world"))); assert(!(!strings.hassuffix("hello world", 'd'))); assert(!( strings.hassuffix("hello world", "hello"))); assert(!( strings.hassuffix("hello world", 'h'))); assert(!(!strings.hassuffix("café", 0xE9u32: rune))); // multibyte }; // ---- contains --------------------------------------------------------- // ref/hare/strings/contains.ha:27. @test fn contains_cases() void = { assert(!(!strings.contains("hello world", "hello"))); assert(!(!strings.contains("hello world", 'h'))); assert(!( strings.contains("hello world", 'x'))); assert(!(!strings.contains("hello world", "world"))); assert(!(!strings.contains("hello world", ""))); // empty hits at 0 assert(!( strings.contains("hello world", "foobar"))); assert(!(!strings.contains("こんにちは", 0x306Bu32: rune))); // 'に' assert(!(!strings.contains("こんにちは", "ちは"))); // Variadic rows. ref/hare/strings/contains.ha:27. assert(!( strings.contains("hello"))); assert(!(!strings.contains("hello world", "world"))); assert(!(!strings.contains("hello", 'l'))); assert(!(!strings.contains("hello world", "foo", "world", 'x'))); assert(!( strings.contains("hello", "foo", 'z', "bar"))); assert(!(!strings.contains("héllo", "x", 0xE9u32: rune))); }; // ---- byteindex -------------------------------------------------------- // ref/hare/strings/index.ha:147 (byteindex tests, both arms). @test fn byteindex_str_cases() void = { match (strings.byteindex("hello", "hello")) { case let i: i32 => { assert(!(i != 0)); }; case void => { abort(); }; }; match (strings.byteindex("hello world!", "world")) { case let i: i32 => { assert(!(i != 6)); }; case void => { abort(); }; }; match (strings.byteindex("hello world!", "orld!")) { case let i: i32 => { assert(!(i != 7)); }; case void => { abort(); }; }; match (strings.byteindex("hello world!", "word")) { case let i: i32 => { abort(); }; case void => void; }; // empty needle hits at 0 (ref/hare/bytes/index.ha:63). match (strings.byteindex("hello", "")) { case let i: i32 => { assert(!(i != 0)); }; case void => { abort(); }; }; // empty haystack, non-empty needle — absent. match (strings.byteindex("", "x")) { case let i: i32 => { abort(); }; case void => void; }; // multibyte substring in multibyte haystack. match (strings.byteindex("こんにちは", "ちは")) { case let i: i32 => { assert(!(i != 9)); }; case void => { abort(); }; }; }; @test fn byteindex_rune_cases() void = { // ASCII rune (1-byte encoding). match (strings.byteindex("hello world", 'w')) { case let i: i32 => { assert(!(i != 6)); }; case void => { abort(); }; }; // 2-byte rune U+00E9 'é' inside "café". match (strings.byteindex("café", 0xE9u32: rune)) { case let i: i32 => { assert(!(i != 3)); }; case void => { abort(); }; }; // 3-byte rune U+3061 'ち' inside "こんにちは". match (strings.byteindex("こんにちは", 0x3061u32: rune)) { case let i: i32 => { assert(!(i != 9)); }; case void => { abort(); }; }; // 4-byte rune U+1F980 '🦀' inside "ab🦀cd". match (strings.byteindex("ab🦀cd", 0x1F980u32: rune)) { case let i: i32 => { assert(!(i != 2)); }; case void => { abort(); }; }; // absent. match (strings.byteindex("こんにちは", 'q')) { case let i: i32 => { abort(); }; case void => void; }; }; // ---- rbyteindex ------------------------------------------------------- @test fn rbyteindex_cases() void = { // Two 'た' in "またあったね" — ref/hare/strings/index.ha:160-161. match (strings.byteindex("またあったね", "た")) { case let i: i32 => { assert(!(i != 3)); }; case void => { abort(); }; }; match (strings.rbyteindex("またあったね", "た")) { case let i: i32 => { assert(!(i != 12)); }; case void => { abort(); }; }; // Rune arm, multi-byte 'に' U+306B. match (strings.rbyteindex("こんにちは", 0x306Bu32: rune)) { case let i: i32 => { assert(!(i != 6)); }; case void => { abort(); }; }; // Absent. match (strings.rbyteindex("abc", 'z')) { case let i: i32 => { abort(); }; case void => void; }; }; // ---- 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. match (strings.index("hello", "ll")) { case let i: i32 => { assert(!(i != 2)); }; case void => { abort(); }; }; // str-arm: absent needle. match (strings.index("hello", "world")) { case let i: i32 => { abort(); }; case void => void; }; // Hare vectors at ref/hare/strings/index.ha:113-119. match (strings.index("hello world!", "hello")) { case let i: i32 => { assert(!(i != 0)); }; case void => { abort(); }; }; match (strings.index("hello world!", "world")) { case let i: i32 => { assert(!(i != 6)); }; case void => { abort(); }; }; match (strings.index("hello world!", "orld!")) { case let i: i32 => { assert(!(i != 7)); }; case void => { abort(); }; }; // Multibyte haystack + str needle: "ちは" at rune index 3 // in "こんにちは" (byteindex returns 9, rune-index is 3). match (strings.index("こんにちは", "ちは")) { case let i: i32 => { assert(!(i != 3)); }; case void => { abort(); }; }; // rune-arm. match (strings.index("hello", 'l')) { case let i: i32 => { assert(!(i != 2)); }; case void => { abort(); }; }; match (strings.index("hello world", 'w')) { case let i: i32 => { assert(!(i != 6)); }; case void => { abort(); }; }; // 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). match (strings.index("héllo", 0xE9u32: rune)) { case let i: i32 => { assert(!(i != 1)); }; case void => { abort(); }; }; // Multibyte rune in multibyte haystack: 'ち' U+3061 at rune 3 // in "こんにちは" (byteindex returns 9, rune-index is 3). match (strings.index("こんにちは", 0x3061u32: rune)) { case let i: i32 => { assert(!(i != 3)); }; case void => { abort(); }; }; match (strings.index("こんにちは", 'q')) { case let i: i32 => { abort(); }; 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). match (strings.index("またあったね", "あった")) { case let i: i32 => { assert(!(i != 2)); }; case void => { abort(); }; }; // str-arm: tail-anchored multibyte needle. "は" is at rune 4 // (byte 12) in "こんにちは". match (strings.index("こんにちは", "は")) { case let i: i32 => { assert(!(i != 4)); }; case void => { abort(); }; }; // 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). match (strings.index("hello", "")) { case let i: i32 => { assert(!(i != 0)); }; case void => { abort(); }; }; match (strings.index("", "")) { case let i: i32 => { assert(!(i != 0)); }; case void => { abort(); }; }; // Empty haystack, non-empty needle — absent. match (strings.index("", "x")) { case let i: i32 => { abort(); }; 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). match (strings.index("こんにちは", "きょうは")) { case let i: i32 => { abort(); }; case void => void; }; // Self-match: haystack == needle, Hare row index.ha:113. match (strings.index("hello", "hello")) { case let i: i32 => { assert(!(i != 0)); }; case void => { abort(); }; }; }; // ---- rindex ----------------------------------------------------------- // ref/hare/strings/index.ha:22. Symmetric: last-occurrence rune index. @test fn rindex_cases() void = { // str-arm. match (strings.rindex("hello", "lo")) { case let i: i32 => { assert(!(i != 3)); }; case void => { abort(); }; }; // Hare vector at ref/hare/strings/index.ha:122. match (strings.rindex("hello world!", "o")) { case let i: i32 => { assert(!(i != 7)); }; case void => { abort(); }; }; match (strings.rindex("hello", "world")) { case let i: i32 => { abort(); }; case void => void; }; // Multibyte: last "た" in "またあったね" — rbyteindex returns // 12, rune-index is 4 (ま=0 た=1 あ=2 っ=3 た=4 ね=5). match (strings.rindex("またあったね", "た")) { case let i: i32 => { assert(!(i != 4)); }; case void => { abort(); }; }; // rune-arm. match (strings.rindex("hello", 'l')) { case let i: i32 => { assert(!(i != 3)); }; case void => { abort(); }; }; match (strings.rindex("aaaaa", 'a')) { case let i: i32 => { assert(!(i != 4)); }; case void => { abort(); }; }; // Multibyte rune: 'に' U+306B at rune 2 in "こんにちは". match (strings.rindex("こんにちは", 0x306Bu32: rune)) { case let i: i32 => { assert(!(i != 2)); }; case void => { abort(); }; }; match (strings.rindex("hello", 'z')) { case let i: i32 => { abort(); }; case void => void; }; }; // ---- trimprefix / trimsuffix ------------------------------------------ // ref/hare/strings/trim.ha:99-107. @test fn trimprefix_cases() void = { assert(!(!streq(strings.trimprefix("", ""), ""))); assert(!(!streq(strings.trimprefix("", "blablabla"), ""))); assert(!(!streq(strings.trimprefix("hello, world", "hello"), ", world"))); assert(!(!streq(strings.trimprefix("blablabla", "bla"), "blabla"))); // equal-length match strips to empty. assert(!(!streq(strings.trimprefix("hello", "hello"), ""))); }; @test fn trimsuffix_cases() void = { assert(!(!streq(strings.trimsuffix("", ""), ""))); assert(!(!streq(strings.trimsuffix("", "blablabla"), ""))); assert(!(!streq(strings.trimsuffix("hello, world", "world"), "hello, "))); assert(!(!streq(strings.trimsuffix("blablabla", "bla"), "blabla"))); assert(!(!streq(strings.trimsuffix("hello", "hello"), ""))); }; // ---- ltrim / rtrim / trim --------------------------------------------- // ref/hare/strings/trim.ha:75-97. 0-arg rows (#9) pin the ASCII // whitespace set (' ', '\t', '\n', '\r' — ref/hare/strings/trim.ha:6). @test fn ltrim_cases() void = { let runes: [9]rune; runes[0] = 'x'; runes[1] = 'a'; runes[2] = 0x1D68Au32: rune; runes[3] = '('; runes[4] = ')'; runes[5] = 'a'; runes[6] = 'b'; runes[7] = 'c'; runes[8] = 'd'; // 0-arg rows (8..10) strip ASCII whitespace per Hare (#9): // only the leading side is stripped for ltrim. let inputs: [11]str; let argo: [11]i32; let argn: [11]i32; let want: [11]str; inputs[0]=""; argo[0]=0; argn[0]=1; want[0]=""; inputs[1]="aaabc"; argo[1]=1; argn[1]=1; want[1]="bc"; inputs[2]="xyz"; argo[2]=1; argn[2]=1; want[2]="xyz"; inputs[3]="aaaa"; argo[3]=1; argn[3]=1; want[3]=""; inputs[4]="𝚊𝚊hi"; argo[4]=2; argn[4]=1; want[4]="hi"; inputs[5]="((()(())))())"; argo[5]=3; argn[5]=2; want[5]=""; inputs[6]="abacadabra"; argo[6]=5; argn[6]=4; want[6]="ra"; inputs[7]="hello"; argo[7]=0; argn[7]=0; want[7]="hello"; inputs[8]=" hello "; argo[8]=0; argn[8]=0; want[8]="hello "; inputs[9]="\t\r\n hello"; argo[9]=0; argn[9]=0; want[9]="hello"; inputs[10]=" "; argo[10]=0; argn[10]=0; want[10]=""; let i: i32 = 0; for (i < 11) { let argv: []rune; argv.ptr = &runes[argo[i]]; argv.len = argn[i]; argv.cap = argn[i]; let got: str = strings.ltrim(inputs[i], argv...); assert(!(!streq(got, want[i]))); i += 1; }; }; @test fn rtrim_cases() void = { let runes: [19]rune; runes[0] = 'x'; runes[1] = 'a'; runes[2] = 0x1D68Au32: rune; runes[3] = 'w'; runes[4] = 'd'; runes[5] = 'o'; runes[6] = 'r'; runes[7] = ' '; runes[8] = 's'; runes[9] = 'i'; runes[10] = 'l'; runes[11] = 'z'; runes[12] = 't'; runes[13] = 'm'; runes[14] = 'n'; runes[15] = 'o'; runes[16] = 'e'; runes[17] = 'a'; runes[18] = 'd'; // 0-arg rows (8..10) strip ASCII whitespace per Hare (#9): // only the trailing side is stripped for rtrim. let inputs: [11]str; let argo: [11]i32; let argn: [11]i32; let want: [11]str; inputs[0]=""; argo[0]=0; argn[0]=1; want[0]=""; inputs[1]="bcaaa"; argo[1]=1; argn[1]=1; want[1]="bc"; inputs[2]="xyz"; argo[2]=1; argn[2]=1; want[2]="xyz"; inputs[3]="aaaa"; argo[3]=1; argn[3]=1; want[3]=""; inputs[4]="hi𝚊𝚊"; argo[4]=2; argn[4]=1; want[4]="hi"; inputs[5]="yellowwooddoor"; argo[5]=3; argn[5]=4; want[5]="yell"; inputs[6]="Sentimentalized sensationalism sensationalized sentimentalisms"; argo[6]=7; argn[6]=12; want[6]="S"; inputs[7]="hello"; argo[7]=0; argn[7]=0; want[7]="hello"; inputs[8]=" hello "; argo[8]=0; argn[8]=0; want[8]=" hello"; inputs[9]="hello, world\r\n\r\n"; argo[9]=0; argn[9]=0; want[9]="hello, world"; inputs[10]=" "; argo[10]=0; argn[10]=0; want[10]=""; let i: i32 = 0; for (i < 11) { let argv: []rune; argv.ptr = &runes[argo[i]]; argv.len = argn[i]; argv.cap = argn[i]; let got: str = strings.rtrim(inputs[i], argv...); assert(!(!streq(got, want[i]))); i += 1; }; }; @test fn trim_cases() void = { let runes: [8]rune; runes[0] = 'x'; runes[1] = 'a'; runes[2] = 'm'; runes[3] = 'i'; runes[4] = 'p'; runes[5] = 's'; runes[6] = '['; runes[7] = ']'; // 0-arg rows (7..9) strip ASCII whitespace per Hare (#9) from // both ends. let inputs: [10]str; let argo: [10]i32; let argn: [10]i32; let want: [10]str; inputs[0]=""; argo[0]=0; argn[0]=1; want[0]=""; inputs[1]="aaabcaaa"; argo[1]=1; argn[1]=1; want[1]="bc"; inputs[2]="xyz"; argo[2]=1; argn[2]=1; want[2]="xyz"; inputs[3]="aaaa"; argo[3]=1; argn[3]=1; want[3]=""; inputs[4]="mississippi"; argo[4]=2; argn[4]=4; want[4]=""; inputs[5]="[[][[[]]][][].[[]][]]][]]]"; argo[5]=6; argn[5]=2; want[5]="."; inputs[6]="hello"; argo[6]=0; argn[6]=0; want[6]="hello"; inputs[7]=" hello "; argo[7]=0; argn[7]=0; want[7]="hello"; inputs[8]="\r\thello\n\r"; argo[8]=0; argn[8]=0; want[8]="hello"; inputs[9]=" "; argo[9]=0; argn[9]=0; want[9]=""; let i: i32 = 0; for (i < 10) { let argv: []rune; argv.ptr = &runes[argo[i]]; argv.len = argn[i]; argv.cap = argn[i]; let got: str = strings.trim(inputs[i], argv...); assert(!(!streq(got, want[i]))); i += 1; }; }; // ---- compare ---------------------------------------------------------- // ref/hare/strings/compare.ha:16. @test fn compare_cases() void = { assert(!(strings.compare("ABC", "ABC") != 0)); assert(!(strings.compare("ABC", "AB") <= 0)); assert(!(strings.compare("AB", "ABC") >= 0)); assert(!(strings.compare("BCD", "ABC") <= 0)); assert(!(strings.compare("ABC", "abc") >= 0)); assert(!(strings.compare("ABC", "こんにちは") >= 0)); }; // ---- sub / bytesub ---------------------------------------------------- // ref/hare/strings/sub.ha:44 (@test fn sub), :79 (@test fn bytesub). Hare's // 2-arg `sub(s, start)` rows are omitted: ww has no default-parameter // syntax (filed as #37). bytesub now validates rune boundaries (#7). @test fn sub_cases() void = { assert(!(!streq(strings.sub("a string", 0, 8), "a string"))); assert(!(!streq(strings.sub("a string", 0, 1), "a"))); assert(!(!streq(strings.sub("a string", 0, 3), "a s"))); assert(!(!streq(strings.sub("a string", 2, 8), "string"))); // start == end yields an empty borrowed view. assert(!(!streq(strings.sub("a string", 4, 4), ""))); assert(!(strings.sub("a string", 4, 4).len != 0)); // Hare vector — rune indices 1..3 over "こんにちは" select bytes // 3..9 ("んに"), not bytes 1..3. assert(!(!streq(strings.sub("こんにちは", 1, 3), "んに"))); // 2-byte rune at rune index 1 in "héllo" — byte offsets 1..3. assert(!(!streq(strings.sub("héllo", 1, 2), "é"))); // start == 0, end == rune-len of full string. assert(!(!streq(strings.sub("héllo", 0, 5), "héllo"))); }; // Match-shape mirrors Hare's `bytesub(...)!` at ref/hare/strings/sub.ha: // 80-86. Inlined at each call site (rather than a helper that takes // `(str | utf8.invalid)` by value) because the union-by-value path // crashes — same lift-on-pass-by-value family as #48. @test fn bytesub_cases() void = { match (strings.bytesub("a string", 0, 8)) { case let s: str => { assert(!(!streq(s, "a string"))); }; case let e: utf8.invalid => { abort(); }; }; match (strings.bytesub("a string", 0, 1)) { case let s: str => { assert(!(!streq(s, "a"))); }; case let e: utf8.invalid => { abort(); }; }; match (strings.bytesub("a string", 0, 3)) { case let s: str => { assert(!(!streq(s, "a s"))); }; case let e: utf8.invalid => { abort(); }; }; match (strings.bytesub("a string", 2, 8)) { case let s: str => { assert(!(!streq(s, "string"))); }; case let e: utf8.invalid => { abort(); }; }; match (strings.bytesub("a string", 4, 4)) { case let s: str => { assert(!(!streq(s, ""))); }; case let e: utf8.invalid => { abort(); }; }; // Hare vector — byte indices 3..9 over "こんにちは" select "んに". match (strings.bytesub("こんにちは", 3, 9)) { case let s: str => { assert(!(!streq(s, "んに"))); }; case let e: utf8.invalid => { abort(); }; }; // Rune/byte axis disagree on identical args (#3): sub(s,0,3) walks 3 // runes and yields 9 bytes; bytesub(s,0,3) yields the first 3 bytes // — one 3-byte codepoint. assert(!(!streq(strings.sub("こんにちは", 0, 3), "こんに"))); match (strings.bytesub("こんにちは", 0, 3)) { case let s: str => { assert(!(!streq(s, "こ"))); }; case let e: utf8.invalid => { abort(); }; }; // Borrowed view: ptr aliases input. let s: str = "hello"; match (strings.bytesub(s, 1, 4)) { case let r: str => { assert(!(r.ptr != s.ptr + 1u64)); assert(!(r.len != 3)); }; case let e: utf8.invalid => { abort(); }; }; // Hare's invalid row (ref/hare/strings/sub.ha:87) — start lands on // a continuation byte (2nd byte of "こ"), bytesub must reject (#7). match (strings.bytesub("こんにちは", 1, 3)) { case let r: str => { abort(); }; case let e: utf8.invalid => void; }; // Symmetric: end lands on a continuation byte (2nd byte of "ん"). match (strings.bytesub("こんにちは", 0, 4)) { case let r: str => { abort(); }; case let e: utf8.invalid => void; }; // end == s.len bypasses the continuation check (s[end] is OOB). match (strings.bytesub("こんにちは", 0, 15)) { case let s: str => { assert(!(!streq(s, "こんにちは"))); }; case let e: utf8.invalid => { abort(); }; }; }; // ---- toutf8 / frombytes roundtrip ------------------------------- // ref/hare/strings/utf8.ha:31. Validation-half coverage lives in // lib/encoding/utf8 (utf8test) per CLAUDE.md rule 9 carve-out. @test fn utf8_roundtrip_cases() void = { let s: str = "hello"; let b: []u8 = strings.toutf8(s); assert(!(b.len != 5)); assert(!(b[0] != 104u8)); // 'h' let r: str = strings.frombytes(b); assert(!(!streq(r, "hello"))); assert(!(r.ptr != s.ptr)); // borrowed, not copied }; // ---- iter / next ------------------------------------------------------ // ref/hare/strings/iter.ha:84-108. Hare's @test fn iter() uses prev + // riter heavily; both are deferred (no `utf8.prev`). Rebuild forward- // only here: empty / ASCII / 2-byte / 3-byte / 4-byte / done@EOI / // mixed-width. @test fn iter_empty_cases() void = { let it: strings.iterator = strings.iter(""); match (strings.next(&it)) { case let r: rune => { abort(); }; case utf8.done => void; }; // Repeated next after done stays done. match (strings.next(&it)) { case let r: rune => { abort(); }; case utf8.done => void; }; }; @test fn iter_ascii_cases() void = { let it: strings.iterator = strings.iter("hi!"); match (strings.next(&it)) { case let r: rune => { assert(!(r != 'h')); }; case utf8.done => { abort(); }; }; match (strings.next(&it)) { case let r: rune => { assert(!(r != 'i')); }; case utf8.done => { abort(); }; }; match (strings.next(&it)) { case let r: rune => { assert(!(r != '!')); }; case utf8.done => { abort(); }; }; match (strings.next(&it)) { case let r: rune => { abort(); }; case utf8.done => void; }; }; @test fn iter_twobyte_cases() void = { let it: strings.iterator = strings.iter("café"); let i: i32 = 0; let expect: [4]rune; expect[0] = 'c'; expect[1] = 'a'; expect[2] = 'f'; expect[3] = 0xE9u32: rune; // 'é' U+00E9 for (i < 4) { match (strings.next(&it)) { case let r: rune => { assert(!(r != expect[i])); }; case utf8.done => { abort(); }; }; i += 1; }; match (strings.next(&it)) { case let r: rune => { abort(); }; case utf8.done => void; }; }; @test fn iter_threebyte_cases() void = { let it: strings.iterator = strings.iter("こんにちは"); let i: i32 = 0; let expect: [5]rune; expect[0] = 0x3053u32: rune; // 'こ' expect[1] = 0x3093u32: rune; // 'ん' expect[2] = 0x306Bu32: rune; // 'に' expect[3] = 0x3061u32: rune; // 'ち' expect[4] = 0x306Fu32: rune; // 'は' for (i < 5) { match (strings.next(&it)) { case let r: rune => { assert(!(r != expect[i])); }; case utf8.done => { abort(); }; }; i += 1; }; match (strings.next(&it)) { case let r: rune => { abort(); }; case utf8.done => void; }; }; @test fn iter_fourbyte_cases() void = { let it: strings.iterator = strings.iter("🦀rust"); let i: i32 = 0; let expect: [5]rune; expect[0] = 0x1F980u32: rune; // '🦀' expect[1] = 'r'; expect[2] = 'u'; expect[3] = 's'; expect[4] = 't'; for (i < 5) { match (strings.next(&it)) { case let r: rune => { assert(!(r != expect[i])); }; case utf8.done => { abort(); }; }; i += 1; }; match (strings.next(&it)) { case let r: rune => { abort(); }; case utf8.done => void; }; }; @test fn iter_mixed_cases() void = { // "Hello, 世界! 🌍" — 1+1+1+1+1+1+1+3+3+1+1+4 = 12 runes, // widths 1/3/4 mixed. let it: strings.iterator = strings.iter("Hello, 世界! 🌍"); let i: i32 = 0; let expect: [12]rune; expect[0] = 'H'; expect[1] = 'e'; expect[2] = 'l'; expect[3] = 'l'; expect[4] = 'o'; expect[5] = ','; expect[6] = ' '; expect[7] = 0x4E16u32: rune; // '世' expect[8] = 0x754Cu32: rune; // '界' expect[9] = '!'; expect[10] = ' '; expect[11] = 0x1F30Du32: rune; // '🌍' for (i < 12) { match (strings.next(&it)) { case let r: rune => { assert(!(r != expect[i])); }; case utf8.done => { abort(); }; }; i += 1; }; match (strings.next(&it)) { case let r: rune => { abort(); }; case utf8.done => void; }; }; // ---- prev / riter / iterstr / slice / position ----------------------- // ref/hare/strings/iter.ha:84-127. The Hare @test fn iter body uses // `s = riter(...)` mid-test to swap the iterator's direction; ww's // sret-into-existing-slot path handles that fine (probed pre-port). @test fn iter_prev_at_start_cases() void = { let it: strings.iterator = strings.iter("hi"); match (strings.prev(&it)) { case utf8.done => void; case let r: rune => { abort(); }; }; }; @test fn iter_prev_ascii_cases() void = { let it: strings.iterator = strings.iter("abc"); match (strings.next(&it)) { case let r: rune => { assert(!(r != 'a')); }; case utf8.done => { abort(); }; }; match (strings.prev(&it)) { case let r: rune => { assert(!(r != 'a')); }; case utf8.done => { abort(); }; }; match (strings.prev(&it)) { case utf8.done => void; case let r: rune => { abort(); }; }; }; // Mirror of ref/hare/strings/iter.ha:84-108 — `iter("こんにちは")`, // step+back+iterstr+riter-reassign sequence. @test fn iter_full_cases() void = { let s: strings.iterator = strings.iter("こんにちは"); match (strings.prev(&s)) { case utf8.done => void; case let r: rune => { abort(); }; }; let expect1: [2]rune; expect1[0] = 0x3053u32: rune; // 'こ' expect1[1] = 0x3093u32: rune; // 'ん' let i: i32 = 0; for (i < 2) { match (strings.next(&s)) { case let r: rune => { assert(!(r != expect1[i])); }; case utf8.done => { abort(); }; }; i += 1; }; assert(!(!streq(strings.iterstr(&s), "にちは"))); match (strings.prev(&s)) { case let r: rune => { assert(!(r != 0x3093u32: rune)); }; // 'ん' case utf8.done => { abort(); }; }; let expect2: [4]rune; expect2[0] = 0x3093u32: rune; // 'ん' expect2[1] = 0x306Bu32: rune; // 'に' expect2[2] = 0x3061u32: rune; // 'ち' expect2[3] = 0x306Fu32: rune; // 'は' i = 0; for (i < 4) { match (strings.next(&s)) { case let r: rune => { assert(!(r != expect2[i])); }; case utf8.done => { abort(); }; }; i += 1; }; match (strings.next(&s)) { case utf8.done => void; case let r: rune => { abort(); }; }; // Repeated next-after-done stays done. match (strings.next(&s)) { case utf8.done => void; case let r: rune => { abort(); }; }; match (strings.prev(&s)) { case let r: rune => { assert(!(r != 0x306Fu32: rune)); }; // 'は' case utf8.done => { abort(); }; }; // Swap to a reverse iterator. sret-into-existing-slot. s = strings.riter("にちは"); let expect3: [3]rune; expect3[0] = 0x306Fu32: rune; // 'は' expect3[1] = 0x3061u32: rune; // 'ち' expect3[2] = 0x306Bu32: rune; // 'に' i = 0; for (i < 3) { match (strings.next(&s)) { case let r: rune => { assert(!(r != expect3[i])); }; case utf8.done => { abort(); }; }; i += 1; }; match (strings.next(&s)) { case utf8.done => void; case let r: rune => { abort(); }; }; match (strings.prev(&s)) { case let r: rune => { assert(!(r != 0x306Bu32: rune)); }; // 'に' case utf8.done => { abort(); }; }; }; @test fn iter_position_cases() void = { let it: strings.iterator = strings.iter("café"); // 5 bytes: c-a-f-é(2) assert(!(strings.position(&it) != 0)); match (strings.next(&it)) { case let r: rune => void; case utf8.done => { abort(); }; }; assert(!(strings.position(&it) != 1)); match (strings.next(&it)) { case let r: rune => void; case utf8.done => { abort(); }; }; match (strings.next(&it)) { case let r: rune => void; case utf8.done => { abort(); }; }; assert(!(strings.position(&it) != 3)); match (strings.next(&it)) { case let r: rune => void; case utf8.done => { abort(); }; }; assert(!(strings.position(&it) != 5)); }; // ref/hare/strings/iter.ha:110 @test fn slice. Hare uses `let t = s;` // to copy the iterator; ww re-initialises t from the same source to // stay in scope of #32 (local struct ident rhs already fixed) without // reaching for #35's sibling latents. @test fn iter_slice_cases() void = { let s: strings.iterator = strings.iter("こんにちは"); let t: strings.iterator = strings.iter("こんにちは"); assert(!(strings.slice(&s, &t).len != 0)); assert(!(strings.slice(&t, &s).len != 0)); let i: i32 = 0; for (i < 2) { match (strings.next(&s)) { case let r: rune => void; case utf8.done => { abort(); }; }; match (strings.next(&t)) { case let r: rune => void; case utf8.done => { abort(); }; }; i += 1; }; assert(!(strings.slice(&s, &t).len != 0)); assert(!(strings.slice(&t, &s).len != 0)); i = 0; for (i < 3) { match (strings.next(&t)) { case let r: rune => void; case utf8.done => { abort(); }; }; i += 1; }; assert(!(!streq(strings.slice(&s, &t), "にちは"))); i = 0; for (i < 3) { match (strings.next(&s)) { case let r: rune => void; case utf8.done => { abort(); }; }; i += 1; }; assert(!(strings.slice(&s, &t).len != 0)); assert(!(strings.slice(&t, &s).len != 0)); }; @test fn iter_iterstr_reverse_cases() void = { // Reverse iter: iterstr is `src[0:offs]` — bytes BEFORE the cursor // (the still-to-be-walked region in reverse direction). let rit: strings.iterator = strings.riter("hello"); assert(!(!streq(strings.iterstr(&rit), "hello"))); match (strings.next(&rit)) { case let r: rune => void; case utf8.done => { abort(); }; }; assert(!(!streq(strings.iterstr(&rit), "hell"))); match (strings.next(&rit)) { case let r: rune => void; case utf8.done => { abort(); }; }; match (strings.next(&rit)) { case let r: rune => void; case utf8.done => { abort(); }; }; assert(!(!streq(strings.iterstr(&rit), "he"))); }; // ---- tokenize / rtokenize / peek_token / remaining_tokens ----------- // ref/hare/strings/tokenize.ha:110 @test fn tokenize. Row drivers // mirror lib/bytes/bytestest expect_token / expect_done but compare // str via streq. fn expect_str_token(t: *strings.tokenizer, want: str) void = { match (strings.peek_token(t)) { case let p: str => { assert(!(!streq(p, want))); }; case bytes.done => { abort(); }; }; match (strings.next_token(t)) { case let n: str => { assert(!(!streq(n, want))); }; case bytes.done => { abort(); }; }; }; fn expect_str_done(t: *strings.tokenizer) void = { match (strings.peek_token(t)) { case let p: str => { abort(); }; case bytes.done => void; }; match (strings.next_token(t)) { case let n: str => { abort(); }; case bytes.done => void; }; }; @test fn tokenize_cases() void = { // Hare vector — single-space delim. ref/hare/strings/tokenize.ha:112. let t: strings.tokenizer = strings.tokenize( "Hello world! My name is Harriet.", " "); expect_str_token(&t, "Hello"); expect_str_token(&t, "world!"); expect_str_token(&t, "My"); expect_str_token(&t, "name"); expect_str_token(&t, "is"); expect_str_token(&t, "Harriet."); expect_str_done(&t); // Multi-byte delim set — space + tab. // ref/hare/strings/tokenize.ha:124. let t2: strings.tokenizer = strings.tokenize( "/dev/sda1\t/ ext4 rw,relatime\t0 0", " \t"); expect_str_token(&t2, "/dev/sda1"); expect_str_token(&t2, "/"); expect_str_token(&t2, "ext4"); expect_str_token(&t2, "rw,relatime"); expect_str_token(&t2, "0"); expect_str_token(&t2, "0"); expect_str_done(&t2); // Consecutive delimiters — empty interior tokens. // ref/hare/strings/tokenize.ha:136. let t3: strings.tokenizer = strings.tokenize("hello world", " "); expect_str_token(&t3, "hello"); expect_str_token(&t3, ""); expect_str_token(&t3, ""); expect_str_token(&t3, ""); expect_str_token(&t3, "world"); expect_str_done(&t3); // Leading + trailing delimiters yield empty tokens. // ref/hare/strings/tokenize.ha:147. let t4: strings.tokenizer = strings.tokenize(" hello world ", " "); expect_str_token(&t4, ""); expect_str_token(&t4, "hello"); expect_str_token(&t4, "world"); expect_str_token(&t4, ""); expect_str_done(&t4); // No delim hit — single full token. let t5: strings.tokenizer = strings.tokenize("abc", " "); expect_str_token(&t5, "abc"); expect_str_done(&t5); // Empty input — done immediately. let t6: strings.tokenizer = strings.tokenize("", " "); expect_str_done(&t6); }; @test fn rtokenize_cases() void = { // Reverse direction — first next_token is the last token. let t: strings.tokenizer = strings.rtokenize( "Hello world! My name is Harriet.", " "); expect_str_token(&t, "Harriet."); expect_str_token(&t, "is"); expect_str_token(&t, "name"); expect_str_token(&t, "My"); expect_str_token(&t, "world!"); expect_str_token(&t, "Hello"); expect_str_done(&t); // Multi-byte delim set, reverse direction. let t2: strings.tokenizer = strings.rtokenize("a b\tc", " \t"); expect_str_token(&t2, "c"); expect_str_token(&t2, "b"); expect_str_token(&t2, "a"); expect_str_done(&t2); // Empty input — done immediately. let t3: strings.tokenizer = strings.rtokenize("", " "); expect_str_done(&t3); }; @test fn peek_token_cases() void = { // Two peeks without advancing return the same token. let t: strings.tokenizer = strings.tokenize("a b c", " "); match (strings.peek_token(&t)) { case let p: str => { assert(!(!streq(p, "a"))); }; case bytes.done => { abort(); }; }; match (strings.peek_token(&t)) { case let p: str => { assert(!(!streq(p, "a"))); }; case bytes.done => { abort(); }; }; // Advance once — peek then returns "b". match (strings.next_token(&t)) { case let n: str => { assert(!(!streq(n, "a"))); }; case bytes.done => { abort(); }; }; match (strings.peek_token(&t)) { case let p: str => { assert(!(!streq(p, "b"))); }; case bytes.done => { abort(); }; }; // Empty input — peek is done. let t2: strings.tokenizer = strings.tokenize("", " "); match (strings.peek_token(&t2)) { case let p: str => { abort(); }; case bytes.done => void; }; }; @test fn remaining_tokens_cases() void = { // ref/hare/strings/tokenize.ha:157. After 2 next_tokens, remaining // is "My name is Harriet.". let t: strings.tokenizer = strings.tokenize( "Hello world! My name is Harriet.", " "); match (strings.next_token(&t)) { case let n: str => { assert(!(!streq(n, "Hello"))); }; case bytes.done => { abort(); }; }; match (strings.next_token(&t)) { case let n: str => { assert(!(!streq(n, "world!"))); }; case bytes.done => { abort(); }; }; if (!streq(strings.remaining_tokens(&t), "My name is Harriet.")) { abort(); }; // Fresh tokenizer — remaining_tokens is the whole input. let t2: strings.tokenizer = strings.tokenize("a b c", " "); assert(!(!streq(strings.remaining_tokens(&t2), "a b c"))); }; // ---- splitn / rsplitn / split ---------------------------------------- // ref/hare/strings/tokenize.ha:245 @test fn split. Hare's vectors // mirrored here directly; element reads go through `&toks.ptr[i]: *str` // rather than `toks[i]` so the 16B str element copy stays out of the // multi-word-store gap noted at cmd/w6c/cgen.c:6515. fn expect_str(toks: []str, i: i32, want: str) void = { assert(!(i >= toks.len)); let p: *str = &toks.ptr[i]; assert(!(p.len != want.len)); let j: i32 = 0; for (j < want.len) { assert(!(p.ptr[j] != want[j])); j += 1; }; }; @test fn splitn_cases() void = { // ref/hare/strings/tokenize.ha:247 — n=4 buckets the trailing // "is Drew" as the remainder slot. let t1: []str = strings.splitn("Hello, my name is Drew", " ", 4); assert(!(t1.len != 4)); expect_str(t1, 0, "Hello,"); expect_str(t1, 1, "my"); expect_str(t1, 2, "name"); expect_str(t1, 3, "is Drew"); os.free(t1.ptr: *void, (t1.cap: u64) * size(str): u64); // ref/hare/strings/tokenize.ha:263 — n > tokens leaves a single // slot holding the unchanged input (delim not found). let t2: []str = strings.splitn("one", "=", 2); assert(!(t2.len != 1)); expect_str(t2, 0, "one"); os.free(t2.ptr: *void, (t2.cap: u64) * size(str): u64); // n == 1 — single slot holding the whole input as remainder. let t3: []str = strings.splitn("a b c", " ", 1); assert(!(t3.len != 1)); expect_str(t3, 0, "a b c"); os.free(t3.ptr: *void, (t3.cap: u64) * size(str): u64); // Empty input — empty result. let t4: []str = strings.splitn("", " ", 5); assert(!(t4.len != 0)); if (t4.cap > 0) { os.free(t4.ptr: *void, (t4.cap: u64) * size(str): u64); }; // Multi-byte delim set (byte-set semantics per // ref/hare/strings/tokenize.ha:35) — split on ',' OR ':' OR ';'. let t5: []str = strings.splitn("hello;world,foo:bar", ",:;", 10); assert(!(t5.len != 4)); expect_str(t5, 0, "hello"); expect_str(t5, 1, "world"); expect_str(t5, 2, "foo"); expect_str(t5, 3, "bar"); os.free(t5.ptr: *void, (t5.cap: u64) * size(str): u64); }; @test fn rsplitn_cases() void = { // ref/hare/strings/tokenize.ha:271 — reverse n=4 with the // "Hello, my" prefix as the remainder slot at index 0. let t1: []str = strings.rsplitn("Hello, my name is Drew", " ", 4); assert(!(t1.len != 4)); expect_str(t1, 0, "Hello, my"); expect_str(t1, 1, "name"); expect_str(t1, 2, "is"); expect_str(t1, 3, "Drew"); os.free(t1.ptr: *void, (t1.cap: u64) * size(str): u64); // n > token count — done short-circuit returns toks UN-reversed // (last-token-first order). Mirrors bytes.rsplitn (Hare's // ref/hare/strings/tokenize.ha:219-224 reverse step is gated // behind the n-1 loop completion). let t2: []str = strings.rsplitn("a b c", " ", 10); assert(!(t2.len != 3)); expect_str(t2, 0, "c"); expect_str(t2, 1, "b"); expect_str(t2, 2, "a"); os.free(t2.ptr: *void, (t2.cap: u64) * size(str): u64); // n == 1 — single slot holding the whole input as remainder. let t3: []str = strings.rsplitn("a b c", " ", 1); assert(!(t3.len != 1)); expect_str(t3, 0, "a b c"); os.free(t3.ptr: *void, (t3.cap: u64) * size(str): u64); // delim absent — first next_token yields the entire input as the // sole token; second iter sees done and short-circuits with the // 1-elem toks un-reversed (single element, reverse is a no-op). let t4: []str = strings.rsplitn("abc", "=", 5); assert(!(t4.len != 1)); expect_str(t4, 0, "abc"); os.free(t4.ptr: *void, (t4.cap: u64) * size(str): u64); }; @test fn split_cases() void = { // ref/hare/strings/tokenize.ha:255 — full split, every delim hit // is a boundary. let t1: []str = strings.split("Hello, my name is Drew", " "); assert(!(t1.len != 5)); expect_str(t1, 0, "Hello,"); expect_str(t1, 1, "my"); expect_str(t1, 2, "name"); expect_str(t1, 3, "is"); expect_str(t1, 4, "Drew"); os.free(t1.ptr: *void, (t1.cap: u64) * size(str): u64); // Leading + trailing delim — empty tokens at ends. let t2: []str = strings.split(" a b ", " "); assert(!(t2.len != 4)); expect_str(t2, 0, ""); expect_str(t2, 1, "a"); expect_str(t2, 2, "b"); expect_str(t2, 3, ""); os.free(t2.ptr: *void, (t2.cap: u64) * size(str): u64); // Multi-byte delim set, byte-set semantics matching Hare's // strings::split example at ref/hare/strings/tokenize.ha:235. let t3: []str = strings.split("hello;world,foo:bar", ",:;"); assert(!(t3.len != 4)); expect_str(t3, 0, "hello"); expect_str(t3, 1, "world"); expect_str(t3, 2, "foo"); expect_str(t3, 3, "bar"); os.free(t3.ptr: *void, (t3.cap: u64) * size(str): u64); }; // ---- lpad / rpad ----------------------------------------------------- // ref/hare/strings/pad.ha:23 (@test fn lpad), :54 (@test fn rpad). Hare's // row triple (smaxlen (early return path), multibyte s with byte-length // counting, and multibyte pad rune (encoded width >1 byte). The // multibyte-pad rows pin Hare's `[..maxlen]` slice contract — a // multibyte trailing pad may be truncated mid-codepoint, exactly as // Hare does. @test fn lpad_cases() void = { // Hare row 1: shorter s, ASCII pad. let r1: str = strings.lpad("2", '0', 5); assert(!(!streq(r1, "00002"))); assert(!(r1.len != 5)); os.free(r1.ptr: *void, r1.len: u64); // Hare row 2: s.len == maxlen — early dup path. let r2: str = strings.lpad("12345", '0', 5); assert(!(!streq(r2, "12345"))); assert(!(r2.len != 5)); os.free(r2.ptr: *void, r2.len: u64); // Hare row 3: empty s, full-width pad. let r3: str = strings.lpad("", '0', 5); assert(!(!streq(r3, "00000"))); assert(!(r3.len != 5)); os.free(r3.ptr: *void, r3.len: u64); // ww row 1: s.len > maxlen — early dup path returns input copy. let r4: str = strings.lpad("abcdef", '_', 3); assert(!(!streq(r4, "abcdef"))); assert(!(r4.len != 6)); os.free(r4.ptr: *void, r4.len: u64); // ww row 2: multibyte s — byte-length contract (Hare `len(s)`). // "café" is 5 bytes; maxlen 7 → 2 pad bytes prepended. let r5: str = strings.lpad("café", '_', 7); assert(!(!streq(r5, "__café"))); assert(!(r5.len != 7)); os.free(r5.ptr: *void, r5.len: u64); // ww row 3: multibyte pad rune. 'α' U+03B1 is 2 bytes (0xCE 0xB1). // s="x" (1 byte), maxlen=5 → npads = 5 - 1 = 4, pad_bytes=2, // total writes = 4*2 + 1 = 9 bytes; result `[..5]` = "αα" (4 bytes) // + 0xCE (truncated leading byte of next α) — exactly Hare's slice. let r6: str = strings.lpad("x", 0x03B1u32: rune, 5); assert(!(r6.len != 5)); assert(!(r6.ptr[0] != 0xCEu8)); // α byte 0 assert(!(r6.ptr[1] != 0xB1u8)); // α byte 1 assert(!(r6.ptr[2] != 0xCEu8)); // α byte 0 assert(!(r6.ptr[3] != 0xB1u8)); // α byte 1 assert(!(r6.ptr[4] != 0xCEu8)); // truncated α byte 0 os.free(r6.ptr: *void, r6.len: u64); }; @test fn rpad_cases() void = { // Hare row 1: shorter s, ASCII pad. let r1: str = strings.rpad("2", '0', 5); assert(!(!streq(r1, "20000"))); assert(!(r1.len != 5)); os.free(r1.ptr: *void, r1.len: u64); // Hare row 2: s.len == maxlen — early dup path. let r2: str = strings.rpad("12345", '0', 5); assert(!(!streq(r2, "12345"))); assert(!(r2.len != 5)); os.free(r2.ptr: *void, r2.len: u64); // Hare row 3: empty s. let r3: str = strings.rpad("", '0', 5); assert(!(!streq(r3, "00000"))); assert(!(r3.len != 5)); os.free(r3.ptr: *void, r3.len: u64); // ww row 1: s.len > maxlen — early dup path. let r4: str = strings.rpad("abcdef", '_', 3); assert(!(!streq(r4, "abcdef"))); assert(!(r4.len != 6)); os.free(r4.ptr: *void, r4.len: u64); // ww row 2: multibyte s — byte-length contract. let r5: str = strings.rpad("café", '_', 7); assert(!(!streq(r5, "café__"))); assert(!(r5.len != 7)); os.free(r5.ptr: *void, r5.len: u64); // ww row 3: multibyte pad rune (2-byte 'α'). s="x" (1 byte), // maxlen=5 → "xαα" appended is 1 + 4 = 5 bytes (no truncation). let r6: str = strings.rpad("x", 0x03B1u32: rune, 5); assert(!(r6.len != 5)); assert(!(r6.ptr[0] != 'x')); assert(!(r6.ptr[1] != 0xCEu8)); // α byte 0 assert(!(r6.ptr[2] != 0xB1u8)); // α byte 1 assert(!(r6.ptr[3] != 0xCEu8)); // α byte 0 assert(!(r6.ptr[4] != 0xB1u8)); // α byte 1 os.free(r6.ptr: *void, r6.len: u64); }; // ---- replace ---------------------------------------------------------- // ref/hare/strings/replace.ha:46 (#4). Hare rows 1-5 (target found // once / multiple times / shorter / longer / multibyte) plus ww rows // (no-match returns fresh copy, empty-replacement, result-shrinks-to- // empty). @test fn replace_cases() void = { // Hare row 1: target found once. match (strings.replace("Hello world!", "world", "there")) { case let s: str => { assert(!(!streq(s, "Hello there!"))); os.free(s.ptr: *void, s.len: u64); }; case nomem => { abort(); }; }; // Hare row 2: needle found four times (replacement same length). match (strings.replace("I like dogs, dogs, birds, dogs", "dogs", "cats")) { case let s: str => { assert(!(!streq(s, "I like cats, cats, birds, cats"))); os.free(s.ptr: *void, s.len: u64); }; case nomem => { abort(); }; }; // Hare row 3: replacement shorter than needle, non-overlapping // matches (Hare advances by needle.len, not 1). match (strings.replace("aaaaaa", "aa", "a")) { case let s: str => { assert(!(!streq(s, "aaa"))); os.free(s.ptr: *void, s.len: u64); }; case nomem => { abort(); }; }; // Hare row 4: replacement longer than needle. match (strings.replace("aaa", "a", "aa")) { case let s: str => { assert(!(!streq(s, "aaaaaa"))); os.free(s.ptr: *void, s.len: u64); }; case nomem => { abort(); }; }; // Hare row 5: multibyte UTF-8 (3-byte runes). match (strings.replace("こんにちは", "にち", "ばん")) { case let s: str => { assert(!(!streq(s, "こんばんは"))); os.free(s.ptr: *void, s.len: u64); }; case nomem => { abort(); }; }; // ww row 1: needle not found — fresh copy (distinct from // borrowed input ptr). let src: str = "hello world"; match (strings.replace(src, "xyz", "abc")) { case let s: str => { assert(!(!streq(s, "hello world"))); assert(!(s.ptr == src.ptr)); os.free(s.ptr: *void, s.len: u64); }; case nomem => { abort(); }; }; // ww row 2: empty replacement — needle is removed. match (strings.replace("foo-bar-baz", "-", "")) { case let s: str => { assert(!(!streq(s, "foobarbaz"))); os.free(s.ptr: *void, s.len: u64); }; case nomem => { abort(); }; }; // ww row 3: result shrinks to empty — total==0 nil-alloc path. match (strings.replace("aaaa", "aa", "")) { case let s: str => { assert(!(!streq(s, ""))); assert(!(s.len != 0)); }; case nomem => { abort(); }; }; // ww row 4: multibyte single-rune replace — every "に" → "X". match (strings.replace("こんにちは", "に", "X")) { case let s: str => { assert(!(!streq(s, "こんXちは"))); os.free(s.ptr: *void, s.len: u64); }; case nomem => { abort(); }; }; // ww row 5: empty input — returns empty. match (strings.replace("", "x", "y")) { case let s: str => { assert(!(s.len != 0)); }; case nomem => { abort(); }; }; }; // ---- cut / rcut ------------------------------------------------------- // ref/hare/strings/tokenize.ha:316 (@test fn cut). str wrappers over // bytes.cut/rcut. The `let (a, b) = cut(...)` destructure drives the // over-cap tuple-return (sret) path end-to-end (a 2nd #10 witness). @test fn cut_cases() void = { // both halves present. let (a0, b0) = strings.cut("hello=world", "="); assert(!(!streq(a0, "hello"))); assert(!(!streq(b0, "world"))); // only first instance is cut; rest stays in the second half. let (a1, b1) = strings.cut("hello=world=foobar", "="); assert(!(!streq(a1, "hello"))); assert(!(!streq(b1, "world=foobar"))); // delim absent -> (whole input, ""). let (a2, b2) = strings.cut("hello world", "="); assert(!(!streq(a2, "hello world"))); assert(!(!streq(b2, ""))); // delim at start -> empty before. let (a3, b3) = strings.cut("=world", "="); assert(!(!streq(a3, ""))); assert(!(!streq(b3, "world"))); // delim at end -> empty after. let (a4, b4) = strings.cut("hello=", "="); assert(!(!streq(a4, "hello"))); assert(!(!streq(b4, ""))); // empty input -> ("", ""). let (a5, b5) = strings.cut("", "="); assert(!(!streq(a5, ""))); assert(!(!streq(b5, ""))); // multi-byte delim present. let (a6, b6) = strings.cut("aXYbXYc", "XY"); assert(!(!streq(a6, "a"))); assert(!(!streq(b6, "bXYc"))); // multi-byte delim absent. let (a7, b7) = strings.cut("abc", "XY"); assert(!(!streq(a7, "abc"))); assert(!(!streq(b7, ""))); // borrowed, not copied: first half aliases the input bytes. let in: str = "hello=world"; let (a8, b8) = strings.cut(in, "="); assert(!(a8.ptr != in.ptr)); }; @test fn rcut_cases() void = { // rcut splits along the LAST instance. let (a0, b0) = strings.rcut("hello=world=foobar", "="); assert(!(!streq(a0, "hello=world"))); assert(!(!streq(b0, "foobar"))); // single instance == cut. let (a1, b1) = strings.rcut("hello=world", "="); assert(!(!streq(a1, "hello"))); assert(!(!streq(b1, "world"))); // delim absent -> (whole input, ""). let (a2, b2) = strings.rcut("hello world", "="); assert(!(!streq(a2, "hello world"))); assert(!(!streq(b2, ""))); // delim at end -> empty after. let (a3, b3) = strings.rcut("hello=", "="); assert(!(!streq(a3, "hello"))); assert(!(!streq(b3, ""))); // delim at start -> empty before. let (a4, b4) = strings.rcut("=world", "="); assert(!(!streq(a4, ""))); assert(!(!streq(b4, "world"))); // empty input -> ("", ""). let (a5, b5) = strings.rcut("", "="); assert(!(!streq(a5, ""))); assert(!(!streq(b5, ""))); // multi-byte delim, two instances -> cut at LAST. let (a6, b6) = strings.rcut("XYaXYb", "XY"); assert(!(!streq(a6, "XYa"))); assert(!(!streq(b6, "b"))); // multi-byte delim absent. let (a7, b7) = strings.rcut("abc", "XY"); assert(!(!streq(a7, "abc"))); assert(!(!streq(b7, ""))); };