From e03a6281d6bec681149bfcbbf3085c884acbc3a4 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 20 May 2026 01:30:12 +0900 Subject: [PATCH] lib/strings+test: port dupall from Hare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lib/strings/strings.ww | 36 ++++++++++++ lib/strings/stringstest.ww | 88 ++++++++++++++++++++++++++++ selfhost/cmd/w6c/main.combined.ww | 36 ++++++++++++ selfhost/cmd/wwdump/main.combined.ww | 36 ++++++++++++ selfhost/test/smoke.combined.ww | 36 ++++++++++++ 5 files changed, 232 insertions(+) diff --git a/lib/strings/strings.ww b/lib/strings/strings.ww index 74727070..7d5ba2f0 100644 --- a/lib/strings/strings.ww +++ b/lib/strings/strings.ww @@ -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. diff --git a/lib/strings/stringstest.ww b/lib/strings/stringstest.ww index 54a58110..893e4bc3 100644 --- a/lib/strings/stringstest.ww +++ b/lib/strings/stringstest.ww @@ -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(); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 39dbe5f2..dfc6cef9 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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. diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index e5994e5c..56d57ca3 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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. diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index d4cb7ed0..40310474 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -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.