W# 0.2.0

Strings

std/str, and what str is made of.

const str = @import("std/str");

A str is a heap object with its length in the header and its bytes inline, which is the same shape an array has. It is immutable, and == compares contents, so a string built at run time equals a literal.

Functions

len(s) i64the length in bytes
concat(a, b) str
eq(a, b) boolthe same answer as ==, as a function value
substr(s, from, to) strhalf-open, so substr(s, 0, len(s)) is s
find(haystack, needle) i64the index, or -1
split(s, sep) []str
join(parts, sep) strlinear: it measures, allocates once and copies
repeat(s, n) strthe same shape
replace(s, from, to) strtwo passes, so it is linear by construction
starts_with(s, prefix) bool
from_int(i64) str, from_float(f64) str
parse_int(s) !i64
byte_at(s, i) u8, from_byte(u8) str
to_lower(s) str, to_upper(s) str
trim(s) strleading and trailing whitespace
hash(s) u64FNV-1a with the SplitMix64 finaliser, which is what std/map keys on

join and repeat are linear

Both were concat in a loop, which copies the accumulator on every iteration. Joining 150,000 pieces into 600 KB took 9.4 seconds. They measure, allocate once and copy now, and the same call takes about 0.08 seconds with compilation included.

That matters because building output from pieces is what a program does. Before the fix, bytes.Buf was around 190 times faster at the same job, so anything that reached for the obvious spelling was paying for it.

str.hash is a builtin rather than W#, because hashing in W# would be one str.byte_at per byte and therefore one stack walk per byte under --gc-stress.

Using it

const str = @import("std/str");

fn main() i64 {
    const greeting = str.concat("hello, ", "world");
    print(greeting);
    print(str.len(greeting));
    print(greeting == "hello, world");
    print(str.substr(greeting, 7, str.len(greeting)));

    for (str.split("id,name,email", ",")) |field| { print(field); }
    print(str.join(str.split("a b c", " "), " | "));
    return 0;
}

Bytes

str is bytes, not characters. len counts bytes and byte_at returns one, so a multi-byte UTF-8 character is several of both. There is no character type and no decoder in the library yet.

To work on a string as a buffer, cross to []u8 through std/bytes:

const bytes = @import("std/bytes");

const b = bytes.of("hello");     // str to []u8
const s = bytes.to_str(b);       // []u8 back to str

split is written in W# rather than as a builtin, because it produces an array of strings and therefore moves references between objects. That is the rule the whole library follows.

Last changed 17 September 2026. Improve this page

On this page