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;
};