Standard Library — Functions and Methods


Contents


Built-in Functions

print

fn print(value: str) -> ()

Writes a string to standard output without a trailing newline.

print("hello ")
print("world")
# output: hello world

Because the parameter type is str, non-string values must be converted before passing them to print. String interpolation handles this automatically:

print("count: {42}")            # int satisfies ToString, so interpolation works

print is available in all source files without import.

println

fn println(value: str) -> ()

Writes a string to standard output followed by a newline (\n).

println("hello world")
println("count: {42}")

println is the standard way to produce line-oriented output. Like print, the parameter type is str — use string interpolation to format non-string values.

println is available in all source files without import.

readln

fn readln() -> Result[str, IoError]

Reads a single line of text from standard input. On success, returns Ok(str) containing the line without the trailing newline character (\n or \r\n). If the input stream contains a line terminator, it is consumed but stripped from the result.

print("What is your name? ")
match readln()
    Ok(name) then println("Hello, {name}!")
    Err(e) then println("Error reading input")
end

Error conditions: Returns Err(IoError) when:

Partial line at EOF: If EOF is reached after reading one or more characters but before encountering a line terminator, readln returns Ok(str) containing the characters read (treating EOF as an implicit line terminator). If EOF is reached immediately (before any characters are read), returns Err(IoError.Eof).

See the IoError enum definition in stdlib/types.md.

readln blocks until a complete line is available or an error occurs. It is intended for interactive and scripting use. For file I/O, see the io standard library module (stdlib/io.md). Programs that require non-blocking or byte-level I/O are not yet available (see roadmap.md).

readln is available in all source files without import.

panic

fn panic(message: str) -> never

Terminates execution immediately with a runtime error. The message is included in the error output. The exact output format and termination mechanism are implementation-defined.

Because panic returns never, it can appear in any expression context — the type checker treats it as satisfying any type:

fn unwrap[T](opt: Option[T]) -> T
    match opt
        Some(value) then return value
        None then panic("unwrap called on None")
    end
end

panic is available in all source files without import.

toList

fn toList[T](iter: Iterable[T]) -> List[T]
    mut result: List[T] = []
    for item in iter
        result.push(item)
    end
    return result
end

Materializes any iterable into a List[T]. This is a free function rather than a method on Iterable[out T] because List[T] is invariant — placing it as a return type on a covariant interface would violate variance checking rules.

doubledEvens = items.map(fn(x) x * 2)
                     .filter(fn(x) x > 5) |> toList    # List[int]

toList is available in all source files without import.

toMap

fn toMap[K, V](iter: Iterable[Tuple[K, V]]) -> Map[K, V]
    where K: Eq[K] & Hash
    mut result: Map[K, V] = {}
    for (k, v) in iter
        result[k] = v
    end
    return result
end

Materializes an iterable of key-value tuples into a Map[K, V]. If duplicate keys appear, the last value wins. The where clause requires K to satisfy Eq[K] and Hash, matching the constraints of Map[K, V] itself.

pairs = [(1, "one"), (2, "two"), (3, "three")]
m = pairs.iter() |> toMap    # Map[int, str]

Like toList, this is a free function rather than a method on Iterable for variance correctness.

toMap is available in all source files without import.

toSet

fn toSet[T](iter: Iterable[T]) -> Set[T]
    where T: Eq[T] & Hash
    mut result: Set[T] = Set.from([])
    for item in iter
        result.add(item)
    end
    return result
end

Materializes any iterable into a Set[T], discarding duplicates. The where clause requires T to satisfy Eq[T] and Hash, matching the constraints of Set[T] itself.

unique = [1, 2, 3, 2, 1].iter().filter(fn(x) x > 1) |> toSet    # Set[int]

Like toList, this is a free function rather than a method on Iterable for variance correctness.

toSet is available in all source files without import.


Interface Conformance Summary

The following table summarizes which built-in types satisfy which prelude interfaces. "Conditional" means the conformance depends on type parameters satisfying the same interface.

Type PartialEq Eq Hash Comparable ToString Iterable
bool
byte
int
uint
float ✅² ❌³ ✅⁴ ✅⁵
str
Option[T] conditional conditional conditional conditional
Result[T, E] conditional conditional conditional conditional
GeneratorResult[Y, R] conditional conditional conditional conditional
List[T] conditional conditional ❌⁶ conditional
Map[K, V] conditional conditional ❌⁶ conditional ✅ (pairs)
Set[T] ❌⁶ conditional
Buffer ❌⁶
Tuple[...] conditional conditional conditional conditional
Generator[Y, R, N] ✅¹

¹ Generator[T, (), never] satisfies Iterable[T]. Generators with non-() R or non-never N do not satisfy Iterable.

² float implements PartialEq[float] with IEEE 754 semantics: NaN != NaN, -0.0 == 0.0. The == and != operators desugar through PartialEq.eq() like any other type. See lang/semantics/values.md §2.11.2.

³ float does not implement Eq[float] because IEEE 754 NaN violates the reflexivity guarantee (NaN != NaN). Generic code constrained by T: Eq[T] cannot use float.

float: Hash guarantees 0.0 and -0.0 produce the same hash. All NaN bit patterns produce the same hash. Note that float cannot be used as a Map key or Set element because both require Eq, which float does not satisfy (see ³). The Hash conformance exists for use in compound types (e.g., a struct containing a float field that implements Eq by excluding NaN values).

float: Comparable — comparison operators (<, >, <=, >=) on float bypass Comparable desugaring and use IEEE 754 intrinsics (all NaN comparisons return false). The compare method provides a total ordering where NaN sorts after all other values. See lang/semantics/values.md §2.11.2.

List[T], Map[K, V], Set[T], and Buffer intentionally omit Hash. All four are mutable containers — hashing a mutable object is dangerous because mutations after insertion would invalidate the hash, breaking map lookup invariants. Tuple[...] implements Hash conditionally because tuples are immutable, making their hash values stable.

Conditional conformance rules:

User-defined types: Structs must explicitly declare implements InterfaceName and provide methods whose visibility matches the interface (pub for pub interfaces, pkg for pkg interfaces, private for private interfaces) — see lang/structs/core.md §4.5.

Built-in type conformance: All built-in types (byte, int, uint, float, str, bool, Option[T], Result[T, E], List[T], Map[K, V], Set[T], Buffer, Tuple[...], Generator[Y, R, N]) have compiler-provided interface conformance. These types do not have visible implements declarations — the conformance rules are built into the language and documented in this section. Enums have automatic PartialEq, Eq, and ToString conformance without requiring implements declarations. For enums with associated data, ToString conformance is conditional on all associated data types satisfying ToString; simple enums (no associated data) satisfy ToString unconditionally.


Primitive Type Methods

The built-in primitive types have methods that are always available. These are not defined via interfaces — they are intrinsic to the types themselves.

Methods without a self parameter are static methods, called on the type name directly (e.g., int.parse("42"), float.INFINITY). This is consistent with how struct static methods work (see §4.1, §4.3). Methods with a self parameter are instance methods, called on a value.

Methods can be called on literal values directly:

x = 42.toFloat()          # 42.0
y = 3.7.round()            # Some(4)
z = 255b.toInt()           # 255

bool Methods

bool has no intrinsic methods.

byte Methods

Conversion:

Signature Description
fn toInt(self) -> int Convert to int. Always succeeds (lossless widening).
fn toUint(self) -> uint Convert to uint. Always succeeds (lossless widening).
fn toFloat(self) -> float Convert to float. Always succeeds (lossless widening).

Arithmetic helpers:

Signature Description
fn pow(self, exp: byte) -> byte Raise self to the power exp. Panics on overflow. 0.pow(0) returns 1.
fn min(self, other: byte) -> byte Return the smaller of self and other.
fn max(self, other: byte) -> byte Return the larger of self and other.
fn clamp(self, low: byte, high: byte) -> byte Clamp self to the range [low, high]. Panics if low > high.

Wrapping arithmetic (modular wrap-around, never panics):

Signature Description
fn wrappingAdd(self, other: byte) -> byte Addition with modular wrapping (mod 256).
fn wrappingSub(self, other: byte) -> byte Subtraction with modular wrapping (mod 256).
fn wrappingMul(self, other: byte) -> byte Multiplication with modular wrapping (mod 256).

Saturating arithmetic (clamps to 0 / 255, never panics):

Signature Description
fn saturatingAdd(self, other: byte) -> byte Addition clamped to [0, 255].
fn saturatingSub(self, other: byte) -> byte Subtraction clamped to [0, 255].
fn saturatingMul(self, other: byte) -> byte Multiplication clamped to [0, 255].

Parsing:

Signature Description
fn parse(s: str) -> Result[byte, str] Parse an unsigned 8-bit integer from a string. Returns Err with a description on failure.

byte.parse accepts an optional leading + sign followed by one or more digits. Decimal is the default. The prefixes 0x (hex), 0o (octal), and 0b (binary) are recognized, matching literal syntax. Underscore separators are NOT accepted. A leading - sign is not accepted. The b suffix used in byte literals (e.g., 42b) is not accepted in parsed strings.

Returns Err if the string is empty, contains invalid characters, or the value exceeds 255.

byte.parse("42")            # Ok(42b)
byte.parse("255")           # Ok(255b)
byte.parse("0xFF")          # Ok(255b)
byte.parse("0b11111111")    # Ok(255b)
byte.parse("256")           # Err("overflow")
byte.parse("-1")            # Err("invalid character: -")
byte.parse("42b")           # Err("invalid character: b")
byte.parse("")              # Err("empty string")

Constants (accessed on the type, not on a value):

Expression Value
byte.MIN 0
byte.MAX 255
x: byte = 200b
x + 100b                    # panic: integer overflow
x.wrappingAdd(100b)        # 44b (wraps mod 256)
x.saturatingAdd(100b)      # 255b (clamped)

y: byte = 0b
y - 1b                      # panic: integer overflow
y.wrappingSub(1b)          # 255b (wraps)
y.saturatingSub(1b)        # 0b (clamped)

# Conversions
42b.toInt()                 # 42
42b.toUint()                # 42u
42b.toFloat()               # 42.0

int Methods

Conversion:

Signature Description
fn toFloat(self) -> float Convert to float. Exact for integers up to 2^53; larger values may lose precision.
fn toUint(self) -> uint Convert to uint. Panics if self is negative.
fn toByte(self) -> byte Convert to byte. Panics if self is outside 0–255.
fn wrappingToUint(self) -> uint Reinterpret the bit pattern as uint. Never panics.
fn wrappingToByte(self) -> byte Truncate to the low 8 bits. Never panics.

Arithmetic helpers:

Signature Description
fn pow(self, exp: int) -> int Raise self to the power exp. Panics if exp is negative. Panics on overflow. 0.pow(0) returns 1.
fn abs(self) -> int Absolute value. Panics if self is int.MIN (result would overflow).
fn min(self, other: int) -> int Return the smaller of self and other.
fn max(self, other: int) -> int Return the larger of self and other.
fn clamp(self, low: int, high: int) -> int Clamp self to the range [low, high]. Panics if low > high.

Wrapping arithmetic (two's complement wrap-around, never panics):

Signature Description
fn wrappingAdd(self, other: int) -> int Addition with two's complement wrapping.
fn wrappingSub(self, other: int) -> int Subtraction with two's complement wrapping.
fn wrappingMul(self, other: int) -> int Multiplication with two's complement wrapping.

Saturating arithmetic (clamps to int.MIN / int.MAX, never panics):

Signature Description
fn saturatingAdd(self, other: int) -> int Addition clamped to [int.MIN, int.MAX].
fn saturatingSub(self, other: int) -> int Subtraction clamped to [int.MIN, int.MAX].
fn saturatingMul(self, other: int) -> int Multiplication clamped to [int.MIN, int.MAX].

Parsing:

Signature Description
fn parse(s: str) -> Result[int, str] Parse a signed integer from a string. Returns Err with a description on failure.

int.parse accepts an optional sign (+ or -) followed by one or more digits. Decimal is the default. The prefixes 0x (hex), 0o (octal), and 0b (binary) are recognized, matching literal syntax. Hex prefixes (0x, 0X) and hex digits (A-F, a-f) are case-insensitive. For example, 0xFF, 0xff, 0XFF, and 0Xff are all valid and equivalent. Underscore separators are NOT accepted - they are only valid in source code literals, not in parsed strings. Leading and trailing whitespace is not accepted.

Returns Err if the string is empty, contains invalid characters, or the value overflows the signed 64-bit range.

int.parse("42")              # Ok(42)
int.parse("-100")            # Ok(-100)
int.parse("+7")              # Ok(7)
int.parse("0xFF")            # Ok(255)
int.parse("0b1010")          # Ok(10)
int.parse("0o77")            # Ok(63)
int.parse("1_000")           # Err("invalid character: _")
int.parse("")                # Err("empty string")
int.parse("hello")           # Err("invalid character: h")
int.parse("99999999999999999999")  # Err("overflow")

Constants (accessed on the type, not on a value):

Expression Value
int.MIN −9,223,372,036,854,775,808 (−2^63)
int.MAX 9,223,372,036,854,775,807 (2^63 − 1)
n = -5
n.abs()                     # 5
n.toFloat()                # -5.0
n.toUint()                 # panic: negative int cannot convert to uint
n.toByte()                 # panic: value outside 0–255
42.toFloat()               # 42.0
42.toByte()                # 42b
10.min(3)                   # 3
10.max(20)                  # 20
15.clamp(0, 10)             # 10

# Wrapping conversions
(-1).wrappingToByte()      # 255b (low 8 bits of two's complement)
256.wrappingToByte()       # 0b

# Wrapping and saturating
x = int.MAX
x + 1                       # panic: integer overflow
x.wrappingAdd(1)           # -9_223_372_036_854_775_808 (wraps)
x.saturatingAdd(1)         # 9_223_372_036_854_775_807 (clamped)

uint Methods

Conversion:

Signature Description
fn toFloat(self) -> float Convert to float. Exact for values up to 2^53; larger values may lose precision.
fn toInt(self) -> int Convert to int. Panics if self exceeds int.MAX.
fn toByte(self) -> byte Convert to byte. Panics if self exceeds 255.
fn wrappingToInt(self) -> int Reinterpret the bit pattern as int. Never panics.
fn wrappingToByte(self) -> byte Truncate to the low 8 bits. Never panics.

Arithmetic helpers:

Signature Description
fn pow(self, exp: uint) -> uint Raise self to the power exp. Panics on overflow. 0u.pow(0u) returns 1u.
fn min(self, other: uint) -> uint Return the smaller of self and other.
fn max(self, other: uint) -> uint Return the larger of self and other.
fn clamp(self, low: uint, high: uint) -> uint Clamp self to the range [low, high]. Panics if low > high.

Wrapping arithmetic (modular wrap-around, never panics):

Signature Description
fn wrappingAdd(self, other: uint) -> uint Addition with modular wrapping.
fn wrappingSub(self, other: uint) -> uint Subtraction with modular wrapping.
fn wrappingMul(self, other: uint) -> uint Multiplication with modular wrapping.

Saturating arithmetic (clamps to 0 / uint.MAX, never panics):

Signature Description
fn saturatingAdd(self, other: uint) -> uint Addition clamped to [0, uint.MAX].
fn saturatingSub(self, other: uint) -> uint Subtraction clamped to [0, uint.MAX].
fn saturatingMul(self, other: uint) -> uint Multiplication clamped to [0, uint.MAX].

Parsing:

Signature Description
fn parse(s: str) -> Result[uint, str] Parse an unsigned decimal integer from a string. Returns Err with a description on failure.

uint.parse accepts an optional leading + sign followed by one or more digits. Decimal is the default. The prefixes 0x (hex), 0o (octal), and 0b (binary) are recognized, matching literal syntax. Underscore separators are NOT accepted - they are only valid in source code literals, not in parsed strings. A leading - sign is not accepted. Leading and trailing whitespace is not accepted. The u suffix used in uint literals (e.g., 42u) is not accepted in parsed strings.

Returns Err if the string is empty, contains invalid characters, or the value overflows the unsigned 64-bit range.

uint.parse("42")            # Ok(42u)
uint.parse("+100")          # Ok(100u)
uint.parse("0xFF")          # Ok(255u)
uint.parse("0b1010")        # Ok(10u)
uint.parse("0o77")          # Ok(63u)
uint.parse("1_000")         # Err("invalid character: _")
uint.parse("-1")            # Err("invalid character: -")
uint.parse("42u")           # Err("invalid character: u")
uint.parse("")              # Err("empty string")

Constants (accessed on the type, not on a value):

Expression Value
uint.MIN 0
uint.MAX 18,446,744,073,709,551,615 (2^64 − 1)
x: uint = 42u
x.toFloat()                # 42.0
x.toInt()                  # 42
x.toByte()                 # 42b
300u.toByte()              # panic: value exceeds 255
300u.wrappingToByte()      # 44b (low 8 bits)

y: uint = 0u
y - 1u                      # panic: integer overflow
y.wrappingSub(1u)          # 18_446_744_073_709_551_615u (wraps)
y.saturatingSub(1u)        # 0u (clamped)

float Methods

Conversion:

Signature Description
fn round(self) -> Option[int] Round to the nearest integer. Halfway values (.5) round up (toward +∞). Returns None if NaN, infinite, or out of int range.
fn floor(self) -> Option[int] Round toward −∞. Returns None if NaN, infinite, or out of int range.
fn ceiling(self) -> Option[int] Round toward +∞. Returns None if NaN, infinite, or out of int range.
fn roundUint(self) -> Option[uint] Round to the nearest unsigned integer. Returns None if NaN, infinite, negative, or out of uint range.
fn floorUint(self) -> Option[uint] Round toward −∞, returning uint. Returns None if NaN, infinite, negative, or out of uint range.
fn ceilingUint(self) -> Option[uint] Round toward +∞, returning uint. Returns None if NaN, infinite, negative, or out of uint range.

Arithmetic helpers:

Signature Description
fn pow(self, exp: float) -> float Raise self to the power exp. Follows IEEE 754 rules; never panics. Negative exponents are allowed: 2.0.pow(-3.0) produces 0.125. 0.0.pow(0.0) returns 1.0.
fn abs(self) -> float Absolute value. (-0.0).abs() returns 0.0. NaN.abs() returns NaN.
fn min(self, other: float) -> float Return the smaller of self and other. If either is NaN, returns NaN.
fn max(self, other: float) -> float Return the larger of self and other. If either is NaN, returns NaN.
fn clamp(self, low: float, high: float) -> float Clamp to [low, high]. Returns NaN if self is NaN. Panics if low > high or if low or high is NaN.
fn sqrt(self) -> float Square root. Returns NaN for negative values.

Special-value testing:

Signature Description
fn isNan(self) -> bool true if self is NaN.
fn isInfinite(self) -> bool true if self is positive or negative infinity.
fn isFinite(self) -> bool true if self is neither NaN nor infinite.

Parsing:

Signature Description
fn parse(s: str) -> Result[float, str] Parse a floating-point number from a string. Returns Err with a description on failure.

float.parse accepts standard decimal floating-point notation: an optional sign (+ or -), integer digits, an optional decimal point with fractional digits, and an optional exponent (e or E followed by an optional sign and digits). The special strings "Infinity", "-Infinity", and "NaN" are also accepted.

Special value strings are case-sensitive. Only exact matches "Infinity", "-Infinity", and "NaN" are accepted. Variants like "inf", "infinity", "nan", or "+Infinity" return Err.

Leading and trailing whitespace is not accepted.

float.parse("3.14")         # Ok(3.14)
float.parse("-0.5")         # Ok(-0.5)
float.parse("1e10")         # Ok(10000000000.0)
float.parse("2.5E-3")       # Ok(0.0025)
float.parse("42")           # Ok(42.0)
float.parse("Infinity")     # Ok(float.INFINITY)
float.parse("-Infinity")    # Ok(float.NEG_INFINITY)
float.parse("NaN")          # Ok(float.NAN)
float.parse("")             # Err("empty string")
float.parse("hello")        # Err("invalid character: h")

Values that overflow float range produce infinity (not an error), consistent with IEEE 754 overflow semantics. Values that underflow (too small to represent as normalized floats) produce subnormal numbers or zero, following IEEE 754 gradual underflow semantics.

Constants (accessed on the type, not on a value):

Expression Description
float.INFINITY Positive infinity (+∞).
float.NEG_INFINITY Negative infinity (−∞).
float.NAN A quiet NaN value.
float.MAX Largest finite positive value (~1.8 × 10^308).
float.MIN Most negative finite value (~−1.8 × 10^308). Equal to -float.MAX.
float.MIN_POSITIVE Smallest positive non-zero value (~5.0 × 10^−324).
float.EPSILON Difference between 1.0 and the next representable float (~2.2 × 10^−16).
3.7.round()                 # Some(4)
3.2.round()                 # Some(3)
2.5.round()                 # Some(3)  (0.5 rounds up)
(-2.5).round()              # Some(-2)
3.7.floor()                 # Some(3)
(-3.2).floor()              # Some(-4)
3.2.ceiling()               # Some(4)
(-3.7).ceiling()            # Some(-3)

# Special values return None
float.NAN.round()           # None
float.INFINITY.floor()      # None
(-1.0 * float.INFINITY).ceilingUint()  # None — negative infinity

# Special values
x = 0.0 / 0.0
x.isNan()                  # true
(1.0 / 0.0).isInfinite()  # true
3.14.isFinite()            # true

# Constants — no literal syntax for special values
inf = float.INFINITY
nan = float.NAN
println("{inf}")             # "Infinity"
println("{nan}")             # "NaN"

str Methods

Signature Description
fn byteLength(self) -> uint Number of bytes (UTF-8 code units) in the string.
fn charLength(self) -> uint Number of Unicode grapheme clusters in the string. Multi-codepoint sequences (emojis with variation selectors, combining characters) count as one.
fn isEmpty(self) -> bool true if byte length is 0.
fn contains(self, substr: str) -> bool true if substr appears anywhere in the string at a grapheme cluster boundary. Case-sensitive.
fn startsWith(self, prefix: str) -> bool true if the string starts with prefix.
fn endsWith(self, suffix: str) -> bool true if the string ends with suffix.
fn indexOf(self, substr: str) -> Option[uint] Grapheme cluster index of the first occurrence at a grapheme cluster boundary, or None. Consistent with slice and charLength indexing. indexOf("") returns Some(0u) for any string, including the empty string (the empty string is trivially found at position 0).
fn slice(self, start: uint, end: uint) -> str? Return a substring by grapheme cluster range. See below for semantics.
fn trim(self) -> str Remove leading and trailing whitespace.
fn toUpper(self) -> str Convert ASCII characters to uppercase.
fn toLower(self) -> str Convert ASCII characters to lowercase.
fn split(self, separator: str) -> List[str] Split into a list of substrings by separator.
fn replace(self, old: str, new: str) -> str Replace all occurrences of old with new.
fn repeat(self, count: uint) -> str Return the string repeated count times.
fn charAt(self, index: uint) -> Option[str] Return the grapheme cluster at index, or None if out of bounds. See below.
fn bytes(self) -> Iterator[byte] Return an iterator over the raw UTF-8 bytes of the string.
fn toBuffer(self) -> Buffer Return a Buffer containing the UTF-8 encoded bytes. Always succeeds.
fn fromBytes(b: Buffer) -> Result[str, str] Static method. Validate b as UTF-8 and return a str. Returns Err with a description if invalid.

No bracket access. Strings do not support bracket access (str[i]). Use charAt(i) or slice(i, i + 1) for single-character access by grapheme cluster index. Bracket access is intentionally omitted because grapheme cluster indexing is O(n), and the explicit method call makes this cost visible.

slice semantics: Indices are grapheme cluster positions (zero-based), consistent with charLength and split(""). start is inclusive, end is exclusive.

"hello".slice(0, 3)         # Some("hel")
"hello".slice(1, 4)         # Some("ell")
"hello".slice(3, 10)        # Some("lo")  — end clamped to 5
"hello".slice(0, 0)         # Some("")
"hello".slice(6, 8)         # None  — start > charLength
"❤️🎉".slice(0, 1)          # Some("❤️")  — grapheme cluster semantics
"".slice(0, 5)              # Some("")
"".slice(1, 5)              # None

Case conversion: toUpper and toLower affect only ASCII characters (code points 0x00-0x7F). Non-ASCII characters are passed through unchanged. For Unicode case conversion (e.g., Turkish İ/i, German ß), use a third-party library.

String splitting: split(separator: str) divides the string into substrings by searching for the separator. Edge case behavior:

"hello".byteLength()        # 5
"hello".charLength()        # 5
"❤️".byteLength()           # 6 (multi-byte emoji)
"❤️".charLength()           # 1 (one grapheme cluster)
"hello".contains("ell")     # true
"hello".startsWith("he")   # true
"hello world".split(" ")    # ["hello", "world"]
"hello".split("")           # ["h", "e", "l", "l", "o"]
"❤️🎉".split("")            # ["❤️", "🎉"]
"hello".split("x")          # ["hello"]
"  hi  ".trim()             # "hi"
"abc".repeat(3u)            # "abcabcabc"
"hello".replace("l", "r")   # "herro"
"ab".replace("", "-")       # "-a-b-"
"".replace("", "-")         # "-"

String replacement: replace(old: str, new: str) replaces all non-overlapping occurrences of old with new. If old is the empty string "", new is inserted at each grapheme cluster boundary: before the first character, between characters, and after the last character. For a string with N grapheme clusters, this produces N+1 insertions. For the empty string (0 grapheme clusters), new is inserted once.

Character access: charAt(index) returns the grapheme cluster at the given zero-based index as a single-character string, or None if the index is out of bounds. Like slice, indexing is by grapheme cluster position and is O(n). charAt(i) is equivalent to slice(i, i + 1) but avoids the range ceremony.

"hello".charAt(0u)          # Some("h")
"hello".charAt(4u)          # Some("o")
"hello".charAt(5u)          # None
"❤️🎉".charAt(0u)           # Some("❤️")
"❤️🎉".charAt(1u)           # Some("🎉")

Byte-level access: bytes() returns an Iterator[byte] over the raw UTF-8 encoded bytes of the string. Each step is O(1). This is the efficient way to process string contents byte-by-byte — for example, lexing ASCII-heavy source code. The iterator yields bytes in order from the first to the last.

"Hi".bytes() |> toList      # [72b, 105b]
"❤️".bytes() |> toList      # [0xE2b, 0x9Db, 0xA4b, 0xEFb, 0xB8b, 0x8Fb]  — 6 UTF-8 bytes

Buffer conversion: toBuffer() returns a Buffer containing the raw UTF-8 bytes of the string. Always succeeds because strings are always valid UTF-8. str.fromBytes(b) is the inverse — it validates the buffer contents as UTF-8 and returns the string, or Err if the bytes are not valid UTF-8.

buf = "Hello".toBuffer()        # Buffer containing [72, 101, 108, 108, 111]
buf.length()                     # 5u
buf.toStr()                      # Ok("Hello")  — via Buffer.toStr()

str.fromBytes(buf)               # Ok("Hello")  — via str.fromBytes()
str.fromBytes(Buffer.from([0xFFb]))  # Err("invalid UTF-8 at byte 0")

str.fromBytes(b) and buf.toStr() perform the same validation and produce the same result. Both are provided for ergonomic use — fromBytes reads naturally when starting from a Buffer, while toStr reads naturally when chaining methods on a buffer.

Substring matching and grapheme clusters: All substring operations (contains, indexOf, startsWith, endsWith, split, replace) match only at grapheme cluster boundaries. A match is recognized only when the substring starts at the beginning of a grapheme cluster and ends at the end of a grapheme cluster. If a byte-level match would start or end inside a multi-codepoint grapheme cluster, it is not considered a match. For example, searching for the combining acute accent "\u0301" inside "e\u0301" (the grapheme cluster "é") returns no match, because the accent does not start at a cluster boundary.

Link copied to clipboard!