From 2b893b9353921644b760c2ad480a997f1fc5897d Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 1 Jun 2026 16:00:33 +0900 Subject: [PATCH] lib/ascii: add strlower_buf/strupper_buf (#11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore Hare's two-tier delegation: strlower/strupper alloc a buffer then delegate to strlower_buf/strupper_buf, which fold ASCII case into a caller-provided buffer. Too-small buffer returns nomem via the `let nm: nomem` value form. ref/hare/ascii/string.ha:21,43. Regen w6c/wwdump/smoke combined.ww — all three embed lib/ascii. --- lib/ascii/ascii.ww | 32 ++++++++++++++++++----- lib/ascii/asciitest.ww | 38 ++++++++++++++++++++++++++++ selfhost/cmd/w6c/main.combined.ww | 32 ++++++++++++++++++----- selfhost/cmd/wwdump/main.combined.ww | 32 ++++++++++++++++++----- selfhost/test/smoke.combined.ww | 32 ++++++++++++++++++----- 5 files changed, 142 insertions(+), 24 deletions(-) diff --git a/lib/ascii/ascii.ww b/lib/ascii/ascii.ww index 5f4de9e1..17d84a57 100644 --- a/lib/ascii/ascii.ww +++ b/lib/ascii/ascii.ww @@ -138,15 +138,10 @@ export fn strcasecmp(a: str, b: str) i32 = { }; // strlower — ASCII-lowercased copy of s, newly allocated. -// Byte-wise fold: ASCII case-fold only touches bytes <0x80; UTF-8 -// multibyte bytes are >=0x80 and pass through unchanged, so byte-wise -// equals Hare's rune fold and is length-preserving. -// _buf variants deferred — ww has no nomem-value form / static-append -// builtin; restore Hare's two-tier delegation when they land (#230). // ref/hare/ascii/string.ha:11. export fn strlower(s: str) (str | nomem) = { // empty bypass: ww alloc([],0) routes through nomem; Hare allocs 0 - // and zero-loops (ref/hare/ascii/string.ha) + // and zero-loops (ref/hare/ascii/string.ha:12). if (s.len == 0) { let r: str; r.ptr = nil; @@ -154,6 +149,22 @@ export fn strlower(s: str) (str | nomem) = { return r; }; let buf: []u8 = alloc([], s.len: u64)?; + return strlower_buf(s, buf); +}; + +// strlower_buf — ASCII-lowercase s into buf (overwrites). nomem if buf +// too small. ref/hare/ascii/string.ha:21. +// Byte-wise fold: ASCII case-fold only touches bytes <0x80; UTF-8 +// multibyte bytes are >=0x80 and pass through unchanged, so byte-wise +// equals Hare's rune fold and is length-preserving. +// ww uses an explicit `buf.cap < s.len` check + `let nm: nomem` value +// because it has no static-append builtin; Hare reaches the same +// nomem-on-too-small via `static append(buf, ...)?` (string.ha:25). +export fn strlower_buf(s: str, buf: []u8) (str | nomem) = { + if (buf.cap < s.len) { + let nm: nomem; + return nm; + }; let i: i32 = 0; for (i < s.len) { buf.ptr[i] = tolower(s[i]: rune): u8; @@ -173,6 +184,15 @@ export fn strupper(s: str) (str | nomem) = { return r; }; let buf: []u8 = alloc([], s.len: u64)?; + return strupper_buf(s, buf); +}; + +// strupper_buf — see strlower_buf. ref/hare/ascii/string.ha:43. +export fn strupper_buf(s: str, buf: []u8) (str | nomem) = { + if (buf.cap < s.len) { + let nm: nomem; + return nm; + }; let i: i32 = 0; for (i < s.len) { buf.ptr[i] = toupper(s[i]: rune): u8; diff --git a/lib/ascii/asciitest.ww b/lib/ascii/asciitest.ww index e5c30fce..101353c9 100644 --- a/lib/ascii/asciitest.ww +++ b/lib/ascii/asciitest.ww @@ -35,7 +35,45 @@ fn checkfold(in: str, lo: str, up: str) void = { signalled = 105; checkfold("aB1z", "ab1z", "AB1Z"); }; +// checkbuf — strlower_buf/strupper_buf into an exactly-sized buf. +fn checkbuf(in: str, lo: str, up: str) void = { + let lstore: [16]u8; let lbuf: []u8 = lstore[0:16]; lbuf.len = 0; + match (ascii.strlower_buf(in, lbuf)) { + case let got: str => { if (got != lo) { fail(); }; }; + case nomem => { fail(); }; + }; + let ustore: [16]u8; let ubuf: []u8 = ustore[0:16]; ubuf.len = 0; + match (ascii.strupper_buf(in, ubuf)) { + case let got: str => { if (got != up) { fail(); }; }; + case nomem => { fail(); }; + }; +}; + +@test fn strfold_buf_cases() void = { + signalled = 110; checkbuf("ABC", "abc", "ABC"); + signalled = 111; checkbuf("aB1z", "ab1z", "AB1Z"); + signalled = 112; checkbuf("", "", ""); + // non-ASCII pins UTF-8 multibyte passthrough through the _buf path + // directly (all bytes >=0x80, untouched by the byte-wise fold). + signalled = 113; checkbuf("こ", "こ", "こ"); + // cap exactly == s.len must succeed — pins the `<` boundary in the + // buf.cap check (a `<=` off-by-one would wrongly return nomem here). + signalled = 114; + let exact: [3]u8; let ebuf: []u8 = exact[0:3]; ebuf.len = 0; + match (ascii.strlower_buf("ABC", ebuf)) { + case let got: str => { if (got != "abc") { fail(); }; }; + case nomem => { fail(); }; + }; + signalled = 115; + let small: [2]u8; let sbuf: []u8 = small[0:2]; sbuf.len = 0; + match (ascii.strlower_buf("ABC", sbuf)) { + case let got: str => { fail(); }; + case nomem => void; + }; +}; + export fn main() i32 = { signalled = 1; strfold_cases(); + signalled = 2; strfold_buf_cases(); return 0; }; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 44ff7b72..b02fd0bd 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -4477,15 +4477,10 @@ export fn strcasecmp(a: str, b: str) i32 = { }; // strlower — ASCII-lowercased copy of s, newly allocated. -// Byte-wise fold: ASCII case-fold only touches bytes <0x80; UTF-8 -// multibyte bytes are >=0x80 and pass through unchanged, so byte-wise -// equals Hare's rune fold and is length-preserving. -// _buf variants deferred — ww has no nomem-value form / static-append -// builtin; restore Hare's two-tier delegation when they land (#230). // ref/hare/ascii/string.ha:11. export fn strlower(s: str) (str | nomem) = { // empty bypass: ww alloc([],0) routes through nomem; Hare allocs 0 - // and zero-loops (ref/hare/ascii/string.ha) + // and zero-loops (ref/hare/ascii/string.ha:12). if (s.len == 0) { let r: str; r.ptr = nil; @@ -4493,6 +4488,22 @@ export fn strlower(s: str) (str | nomem) = { return r; }; let buf: []u8 = alloc([], s.len: u64)?; + return strlower_buf(s, buf); +}; + +// strlower_buf — ASCII-lowercase s into buf (overwrites). nomem if buf +// too small. ref/hare/ascii/string.ha:21. +// Byte-wise fold: ASCII case-fold only touches bytes <0x80; UTF-8 +// multibyte bytes are >=0x80 and pass through unchanged, so byte-wise +// equals Hare's rune fold and is length-preserving. +// ww uses an explicit `buf.cap < s.len` check + `let nm: nomem` value +// because it has no static-append builtin; Hare reaches the same +// nomem-on-too-small via `static append(buf, ...)?` (string.ha:25). +export fn strlower_buf(s: str, buf: []u8) (str | nomem) = { + if (buf.cap < s.len) { + let nm: nomem; + return nm; + }; let i: i32 = 0; for (i < s.len) { buf.ptr[i] = tolower(s[i]: rune): u8; @@ -4512,6 +4523,15 @@ export fn strupper(s: str) (str | nomem) = { return r; }; let buf: []u8 = alloc([], s.len: u64)?; + return strupper_buf(s, buf); +}; + +// strupper_buf — see strlower_buf. ref/hare/ascii/string.ha:43. +export fn strupper_buf(s: str, buf: []u8) (str | nomem) = { + if (buf.cap < s.len) { + let nm: nomem; + return nm; + }; let i: i32 = 0; for (i < s.len) { buf.ptr[i] = toupper(s[i]: rune): u8; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index e39a37cb..f55b4e42 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -4477,15 +4477,10 @@ export fn strcasecmp(a: str, b: str) i32 = { }; // strlower — ASCII-lowercased copy of s, newly allocated. -// Byte-wise fold: ASCII case-fold only touches bytes <0x80; UTF-8 -// multibyte bytes are >=0x80 and pass through unchanged, so byte-wise -// equals Hare's rune fold and is length-preserving. -// _buf variants deferred — ww has no nomem-value form / static-append -// builtin; restore Hare's two-tier delegation when they land (#230). // ref/hare/ascii/string.ha:11. export fn strlower(s: str) (str | nomem) = { // empty bypass: ww alloc([],0) routes through nomem; Hare allocs 0 - // and zero-loops (ref/hare/ascii/string.ha) + // and zero-loops (ref/hare/ascii/string.ha:12). if (s.len == 0) { let r: str; r.ptr = nil; @@ -4493,6 +4488,22 @@ export fn strlower(s: str) (str | nomem) = { return r; }; let buf: []u8 = alloc([], s.len: u64)?; + return strlower_buf(s, buf); +}; + +// strlower_buf — ASCII-lowercase s into buf (overwrites). nomem if buf +// too small. ref/hare/ascii/string.ha:21. +// Byte-wise fold: ASCII case-fold only touches bytes <0x80; UTF-8 +// multibyte bytes are >=0x80 and pass through unchanged, so byte-wise +// equals Hare's rune fold and is length-preserving. +// ww uses an explicit `buf.cap < s.len` check + `let nm: nomem` value +// because it has no static-append builtin; Hare reaches the same +// nomem-on-too-small via `static append(buf, ...)?` (string.ha:25). +export fn strlower_buf(s: str, buf: []u8) (str | nomem) = { + if (buf.cap < s.len) { + let nm: nomem; + return nm; + }; let i: i32 = 0; for (i < s.len) { buf.ptr[i] = tolower(s[i]: rune): u8; @@ -4512,6 +4523,15 @@ export fn strupper(s: str) (str | nomem) = { return r; }; let buf: []u8 = alloc([], s.len: u64)?; + return strupper_buf(s, buf); +}; + +// strupper_buf — see strlower_buf. ref/hare/ascii/string.ha:43. +export fn strupper_buf(s: str, buf: []u8) (str | nomem) = { + if (buf.cap < s.len) { + let nm: nomem; + return nm; + }; let i: i32 = 0; for (i < s.len) { buf.ptr[i] = toupper(s[i]: rune): u8; diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index 6721b053..d043652b 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -4477,15 +4477,10 @@ export fn strcasecmp(a: str, b: str) i32 = { }; // strlower — ASCII-lowercased copy of s, newly allocated. -// Byte-wise fold: ASCII case-fold only touches bytes <0x80; UTF-8 -// multibyte bytes are >=0x80 and pass through unchanged, so byte-wise -// equals Hare's rune fold and is length-preserving. -// _buf variants deferred — ww has no nomem-value form / static-append -// builtin; restore Hare's two-tier delegation when they land (#230). // ref/hare/ascii/string.ha:11. export fn strlower(s: str) (str | nomem) = { // empty bypass: ww alloc([],0) routes through nomem; Hare allocs 0 - // and zero-loops (ref/hare/ascii/string.ha) + // and zero-loops (ref/hare/ascii/string.ha:12). if (s.len == 0) { let r: str; r.ptr = nil; @@ -4493,6 +4488,22 @@ export fn strlower(s: str) (str | nomem) = { return r; }; let buf: []u8 = alloc([], s.len: u64)?; + return strlower_buf(s, buf); +}; + +// strlower_buf — ASCII-lowercase s into buf (overwrites). nomem if buf +// too small. ref/hare/ascii/string.ha:21. +// Byte-wise fold: ASCII case-fold only touches bytes <0x80; UTF-8 +// multibyte bytes are >=0x80 and pass through unchanged, so byte-wise +// equals Hare's rune fold and is length-preserving. +// ww uses an explicit `buf.cap < s.len` check + `let nm: nomem` value +// because it has no static-append builtin; Hare reaches the same +// nomem-on-too-small via `static append(buf, ...)?` (string.ha:25). +export fn strlower_buf(s: str, buf: []u8) (str | nomem) = { + if (buf.cap < s.len) { + let nm: nomem; + return nm; + }; let i: i32 = 0; for (i < s.len) { buf.ptr[i] = tolower(s[i]: rune): u8; @@ -4512,6 +4523,15 @@ export fn strupper(s: str) (str | nomem) = { return r; }; let buf: []u8 = alloc([], s.len: u64)?; + return strupper_buf(s, buf); +}; + +// strupper_buf — see strlower_buf. ref/hare/ascii/string.ha:43. +export fn strupper_buf(s: str, buf: []u8) (str | nomem) = { + if (buf.cap < s.len) { + let nm: nomem; + return nm; + }; let i: i32 = 0; for (i < s.len) { buf.ptr[i] = toupper(s[i]: rune): u8;