bytes: make search linear and splits consistent
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
41
lib/bytes/mutate_test.ww
Normal 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; };
|
||||
};
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user