bytes: make search linear and splits consistent

This commit is contained in:
2026-08-09 17:43:31 +09:00
parent 8620e64313
commit 2d947c469e
4 changed files with 178 additions and 37 deletions

View File

@@ -1,10 +1,6 @@
// Hare port of the in-tree subset; see ref/hare/bytes/.
//
// 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.
// - peektoken dispatches index/rindex by branching on `reverse`
// rather than a function-pointer `ifunc` (ref/hare/bytes/tokenize.ha:97).
// ww has no fn pointers in scope yet — same pattern as lib/strings
@@ -18,7 +14,6 @@
package bytes;
import os;
import types;
// done — iteration sentinel returned by nexttoken / peektoken at
@@ -48,6 +43,92 @@ export fn equal(a: []u8, b: []u8) bool = {
return true;
};
// Maximal suffix and period for the Crochemore-Perrin two-way search.
// The signed -1 sentinel is the i32 translation of Hare's SIZE_MAX in
// ref/hare/bytes/two_way.ha:68.
fn indexmaxsuf(x: []u8, inv: bool) (i32, i32) = {
let i: i32 = -1;
let j: i32 = 0;
let k: i32 = 1;
let p: i32 = 1;
for (j + k < x.len) {
let a: u8 = x[j + k];
let b: u8 = x[i + k];
if (a == b) {
if (k == p) { j += p; k = 1; }
else { k += 1; };
} else if ((a < b) != inv) {
j += k;
k = 1;
p = j - i;
} else {
i = j;
j += 1;
k = 1;
p = 1;
};
};
return (i, p);
};
// Allocation-free O(n+m) forward search. This follows
// ref/hare/bytes/two_way.ha, with the critical-period test stated directly on
// the needle: periodicity is a property of the needle, never the haystack.
fn indextwoway(haystack: []u8, needle: []u8) (i32 | void) = {
let n: i32 = haystack.len;
let m: i32 = needle.len;
if (n < m) { return; };
if (n == m) {
if (equal(haystack, needle)) { return 0; };
return;
};
let (i0, p0) = indexmaxsuf(needle, false);
let (i1, p1) = indexmaxsuf(needle, true);
let ms: i32 = i0;
let per: i32 = p0;
if (i0 < i1) { ms = i1; per = p1; };
let periodic: bool = per + ms + 1 <= m;
let q: i32 = 0;
for (periodic && q <= ms) {
if (needle[q] != needle[per + q]) { periodic = false; };
q += 1;
};
let mem0: i32 = 0;
if (periodic) {
mem0 = m - per;
} else {
let left: i32 = ms + 1;
let right: i32 = m - ms - 1;
if (left > right) { per = left + 1; }
else { per = right + 1; };
};
let off: i32 = 0;
let mem: i32 = 0;
for (off <= n - m) {
let i: i32 = ms + 1;
if (mem > i) { i = mem; };
for (i < m && needle[i] == haystack[off + i]) { i += 1; };
if (i < m) {
off += i - ms;
mem = 0;
continue;
};
i = ms + 1;
for (i > mem && needle[i - 1] == haystack[off + i - 1]) {
i -= 1;
};
if (i <= mem) { return off; };
off += per;
mem = mem0;
};
return;
};
// ref/hare/bytes/index.ha:6.
export fn index(s: []u8, needle: (u8 | []u8)) (i32 | void) = {
match (needle) {
@@ -61,20 +142,8 @@ export fn index(s: []u8, needle: (u8 | []u8)) (i32 | void) = {
};
case let sub: []u8 => {
if (sub.len == 0) { return 0; };
if (sub.len > s.len) { return; };
let last: i32 = s.len - sub.len;
let i: i32 = 0;
for (i <= last) {
let j: i32 = 0;
let ok: bool = true;
for (j < sub.len) {
if (s[i + j] != sub[j]) { ok = false; j = sub.len; }
else { j += 1; };
};
if (ok) { return i; };
i += 1;
};
return;
if (sub.len == 1) { return index(s, sub[0]); };
return indextwoway(s, sub);
};
};
return;
@@ -215,8 +284,6 @@ export fn zero(s: []u8) void = {
// lifetime. ref/hare/bytes/tokenize.ha:22.
export fn tokenize(in: []u8, delim: u8...) tokenizer = {
assert(delim.len > 0, "bytes.tokenize called with empty slice");
assert((in.len: i64) < types.I64_MAX,
"bytes.tokenize: input length exceeds I64_MAX");
let t: tokenizer;
t.in = in;
t.delim = delim;
@@ -231,8 +298,6 @@ export fn tokenize(in: []u8, delim: u8...) tokenizer = {
// ref/hare/bytes/tokenize.ha:40.
export fn rtokenize(in: []u8, delim: u8...) tokenizer = {
assert(delim.len > 0, "bytes.rtokenize called with empty slice");
assert((in.len: i64) < types.I64_MAX,
"bytes.rtokenize: input length exceeds I64_MAX");
let t: tokenizer;
t.in = in;
t.delim = delim;
@@ -383,10 +448,12 @@ export fn remainingtokens(s: *tokenizer) []u8 = {
export fn splitn(in: []u8, delim: []u8, n: i32) [][]u8 = {
assert(delim.len > 0,
"bytes.splitn must not be called with an empty delimiter");
assert(n >= 0, "bytes.splitn: token limit must not be negative");
let toks: [][]u8;
toks.ptr = nil: *[]u8;
toks.len = 0;
toks.cap = 0;
if (n == 0) { return toks; };
let tok: tokenizer = tokenize(in, delim...);
let i: i32 = 0;
for (i < n - 1) {
@@ -409,27 +476,22 @@ export fn splitn(in: []u8, delim: []u8, n: i32) [][]u8 = {
// The trailing slot holds the unconsumed prefix (everything before
// the n-th-from-last delim hit).
//
// When the input has fewer than n tokens, the `done` short-circuit
// returns toks UN-reversed (in last-token-first order). Mirrors Hare
// at ref/hare/bytes/tokenize.ha:196-199 where the in-place reverse
// step is gated behind the n-1 loop running to completion. Only the
// "loop ran to completion AND peek saw a remainder" path applies the
// reverse; both early-exit paths skip it.
//
// ref/hare/bytes/tokenize.ha:186.
export fn rsplitn(in: []u8, delim: []u8, n: i32) [][]u8 = {
assert(delim.len > 0,
"bytes.rsplitn called with empty delimiter");
assert(n >= 0, "bytes.rsplitn: token limit must not be negative");
let toks: [][]u8;
toks.ptr = nil: *[]u8;
toks.len = 0;
toks.cap = 0;
if (n == 0) { return toks; };
let tok: tokenizer = rtokenize(in, delim...);
let i: i32 = 0;
for (i < n - 1) {
match (nexttoken(&tok)) {
case let s: []u8 => { append(toks, s); };
case done => { return toks; };
case done => { break; };
};
i += 1;
};

View File

@@ -110,6 +110,27 @@ import bytes;
case let i: i32 => { assert(!(i != 3)); };
case void => abort();
};
// Periodic long needles exercise the two-way path's critical-period
// memory. The first candidate shares a long prefix and must be skipped
// without missing the later match; the second never matches.
let hp: [18]u8;
let np: [7]u8;
let i: i32 = 0;
for (i < 18) { hp[i] = 'a'; i += 1; };
hp[8] = 'b'; hp[17] = 'b';
i = 0;
for (i < 6) { np[i] = 'a'; i += 1; };
np[6] = 'b';
match (bytes.index(hp[0:18], np[0:7])) {
case let off: i32 => { assert(!(off != 2)); };
case void => abort();
};
np[6] = 'c';
match (bytes.index(hp[0:18], np[0:7])) {
case let off: i32 => abort();
case void => void;
};
};
// ref/hare/bytes/index.ha:118.

41
lib/bytes/mutate_test.ww Normal file
View File

@@ -0,0 +1,41 @@
// Mutation primitives mirror ref/hare/bytes/{reverse,zero}.ha.
package bytes_test;
import bytes;
@test fn reverse_cases() void = {
let z: [1]u8;
bytes.reverse(z[0:0]);
let one: [1]u8; one[0] = 7u8;
bytes.reverse(one[0:1]);
assert(!(one[0] != 7u8));
let odd: [5]u8;
let i: i32 = 0;
for (i < 5) { odd[i] = (i + 1): u8; i += 1; };
bytes.reverse(odd[0:5]);
assert(!(odd[0] != 5u8 || odd[1] != 4u8 || odd[2] != 3u8 ||
odd[3] != 2u8 || odd[4] != 1u8));
let even: [4]u8;
i = 0;
for (i < 4) { even[i] = (i + 1): u8; i += 1; };
bytes.reverse(even[0:4]);
assert(!(even[0] != 4u8 || even[1] != 3u8 || even[2] != 2u8 ||
even[3] != 1u8));
};
@test fn zero_cases() void = {
let z: [1]u8; z[0] = 9u8;
bytes.zero(z[0:0]);
assert(!(z[0] != 9u8));
let v: [5]u8;
let i: i32 = 0;
for (i < 5) { v[i] = (i + 1): u8; i += 1; };
bytes.zero(v[0:5]);
i = 0;
for (i < 5) { assert(!(v[i] != 0u8)); i += 1; };
};

View File

@@ -4,6 +4,7 @@ package bytes_test;
import bytes;
import os;
import test;
// ref/hare/bytes/tokenize.ha:258. Hare's @test fn tokenize / rtokenize
// drives the iterator through an expected-token sequence and asserts
@@ -286,6 +287,15 @@ fn expect_tok(toks: [][]u8, i: i32, want: []u8) void = {
expect_tok(t6, 1, d[3:5]);
expect_tok(t6, 2, d[6:8]);
os.free(t6.ptr: *void, (t6.cap: u64) * 24u64);
let t7: [][]u8 = bytes.splitn(b[0:5], zd[0:1], 0);
assert(!(t7.len != 0 || t7.cap != 0));
};
@test fn splitn_negative_aborts() void = {
test.expectabort();
let in: [1]u8; let delim: [1]u8; delim[0] = 1u8;
bytes.splitn(in[0:1], delim[0:1], -1);
};
@test fn rsplitn_cases() void = {
@@ -307,17 +317,15 @@ fn expect_tok(toks: [][]u8, i: i32, want: []u8) void = {
expect_tok(t1, 3, a[18:22]);
os.free(t1.ptr: *void, (t1.cap: u64) * 24u64);
// n > token count — done short-circuit returns the toks in
// reverse-iteration order (last token first). Mirrors Hare's
// behavior at ref/hare/bytes/tokenize.ha:196-199 where the
// reverse-step is gated behind the n-1 loop completion.
// n > token count still returns input order. The result order must not
// change merely because the limit exceeds the number of tokens.
let b: [5]u8; b[0] = 1u8; b[1] = 0u8; b[2] = 2u8; b[3] = 0u8; b[4] = 3u8;
let zd: [1]u8; zd[0] = 0u8;
let t2: [][]u8 = bytes.rsplitn(b[0:5], zd[0:1], 10);
assert(!(t2.len != 3));
expect_tok(t2, 0, b[4:5]);
expect_tok(t2, 0, b[0:1]);
expect_tok(t2, 1, b[2:3]);
expect_tok(t2, 2, b[0:1]);
expect_tok(t2, 2, b[4:5]);
os.free(t2.ptr: *void, (t2.cap: u64) * 24u64);
// n == 1 — single slot holding the whole input as remainder.
@@ -336,6 +344,15 @@ fn expect_tok(toks: [][]u8, i: i32, want: []u8) void = {
assert(!(t4.len != 1));
expect_tok(t4, 0, c[0:3]);
os.free(t4.ptr: *void, (t4.cap: u64) * 24u64);
let t5: [][]u8 = bytes.rsplitn(b[0:5], zd[0:1], 0);
assert(!(t5.len != 0 || t5.cap != 0));
};
@test fn rsplitn_negative_aborts() void = {
test.expectabort();
let in: [1]u8; let delim: [1]u8; delim[0] = 1u8;
bytes.rsplitn(in[0:1], delim[0:1], -1);
};
@test fn split_cases() void = {