lib/strings+test: split sub byte-wise vs rune-wise per Hare

The byte-indexed silent-clamp sub from ww was Hare's bytesub wearing
the wrong name. Renamed accordingly; added the real rune-indexed sub
per ref/hare/strings/sub.ha:30-42, with utf8bytelenbounded helper
per :10. Both forms assert on start>end; bytesub also asserts
end<=len(s).

lib/getopt/getopt.ww:314 migrated to bytesub — its bi index is a
byte offset over the arg's bytes.

Tests cover ASCII parity, multi-byte UTF-8 (こんにちは / héllo) where
rune index ≠ byte index, and a row contrasting identical args to
make the distinction explicit. OOB-abort coverage deferred until
the assert_aborts harness lands (#38).

Selfhost combined.ww snapshots regenerated — they're bootstrap-stage
inputs and would otherwise compile the old byte-wise sub. Two-arg
default form omitted (#37, ww has no default parameter values).
This commit is contained in:
2026-05-19 22:25:06 +09:00
parent 65db360b91
commit 031f5f9ec8
6 changed files with 214 additions and 49 deletions

View File

@@ -171,20 +171,46 @@ export fn join(delim: str, strs: str...) str = {
return r;
};
// sub — borrowed `s[start..end]`. ref/hare/strings/sub.ha:30 is
// rune-wise; this ww form is byte-wise (no rune iterator yet, planned
// for commit 2). Clamps out-of-range silently where Hare aborts —
// retained for the existing getopt caller; will graduate when the
// rune-wise form lands.
// utf8bytelenbounded — walk `it` forward `end` runes and return the
// resulting byte offset. ref/hare/strings/sub.ha:10. Aborts on
// short input per Hare's contract for the rune-wise [[sub]].
fn utf8bytelenbounded(it: *iterator, end: i32) i32 = {
let i: i32 = 0;
for (i < end) {
match (next(it)) {
case let r: rune => void;
case utf8.done => abort("strings.sub: index exceeds string length");
};
i += 1;
};
return it.offs;
};
// sub — borrowed substring [start, end) where start/end are rune
// indices. ref/hare/strings/sub.ha:30. Hare's 2-arg `sub(s, start)`
// defaulting end=END is omitted: ww has no default-parameter syntax
// (filed as #37). Byte-indexed counterpart: [[bytesub]].
export fn sub(s: str, start: i32, end: i32) str = {
let lo: i32 = start;
let hi: i32 = end;
if (lo < 0) { lo = 0; };
if (hi > s.len) { hi = s.len; };
if (hi < lo) { hi = lo; };
os.assert(start <= end, "strings.sub: start is higher than end");
let it: iterator = iter(s);
let starti: i32 = utf8bytelenbounded(&it, start);
let endi: i32 = utf8bytelenbounded(&it, end - start);
let r: str;
r.ptr = s.ptr + (lo: u64);
r.len = hi - lo;
r.ptr = s.ptr + (starti: u64);
r.len = endi - starti;
return r;
};
// bytesub — borrowed substring [start, end) where start/end are byte
// offsets. ref/hare/strings/sub.ha:59. Hare's validating return
// `(str | utf8::invalid)` is dropped (filed as #7); a multibyte-split
// argument will yield a sub-string whose bytes don't decode cleanly.
export fn bytesub(s: str, start: i32, end: i32) str = {
os.assert(start <= end, "strings.bytesub: start is higher than end");
os.assert(end <= s.len, "strings.bytesub: end exceeds string length");
let r: str;
r.ptr = s.ptr + (start: u64);
r.len = end - start;
return r;
};

View File

@@ -586,6 +586,65 @@ fn streq(a: str, b: str) bool = {
if (strings.compare("ABC", "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). The `is utf8::invalid` row for bytesub is
// deferred to the validating port (#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(); };
};
@test fn bytesub_cases() void = {
signalled = 1800;
if (!streq(strings.bytesub("a string", 0, 8), "a string")) { fail(); };
signalled = 1801;
if (!streq(strings.bytesub("a string", 0, 1), "a")) { fail(); };
signalled = 1802;
if (!streq(strings.bytesub("a string", 0, 3), "a s")) { fail(); };
signalled = 1803;
if (!streq(strings.bytesub("a string", 2, 8), "string")) { fail(); };
signalled = 1804;
if (!streq(strings.bytesub("a string", 4, 4), "")) { fail(); };
// Hare vector — byte indices 3..9 over "こんにちは" select "んに".
signalled = 1805;
if (!streq(strings.bytesub("こんにちは", 3, 9), "んに")) { 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(); };
if (!streq(strings.bytesub("こんにちは", 0, 3), "こ")) { fail(); };
// Borrowed view: ptr aliases input.
signalled = 1807;
let s: str = "hello";
let r: str = strings.bytesub(s, 1, 4);
if (r.ptr != s.ptr + 1u64) { fail(); };
if (r.len != 3) { fail(); };
};
// ---- toutf8 / fromutf8_unsafe roundtrip -------------------------------
// ref/hare/strings/utf8.ha:31.
@@ -1332,6 +1391,8 @@ export fn main() i32 = {
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 = 16; iter_empty_cases();
signalled = 17; iter_ascii_cases();