diff --git a/lib/bytes/bytes.ww b/lib/bytes/bytes.ww index 061a6ae2..f144db39 100644 --- a/lib/bytes/bytes.ww +++ b/lib/bytes/bytes.ww @@ -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; }; diff --git a/lib/bytes/index_test.ww b/lib/bytes/index_test.ww index d313f6a9..f67d7d9a 100644 --- a/lib/bytes/index_test.ww +++ b/lib/bytes/index_test.ww @@ -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. diff --git a/lib/bytes/mutate_test.ww b/lib/bytes/mutate_test.ww new file mode 100644 index 00000000..32b051d0 --- /dev/null +++ b/lib/bytes/mutate_test.ww @@ -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; }; +}; diff --git a/lib/bytes/tokenize_test.ww b/lib/bytes/tokenize_test.ww index a074d0cd..38bca2f4 100644 --- a/lib/bytes/tokenize_test.ww +++ b/lib/bytes/tokenize_test.ww @@ -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 = {