Files
ww/lib/strings/stringstest.ww
Hojun-Cho a651883c14 lib/strings+test: Hare port (dup/concat/trim/index/contains/has{pre,suf}fix/compare/utf8)
Hare-faithful index/predicate family per ref/hare/strings/{dup,
concat,trim,index,suffix,contains,compare,utf8}.ha. Non-variadic
subset (concat 2-arg, trim single-rune, contains single-needle)
pending task #16 — cstage variadic-pack drops .len on multi-field
element types; ship the Hare-faithful single-arg shape now, file
the variadic upgrade as follow-up. `sub` follow-up filed as #29
(commit 2 with iterator + utf8.chars relocation).

Surface: dup, concat, trim/trimprefix/trimsuffix (single rune),
hasprefix, hassuffix (both with (str|rune) sum needle),
byteindex, rbyteindex (both with (str|rune) sum needle),
contains (single str needle), compare, toutf8, fromutf8_unsafe,
runebytes helper. (str|rune) match arms route the rune via
utf8.encoderune into a [4]u8 scratch then bytes.index/rindex —
drew-devault's directive for clean Hare-fidelity over invented
ASCII-only rune-byte arms.

byteindex / rbyteindex rune-arm semantic correction —
corpus-coverage-blind unmask. Pre-existing impl scanned for
`r: u8` (broken for all rune values >0x7F since strings.ww first
landed; no caller exercised it). Replaced with utf8.encoderune-
based scan via runebytes helper. Severity-marker: silent
wrong-result for any non-ASCII rune needle, masked by zero
in-tree callers until lib/strings + utf8 chain pulled the shape
in.

Build-system propagation: lib/strings depends transitively on
lib/encoding/utf8 (via byteindex's rune arm). cmd/ww driver's
locate_import_in (cmd/ww/main.c:85) walks `<dir>/<name>.ww` and
`<dir>/<name>/<name>.ww` only — `use utf8;` doesn't find
lib/encoding/utf8/utf8.ww without explicit `-I lib/encoding/utf8`.
Propagated through 5 wwstage-tool Makefile targets + 7 test
wrappers + test/wcc/995_self_rebuild.c sprintf lines. Task #17
filed for the principled resolver fix (subdir walk vs Hare's
qualified `use encoding::utf8;` notation).

This commit chain (#15 strings) surfaced 7 cgen bugs during
landing: #16 cstage variadic-pack, #17 resolver nested-paths,
#27 aliaslookup leaf-collision, #22 zero-init !void/void-alias
let-decl, #15-cstage retscr SSoT name, #24 composite CALL return
as composite arg, #28 N_DOT calleeparams. All blocking ones
fixed (#16/#17 deferred-with-stopgap, others fixed in their
respective commits). Pre-flight + stop-and-surface discipline
held throughout — no workarounds shipped in stdlib.

Tests:
  - 966_strings_run drives lib/strings/stringstest.ww via ww run.
    15 @test fns: dup (alloc, multibyte), concat (empty, lopsided,
    multibyte), trim/ltrim/rtrim incl. 4-byte rune U+1D68A,
    hasprefix/hassuffix with (str|rune) incl. multibyte,
    byteindex/rbyteindex both arms 1/2/3/4-byte rune coverage,
    compare. Cited from ref/hare/strings/+test.ha where vectors
    apply.

100/100 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
2026-05-18 10:28:44 +09:00

291 lines
10 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// stringstest — exercises lib/strings. Run with
// `out/bin/ww run lib/strings/stringstest.ww -I lib/encoding/utf8`.
// Same signalled-then-fail()-with-+10 shape as bytes / utf8 / hex /
// time tests: non-zero exit pinpoints the failing scenario.
//
// Vectors mirror ref/hare/strings/{dup,concat,trim,contains,index,
// suffix,compare}.ha where ww can express them.
use strings;
use os;
let signalled: i32 = 0;
fn fail() void = { os.exit(signalled + 10); };
fn streq(a: str, b: str) bool = {
if (a.len != b.len) { return false; };
let i: i32 = 0;
for (i < a.len) {
if (a[i] != b[i]) { return false; };
i += 1;
};
return true;
};
// ---- dup --------------------------------------------------------------
// ref/hare/strings/dup.ha:45.
@test fn dup_cases() void = {
let e: str = strings.dup("");
if (!streq(e, "")) { fail(); };
if (e.len != 0) { fail(); };
let h: str = strings.dup("hello");
if (!streq(h, "hello")) { fail(); };
defer os.free(h.ptr: *void, h.len: u64);
// multi-byte UTF-8: dup must copy raw bytes, not aliased view.
let m: str = strings.dup("こんにちは");
if (m.len != 15) { fail(); };
if (!streq(m, "こんにちは")) { fail(); };
if (m.ptr == "こんにちは".ptr) { fail(); }; // fresh alloc
defer os.free(m.ptr: *void, m.len: u64);
};
// ---- concat -----------------------------------------------------------
// ref/hare/strings/concat.ha:18 (2-arg subset).
@test fn concat_cases() void = {
let a: str = strings.concat("hello ", "world");
if (!streq(a, "hello world")) { fail(); };
defer os.free(a.ptr: *void, a.len: u64);
let e: str = strings.concat("", "");
if (!streq(e, "")) { fail(); };
// e.len == 0 — os.free guarded, skip.
let l: str = strings.concat("", "world");
if (!streq(l, "world")) { fail(); };
defer os.free(l.ptr: *void, l.len: u64);
let r: str = strings.concat("hello", "");
if (!streq(r, "hello")) { fail(); };
defer os.free(r.ptr: *void, r.len: u64);
let m: str = strings.concat("こん", "にちは");
if (!streq(m, "こんにちは")) { fail(); };
defer os.free(m.ptr: *void, m.len: u64);
};
// ---- hasprefix --------------------------------------------------------
// ref/hare/strings/suffix.ha:18.
@test fn hasprefix_cases() void = {
if (!strings.hasprefix("hello world", "hello")) { fail(); };
if (!strings.hasprefix("hello world", 'h')) { fail(); };
if ( strings.hasprefix("hello world", "world")) { fail(); };
if ( strings.hasprefix("hello world", 'q')) { fail(); };
if (!strings.hasprefix("hello", "hello")) { fail(); }; // equal-len
if (!strings.hasprefix("anything", "")) { fail(); }; // empty prefix
if ( strings.hasprefix("", "x")) { fail(); };
// multibyte rune prefix — '\'é\'' literal blocked by single-byte
// lexrune (lib/ww/lex/lex.ww:659); pass codepoint directly.
if (!strings.hasprefix("éclat", 0xE9u32: rune)) { fail(); };
if (!strings.hasprefix("🦀rust", 0x1F980u32: rune)) { fail(); };
};
// ---- hassuffix --------------------------------------------------------
// ref/hare/strings/suffix.ha:36.
@test fn hassuffix_cases() void = {
if (!strings.hassuffix("hello world", "world")) { fail(); };
if (!strings.hassuffix("hello world", 'd')) { fail(); };
if ( strings.hassuffix("hello world", "hello")) { fail(); };
if ( strings.hassuffix("hello world", 'h')) { fail(); };
if (!strings.hassuffix("café", 0xE9u32: rune)) { fail(); }; // multibyte
};
// ---- contains ---------------------------------------------------------
// ref/hare/strings/contains.ha:27.
@test fn contains_cases() void = {
if (!strings.contains("hello world", "hello")) { fail(); };
if (!strings.contains("hello world", 'h')) { fail(); };
if ( strings.contains("hello world", 'x')) { fail(); };
if (!strings.contains("hello world", "world")) { fail(); };
if (!strings.contains("hello world", "")) { fail(); }; // empty hits at 0
if ( strings.contains("hello world", "foobar")) { fail(); };
if (!strings.contains("こんにちは", 0x306Bu32: rune)) { fail(); }; // 'に'
if (!strings.contains("こんにちは", "ちは")) { fail(); };
};
// ---- byteindex --------------------------------------------------------
// ref/hare/strings/index.ha:147 (byteindex tests, both arms).
@test fn byteindex_str_cases() void = {
match (strings.byteindex("hello", "hello")) {
case let i: i32 => { if (i != 0) { fail(); }; };
case void => { fail(); };
};
match (strings.byteindex("hello world!", "world")) {
case let i: i32 => { if (i != 6) { fail(); }; };
case void => { fail(); };
};
match (strings.byteindex("hello world!", "orld!")) {
case let i: i32 => { if (i != 7) { fail(); }; };
case void => { fail(); };
};
match (strings.byteindex("hello world!", "word")) {
case let i: i32 => { fail(); };
case void => void;
};
// empty needle hits at 0 (ref/hare/bytes/index.ha:63).
match (strings.byteindex("hello", "")) {
case let i: i32 => { if (i != 0) { fail(); }; };
case void => { fail(); };
};
// empty haystack, non-empty needle — absent.
match (strings.byteindex("", "x")) {
case let i: i32 => { fail(); };
case void => void;
};
// multibyte substring in multibyte haystack.
match (strings.byteindex("こんにちは", "ちは")) {
case let i: i32 => { if (i != 9) { fail(); }; };
case void => { fail(); };
};
};
@test fn byteindex_rune_cases() void = {
// ASCII rune (1-byte encoding).
match (strings.byteindex("hello world", 'w')) {
case let i: i32 => { if (i != 6) { fail(); }; };
case void => { fail(); };
};
// 2-byte rune U+00E9 'é' inside "café".
match (strings.byteindex("café", 0xE9u32: rune)) {
case let i: i32 => { if (i != 3) { fail(); }; };
case void => { fail(); };
};
// 3-byte rune U+3061 'ち' inside "こんにちは".
match (strings.byteindex("こんにちは", 0x3061u32: rune)) {
case let i: i32 => { if (i != 9) { fail(); }; };
case void => { fail(); };
};
// 4-byte rune U+1F980 '🦀' inside "ab🦀cd".
match (strings.byteindex("ab🦀cd", 0x1F980u32: rune)) {
case let i: i32 => { if (i != 2) { fail(); }; };
case void => { fail(); };
};
// absent.
match (strings.byteindex("こんにちは", 'q')) {
case let i: i32 => { fail(); };
case void => void;
};
};
// ---- rbyteindex -------------------------------------------------------
@test fn rbyteindex_cases() void = {
// Two 'た' in "またあったね" — ref/hare/strings/index.ha:160-161.
match (strings.byteindex("またあったね", "た")) {
case let i: i32 => { if (i != 3) { fail(); }; };
case void => { fail(); };
};
match (strings.rbyteindex("またあったね", "た")) {
case let i: i32 => { if (i != 12) { fail(); }; };
case void => { fail(); };
};
// Rune arm, multi-byte 'に' U+306B.
match (strings.rbyteindex("こんにちは", 0x306Bu32: rune)) {
case let i: i32 => { if (i != 6) { fail(); }; };
case void => { fail(); };
};
// Absent.
match (strings.rbyteindex("abc", 'z')) {
case let i: i32 => { fail(); };
case void => void;
};
};
// ---- trimprefix / trimsuffix ------------------------------------------
// ref/hare/strings/trim.ha:99-107.
@test fn trimprefix_cases() void = {
if (!streq(strings.trimprefix("", ""), "")) { fail(); };
if (!streq(strings.trimprefix("", "blablabla"), "")) { fail(); };
if (!streq(strings.trimprefix("hello, world", "hello"), ", world")) { fail(); };
if (!streq(strings.trimprefix("blablabla", "bla"), "blabla")) { fail(); };
// equal-length match strips to empty.
if (!streq(strings.trimprefix("hello", "hello"), "")) { fail(); };
};
@test fn trimsuffix_cases() void = {
if (!streq(strings.trimsuffix("", ""), "")) { fail(); };
if (!streq(strings.trimsuffix("", "blablabla"), "")) { fail(); };
if (!streq(strings.trimsuffix("hello, world", "world"), "hello, ")) { fail(); };
if (!streq(strings.trimsuffix("blablabla", "bla"), "blabla")) { fail(); };
if (!streq(strings.trimsuffix("hello", "hello"), "")) { fail(); };
};
// ---- ltrim / rtrim / trim (single-rune subset) ------------------------
// ref/hare/strings/trim.ha:75-97. Vectors restricted to single-rune
// patterns (Hare's `rune...` blocks on task #16).
@test fn ltrim_cases() void = {
if (!streq(strings.ltrim("", 'x'), "")) { fail(); };
if (!streq(strings.ltrim("aaabc", 'a'), "bc")) { fail(); };
if (!streq(strings.ltrim("xyz", 'a'), "xyz")) { fail(); }; // no match
if (!streq(strings.ltrim("aaaa", 'a'), "")) { fail(); }; // all stripped
// 4-byte rune pattern — '𝚊' = U+1D68A.
if (!streq(strings.ltrim("𝚊𝚊hi", 0x1D68Au32: rune), "hi")) { fail(); };
};
@test fn rtrim_cases() void = {
if (!streq(strings.rtrim("", 'x'), "")) { fail(); };
if (!streq(strings.rtrim("bcaaa", 'a'), "bc")) { fail(); };
if (!streq(strings.rtrim("xyz", 'a'), "xyz")) { fail(); };
if (!streq(strings.rtrim("aaaa", 'a'), "")) { fail(); };
if (!streq(strings.rtrim("hi𝚊𝚊", 0x1D68Au32: rune), "hi")) { fail(); };
};
@test fn trim_cases() void = {
if (!streq(strings.trim("", 'x'), "")) { fail(); };
if (!streq(strings.trim("aaabcaaa", 'a'), "bc")) { fail(); };
if (!streq(strings.trim("xyz", 'a'), "xyz")) { fail(); };
if (!streq(strings.trim("aaaa", 'a'), "")) { fail(); };
};
// ---- compare ----------------------------------------------------------
// ref/hare/strings/compare.ha:16.
@test fn compare_cases() void = {
if (strings.compare("ABC", "ABC") != 0) { fail(); };
if (strings.compare("ABC", "AB") <= 0) { fail(); };
if (strings.compare("AB", "ABC") >= 0) { fail(); };
if (strings.compare("BCD", "ABC") <= 0) { fail(); };
if (strings.compare("ABC", "abc") >= 0) { fail(); };
};
// ---- toutf8 / fromutf8_unsafe roundtrip -------------------------------
// ref/hare/strings/utf8.ha:31.
@test fn utf8_roundtrip_cases() void = {
let s: str = "hello";
let b: []u8 = strings.toutf8(s);
if (b.len != 5) { fail(); };
if (b[0] != 104u8) { fail(); }; // 'h'
let r: str = strings.fromutf8_unsafe(b);
if (!streq(r, "hello")) { fail(); };
if (r.ptr != s.ptr) { fail(); }; // borrowed, not copied
};
export fn main() i32 = {
signalled = 1; dup_cases();
signalled = 2; concat_cases();
signalled = 3; hasprefix_cases();
signalled = 4; hassuffix_cases();
signalled = 5; contains_cases();
signalled = 6; byteindex_str_cases();
signalled = 7; byteindex_rune_cases();
signalled = 8; rbyteindex_cases();
signalled = 9; trimprefix_cases();
signalled = 10; trimsuffix_cases();
signalled = 11; ltrim_cases();
signalled = 12; rtrim_cases();
signalled = 13; trim_cases();
signalled = 14; compare_cases();
signalled = 15; utf8_roundtrip_cases();
return 0;
};