String
String module: functions for working with UTF-8 encoded strings.
March strings are immutable UTF-8 byte sequences. Every string is a refcounted heap allocation with a 24-byte header — there is NO small-string optimisation, so even a 3-byte string costs a malloc. (This comment previously claimed a fat-pointer representation with 15-byte inline storage. That was never true; see specs/2026-07-26-string-performance-profile.md for what the representation actually costs, measured.)
Key naming conventions:
- Functions with
byte_in their name operate on raw byte offsets (O(1)). codepoint_countcounts Unicode codepoints (NOT user-perceived
graphemes — combining characters and emoji clusters count as multiple
codepoints). `grapheme_count` is a legacy ALIAS for it and counts
codepoints too, despite the name: `"e" ++ combining-acute` is one grapheme
but both functions return 2, and a ZWJ emoji sequence is one grapheme but
both return 3. Prefer `codepoint_count`, whose name matches what it does.- Use
byte_sizefor performance-critical paths.
Functions
Return the number of bytes in the UTF-8 encoding of s.
String.byte_size("hello") -- 5
String.byte_size("é") -- 2 (2-byte UTF-8 sequence)
String.byte_size("") -- 0
Cost: O(1) — reads a stored length field.Count the number of Unicode codepoints in the UTF-8 string s.
For ASCII strings this equals the number of visible characters.
For strings with multi-byte UTF-8 sequences, this counts codepoints,
NOT grapheme clusters — combining marks (`á` = `a` + ◌́), emoji
modifiers, and ZWJ sequences each count as multiple codepoints.
String.codepoint_count("hello") -- 5
String.codepoint_count("é") -- 1 (NFC: one codepoint, two bytes)
String.codepoint_count("é") -- 2 (NFD: e + combining acute)
String.codepoint_count("") -- 0
Use a dedicated Unicode library when you need true grapheme cluster
segmentation (for cursor-position arithmetic in a text editor, for
example).
Cost: O(n) — scans the byte sequence.Concatenate two strings.
String.concat("foo", "bar") -- "foobar"
Prefer the `++` operator for inline concatenation. Use `IOList` when
building many string segments to avoid O(n²) copying.
Cost: O(|a| + |b|) — allocates a new string.Return true if substring appears anywhere in s.
String.contains("hello world", "world") -- true
String.contains("hello world", "xyz") -- false
String.contains("abc", "") -- true (empty is always found)
Cost: O(n * m) worst case, where n = byte_size(s) and m = byte_size(substring).Return true if s ends with suffix.
String.ends_with("hello", "lo") -- true
String.ends_with("hello", "he") -- false
String.ends_with("hello", "") -- true
Cost: O(|suffix|).Convert a float to a string using OCaml's default float formatting.
String.from_float(3.14) -- "3.14"
String.from_float(1.0) -- "1."
String.from_float(1e10) -- "10000000000."
The exact format matches OCaml's `string_of_float`.
Cost: O(1).Convert an integer to its decimal string representation.
String.from_int(42) -- "42"
String.from_int(-7) -- "-7"
String.from_int(0) -- "0"
Cost: O(digits).Legacy alias for codepoint_count. Despite the name, this function counts codepoints, not grapheme clusters; new code should call codepoint_count directly. Retained for backward compatibility.
Find the byte offset of the first occurrence of substring in s. Returns Some(offset) if found, None if not found.
String.index_of("hello", "ll") -- Some(2)
String.index_of("hello", "xyz") -- None
String.index_of("hello", "") -- Some(0)
Cost: O(n * m) worst case.Find the first occurrence of substring at or after byte offset start.
The result is an index into `s` itself, not relative to `start`, so it can be
fed straight back in as the next `start` when tokenizing.
Prefer this over slicing off the tail and searching again: the tail form
re-copies the remaining bytes on every step, making a full tokenize O(n²)
in bytes copied.
String.index_of_from("a,b,c", ",", 0) -- Some(1)
String.index_of_from("a,b,c", ",", 2) -- Some(3)
String.index_of_from("a,b,c", ",", 4) -- None
A negative `start` is treated as 0; a `start` past the end returns `None`;
an empty `substring` matches at `start`.
Cost: O((n - start) * m) worst case.Return true if s contains no bytes.
String.is_empty("") -- true
String.is_empty("x") -- false
String.is_empty(" ") -- false
Cost: O(1).Join a list of strings with a separator inserted between each element.
String.join(["a", "b", "c"], "-") -- "a-b-c"
String.join([], ",") -- ""
String.join(["solo"], ",") -- "solo"
Cost: O(total bytes in list + (n-1) * |sep|).Find the byte offset of the last occurrence of substring in s. Returns Some(offset) if found, None if not found.
String.last_index_of("hello.world.txt", ".") -- Some(11)
String.last_index_of("hello", "xyz") -- None
String.last_index_of("hello", "") -- Some(5)
Cost: O(n * m) worst case.Pad s on the left with fill until the byte length reaches width. If byte_size(s) >= width, s is returned unchanged.
String.pad_left("hi", 5, "0") -- "000hi"
String.pad_left("hello", 3, "0") -- "hello" (already long enough)
`fill` must be a single-byte string (single ASCII character).
Cost: O(width).Pad s on the right with fill until the byte length reaches width. If byte_size(s) >= width, s is returned unchanged.
String.pad_right("hi", 5, ".") -- "hi..."
String.pad_right("hello", 3, ".") -- "hello"
`fill` must be a single-byte string (single ASCII character).
Cost: O(width).Repeat s exactly n times.
String.repeat("ab", 3) -- "ababab"
String.repeat("x", 0) -- ""
String.repeat("x", 1) -- "x"
Cost: O(n * |s|) — allocates a new string.Replace the first occurrence of old in s with new.
String.replace("aabbaa", "a", "x") -- "xabbaa"
String.replace("hello", "xyz", "!") -- "hello" (no match)
If `old` is the empty string, `s` is returned unchanged.
See `replace_all` to replace every occurrence.
Cost: O(n) scan + O(n) for the result string.Replace every non-overlapping occurrence of old in s with new.
String.replace_all("aabbaa", "a", "x") -- "xxbbxx"
String.replace_all("hello", "l", "r") -- "herro"
If `old` is the empty string, `s` is returned unchanged.
Cost: O(n) where n = byte_size(s).Return s with its bytes reversed.
String.reverse("hello") -- "olleh"
String.reverse("racecar") -- "racecar"
Note: byte-reversal is not the same as grapheme-reversal for multi-byte
UTF-8 codepoints. This function is primarily useful for ASCII strings.
Cost: O(n).Alias for slice_bytes: extract len bytes starting at start.
Return a byte-indexed substring of s starting at byte offset start with length len bytes.
String.slice_bytes("hello world", 6, 5) -- "world"
String.slice_bytes("hello", 0, 100) -- "hello" (clamped)
Both `start` and `len` are clamped to valid byte ranges; no exception is
raised for out-of-bounds values. The returned string is a copy (the
interpreter does not yet implement GC slices).
Cost: O(len) for the copy.Split s on every occurrence of sep, returning a List(String).
String.split("a,b,c", ",") -- ["a", "b", "c"]
String.split("hello", "") -- ["hello"] (empty sep returns whole string)
String.split("aXbXc", "X") -- ["a", "b", "c"]
Adjacent separators produce empty strings in the result list.
Cost: O(n) where n = byte_size(s).Split s on the first occurrence of sep. Returns Some(head, tail) if sep is found, or None if not found.
String.split_first("a:b:c", ":") -- Some("a", "b:c")
String.split_first("hello", ":") -- None
String.split_first("key=val", "=") -- Some("key", "val")
Useful for parsing key-value pairs or protocols where only the first
delimiter matters.
Cost: O(n) scan.Return true if s begins with prefix.
String.starts_with("hello", "he") -- true
String.starts_with("hello", "lo") -- false
String.starts_with("hello", "") -- true
Cost: O(|prefix|).Parse s as a floating-point number. Returns Ok(f) on success, Err(msg) if the string is not a valid float.
String.to_float("3.14") -- Ok(3.14)
String.to_float("1e10") -- Ok(10000000000.0)
String.to_float("abc") -- Err("not a valid float")
Cost: O(n).Parse s as a decimal integer. Returns Ok(n) on success, Err(msg) if the string is not a valid integer.
String.to_int("42") -- Ok(42)
String.to_int("-7") -- Ok(-7)
String.to_int("abc") -- Err("not a valid integer")
Cost: O(n).Convert all ASCII letters in s to lowercase.
String.to_lowercase("HELLO WORLD") -- "hello world"
Only ASCII letters are affected.
Cost: O(n).Convert all ASCII letters in s to uppercase.
String.to_uppercase("Hello World") -- "HELLO WORLD"
Only ASCII letters (A–Z, a–z) are affected. For full Unicode case mapping
use an external library.
Cost: O(n).Remove leading and trailing ASCII whitespace (spaces, tabs, newlines, CR).
String.trim(" hello ") -- "hello"
String.trim("\thello\n") -- "hello"
String.trim("hello") -- "hello"
Cost: O(n).Remove trailing ASCII whitespace only.
String.trim_end(" hello ") -- " hello"
Cost: O(n).Remove leading ASCII whitespace only.
String.trim_start(" hello ") -- "hello "
Cost: O(n).