From 18fe1a7a31c749d6781d6669558a11fa2776c8c1 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 19 May 2026 22:52:23 +0900 Subject: [PATCH] lib/strings+test: 0-arg trim strips ASCII whitespace per Hare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 0-arg ltrim/rtrim/trim used to return input unchanged. Hare's 0-arg form strips [' ', '\n', '\t', '\r'] (ref/hare/strings/trim.ha:6). Aligned by delegating to bytes.ltrim/bytes.rtrim with the whitespace set spread inline at the call site — the obvious `let ws = whitespace[0:4]` shape produces a slice whose ptr does NOT alias storage (#40). N-arg forms (strip-specific-runes) untouched. Test rows retargeted to Hare's canonical inputs from trim.ha:78/85 so '\r' is exercised alongside ' '/'\t'/'\n'. --- lib/strings/strings.ww | 27 ++++++++++----- lib/strings/stringstest.ww | 51 ++++++++++++++++++---------- selfhost/cmd/w6c/main.combined.ww | 27 ++++++++++----- selfhost/cmd/wwdump/main.combined.ww | 27 ++++++++++----- selfhost/test/smoke.combined.ww | 27 ++++++++++----- 5 files changed, 105 insertions(+), 54 deletions(-) diff --git a/lib/strings/strings.ww b/lib/strings/strings.ww index 75052521..1ddec4bb 100644 --- a/lib/strings/strings.ww +++ b/lib/strings/strings.ww @@ -3,10 +3,6 @@ // // Documented divergences from Hare: // -// - `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). // - `byteindex` / `rbyteindex` rune arms encode via // `utf8.encoderune`; the legacy impls scanned for `r: u8` (an // undocumented ASCII-only restriction that silently dropped @@ -402,12 +398,21 @@ export fn trimsuffix(input: str, suffix: str) str = { return r; }; +// whitespace — ASCII whitespace set used by the 0-arg ltrim/rtrim/trim +// branches (#9). ref/hare/strings/trim.ha:6. +let whitespace: [4]u8 = [0x20u8, 0x0Au8, 0x09u8, 0x0Du8]; + // 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. +// 0-arg strips ASCII whitespace via [[bytes.ltrim]] (#9). +// ref/hare/strings/trim.ha:11. The spread expression is inlined +// because `let ws: []u8 = whitespace[0:4]` produces a slice whose +// ptr doesn't track the module-level array storage (filed as #40); +// `b.flush = flushdefault[0:1]` in lib/bufio is the same shape via +// the working field-assign path. export fn ltrim(input: str, trim: rune...) str = { - if (trim.len == 0) { return input; }; + if (trim.len == 0) { + return fromutf8_unsafe(bytes.ltrim(toutf8(input), whitespace[0:4]...)); + }; let it: iterator = iter(input); for (true) { match (next(&it)) { @@ -433,9 +438,13 @@ export fn ltrim(input: str, trim: rune...) str = { }; // rtrim — strip trailing runes that occur in `trim`. Borrowed view. +// 0-arg strips ASCII whitespace via [[bytes.rtrim]] (#9). Spread is +// inlined to dodge #40 — see [[ltrim]]. // ref/hare/strings/trim.ha:32. export fn rtrim(input: str, trim: rune...) str = { - if (trim.len == 0) { return input; }; + if (trim.len == 0) { + return fromutf8_unsafe(bytes.rtrim(toutf8(input), whitespace[0:4]...)); + }; let it: iterator = riter(input); for (true) { match (next(&it)) { diff --git a/lib/strings/stringstest.ww b/lib/strings/stringstest.ww index 782f7447..4b672aa5 100644 --- a/lib/strings/stringstest.ww +++ b/lib/strings/stringstest.ww @@ -446,9 +446,8 @@ fn streq(a: str, b: str) bool = { }; // ---- 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. +// 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; @@ -462,10 +461,12 @@ fn streq(a: str, b: str) bool = { runes[7] = 'c'; runes[8] = 'd'; - let inputs: [8]str; - let argo: [8]i32; - let argn: [8]i32; - let want: [8]str; + // 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"; @@ -474,9 +475,12 @@ fn streq(a: str, b: str) bool = { 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 < 8) { + for (i < 11) { signalled = 1100 + i; let argv: []rune; argv.ptr = &runes[argo[i]]; @@ -510,10 +514,12 @@ fn streq(a: str, b: str) bool = { runes[17] = 'a'; runes[18] = 'd'; - let inputs: [8]str; - let argo: [8]i32; - let argn: [8]i32; - let want: [8]str; + // 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"; @@ -524,9 +530,13 @@ fn streq(a: str, b: str) bool = { 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 < 8) { + for (i < 11) { signalled = 1200 + i; let argv: []rune; argv.ptr = &runes[argo[i]]; @@ -549,10 +559,12 @@ fn streq(a: str, b: str) bool = { runes[6] = '['; runes[7] = ']'; - let inputs: [7]str; - let argo: [7]i32; - let argn: [7]i32; - let want: [7]str; + // 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"; @@ -561,9 +573,12 @@ fn streq(a: str, b: str) bool = { 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 < 7) { + for (i < 10) { signalled = 1300 + i; let argv: []rune; argv.ptr = &runes[argo[i]]; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index f6540983..c46cc84a 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -1840,10 +1840,6 @@ export fn position(d: *decoder) i32 = { // // Documented divergences from Hare: // -// - `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). // - `byteindex` / `rbyteindex` rune arms encode via // `utf8.encoderune`; the legacy impls scanned for `r: u8` (an // undocumented ASCII-only restriction that silently dropped @@ -2239,12 +2235,21 @@ export fn trimsuffix(input: str, suffix: str) str = { return r; }; +// whitespace — ASCII whitespace set used by the 0-arg ltrim/rtrim/trim +// branches (#9). ref/hare/strings/trim.ha:6. +let whitespace: [4]u8 = [0x20u8, 0x0Au8, 0x09u8, 0x0Du8]; + // 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. +// 0-arg strips ASCII whitespace via [[bytes.ltrim]] (#9). +// ref/hare/strings/trim.ha:11. The spread expression is inlined +// because `let ws: []u8 = whitespace[0:4]` produces a slice whose +// ptr doesn't track the module-level array storage (filed as #40); +// `b.flush = flushdefault[0:1]` in lib/bufio is the same shape via +// the working field-assign path. export fn ltrim(input: str, trim: rune...) str = { - if (trim.len == 0) { return input; }; + if (trim.len == 0) { + return fromutf8_unsafe(bytes.ltrim(toutf8(input), whitespace[0:4]...)); + }; let it: iterator = iter(input); for (true) { match (next(&it)) { @@ -2270,9 +2275,13 @@ export fn ltrim(input: str, trim: rune...) str = { }; // rtrim — strip trailing runes that occur in `trim`. Borrowed view. +// 0-arg strips ASCII whitespace via [[bytes.rtrim]] (#9). Spread is +// inlined to dodge #40 — see [[ltrim]]. // ref/hare/strings/trim.ha:32. export fn rtrim(input: str, trim: rune...) str = { - if (trim.len == 0) { return input; }; + if (trim.len == 0) { + return fromutf8_unsafe(bytes.rtrim(toutf8(input), whitespace[0:4]...)); + }; let it: iterator = riter(input); for (true) { match (next(&it)) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 820ca563..f8a0ea17 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -1840,10 +1840,6 @@ export fn position(d: *decoder) i32 = { // // Documented divergences from Hare: // -// - `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). // - `byteindex` / `rbyteindex` rune arms encode via // `utf8.encoderune`; the legacy impls scanned for `r: u8` (an // undocumented ASCII-only restriction that silently dropped @@ -2239,12 +2235,21 @@ export fn trimsuffix(input: str, suffix: str) str = { return r; }; +// whitespace — ASCII whitespace set used by the 0-arg ltrim/rtrim/trim +// branches (#9). ref/hare/strings/trim.ha:6. +let whitespace: [4]u8 = [0x20u8, 0x0Au8, 0x09u8, 0x0Du8]; + // 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. +// 0-arg strips ASCII whitespace via [[bytes.ltrim]] (#9). +// ref/hare/strings/trim.ha:11. The spread expression is inlined +// because `let ws: []u8 = whitespace[0:4]` produces a slice whose +// ptr doesn't track the module-level array storage (filed as #40); +// `b.flush = flushdefault[0:1]` in lib/bufio is the same shape via +// the working field-assign path. export fn ltrim(input: str, trim: rune...) str = { - if (trim.len == 0) { return input; }; + if (trim.len == 0) { + return fromutf8_unsafe(bytes.ltrim(toutf8(input), whitespace[0:4]...)); + }; let it: iterator = iter(input); for (true) { match (next(&it)) { @@ -2270,9 +2275,13 @@ export fn ltrim(input: str, trim: rune...) str = { }; // rtrim — strip trailing runes that occur in `trim`. Borrowed view. +// 0-arg strips ASCII whitespace via [[bytes.rtrim]] (#9). Spread is +// inlined to dodge #40 — see [[ltrim]]. // ref/hare/strings/trim.ha:32. export fn rtrim(input: str, trim: rune...) str = { - if (trim.len == 0) { return input; }; + if (trim.len == 0) { + return fromutf8_unsafe(bytes.rtrim(toutf8(input), whitespace[0:4]...)); + }; let it: iterator = riter(input); for (true) { match (next(&it)) { diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index d6e21a4d..4f3b364f 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -1731,10 +1731,6 @@ export fn position(d: *decoder) i32 = { // // Documented divergences from Hare: // -// - `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). // - `byteindex` / `rbyteindex` rune arms encode via // `utf8.encoderune`; the legacy impls scanned for `r: u8` (an // undocumented ASCII-only restriction that silently dropped @@ -2130,12 +2126,21 @@ export fn trimsuffix(input: str, suffix: str) str = { return r; }; +// whitespace — ASCII whitespace set used by the 0-arg ltrim/rtrim/trim +// branches (#9). ref/hare/strings/trim.ha:6. +let whitespace: [4]u8 = [0x20u8, 0x0Au8, 0x09u8, 0x0Du8]; + // 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. +// 0-arg strips ASCII whitespace via [[bytes.ltrim]] (#9). +// ref/hare/strings/trim.ha:11. The spread expression is inlined +// because `let ws: []u8 = whitespace[0:4]` produces a slice whose +// ptr doesn't track the module-level array storage (filed as #40); +// `b.flush = flushdefault[0:1]` in lib/bufio is the same shape via +// the working field-assign path. export fn ltrim(input: str, trim: rune...) str = { - if (trim.len == 0) { return input; }; + if (trim.len == 0) { + return fromutf8_unsafe(bytes.ltrim(toutf8(input), whitespace[0:4]...)); + }; let it: iterator = iter(input); for (true) { match (next(&it)) { @@ -2161,9 +2166,13 @@ export fn ltrim(input: str, trim: rune...) str = { }; // rtrim — strip trailing runes that occur in `trim`. Borrowed view. +// 0-arg strips ASCII whitespace via [[bytes.rtrim]] (#9). Spread is +// inlined to dodge #40 — see [[ltrim]]. // ref/hare/strings/trim.ha:32. export fn rtrim(input: str, trim: rune...) str = { - if (trim.len == 0) { return input; }; + if (trim.len == 0) { + return fromutf8_unsafe(bytes.rtrim(toutf8(input), whitespace[0:4]...)); + }; let it: iterator = riter(input); for (true) { match (next(&it)) {