diff --git a/lib/strings/strings.ww b/lib/strings/strings.ww index 3dff97a6..2178d557 100644 --- a/lib/strings/strings.ww +++ b/lib/strings/strings.ww @@ -906,6 +906,62 @@ export fn lpad(s: str, p: rune, maxlen: i32) str = { return r; }; +// replace — fresh allocation of `s` with every non-overlapping +// occurrence of `needle` replaced by `target`. Caller releases with +// `os.free(r.ptr, r.len: u64)`. ref/hare/strings/replace.ha:8 (#4). +// +// Hare delegates to [[multireplace]] with a single pair; ww has no +// `(str, str)` variadic shape today (#39), so this is a standalone +// two-pass implementation: pass 1 counts matches to size the result, +// pass 2 copies chunks and `target` into a single fresh buffer. +// Single nomem path (the `alloc([], total)?`) preserves Hare's +// signature without a per-write `append(...)?` (ww's append builtin +// aborts on OOM, #11). Empty `needle` would hasprefix-match every +// position with a zero stride — same infinite loop Hare exhibits at +// ref/hare/strings/replace.ha:31; not gated. +export fn replace(s: str, needle: str, target: str) (str | nomem) = { + let sb: []u8 = toutf8(s); + let nb: []u8 = toutf8(needle); + let tb: []u8 = toutf8(target); + let count: i32 = 0; + let i: i32 = 0; + for (i < sb.len) { + if (bytes.hasprefix(sb[i:sb.len], nb)) { + count += 1; + i += nb.len; + } else { + i += 1; + }; + }; + let total: i32 = sb.len + count * (tb.len - nb.len); + if (total == 0) { + let r: str; + r.ptr = nil; + r.len = 0; + return r; + }; + let res: []u8 = alloc([], total)?; + let off: i32 = 0; + i = 0; + for (i < sb.len) { + if (bytes.hasprefix(sb[i:sb.len], nb)) { + let j: i32 = 0; + for (j < tb.len) { + res.ptr[off + j] = tb.ptr[j]; + j += 1; + }; + off += tb.len; + i += nb.len; + } else { + res.ptr[off] = sb.ptr[i]; + off += 1; + i += 1; + }; + }; + res.len = total; + return fromutf8_unsafe(res); +}; + // rpad — right-pad `s` with `p` rune until the result reaches `maxlen` // bytes. Symmetric with [[lpad]]. ref/hare/strings/pad.ha:39. export fn rpad(s: str, p: rune, maxlen: i32) str = { diff --git a/lib/strings/stringstest.ww b/lib/strings/stringstest.ww index dc8a1210..e3b725a1 100644 --- a/lib/strings/stringstest.ww +++ b/lib/strings/stringstest.ww @@ -1664,6 +1664,117 @@ fn expect_str(toks: []str, i: i32, want: str) void = { os.free(r6.ptr: *void, r6.len: u64); }; +// ---- replace ---------------------------------------------------------- +// ref/hare/strings/replace.ha:46 (#4). Hare rows 1-5 (target found +// once / multiple times / shorter / longer / multibyte) plus ww rows +// (no-match returns fresh copy, empty-replacement, result-shrinks-to- +// empty). Per-row `signalled` 1810+i narrows the failing row. + +@test fn replace_cases() void = { + // Hare row 1: target found once. + signalled = 1810; + match (strings.replace("Hello world!", "world", "there")) { + case let s: str => { + if (!streq(s, "Hello there!")) { fail(); }; + os.free(s.ptr: *void, s.len: u64); + }; + case nomem => { fail(); }; + }; + + // Hare row 2: needle found four times (replacement same length). + signalled = 1811; + match (strings.replace("I like dogs, dogs, birds, dogs", "dogs", "cats")) { + case let s: str => { + if (!streq(s, "I like cats, cats, birds, cats")) { fail(); }; + os.free(s.ptr: *void, s.len: u64); + }; + case nomem => { fail(); }; + }; + + // Hare row 3: replacement shorter than needle, non-overlapping + // matches (Hare advances by needle.len, not 1). + signalled = 1812; + match (strings.replace("aaaaaa", "aa", "a")) { + case let s: str => { + if (!streq(s, "aaa")) { fail(); }; + os.free(s.ptr: *void, s.len: u64); + }; + case nomem => { fail(); }; + }; + + // Hare row 4: replacement longer than needle. + signalled = 1813; + match (strings.replace("aaa", "a", "aa")) { + case let s: str => { + if (!streq(s, "aaaaaa")) { fail(); }; + os.free(s.ptr: *void, s.len: u64); + }; + case nomem => { fail(); }; + }; + + // Hare row 5: multibyte UTF-8 (3-byte runes). + signalled = 1814; + match (strings.replace("こんにちは", "にち", "ばん")) { + case let s: str => { + if (!streq(s, "こんばんは")) { fail(); }; + os.free(s.ptr: *void, s.len: u64); + }; + case nomem => { fail(); }; + }; + + // ww row 1: needle not found — fresh copy (distinct from + // borrowed input ptr). + signalled = 1815; + let src: str = "hello world"; + match (strings.replace(src, "xyz", "abc")) { + case let s: str => { + if (!streq(s, "hello world")) { fail(); }; + if (s.ptr == src.ptr) { fail(); }; + os.free(s.ptr: *void, s.len: u64); + }; + case nomem => { fail(); }; + }; + + // ww row 2: empty replacement — needle is removed. + signalled = 1816; + match (strings.replace("foo-bar-baz", "-", "")) { + case let s: str => { + if (!streq(s, "foobarbaz")) { fail(); }; + os.free(s.ptr: *void, s.len: u64); + }; + case nomem => { fail(); }; + }; + + // ww row 3: result shrinks to empty — total==0 nil-alloc path. + signalled = 1817; + match (strings.replace("aaaa", "aa", "")) { + case let s: str => { + if (!streq(s, "")) { fail(); }; + if (s.len != 0) { fail(); }; + }; + case nomem => { fail(); }; + }; + + // ww row 4: multibyte single-rune replace — every "に" → "X". + signalled = 1818; + match (strings.replace("こんにちは", "に", "X")) { + case let s: str => { + if (!streq(s, "こんXちは")) { fail(); }; + os.free(s.ptr: *void, s.len: u64); + }; + case nomem => { fail(); }; + }; + + // ww row 5: empty input — returns empty. + signalled = 1819; + match (strings.replace("", "x", "y")) { + case let s: str => { + if (s.len != 0) { fail(); }; + }; + case nomem => { fail(); }; + }; +}; + export fn main() i32 = { signalled = 1; dup_cases(); signalled = 42; dupall_cases(); @@ -1708,5 +1819,6 @@ export fn main() i32 = { signalled = 37; split_cases(); signalled = 38; lpad_cases(); signalled = 39; rpad_cases(); + signalled = 44; replace_cases(); return 0; }; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 285c5dd8..63a54036 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -2743,6 +2743,62 @@ export fn lpad(s: str, p: rune, maxlen: i32) str = { return r; }; +// replace — fresh allocation of `s` with every non-overlapping +// occurrence of `needle` replaced by `target`. Caller releases with +// `os.free(r.ptr, r.len: u64)`. ref/hare/strings/replace.ha:8 (#4). +// +// Hare delegates to [[multireplace]] with a single pair; ww has no +// `(str, str)` variadic shape today (#39), so this is a standalone +// two-pass implementation: pass 1 counts matches to size the result, +// pass 2 copies chunks and `target` into a single fresh buffer. +// Single nomem path (the `alloc([], total)?`) preserves Hare's +// signature without a per-write `append(...)?` (ww's append builtin +// aborts on OOM, #11). Empty `needle` would hasprefix-match every +// position with a zero stride — same infinite loop Hare exhibits at +// ref/hare/strings/replace.ha:31; not gated. +export fn replace(s: str, needle: str, target: str) (str | nomem) = { + let sb: []u8 = toutf8(s); + let nb: []u8 = toutf8(needle); + let tb: []u8 = toutf8(target); + let count: i32 = 0; + let i: i32 = 0; + for (i < sb.len) { + if (bytes.hasprefix(sb[i:sb.len], nb)) { + count += 1; + i += nb.len; + } else { + i += 1; + }; + }; + let total: i32 = sb.len + count * (tb.len - nb.len); + if (total == 0) { + let r: str; + r.ptr = nil; + r.len = 0; + return r; + }; + let res: []u8 = alloc([], total)?; + let off: i32 = 0; + i = 0; + for (i < sb.len) { + if (bytes.hasprefix(sb[i:sb.len], nb)) { + let j: i32 = 0; + for (j < tb.len) { + res.ptr[off + j] = tb.ptr[j]; + j += 1; + }; + off += tb.len; + i += nb.len; + } else { + res.ptr[off] = sb.ptr[i]; + off += 1; + i += 1; + }; + }; + res.len = total; + return fromutf8_unsafe(res); +}; + // rpad — right-pad `s` with `p` rune until the result reaches `maxlen` // bytes. Symmetric with [[lpad]]. ref/hare/strings/pad.ha:39. export fn rpad(s: str, p: rune, maxlen: i32) str = { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index f27cd253..3ff6ab72 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -2743,6 +2743,62 @@ export fn lpad(s: str, p: rune, maxlen: i32) str = { return r; }; +// replace — fresh allocation of `s` with every non-overlapping +// occurrence of `needle` replaced by `target`. Caller releases with +// `os.free(r.ptr, r.len: u64)`. ref/hare/strings/replace.ha:8 (#4). +// +// Hare delegates to [[multireplace]] with a single pair; ww has no +// `(str, str)` variadic shape today (#39), so this is a standalone +// two-pass implementation: pass 1 counts matches to size the result, +// pass 2 copies chunks and `target` into a single fresh buffer. +// Single nomem path (the `alloc([], total)?`) preserves Hare's +// signature without a per-write `append(...)?` (ww's append builtin +// aborts on OOM, #11). Empty `needle` would hasprefix-match every +// position with a zero stride — same infinite loop Hare exhibits at +// ref/hare/strings/replace.ha:31; not gated. +export fn replace(s: str, needle: str, target: str) (str | nomem) = { + let sb: []u8 = toutf8(s); + let nb: []u8 = toutf8(needle); + let tb: []u8 = toutf8(target); + let count: i32 = 0; + let i: i32 = 0; + for (i < sb.len) { + if (bytes.hasprefix(sb[i:sb.len], nb)) { + count += 1; + i += nb.len; + } else { + i += 1; + }; + }; + let total: i32 = sb.len + count * (tb.len - nb.len); + if (total == 0) { + let r: str; + r.ptr = nil; + r.len = 0; + return r; + }; + let res: []u8 = alloc([], total)?; + let off: i32 = 0; + i = 0; + for (i < sb.len) { + if (bytes.hasprefix(sb[i:sb.len], nb)) { + let j: i32 = 0; + for (j < tb.len) { + res.ptr[off + j] = tb.ptr[j]; + j += 1; + }; + off += tb.len; + i += nb.len; + } else { + res.ptr[off] = sb.ptr[i]; + off += 1; + i += 1; + }; + }; + res.len = total; + return fromutf8_unsafe(res); +}; + // rpad — right-pad `s` with `p` rune until the result reaches `maxlen` // bytes. Symmetric with [[lpad]]. ref/hare/strings/pad.ha:39. export fn rpad(s: str, p: rune, maxlen: i32) str = { diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index 0cf03025..7f892622 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -2634,6 +2634,62 @@ export fn lpad(s: str, p: rune, maxlen: i32) str = { return r; }; +// replace — fresh allocation of `s` with every non-overlapping +// occurrence of `needle` replaced by `target`. Caller releases with +// `os.free(r.ptr, r.len: u64)`. ref/hare/strings/replace.ha:8 (#4). +// +// Hare delegates to [[multireplace]] with a single pair; ww has no +// `(str, str)` variadic shape today (#39), so this is a standalone +// two-pass implementation: pass 1 counts matches to size the result, +// pass 2 copies chunks and `target` into a single fresh buffer. +// Single nomem path (the `alloc([], total)?`) preserves Hare's +// signature without a per-write `append(...)?` (ww's append builtin +// aborts on OOM, #11). Empty `needle` would hasprefix-match every +// position with a zero stride — same infinite loop Hare exhibits at +// ref/hare/strings/replace.ha:31; not gated. +export fn replace(s: str, needle: str, target: str) (str | nomem) = { + let sb: []u8 = toutf8(s); + let nb: []u8 = toutf8(needle); + let tb: []u8 = toutf8(target); + let count: i32 = 0; + let i: i32 = 0; + for (i < sb.len) { + if (bytes.hasprefix(sb[i:sb.len], nb)) { + count += 1; + i += nb.len; + } else { + i += 1; + }; + }; + let total: i32 = sb.len + count * (tb.len - nb.len); + if (total == 0) { + let r: str; + r.ptr = nil; + r.len = 0; + return r; + }; + let res: []u8 = alloc([], total)?; + let off: i32 = 0; + i = 0; + for (i < sb.len) { + if (bytes.hasprefix(sb[i:sb.len], nb)) { + let j: i32 = 0; + for (j < tb.len) { + res.ptr[off + j] = tb.ptr[j]; + j += 1; + }; + off += tb.len; + i += nb.len; + } else { + res.ptr[off] = sb.ptr[i]; + off += 1; + i += 1; + }; + }; + res.len = total; + return fromutf8_unsafe(res); +}; + // rpad — right-pad `s` with `p` rune until the result reaches `maxlen` // bytes. Symmetric with [[lpad]]. ref/hare/strings/pad.ha:39. export fn rpad(s: str, p: rune, maxlen: i32) str = {