diff --git a/Makefile b/Makefile index 6ed158af..228d9893 100644 --- a/Makefile +++ b/Makefile @@ -405,7 +405,8 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_opaque_assign_cast_run \ $(BIN)/test_sort_run \ $(BIN)/test_bufio_run $(BIN)/test_random_run \ - $(BIN)/test_asserttyped_gap + $(BIN)/test_asserttyped_gap \ + $(BIN)/test_ascii_run $(BIN)/test_smoke: test/wcc/000_smoke.c $(LIB)/libwcc.a | $(BIN) $(CC) $(CFLAGS) $(INCS) -o $@ $< -L$(LIB) -lwcc @@ -1376,6 +1377,10 @@ $(BIN)/test_bytes_run: test/wcc/967_bytes_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_ascii_run: test/wcc/904_ascii_run.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_decimal_run: test/wcc/922_decimal_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/lib/ascii/ascii.ww b/lib/ascii/ascii.ww index 8da21132..5f4de9e1 100644 --- a/lib/ascii/ascii.ww +++ b/lib/ascii/ascii.ww @@ -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); +}; diff --git a/lib/ascii/asciitest.ww b/lib/ascii/asciitest.ww new file mode 100644 index 00000000..e5c30fce --- /dev/null +++ b/lib/ascii/asciitest.ww @@ -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; +}; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 344de431..a4c078a0 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -4290,6 +4290,8 @@ let POW5_TABLE: [26]u64 = [ package ascii; +import strings; + export fn isdigit(c: rune) bool = { if (c < 48) { return false; }; if (c > 57) { return false; }; @@ -4420,6 +4422,51 @@ 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); +}; + // strconv — string-to-float. Mirrors ref/hare/strconv/stof.ha // (Hare in turn adapts Go): Eisel-Lemire fast path [1] with the // Simple-Decimal-Conversion slow path [2] (decimal.ww) as fallback. diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index cff38fc4..7d51e352 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -2306,143 +2306,6 @@ let POW5_TABLE: [26]u64 = [ 59604644775390625u64, 298023223876953125u64, ]; -// ascii — rune-class predicates and case folding for the ASCII range. -// Matches Hare's ascii::isdigit family (rune-taking signature). Runes -// outside 0..127 always answer `false`. The lexer hot path uses these -// inline; they are expected to inline to a couple of compares. - -package ascii; - -export fn isdigit(c: rune) bool = { - if (c < 48) { return false; }; - if (c > 57) { return false; }; - return true; -}; - -export fn isupper(c: rune) bool = { - if (c < 65) { return false; }; - if (c > 90) { return false; }; - return true; -}; - -export fn islower(c: rune) bool = { - if (c < 97) { return false; }; - if (c > 122) { return false; }; - return true; -}; - -export fn isalpha(c: rune) bool = { - if (isupper(c)) { return true; }; - return islower(c); -}; - -export fn isalnum(c: rune) bool = { - if (isalpha(c)) { return true; }; - return isdigit(c); -}; - -// isspace — the C/Hare set: space, tab, NL, VT, FF, CR. -export fn isspace(c: rune) bool = { - if (c == 32) { return true; }; // ' ' - if (c == 9) { return true; }; // '\t' - if (c == 10) { return true; }; // '\n' - if (c == 11) { return true; }; // '\v' - if (c == 12) { return true; }; // '\f' - if (c == 13) { return true; }; // '\r' - return false; -}; - -export fn isxdigit(c: rune) bool = { - if (isdigit(c)) { return true; }; - if (c >= 65) { - if (c <= 70) { return true; }; // 'A'..'F' - }; - if (c >= 97) { - if (c <= 102) { return true; }; // 'a'..'f' - }; - return false; -}; - -// valid — `c` is in the 0..127 ASCII range. -export fn valid(c: rune) bool = { - if (c < 0) { return false; }; - if (c > 127) { return false; }; - return true; -}; - -// validstr — every byte in `s` is ASCII (0..127). -export fn validstr(s: str) bool = { - let i: i32 = 0; - for (i < s.len) { - // High-bit test rather than `> 127u8`; both cgens lower - // the bitwise form identically. The `> u8` form picks - // JA vs JG depending on signed/unsigned dispatch. - if ((s[i] & 128u8) != 0u8) { return false; }; - i += 1; - }; - return true; -}; - -// iscntrl — control chars: 0..31 and 127. -export fn iscntrl(c: rune) bool = { - if (c >= 0) { if (c <= 31) { return true; }; }; - if (c == 127) { return true; }; - return false; -}; - -// isblank — space and tab. -export fn isblank(c: rune) bool = { - if (c == 32) { return true; }; // ' ' - if (c == 9) { return true; }; // '\t' - return false; -}; - -// isprint — printable: space through '~'. -export fn isprint(c: rune) bool = { - if (c < 32) { return false; }; - if (c > 126) { return false; }; - return true; -}; - -// isgraph — printable, non-space. -export fn isgraph(c: rune) bool = { - if (c < 33) { return false; }; - if (c > 126) { return false; }; - return true; -}; - -// ispunct — printable, non-alnum, non-space. -export fn ispunct(c: rune) bool = { - if (!isgraph(c)) { return false; }; - if (isalnum(c)) { return false; }; - return true; -}; - -// tolower / toupper — fold ASCII case. Non-letters pass through. -export fn tolower(c: rune) rune = { - if (isupper(c)) { return c + 32; }; - return c; -}; - -export fn toupper(c: rune) rune = { - if (islower(c)) { return c - 32; }; - return c; -}; - -// strcasecmp — three-way ASCII case-insensitive compare. -export fn strcasecmp(a: str, b: str) i32 = { - let n: i32 = a.len; - if (b.len < n) { n = b.len; }; - let i: i32 = 0; - for (i < n) { - let ca: rune = tolower(a[i]: rune); - let cb: rune = tolower(b[i]: rune); - if (ca != cb) { return (ca - cb): i32; }; - i += 1; - }; - return a.len - b.len; -}; - // types — integer limits. Mirrors Hare's types::limits (I8_MAX, …) // platform-fixed for amd64. Numeric helpers live in lib/math, matching // Hare's split between types::limits and math::. @@ -4420,6 +4283,190 @@ export fn rpad(s: str, p: rune, maxlen: i32) str = { return frombytes(buf); }; +// ascii — rune-class predicates and case folding for the ASCII range. +// Matches Hare's ascii::isdigit family (rune-taking signature). Runes +// outside 0..127 always answer `false`. The lexer hot path uses these +// inline; they are expected to inline to a couple of compares. + +package ascii; + +import strings; + +export fn isdigit(c: rune) bool = { + if (c < 48) { return false; }; + if (c > 57) { return false; }; + return true; +}; + +export fn isupper(c: rune) bool = { + if (c < 65) { return false; }; + if (c > 90) { return false; }; + return true; +}; + +export fn islower(c: rune) bool = { + if (c < 97) { return false; }; + if (c > 122) { return false; }; + return true; +}; + +export fn isalpha(c: rune) bool = { + if (isupper(c)) { return true; }; + return islower(c); +}; + +export fn isalnum(c: rune) bool = { + if (isalpha(c)) { return true; }; + return isdigit(c); +}; + +// isspace — the C/Hare set: space, tab, NL, VT, FF, CR. +export fn isspace(c: rune) bool = { + if (c == 32) { return true; }; // ' ' + if (c == 9) { return true; }; // '\t' + if (c == 10) { return true; }; // '\n' + if (c == 11) { return true; }; // '\v' + if (c == 12) { return true; }; // '\f' + if (c == 13) { return true; }; // '\r' + return false; +}; + +export fn isxdigit(c: rune) bool = { + if (isdigit(c)) { return true; }; + if (c >= 65) { + if (c <= 70) { return true; }; // 'A'..'F' + }; + if (c >= 97) { + if (c <= 102) { return true; }; // 'a'..'f' + }; + return false; +}; + +// valid — `c` is in the 0..127 ASCII range. +export fn valid(c: rune) bool = { + if (c < 0) { return false; }; + if (c > 127) { return false; }; + return true; +}; + +// validstr — every byte in `s` is ASCII (0..127). +export fn validstr(s: str) bool = { + let i: i32 = 0; + for (i < s.len) { + // High-bit test rather than `> 127u8`; both cgens lower + // the bitwise form identically. The `> u8` form picks + // JA vs JG depending on signed/unsigned dispatch. + if ((s[i] & 128u8) != 0u8) { return false; }; + i += 1; + }; + return true; +}; + +// iscntrl — control chars: 0..31 and 127. +export fn iscntrl(c: rune) bool = { + if (c >= 0) { if (c <= 31) { return true; }; }; + if (c == 127) { return true; }; + return false; +}; + +// isblank — space and tab. +export fn isblank(c: rune) bool = { + if (c == 32) { return true; }; // ' ' + if (c == 9) { return true; }; // '\t' + return false; +}; + +// isprint — printable: space through '~'. +export fn isprint(c: rune) bool = { + if (c < 32) { return false; }; + if (c > 126) { return false; }; + return true; +}; + +// isgraph — printable, non-space. +export fn isgraph(c: rune) bool = { + if (c < 33) { return false; }; + if (c > 126) { return false; }; + return true; +}; + +// ispunct — printable, non-alnum, non-space. +export fn ispunct(c: rune) bool = { + if (!isgraph(c)) { return false; }; + if (isalnum(c)) { return false; }; + return true; +}; + +// tolower / toupper — fold ASCII case. Non-letters pass through. +export fn tolower(c: rune) rune = { + if (isupper(c)) { return c + 32; }; + return c; +}; + +export fn toupper(c: rune) rune = { + if (islower(c)) { return c - 32; }; + return c; +}; + +// strcasecmp — three-way ASCII case-insensitive compare. +export fn strcasecmp(a: str, b: str) i32 = { + let n: i32 = a.len; + if (b.len < n) { n = b.len; }; + let i: i32 = 0; + for (i < n) { + let ca: rune = tolower(a[i]: rune); + let cb: rune = tolower(b[i]: rune); + if (ca != cb) { return (ca - cb): i32; }; + i += 1; + }; + 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); +}; + // strconv — string-to-float. Mirrors ref/hare/strconv/stof.ha // (Hare in turn adapts Go): Eisel-Lemire fast path [1] with the // Simple-Decimal-Conversion slow path [2] (decimal.ww) as fallback. diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index 2962d604..62f1599e 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -2306,143 +2306,6 @@ let POW5_TABLE: [26]u64 = [ 59604644775390625u64, 298023223876953125u64, ]; -// ascii — rune-class predicates and case folding for the ASCII range. -// Matches Hare's ascii::isdigit family (rune-taking signature). Runes -// outside 0..127 always answer `false`. The lexer hot path uses these -// inline; they are expected to inline to a couple of compares. - -package ascii; - -export fn isdigit(c: rune) bool = { - if (c < 48) { return false; }; - if (c > 57) { return false; }; - return true; -}; - -export fn isupper(c: rune) bool = { - if (c < 65) { return false; }; - if (c > 90) { return false; }; - return true; -}; - -export fn islower(c: rune) bool = { - if (c < 97) { return false; }; - if (c > 122) { return false; }; - return true; -}; - -export fn isalpha(c: rune) bool = { - if (isupper(c)) { return true; }; - return islower(c); -}; - -export fn isalnum(c: rune) bool = { - if (isalpha(c)) { return true; }; - return isdigit(c); -}; - -// isspace — the C/Hare set: space, tab, NL, VT, FF, CR. -export fn isspace(c: rune) bool = { - if (c == 32) { return true; }; // ' ' - if (c == 9) { return true; }; // '\t' - if (c == 10) { return true; }; // '\n' - if (c == 11) { return true; }; // '\v' - if (c == 12) { return true; }; // '\f' - if (c == 13) { return true; }; // '\r' - return false; -}; - -export fn isxdigit(c: rune) bool = { - if (isdigit(c)) { return true; }; - if (c >= 65) { - if (c <= 70) { return true; }; // 'A'..'F' - }; - if (c >= 97) { - if (c <= 102) { return true; }; // 'a'..'f' - }; - return false; -}; - -// valid — `c` is in the 0..127 ASCII range. -export fn valid(c: rune) bool = { - if (c < 0) { return false; }; - if (c > 127) { return false; }; - return true; -}; - -// validstr — every byte in `s` is ASCII (0..127). -export fn validstr(s: str) bool = { - let i: i32 = 0; - for (i < s.len) { - // High-bit test rather than `> 127u8`; both cgens lower - // the bitwise form identically. The `> u8` form picks - // JA vs JG depending on signed/unsigned dispatch. - if ((s[i] & 128u8) != 0u8) { return false; }; - i += 1; - }; - return true; -}; - -// iscntrl — control chars: 0..31 and 127. -export fn iscntrl(c: rune) bool = { - if (c >= 0) { if (c <= 31) { return true; }; }; - if (c == 127) { return true; }; - return false; -}; - -// isblank — space and tab. -export fn isblank(c: rune) bool = { - if (c == 32) { return true; }; // ' ' - if (c == 9) { return true; }; // '\t' - return false; -}; - -// isprint — printable: space through '~'. -export fn isprint(c: rune) bool = { - if (c < 32) { return false; }; - if (c > 126) { return false; }; - return true; -}; - -// isgraph — printable, non-space. -export fn isgraph(c: rune) bool = { - if (c < 33) { return false; }; - if (c > 126) { return false; }; - return true; -}; - -// ispunct — printable, non-alnum, non-space. -export fn ispunct(c: rune) bool = { - if (!isgraph(c)) { return false; }; - if (isalnum(c)) { return false; }; - return true; -}; - -// tolower / toupper — fold ASCII case. Non-letters pass through. -export fn tolower(c: rune) rune = { - if (isupper(c)) { return c + 32; }; - return c; -}; - -export fn toupper(c: rune) rune = { - if (islower(c)) { return c - 32; }; - return c; -}; - -// strcasecmp — three-way ASCII case-insensitive compare. -export fn strcasecmp(a: str, b: str) i32 = { - let n: i32 = a.len; - if (b.len < n) { n = b.len; }; - let i: i32 = 0; - for (i < n) { - let ca: rune = tolower(a[i]: rune); - let cb: rune = tolower(b[i]: rune); - if (ca != cb) { return (ca - cb): i32; }; - i += 1; - }; - return a.len - b.len; -}; - // types — integer limits. Mirrors Hare's types::limits (I8_MAX, …) // platform-fixed for amd64. Numeric helpers live in lib/math, matching // Hare's split between types::limits and math::. @@ -4420,6 +4283,190 @@ export fn rpad(s: str, p: rune, maxlen: i32) str = { return frombytes(buf); }; +// ascii — rune-class predicates and case folding for the ASCII range. +// Matches Hare's ascii::isdigit family (rune-taking signature). Runes +// outside 0..127 always answer `false`. The lexer hot path uses these +// inline; they are expected to inline to a couple of compares. + +package ascii; + +import strings; + +export fn isdigit(c: rune) bool = { + if (c < 48) { return false; }; + if (c > 57) { return false; }; + return true; +}; + +export fn isupper(c: rune) bool = { + if (c < 65) { return false; }; + if (c > 90) { return false; }; + return true; +}; + +export fn islower(c: rune) bool = { + if (c < 97) { return false; }; + if (c > 122) { return false; }; + return true; +}; + +export fn isalpha(c: rune) bool = { + if (isupper(c)) { return true; }; + return islower(c); +}; + +export fn isalnum(c: rune) bool = { + if (isalpha(c)) { return true; }; + return isdigit(c); +}; + +// isspace — the C/Hare set: space, tab, NL, VT, FF, CR. +export fn isspace(c: rune) bool = { + if (c == 32) { return true; }; // ' ' + if (c == 9) { return true; }; // '\t' + if (c == 10) { return true; }; // '\n' + if (c == 11) { return true; }; // '\v' + if (c == 12) { return true; }; // '\f' + if (c == 13) { return true; }; // '\r' + return false; +}; + +export fn isxdigit(c: rune) bool = { + if (isdigit(c)) { return true; }; + if (c >= 65) { + if (c <= 70) { return true; }; // 'A'..'F' + }; + if (c >= 97) { + if (c <= 102) { return true; }; // 'a'..'f' + }; + return false; +}; + +// valid — `c` is in the 0..127 ASCII range. +export fn valid(c: rune) bool = { + if (c < 0) { return false; }; + if (c > 127) { return false; }; + return true; +}; + +// validstr — every byte in `s` is ASCII (0..127). +export fn validstr(s: str) bool = { + let i: i32 = 0; + for (i < s.len) { + // High-bit test rather than `> 127u8`; both cgens lower + // the bitwise form identically. The `> u8` form picks + // JA vs JG depending on signed/unsigned dispatch. + if ((s[i] & 128u8) != 0u8) { return false; }; + i += 1; + }; + return true; +}; + +// iscntrl — control chars: 0..31 and 127. +export fn iscntrl(c: rune) bool = { + if (c >= 0) { if (c <= 31) { return true; }; }; + if (c == 127) { return true; }; + return false; +}; + +// isblank — space and tab. +export fn isblank(c: rune) bool = { + if (c == 32) { return true; }; // ' ' + if (c == 9) { return true; }; // '\t' + return false; +}; + +// isprint — printable: space through '~'. +export fn isprint(c: rune) bool = { + if (c < 32) { return false; }; + if (c > 126) { return false; }; + return true; +}; + +// isgraph — printable, non-space. +export fn isgraph(c: rune) bool = { + if (c < 33) { return false; }; + if (c > 126) { return false; }; + return true; +}; + +// ispunct — printable, non-alnum, non-space. +export fn ispunct(c: rune) bool = { + if (!isgraph(c)) { return false; }; + if (isalnum(c)) { return false; }; + return true; +}; + +// tolower / toupper — fold ASCII case. Non-letters pass through. +export fn tolower(c: rune) rune = { + if (isupper(c)) { return c + 32; }; + return c; +}; + +export fn toupper(c: rune) rune = { + if (islower(c)) { return c - 32; }; + return c; +}; + +// strcasecmp — three-way ASCII case-insensitive compare. +export fn strcasecmp(a: str, b: str) i32 = { + let n: i32 = a.len; + if (b.len < n) { n = b.len; }; + let i: i32 = 0; + for (i < n) { + let ca: rune = tolower(a[i]: rune); + let cb: rune = tolower(b[i]: rune); + if (ca != cb) { return (ca - cb): i32; }; + i += 1; + }; + 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); +}; + // strconv — string-to-float. Mirrors ref/hare/strconv/stof.ha // (Hare in turn adapts Go): Eisel-Lemire fast path [1] with the // Simple-Decimal-Conversion slow path [2] (decimal.ww) as fallback. diff --git a/test/wcc/904_ascii_run.c b/test/wcc/904_ascii_run.c new file mode 100644 index 00000000..f43679cb --- /dev/null +++ b/test/wcc/904_ascii_run.c @@ -0,0 +1,49 @@ +/* + * 904_ascii_run — execute the lib/ascii @test fixture under the + * C-side `ww run` driver and assert exit 0. + * + * Same thin-wrapper shape as 967_bytes_run / 968_utf8_run: + * asciitest.ww carries its own `export fn main()` that drives the + * @test fns and signals which case failed via the exit code. + */ +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + const char *src = "lib/ascii/asciitest.ww"; + char path[1024], cmd[2048]; + snprintf(path, sizeof path, "%s/%s", cwd, src); + snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path); + int rc = runwait(cmd); + if (rc != 0) { + fprintf(stderr, "ascii_run FAIL: %s exited %d\n", src, rc); + return 1; + } + printf("ascii_run: %s ok\n", src); + return 0; +}