From 034bb353d9f00e8535bf39878d1cd98010c39a35 Mon Sep 17 00:00:00 2001 From: zaidejjo Date: Wed, 7 Oct 2026 15:08:16 +0300 Subject: [PATCH] fix(stdlib): rfind_in empty returns clamped end (matches wrapper semantics) --- crates/zz_codegen/src/runtime/strings.c | 2 +- crates/zz_stdlib/src/natives/str_mod/mod.rs | 2 +- docs/stdlib.md | 4 +++- tests/fixtures/stdlib/str_find_test.zz | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/zz_codegen/src/runtime/strings.c b/crates/zz_codegen/src/runtime/strings.c index c8ef5a4e..d54435cb 100644 --- a/crates/zz_codegen/src/runtime/strings.c +++ b/crates/zz_codegen/src/runtime/strings.c @@ -1589,7 +1589,7 @@ zz_value zz_str_rfind_in(zz_value s, zz_value sub, zz_value start, zz_value end, size_t needle_len = sub.s->len; size_t lo, hi; clamp_span_c((int64_t)s.s->len, start.i, end.i, &lo, &hi); - if (needle_len == 0) return (zz_value){ZZ_INT, {.i = (int64_t)lo}}; + if (needle_len == 0) return (zz_value){ZZ_INT, {.i = (int64_t)hi}}; const char *fin = src + hi; const char *p = src + lo; int64_t best = -1; diff --git a/crates/zz_stdlib/src/natives/str_mod/mod.rs b/crates/zz_stdlib/src/natives/str_mod/mod.rs index 3a00224e..01f0cc9b 100644 --- a/crates/zz_stdlib/src/natives/str_mod/mod.rs +++ b/crates/zz_stdlib/src/natives/str_mod/mod.rs @@ -261,7 +261,7 @@ fn find_in(hay: &[u8], needle: &[u8], start: i64, end: i64) -> i64 { fn rfind_in(hay: &[u8], needle: &[u8], start: i64, end: i64) -> i64 { let (s, e) = clamp_span(hay.len() as i64, start, end); if needle.is_empty() { - return s as i64; + return e as i64; } match memchr::memmem::rfind(&hay[s..e], needle) { Some(rel) => s as i64 + rel as i64, diff --git a/docs/stdlib.md b/docs/stdlib.md index 7e682320..fec8c8a8 100644 --- a/docs/stdlib.md +++ b/docs/stdlib.md @@ -93,7 +93,9 @@ import std.str | `bytes.to_ints` | `bytes.to_ints(b: bytes) -> [int]` | Opaque byte buffer as plain ints | Offsets are bytes (O(1) per call, O(n) streaming total; matches Rust -`str::find` semantics). `length`/slicing stay char-oriented — convert +`str::find` semantics). Empty `sub`: `find`/`rfind` return the clamped +`from`, `find_in` the clamped start, `rfind_in` the clamped end, +`count_in` returns 0; `_at` never matches empty. `length`/slicing stay char-oriented — convert explicitly when mixing. Negative inputs clamp to 0; empty `sub` returns the clamped position for `find`/`rfind` and never matches for `_at`. `trim_span` strips Unicode White_Space identically on VM and AOT. diff --git a/tests/fixtures/stdlib/str_find_test.zz b/tests/fixtures/stdlib/str_find_test.zz index f4de58c7..3db1c062 100644 --- a/tests/fixtures/stdlib/str_find_test.zz +++ b/tests/fixtures/stdlib/str_find_test.zz @@ -49,6 +49,7 @@ func main() { println("in13={str.find_in(hay, hello_pat, 1, 12)}") println("inmiss={str.find_in(hay, hello_pat, 14, 24)}") println("inempty={str.find_in(hay, empty_pat, 3, 9)}") + println("rinemptyin={str.rfind_in(hay, empty_pat, 3, 9)}") println("rin0={str.rfind_in(hay, hello_pat, 0, 24)}") println("rin13={str.rfind_in(hay, hello_pat, 0, 12)}") println("rinmiss={str.rfind_in(hay, hello_pat, 14, 24)}")