lib/strings+test: Hare port (dup/concat/trim/index/contains/has{pre,suf}fix/compare/utf8)
Hare-faithful index/predicate family per ref/hare/strings/{dup,
concat,trim,index,suffix,contains,compare,utf8}.ha. Non-variadic
subset (concat 2-arg, trim single-rune, contains single-needle)
pending task #16 — cstage variadic-pack drops .len on multi-field
element types; ship the Hare-faithful single-arg shape now, file
the variadic upgrade as follow-up. `sub` follow-up filed as #29
(commit 2 with iterator + utf8.chars relocation).
Surface: dup, concat, trim/trimprefix/trimsuffix (single rune),
hasprefix, hassuffix (both with (str|rune) sum needle),
byteindex, rbyteindex (both with (str|rune) sum needle),
contains (single str needle), compare, toutf8, fromutf8_unsafe,
runebytes helper. (str|rune) match arms route the rune via
utf8.encoderune into a [4]u8 scratch then bytes.index/rindex —
drew-devault's directive for clean Hare-fidelity over invented
ASCII-only rune-byte arms.
byteindex / rbyteindex rune-arm semantic correction —
corpus-coverage-blind unmask. Pre-existing impl scanned for
`r: u8` (broken for all rune values >0x7F since strings.ww first
landed; no caller exercised it). Replaced with utf8.encoderune-
based scan via runebytes helper. Severity-marker: silent
wrong-result for any non-ASCII rune needle, masked by zero
in-tree callers until lib/strings + utf8 chain pulled the shape
in.
Build-system propagation: lib/strings depends transitively on
lib/encoding/utf8 (via byteindex's rune arm). cmd/ww driver's
locate_import_in (cmd/ww/main.c:85) walks `<dir>/<name>.ww` and
`<dir>/<name>/<name>.ww` only — `use utf8;` doesn't find
lib/encoding/utf8/utf8.ww without explicit `-I lib/encoding/utf8`.
Propagated through 5 wwstage-tool Makefile targets + 7 test
wrappers + test/wcc/995_self_rebuild.c sprintf lines. Task #17
filed for the principled resolver fix (subdir walk vs Hare's
qualified `use encoding::utf8;` notation).
This commit chain (#15 strings) surfaced 7 cgen bugs during
landing: #16 cstage variadic-pack, #17 resolver nested-paths,
#27 aliaslookup leaf-collision, #22 zero-init !void/void-alias
let-decl, #15-cstage retscr SSoT name, #24 composite CALL return
as composite arg, #28 N_DOT calleeparams. All blocking ones
fixed (#16/#17 deferred-with-stopgap, others fixed in their
respective commits). Pre-flight + stop-and-surface discipline
held throughout — no workarounds shipped in stdlib.
Tests:
- 966_strings_run drives lib/strings/stringstest.ww via ww run.
15 @test fns: dup (alloc, multibyte), concat (empty, lopsided,
multibyte), trim/ltrim/rtrim incl. 4-byte rune U+1D68A,
hasprefix/hassuffix with (str|rune) incl. multibyte,
byteindex/rbyteindex both arms 1/2/3/4-byte rune coverage,
compare. Cited from ref/hare/strings/+test.ha where vectors
apply.
100/100 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
This commit is contained in:
@@ -1,12 +1,53 @@
|
||||
// strings — operations over the immutable str type ({ *u8, len }).
|
||||
// Mirrors Hare's strings::; `len` and `is-empty` aren't functions
|
||||
// (callers use `s.len` and `s.len == 0` directly).
|
||||
// strings — operations over str ({ptr,len}). Hare port; see
|
||||
// ref/hare/strings/.
|
||||
//
|
||||
// Documented divergences from Hare:
|
||||
//
|
||||
// - `concat(a, b)` is 2-arg. Hare ships `concat(strs: str...)`
|
||||
// (ref/hare/strings/concat.ha:5). Blocks on task #16 (cstage
|
||||
// variadic-pack drops .len of multi-field element type). Cite
|
||||
// reverts on fix.
|
||||
// - `trim` / `ltrim` / `rtrim` take a single rune. Hare's are
|
||||
// `(exclude: rune...)` (ref/hare/strings/trim.ha:54). Same
|
||||
// blocker as concat. Hare's no-rune branch (strip whitespace)
|
||||
// is also dropped — depends on a rune set.
|
||||
// - `contains` is non-variadic. Hare's is
|
||||
// `contains(haystack, needles: (str | rune)...)`
|
||||
// (ref/hare/strings/contains.ha:9). Same blocker.
|
||||
// - `byteindex` / `rbyteindex` rune arms encode via
|
||||
// `utf8.encoderune`; the legacy impls scanned for `r: u8` (an
|
||||
// undocumented ASCII-only restriction that silently dropped
|
||||
// to the wrong byte for U+80..U+7FF and higher).
|
||||
// - `dup(s: str) str` — Hare returns `(str | nomem)`. ww's
|
||||
// `os.alloc` aborts on OOM (no `nomem` type), so we return plain
|
||||
// `str`. Empty input returns `{nil, 0}`; Hare returns the static
|
||||
// empty string — same observable result.
|
||||
|
||||
use bytes;
|
||||
use utf8;
|
||||
use os;
|
||||
|
||||
// compare — bytewise three-way comparison: negative if a<b, 0 if equal,
|
||||
// positive if a>b. Matches Hare's strings::compare. ASCII-order, not
|
||||
// locale-aware. Callers that just need equality use `compare(a, b) == 0`.
|
||||
// toutf8 — borrowed []u8 view of `s`. ref/hare/strings/utf8.ha:29.
|
||||
// `cap` equals `len`; the slice does not own a separate allocation.
|
||||
export fn toutf8(s: str) []u8 = {
|
||||
let r: []u8;
|
||||
r.ptr = s.ptr;
|
||||
r.len = s.len;
|
||||
r.cap = s.len;
|
||||
return r;
|
||||
};
|
||||
|
||||
// fromutf8_unsafe — borrowed str view of `in`. Does not validate.
|
||||
// ref/hare/strings/utf8.ha:10.
|
||||
export fn fromutf8_unsafe(in: []u8) str = {
|
||||
let r: str;
|
||||
r.ptr = in.ptr;
|
||||
r.len = in.len;
|
||||
return r;
|
||||
};
|
||||
|
||||
// compare — three-way bytewise codepoint-order comparison.
|
||||
// ref/hare/strings/compare.ha:12.
|
||||
export fn compare(a: str, b: str) i32 = {
|
||||
let n: i32 = a.len;
|
||||
if (b.len < n) { n = b.len; };
|
||||
@@ -18,97 +59,8 @@ export fn compare(a: str, b: str) i32 = {
|
||||
return a.len - b.len;
|
||||
};
|
||||
|
||||
export fn hasprefix(s: str, p: str) bool = {
|
||||
if (p.len > s.len) { return false; };
|
||||
let i: i32 = 0;
|
||||
for (i < p.len) {
|
||||
if (s[i] != p[i]) { return false; };
|
||||
i += 1;
|
||||
};
|
||||
return true;
|
||||
};
|
||||
|
||||
export fn hassuffix(s: str, suf: str) 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;
|
||||
};
|
||||
|
||||
// byteindex — first byte position of `needle` in `s`. Mirrors Hare's
|
||||
// strings::byteindex: a single-codepoint rune scans for the byte that
|
||||
// encodes it (ASCII only here — multi-byte UTF-8 awaits utf8 encode),
|
||||
// a str needle scans for the substring. Returns void if absent.
|
||||
export fn byteindex(s: str, needle: (str | rune)) (i32 | void) = {
|
||||
match (needle) {
|
||||
case let r: rune => {
|
||||
let c: u8 = r: u8;
|
||||
let i: i32 = 0;
|
||||
for (i < s.len) {
|
||||
if (s[i] == c) { return i; };
|
||||
i += 1;
|
||||
};
|
||||
return;
|
||||
};
|
||||
case let sub: str => {
|
||||
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;
|
||||
};
|
||||
|
||||
// contains — true iff `sub` appears in `s`. Mirrors Hare's
|
||||
// strings::contains shape (byte-wise on the str-needle case).
|
||||
export fn contains(s: str, sub: str) bool = {
|
||||
let r: (i32 | void) = byteindex(s, sub);
|
||||
match (r) {
|
||||
case let i: i32 => return true;
|
||||
case void => return false;
|
||||
};
|
||||
return false;
|
||||
};
|
||||
|
||||
// concat — joins two strings into a fresh str. Caller owns the
|
||||
// returned str's storage; release via `os.free(r.ptr, r.len)`. Mirrors
|
||||
// Hare's strings::concat shape.
|
||||
export fn concat(a: str, b: str) str = {
|
||||
let total: i32 = a.len + b.len;
|
||||
let buf: *u8 = os.alloc(total: u64): *u8;
|
||||
let i: i32 = 0;
|
||||
for (i < a.len) { buf[i] = a[i]; i += 1; };
|
||||
let j: i32 = 0;
|
||||
for (j < b.len) { buf[a.len + j] = b[j]; j += 1; };
|
||||
let r: str;
|
||||
r.ptr = buf;
|
||||
r.len = total;
|
||||
return r;
|
||||
};
|
||||
|
||||
// dup — duplicate a string into a fresh allocation. Caller owns the
|
||||
// returned str's storage; release via `os.free(r.ptr, r.len)`. Mirrors
|
||||
// Hare's strings::dup shape — Hare returns `(str | nomem)`, ww doesn't
|
||||
// have nomem (os.alloc aborts on OOM), so we return plain `str`.
|
||||
//
|
||||
// Empty input yields a `{nil, 0}` str — Hare returns the static empty
|
||||
// string; same observable result.
|
||||
// dup — allocate a fresh copy of `s`. Caller releases with
|
||||
// `os.free(r.ptr, r.len: u64)`. ref/hare/strings/dup.ha:7.
|
||||
export fn dup(s: str) str = {
|
||||
let r: str;
|
||||
r.ptr = nil;
|
||||
@@ -122,19 +74,14 @@ export fn dup(s: str) str = {
|
||||
return r;
|
||||
};
|
||||
|
||||
// freeall — release every str element in `s` (those that were
|
||||
// individually allocated) plus the slice's backing storage. Mirrors
|
||||
// Hare's strings::freeall — the natural disposer for any function
|
||||
// returning a fresh `[]str` of dup'd elements (e.g. shlex.split).
|
||||
// freeall — release each element + the slice header. The natural
|
||||
// disposer for any `[]str` of dup'd elements (e.g. shlex.split).
|
||||
// ref/hare/strings/dup.ha:38.
|
||||
//
|
||||
// Each element is freed via os.free at its own length; the slice
|
||||
// header storage is freed at `cap * 16` bytes (one str = 16B). Empty
|
||||
// elements (`{nil, 0}` from a zero-length dup) are skipped — calling
|
||||
// os.free on a nil pointer at len 0 would tickle the rt_free guard
|
||||
// that the runtime treats as a logic bug.
|
||||
//
|
||||
// `cap == 0` means the slice was never grown (empty `[]str` with no
|
||||
// backing allocation); skip the header free in that case too.
|
||||
// Empty elements (`{nil, 0}` from a zero-length dup) are skipped:
|
||||
// os.free on a nil pointer at len 0 tickles the rt_free guard. The
|
||||
// slice header itself is freed at `cap * 16` (one str = 16B); a
|
||||
// never-grown slice (cap == 0) skips the header free.
|
||||
export fn freeall(s: []str) void = {
|
||||
let i: i32 = 0;
|
||||
for (i < s.len) {
|
||||
@@ -148,44 +95,27 @@ export fn freeall(s: []str) void = {
|
||||
};
|
||||
};
|
||||
|
||||
// rbyteindex — last byte position of `needle` in `s`. Mirrors Hare's
|
||||
// strings::rbyteindex. Rune needle scans for the byte that encodes it
|
||||
// (ASCII only); str needle scans for the substring. Empty str needle
|
||||
// matches at s.len.
|
||||
export fn rbyteindex(s: str, needle: (str | rune)) (i32 | void) = {
|
||||
match (needle) {
|
||||
case let r: rune => {
|
||||
let c: u8 = r: u8;
|
||||
let i: i32 = s.len - 1;
|
||||
for (i >= 0) {
|
||||
if (s[i] == c) { return i; };
|
||||
i -= 1;
|
||||
};
|
||||
return;
|
||||
};
|
||||
case let sub: str => {
|
||||
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;
|
||||
// concat — fresh allocation containing `a` then `b`. Caller releases
|
||||
// with `os.free(r.ptr, r.len: u64)`. ref/hare/strings/concat.ha:5
|
||||
// (subset: Hare's `(strs: str...)` blocks on task #16).
|
||||
export fn concat(a: str, b: str) str = {
|
||||
let total: i32 = a.len + b.len;
|
||||
let buf: *u8 = os.alloc(total: u64): *u8;
|
||||
let i: i32 = 0;
|
||||
for (i < a.len) { buf[i] = a[i]; i += 1; };
|
||||
let j: i32 = 0;
|
||||
for (j < b.len) { buf[a.len + j] = b[j]; j += 1; };
|
||||
let r: str;
|
||||
r.ptr = buf;
|
||||
r.len = total;
|
||||
return r;
|
||||
};
|
||||
|
||||
// sub — borrowed substring `s[start..end]`. Mirrors Hare's
|
||||
// strings::sub. Caller must ensure 0 <= start <= end <= s.len; out-of-
|
||||
// range indices are clamped silently here, where Hare aborts.
|
||||
// sub — borrowed `s[start..end]`. ref/hare/strings/sub.ha:30 is
|
||||
// rune-wise; this ww form is byte-wise (no rune iterator yet, planned
|
||||
// for commit 2). Clamps out-of-range silently where Hare aborts —
|
||||
// retained for the existing getopt caller; will graduate when the
|
||||
// rune-wise form lands.
|
||||
export fn sub(s: str, start: i32, end: i32) str = {
|
||||
let lo: i32 = start;
|
||||
let hi: i32 = end;
|
||||
@@ -198,56 +128,143 @@ export fn sub(s: str, start: i32, end: i32) str = {
|
||||
return r;
|
||||
};
|
||||
|
||||
// trimprefix — `s` with `pre` stripped from the front, or `s`
|
||||
// unchanged if it doesn't start with `pre`. Returns a borrowed view.
|
||||
// Mirrors Hare's strings::trimprefix.
|
||||
export fn trimprefix(s: str, pre: str) str = {
|
||||
if (!hasprefix(s, pre)) { return s; };
|
||||
// runebytes — encode `r` into caller's `scratch` (must hold 4 bytes)
|
||||
// and return the borrowed slice trimmed to the encoded length. Hare
|
||||
// inlines the same shape at ref/hare/strings/index.ha:132.
|
||||
fn runebytes(scratch: []u8, r: rune) []u8 = {
|
||||
let n: i32 = utf8.encoderune(scratch, r);
|
||||
let s: []u8;
|
||||
s.ptr = scratch.ptr;
|
||||
s.len = n;
|
||||
s.cap = n;
|
||||
return s;
|
||||
};
|
||||
|
||||
// hasprefix — true iff `in` begins with `prefix`.
|
||||
// ref/hare/strings/suffix.ha:8.
|
||||
export fn hasprefix(in: str, prefix: (str | rune)) bool = {
|
||||
let scratch: [4]u8;
|
||||
let p: []u8 = match (prefix) {
|
||||
case let s: str => yield toutf8(s);
|
||||
case let r: rune => yield runebytes(scratch[0:4], r);
|
||||
};
|
||||
return bytes.hasprefix(toutf8(in), p);
|
||||
};
|
||||
|
||||
// hassuffix — true iff `in` ends with `suff`.
|
||||
// ref/hare/strings/suffix.ha:26.
|
||||
export fn hassuffix(in: str, suff: (str | rune)) bool = {
|
||||
let scratch: [4]u8;
|
||||
let s: []u8 = match (suff) {
|
||||
case let v: str => yield toutf8(v);
|
||||
case let r: rune => yield runebytes(scratch[0:4], r);
|
||||
};
|
||||
return bytes.hassuffix(toutf8(in), s);
|
||||
};
|
||||
|
||||
// byteindex — byte-wise offset of `needle` in `haystack`, or void if
|
||||
// absent. ref/hare/strings/index.ha:127. Rune arm encodes via
|
||||
// utf8.encoderune (Hare passes the encoded slice straight to
|
||||
// bytes::index).
|
||||
export fn byteindex(haystack: str, needle: (str | rune)) (i32 | void) = {
|
||||
let scratch: [4]u8;
|
||||
let n: []u8 = match (needle) {
|
||||
case let s: str => yield toutf8(s);
|
||||
case let r: rune => yield runebytes(scratch[0:4], r);
|
||||
};
|
||||
return bytes.index(toutf8(haystack), n);
|
||||
};
|
||||
|
||||
// rbyteindex — byte-wise offset of the last `needle` in `haystack`.
|
||||
// ref/hare/strings/index.ha:138.
|
||||
export fn rbyteindex(haystack: str, needle: (str | rune)) (i32 | void) = {
|
||||
let scratch: [4]u8;
|
||||
let n: []u8 = match (needle) {
|
||||
case let s: str => yield toutf8(s);
|
||||
case let r: rune => yield runebytes(scratch[0:4], r);
|
||||
};
|
||||
return bytes.rindex(toutf8(haystack), n);
|
||||
};
|
||||
|
||||
// contains — true iff `needle` occurs in `haystack`.
|
||||
// ref/hare/strings/contains.ha:9 (subset: Hare's variadic form
|
||||
// `(needles: (str | rune)...)` blocks on task #16).
|
||||
export fn contains(haystack: str, needle: (str | rune)) bool = {
|
||||
match (byteindex(haystack, needle)) {
|
||||
case let i: i32 => return true;
|
||||
case void => return false;
|
||||
};
|
||||
return false;
|
||||
};
|
||||
|
||||
// trimprefix — `s` with `prefix` stripped from the front, or `s`
|
||||
// unchanged if it doesn't start with `prefix`. Borrowed view.
|
||||
// ref/hare/strings/trim.ha:60.
|
||||
export fn trimprefix(input: str, prefix: str) str = {
|
||||
if (!hasprefix(input, prefix)) { return input; };
|
||||
let r: str;
|
||||
r.ptr = s.ptr + (pre.len: u64);
|
||||
r.len = s.len - pre.len;
|
||||
r.ptr = input.ptr + (prefix.len: u64);
|
||||
r.len = input.len - prefix.len;
|
||||
return r;
|
||||
};
|
||||
|
||||
// trimsuffix — `s` with `suf` stripped from the end, or `s` unchanged
|
||||
// if it doesn't end with `suf`. Returns a borrowed view. Mirrors
|
||||
// Hare's strings::trimsuffix.
|
||||
export fn trimsuffix(s: str, suf: str) str = {
|
||||
if (!hassuffix(s, suf)) { return s; };
|
||||
// trimsuffix — symmetric. ref/hare/strings/trim.ha:69.
|
||||
export fn trimsuffix(input: str, suffix: str) str = {
|
||||
if (!hassuffix(input, suffix)) { return input; };
|
||||
let r: str;
|
||||
r.ptr = s.ptr;
|
||||
r.len = s.len - suf.len;
|
||||
r.ptr = input.ptr;
|
||||
r.len = input.len - suffix.len;
|
||||
return r;
|
||||
};
|
||||
|
||||
// ltrimbyte / rtrimbyte / trimbyte — strip occurrences of a single
|
||||
// byte from the left, right, or both ends. Returns a borrowed view.
|
||||
// Hare's strings::ltrim / rtrim / trim take a rune varargs set; ww's
|
||||
// subset takes a single byte (the common ASCII case).
|
||||
export fn ltrimbyte(s: str, c: u8) str = {
|
||||
// ltrim — strip occurrences of `exclude` (encoded as UTF-8) from the
|
||||
// front. Borrowed view. ref/hare/strings/trim.ha:11 (subset: single
|
||||
// rune; Hare's `(trim: rune...)` blocks on task #16). The no-rune
|
||||
// strip-whitespace branch is omitted for the same reason.
|
||||
export fn ltrim(input: str, exclude: rune) str = {
|
||||
let scratch: [4]u8;
|
||||
let pat: []u8 = runebytes(scratch[0:4], exclude);
|
||||
let i: i32 = 0;
|
||||
for (i < s.len) {
|
||||
if (s[i] != c) { break; };
|
||||
i += 1;
|
||||
for (i + pat.len <= input.len) {
|
||||
let j: i32 = 0;
|
||||
let ok: bool = true;
|
||||
for (j < pat.len) {
|
||||
if (input[i + j] != pat[j]) { ok = false; j = pat.len; }
|
||||
else { j += 1; };
|
||||
};
|
||||
if (!ok) { break; };
|
||||
i += pat.len;
|
||||
};
|
||||
let r: str;
|
||||
r.ptr = s.ptr + (i: u64);
|
||||
r.len = s.len - i;
|
||||
r.ptr = input.ptr + (i: u64);
|
||||
r.len = input.len - i;
|
||||
return r;
|
||||
};
|
||||
|
||||
export fn rtrimbyte(s: str, c: u8) str = {
|
||||
let n: i32 = s.len;
|
||||
for (n > 0) {
|
||||
if (s[n - 1] != c) { break; };
|
||||
n -= 1;
|
||||
// rtrim — strip occurrences of `exclude` from the end. Borrowed view.
|
||||
// ref/hare/strings/trim.ha:32 (same subset note).
|
||||
export fn rtrim(input: str, exclude: rune) str = {
|
||||
let scratch: [4]u8;
|
||||
let pat: []u8 = runebytes(scratch[0:4], exclude);
|
||||
let n: i32 = input.len;
|
||||
for (n >= pat.len) {
|
||||
let off: i32 = n - pat.len;
|
||||
let j: i32 = 0;
|
||||
let ok: bool = true;
|
||||
for (j < pat.len) {
|
||||
if (input[off + j] != pat[j]) { ok = false; j = pat.len; }
|
||||
else { j += 1; };
|
||||
};
|
||||
if (!ok) { break; };
|
||||
n -= pat.len;
|
||||
};
|
||||
let r: str;
|
||||
r.ptr = s.ptr;
|
||||
r.ptr = input.ptr;
|
||||
r.len = n;
|
||||
return r;
|
||||
};
|
||||
|
||||
export fn trimbyte(s: str, c: u8) str = {
|
||||
return rtrimbyte(ltrimbyte(s, c), c);
|
||||
// trim — strip from both ends. ref/hare/strings/trim.ha:54.
|
||||
export fn trim(input: str, exclude: rune) str = {
|
||||
return ltrim(rtrim(input, exclude), exclude);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user