lib: split strings tests per ref/hare/strings families + helpers file

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).
This commit is contained in:
2026-08-08 19:42:49 +09:00
parent 5a263cb115
commit 51cb8e7d64
12 changed files with 1483 additions and 1387 deletions

114
lib/strings/concat_test.ww Normal file
View File

@@ -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;
};
};

112
lib/strings/dup_test.ww Normal file
View File

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

View File

@@ -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;
};

305
lib/strings/iter_test.ww Normal file
View File

@@ -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")));
};

107
lib/strings/pad_test.ww Normal file
View File

@@ -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 (s<maxlen, s==maxlen, s=="" empty) is mirrored; ww extras
// pin s>maxlen (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);
};

110
lib/strings/replace_test.ww Normal file
View File

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

File diff suppressed because it is too large Load Diff

96
lib/strings/sub_test.ww Normal file
View File

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

View File

@@ -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, "")));
};

170
lib/strings/trim_test.ww Normal file
View File

@@ -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;
};
};

23
lib/strings/utf8_test.ww Normal file
View File

@@ -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
};