lib/ascii: add strlower/strupper

Port ref/hare/ascii/string.ha strlower/strupper as the allocating entry
points: byte-wise ASCII case fold, equivalent to Hare's rune fold since
case-folding only touches bytes <0x80 and every UTF-8 multibyte byte is
>=0x80 (passes through unchanged, length-preserving). nomem arises only
from the allocation's `?`.

strlower_buf/strupper_buf are deferred: ww has no nomem-value form or
capacity-bounded static-append to express Hare's too-small-buffer path
(#230); restore the two-tier delegation when those land.

Divergence (rule 7): the empty-input fast path returns a nil/0 str
because ww's alloc([], 0) routes through nomem, whereas Hare allocs a
zero-length buffer and zero-loops; documented at the bypass site.

Test vectors mirror Hare's @test (ABC/abc/[[[/こ/empty/aB1z). Adds
lib/ascii/asciitest.ww + test/wcc/904_ascii_run.c (registered in the
Makefile TESTS list and a build rule). Regenerates the ascii-embedding
selfhost combined.ww amalgams (#110 freshness); the wwdump amalgam also
reorders the ascii block after strings to satisfy the new import edge.
This commit is contained in:
2026-06-01 09:15:18 +09:00
parent 8481a05c3a
commit 07fed80fab
7 changed files with 558 additions and 275 deletions

View File

@@ -5,6 +5,8 @@
package ascii;
import strings;
export fn isdigit(c: rune) bool = {
if (c < 48) { return false; };
if (c > 57) { return false; };
@@ -134,3 +136,48 @@ export fn strcasecmp(a: str, b: str) i32 = {
};
return a.len - b.len;
};
// 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)
if (s.len == 0) {
let r: str;
r.ptr = nil;
r.len = 0;
return r;
};
let buf: []u8 = alloc([], s.len: u64)?;
let i: i32 = 0;
for (i < s.len) {
buf.ptr[i] = tolower(s[i]: rune): u8;
i += 1;
};
buf.len = s.len;
return strings.frombytes(buf);
};
// strupper — ASCII-uppercased copy of s, newly allocated.
// ref/hare/ascii/string.ha:33.
export fn strupper(s: str) (str | nomem) = {
if (s.len == 0) {
let r: str;
r.ptr = nil;
r.len = 0;
return r;
};
let buf: []u8 = alloc([], s.len: u64)?;
let i: i32 = 0;
for (i < s.len) {
buf.ptr[i] = toupper(s[i]: rune): u8;
i += 1;
};
buf.len = s.len;
return strings.frombytes(buf);
};

41
lib/ascii/asciitest.ww Normal file
View File

@@ -0,0 +1,41 @@
// asciitest — exercises lib/ascii case folding. Run with
// `ww run lib/ascii/asciitest.ww`. Same signalled-then-fail()-with-+10
// pattern as bytestest: a non-zero exit pinpoints the failing scenario.
//
// Vectors mirror Hare's @test fn strcasecmp in ref/hare/ascii/string.ha.
package ascii;
import ascii;
import os;
let signalled: i32 = 0;
fn fail() void = { os.exit(signalled + 10); };
// checkfold — one table row: strlower(in) == lo and strupper(in) == up.
fn checkfold(in: str, lo: str, up: str) void = {
match (ascii.strlower(in)) {
case let got: str => { if (got != lo) { fail(); }; };
case nomem => { fail(); };
};
match (ascii.strupper(in)) {
case let got: str => { if (got != up) { fail(); }; };
case nomem => { fail(); };
};
};
// ref/hare/ascii/string.ha:70 case-fold vectors. The こ row pins that a
// UTF-8 multibyte sequence (all bytes >=0x80) passes through unchanged.
@test fn strfold_cases() void = {
signalled = 100; checkfold("ABC", "abc", "ABC");
signalled = 101; checkfold("abc", "abc", "ABC");
signalled = 102; checkfold("[[[", "[[[", "[[[");
signalled = 103; checkfold("こ", "こ", "こ");
signalled = 104; checkfold("", "", "");
signalled = 105; checkfold("aB1z", "ab1z", "AB1Z");
};
export fn main() i32 = {
signalled = 1; strfold_cases();
return 0;
};