The flat checker scope makes ANY decl named assert/abort anywhere in the combined unit disable the builtin unit-wide (the #45 shadow shape: scope_lookup_prefer's cross-module fallback finds it). lib carried three colliding @symbol("rt_abort") shims (os, time, strconv/stof) plus the os.assert wrapper, so a bare assert(cond) in ANY program importing os mis-bound os.assert and failed arity — a hard blocker for regex fold-5 (regex.ha:660/670 bring builtin-assert mass). Ruled respell-now per the recurrence test (#45 -> #58). Delete the shims and the os.assert wrapper; every bare abort(msg) caller (regex, strings, utf8, hash, getopt, encoding/*, time, stof) now lands on the builtin, and the ~40 os.assert(c, m) sites respell to the builtin assert(c, m) — restoring the exact Hare spelling the lib ports diverged from (e.g. ref/hare/bytes/tokenize.ha:23). os.assert had no Hare counterpart (Hare's assert is a language builtin); rule-9 wrapper removed. temp/dirs/bufio already use the non-colliding rtabort spelling and keep it. Now-dead 'import os;' lines kept (pre-existing precedent: lib/strconv/strconv.ww carries one); a tree-wide dead-import sweep is a separate concern. regex.ww's if+abort workarounds citing #58 stay for the fold-5 owner to fold back into assert. combined.ww regenerated for all five selfhost tools + the smoke fixture via make.
551 lines
15 KiB
Plaintext
551 lines
15 KiB
Plaintext
// 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.
|
|
// - peek_token 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
|
|
// `move`. Outwardly identical.
|
|
// - tokenize / rtokenize zero the `delim` field on the constructed
|
|
// tokenizer when `in` is empty, rather than mutating the variadic
|
|
// param before the struct write (ref/hare/bytes/tokenize.ha:26-28).
|
|
// Semantically identical; the variadic param is borrowed and
|
|
// captured-by-value into the struct, so mutating either side
|
|
// yields the same observable state.
|
|
|
|
package bytes;
|
|
|
|
import os;
|
|
import types;
|
|
|
|
// done — iteration sentinel returned by next_token / peek_token at
|
|
// end-of-input. ref/hare/bytes/tokenize.ha uses the built-in `done`
|
|
// token; ww spells it per-package the same way lib/encoding/utf8 does
|
|
// (utf8.ww:36). Plain `void` (not `!void`): continuation signal.
|
|
export type done = void;
|
|
|
|
// tokenizer — cursor over an input slice. Layout mirrors
|
|
// ref/hare/bytes/tokenize.ha:6-10. `p` is the cached peek-position;
|
|
// I64_MAX (forward) / I64_MIN (reverse) are the unprimed sentinels.
|
|
// p < 0 also identifies a reverse-direction iterator.
|
|
export type tokenizer = struct {
|
|
in: []u8,
|
|
delim: []u8,
|
|
p: i64,
|
|
};
|
|
|
|
// 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 (a[i] != b[i]) { return false; };
|
|
i += 1;
|
|
};
|
|
return true;
|
|
};
|
|
|
|
// 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 => {
|
|
let i: i32 = 0;
|
|
for (i < s.len) {
|
|
if (s[i] == c) { return i; };
|
|
i += 1;
|
|
};
|
|
return;
|
|
};
|
|
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;
|
|
};
|
|
};
|
|
return;
|
|
};
|
|
|
|
// 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 => {
|
|
let i: i32 = s.len - 1;
|
|
for (i >= 0) {
|
|
if (s[i] == c) { return i; };
|
|
i -= 1;
|
|
};
|
|
return;
|
|
};
|
|
case let sub: []u8 => {
|
|
if (sub.len == 0) { return s.len; };
|
|
if (sub.len > s.len) { return; };
|
|
let i: i32 = s.len - sub.len;
|
|
for (i >= 0) {
|
|
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;
|
|
};
|
|
};
|
|
return;
|
|
};
|
|
|
|
// contains — true iff any of `needles` (byte or sub-slice) appears in `s`.
|
|
// ref/hare/bytes/contains.ha:6.
|
|
export fn contains(s: []u8, needles: (u8 | []u8)...) bool = {
|
|
let i: i32 = 0;
|
|
for (i < needles.len) {
|
|
match (needles[i]) {
|
|
case let b: u8 => {
|
|
match (index(s, b)) {
|
|
case let bo: i32 => return true;
|
|
case void => void;
|
|
};
|
|
};
|
|
case let n: []u8 => {
|
|
match (index(s, n)) {
|
|
case let bo: i32 => return true;
|
|
case void => void;
|
|
};
|
|
};
|
|
};
|
|
i += 1;
|
|
};
|
|
return false;
|
|
};
|
|
|
|
// ltrim — borrowed view of `in` with leading bytes in `trim` stripped.
|
|
// `trim` must be non-empty. ref/hare/bytes/trim.ha:7.
|
|
export fn ltrim(in: []u8, trim: u8...) []u8 = {
|
|
assert(trim.len > 0, "bytes.ltrim called with empty trim set");
|
|
let i: i32 = 0;
|
|
for (i < in.len && contains(trim, in[i])) { i += 1; };
|
|
let r: []u8;
|
|
r.ptr = in.ptr + (i: u64);
|
|
r.len = in.len - i;
|
|
r.cap = r.len;
|
|
return r;
|
|
};
|
|
|
|
// rtrim — borrowed view of `in` with trailing bytes in `trim` stripped.
|
|
// `trim` must be non-empty. ref/hare/bytes/trim.ha:17. Hare's loop uses
|
|
// `size` underflow at i==0 to terminate; ww indices are signed i32, so
|
|
// the equivalent termination is spelled `i >= 0` explicitly.
|
|
export fn rtrim(in: []u8, trim: u8...) []u8 = {
|
|
assert(trim.len > 0, "bytes.rtrim called with empty trim set");
|
|
let i: i32 = in.len - 1;
|
|
for (i >= 0 && contains(trim, in[i])) { i -= 1; };
|
|
let r: []u8;
|
|
r.ptr = in.ptr;
|
|
r.len = i + 1;
|
|
r.cap = r.len;
|
|
return r;
|
|
};
|
|
|
|
// trim — borrowed view of `in` with both ends in `trim` stripped.
|
|
// ref/hare/bytes/trim.ha:27.
|
|
export fn trim(in: []u8, trim: u8...) []u8 = {
|
|
return ltrim(rtrim(in, trim...), trim...);
|
|
};
|
|
|
|
// 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;
|
|
for (i < pre.len) {
|
|
if (s[i] != pre[i]) { return false; };
|
|
i += 1;
|
|
};
|
|
return true;
|
|
};
|
|
|
|
// 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;
|
|
let i: i32 = 0;
|
|
for (i < suf.len) {
|
|
if (s[off + i] != suf[i]) { return false; };
|
|
i += 1;
|
|
};
|
|
return true;
|
|
};
|
|
|
|
// 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;
|
|
for (i < j) {
|
|
let t: u8 = s[i];
|
|
s[i] = s[j];
|
|
s[j] = t;
|
|
i += 1;
|
|
j -= 1;
|
|
};
|
|
};
|
|
|
|
// 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) {
|
|
s[i] = 0u8;
|
|
i += 1;
|
|
};
|
|
};
|
|
|
|
// tokenize — iterator yielding tokens from `in` separated by any byte
|
|
// in `delim`. Leading / trailing / adjacent delims yield empty tokens.
|
|
// `delim` is borrowed; caller keeps it valid for the tokenizer's
|
|
// 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;
|
|
if (in.len == 0) {
|
|
t.delim.len = 0;
|
|
t.delim.cap = 0;
|
|
};
|
|
t.p = types.I64_MAX;
|
|
return t;
|
|
};
|
|
|
|
// rtokenize — reverse-direction tokenize. First next_token yields the
|
|
// last token, last next_token yields the first. 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;
|
|
if (in.len == 0) {
|
|
t.delim.len = 0;
|
|
t.delim.cap = 0;
|
|
};
|
|
t.p = types.I64_MIN;
|
|
return t;
|
|
};
|
|
|
|
// peek_token — next token without advancing the cursor. Returns done
|
|
// once `s.delim` has been zeroed by a prior past-end next_token.
|
|
// ref/hare/bytes/tokenize.ha:91.
|
|
export fn peek_token(s: *tokenizer) ([]u8 | done) = {
|
|
if (s.delim.len == 0) {
|
|
let d: done; return d;
|
|
};
|
|
|
|
let reverse: bool = s.p < 0i64;
|
|
let known: bool = false;
|
|
if (reverse) {
|
|
if (s.p != types.I64_MIN) { known = true; };
|
|
} else {
|
|
if (s.p != types.I64_MAX) { known = true; };
|
|
};
|
|
if (!known) {
|
|
let i: i64 = types.I64_MAX;
|
|
if (reverse) { i = types.I64_MIN; };
|
|
let dlen: i64 = 0i64;
|
|
let slen: i64 = s.in.len: i64;
|
|
|
|
let k: i32 = 0;
|
|
for (k < s.delim.len) {
|
|
let d: u8 = s.delim[k];
|
|
let ix_found: bool = false;
|
|
let ix_val: i32 = 0;
|
|
if (reverse) {
|
|
match (rindex(s.in, d)) {
|
|
case let v: i32 => { ix_found = true; ix_val = v; };
|
|
case void => void;
|
|
};
|
|
} else {
|
|
match (index(s.in, d)) {
|
|
case let v: i32 => { ix_found = true; ix_val = v; };
|
|
case void => void;
|
|
};
|
|
};
|
|
if (ix_found) {
|
|
if (!reverse) {
|
|
if ((ix_val: i64) < i) { i = ix_val: i64; dlen = 1i64; };
|
|
} else {
|
|
if ((ix_val: i64) > i) { i = ix_val: i64; dlen = 1i64; };
|
|
};
|
|
} else {
|
|
if (!reverse) {
|
|
if (slen < i) { i = slen; };
|
|
} else {
|
|
if (0i64 > i) { i = 0i64; };
|
|
};
|
|
};
|
|
k += 1;
|
|
};
|
|
|
|
if (reverse) {
|
|
if (i == slen) {
|
|
s.p = -(slen + 1i64);
|
|
} else {
|
|
s.p = i + dlen - slen - 1i64;
|
|
};
|
|
} else {
|
|
s.p = i;
|
|
};
|
|
};
|
|
|
|
let r: []u8;
|
|
if (reverse) {
|
|
let start: i32 = (s.in.len: i64 + s.p + 1i64): i32;
|
|
r.ptr = s.in.ptr + (start: u64);
|
|
r.len = s.in.len - start;
|
|
r.cap = r.len;
|
|
} else {
|
|
let end: i32 = s.p: i32;
|
|
r.ptr = s.in.ptr;
|
|
r.len = end;
|
|
r.cap = end;
|
|
};
|
|
return r;
|
|
};
|
|
|
|
// next_token — current token, then advance past it and the delim.
|
|
// Once the input is exhausted, returns done and zeros `s.delim` so
|
|
// subsequent peeks short-circuit. ref/hare/bytes/tokenize.ha:59.
|
|
export fn next_token(s: *tokenizer) ([]u8 | done) = {
|
|
let b: []u8;
|
|
match (peek_token(s)) {
|
|
case let v: []u8 => { b = v; };
|
|
case done => { let d: done; return d; };
|
|
};
|
|
|
|
let slen: i64 = s.in.len: i64;
|
|
let reverse: bool = s.p < 0i64;
|
|
if (reverse) {
|
|
if (slen + s.p + 1i64 == 0i64) {
|
|
s.delim.len = 0;
|
|
s.delim.cap = 0;
|
|
s.in.len = 0;
|
|
s.in.cap = 0;
|
|
} else {
|
|
let end: i32 = (slen + s.p + 1i64 - 1i64): i32;
|
|
s.in.len = end;
|
|
s.in.cap = end;
|
|
};
|
|
s.p = types.I64_MIN;
|
|
} else {
|
|
if (s.p == slen) {
|
|
s.delim.len = 0;
|
|
s.delim.cap = 0;
|
|
s.in.len = 0;
|
|
s.in.cap = 0;
|
|
} else {
|
|
let adv: u64 = (s.p: u64) + 1u64;
|
|
let adv_i32: i32 = (s.p: i32) + 1;
|
|
s.in.ptr = s.in.ptr + adv;
|
|
s.in.len = s.in.len - adv_i32;
|
|
s.in.cap = s.in.cap - adv_i32;
|
|
};
|
|
s.p = types.I64_MAX;
|
|
};
|
|
return b;
|
|
};
|
|
|
|
// remaining_tokens — the unconsumed portion of `s.in`. Read-only view.
|
|
// ref/hare/bytes/tokenize.ha:145.
|
|
export fn remaining_tokens(s: *tokenizer) []u8 = {
|
|
return s.in;
|
|
};
|
|
|
|
// splitn — split `in` on any byte in `delim`, returning up to `n`
|
|
// tokens via forward iteration. The trailing slot (when more than
|
|
// `n - 1` tokens exist) holds the unconsumed remainder.
|
|
//
|
|
// The caller frees the returned slice via
|
|
// `os.free(r.ptr: *void, (r.cap: u64) * 24u64)`. Element bytes are
|
|
// borrowed from `in`.
|
|
//
|
|
// Hare's `([][]u8 | nomem)` collapses to `[][]u8` here: ww os.alloc
|
|
// has no recoverable failure path. Same precedent as
|
|
// shlex.split / getopt.tryparse.
|
|
//
|
|
// ref/hare/bytes/tokenize.ha:156.
|
|
export fn splitn(in: []u8, delim: []u8, n: i32) [][]u8 = {
|
|
assert(delim.len > 0,
|
|
"bytes.splitn must not be called with an empty delimiter");
|
|
let toks: [][]u8;
|
|
toks.ptr = nil: *[]u8;
|
|
toks.len = 0;
|
|
toks.cap = 0;
|
|
let tok: tokenizer = tokenize(in, delim...);
|
|
let i: i32 = 0;
|
|
for (i < n - 1) {
|
|
match (next_token(&tok)) {
|
|
case let s: []u8 => { append(toks, s); };
|
|
case done => { return toks; };
|
|
};
|
|
i += 1;
|
|
};
|
|
match (peek_token(&tok)) {
|
|
case done => void;
|
|
case let pk: []u8 => {
|
|
let r: []u8 = remaining_tokens(&tok);
|
|
append(toks, r);
|
|
};
|
|
};
|
|
return toks;
|
|
};
|
|
|
|
// rsplitn — reverse-direction counterpart to [[splitn]]: tokens are
|
|
// collected from the end of `in`. 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");
|
|
let toks: [][]u8;
|
|
toks.ptr = nil: *[]u8;
|
|
toks.len = 0;
|
|
toks.cap = 0;
|
|
let tok: tokenizer = rtokenize(in, delim...);
|
|
let i: i32 = 0;
|
|
for (i < n - 1) {
|
|
match (next_token(&tok)) {
|
|
case let s: []u8 => { append(toks, s); };
|
|
case done => { return toks; };
|
|
};
|
|
i += 1;
|
|
};
|
|
match (peek_token(&tok)) {
|
|
case done => void;
|
|
case let pk: []u8 => {
|
|
let r: []u8 = remaining_tokens(&tok);
|
|
append(toks, r);
|
|
};
|
|
};
|
|
|
|
// In-place reverse so callers see argv-order, matching Hare
|
|
// (ref/hare/bytes/tokenize.ha:207). Element copy is field-wise
|
|
// through `*[]u8` because `toks[i] = toks[j]` (full 24B slice
|
|
// store) lands in the multi-word-store gap noted at
|
|
// cmd/w6c/cgen.c:6515-6523.
|
|
let a: i32 = 0;
|
|
let b: i32 = toks.len - 1;
|
|
for (a < b) {
|
|
let pa: *[]u8 = &toks.ptr[a];
|
|
let pb: *[]u8 = &toks.ptr[b];
|
|
let tp: *u8 = pa.ptr;
|
|
let tl: i32 = pa.len;
|
|
let tc: i32 = pa.cap;
|
|
pa.ptr = pb.ptr;
|
|
pa.len = pb.len;
|
|
pa.cap = pb.cap;
|
|
pb.ptr = tp;
|
|
pb.len = tl;
|
|
pb.cap = tc;
|
|
a += 1;
|
|
b -= 1;
|
|
};
|
|
return toks;
|
|
};
|
|
|
|
// split — full split of `in` on `delim` (no token cap). Mirrors
|
|
// `splitn(in, delim, types::SIZE_MAX)`. ww uses `types.I32_MAX`
|
|
// because the index type is i32 (lib/CLAUDE.md).
|
|
//
|
|
// ref/hare/bytes/tokenize.ha:225.
|
|
export fn split(in: []u8, delim: []u8) [][]u8 = {
|
|
return splitn(in, delim, types.I32_MAX);
|
|
};
|
|
|
|
// cut — split `in` along the first instance of `delim`, returning the
|
|
// portion before and the portion after the delimiter as a borrowed
|
|
// tuple. When `delim` is absent, the whole input is the first half and
|
|
// the second is empty. ref/hare/bytes/tokenize.ha:392.
|
|
//
|
|
// Delim is spelled (u8 | []u8) to match index/rindex (bytes.ww:57/91);
|
|
// the tagged union is an unordered set, so this is the same type as
|
|
// Hare's ([]u8 | u8), not a divergence.
|
|
export fn cut(in: []u8, delim: (u8 | []u8)) ([]u8, []u8) = {
|
|
let ln: i32 = match (delim) {
|
|
case let c: u8 => yield 1i32;
|
|
case let sub: []u8 => {
|
|
assert(sub.len > 0,
|
|
"bytes.cut called with empty delimiter");
|
|
yield sub.len;
|
|
};
|
|
};
|
|
match (index(in, delim)) {
|
|
case let i: i32 => {
|
|
let lo: i32 = i + ln;
|
|
return (in[0:i], in[lo:in.len]);
|
|
};
|
|
case void => {
|
|
let empty: []u8;
|
|
empty.ptr = nil; empty.len = 0; empty.cap = 0;
|
|
return (in, empty);
|
|
};
|
|
};
|
|
};
|
|
|
|
// rcut — like [[cut]] but splits along the last instance of `delim`.
|
|
// ref/hare/bytes/tokenize.ha:413.
|
|
export fn rcut(in: []u8, delim: (u8 | []u8)) ([]u8, []u8) = {
|
|
let ln: i32 = match (delim) {
|
|
case let c: u8 => yield 1i32;
|
|
case let sub: []u8 => {
|
|
assert(sub.len > 0,
|
|
"bytes.rcut called with empty delimiter");
|
|
yield sub.len;
|
|
};
|
|
};
|
|
match (rindex(in, delim)) {
|
|
case let i: i32 => {
|
|
let lo: i32 = i + ln;
|
|
return (in[0:i], in[lo:in.len]);
|
|
};
|
|
case void => {
|
|
let empty: []u8;
|
|
empty.ptr = nil; empty.len = 0; empty.cap = 0;
|
|
return (in, empty);
|
|
};
|
|
};
|
|
};
|