From 51cb8e7d648b48feb8931363d698cfe54245476f Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 8 Aug 2026 19:42:49 +0900 Subject: [PATCH] lib: split strings tests per ref/hare/strings families + helpers file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unblocked by the package-mode ruling: same-package test files now compose everywhere (coordinator dir-mode owns test-library and the libbyteid dir entry owns byte identity), so the per-file standalone constraint is gone and Go's stdlib layout applies directly. The 15-banner strings_test.ww monolith dissolves into per-ref-file family files — dup, concat, trim, sub, utf8, iter, tokenize (tokenize+splitn+cut, all of ref tokenize.ha), pad, replace — plus helpers_test.ww holding exactly the one cross-family helper (streq), Go's shared test-helper idiom. Pure moves; family-local helpers (expect_str_token et al.) stay in their family file. Leading banners that merely restated the filename dropped; 6 interior sub-family boundaries remain. lib/ census 141 -> 132. libbyteid gains the dx() dir-mode entry form decided in B2: compose every *_test.ww under moddir exactly as pkgcombined does (module-reset separators, byte-lex order) and byte-id the composed root through both driver stages. The five per-file strings fx entries retire into one dx("lib/strings") — every source line they covered is inside the composed unit, and the completeness scan still keys the module. NENTEXPECT 60 -> 56. Recorded, not split: fmt_test's 13 banners are scenario groups over the one fprint surface and ref/hare/fmt itself keeps a single +test.ha — a print/wrappers split would fight the ref; memio stays per the standing ruling (everything ww ports lives in ref stream.ha). --- lib/strings/concat_test.ww | 114 +++ lib/strings/dup_test.ww | 112 +++ lib/strings/helpers_test.ww | 15 + lib/strings/iter_test.ww | 305 ++++++++ lib/strings/pad_test.ww | 107 +++ lib/strings/replace_test.ww | 110 +++ lib/strings/strings_test.ww | 1373 --------------------------------- lib/strings/sub_test.ww | 96 +++ lib/strings/tokenize_test.ww | 385 +++++++++ lib/strings/trim_test.ww | 170 ++++ lib/strings/utf8_test.ww | 23 + test/byteid/libbyteid_test.ww | 60 +- 12 files changed, 1483 insertions(+), 1387 deletions(-) create mode 100644 lib/strings/concat_test.ww create mode 100644 lib/strings/dup_test.ww create mode 100644 lib/strings/helpers_test.ww create mode 100644 lib/strings/iter_test.ww create mode 100644 lib/strings/pad_test.ww create mode 100644 lib/strings/replace_test.ww delete mode 100644 lib/strings/strings_test.ww create mode 100644 lib/strings/sub_test.ww create mode 100644 lib/strings/tokenize_test.ww create mode 100644 lib/strings/trim_test.ww create mode 100644 lib/strings/utf8_test.ww diff --git a/lib/strings/concat_test.ww b/lib/strings/concat_test.ww new file mode 100644 index 00000000..a7d77c12 --- /dev/null +++ b/lib/strings/concat_test.ww @@ -0,0 +1,114 @@ +// Vectors mirror ref/hare/strings/concat.ha where ww can express them. +// External strings_test package per the Go foo_test idiom (rule-5/9 +// carve-out, task #16); the shared streq helper lives in +// helpers_test.ww — same-package test files compose in dir-mode +// (the package, not the file, is the unit of testing). +package strings_test; + +import strings; +import os; + +// 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; + }; +}; + diff --git a/lib/strings/dup_test.ww b/lib/strings/dup_test.ww new file mode 100644 index 00000000..7be07ea6 --- /dev/null +++ b/lib/strings/dup_test.ww @@ -0,0 +1,112 @@ +// Vectors mirror ref/hare/strings/dup.ha where ww can express them. +// External strings_test package per the Go foo_test idiom (rule-5/9 +// carve-out, task #16); the shared streq helper lives in +// helpers_test.ww — same-package test files compose in dir-mode +// (the package, not the file, is the unit of testing). +package strings_test; + +import strings; +import os; + +// 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(); }; + }; +}; + diff --git a/lib/strings/helpers_test.ww b/lib/strings/helpers_test.ww new file mode 100644 index 00000000..11dd41b5 --- /dev/null +++ b/lib/strings/helpers_test.ww @@ -0,0 +1,15 @@ +// Shared helpers of package strings_test, composed into every family +// file by dir-mode (Go's shared test-helper idiom: one helpers file in +// the same test package). Holds exactly the cross-family helper; a +// helper used by one family stays in that family's file. +package strings_test; + +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; +}; diff --git a/lib/strings/iter_test.ww b/lib/strings/iter_test.ww new file mode 100644 index 00000000..1de072bb --- /dev/null +++ b/lib/strings/iter_test.ww @@ -0,0 +1,305 @@ +// Vectors mirror ref/hare/strings/iter.ha where ww can express them. +// External strings_test package per the Go foo_test idiom (rule-5/9 +// carve-out, task #16); the shared streq helper lives in +// helpers_test.ww — same-package test files compose in dir-mode +// (the package, not the file, is the unit of testing). +package strings_test; + +import strings; +import encoding.utf8; + +// 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"))); +}; + diff --git a/lib/strings/pad_test.ww b/lib/strings/pad_test.ww new file mode 100644 index 00000000..999a8622 --- /dev/null +++ b/lib/strings/pad_test.ww @@ -0,0 +1,107 @@ +// Vectors mirror ref/hare/strings/pad.ha where ww can express them. +// External strings_test package per the Go foo_test idiom (rule-5/9 +// carve-out, task #16); the shared streq helper lives in +// helpers_test.ww — same-package test files compose in dir-mode +// (the package, not the file, is the unit of testing). +package strings_test; + +import strings; +import os; + +// 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); +}; + diff --git a/lib/strings/replace_test.ww b/lib/strings/replace_test.ww new file mode 100644 index 00000000..09b9dc6a --- /dev/null +++ b/lib/strings/replace_test.ww @@ -0,0 +1,110 @@ +// Vectors mirror ref/hare/strings/replace.ha where ww can express them. +// External strings_test package per the Go foo_test idiom (rule-5/9 +// carve-out, task #16); the shared streq helper lives in +// helpers_test.ww — same-package test files compose in dir-mode +// (the package, not the file, is the unit of testing). +package strings_test; + +import strings; +import os; + +// 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(); }; + }; +}; + diff --git a/lib/strings/strings_test.ww b/lib/strings/strings_test.ww deleted file mode 100644 index e043d98a..00000000 --- a/lib/strings/strings_test.ww +++ /dev/null @@ -1,1373 +0,0 @@ -// 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,sub,utf8,iter, -// tokenize,pad,replace}.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; - }; -}; - -// ---- 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; - }; -}; - -// ---- 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 / peektoken / remainingtokens ----------- -// 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.peektoken(t)) { - case let p: str => { assert(!(!streq(p, want))); }; - case bytes.done => { abort(); }; - }; - match (strings.nexttoken(t)) { - case let n: str => { assert(!(!streq(n, want))); }; - case bytes.done => { abort(); }; - }; -}; - -fn expect_str_done(t: *strings.tokenizer) void = { - match (strings.peektoken(t)) { - case let p: str => { abort(); }; - case bytes.done => void; - }; - match (strings.nexttoken(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 nexttoken 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 peektoken_cases() void = { - // Two peeks without advancing return the same token. - let t: strings.tokenizer = strings.tokenize("a b c", " "); - match (strings.peektoken(&t)) { - case let p: str => { assert(!(!streq(p, "a"))); }; - case bytes.done => { abort(); }; - }; - match (strings.peektoken(&t)) { - case let p: str => { assert(!(!streq(p, "a"))); }; - case bytes.done => { abort(); }; - }; - // Advance once — peek then returns "b". - match (strings.nexttoken(&t)) { - case let n: str => { assert(!(!streq(n, "a"))); }; - case bytes.done => { abort(); }; - }; - match (strings.peektoken(&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.peektoken(&t2)) { - case let p: str => { abort(); }; - case bytes.done => void; - }; -}; - -@test fn remainingtokens_cases() void = { - // ref/hare/strings/tokenize.ha:157. After 2 nexttokens, remaining - // is "My name is Harriet.". - let t: strings.tokenizer = strings.tokenize( - "Hello world! My name is Harriet.", " "); - match (strings.nexttoken(&t)) { - case let n: str => { assert(!(!streq(n, "Hello"))); }; - case bytes.done => { abort(); }; - }; - match (strings.nexttoken(&t)) { - case let n: str => { assert(!(!streq(n, "world!"))); }; - case bytes.done => { abort(); }; - }; - if (!streq(strings.remainingtokens(&t), "My name is Harriet.")) { - abort(); - }; - - // Fresh tokenizer — remainingtokens is the whole input. - let t2: strings.tokenizer = strings.tokenize("a b c", " "); - assert(!(!streq(strings.remainingtokens(&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 nexttoken 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, ""))); -}; diff --git a/lib/strings/sub_test.ww b/lib/strings/sub_test.ww new file mode 100644 index 00000000..991132f6 --- /dev/null +++ b/lib/strings/sub_test.ww @@ -0,0 +1,96 @@ +// Vectors mirror ref/hare/strings/sub.ha where ww can express them. +// External strings_test package per the Go foo_test idiom (rule-5/9 +// carve-out, task #16); the shared streq helper lives in +// helpers_test.ww — same-package test files compose in dir-mode +// (the package, not the file, is the unit of testing). +package strings_test; + +import strings; +import encoding.utf8; + +// 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(); }; + }; +}; + diff --git a/lib/strings/tokenize_test.ww b/lib/strings/tokenize_test.ww new file mode 100644 index 00000000..72e5ee47 --- /dev/null +++ b/lib/strings/tokenize_test.ww @@ -0,0 +1,385 @@ +// Vectors mirror ref/hare/strings/tokenize.ha where ww can express them. +// External strings_test package per the Go foo_test idiom (rule-5/9 +// carve-out, task #16); the shared streq helper lives in +// helpers_test.ww — same-package test files compose in dir-mode +// (the package, not the file, is the unit of testing). +package strings_test; + +import strings; +import os; + +// 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.peektoken(t)) { + case let p: str => { assert(!(!streq(p, want))); }; + case bytes.done => { abort(); }; + }; + match (strings.nexttoken(t)) { + case let n: str => { assert(!(!streq(n, want))); }; + case bytes.done => { abort(); }; + }; +}; + +fn expect_str_done(t: *strings.tokenizer) void = { + match (strings.peektoken(t)) { + case let p: str => { abort(); }; + case bytes.done => void; + }; + match (strings.nexttoken(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 nexttoken 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 peektoken_cases() void = { + // Two peeks without advancing return the same token. + let t: strings.tokenizer = strings.tokenize("a b c", " "); + match (strings.peektoken(&t)) { + case let p: str => { assert(!(!streq(p, "a"))); }; + case bytes.done => { abort(); }; + }; + match (strings.peektoken(&t)) { + case let p: str => { assert(!(!streq(p, "a"))); }; + case bytes.done => { abort(); }; + }; + // Advance once — peek then returns "b". + match (strings.nexttoken(&t)) { + case let n: str => { assert(!(!streq(n, "a"))); }; + case bytes.done => { abort(); }; + }; + match (strings.peektoken(&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.peektoken(&t2)) { + case let p: str => { abort(); }; + case bytes.done => void; + }; +}; + +@test fn remainingtokens_cases() void = { + // ref/hare/strings/tokenize.ha:157. After 2 nexttokens, remaining + // is "My name is Harriet.". + let t: strings.tokenizer = strings.tokenize( + "Hello world! My name is Harriet.", " "); + match (strings.nexttoken(&t)) { + case let n: str => { assert(!(!streq(n, "Hello"))); }; + case bytes.done => { abort(); }; + }; + match (strings.nexttoken(&t)) { + case let n: str => { assert(!(!streq(n, "world!"))); }; + case bytes.done => { abort(); }; + }; + if (!streq(strings.remainingtokens(&t), "My name is Harriet.")) { + abort(); + }; + + // Fresh tokenizer — remainingtokens is the whole input. + let t2: strings.tokenizer = strings.tokenize("a b c", " "); + assert(!(!streq(strings.remainingtokens(&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 nexttoken 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); +}; + +// ---- 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, ""))); +}; diff --git a/lib/strings/trim_test.ww b/lib/strings/trim_test.ww new file mode 100644 index 00000000..aba0d910 --- /dev/null +++ b/lib/strings/trim_test.ww @@ -0,0 +1,170 @@ +// Vectors mirror ref/hare/strings/trim.ha where ww can express them. +// External strings_test package per the Go foo_test idiom (rule-5/9 +// carve-out, task #16); the shared streq helper lives in +// helpers_test.ww — same-package test files compose in dir-mode +// (the package, not the file, is the unit of testing). +package strings_test; + +import strings; + +// 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; + }; +}; + diff --git a/lib/strings/utf8_test.ww b/lib/strings/utf8_test.ww new file mode 100644 index 00000000..b14ad432 --- /dev/null +++ b/lib/strings/utf8_test.ww @@ -0,0 +1,23 @@ +// Vectors mirror ref/hare/strings/utf8.ha where ww can express them. +// External strings_test package per the Go foo_test idiom (rule-5/9 +// carve-out, task #16); the shared streq helper lives in +// helpers_test.ww — same-package test files compose in dir-mode +// (the package, not the file, is the unit of testing). +package strings_test; + +import strings; +import encoding.utf8; + +// 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 +}; + diff --git a/test/byteid/libbyteid_test.ww b/test/byteid/libbyteid_test.ww index c8b6ec33..273a1008 100644 --- a/test/byteid/libbyteid_test.ww +++ b/test/byteid/libbyteid_test.ww @@ -43,7 +43,7 @@ import time; def MID: i32 = 0; def MDIVERGE: i32 = 1; def MWWREJECT: i32 = 2; -def NENTEXPECT: i32 = 60; +def NENTEXPECT: i32 = 56; type ent = struct { fixture: str, // repo-relative .ww; "" -> probe entry @@ -83,6 +83,17 @@ fn pri(p: str, inc: str, sentinel: str, moddir: str) ent = { return e; }; +// Dir-mode entry: byte-id the module's COMPOSED test package (every +// *_test.ww under moddir, module-reset separators, byte-lex order — +// the coordinator's pkgcombined shape) through both driver stages. +// Needed once a package's test files share a helpers file and stop +// being standalone single-file roots (the B3 strings split). +fn dx(moddir: str) ent = { + let e: ent = fx(""); + e.moddir = moddir; + return e; +}; + // The 60-unit roster. Graduation history lives in git (the retired C // carrier's table comments); cites are kept only where a non-ID pin // would need them. @@ -116,11 +127,9 @@ fn corpus() []ent = { append(es, fx("lib/strconv/test/ftos_test.ww")); append(es, fx("lib/strconv/test/stof_test.ww")); append(es, fx("lib/strconv/test/int_test.ww")); - append(es, fx("lib/strings/strings_test.ww")); - append(es, fx("lib/strings/suffix_test.ww")); - append(es, fx("lib/strings/contains_test.ww")); - append(es, fx("lib/strings/index_test.ww")); - append(es, fx("lib/strings/compare_test.ww")); + // strings' families share helpers_test.ww (B3 split) — the + // single-file roots dissolved; the composed package is the root. + append(es, dx("lib/strings")); append(es, fx("lib/temp/temp_test.ww")); append(es, fx("lib/time/arithm_test.ww")); append(es, fx("lib/time/duration_test.ww")); @@ -271,9 +280,30 @@ fn checkone(e: *ent) void = { testenv.writefile(strings.concat(td, "/", base), testenv.readfile(strings.concat(testenv.repo(), "/", e.fixture))); - } else { + } else { if (e.probe.len != 0) { testenv.writefile(strings.concat(td, "/", base), e.probe); - }; + } else { + // dir-mode: compose every *_test.ww under moddir exactly + // as pkgcombined does (external group: test sources only, + // module-reset separators; testenv.listdir sorts). + base = "combined.ww"; + let srcdir: str = strings.concat(testenv.repo(), "/", + e.moddir); + let names: []str = testenv.listdir(srcdir); + let body: str = ""; + let ni: i32 = 0; + for (ni < names.len) { + if (strings.hassuffix(names[ni], "_test.ww")) { + body = strings.concat(body, + "//ww:module-reset\n", + testenv.readfile(strings.concat( + srcdir, "/", names[ni])), + "\n"); + }; + ni += 1; + }; + testenv.writefile(strings.concat(td, "/", base), body); + }; }; // dirname(fixture) leads the search path so bare same-module // imports resolve as they do under the in-tree driver run. @@ -281,17 +311,19 @@ fn checkone(e: *ent) void = { if (e.fixture.len != 0) { append(incs, strings.concat(testenv.repo(), "/", dirnameof(e.fixture))); - }; + } else { if (e.probe.len == 0 && e.moddir.len != 0) { + append(incs, strings.concat(testenv.repo(), "/", e.moddir)); + }; }; if (e.inc.len != 0) { append(incs, strings.concat(testenv.repo(), "/", e.inc)); }; - // @test fixtures are main-less, so `test -S` carries -T - // (synthesizes the entry + auto-bundles lib/test); import probes - // carry their own fn main(), which -T loud-rejects (910), so they - // `build -S`. + // @test fixtures and dir-mode compositions are main-less, so + // `test -S` carries -T (synthesizes the entry + auto-bundles + // lib/test); import probes carry their own fn main(), which -T + // loud-rejects (910), so they `build -S`. let sub: str = "test"; - if (e.fixture.len == 0) { sub = "build"; }; + if (e.probe.len != 0) { sub = "build"; }; let stem: str = stripext(base); let stemc: str = strings.concat(td, "/c_", stem); let stemw: str = strings.concat(td, "/w_", stem);