lib/strings+test: port dupall from Hare

ref/hare/strings/dup.ha:26-35. Returns ([]str | nomem); duplicates
every str in the input slice via the now-graduated alloc-slice
builtin (#45 unblocked `let s: []str = alloc([], n)?`). Loop body
uses appendstr because `[]str` element is 16B and the bare `append`
builtin truncates (#11) — pre-allocated cap=s.len means rt_ensure's
grow branch never fires.

Defer-rollback omitted: with `dup()` still unchecked (graduation
tracked by #46), the only nomem source is the initial slice alloc,
so there is no partial state to roll back. Will revisit when #46
lands.

Empty-input early-return short-circuits via {nil,0,0} because
rt_alloc(0) is an mmap of 0 bytes which the kernel rejects with
-EINVAL — Hare hands back a sentinel. Localized at the call site
pending #47.

Tests assert independent allocations at every index of multi-element
inputs, including a multibyte row.
This commit is contained in:
2026-05-20 01:30:12 +09:00
parent 4d4ad36b70
commit e03a6281d6
5 changed files with 232 additions and 0 deletions

View File

@@ -77,6 +77,42 @@ export fn dup(s: str) str = {
return r;
};
// dupall — fresh `[]str` whose elements are independent copies of
// `s`'s elements. Caller releases via [[freeall]].
// ref/hare/strings/dup.ha:26 (#6).
//
// Hare gates the per-element dup behind `?` and rolls back via
// `defer if (!ok) freeall(newsl)`. ww has no `defer if`; more
// importantly, ww's [[dup]] is still unchecked (returns plain `str`,
// aborts via os.alloc on OOM — see top-of-file divergence note),
// so the only nomem propagation point is the initial slice alloc.
// With no inner failure path, the rollback is structurally a no-op
// and is omitted; it returns once dup graduates to `(str | nomem)`
// (#46). The pre-allocated slice has `cap == s.len`, so appendstr's
// rt_ensure call never reaches the grow branch.
//
// Empty input bypasses the alloc: rt_alloc(0) is an mmap of 0 bytes
// which returns -EINVAL, and the alloc-slice `?` shortcut routes
// that through nomem — Hare's heap allocator hands back a sentinel
// instead (#47). Return `{nil, 0, 0}` directly so callers get the
// Hare-observable shape (len==0, freeall is a no-op via cap==0).
export fn dupall(s: []str) ([]str | nomem) = {
if (s.len == 0) {
let r: []str;
r.ptr = nil: *str;
r.len = 0;
r.cap = 0;
return r;
};
let newsl: []str = alloc([], s.len)?;
let i: i32 = 0;
for (i < s.len) {
appendstr(&newsl, dup(s[i]));
i += 1;
};
return newsl;
};
// 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.

View File

@@ -45,6 +45,93 @@ fn streq(a: str, b: str) bool = {
defer os.free(m.ptr: *void, m.len: u64);
};
// ---- dupall -----------------------------------------------------------
// ref/hare/strings/dup.ha:55 (#6). Per-row `signalled` bump narrows a
// failure exit code; element reads go through `&toks.ptr[i]: *str`
// per the splitn cases (16B element copy gap, cgen.c:6515).
@test fn dupall_cases() void = {
// Empty input — empty result, mirrors Hare's `payload = []`.
signalled = 1800;
let empty: []str;
empty.ptr = nil: *str;
empty.len = 0;
empty.cap = 0;
match (strings.dupall(empty)) {
case let r: []str => {
if (r.len != 0) { fail(); };
strings.freeall(r);
};
case nomem => { fail(); };
};
// Two-element ASCII — each output element is a fresh allocation
// independent of the input (ptr differs from the borrowed source).
signalled = 1801;
let in2: [2]str;
in2[0] = "hello";
in2[1] = "world";
let src2: []str;
src2.ptr = &in2[0];
src2.len = 2;
src2.cap = 2;
match (strings.dupall(src2)) {
case let r: []str => {
if (r.len != 2) { fail(); };
expect_str(r, 0, "hello");
expect_str(r, 1, "world");
let p0: *str = &r.ptr[0];
if (p0.ptr == in2[0].ptr) { fail(); };
let p1: *str = &r.ptr[1];
if (p1.ptr == in2[1].ptr) { fail(); };
strings.freeall(r);
};
case nomem => { fail(); };
};
// Singleton — `only` rune-equivalent of Hare's `["only"]`.
signalled = 1802;
let in1: [1]str;
in1[0] = "only";
let src1: []str;
src1.ptr = &in1[0];
src1.len = 1;
src1.cap = 1;
match (strings.dupall(src1)) {
case let r: []str => {
if (r.len != 1) { fail(); };
expect_str(r, 0, "only");
strings.freeall(r);
};
case nomem => { fail(); };
};
// Multibyte — UTF-8 bytes (5-byte and 15-byte) round-trip.
signalled = 1803;
let inm: [2]str;
inm[0] = "héllo";
inm[1] = "こんにちは";
let srcm: []str;
srcm.ptr = &inm[0];
srcm.len = 2;
srcm.cap = 2;
match (strings.dupall(srcm)) {
case let r: []str => {
if (r.len != 2) { fail(); };
expect_str(r, 0, "héllo");
expect_str(r, 1, "こんにちは");
let p0: *str = &r.ptr[0];
if (p0.len != 6) { fail(); }; // é is 2 bytes
if (p0.ptr == inm[0].ptr) { fail(); };
let p1: *str = &r.ptr[1];
if (p1.len != 15) { fail(); }; // each kana is 3 bytes
if (p1.ptr == inm[1].ptr) { fail(); };
strings.freeall(r);
};
case nomem => { fail(); };
};
};
// ---- concat -----------------------------------------------------------
// ref/hare/strings/concat.ha:18. Rows mirror Hare's vectors (0/1/2/3-arg,
// empty-mid, 2-empty) plus empty-first / empty-last / multibyte. The
@@ -1441,6 +1528,7 @@ fn expect_str(toks: []str, i: i32, want: str) void = {
export fn main() i32 = {
signalled = 1; dup_cases();
signalled = 42; dupall_cases();
signalled = 2; concat_cases();
signalled = 30; join_cases();
signalled = 3; hasprefix_cases();

View File

@@ -1914,6 +1914,42 @@ export fn dup(s: str) str = {
return r;
};
// dupall — fresh `[]str` whose elements are independent copies of
// `s`'s elements. Caller releases via [[freeall]].
// ref/hare/strings/dup.ha:26 (#6).
//
// Hare gates the per-element dup behind `?` and rolls back via
// `defer if (!ok) freeall(newsl)`. ww has no `defer if`; more
// importantly, ww's [[dup]] is still unchecked (returns plain `str`,
// aborts via os.alloc on OOM — see top-of-file divergence note),
// so the only nomem propagation point is the initial slice alloc.
// With no inner failure path, the rollback is structurally a no-op
// and is omitted; it returns once dup graduates to `(str | nomem)`
// (#46). The pre-allocated slice has `cap == s.len`, so appendstr's
// rt_ensure call never reaches the grow branch.
//
// Empty input bypasses the alloc: rt_alloc(0) is an mmap of 0 bytes
// which returns -EINVAL, and the alloc-slice `?` shortcut routes
// that through nomem — Hare's heap allocator hands back a sentinel
// instead (#47). Return `{nil, 0, 0}` directly so callers get the
// Hare-observable shape (len==0, freeall is a no-op via cap==0).
export fn dupall(s: []str) ([]str | nomem) = {
if (s.len == 0) {
let r: []str;
r.ptr = nil: *str;
r.len = 0;
r.cap = 0;
return r;
};
let newsl: []str = alloc([], s.len)?;
let i: i32 = 0;
for (i < s.len) {
appendstr(&newsl, dup(s[i]));
i += 1;
};
return newsl;
};
// 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.

View File

@@ -1914,6 +1914,42 @@ export fn dup(s: str) str = {
return r;
};
// dupall — fresh `[]str` whose elements are independent copies of
// `s`'s elements. Caller releases via [[freeall]].
// ref/hare/strings/dup.ha:26 (#6).
//
// Hare gates the per-element dup behind `?` and rolls back via
// `defer if (!ok) freeall(newsl)`. ww has no `defer if`; more
// importantly, ww's [[dup]] is still unchecked (returns plain `str`,
// aborts via os.alloc on OOM — see top-of-file divergence note),
// so the only nomem propagation point is the initial slice alloc.
// With no inner failure path, the rollback is structurally a no-op
// and is omitted; it returns once dup graduates to `(str | nomem)`
// (#46). The pre-allocated slice has `cap == s.len`, so appendstr's
// rt_ensure call never reaches the grow branch.
//
// Empty input bypasses the alloc: rt_alloc(0) is an mmap of 0 bytes
// which returns -EINVAL, and the alloc-slice `?` shortcut routes
// that through nomem — Hare's heap allocator hands back a sentinel
// instead (#47). Return `{nil, 0, 0}` directly so callers get the
// Hare-observable shape (len==0, freeall is a no-op via cap==0).
export fn dupall(s: []str) ([]str | nomem) = {
if (s.len == 0) {
let r: []str;
r.ptr = nil: *str;
r.len = 0;
r.cap = 0;
return r;
};
let newsl: []str = alloc([], s.len)?;
let i: i32 = 0;
for (i < s.len) {
appendstr(&newsl, dup(s[i]));
i += 1;
};
return newsl;
};
// 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.

View File

@@ -1805,6 +1805,42 @@ export fn dup(s: str) str = {
return r;
};
// dupall — fresh `[]str` whose elements are independent copies of
// `s`'s elements. Caller releases via [[freeall]].
// ref/hare/strings/dup.ha:26 (#6).
//
// Hare gates the per-element dup behind `?` and rolls back via
// `defer if (!ok) freeall(newsl)`. ww has no `defer if`; more
// importantly, ww's [[dup]] is still unchecked (returns plain `str`,
// aborts via os.alloc on OOM — see top-of-file divergence note),
// so the only nomem propagation point is the initial slice alloc.
// With no inner failure path, the rollback is structurally a no-op
// and is omitted; it returns once dup graduates to `(str | nomem)`
// (#46). The pre-allocated slice has `cap == s.len`, so appendstr's
// rt_ensure call never reaches the grow branch.
//
// Empty input bypasses the alloc: rt_alloc(0) is an mmap of 0 bytes
// which returns -EINVAL, and the alloc-slice `?` shortcut routes
// that through nomem — Hare's heap allocator hands back a sentinel
// instead (#47). Return `{nil, 0, 0}` directly so callers get the
// Hare-observable shape (len==0, freeall is a no-op via cap==0).
export fn dupall(s: []str) ([]str | nomem) = {
if (s.len == 0) {
let r: []str;
r.ptr = nil: *str;
r.len = 0;
r.cap = 0;
return r;
};
let newsl: []str = alloc([], s.len)?;
let i: i32 = 0;
for (i < s.len) {
appendstr(&newsl, dup(s[i]));
i += 1;
};
return newsl;
};
// 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.