Files
ww/lib/strings/stringstest.ww
Hojun-Cho 6d006da26c lib/strings+test: graduate fromutf8 + bytesub to validating return
fromutf8(in: []u8) (str | utf8.invalid) and the bytesub form per
ref/hare/strings/utf8.ha:22 and sub.ha:59. bytesub keeps its byte
asserts (ww extension over Hare; predates #7).

fromutf8 walks the utf8 decoder via utf8.next rather than the
shorter `utf8.validate(in)?` form. Two compiler bugs in the way:
cross-shape `(void | invalid) → (str | invalid)` propagation is
#19, and (void | !void) match-bind locals diverge between stages /
str→union lift SIGSEGVs in cstage — both filed as #48. The
decoder-walk form bypasses both and matches what
ref/hare/strings/utf8.ha actually does in source.

getopt.ww:314 caller updated to match the new (str | invalid)
return; bi+1 cannot hit a continuation byte in well-formed argv
(bi is a just-matched ASCII flag), so abort spells the precondition.

bytesub_cases rewritten as exhaustive match; new rows cover
start-on-continuation and end-on-continuation invalid arms plus an
end==s.len bypass. fromutf8_cases is new — Hare vector + edge
bytes + multibyte parity rows.
2026-05-20 02:02:28 +09:00

1713 lines
56 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`.
// 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.
package strings;
import strings;
import encoding.utf8;
import 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);
};
// ---- dupall -----------------------------------------------------------
// ref/hare/strings/dup.ha:55 (#6). Per-row `signalled` bump narrows a
// failure exit code; element reads go through `&toks.ptr[i]: *str`
// per the splitn cases (16B element copy gap, cgen.c:6515).
@test fn dupall_cases() void = {
// Empty input — empty result, mirrors Hare's `payload = []`.
signalled = 1800;
let empty: []str;
empty.ptr = nil: *str;
empty.len = 0;
empty.cap = 0;
match (strings.dupall(empty)) {
case let r: []str => {
if (r.len != 0) { fail(); };
strings.freeall(r);
};
case nomem => { fail(); };
};
// Two-element ASCII — each output element is a fresh allocation
// independent of the input (ptr differs from the borrowed source).
signalled = 1801;
let in2: [2]str;
in2[0] = "hello";
in2[1] = "world";
let src2: []str;
src2.ptr = &in2[0];
src2.len = 2;
src2.cap = 2;
match (strings.dupall(src2)) {
case let r: []str => {
if (r.len != 2) { fail(); };
expect_str(r, 0, "hello");
expect_str(r, 1, "world");
let p0: *str = &r.ptr[0];
if (p0.ptr == in2[0].ptr) { fail(); };
let p1: *str = &r.ptr[1];
if (p1.ptr == in2[1].ptr) { fail(); };
strings.freeall(r);
};
case nomem => { fail(); };
};
// Singleton — `only` rune-equivalent of Hare's `["only"]`.
signalled = 1802;
let in1: [1]str;
in1[0] = "only";
let src1: []str;
src1.ptr = &in1[0];
src1.len = 1;
src1.cap = 1;
match (strings.dupall(src1)) {
case let r: []str => {
if (r.len != 1) { fail(); };
expect_str(r, 0, "only");
strings.freeall(r);
};
case nomem => { fail(); };
};
// Multibyte — UTF-8 bytes (5-byte and 15-byte) round-trip.
signalled = 1803;
let inm: [2]str;
inm[0] = "héllo";
inm[1] = "こんにちは";
let srcm: []str;
srcm.ptr = &inm[0];
srcm.len = 2;
srcm.cap = 2;
match (strings.dupall(srcm)) {
case let r: []str => {
if (r.len != 2) { fail(); };
expect_str(r, 0, "héllo");
expect_str(r, 1, "こんにちは");
let p0: *str = &r.ptr[0];
if (p0.len != 6) { fail(); }; // é is 2 bytes
if (p0.ptr == inm[0].ptr) { fail(); };
let p1: *str = &r.ptr[1];
if (p1.len != 15) { fail(); }; // each kana is 3 bytes
if (p1.ptr == inm[1].ptr) { fail(); };
strings.freeall(r);
};
case nomem => { fail(); };
};
};
// ---- concat -----------------------------------------------------------
// ref/hare/strings/concat.ha:18. Rows mirror Hare's vectors (0/1/2/3-arg,
// empty-mid, 2-empty) plus empty-first / empty-last / multibyte. The
// per-row `signalled` bump narrows a failure exit code to the offending
// row.
@test fn concat_cases() void = {
let pool: [17]str;
pool[0] = "hello";
pool[1] = "hello ";
pool[2] = "world";
pool[3] = "hello";
pool[4] = " ";
pool[5] = "world";
pool[6] = "hello";
pool[7] = "";
pool[8] = "world";
pool[9] = "";
pool[10] = "";
pool[11] = "";
pool[12] = "world";
pool[13] = "hello";
pool[14] = "";
pool[15] = "こん";
pool[16] = "にちは";
let argo: [9]i32;
let argn: [9]i32;
let want: [9]str;
let labels: [9]str;
argo[0]=0; argn[0]=0; want[0]=""; labels[0]="0-arg";
argo[1]=0; argn[1]=1; want[1]="hello"; labels[1]="1-arg";
argo[2]=1; argn[2]=2; want[2]="hello world"; labels[2]="2-arg";
argo[3]=3; argn[3]=3; want[3]="hello world"; labels[3]="3-arg";
argo[4]=6; argn[4]=3; want[4]="helloworld"; labels[4]="empty-mid";
argo[5]=9; argn[5]=2; want[5]=""; labels[5]="2-empty";
argo[6]=11; argn[6]=2; want[6]="world"; labels[6]="empty-first";
argo[7]=13; argn[7]=2; want[7]="hello"; labels[7]="empty-last";
argo[8]=15; argn[8]=2; want[8]="こんにちは"; labels[8]="multibyte";
let i: i32 = 0;
for (i < 9) {
signalled = 200 + i;
let argv: []str;
argv.ptr = &pool[argo[i]];
argv.len = argn[i];
argv.cap = argn[i];
let got: str = strings.concat(argv...);
if (!streq(got, want[i])) { fail(); };
if (got.len > 0) { os.free(got.ptr: *void, got.len: u64); };
i += 1;
};
};
// ---- join -------------------------------------------------------------
// ref/hare/strings/concat.ha:64. Rows mirror Hare's @test fn join
// (0-arg, 1-arg, empty-sep, 3-arg.) plus 2-arg, all-empties, long sep,
// multibyte, empty-mid (delim still inserted around the empty slot).
// Per-row `signalled` 1620+i narrows the failing row.
@test fn join_cases() void = {
let pool: [19]str;
pool[0] = "hello";
pool[1] = "a";
pool[2] = "b";
pool[3] = "a";
pool[4] = "b";
pool[5] = "c";
pool[6] = "a";
pool[7] = "b";
pool[8] = "c";
pool[9] = "";
pool[10] = "";
pool[11] = "a";
pool[12] = "b";
pool[13] = "c";
pool[14] = "こん";
pool[15] = "にちは";
pool[16] = "a";
pool[17] = "";
pool[18] = "b";
let argo: [9]i32;
let argn: [9]i32;
let seps: [9]str;
let want: [9]str;
argo[0]=0; argn[0]=0; seps[0]="."; want[0]="";
argo[1]=0; argn[1]=1; seps[1]="."; want[1]="hello";
argo[2]=1; argn[2]=2; seps[2]=", "; want[2]="a, b";
argo[3]=3; argn[3]=3; seps[3]="."; want[3]="a.b.c";
argo[4]=6; argn[4]=3; seps[4]=""; want[4]="abc";
argo[5]=9; argn[5]=2; seps[5]=","; want[5]=",";
argo[6]=11; argn[6]=3; seps[6]=" :: "; want[6]="a :: b :: c";
argo[7]=14; argn[7]=2; seps[7]="・"; want[7]="こん・にちは";
argo[8]=16; argn[8]=3; seps[8]="-"; want[8]="a--b";
let i: i32 = 0;
for (i < 9) {
signalled = 1620 + i;
let argv: []str;
argv.ptr = &pool[argo[i]];
argv.len = argn[i];
argv.cap = argn[i];
let got: str = strings.join(seps[i], argv...);
if (!streq(got, want[i])) { fail(); };
if (got.len > 0) { os.free(got.ptr: *void, got.len: u64); };
i += 1;
};
};
// ---- 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(); };
// Variadic rows. ref/hare/strings/contains.ha:27.
signalled = 1600;
if ( strings.contains("hello")) { fail(); };
signalled = 1601;
if (!strings.contains("hello world", "world")) { fail(); };
signalled = 1602;
if (!strings.contains("hello", 'l')) { fail(); };
signalled = 1603;
if (!strings.contains("hello world", "foo", "world", 'x')) { fail(); };
signalled = 1604;
if ( strings.contains("hello", "foo", 'z', "bar")) { fail(); };
signalled = 1605;
if (!strings.contains("héllo", "x", 0xE9u32: rune)) { 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;
};
};
// ---- index ------------------------------------------------------------
// ref/hare/strings/index.ha:108. Rune-wise offset, NOT byte-wise — the
// multibyte rows pin that distinction (Hare doc at index.ha:7).
@test fn index_cases() void = {
// str-arm: ASCII haystack/needle, mid-string match.
signalled = 1400;
match (strings.index("hello", "ll")) {
case let i: i32 => { if (i != 2) { fail(); }; };
case void => { fail(); };
};
// str-arm: absent needle.
signalled = 1401;
match (strings.index("hello", "world")) {
case let i: i32 => { fail(); };
case void => void;
};
// Hare vectors at ref/hare/strings/index.ha:113-119.
signalled = 1402;
match (strings.index("hello world!", "hello")) {
case let i: i32 => { if (i != 0) { fail(); }; };
case void => { fail(); };
};
signalled = 1403;
match (strings.index("hello world!", "world")) {
case let i: i32 => { if (i != 6) { fail(); }; };
case void => { fail(); };
};
signalled = 1404;
match (strings.index("hello world!", "orld!")) {
case let i: i32 => { if (i != 7) { fail(); }; };
case void => { fail(); };
};
// Multibyte haystack + str needle: "ちは" at rune index 3
// in "こんにちは" (byteindex returns 9, rune-index is 3).
signalled = 1405;
match (strings.index("こんにちは", "ちは")) {
case let i: i32 => { if (i != 3) { fail(); }; };
case void => { fail(); };
};
// rune-arm.
signalled = 1406;
match (strings.index("hello", 'l')) {
case let i: i32 => { if (i != 2) { fail(); }; };
case void => { fail(); };
};
signalled = 1407;
match (strings.index("hello world", 'w')) {
case let i: i32 => { if (i != 6) { fail(); }; };
case void => { fail(); };
};
// Multibyte rune: 'é' U+00E9 at rune 1 in "héllo" — pins
// rune-index vs byte-index (byteindex returns 1; rune-index is
// 1 also — but the str-arm's 1405 row covers the distinction).
signalled = 1408;
match (strings.index("héllo", 0xE9u32: rune)) {
case let i: i32 => { if (i != 1) { fail(); }; };
case void => { fail(); };
};
// Multibyte rune in multibyte haystack: 'ち' U+3061 at rune 3
// in "こんにちは" (byteindex returns 9, rune-index is 3).
signalled = 1409;
match (strings.index("こんにちは", 0x3061u32: rune)) {
case let i: i32 => { if (i != 3) { fail(); }; };
case void => { fail(); };
};
signalled = 1410;
match (strings.index("こんにちは", 'q')) {
case let i: i32 => { fail(); };
case void => void;
};
// str-arm: rune-index ≠ byte-index again, mid-string match.
// "あった" starts at rune 2 (byte 6) in "またあったね"
// (each kana is 3 bytes; ref/hare/strings/index.ha:60). Pins
// the dual-iterator walk against the discarded byteindex-and-walk
// shape (#10).
signalled = 1411;
match (strings.index("またあったね", "あった")) {
case let i: i32 => { if (i != 2) { fail(); }; };
case void => { fail(); };
};
// str-arm: tail-anchored multibyte needle. "は" is at rune 4
// (byte 12) in "こんにちは".
signalled = 1412;
match (strings.index("こんにちは", "は")) {
case let i: i32 => { if (i != 4) { fail(); }; };
case void => { fail(); };
};
// Empty needle hits at rune 0 — Hare's `index_string` falls into
// the `needle_rune is done` branch on the very first inner step
// (ref/hare/strings/index.ha:70).
signalled = 1413;
match (strings.index("hello", "")) {
case let i: i32 => { if (i != 0) { fail(); }; };
case void => { fail(); };
};
signalled = 1414;
match (strings.index("", "")) {
case let i: i32 => { if (i != 0) { fail(); }; };
case void => { fail(); };
};
// Empty haystack, non-empty needle — absent.
signalled = 1415;
match (strings.index("", "x")) {
case let i: i32 => { fail(); };
case void => void;
};
// Multibyte haystack, multibyte absent needle — exercises the
// inner-loop mismatch-break across runes (#10, Hare row
// ref/hare/strings/index.ha:119).
signalled = 1416;
match (strings.index("こんにちは", "きょうは")) {
case let i: i32 => { fail(); };
case void => void;
};
// Self-match: haystack == needle, Hare row index.ha:113.
signalled = 1417;
match (strings.index("hello", "hello")) {
case let i: i32 => { if (i != 0) { fail(); }; };
case void => { fail(); };
};
};
// ---- rindex -----------------------------------------------------------
// ref/hare/strings/index.ha:22. Symmetric: last-occurrence rune index.
@test fn rindex_cases() void = {
// str-arm.
signalled = 1500;
match (strings.rindex("hello", "lo")) {
case let i: i32 => { if (i != 3) { fail(); }; };
case void => { fail(); };
};
// Hare vector at ref/hare/strings/index.ha:122.
signalled = 1501;
match (strings.rindex("hello world!", "o")) {
case let i: i32 => { if (i != 7) { fail(); }; };
case void => { fail(); };
};
signalled = 1502;
match (strings.rindex("hello", "world")) {
case let i: i32 => { fail(); };
case void => void;
};
// Multibyte: last "た" in "またあったね" — rbyteindex returns
// 12, rune-index is 4 (ま=0 た=1 あ=2 っ=3 た=4 ね=5).
signalled = 1503;
match (strings.rindex("またあったね", "た")) {
case let i: i32 => { if (i != 4) { fail(); }; };
case void => { fail(); };
};
// rune-arm.
signalled = 1504;
match (strings.rindex("hello", 'l')) {
case let i: i32 => { if (i != 3) { fail(); }; };
case void => { fail(); };
};
signalled = 1505;
match (strings.rindex("aaaaa", 'a')) {
case let i: i32 => { if (i != 4) { fail(); }; };
case void => { fail(); };
};
// Multibyte rune: 'に' U+306B at rune 2 in "こんにちは".
signalled = 1506;
match (strings.rindex("こんにちは", 0x306Bu32: rune)) {
case let i: i32 => { if (i != 2) { fail(); }; };
case void => { fail(); };
};
signalled = 1507;
match (strings.rindex("hello", '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 ---------------------------------------------
// ref/hare/strings/trim.ha:75-97. 0-arg rows (#9) pin the ASCII
// whitespace set (' ', '\t', '\n', '\r' — ref/hare/strings/trim.ha:6).
@test fn ltrim_cases() void = {
let runes: [9]rune;
runes[0] = 'x';
runes[1] = 'a';
runes[2] = 0x1D68Au32: rune;
runes[3] = '(';
runes[4] = ')';
runes[5] = 'a';
runes[6] = 'b';
runes[7] = 'c';
runes[8] = 'd';
// 0-arg rows (8..10) strip ASCII whitespace per Hare (#9):
// only the leading side is stripped for ltrim.
let inputs: [11]str;
let argo: [11]i32;
let argn: [11]i32;
let want: [11]str;
inputs[0]=""; argo[0]=0; argn[0]=1; want[0]="";
inputs[1]="aaabc"; argo[1]=1; argn[1]=1; want[1]="bc";
inputs[2]="xyz"; argo[2]=1; argn[2]=1; want[2]="xyz";
inputs[3]="aaaa"; argo[3]=1; argn[3]=1; want[3]="";
inputs[4]="𝚊𝚊hi"; argo[4]=2; argn[4]=1; want[4]="hi";
inputs[5]="((()(())))())"; argo[5]=3; argn[5]=2; want[5]="";
inputs[6]="abacadabra"; argo[6]=5; argn[6]=4; want[6]="ra";
inputs[7]="hello"; argo[7]=0; argn[7]=0; want[7]="hello";
inputs[8]=" hello "; argo[8]=0; argn[8]=0; want[8]="hello ";
inputs[9]="\t\r\n hello"; argo[9]=0; argn[9]=0; want[9]="hello";
inputs[10]=" "; argo[10]=0; argn[10]=0; want[10]="";
let i: i32 = 0;
for (i < 11) {
signalled = 1100 + i;
let argv: []rune;
argv.ptr = &runes[argo[i]];
argv.len = argn[i];
argv.cap = argn[i];
let got: str = strings.ltrim(inputs[i], argv...);
if (!streq(got, want[i])) { fail(); };
i += 1;
};
};
@test fn rtrim_cases() void = {
let runes: [19]rune;
runes[0] = 'x';
runes[1] = 'a';
runes[2] = 0x1D68Au32: rune;
runes[3] = 'w';
runes[4] = 'd';
runes[5] = 'o';
runes[6] = 'r';
runes[7] = ' ';
runes[8] = 's';
runes[9] = 'i';
runes[10] = 'l';
runes[11] = 'z';
runes[12] = 't';
runes[13] = 'm';
runes[14] = 'n';
runes[15] = 'o';
runes[16] = 'e';
runes[17] = 'a';
runes[18] = 'd';
// 0-arg rows (8..10) strip ASCII whitespace per Hare (#9):
// only the trailing side is stripped for rtrim.
let inputs: [11]str;
let argo: [11]i32;
let argn: [11]i32;
let want: [11]str;
inputs[0]=""; argo[0]=0; argn[0]=1; want[0]="";
inputs[1]="bcaaa"; argo[1]=1; argn[1]=1; want[1]="bc";
inputs[2]="xyz"; argo[2]=1; argn[2]=1; want[2]="xyz";
inputs[3]="aaaa"; argo[3]=1; argn[3]=1; want[3]="";
inputs[4]="hi𝚊𝚊"; argo[4]=2; argn[4]=1; want[4]="hi";
inputs[5]="yellowwooddoor";
argo[5]=3; argn[5]=4; want[5]="yell";
inputs[6]="Sentimentalized sensationalism sensationalized sentimentalisms";
argo[6]=7; argn[6]=12; want[6]="S";
inputs[7]="hello"; argo[7]=0; argn[7]=0; want[7]="hello";
inputs[8]=" hello "; argo[8]=0; argn[8]=0; want[8]=" hello";
inputs[9]="hello, world\r\n\r\n";
argo[9]=0; argn[9]=0; want[9]="hello, world";
inputs[10]=" "; argo[10]=0; argn[10]=0; want[10]="";
let i: i32 = 0;
for (i < 11) {
signalled = 1200 + i;
let argv: []rune;
argv.ptr = &runes[argo[i]];
argv.len = argn[i];
argv.cap = argn[i];
let got: str = strings.rtrim(inputs[i], argv...);
if (!streq(got, want[i])) { fail(); };
i += 1;
};
};
@test fn trim_cases() void = {
let runes: [8]rune;
runes[0] = 'x';
runes[1] = 'a';
runes[2] = 'm';
runes[3] = 'i';
runes[4] = 'p';
runes[5] = 's';
runes[6] = '[';
runes[7] = ']';
// 0-arg rows (7..9) strip ASCII whitespace per Hare (#9) from
// both ends.
let inputs: [10]str;
let argo: [10]i32;
let argn: [10]i32;
let want: [10]str;
inputs[0]=""; argo[0]=0; argn[0]=1; want[0]="";
inputs[1]="aaabcaaa"; argo[1]=1; argn[1]=1; want[1]="bc";
inputs[2]="xyz"; argo[2]=1; argn[2]=1; want[2]="xyz";
inputs[3]="aaaa"; argo[3]=1; argn[3]=1; want[3]="";
inputs[4]="mississippi"; argo[4]=2; argn[4]=4; want[4]="";
inputs[5]="[[][[[]]][][].[[]][]]][]]]";
argo[5]=6; argn[5]=2; want[5]=".";
inputs[6]="hello"; argo[6]=0; argn[6]=0; want[6]="hello";
inputs[7]=" hello "; argo[7]=0; argn[7]=0; want[7]="hello";
inputs[8]="\r\thello\n\r"; argo[8]=0; argn[8]=0; want[8]="hello";
inputs[9]=" "; argo[9]=0; argn[9]=0; want[9]="";
let i: i32 = 0;
for (i < 10) {
signalled = 1300 + i;
let argv: []rune;
argv.ptr = &runes[argo[i]];
argv.len = argn[i];
argv.cap = argn[i];
let got: str = strings.trim(inputs[i], argv...);
if (!streq(got, want[i])) { fail(); };
i += 1;
};
};
// ---- 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(); };
if (strings.compare("ABC", "こんにちは") >= 0) { fail(); };
};
// ---- sub / bytesub ----------------------------------------------------
// ref/hare/strings/sub.ha:44 (@test fn sub), :79 (@test fn bytesub). Hare's
// 2-arg `sub(s, start)` rows are omitted: ww has no default-parameter
// syntax (filed as #37). bytesub now validates rune boundaries (#7).
@test fn sub_cases() void = {
signalled = 1790;
if (!streq(strings.sub("a string", 0, 8), "a string")) { fail(); };
signalled = 1791;
if (!streq(strings.sub("a string", 0, 1), "a")) { fail(); };
signalled = 1792;
if (!streq(strings.sub("a string", 0, 3), "a s")) { fail(); };
signalled = 1793;
if (!streq(strings.sub("a string", 2, 8), "string")) { fail(); };
// start == end yields an empty borrowed view.
signalled = 1794;
if (!streq(strings.sub("a string", 4, 4), "")) { fail(); };
if (strings.sub("a string", 4, 4).len != 0) { fail(); };
// Hare vector — rune indices 1..3 over "こんにちは" select bytes
// 3..9 ("んに"), not bytes 1..3.
signalled = 1795;
if (!streq(strings.sub("こんにちは", 1, 3), "んに")) { fail(); };
// 2-byte rune at rune index 1 in "héllo" — byte offsets 1..3.
signalled = 1796;
if (!streq(strings.sub("héllo", 1, 2), "é")) { fail(); };
// start == 0, end == rune-len of full string.
signalled = 1797;
if (!streq(strings.sub("héllo", 0, 5), "héllo")) { fail(); };
};
// Match-shape mirrors Hare's `bytesub(...)!` at ref/hare/strings/sub.ha:
// 80-86. Inlined at each call site (rather than a helper that takes
// `(str | utf8.invalid)` by value) because the union-by-value path
// crashes — same lift-on-pass-by-value family as #48.
@test fn bytesub_cases() void = {
signalled = 1800;
match (strings.bytesub("a string", 0, 8)) {
case let s: str => { if (!streq(s, "a string")) { fail(); }; };
case let e: utf8.invalid => { fail(); };
};
signalled = 1801;
match (strings.bytesub("a string", 0, 1)) {
case let s: str => { if (!streq(s, "a")) { fail(); }; };
case let e: utf8.invalid => { fail(); };
};
signalled = 1802;
match (strings.bytesub("a string", 0, 3)) {
case let s: str => { if (!streq(s, "a s")) { fail(); }; };
case let e: utf8.invalid => { fail(); };
};
signalled = 1803;
match (strings.bytesub("a string", 2, 8)) {
case let s: str => { if (!streq(s, "string")) { fail(); }; };
case let e: utf8.invalid => { fail(); };
};
signalled = 1804;
match (strings.bytesub("a string", 4, 4)) {
case let s: str => { if (!streq(s, "")) { fail(); }; };
case let e: utf8.invalid => { fail(); };
};
// Hare vector — byte indices 3..9 over "こんにちは" select "んに".
signalled = 1805;
match (strings.bytesub("こんにちは", 3, 9)) {
case let s: str => { if (!streq(s, "んに")) { fail(); }; };
case let e: utf8.invalid => { fail(); };
};
// Rune/byte axis disagree on identical args (#3): sub(s,0,3) walks 3
// runes and yields 9 bytes; bytesub(s,0,3) yields the first 3 bytes
// — one 3-byte codepoint.
signalled = 1806;
if (!streq(strings.sub("こんにちは", 0, 3), "こんに")) { fail(); };
match (strings.bytesub("こんにちは", 0, 3)) {
case let s: str => { if (!streq(s, "こ")) { fail(); }; };
case let e: utf8.invalid => { fail(); };
};
// Borrowed view: ptr aliases input.
signalled = 1807;
let s: str = "hello";
match (strings.bytesub(s, 1, 4)) {
case let r: str => {
if (r.ptr != s.ptr + 1u64) { fail(); };
if (r.len != 3) { fail(); };
};
case let e: utf8.invalid => { fail(); };
};
// Hare's invalid row (ref/hare/strings/sub.ha:87) — start lands on
// a continuation byte (2nd byte of "こ"), bytesub must reject (#7).
signalled = 1808;
match (strings.bytesub("こんにちは", 1, 3)) {
case let r: str => { fail(); };
case let e: utf8.invalid => void;
};
// Symmetric: end lands on a continuation byte (2nd byte of "ん").
signalled = 1809;
match (strings.bytesub("こんにちは", 0, 4)) {
case let r: str => { fail(); };
case let e: utf8.invalid => void;
};
// end == s.len bypasses the continuation check (s[end] is OOB).
signalled = 1810;
match (strings.bytesub("こんにちは", 0, 15)) {
case let s: str => { if (!streq(s, "こんにちは")) { fail(); }; };
case let e: utf8.invalid => { 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
};
// ---- fromutf8 ---------------------------------------------------------
// ref/hare/strings/utf8.ha:22 + @test fn utf8 (line 31). Validating
// sibling of fromutf8_unsafe — walks via [[utf8.validate]] and returns
// `utf8.invalid` on any malformed byte sequence (#7).
@test fn fromutf8_cases() void = {
// Hare vector — "hello world" round-trips.
signalled = 1900;
let hb: [11]u8;
hb[0] = 0x68u8; hb[1] = 0x65u8; hb[2] = 0x6Cu8; hb[3] = 0x6Cu8;
hb[4] = 0x6Fu8; hb[5] = 0x20u8; hb[6] = 0x77u8; hb[7] = 0x6Fu8;
hb[8] = 0x72u8; hb[9] = 0x6Cu8; hb[10] = 0x64u8;
match (strings.fromutf8(hb[0:11])) {
case let s: str => { if (!streq(s, "hello world")) { fail(); }; };
case let e: utf8.invalid => { fail(); };
};
// Empty input — Hare's `fromutf8([])! == ""`.
signalled = 1901;
let eb: [1]u8;
match (strings.fromutf8(eb[0:0])) {
case let s: str => { if (s.len != 0) { fail(); }; };
case let e: utf8.invalid => { fail(); };
};
// ASCII-only borrowed view: ptr aliases input.
signalled = 1902;
let s: str = "abc";
let bs: []u8 = strings.toutf8(s);
match (strings.fromutf8(bs)) {
case let r: str => {
if (!streq(r, "abc")) { fail(); };
if (r.ptr != s.ptr) { fail(); };
};
case let e: utf8.invalid => { fail(); };
};
// Multibyte Japanese — full UTF-8 round-trip.
signalled = 1903;
let jb: [15]u8;
jb[0] = 0xE3u8; jb[1] = 0x81u8; jb[2] = 0x93u8; // こ
jb[3] = 0xE3u8; jb[4] = 0x82u8; jb[5] = 0x93u8; // ん
jb[6] = 0xE3u8; jb[7] = 0x81u8; jb[8] = 0xABu8; // に
jb[9] = 0xE3u8; jb[10] = 0x81u8; jb[11] = 0xA1u8; // ち
jb[12] = 0xE3u8; jb[13] = 0x81u8; jb[14] = 0xAFu8; // は
match (strings.fromutf8(jb[0:15])) {
case let s: str => { if (!streq(s, "こんにちは")) { fail(); }; };
case let e: utf8.invalid => { fail(); };
};
// Multibyte French — mixed-width "héllo".
signalled = 1904;
let fb: [6]u8;
fb[0] = 0x68u8; // h
fb[1] = 0xC3u8; fb[2] = 0xA9u8; // é
fb[3] = 0x6Cu8; fb[4] = 0x6Cu8; fb[5] = 0x6Fu8; // llo
match (strings.fromutf8(fb[0:6])) {
case let s: str => { if (!streq(s, "héllo")) { fail(); }; };
case let e: utf8.invalid => { fail(); };
};
// Lone 0x80 continuation byte — invalid.
signalled = 1905;
let c: [1]u8;
c[0] = 0x80u8;
match (strings.fromutf8(c[0:1])) {
case let r: str => { fail(); };
case let e: utf8.invalid => void;
};
// Overlong 2-byte encoding of NUL (0xC0 0x80) — invalid per
// ref/hare/encoding/utf8/decodetable.ha (state 0 rejects 0xC0).
signalled = 1906;
let ov: [2]u8;
ov[0] = 0xC0u8; ov[1] = 0x80u8;
match (strings.fromutf8(ov[0:2])) {
case let r: str => { fail(); };
case let e: utf8.invalid => void;
};
// Truncated multibyte — 0xE3 0x81 (2 of 3 bytes for U+3053) is
// rejected as invalid by [[utf8.validate]] (state != 0 at EOI).
signalled = 1907;
let tr: [2]u8;
tr[0] = 0xE3u8; tr[1] = 0x81u8;
match (strings.fromutf8(tr[0:2])) {
case let r: str => { fail(); };
case let e: utf8.invalid => void;
};
};
// ---- iter / next ------------------------------------------------------
// ref/hare/strings/iter.ha:84-108. Hare's @test fn iter() uses prev +
// riter heavily; both are deferred (no `utf8.prev`). Rebuild forward-
// only here: empty / ASCII / 2-byte / 3-byte / 4-byte / done@EOI /
// mixed-width.
@test fn iter_empty_cases() void = {
let it: strings.iterator = strings.iter("");
match (strings.next(&it)) {
case let r: rune => { fail(); };
case utf8.done => void;
};
// Repeated next after done stays done.
match (strings.next(&it)) {
case let r: rune => { fail(); };
case utf8.done => void;
};
};
@test fn iter_ascii_cases() void = {
let it: strings.iterator = strings.iter("hi!");
match (strings.next(&it)) {
case let r: rune => { if (r != 'h') { fail(); }; };
case utf8.done => { fail(); };
};
match (strings.next(&it)) {
case let r: rune => { if (r != 'i') { fail(); }; };
case utf8.done => { fail(); };
};
match (strings.next(&it)) {
case let r: rune => { if (r != '!') { fail(); }; };
case utf8.done => { fail(); };
};
match (strings.next(&it)) {
case let r: rune => { fail(); };
case utf8.done => void;
};
};
@test fn iter_twobyte_cases() void = {
let it: strings.iterator = strings.iter("café");
let i: i32 = 0;
let expect: [4]rune;
expect[0] = 'c'; expect[1] = 'a'; expect[2] = 'f';
expect[3] = 0xE9u32: rune; // 'é' U+00E9
for (i < 4) {
match (strings.next(&it)) {
case let r: rune => { if (r != expect[i]) { fail(); }; };
case utf8.done => { fail(); };
};
i += 1;
};
match (strings.next(&it)) {
case let r: rune => { fail(); };
case utf8.done => void;
};
};
@test fn iter_threebyte_cases() void = {
let it: strings.iterator = strings.iter("こんにちは");
let i: i32 = 0;
let expect: [5]rune;
expect[0] = 0x3053u32: rune; // 'こ'
expect[1] = 0x3093u32: rune; // 'ん'
expect[2] = 0x306Bu32: rune; // 'に'
expect[3] = 0x3061u32: rune; // 'ち'
expect[4] = 0x306Fu32: rune; // 'は'
for (i < 5) {
match (strings.next(&it)) {
case let r: rune => { if (r != expect[i]) { fail(); }; };
case utf8.done => { fail(); };
};
i += 1;
};
match (strings.next(&it)) {
case let r: rune => { fail(); };
case utf8.done => void;
};
};
@test fn iter_fourbyte_cases() void = {
let it: strings.iterator = strings.iter("🦀rust");
let i: i32 = 0;
let expect: [5]rune;
expect[0] = 0x1F980u32: rune; // '🦀'
expect[1] = 'r'; expect[2] = 'u'; expect[3] = 's'; expect[4] = 't';
for (i < 5) {
match (strings.next(&it)) {
case let r: rune => { if (r != expect[i]) { fail(); }; };
case utf8.done => { fail(); };
};
i += 1;
};
match (strings.next(&it)) {
case let r: rune => { fail(); };
case utf8.done => void;
};
};
@test fn iter_mixed_cases() void = {
// "Hello, 世界! 🌍" — 1+1+1+1+1+1+1+3+3+1+1+4 = 12 runes,
// widths 1/3/4 mixed.
let it: strings.iterator = strings.iter("Hello, 世界! 🌍");
let i: i32 = 0;
let expect: [12]rune;
expect[0] = 'H'; expect[1] = 'e'; expect[2] = 'l'; expect[3] = 'l';
expect[4] = 'o'; expect[5] = ','; expect[6] = ' ';
expect[7] = 0x4E16u32: rune; // '世'
expect[8] = 0x754Cu32: rune; // '界'
expect[9] = '!'; expect[10] = ' ';
expect[11] = 0x1F30Du32: rune; // '🌍'
for (i < 12) {
match (strings.next(&it)) {
case let r: rune => { if (r != expect[i]) { fail(); }; };
case utf8.done => { fail(); };
};
i += 1;
};
match (strings.next(&it)) {
case let r: rune => { fail(); };
case utf8.done => void;
};
};
// ---- prev / riter / iterstr / slice / position -----------------------
// ref/hare/strings/iter.ha:84-127. The Hare @test fn iter body uses
// `s = riter(...)` mid-test to swap the iterator's direction; ww's
// sret-into-existing-slot path handles that fine (probed pre-port).
@test fn iter_prev_at_start_cases() void = {
let it: strings.iterator = strings.iter("hi");
match (strings.prev(&it)) {
case utf8.done => void;
case let r: rune => { fail(); };
};
};
@test fn iter_prev_ascii_cases() void = {
let it: strings.iterator = strings.iter("abc");
match (strings.next(&it)) {
case let r: rune => { if (r != 'a') { fail(); }; };
case utf8.done => { fail(); };
};
match (strings.prev(&it)) {
case let r: rune => { if (r != 'a') { fail(); }; };
case utf8.done => { fail(); };
};
match (strings.prev(&it)) {
case utf8.done => void;
case let r: rune => { fail(); };
};
};
// Mirror of ref/hare/strings/iter.ha:84-108 — `iter("こんにちは")`,
// step+back+iterstr+riter-reassign sequence.
@test fn iter_full_cases() void = {
let s: strings.iterator = strings.iter("こんにちは");
match (strings.prev(&s)) {
case utf8.done => void;
case let r: rune => { fail(); };
};
let expect1: [2]rune;
expect1[0] = 0x3053u32: rune; // 'こ'
expect1[1] = 0x3093u32: rune; // 'ん'
let i: i32 = 0;
for (i < 2) {
match (strings.next(&s)) {
case let r: rune => { if (r != expect1[i]) { fail(); }; };
case utf8.done => { fail(); };
};
i += 1;
};
if (!streq(strings.iterstr(&s), "にちは")) { fail(); };
match (strings.prev(&s)) {
case let r: rune => { if (r != 0x3093u32: rune) { fail(); }; }; // 'ん'
case utf8.done => { fail(); };
};
let expect2: [4]rune;
expect2[0] = 0x3093u32: rune; // 'ん'
expect2[1] = 0x306Bu32: rune; // 'に'
expect2[2] = 0x3061u32: rune; // 'ち'
expect2[3] = 0x306Fu32: rune; // 'は'
i = 0;
for (i < 4) {
match (strings.next(&s)) {
case let r: rune => { if (r != expect2[i]) { fail(); }; };
case utf8.done => { fail(); };
};
i += 1;
};
match (strings.next(&s)) {
case utf8.done => void;
case let r: rune => { fail(); };
};
// Repeated next-after-done stays done.
match (strings.next(&s)) {
case utf8.done => void;
case let r: rune => { fail(); };
};
match (strings.prev(&s)) {
case let r: rune => { if (r != 0x306Fu32: rune) { fail(); }; }; // 'は'
case utf8.done => { fail(); };
};
// Swap to a reverse iterator. sret-into-existing-slot.
s = strings.riter("にちは");
let expect3: [3]rune;
expect3[0] = 0x306Fu32: rune; // 'は'
expect3[1] = 0x3061u32: rune; // 'ち'
expect3[2] = 0x306Bu32: rune; // 'に'
i = 0;
for (i < 3) {
match (strings.next(&s)) {
case let r: rune => { if (r != expect3[i]) { fail(); }; };
case utf8.done => { fail(); };
};
i += 1;
};
match (strings.next(&s)) {
case utf8.done => void;
case let r: rune => { fail(); };
};
match (strings.prev(&s)) {
case let r: rune => { if (r != 0x306Bu32: rune) { fail(); }; }; // 'に'
case utf8.done => { fail(); };
};
};
@test fn iter_position_cases() void = {
let it: strings.iterator = strings.iter("café"); // 5 bytes: c-a-f-é(2)
if (strings.position(&it) != 0) { fail(); };
match (strings.next(&it)) { case let r: rune => void; case utf8.done => { fail(); }; };
if (strings.position(&it) != 1) { fail(); };
match (strings.next(&it)) { case let r: rune => void; case utf8.done => { fail(); }; };
match (strings.next(&it)) { case let r: rune => void; case utf8.done => { fail(); }; };
if (strings.position(&it) != 3) { fail(); };
match (strings.next(&it)) { case let r: rune => void; case utf8.done => { fail(); }; };
if (strings.position(&it) != 5) { fail(); };
};
// ref/hare/strings/iter.ha:110 @test fn slice. Hare uses `let t = s;`
// to copy the iterator; ww re-initialises t from the same source to
// stay in scope of #32 (local struct ident rhs already fixed) without
// reaching for #35's sibling latents.
@test fn iter_slice_cases() void = {
let s: strings.iterator = strings.iter("こんにちは");
let t: strings.iterator = strings.iter("こんにちは");
if (strings.slice(&s, &t).len != 0) { fail(); };
if (strings.slice(&t, &s).len != 0) { fail(); };
let i: i32 = 0;
for (i < 2) {
match (strings.next(&s)) {
case let r: rune => void;
case utf8.done => { fail(); };
};
match (strings.next(&t)) {
case let r: rune => void;
case utf8.done => { fail(); };
};
i += 1;
};
if (strings.slice(&s, &t).len != 0) { fail(); };
if (strings.slice(&t, &s).len != 0) { fail(); };
i = 0;
for (i < 3) {
match (strings.next(&t)) {
case let r: rune => void;
case utf8.done => { fail(); };
};
i += 1;
};
if (!streq(strings.slice(&s, &t), "にちは")) { fail(); };
i = 0;
for (i < 3) {
match (strings.next(&s)) {
case let r: rune => void;
case utf8.done => { fail(); };
};
i += 1;
};
if (strings.slice(&s, &t).len != 0) { fail(); };
if (strings.slice(&t, &s).len != 0) { fail(); };
};
@test fn iter_iterstr_reverse_cases() void = {
// Reverse iter: iterstr is `src[0:offs]` — bytes BEFORE the cursor
// (the still-to-be-walked region in reverse direction).
let rit: strings.iterator = strings.riter("hello");
if (!streq(strings.iterstr(&rit), "hello")) { fail(); };
match (strings.next(&rit)) { case let r: rune => void; case utf8.done => { fail(); }; };
if (!streq(strings.iterstr(&rit), "hell")) { fail(); };
match (strings.next(&rit)) { case let r: rune => void; case utf8.done => { fail(); }; };
match (strings.next(&rit)) { case let r: rune => void; case utf8.done => { fail(); }; };
if (!streq(strings.iterstr(&rit), "he")) { fail(); };
};
// ---- tokenize / rtokenize / peek_token / remaining_tokens -----------
// ref/hare/strings/tokenize.ha:110 @test fn tokenize. Row drivers
// mirror lib/bytes/bytestest expect_token / expect_done but compare
// str via streq.
fn expect_str_token(t: *strings.tokenizer, want: str) void = {
match (strings.peek_token(t)) {
case let p: str => { if (!streq(p, want)) { fail(); }; };
case bytes.done => { fail(); };
};
match (strings.next_token(t)) {
case let n: str => { if (!streq(n, want)) { fail(); }; };
case bytes.done => { fail(); };
};
};
fn expect_str_done(t: *strings.tokenizer) void = {
match (strings.peek_token(t)) {
case let p: str => { fail(); };
case bytes.done => void;
};
match (strings.next_token(t)) {
case let n: str => { fail(); };
case bytes.done => void;
};
};
@test fn tokenize_cases() void = {
// Hare vector — single-space delim. ref/hare/strings/tokenize.ha:112.
signalled = 1700;
let t: strings.tokenizer = strings.tokenize(
"Hello world! My name is Harriet.", " ");
expect_str_token(&t, "Hello");
expect_str_token(&t, "world!");
expect_str_token(&t, "My");
expect_str_token(&t, "name");
expect_str_token(&t, "is");
expect_str_token(&t, "Harriet.");
expect_str_done(&t);
// Multi-byte delim set — space + tab.
// ref/hare/strings/tokenize.ha:124.
signalled = 1701;
let t2: strings.tokenizer = strings.tokenize(
"/dev/sda1\t/ ext4 rw,relatime\t0 0", " \t");
expect_str_token(&t2, "/dev/sda1");
expect_str_token(&t2, "/");
expect_str_token(&t2, "ext4");
expect_str_token(&t2, "rw,relatime");
expect_str_token(&t2, "0");
expect_str_token(&t2, "0");
expect_str_done(&t2);
// Consecutive delimiters — empty interior tokens.
// ref/hare/strings/tokenize.ha:136.
signalled = 1702;
let t3: strings.tokenizer = strings.tokenize("hello world", " ");
expect_str_token(&t3, "hello");
expect_str_token(&t3, "");
expect_str_token(&t3, "");
expect_str_token(&t3, "");
expect_str_token(&t3, "world");
expect_str_done(&t3);
// Leading + trailing delimiters yield empty tokens.
// ref/hare/strings/tokenize.ha:147.
signalled = 1703;
let t4: strings.tokenizer = strings.tokenize(" hello world ", " ");
expect_str_token(&t4, "");
expect_str_token(&t4, "hello");
expect_str_token(&t4, "world");
expect_str_token(&t4, "");
expect_str_done(&t4);
// No delim hit — single full token.
signalled = 1704;
let t5: strings.tokenizer = strings.tokenize("abc", " ");
expect_str_token(&t5, "abc");
expect_str_done(&t5);
// Empty input — done immediately.
signalled = 1705;
let t6: strings.tokenizer = strings.tokenize("", " ");
expect_str_done(&t6);
};
@test fn rtokenize_cases() void = {
// Reverse direction — first next_token is the last token.
signalled = 1710;
let t: strings.tokenizer = strings.rtokenize(
"Hello world! My name is Harriet.", " ");
expect_str_token(&t, "Harriet.");
expect_str_token(&t, "is");
expect_str_token(&t, "name");
expect_str_token(&t, "My");
expect_str_token(&t, "world!");
expect_str_token(&t, "Hello");
expect_str_done(&t);
// Multi-byte delim set, reverse direction.
signalled = 1711;
let t2: strings.tokenizer = strings.rtokenize("a b\tc", " \t");
expect_str_token(&t2, "c");
expect_str_token(&t2, "b");
expect_str_token(&t2, "a");
expect_str_done(&t2);
// Empty input — done immediately.
signalled = 1712;
let t3: strings.tokenizer = strings.rtokenize("", " ");
expect_str_done(&t3);
};
@test fn peek_token_cases() void = {
// Two peeks without advancing return the same token.
signalled = 1720;
let t: strings.tokenizer = strings.tokenize("a b c", " ");
match (strings.peek_token(&t)) {
case let p: str => { if (!streq(p, "a")) { fail(); }; };
case bytes.done => { fail(); };
};
match (strings.peek_token(&t)) {
case let p: str => { if (!streq(p, "a")) { fail(); }; };
case bytes.done => { fail(); };
};
// Advance once — peek then returns "b".
signalled = 1721;
match (strings.next_token(&t)) {
case let n: str => { if (!streq(n, "a")) { fail(); }; };
case bytes.done => { fail(); };
};
match (strings.peek_token(&t)) {
case let p: str => { if (!streq(p, "b")) { fail(); }; };
case bytes.done => { fail(); };
};
// Empty input — peek is done.
signalled = 1722;
let t2: strings.tokenizer = strings.tokenize("", " ");
match (strings.peek_token(&t2)) {
case let p: str => { fail(); };
case bytes.done => void;
};
};
@test fn remaining_tokens_cases() void = {
// ref/hare/strings/tokenize.ha:157. After 2 next_tokens, remaining
// is "My name is Harriet.".
signalled = 1730;
let t: strings.tokenizer = strings.tokenize(
"Hello world! My name is Harriet.", " ");
match (strings.next_token(&t)) {
case let n: str => { if (!streq(n, "Hello")) { fail(); }; };
case bytes.done => { fail(); };
};
match (strings.next_token(&t)) {
case let n: str => { if (!streq(n, "world!")) { fail(); }; };
case bytes.done => { fail(); };
};
if (!streq(strings.remaining_tokens(&t), "My name is Harriet.")) {
fail();
};
// Fresh tokenizer — remaining_tokens is the whole input.
signalled = 1731;
let t2: strings.tokenizer = strings.tokenize("a b c", " ");
if (!streq(strings.remaining_tokens(&t2), "a b c")) { fail(); };
};
// ---- splitn / rsplitn / split ----------------------------------------
// ref/hare/strings/tokenize.ha:245 @test fn split. Hare's vectors
// mirrored here directly; element reads go through `&toks.ptr[i]: *str`
// rather than `toks[i]` so the 16B str element copy stays out of the
// multi-word-store gap noted at cmd/w6c/cgen.c:6515.
fn expect_str(toks: []str, i: i32, want: str) void = {
if (i >= toks.len) { fail(); };
let p: *str = &toks.ptr[i];
if (p.len != want.len) { fail(); };
let j: i32 = 0;
for (j < want.len) {
if (p.ptr[j] != want[j]) { fail(); };
j += 1;
};
};
@test fn splitn_cases() void = {
// ref/hare/strings/tokenize.ha:247 — n=4 buckets the trailing
// "is Drew" as the remainder slot.
signalled = 1740;
let t1: []str = strings.splitn("Hello, my name is Drew", " ", 4);
if (t1.len != 4) { fail(); };
expect_str(t1, 0, "Hello,");
expect_str(t1, 1, "my");
expect_str(t1, 2, "name");
expect_str(t1, 3, "is Drew");
os.free(t1.ptr: *void, (t1.cap: u64) * 16u64);
// ref/hare/strings/tokenize.ha:263 — n > tokens leaves a single
// slot holding the unchanged input (delim not found).
signalled = 1741;
let t2: []str = strings.splitn("one", "=", 2);
if (t2.len != 1) { fail(); };
expect_str(t2, 0, "one");
os.free(t2.ptr: *void, (t2.cap: u64) * 16u64);
// n == 1 — single slot holding the whole input as remainder.
signalled = 1742;
let t3: []str = strings.splitn("a b c", " ", 1);
if (t3.len != 1) { fail(); };
expect_str(t3, 0, "a b c");
os.free(t3.ptr: *void, (t3.cap: u64) * 16u64);
// Empty input — empty result.
signalled = 1743;
let t4: []str = strings.splitn("", " ", 5);
if (t4.len != 0) { fail(); };
if (t4.cap > 0) {
os.free(t4.ptr: *void, (t4.cap: u64) * 16u64);
};
// Multi-byte delim set (byte-set semantics per
// ref/hare/strings/tokenize.ha:35) — split on ',' OR ':' OR ';'.
signalled = 1744;
let t5: []str = strings.splitn("hello;world,foo:bar", ",:;", 10);
if (t5.len != 4) { fail(); };
expect_str(t5, 0, "hello");
expect_str(t5, 1, "world");
expect_str(t5, 2, "foo");
expect_str(t5, 3, "bar");
os.free(t5.ptr: *void, (t5.cap: u64) * 16u64);
};
@test fn rsplitn_cases() void = {
// ref/hare/strings/tokenize.ha:271 — reverse n=4 with the
// "Hello, my" prefix as the remainder slot at index 0.
signalled = 1750;
let t1: []str = strings.rsplitn("Hello, my name is Drew", " ", 4);
if (t1.len != 4) { fail(); };
expect_str(t1, 0, "Hello, my");
expect_str(t1, 1, "name");
expect_str(t1, 2, "is");
expect_str(t1, 3, "Drew");
os.free(t1.ptr: *void, (t1.cap: u64) * 16u64);
// n > token count — done short-circuit returns toks UN-reversed
// (last-token-first order). Mirrors bytes.rsplitn (Hare's
// ref/hare/strings/tokenize.ha:219-224 reverse step is gated
// behind the n-1 loop completion).
signalled = 1751;
let t2: []str = strings.rsplitn("a b c", " ", 10);
if (t2.len != 3) { fail(); };
expect_str(t2, 0, "c");
expect_str(t2, 1, "b");
expect_str(t2, 2, "a");
os.free(t2.ptr: *void, (t2.cap: u64) * 16u64);
// n == 1 — single slot holding the whole input as remainder.
signalled = 1752;
let t3: []str = strings.rsplitn("a b c", " ", 1);
if (t3.len != 1) { fail(); };
expect_str(t3, 0, "a b c");
os.free(t3.ptr: *void, (t3.cap: u64) * 16u64);
// delim absent — first next_token yields the entire input as the
// sole token; second iter sees done and short-circuits with the
// 1-elem toks un-reversed (single element, reverse is a no-op).
signalled = 1753;
let t4: []str = strings.rsplitn("abc", "=", 5);
if (t4.len != 1) { fail(); };
expect_str(t4, 0, "abc");
os.free(t4.ptr: *void, (t4.cap: u64) * 16u64);
};
@test fn split_cases() void = {
// ref/hare/strings/tokenize.ha:255 — full split, every delim hit
// is a boundary.
signalled = 1760;
let t1: []str = strings.split("Hello, my name is Drew", " ");
if (t1.len != 5) { fail(); };
expect_str(t1, 0, "Hello,");
expect_str(t1, 1, "my");
expect_str(t1, 2, "name");
expect_str(t1, 3, "is");
expect_str(t1, 4, "Drew");
os.free(t1.ptr: *void, (t1.cap: u64) * 16u64);
// Leading + trailing delim — empty tokens at ends.
signalled = 1761;
let t2: []str = strings.split(" a b ", " ");
if (t2.len != 4) { fail(); };
expect_str(t2, 0, "");
expect_str(t2, 1, "a");
expect_str(t2, 2, "b");
expect_str(t2, 3, "");
os.free(t2.ptr: *void, (t2.cap: u64) * 16u64);
// Multi-byte delim set, byte-set semantics matching Hare's
// strings::split example at ref/hare/strings/tokenize.ha:235.
signalled = 1762;
let t3: []str = strings.split("hello;world,foo:bar", ",:;");
if (t3.len != 4) { fail(); };
expect_str(t3, 0, "hello");
expect_str(t3, 1, "world");
expect_str(t3, 2, "foo");
expect_str(t3, 3, "bar");
os.free(t3.ptr: *void, (t3.cap: u64) * 16u64);
};
// ---- lpad / rpad -----------------------------------------------------
// ref/hare/strings/pad.ha:23 (@test fn lpad), :54 (@test fn rpad). Hare's
// row triple (s<maxlen, s==maxlen, s=="" empty) is mirrored; ww extras
// pin s>maxlen (early return path), multibyte s with byte-length
// counting, and multibyte pad rune (encoded width >1 byte). The
// multibyte-pad rows pin Hare's `[..maxlen]` slice contract — a
// multibyte trailing pad may be truncated mid-codepoint, exactly as
// Hare does.
@test fn lpad_cases() void = {
// Hare row 1: shorter s, ASCII pad.
signalled = 1770;
let r1: str = strings.lpad("2", '0', 5);
if (!streq(r1, "00002")) { fail(); };
if (r1.len != 5) { fail(); };
os.free(r1.ptr: *void, r1.len: u64);
// Hare row 2: s.len == maxlen — early dup path.
signalled = 1771;
let r2: str = strings.lpad("12345", '0', 5);
if (!streq(r2, "12345")) { fail(); };
if (r2.len != 5) { fail(); };
os.free(r2.ptr: *void, r2.len: u64);
// Hare row 3: empty s, full-width pad.
signalled = 1772;
let r3: str = strings.lpad("", '0', 5);
if (!streq(r3, "00000")) { fail(); };
if (r3.len != 5) { fail(); };
os.free(r3.ptr: *void, r3.len: u64);
// ww row 1: s.len > maxlen — early dup path returns input copy.
signalled = 1773;
let r4: str = strings.lpad("abcdef", '_', 3);
if (!streq(r4, "abcdef")) { fail(); };
if (r4.len != 6) { fail(); };
os.free(r4.ptr: *void, r4.len: u64);
// ww row 2: multibyte s — byte-length contract (Hare `len(s)`).
// "café" is 5 bytes; maxlen 7 → 2 pad bytes prepended.
signalled = 1774;
let r5: str = strings.lpad("café", '_', 7);
if (!streq(r5, "__café")) { fail(); };
if (r5.len != 7) { fail(); };
os.free(r5.ptr: *void, r5.len: u64);
// ww row 3: multibyte pad rune. 'α' U+03B1 is 2 bytes (0xCE 0xB1).
// s="x" (1 byte), maxlen=5 → npads = 5 - 1 = 4, pad_bytes=2,
// total writes = 4*2 + 1 = 9 bytes; result `[..5]` = "αα" (4 bytes)
// + 0xCE (truncated leading byte of next α) — exactly Hare's slice.
signalled = 1775;
let r6: str = strings.lpad("x", 0x03B1u32: rune, 5);
if (r6.len != 5) { fail(); };
if (r6.ptr[0] != 0xCEu8) { fail(); }; // α byte 0
if (r6.ptr[1] != 0xB1u8) { fail(); }; // α byte 1
if (r6.ptr[2] != 0xCEu8) { fail(); }; // α byte 0
if (r6.ptr[3] != 0xB1u8) { fail(); }; // α byte 1
if (r6.ptr[4] != 0xCEu8) { fail(); }; // truncated α byte 0
os.free(r6.ptr: *void, r6.len: u64);
};
@test fn rpad_cases() void = {
// Hare row 1: shorter s, ASCII pad.
signalled = 1780;
let r1: str = strings.rpad("2", '0', 5);
if (!streq(r1, "20000")) { fail(); };
if (r1.len != 5) { fail(); };
os.free(r1.ptr: *void, r1.len: u64);
// Hare row 2: s.len == maxlen — early dup path.
signalled = 1781;
let r2: str = strings.rpad("12345", '0', 5);
if (!streq(r2, "12345")) { fail(); };
if (r2.len != 5) { fail(); };
os.free(r2.ptr: *void, r2.len: u64);
// Hare row 3: empty s.
signalled = 1782;
let r3: str = strings.rpad("", '0', 5);
if (!streq(r3, "00000")) { fail(); };
if (r3.len != 5) { fail(); };
os.free(r3.ptr: *void, r3.len: u64);
// ww row 1: s.len > maxlen — early dup path.
signalled = 1783;
let r4: str = strings.rpad("abcdef", '_', 3);
if (!streq(r4, "abcdef")) { fail(); };
if (r4.len != 6) { fail(); };
os.free(r4.ptr: *void, r4.len: u64);
// ww row 2: multibyte s — byte-length contract.
signalled = 1784;
let r5: str = strings.rpad("café", '_', 7);
if (!streq(r5, "café__")) { fail(); };
if (r5.len != 7) { fail(); };
os.free(r5.ptr: *void, r5.len: u64);
// ww row 3: multibyte pad rune (2-byte 'α'). s="x" (1 byte),
// maxlen=5 → "xαα" appended is 1 + 4 = 5 bytes (no truncation).
signalled = 1785;
let r6: str = strings.rpad("x", 0x03B1u32: rune, 5);
if (r6.len != 5) { fail(); };
if (r6.ptr[0] != 'x') { fail(); };
if (r6.ptr[1] != 0xCEu8) { fail(); }; // α byte 0
if (r6.ptr[2] != 0xB1u8) { fail(); }; // α byte 1
if (r6.ptr[3] != 0xCEu8) { fail(); }; // α byte 0
if (r6.ptr[4] != 0xB1u8) { fail(); }; // α byte 1
os.free(r6.ptr: *void, r6.len: u64);
};
export fn main() i32 = {
signalled = 1; dup_cases();
signalled = 42; dupall_cases();
signalled = 2; concat_cases();
signalled = 30; join_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 = 28; index_cases();
signalled = 29; rindex_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 = 40; sub_cases();
signalled = 41; bytesub_cases();
signalled = 15; utf8_roundtrip_cases();
signalled = 43; fromutf8_cases();
signalled = 16; iter_empty_cases();
signalled = 17; iter_ascii_cases();
signalled = 18; iter_twobyte_cases();
signalled = 19; iter_threebyte_cases();
signalled = 20; iter_fourbyte_cases();
signalled = 21; iter_mixed_cases();
signalled = 22; iter_prev_at_start_cases();
signalled = 23; iter_prev_ascii_cases();
signalled = 24; iter_full_cases();
signalled = 25; iter_position_cases();
signalled = 26; iter_iterstr_reverse_cases();
signalled = 27; iter_slice_cases();
signalled = 31; tokenize_cases();
signalled = 32; rtokenize_cases();
signalled = 33; peek_token_cases();
signalled = 34; remaining_tokens_cases();
signalled = 35; splitn_cases();
signalled = 36; rsplitn_cases();
signalled = 37; split_cases();
signalled = 38; lpad_cases();
signalled = 39; rpad_cases();
return 0;
};