lib/bytes+test: Hare port (equal / index / rindex / contains / has{prefix,suffix} / reverse / zero)

Mirrors ref/hare/bytes/{equal,index,contains,reverse,zero}.ha for
the in-tree subset used by lib/encoding, lib/bufio, lib/memio;
converts 4 hextest sites from local beq to bytes.equal and drops
the now-dead beq in utf8test.

Surface:
  - equal(a, b: []u8) bool
  - index(s: []u8, needle: (u8 | []u8)) (i32 | void)
  - rindex(s: []u8, needle: (u8 | []u8)) (i32 | void)
  - contains(s: []u8, needle: (u8 | []u8)) bool
  - hasprefix(s, pre: []u8) bool
  - hassuffix(s, suf: []u8) bool
  - reverse(s: []u8) void  (already present, citation added)
  - zero(s: []u8) void  (already present, citation added)

Two documented Hare-fidelity gaps (cited in lib/bytes/bytes.ww
header, no in-tree caller demands them yet):
  - index_slice / rindex_slice use naive O(n·m). Hare specialises
    2/3/4-byte needles + falls back to Crochemore-Perrin two-way
    (ref/hare/bytes/two_way.ha). Correctness equivalent.
  - contains takes a single needle. Hare uses variadic
    needle: (u8 | []u8)... (ref/hare/bytes/contains.ha:5).

Tests: 967_bytes_run drives lib/bytes/bytestest.ww via ww run.
Eight @test fns × table-driven row sets: equal (5), index_byte
(5), index_slice (10), rindex_byte (3), rindex_slice (3),
contains (4), hasprefix (6 verbatim from contains.ha:25),
hassuffix (6 verbatim from contains.ha:40).

Call-site conversions in the same commit (the conversions are
the proof the API is wired): lib/encoding/hex/hextest.ww drops
the local beq helper and 4 callers switch to bytes.equal;
lib/encoding/utf8/utf8test.ww drops the dead beq helper.

91/91 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
This commit is contained in:
2026-05-18 00:48:14 +09:00
parent a7700f201b
commit a6abac22d8
6 changed files with 336 additions and 42 deletions

View File

@@ -1,18 +1,31 @@
// bytes — slice operations over []u8.
// bytes — slice operations over []u8. Mirrors Hare's bytes module
// (ref/hare/bytes/) for the in-tree subset: search/equality/prefix
// helpers used by lib/encoding, lib/bufio, lib/memio.
//
// Documented divergences from Hare:
// - index_slice / rindex_slice use naive O(n·m); Hare specialises
// 2/3/4-byte needles and falls back to two_way (Crochemore-Perrin)
// for longer (ref/hare/bytes/index.ha:61, ref/hare/bytes/two_way.ha).
// Correctness equivalent.
// - contains takes a single needle; Hare's contains is variadic
// `(u8 | []u8)...` (ref/hare/bytes/contains.ha:5). No caller needs
// the variadic shape yet; graduate when one does.
// equal — true iff `a` and `b` have the same length and contents.
// ref/hare/bytes/equal.ha:9.
export fn equal(a: []u8, b: []u8) bool = {
if (a.len != b.len) { return false; };
let i: i32 = 0;
for (i < a.len) {
if (i >= b.len) { return false; };
if (a[i] != b[i]) { return false; };
i += 1;
};
return i == b.len;
return true;
};
// index — first index of `needle` in `s`. Mirrors Hare's bytes::index:
// `u8` needle scans for the byte, `[]u8` needle scans for the
// substring. Returns void if absent.
// index — first offset of `needle` in `s`. u8 needle scans for the
// byte; []u8 needle scans for the substring. void if absent.
// ref/hare/bytes/index.ha:6.
export fn index(s: []u8, needle: (u8 | []u8)) (i32 | void) = {
match (needle) {
case let c: u8 => {
@@ -44,8 +57,9 @@ export fn index(s: []u8, needle: (u8 | []u8)) (i32 | void) = {
return;
};
// rindex — last index of `needle` in `s`. Mirrors Hare's bytes::rindex.
// Empty []u8 needle matches at s.len.
// rindex — last offset of `needle` in `s`. Empty []u8 needle returns
// s.len (ref/hare/bytes/index.ha:103 — Hare's loop yields r-0 at i=0).
// ref/hare/bytes/index.ha:86.
export fn rindex(s: []u8, needle: (u8 | []u8)) (i32 | void) = {
match (needle) {
case let c: u8 => {
@@ -76,18 +90,18 @@ export fn rindex(s: []u8, needle: (u8 | []u8)) (i32 | void) = {
return;
};
// contains — true iff `sub` appears in `s`. Mirrors Hare's
// bytes::contains for the slice case.
export fn contains(s: []u8, sub: []u8) bool = {
let r: (i32 | void) = index(s, sub);
match (r) {
// contains — true iff `needle` (byte or sub-slice) appears in `s`.
// ref/hare/bytes/contains.ha:5 (variadic subset; see header note).
export fn contains(s: []u8, needle: (u8 | []u8)) bool = {
match (index(s, needle)) {
case let i: i32 => return true;
case void => return false;
};
return false;
};
// hasprefix — `s` starts with `pre`. Mirrors Hare's bytes::hasprefix.
// hasprefix — true iff `s` starts with `pre`.
// ref/hare/bytes/contains.ha:21.
export fn hasprefix(s: []u8, pre: []u8) bool = {
if (pre.len > s.len) { return false; };
let i: i32 = 0;
@@ -98,7 +112,8 @@ export fn hasprefix(s: []u8, pre: []u8) bool = {
return true;
};
// hassuffix — `s` ends with `suf`. Mirrors Hare's bytes::hassuffix.
// hassuffix — true iff `s` ends with `suf`.
// ref/hare/bytes/contains.ha:35.
export fn hassuffix(s: []u8, suf: []u8) bool = {
if (suf.len > s.len) { return false; };
let off: i32 = s.len - suf.len;
@@ -110,7 +125,7 @@ export fn hassuffix(s: []u8, suf: []u8) bool = {
return true;
};
// reverse — in-place reverse of `s`. Mirrors Hare's bytes::reverse.
// reverse — in-place reverse of `s`. ref/hare/bytes/reverse.ha:5.
export fn reverse(s: []u8) void = {
let i: i32 = 0;
let j: i32 = s.len - 1;
@@ -123,7 +138,7 @@ export fn reverse(s: []u8) void = {
};
};
// zero — set every byte of `s` to 0. Mirrors Hare's bytes::zero.
// zero — set every byte of `s` to 0. ref/hare/bytes/zero.ha:5.
export fn zero(s: []u8) void = {
let i: i32 = 0;
for (i < s.len) {

245
lib/bytes/bytestest.ww Normal file
View File

@@ -0,0 +1,245 @@
// bytestest — exercises lib/bytes. Run with
// `out/bin/ww run lib/bytes/bytestest.ww`. Same signalled-then-
// fail()-with-+10 pattern as hex / utf8 / time tests: non-zero exit
// pinpoints the failing scenario.
//
// Vectors mirror Hare's @test fns in ref/hare/bytes/equal.ha,
// ref/hare/bytes/index.ha, ref/hare/bytes/contains.ha.
use bytes;
use os;
let signalled: i32 = 0;
fn fail() void = { os.exit(signalled + 10); };
// ---- equal ------------------------------------------------------------
// ref/hare/bytes/equal.ha:21.
@test fn equal_cases() void = {
let a: [3]u8; a[0] = 1u8; a[1] = 2u8; a[2] = 3u8;
let b: [3]u8; b[0] = 1u8; b[1] = 2u8; b[2] = 3u8;
let c: [3]u8; c[0] = 1u8; c[1] = 4u8; c[2] = 5u8;
let d: [4]u8; d[0] = 1u8; d[1] = 2u8; d[2] = 3u8; d[3] = 4u8;
let e: [2]u8; e[0] = 1u8; e[1] = 2u8;
let z: [1]u8;
if (!bytes.equal(a[0:3], b[0:3])) { fail(); };
if ( bytes.equal(a[0:3], c[0:3])) { fail(); };
if ( bytes.equal(a[0:3], d[0:4])) { fail(); };
if ( bytes.equal(a[0:3], e[0:2])) { fail(); };
if (!bytes.equal(z[0:0], z[0:0])) { fail(); }; // empty-empty
};
// ---- index(u8) --------------------------------------------------------
// ref/hare/bytes/index.ha:112.
@test fn index_byte_cases() void = {
let a: [4]u8; a[0] = 1u8; a[1] = 3u8; a[2] = 3u8; a[3] = 7u8;
match (bytes.index(a[0:4], 1u8)) {
case let i: i32 => { if (i != 0) { fail(); }; };
case void => { fail(); };
};
match (bytes.index(a[0:4], 3u8)) {
case let i: i32 => { if (i != 1) { fail(); }; };
case void => { fail(); };
};
match (bytes.index(a[0:4], 7u8)) {
case let i: i32 => { if (i != 3) { fail(); }; };
case void => { fail(); };
};
match (bytes.index(a[0:4], 42u8)) {
case let i: i32 => { fail(); };
case void => void;
};
let z: [1]u8;
match (bytes.index(z[0:0], 42u8)) {
case let i: i32 => { fail(); };
case void => void;
};
};
// ---- index([]u8) ------------------------------------------------------
// ref/hare/bytes/index.ha:128-139. Vector strings copied verbatim where
// representable as ASCII byte sequences.
@test fn index_slice_cases() void = {
let h1: [4]u8; h1[0] = 1u8; h1[1] = 42u8; h1[2] = 24u8; h1[3] = 0u8;
let n1: [2]u8; n1[0] = 42u8; n1[1] = 24u8;
match (bytes.index(h1[0:3], n1[0:2])) {
case let i: i32 => { if (i != 1) { fail(); }; };
case void => { fail(); };
};
let h2: [4]u8; h2[0] = 1u8; h2[1] = 3u8; h2[2] = 3u8; h2[3] = 7u8;
let n2: [2]u8; n2[0] = 3u8; n2[1] = 3u8;
match (bytes.index(h2[0:4], n2[0:2])) {
case let i: i32 => { if (i != 1) { fail(); }; };
case void => { fail(); };
};
// needle longer than haystack — void
let h3: [3]u8; h3[0] = 1u8; h3[1] = 2u8; h3[2] = 3u8;
let n3: [4]u8; n3[0] = 1u8; n3[1] = 2u8; n3[2] = 3u8; n3[3] = 4u8;
match (bytes.index(h3[0:3], n3[0:4])) {
case let i: i32 => { fail(); };
case void => void;
};
// len(haystack) == len(needle), match — offset 0
let h4: [2]u8; h4[0] = 42u8; h4[1] = 20u8;
let n4: [2]u8; n4[0] = 42u8; n4[1] = 20u8;
match (bytes.index(h4[0:2], n4[0:2])) {
case let i: i32 => { if (i != 0) { fail(); }; };
case void => { fail(); };
};
// len(haystack) == len(needle), no match — void
let h5: [4]u8; h5[0] = 1u8; h5[1] = 1u8; h5[2] = 1u8; h5[3] = 2u8;
let n5: [4]u8; n5[0] = 1u8; n5[1] = 1u8; n5[2] = 1u8; n5[3] = 3u8;
match (bytes.index(h5[0:4], n5[0:4])) {
case let i: i32 => { fail(); };
case void => void;
};
// Partial-prefix recovery — needle [1,1,2] aligns at i=1 after the
// false-start at i=0 ([1,1,1] mismatches at byte 2). Pins the
// naive scanner's restart discipline.
let h6: [4]u8; h6[0] = 1u8; h6[1] = 1u8; h6[2] = 1u8; h6[3] = 2u8;
let n6s: [3]u8; n6s[0] = 1u8; n6s[1] = 1u8; n6s[2] = 2u8;
match (bytes.index(h6[0:4], n6s[0:3])) {
case let i: i32 => { if (i != 1) { fail(); }; };
case void => { fail(); };
};
// Same shape, longer haystack with no match anywhere.
let h7: [5]u8; h7[0] = 1u8; h7[1] = 1u8; h7[2] = 1u8; h7[3] = 3u8; h7[4] = 2u8;
let n7: [4]u8; n7[0] = 1u8; n7[1] = 1u8; n7[2] = 1u8; n7[3] = 2u8;
match (bytes.index(h7[0:5], n7[0:4])) {
case let i: i32 => { fail(); };
case void => void;
};
// empty needle — Hare returns 0 (ref/hare/bytes/index.ha:63).
let z: [1]u8;
let zn: [1]u8;
match (bytes.index(h2[0:4], zn[0:0])) {
case let i: i32 => { if (i != 0) { fail(); }; };
case void => { fail(); };
};
// empty haystack, non-empty needle — void
match (bytes.index(z[0:0], n3[0:3])) {
case let i: i32 => { fail(); };
case void => void;
};
// single-byte slice needle — should semantically equal u8 arm
let n6: [1]u8; n6[0] = 7u8;
match (bytes.index(h2[0:4], n6[0:1])) {
case let i: i32 => { if (i != 3) { fail(); }; };
case void => { fail(); };
};
};
// ---- rindex(u8) -------------------------------------------------------
// ref/hare/bytes/index.ha:118.
@test fn rindex_byte_cases() void = {
let a: [4]u8; a[0] = 1u8; a[1] = 3u8; a[2] = 3u8; a[3] = 7u8;
match (bytes.rindex(a[0:4], 3u8)) {
case let i: i32 => { if (i != 2) { fail(); }; };
case void => { fail(); };
};
match (bytes.rindex(a[0:4], 42u8)) {
case let i: i32 => { fail(); };
case void => void;
};
let z: [1]u8;
match (bytes.rindex(z[0:0], 42u8)) {
case let i: i32 => { fail(); };
case void => void;
};
};
// ---- rindex([]u8) -----------------------------------------------------
// ref/hare/bytes/index.ha:123-125. Distinguishes from index when the
// needle appears more than once.
@test fn rindex_slice_cases() void = {
let a: [4]u8; a[0] = 1u8; a[1] = 1u8; a[2] = 1u8; a[3] = 2u8;
let n11: [2]u8; n11[0] = 1u8; n11[1] = 1u8;
match (bytes.rindex(a[0:4], n11[0:2])) {
case let i: i32 => { if (i != 1) { fail(); }; };
case void => { fail(); };
};
let n12: [2]u8; n12[0] = 1u8; n12[1] = 2u8;
match (bytes.rindex(a[0:4], n12[0:2])) {
case let i: i32 => { if (i != 2) { fail(); }; };
case void => { fail(); };
};
// absent
let n99: [2]u8; n99[0] = 9u8; n99[1] = 9u8;
match (bytes.rindex(a[0:4], n99[0:2])) {
case let i: i32 => { fail(); };
case void => void;
};
};
// ---- contains ---------------------------------------------------------
@test fn contains_cases() void = {
let a: [4]u8; a[0] = 1u8; a[1] = 3u8; a[2] = 3u8; a[3] = 7u8;
if (!bytes.contains(a[0:4], 7u8)) { fail(); };
if ( bytes.contains(a[0:4], 42u8)) { fail(); };
let n: [2]u8; n[0] = 3u8; n[1] = 3u8;
if (!bytes.contains(a[0:4], n[0:2])) { fail(); };
let m: [2]u8; m[0] = 9u8; m[1] = 9u8;
if ( bytes.contains(a[0:4], m[0:2])) { fail(); };
};
// ---- hasprefix --------------------------------------------------------
// ref/hare/bytes/contains.ha:25.
@test fn hasprefix_cases() void = {
let z: [1]u8;
if (!bytes.hasprefix(z[0:0], z[0:0])) { fail(); };
let one: [1]u8; one[0] = 0u8;
if (!bytes.hasprefix(one[0:1], z[0:0])) { fail(); };
if ( bytes.hasprefix(z[0:0], one[0:1])) { fail(); };
let a: [3]u8; a[0] = 1u8; a[1] = 2u8; a[2] = 3u8;
let p12: [2]u8; p12[0] = 1u8; p12[1] = 2u8;
if (!bytes.hasprefix(a[0:3], p12[0:2])) { fail(); };
let p11: [2]u8; p11[0] = 1u8; p11[1] = 1u8;
if ( bytes.hasprefix(a[0:3], p11[0:2])) { fail(); };
let pl: [4]u8; pl[0] = 1u8; pl[1] = 2u8; pl[2] = 3u8; pl[3] = 4u8;
if ( bytes.hasprefix(a[0:3], pl[0:4])) { fail(); };
};
// ---- hassuffix --------------------------------------------------------
// ref/hare/bytes/contains.ha:40.
@test fn hassuffix_cases() void = {
let z: [1]u8;
if (!bytes.hassuffix(z[0:0], z[0:0])) { fail(); };
let one: [1]u8; one[0] = 0u8;
if (!bytes.hassuffix(one[0:1], z[0:0])) { fail(); };
if ( bytes.hassuffix(z[0:0], one[0:1])) { fail(); };
let a: [3]u8; a[0] = 1u8; a[1] = 2u8; a[2] = 3u8;
let s23: [2]u8; s23[0] = 2u8; s23[1] = 3u8;
if (!bytes.hassuffix(a[0:3], s23[0:2])) { fail(); };
let s22: [2]u8; s22[0] = 2u8; s22[1] = 2u8;
if ( bytes.hassuffix(a[0:3], s22[0:2])) { fail(); };
let a4: [4]u8; a4[0] = 1u8; a4[1] = 2u8; a4[2] = 3u8; a4[3] = 4u8;
let s234: [3]u8; s234[0] = 2u8; s234[1] = 3u8; s234[2] = 4u8;
if (!bytes.hassuffix(a4[0:4], s234[0:3])) { fail(); };
};
export fn main() i32 = {
signalled = 1; equal_cases();
signalled = 2; index_byte_cases();
signalled = 3; index_slice_cases();
signalled = 4; rindex_byte_cases();
signalled = 5; rindex_slice_cases();
signalled = 6; contains_cases();
signalled = 7; hasprefix_cases();
signalled = 8; hassuffix_cases();
return 0;
};