From 8683202a4c4158a6811e84a764b23b31b5f7cb98 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 19 May 2026 16:19:12 +0900 Subject: [PATCH] lib/bytes+test: port ltrim/rtrim/trim from Hare (u8... variadic) --- lib/bytes/bytes.ww | 34 +++++++++ lib/bytes/bytestest.ww | 104 +++++++++++++++++++++++++++ selfhost/cmd/w6c/main.combined.ww | 34 +++++++++ selfhost/cmd/wwdump/main.combined.ww | 34 +++++++++ selfhost/test/smoke.combined.ww | 34 +++++++++ 5 files changed, 240 insertions(+) diff --git a/lib/bytes/bytes.ww b/lib/bytes/bytes.ww index 03097fa1..00419e51 100644 --- a/lib/bytes/bytes.ww +++ b/lib/bytes/bytes.ww @@ -142,6 +142,40 @@ export fn contains(s: []u8, needles: (u8 | []u8)...) bool = { return false; }; +// ltrim — borrowed view of `in` with leading bytes in `trim` stripped. +// `trim` must be non-empty. ref/hare/bytes/trim.ha:7. +export fn ltrim(in: []u8, trim: u8...) []u8 = { + os.assert(trim.len > 0, "bytes.ltrim called with empty trim set"); + let i: i32 = 0; + for (i < in.len && contains(trim, in[i])) { i += 1; }; + let r: []u8; + r.ptr = in.ptr + (i: u64); + r.len = in.len - i; + r.cap = r.len; + return r; +}; + +// rtrim — borrowed view of `in` with trailing bytes in `trim` stripped. +// `trim` must be non-empty. ref/hare/bytes/trim.ha:17. Hare's loop uses +// `size` underflow at i==0 to terminate; ww indices are signed i32, so +// the equivalent termination is spelled `i >= 0` explicitly. +export fn rtrim(in: []u8, trim: u8...) []u8 = { + os.assert(trim.len > 0, "bytes.rtrim called with empty trim set"); + let i: i32 = in.len - 1; + for (i >= 0 && contains(trim, in[i])) { i -= 1; }; + let r: []u8; + r.ptr = in.ptr; + r.len = i + 1; + r.cap = r.len; + return r; +}; + +// trim — borrowed view of `in` with both ends in `trim` stripped. +// ref/hare/bytes/trim.ha:27. +export fn trim(in: []u8, trim: u8...) []u8 = { + return ltrim(rtrim(in, trim...), trim...); +}; + // hasprefix — true iff `s` starts with `pre`. // ref/hare/bytes/contains.ha:21. export fn hasprefix(s: []u8, pre: []u8) bool = { diff --git a/lib/bytes/bytestest.ww b/lib/bytes/bytestest.ww index 9a0288df..a2d1cc45 100644 --- a/lib/bytes/bytestest.ww +++ b/lib/bytes/bytestest.ww @@ -467,6 +467,107 @@ fn expect_done(t: *bytes.tokenizer) void = { if (!bytes.equal(r2, e_12[0:2])) { fail(); }; }; +// ---- ltrim / rtrim / trim --------------------------------------------- +// ref/hare/bytes/trim.ha:29 — Hare's @test fn trim pins +// `trim([0,1,2,3,5,0], 0) == [1,2,3,5]`, `trim([0,0,0], 0) == []`, +// `trim([], 0) == []`. ww spreads the matrix across ltrim/rtrim/trim. + +fn beq(got: []u8, want: []u8) bool = { + if (got.len != want.len) { return false; }; + let i: i32 = 0; + for (i < got.len) { + if (got[i] != want[i]) { return false; }; + i += 1; + }; + return true; +}; + +@test fn ltrim_cases() void = { + let z: [1]u8; + + // [0,0,1,2] / 0 -> [1,2] + signalled = 1780; + let a: [4]u8; a[0] = 0u8; a[1] = 0u8; a[2] = 1u8; a[3] = 2u8; + let ea: [2]u8; ea[0] = 1u8; ea[1] = 2u8; + if (!beq(bytes.ltrim(a[0:4], 0u8), ea[0:2])) { fail(); }; + + // [1,2,3] / 0 -> [1,2,3] (no leading match) + signalled = 1781; + let b: [3]u8; b[0] = 1u8; b[1] = 2u8; b[2] = 3u8; + if (!beq(bytes.ltrim(b[0:3], 0u8), b[0:3])) { fail(); }; + + // [0,0,0] / 0 -> [] (full match) + signalled = 1782; + let c: [3]u8; + if (!beq(bytes.ltrim(c[0:3], 0u8), z[0:0])) { fail(); }; + + // [] / 0 -> [] (empty input) + signalled = 1783; + if (!beq(bytes.ltrim(z[0:0], 0u8), z[0:0])) { fail(); }; +}; + +@test fn rtrim_cases() void = { + let z: [1]u8; + + // [1,2,0,0] / 0 -> [1,2] + signalled = 1790; + let a: [4]u8; a[0] = 1u8; a[1] = 2u8; a[2] = 0u8; a[3] = 0u8; + let ea: [2]u8; ea[0] = 1u8; ea[1] = 2u8; + if (!beq(bytes.rtrim(a[0:4], 0u8), ea[0:2])) { fail(); }; + + // [1,2,3] / 0 -> [1,2,3] (no trailing match) + signalled = 1791; + let b: [3]u8; b[0] = 1u8; b[1] = 2u8; b[2] = 3u8; + if (!beq(bytes.rtrim(b[0:3], 0u8), b[0:3])) { fail(); }; + + // [0,0,0] / 0 -> [] (full match) + signalled = 1792; + let c: [3]u8; + if (!beq(bytes.rtrim(c[0:3], 0u8), z[0:0])) { fail(); }; + + // [] / 0 -> [] (empty input) + signalled = 1793; + if (!beq(bytes.rtrim(z[0:0], 0u8), z[0:0])) { fail(); }; +}; + +@test fn trim_cases() void = { + let z: [1]u8; + + // [0,1,2,3,5,0] / 0 -> [1,2,3,5] + signalled = 1800; + let a: [6]u8; + a[0] = 0u8; a[1] = 1u8; a[2] = 2u8; + a[3] = 3u8; a[4] = 5u8; a[5] = 0u8; + let ea: [4]u8; ea[0] = 1u8; ea[1] = 2u8; ea[2] = 3u8; ea[3] = 5u8; + if (!beq(bytes.trim(a[0:6], 0u8), ea[0:4])) { fail(); }; + + // [0,5,0] / 5 -> [0,5,0] (only 5 in trim set; boundary mismatch) + signalled = 1801; + let b: [3]u8; b[0] = 0u8; b[1] = 5u8; b[2] = 0u8; + if (!beq(bytes.trim(b[0:3], 5u8), b[0:3])) { fail(); }; + + // [0,1,42,1,0] / {0,42} -> [1,42,1] (multi-byte trim set) + signalled = 1802; + let c: [5]u8; + c[0] = 0u8; c[1] = 1u8; c[2] = 42u8; c[3] = 1u8; c[4] = 0u8; + let ec: [3]u8; ec[0] = 1u8; ec[1] = 42u8; ec[2] = 1u8; + if (!beq(bytes.trim(c[0:5], 0u8, 42u8), ec[0:3])) { fail(); }; + + // [0,0,0] / 0 -> [] (full match) + signalled = 1803; + let d: [3]u8; + if (!beq(bytes.trim(d[0:3], 0u8), z[0:0])) { fail(); }; + + // [] / 0 -> [] (empty input, Hare ref/hare/bytes/trim.ha:34) + signalled = 1804; + if (!beq(bytes.trim(z[0:0], 0u8), z[0:0])) { fail(); }; + + // [1,2,3,5] / 0 -> [1,2,3,5] (Hare ref/hare/bytes/trim.ha:31) + signalled = 1805; + let e: [4]u8; e[0] = 1u8; e[1] = 2u8; e[2] = 3u8; e[3] = 5u8; + if (!beq(bytes.trim(e[0:4], 0u8), e[0:4])) { fail(); }; +}; + // ---- splitn / rsplitn / split ----------------------------------------- // ref/hare/bytes/tokenize.ha:330 (@test fn split). Hare's table mixes // strings; ww spells the vectors as byte arrays explicitly. @@ -662,5 +763,8 @@ export fn main() i32 = { signalled = 13; splitn_cases(); signalled = 14; rsplitn_cases(); signalled = 15; split_cases(); + signalled = 16; ltrim_cases(); + signalled = 17; rtrim_cases(); + signalled = 18; trim_cases(); return 0; }; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 6af4b935..d052f69a 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -1023,6 +1023,40 @@ export fn contains(s: []u8, needles: (u8 | []u8)...) bool = { return false; }; +// ltrim — borrowed view of `in` with leading bytes in `trim` stripped. +// `trim` must be non-empty. ref/hare/bytes/trim.ha:7. +export fn ltrim(in: []u8, trim: u8...) []u8 = { + os.assert(trim.len > 0, "bytes.ltrim called with empty trim set"); + let i: i32 = 0; + for (i < in.len && contains(trim, in[i])) { i += 1; }; + let r: []u8; + r.ptr = in.ptr + (i: u64); + r.len = in.len - i; + r.cap = r.len; + return r; +}; + +// rtrim — borrowed view of `in` with trailing bytes in `trim` stripped. +// `trim` must be non-empty. ref/hare/bytes/trim.ha:17. Hare's loop uses +// `size` underflow at i==0 to terminate; ww indices are signed i32, so +// the equivalent termination is spelled `i >= 0` explicitly. +export fn rtrim(in: []u8, trim: u8...) []u8 = { + os.assert(trim.len > 0, "bytes.rtrim called with empty trim set"); + let i: i32 = in.len - 1; + for (i >= 0 && contains(trim, in[i])) { i -= 1; }; + let r: []u8; + r.ptr = in.ptr; + r.len = i + 1; + r.cap = r.len; + return r; +}; + +// trim — borrowed view of `in` with both ends in `trim` stripped. +// ref/hare/bytes/trim.ha:27. +export fn trim(in: []u8, trim: u8...) []u8 = { + return ltrim(rtrim(in, trim...), trim...); +}; + // hasprefix — true iff `s` starts with `pre`. // ref/hare/bytes/contains.ha:21. export fn hasprefix(s: []u8, pre: []u8) bool = { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 05c0fcea..ea00e91e 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -1023,6 +1023,40 @@ export fn contains(s: []u8, needles: (u8 | []u8)...) bool = { return false; }; +// ltrim — borrowed view of `in` with leading bytes in `trim` stripped. +// `trim` must be non-empty. ref/hare/bytes/trim.ha:7. +export fn ltrim(in: []u8, trim: u8...) []u8 = { + os.assert(trim.len > 0, "bytes.ltrim called with empty trim set"); + let i: i32 = 0; + for (i < in.len && contains(trim, in[i])) { i += 1; }; + let r: []u8; + r.ptr = in.ptr + (i: u64); + r.len = in.len - i; + r.cap = r.len; + return r; +}; + +// rtrim — borrowed view of `in` with trailing bytes in `trim` stripped. +// `trim` must be non-empty. ref/hare/bytes/trim.ha:17. Hare's loop uses +// `size` underflow at i==0 to terminate; ww indices are signed i32, so +// the equivalent termination is spelled `i >= 0` explicitly. +export fn rtrim(in: []u8, trim: u8...) []u8 = { + os.assert(trim.len > 0, "bytes.rtrim called with empty trim set"); + let i: i32 = in.len - 1; + for (i >= 0 && contains(trim, in[i])) { i -= 1; }; + let r: []u8; + r.ptr = in.ptr; + r.len = i + 1; + r.cap = r.len; + return r; +}; + +// trim — borrowed view of `in` with both ends in `trim` stripped. +// ref/hare/bytes/trim.ha:27. +export fn trim(in: []u8, trim: u8...) []u8 = { + return ltrim(rtrim(in, trim...), trim...); +}; + // hasprefix — true iff `s` starts with `pre`. // ref/hare/bytes/contains.ha:21. export fn hasprefix(s: []u8, pre: []u8) bool = { diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index 1cab3412..e3eba485 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -914,6 +914,40 @@ export fn contains(s: []u8, needles: (u8 | []u8)...) bool = { return false; }; +// ltrim — borrowed view of `in` with leading bytes in `trim` stripped. +// `trim` must be non-empty. ref/hare/bytes/trim.ha:7. +export fn ltrim(in: []u8, trim: u8...) []u8 = { + os.assert(trim.len > 0, "bytes.ltrim called with empty trim set"); + let i: i32 = 0; + for (i < in.len && contains(trim, in[i])) { i += 1; }; + let r: []u8; + r.ptr = in.ptr + (i: u64); + r.len = in.len - i; + r.cap = r.len; + return r; +}; + +// rtrim — borrowed view of `in` with trailing bytes in `trim` stripped. +// `trim` must be non-empty. ref/hare/bytes/trim.ha:17. Hare's loop uses +// `size` underflow at i==0 to terminate; ww indices are signed i32, so +// the equivalent termination is spelled `i >= 0` explicitly. +export fn rtrim(in: []u8, trim: u8...) []u8 = { + os.assert(trim.len > 0, "bytes.rtrim called with empty trim set"); + let i: i32 = in.len - 1; + for (i >= 0 && contains(trim, in[i])) { i -= 1; }; + let r: []u8; + r.ptr = in.ptr; + r.len = i + 1; + r.cap = r.len; + return r; +}; + +// trim — borrowed view of `in` with both ends in `trim` stripped. +// ref/hare/bytes/trim.ha:27. +export fn trim(in: []u8, trim: u8...) []u8 = { + return ltrim(rtrim(in, trim...), trim...); +}; + // hasprefix — true iff `s` starts with `pre`. // ref/hare/bytes/contains.ha:21. export fn hasprefix(s: []u8, pre: []u8) bool = {