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).
This commit is contained in:
290
lib/strings/stringstest.ww
Normal file
290
lib/strings/stringstest.ww
Normal file
@@ -0,0 +1,290 @@
|
||||
// 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;
|
||||
};
|
||||
Reference in New Issue
Block a user