From e5c1d6baa42f46046af1956a96fdd862a2c0c15b Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 19 May 2026 16:01:25 +0900 Subject: [PATCH] lib/strings+test: port lpad/rpad from Hare --- lib/strings/strings.ww | 63 +++++++++++++++ lib/strings/stringstest.ww | 112 +++++++++++++++++++++++++++ selfhost/cmd/w6c/main.combined.ww | 63 +++++++++++++++ selfhost/cmd/wwdump/main.combined.ww | 63 +++++++++++++++ selfhost/test/smoke.combined.ww | 63 +++++++++++++++ 5 files changed, 364 insertions(+) diff --git a/lib/strings/strings.ww b/lib/strings/strings.ww index dc35e7f2..e5395948 100644 --- a/lib/strings/strings.ww +++ b/lib/strings/strings.ww @@ -724,3 +724,66 @@ export fn rsplitn(in: str, delim: str, n: i32) []str = { export fn split(in: str, delim: str) []str = { return splitn(in, delim, types.I32_MAX); }; + +// lpad — left-pad `s` with `p` rune until the result reaches `maxlen` +// bytes. Length comparison is BYTES, mirroring Hare's `len(s) >= maxlen` +// at ref/hare/strings/pad.ha:9. A multibyte `p` whose encoded width +// doesn't divide `maxlen - s.len` evenly leaves a trailing pad byte +// pair sliced mid-codepoint at byte `maxlen-1`, exactly as Hare's +// `res[..maxlen]` does (ref/hare/strings/pad.ha:20). When +// `(maxlen - s.len) * pad.len >= maxlen` (multibyte pad overflows the +// budget), `s` is entirely sliced off — same as Hare. Caller releases +// with `os.free(r.ptr, r.len: u64)`. Hare's `nomem` return is dropped: +// `os.alloc` aborts on OOM. Buf size == r.len keeps the free-contract +// shape of [[dup]] / [[concat]] / [[join]]; Hare's `alloc([], maxlen)!` +// over-allocs via append then slices, but Hare's slice-free recovers +// the true capacity from the heap allocator (rt/ensure.ha:24), which +// ww's munmap-based `os.free` cannot do. +export fn lpad(s: str, p: rune, maxlen: i32) str = { + if (s.len >= maxlen) { return dup(s); }; + let scratch: [4]u8; + let pad: []u8 = runebytes(scratch[0:4], p); + let buf: *u8 = os.alloc(maxlen: u64): *u8; + let padwrite: i32 = (maxlen - s.len) * pad.len; + if (padwrite > maxlen) { padwrite = maxlen; }; + let off: i32 = 0; + for (off < padwrite) { + buf[off] = pad.ptr[off % pad.len]; + off += 1; + }; + let k: i32 = 0; + let srem: i32 = maxlen - off; + if (srem > s.len) { srem = s.len; }; + for (k < srem) { + buf[off + k] = s[k]; + k += 1; + }; + let r: str; + r.ptr = buf; + r.len = maxlen; + return r; +}; + +// rpad — right-pad `s` with `p` rune until the result reaches `maxlen` +// bytes. Symmetric with [[lpad]]. ref/hare/strings/pad.ha:39. +export fn rpad(s: str, p: rune, maxlen: i32) str = { + if (s.len >= maxlen) { return dup(s); }; + let scratch: [4]u8; + let pad: []u8 = runebytes(scratch[0:4], p); + let buf: *u8 = os.alloc(maxlen: u64): *u8; + let k: i32 = 0; + for (k < s.len) { + buf[k] = s[k]; + k += 1; + }; + let padwrite: i32 = maxlen - s.len; + let i: i32 = 0; + for (i < padwrite) { + buf[s.len + i] = pad.ptr[i % pad.len]; + i += 1; + }; + let r: str; + r.ptr = buf; + r.len = maxlen; + return r; +}; diff --git a/lib/strings/stringstest.ww b/lib/strings/stringstest.ww index 25e5bd1b..0a519458 100644 --- a/lib/strings/stringstest.ww +++ b/lib/strings/stringstest.ww @@ -1204,6 +1204,116 @@ fn expect_str(toks: []str, i: i32, want: str) void = { os.free(t3.ptr: *void, (t3.cap: u64) * 16u64); }; +// ---- 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. + signalled = 1770; + let r1: str = strings.lpad("2", '0', 5); + if (!streq(r1, "00002")) { fail(); }; + if (r1.len != 5) { fail(); }; + os.free(r1.ptr: *void, r1.len: u64); + + // Hare row 2: s.len == maxlen — early dup path. + signalled = 1771; + let r2: str = strings.lpad("12345", '0', 5); + if (!streq(r2, "12345")) { fail(); }; + if (r2.len != 5) { fail(); }; + os.free(r2.ptr: *void, r2.len: u64); + + // Hare row 3: empty s, full-width pad. + signalled = 1772; + let r3: str = strings.lpad("", '0', 5); + if (!streq(r3, "00000")) { fail(); }; + if (r3.len != 5) { fail(); }; + os.free(r3.ptr: *void, r3.len: u64); + + // ww row 1: s.len > maxlen — early dup path returns input copy. + signalled = 1773; + let r4: str = strings.lpad("abcdef", '_', 3); + if (!streq(r4, "abcdef")) { fail(); }; + if (r4.len != 6) { fail(); }; + 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. + signalled = 1774; + let r5: str = strings.lpad("café", '_', 7); + if (!streq(r5, "__café")) { fail(); }; + if (r5.len != 7) { fail(); }; + 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. + signalled = 1775; + let r6: str = strings.lpad("x", 0x03B1u32: rune, 5); + if (r6.len != 5) { fail(); }; + if (r6.ptr[0] != 0xCEu8) { fail(); }; // α byte 0 + if (r6.ptr[1] != 0xB1u8) { fail(); }; // α byte 1 + if (r6.ptr[2] != 0xCEu8) { fail(); }; // α byte 0 + if (r6.ptr[3] != 0xB1u8) { fail(); }; // α byte 1 + if (r6.ptr[4] != 0xCEu8) { fail(); }; // truncated α byte 0 + os.free(r6.ptr: *void, r6.len: u64); +}; + +@test fn rpad_cases() void = { + // Hare row 1: shorter s, ASCII pad. + signalled = 1780; + let r1: str = strings.rpad("2", '0', 5); + if (!streq(r1, "20000")) { fail(); }; + if (r1.len != 5) { fail(); }; + os.free(r1.ptr: *void, r1.len: u64); + + // Hare row 2: s.len == maxlen — early dup path. + signalled = 1781; + let r2: str = strings.rpad("12345", '0', 5); + if (!streq(r2, "12345")) { fail(); }; + if (r2.len != 5) { fail(); }; + os.free(r2.ptr: *void, r2.len: u64); + + // Hare row 3: empty s. + signalled = 1782; + let r3: str = strings.rpad("", '0', 5); + if (!streq(r3, "00000")) { fail(); }; + if (r3.len != 5) { fail(); }; + os.free(r3.ptr: *void, r3.len: u64); + + // ww row 1: s.len > maxlen — early dup path. + signalled = 1783; + let r4: str = strings.rpad("abcdef", '_', 3); + if (!streq(r4, "abcdef")) { fail(); }; + if (r4.len != 6) { fail(); }; + os.free(r4.ptr: *void, r4.len: u64); + + // ww row 2: multibyte s — byte-length contract. + signalled = 1784; + let r5: str = strings.rpad("café", '_', 7); + if (!streq(r5, "café__")) { fail(); }; + if (r5.len != 7) { fail(); }; + 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). + signalled = 1785; + let r6: str = strings.rpad("x", 0x03B1u32: rune, 5); + if (r6.len != 5) { fail(); }; + if (r6.ptr[0] != 'x') { fail(); }; + if (r6.ptr[1] != 0xCEu8) { fail(); }; // α byte 0 + if (r6.ptr[2] != 0xB1u8) { fail(); }; // α byte 1 + if (r6.ptr[3] != 0xCEu8) { fail(); }; // α byte 0 + if (r6.ptr[4] != 0xB1u8) { fail(); }; // α byte 1 + os.free(r6.ptr: *void, r6.len: u64); +}; + export fn main() i32 = { signalled = 1; dup_cases(); signalled = 2; concat_cases(); @@ -1242,5 +1352,7 @@ export fn main() i32 = { signalled = 35; splitn_cases(); signalled = 36; rsplitn_cases(); signalled = 37; split_cases(); + signalled = 38; lpad_cases(); + signalled = 39; rpad_cases(); return 0; }; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index ceca605c..6af4b935 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -2522,6 +2522,69 @@ export fn split(in: str, delim: str) []str = { return splitn(in, delim, types.I32_MAX); }; +// lpad — left-pad `s` with `p` rune until the result reaches `maxlen` +// bytes. Length comparison is BYTES, mirroring Hare's `len(s) >= maxlen` +// at ref/hare/strings/pad.ha:9. A multibyte `p` whose encoded width +// doesn't divide `maxlen - s.len` evenly leaves a trailing pad byte +// pair sliced mid-codepoint at byte `maxlen-1`, exactly as Hare's +// `res[..maxlen]` does (ref/hare/strings/pad.ha:20). When +// `(maxlen - s.len) * pad.len >= maxlen` (multibyte pad overflows the +// budget), `s` is entirely sliced off — same as Hare. Caller releases +// with `os.free(r.ptr, r.len: u64)`. Hare's `nomem` return is dropped: +// `os.alloc` aborts on OOM. Buf size == r.len keeps the free-contract +// shape of [[dup]] / [[concat]] / [[join]]; Hare's `alloc([], maxlen)!` +// over-allocs via append then slices, but Hare's slice-free recovers +// the true capacity from the heap allocator (rt/ensure.ha:24), which +// ww's munmap-based `os.free` cannot do. +export fn lpad(s: str, p: rune, maxlen: i32) str = { + if (s.len >= maxlen) { return dup(s); }; + let scratch: [4]u8; + let pad: []u8 = runebytes(scratch[0:4], p); + let buf: *u8 = os.alloc(maxlen: u64): *u8; + let padwrite: i32 = (maxlen - s.len) * pad.len; + if (padwrite > maxlen) { padwrite = maxlen; }; + let off: i32 = 0; + for (off < padwrite) { + buf[off] = pad.ptr[off % pad.len]; + off += 1; + }; + let k: i32 = 0; + let srem: i32 = maxlen - off; + if (srem > s.len) { srem = s.len; }; + for (k < srem) { + buf[off + k] = s[k]; + k += 1; + }; + let r: str; + r.ptr = buf; + r.len = maxlen; + return r; +}; + +// rpad — right-pad `s` with `p` rune until the result reaches `maxlen` +// bytes. Symmetric with [[lpad]]. ref/hare/strings/pad.ha:39. +export fn rpad(s: str, p: rune, maxlen: i32) str = { + if (s.len >= maxlen) { return dup(s); }; + let scratch: [4]u8; + let pad: []u8 = runebytes(scratch[0:4], p); + let buf: *u8 = os.alloc(maxlen: u64): *u8; + let k: i32 = 0; + for (k < s.len) { + buf[k] = s[k]; + k += 1; + }; + let padwrite: i32 = maxlen - s.len; + let i: i32 = 0; + for (i < padwrite) { + buf[s.len + i] = pad.ptr[i % pad.len]; + i += 1; + }; + let r: str; + r.ptr = buf; + r.len = maxlen; + return r; +}; + // strconv — number↔string conversions. // // Mirrors Hare's strconv:: surface. The *tos functions return a diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 6ead08e5..05c0fcea 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -2522,6 +2522,69 @@ export fn split(in: str, delim: str) []str = { return splitn(in, delim, types.I32_MAX); }; +// lpad — left-pad `s` with `p` rune until the result reaches `maxlen` +// bytes. Length comparison is BYTES, mirroring Hare's `len(s) >= maxlen` +// at ref/hare/strings/pad.ha:9. A multibyte `p` whose encoded width +// doesn't divide `maxlen - s.len` evenly leaves a trailing pad byte +// pair sliced mid-codepoint at byte `maxlen-1`, exactly as Hare's +// `res[..maxlen]` does (ref/hare/strings/pad.ha:20). When +// `(maxlen - s.len) * pad.len >= maxlen` (multibyte pad overflows the +// budget), `s` is entirely sliced off — same as Hare. Caller releases +// with `os.free(r.ptr, r.len: u64)`. Hare's `nomem` return is dropped: +// `os.alloc` aborts on OOM. Buf size == r.len keeps the free-contract +// shape of [[dup]] / [[concat]] / [[join]]; Hare's `alloc([], maxlen)!` +// over-allocs via append then slices, but Hare's slice-free recovers +// the true capacity from the heap allocator (rt/ensure.ha:24), which +// ww's munmap-based `os.free` cannot do. +export fn lpad(s: str, p: rune, maxlen: i32) str = { + if (s.len >= maxlen) { return dup(s); }; + let scratch: [4]u8; + let pad: []u8 = runebytes(scratch[0:4], p); + let buf: *u8 = os.alloc(maxlen: u64): *u8; + let padwrite: i32 = (maxlen - s.len) * pad.len; + if (padwrite > maxlen) { padwrite = maxlen; }; + let off: i32 = 0; + for (off < padwrite) { + buf[off] = pad.ptr[off % pad.len]; + off += 1; + }; + let k: i32 = 0; + let srem: i32 = maxlen - off; + if (srem > s.len) { srem = s.len; }; + for (k < srem) { + buf[off + k] = s[k]; + k += 1; + }; + let r: str; + r.ptr = buf; + r.len = maxlen; + return r; +}; + +// rpad — right-pad `s` with `p` rune until the result reaches `maxlen` +// bytes. Symmetric with [[lpad]]. ref/hare/strings/pad.ha:39. +export fn rpad(s: str, p: rune, maxlen: i32) str = { + if (s.len >= maxlen) { return dup(s); }; + let scratch: [4]u8; + let pad: []u8 = runebytes(scratch[0:4], p); + let buf: *u8 = os.alloc(maxlen: u64): *u8; + let k: i32 = 0; + for (k < s.len) { + buf[k] = s[k]; + k += 1; + }; + let padwrite: i32 = maxlen - s.len; + let i: i32 = 0; + for (i < padwrite) { + buf[s.len + i] = pad.ptr[i % pad.len]; + i += 1; + }; + let r: str; + r.ptr = buf; + r.len = maxlen; + return r; +}; + // strconv — number↔string conversions. // // Mirrors Hare's strconv:: surface. The *tos functions return a diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index dcd607a2..1cab3412 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -2413,6 +2413,69 @@ export fn split(in: str, delim: str) []str = { return splitn(in, delim, types.I32_MAX); }; +// lpad — left-pad `s` with `p` rune until the result reaches `maxlen` +// bytes. Length comparison is BYTES, mirroring Hare's `len(s) >= maxlen` +// at ref/hare/strings/pad.ha:9. A multibyte `p` whose encoded width +// doesn't divide `maxlen - s.len` evenly leaves a trailing pad byte +// pair sliced mid-codepoint at byte `maxlen-1`, exactly as Hare's +// `res[..maxlen]` does (ref/hare/strings/pad.ha:20). When +// `(maxlen - s.len) * pad.len >= maxlen` (multibyte pad overflows the +// budget), `s` is entirely sliced off — same as Hare. Caller releases +// with `os.free(r.ptr, r.len: u64)`. Hare's `nomem` return is dropped: +// `os.alloc` aborts on OOM. Buf size == r.len keeps the free-contract +// shape of [[dup]] / [[concat]] / [[join]]; Hare's `alloc([], maxlen)!` +// over-allocs via append then slices, but Hare's slice-free recovers +// the true capacity from the heap allocator (rt/ensure.ha:24), which +// ww's munmap-based `os.free` cannot do. +export fn lpad(s: str, p: rune, maxlen: i32) str = { + if (s.len >= maxlen) { return dup(s); }; + let scratch: [4]u8; + let pad: []u8 = runebytes(scratch[0:4], p); + let buf: *u8 = os.alloc(maxlen: u64): *u8; + let padwrite: i32 = (maxlen - s.len) * pad.len; + if (padwrite > maxlen) { padwrite = maxlen; }; + let off: i32 = 0; + for (off < padwrite) { + buf[off] = pad.ptr[off % pad.len]; + off += 1; + }; + let k: i32 = 0; + let srem: i32 = maxlen - off; + if (srem > s.len) { srem = s.len; }; + for (k < srem) { + buf[off + k] = s[k]; + k += 1; + }; + let r: str; + r.ptr = buf; + r.len = maxlen; + return r; +}; + +// rpad — right-pad `s` with `p` rune until the result reaches `maxlen` +// bytes. Symmetric with [[lpad]]. ref/hare/strings/pad.ha:39. +export fn rpad(s: str, p: rune, maxlen: i32) str = { + if (s.len >= maxlen) { return dup(s); }; + let scratch: [4]u8; + let pad: []u8 = runebytes(scratch[0:4], p); + let buf: *u8 = os.alloc(maxlen: u64): *u8; + let k: i32 = 0; + for (k < s.len) { + buf[k] = s[k]; + k += 1; + }; + let padwrite: i32 = maxlen - s.len; + let i: i32 = 0; + for (i < padwrite) { + buf[s.len + i] = pad.ptr[i % pad.len]; + i += 1; + }; + let r: str; + r.ptr = buf; + r.len = maxlen; + return r; +}; + // strconv — number↔string conversions. // // Mirrors Hare's strconv:: surface. The *tos functions return a