Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/zz_cli/tests/batch_lists.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ pub const ELIGIBLE: &[(&str, &str, bool)] = &[
("stdlib", "vec_nested_str_parity.zz", true),
("stdlib", "console.zz", false),
("stdlib", "str_extended_test.zz", false),
("stdlib", "str_find_test.zz", true),
("stdlib", "str_bytes_test.zz", true),
("syntax", "match.zz", false),
("syntax", "match_assign.zz", true),
("syntax", "return_in_loops.zz", false),
Expand Down
1 change: 1 addition & 0 deletions crates/zz_codegen/src/lower/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,7 @@ pub(crate) fn native_impl(name: &str) -> Option<&'static str> {
"str.to_lower" | "std.str.to_lower" | "str.lower" | "std.str.lower" => Some("zz_str_lower"),
"str.to_upper" | "std.str.to_upper" | "str.upper" | "std.str.upper" => Some("zz_str_upper"),
"str.replace" | "std.str.replace" => Some("zz_str_replace"),
"str.count" | "std.str.count" => Some("zz_str_count"),
"str.contains" | "std.str.contains" => Some("zz_str_contains"),
"str.starts_with" | "std.str.starts_with" | "str.startswith" | "std.str.startswith" => {
Some("zz_str_startswith")
Expand Down
69 changes: 62 additions & 7 deletions crates/zz_codegen/src/runtime/strings.c
Original file line number Diff line number Diff line change
Expand Up @@ -1414,6 +1414,46 @@ zz_value zz_str_replace(zz_value s, zz_value old_s, zz_value new_s, int *err) {
return (zz_value){ZZ_STR, {.s = out}};
}

// memchr-skip search core: jump to the next first-needle-byte, then
// verify with memcmp. Portable C89 + memchr, near-memmem speed for
// short needles (the common case).
static const char *scan_skip(const char *h, const char *hend, char first) {
const char *p = h;
while (p < hend) {
const char *hit = (const char *)memchr(p, first, (size_t)(hend - p));
if (!hit) return hend;
p = hit;
return p;
}
return hend;
}

// str.count(s, sub) — non-overlapping occurrences, no allocation.
// Empty sub counts chars+1 (matches the split-based version it replaces).
zz_value zz_str_count(zz_value s, zz_value sub, int *err) {
(void)err;
if (s.tag != ZZ_STR || sub.tag != ZZ_STR) return (zz_value){ZZ_INT, {.i = 0}};
const char *src = zz_str_ptr(s.s);
size_t src_len = s.s->len;
const char *needle = zz_str_ptr(sub.s);
size_t needle_len = sub.s->len;
if (needle_len == 0) return (zz_value){ZZ_INT, {.i = (int64_t)zz_str_char_len(s.s) + 1}};
int64_t n = 0;
const char *p = src;
const char *end = src + src_len;
while (p + needle_len <= end) {
p = scan_skip(p, end, needle[0]);
if (p + needle_len > end) break;
if (memcmp(p, needle, needle_len) == 0) {
n++;
p += needle_len;
} else {
p++;
}
}
return (zz_value){ZZ_INT, {.i = n}};
}

// str.contains(s, sub) — check if s contains sub.
zz_value zz_str_contains(zz_value s, zz_value sub, int *err) {
(void)err;
Expand All @@ -1423,8 +1463,13 @@ zz_value zz_str_contains(zz_value s, zz_value sub, int *err) {
const char *needle = zz_str_ptr(sub.s);
size_t needle_len = sub.s->len;
if (needle_len == 0) return (zz_value){ZZ_BOOL, {.b = true}};
for (size_t i = 0; i + needle_len <= src_len; i++) {
if (memcmp(src + i, needle, needle_len) == 0) return (zz_value){ZZ_BOOL, {.b = true}};
const char *end = src + src_len;
const char *p = src;
while (p + needle_len <= end) {
p = scan_skip(p, end, needle[0]);
if (p + needle_len > end) break;
if (memcmp(p, needle, needle_len) == 0) return (zz_value){ZZ_BOOL, {.b = true}};
p++;
}
return (zz_value){ZZ_BOOL, {.b = false}};
}
Expand Down Expand Up @@ -1453,8 +1498,13 @@ static int64_t find_from(zz_value s, zz_value sub, int64_t from) {
size_t base = snap_fwd(src, src_len, (size_t)start);
if (needle_len == 0) return (int64_t)base;
if (base >= src_len) return -1;
for (size_t i = base; i + needle_len <= src_len; i++) {
if (memcmp(src + i, needle, needle_len) == 0) return (int64_t)i;
const char *end = src + src_len;
const char *p = src + base;
while (p + needle_len <= end) {
p = scan_skip(p, end, needle[0]);
if (p + needle_len > end) break;
if (memcmp(p, needle, needle_len) == 0) return (int64_t)(p - src);
p++;
}
return -1;
}
Expand All @@ -1469,9 +1519,14 @@ static int64_t rfind_from(zz_value s, zz_value sub, int64_t from) {
size_t end = snap_bwd(src, (size_t)end_c);
if (needle_len == 0) return (int64_t)end;
int64_t best = -1;
for (size_t i = 0; i + needle_len <= src_len; i++) {
if (i > end) break;
if (memcmp(src + i, needle, needle_len) == 0) best = (int64_t)i;
const char *fin = src + src_len;
const char *p = src;
while (p + needle_len <= fin) {
p = scan_skip(p, fin, needle[0]);
if (p + needle_len > fin) break;
if ((size_t)(p - src) > end) break;
if (memcmp(p, needle, needle_len) == 0) best = (int64_t)(p - src);
p++;
}
return best;
}
Expand Down
1 change: 1 addition & 0 deletions crates/zz_codegen/src/runtime/strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ zz_value zz_str_length(zz_value s, int *err);
zz_value zz_str_lower(zz_value s, int *err);
zz_value zz_str_upper(zz_value s, int *err);
zz_value zz_str_replace(zz_value s, zz_value old_s, zz_value new_s, int *err);
zz_value zz_str_count(zz_value s, zz_value sub, int *err);
zz_value zz_str_contains(zz_value s, zz_value sub, int *err);
zz_value zz_str_startswith(zz_value s, zz_value prefix, int *err);
zz_value zz_str_endswith(zz_value s, zz_value suffix, int *err);
Expand Down
2 changes: 0 additions & 2 deletions crates/zz_codegen/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ const KNOWN_CODEGEN_GAPS: &[(&str, &str)] = &[
"numeric constant; lowers to float literal",
),
("std.str.repeat", "pure ZZ; no C codegen"),
("std.str.count", "pure ZZ; no C codegen"),
("std.str.is_empty", "pure ZZ; no C codegen"),
("std.str.reverse", "pure ZZ; no C codegen"),
("std.str.pad_left", "pure ZZ; no C codegen"),
Expand Down Expand Up @@ -172,7 +171,6 @@ const KNOWN_CODEGEN_GAPS: &[(&str, &str)] = &[
("math.mean_f", "pure ZZ alias"),
("math.median_f", "pure ZZ alias"),
("str.repeat", "pure ZZ alias"),
("str.count", "pure ZZ alias"),
("str.is_empty", "pure ZZ alias"),
("str.reverse", "pure ZZ alias"),
("str.pad_left", "pure ZZ alias"),
Expand Down
1 change: 1 addition & 0 deletions crates/zz_stdlib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ zz_native_rt = { path = "../zz_native_rt" }
base64 = "0.22"
crossbeam-deque = "0.8"
libc = "0.2"
memchr = "2"
urlencoding = "2"
reqwest = { version = "0.12", features = ["blocking"] }
rusqlite = { version = "0.32", features = ["bundled", "column_decltype"] }
Expand Down
14 changes: 14 additions & 0 deletions crates/zz_stdlib/src/natives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,13 @@ fn build_stdlib_natives() -> HashMap<String, NativeEntry> {
f: str_mod::str_contains,
},
);
m.insert(
"std.str.count".into(),
NativeEntry {
arity: 2,
f: str_mod::str_count,
},
);
m.insert(
"std.str.find".into(),
NativeEntry {
Expand Down Expand Up @@ -433,6 +440,13 @@ fn build_stdlib_natives() -> HashMap<String, NativeEntry> {
f: str_mod::str_contains,
},
);
m.insert(
"str.count".into(),
NativeEntry {
arity: 2,
f: str_mod::str_count,
},
);
m.insert(
"str.find".into(),
NativeEntry {
Expand Down
19 changes: 19 additions & 0 deletions crates/zz_stdlib/src/natives/str_mod/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,25 @@ fn byte_rfind(s: &str, sub: &str, from: i64) -> i64 {
// str.bytes(s) — UTF-8 bytes as plain ints. One O(n) copy; the
// result composes with every [int] API (indexing, snapshots, the
// bytes.* builder vocabulary). The bridge find/rfind/trim_span need.
// str.count(s, sub) — non-overlapping occurrences, no allocation.
// Edge semantics mirror the old split-based version exactly:
// empty sub counts chars(s)+1 (split inserts between every char plus
// both ends: "abc" -> 4, "" -> 1).
pub(crate) fn str_count(
_interp: &mut Interp,
args: &mut Vec<Value>,
_span: Span,
) -> Result<Value, EvalError> {
let s = expect_str(args, 0, "std.str.count")?;
let sub = expect_str(args, 1, "std.str.count")?;
if sub.is_empty() {
return Ok(Value::Int(s.chars().count() as i64 + 1));
}
Ok(Value::Int(
memchr::memmem::find_iter(s.as_bytes(), sub.as_bytes()).count() as i64,
))
}

pub(crate) fn str_bytes(
_interp: &mut Interp,
args: &mut Vec<Value>,
Expand Down
7 changes: 1 addition & 6 deletions crates/zz_stdlib/zz/str/mod.zz
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
// - str.repeat uses doubling strategy: O(n log n) instead of O(n^2).
// - str.pad_left/right build padding once, concat once: O(k) not O(k^2).

// ── Repetition & counting ────────────────────────────────────────────────────
// ── Repetition ───────────────────────────────────────────────────────────

func str.repeat(s: str, n: int) -> str {
// Doubling strategy: build result in O(log n) concatenations.
Expand All @@ -31,11 +31,6 @@ func str.repeat(s: str, n: int) -> str {
result
}

func str.count(s: str, sub: str) -> int {
parts := str.split(s, sub)
n := vec.len(parts) - 1
n
}

// ── Queries ──────────────────────────────────────────────────────────────────

Expand Down
1 change: 1 addition & 0 deletions docs/stdlib.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import std.str
| `str.length` | `str.length(s: str) -> int` | String length |
| `str.split` | `str.split(s: str, sep: str) -> [str]` | Split by separator |
| `str.contains` | `str.contains(s: str, sub: str) -> bool` | Check substring |
| `str.count` | `str.count(s: str, sub: str) -> int` | Non-overlapping occurrences (empty `sub` counts chars+1) |
| `str.trim` | `str.trim(s: str) -> str` | Trim whitespace |
| `str.to_upper` | `str.to_upper(s: str) -> str` | Uppercase |
| `str.to_lower` | `str.to_lower(s: str) -> str` | Lowercase |
Expand Down
8 changes: 8 additions & 0 deletions tests/fixtures/stdlib/str_find_test.zz
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ func main() {
caf_pat := "caf"
aa_pat := "aa"
aaa_str := "aaa"
abc3 := "abc"
aaaa4 := "aaaa"
println("find0={str.find(hay, hello_pat, 0)}")
println("find6={str.find(hay, hello_pat, 6)}")
println("findmiss={str.find(hay, zzz_pat, 0)}")
Expand Down Expand Up @@ -41,5 +43,11 @@ func main() {
println("nbspan={nsp[0]},{nsp[1]}")
println("overlap={str.find(aaa_str, aa_pat, 0)}")
println("stdspell={std.str.find(hay, moon_pat, 0)}")
aaa3 := "aaa"
println("count1={str.count(aaa3, aa_pat)}")
println("countempty={str.count(abc3, empty_pat)}")
println("countboth={str.count(empty_pat, empty_pat)}")
println("countnohay={str.count(empty_pat, moon_pat)}")
println("countfour={str.count(aaaa4, aa_pat)}")
println("str_find_ok")
}
Loading