lib/strings+test: graduate trim/ltrim/rtrim to Hare rune... variadic

ltrim(input, exclude: rune) -> ltrim(input, trim: rune...) per
ref/hare/strings/trim.ha:11. rtrim mirrors via riter/prev per :32.
trim thin wrapper ltrim(rtrim(input, trim...), trim...) per :54.
Unblocked by #15 (5609d04) — multi-rune iter+match-prev composition
in non-leaf callees now produces byte-identical asm + correct runtime
on both stages.

Subset: 0-arg call (len(trim)==0) returns input unchanged. lib/bytes
whitespace-default future commit will graduate strings.trim's
whitespace... fast path.

ltrim_cases / rtrim_cases / trim_cases table-driven via parallel runes
+ inputs + argo/argn + want arrays + loop. Hare ref/hare/strings/trim.ha:75
vectors covered: bracket-pair, "mississippi" multi-rune, "yellowwooddoor"
multi-rune, Sentimentalized long form, plus 0-arg cases and 4-byte rune
"abacadabra"/"ahi" coverage. Bisect via signalled = 1100/1200/1300 + i.

make test 122/122; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.
This commit is contained in:
2026-05-19 02:32:17 +09:00
parent 5609d0456f
commit c305b04cb5
5 changed files with 344 additions and 214 deletions

View File

@@ -3,14 +3,10 @@
//
// Documented divergences from Hare:
//
// - `trim` / `ltrim` / `rtrim` take a single rune. Hare's are
// `(trim: rune...)` (ref/hare/strings/trim.ha:11,32,54). The
// port body uses `iter`/`next` + inner match against the pack +
// `prev` step-back; that shape triggers #36 (wwstage scanlocals
// misses match-arm `case let` bindings, slot offsets diverge —
// `.ai/probe_trim_36extra.{ww,diff}`). Hare's no-rune
// strip-whitespace branch additionally needs `lib/bytes`
// variadic graduation.
// - `trim` / `ltrim` / `rtrim` 0-arg returns the input unchanged.
// Hare strips ASCII whitespace via `bytes::ltrim(input,
// whitespace...)`; that needs `lib/bytes` variadic graduation
// (future commit).
// - `contains` is non-variadic. Hare's is
// `contains(haystack, needles: (str | rune)...)`
// (ref/hare/strings/contains.ha:9). Gated on `(str|rune)...`
@@ -243,56 +239,67 @@ export fn trimsuffix(input: str, suffix: str) str = {
return r;
};
// ltrim — strip occurrences of `exclude` (encoded as UTF-8) from the
// front. Borrowed view. ref/hare/strings/trim.ha:11 (subset: single
// rune; Hare's `(trim: rune...)` blocks on task #16). The no-rune
// strip-whitespace branch is omitted for the same reason.
export fn ltrim(input: str, exclude: rune) str = {
let scratch: [4]u8;
let pat: []u8 = runebytes(scratch[0:4], exclude);
let i: i32 = 0;
for (i + pat.len <= input.len) {
let j: i32 = 0;
let ok: bool = true;
for (j < pat.len) {
if (input[i + j] != pat[j]) { ok = false; j = pat.len; }
else { j += 1; };
// ltrim — strip leading runes that occur in `trim`. Borrowed view.
// Empty `trim` returns input unchanged (Hare's no-rune branch strips
// ASCII whitespace via `bytes::ltrim`; needs lib/bytes variadic
// graduation). ref/hare/strings/trim.ha:11.
export fn ltrim(input: str, trim: rune...) str = {
if (trim.len == 0) { return input; };
let it: iterator = iter(input);
for (true) {
match (next(&it)) {
case let r: rune => {
let j: i32 = 0;
let found: bool = false;
for (j < trim.len) {
if (r == trim[j]) { found = true; j = trim.len; }
else { j += 1; };
};
if (!found) {
match (prev(&it)) {
case let r2: rune => void;
case utf8.done => void;
};
break;
};
};
case utf8.done => break;
};
if (!ok) { break; };
i += pat.len;
};
let r: str;
r.ptr = input.ptr + (i: u64);
r.len = input.len - i;
return r;
return iterstr(&it);
};
// rtrim — strip occurrences of `exclude` from the end. Borrowed view.
// ref/hare/strings/trim.ha:32 (same subset note).
export fn rtrim(input: str, exclude: rune) str = {
let scratch: [4]u8;
let pat: []u8 = runebytes(scratch[0:4], exclude);
let n: i32 = input.len;
for (n >= pat.len) {
let off: i32 = n - pat.len;
let j: i32 = 0;
let ok: bool = true;
for (j < pat.len) {
if (input[off + j] != pat[j]) { ok = false; j = pat.len; }
else { j += 1; };
// rtrim — strip trailing runes that occur in `trim`. Borrowed view.
// ref/hare/strings/trim.ha:32.
export fn rtrim(input: str, trim: rune...) str = {
if (trim.len == 0) { return input; };
let it: iterator = riter(input);
for (true) {
match (next(&it)) {
case let r: rune => {
let j: i32 = 0;
let found: bool = false;
for (j < trim.len) {
if (r == trim[j]) { found = true; j = trim.len; }
else { j += 1; };
};
if (!found) {
match (prev(&it)) {
case let r2: rune => void;
case utf8.done => void;
};
break;
};
};
case utf8.done => break;
};
if (!ok) { break; };
n -= pat.len;
};
let r: str;
r.ptr = input.ptr;
r.len = n;
return r;
return iterstr(&it);
};
// trim — strip from both ends. ref/hare/strings/trim.ha:54.
export fn trim(input: str, exclude: rune) str = {
return ltrim(rtrim(input, exclude), exclude);
export fn trim(input: str, trim: rune...) str = {
return ltrim(rtrim(input, trim...), trim...);
};
// iterator — UTF-8 rune cursor over a `str`. Layout flattens Hare's

View File

@@ -250,32 +250,134 @@ fn streq(a: str, b: str) bool = {
if (!streq(strings.trimsuffix("hello", "hello"), "")) { fail(); };
};
// ---- ltrim / rtrim / trim (single-rune subset) ------------------------
// ref/hare/strings/trim.ha:75-97. Vectors restricted to single-rune
// patterns (Hare's `rune...` blocks on task #16).
// ---- ltrim / rtrim / trim ---------------------------------------------
// ref/hare/strings/trim.ha:75-97. Bytes.ltrim whitespace-default rows
// (`ltrim(" hi") == "hi"`, etc.) deferred — they need lib/bytes
// variadic graduation.
@test fn ltrim_cases() void = {
if (!streq(strings.ltrim("", 'x'), "")) { fail(); };
if (!streq(strings.ltrim("aaabc", 'a'), "bc")) { fail(); };
if (!streq(strings.ltrim("xyz", 'a'), "xyz")) { fail(); }; // no match
if (!streq(strings.ltrim("aaaa", 'a'), "")) { fail(); }; // all stripped
// 4-byte rune pattern — '𝚊' = U+1D68A.
if (!streq(strings.ltrim("𝚊𝚊hi", 0x1D68Au32: rune), "hi")) { fail(); };
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';
let inputs: [8]str;
let argo: [8]i32;
let argn: [8]i32;
let want: [8]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";
let i: i32 = 0;
for (i < 8) {
signalled = 1100 + i;
let argv: []rune;
argv.ptr = &runes[argo[i]];
argv.len = argn[i];
argv.cap = argn[i];
let got: str = strings.ltrim(inputs[i], argv...);
if (!streq(got, want[i])) { fail(); };
i += 1;
};
};
@test fn rtrim_cases() void = {
if (!streq(strings.rtrim("", 'x'), "")) { fail(); };
if (!streq(strings.rtrim("bcaaa", 'a'), "bc")) { fail(); };
if (!streq(strings.rtrim("xyz", 'a'), "xyz")) { fail(); };
if (!streq(strings.rtrim("aaaa", 'a'), "")) { fail(); };
if (!streq(strings.rtrim("hi𝚊𝚊", 0x1D68Au32: rune), "hi")) { fail(); };
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';
let inputs: [8]str;
let argo: [8]i32;
let argn: [8]i32;
let want: [8]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";
let i: i32 = 0;
for (i < 8) {
signalled = 1200 + i;
let argv: []rune;
argv.ptr = &runes[argo[i]];
argv.len = argn[i];
argv.cap = argn[i];
let got: str = strings.rtrim(inputs[i], argv...);
if (!streq(got, want[i])) { fail(); };
i += 1;
};
};
@test fn trim_cases() void = {
if (!streq(strings.trim("", 'x'), "")) { fail(); };
if (!streq(strings.trim("aaabcaaa", 'a'), "bc")) { fail(); };
if (!streq(strings.trim("xyz", 'a'), "xyz")) { fail(); };
if (!streq(strings.trim("aaaa", 'a'), "")) { fail(); };
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] = ']';
let inputs: [7]str;
let argo: [7]i32;
let argn: [7]i32;
let want: [7]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";
let i: i32 = 0;
for (i < 7) {
signalled = 1300 + i;
let argv: []rune;
argv.ptr = &runes[argo[i]];
argv.len = argn[i];
argv.cap = argn[i];
let got: str = strings.trim(inputs[i], argv...);
if (!streq(got, want[i])) { fail(); };
i += 1;
};
};
// ---- compare ----------------------------------------------------------