str
stdlib/str.hk: methods on the string type.
The primitive operations are @intrinsic: lowering emits Op::HostCall and the scheduler dispatches into the registered Rust closure. length and the byte-indexed startswith / contains / slice round out the surface; isempty and slice_from are derived methods. All offsets are byte offsets; Unicode-aware variants are deferred. There is no concat: text is assembled by interpolation, join, or a StringBuilder (HANKI.md §4).
Surface (method-style): s.length s.startswith(prefix) s.endswith(suffix) s.contains(needle) s.isempty s.slice(start, end) s.slicefrom(i) s.tobytes() s.find(needle) s.split(sep) sep.join(parts) s.trim() / s.trimstart() / s.trim_end() s.replace(from, to)
impl string { ... } produces inherent methods qualified as string.length, string.find, etc. The C-ABI symbols (consumed by AOT-emitted code via hanki_intrinsic_*) follow the same qualification; see intrinsic_c_abi_symbol in llvm_lower.
i32 and string conversion sits on the source type: Display<i32>::to_string and i32.parse (see display.hk, i32.hk).
impl string
length
prop length(self) -> int
The length in UTF-8 bytes (not code points): é is two bytes.
"abc".length => 3
"héllo".length => 6
starts_with?
def starts_with?(self, prefix: string) -> bool
True when self begins with prefix. The empty prefix always matches.
"hello".starts_with?("hel") => true
"hello".starts_with?("world") => false
contains?
def contains?(self, needle: string) -> bool
True when needle occurs as a contiguous byte substring of self. The empty needle always matches.
"hello".contains?("ell") => true
"hello".contains?("xyz") => false
"hello".contains?("") => true
toasciiuppercase
def to_ascii_uppercase(self) -> string
self with every ASCII letter a to z upper-cased; bytes outside that range (digits, punctuation, every non-ASCII byte) are left unchanged.
"Hi, World!".to_ascii_uppercase() => "HI, WORLD!"
"héllo".to_ascii_uppercase() => "HéLLO"
toasciilowercase
def to_ascii_lowercase(self) -> string
self with every ASCII letter A to Z lower-cased; bytes outside that range (digits, punctuation, every non-ASCII byte) are left unchanged.
"Hi, World!".to_ascii_lowercase() => "hi, world!"
"HÉLLO".to_ascii_lowercase() => "hÉllo"
empty?
prop empty?(self) -> bool
True when self is the empty string. s.empty? is s.length == 0.
"".empty? => true
"abc".empty? => false
slice
def slice(self, start: int, stop: int) -> string
The substring over the byte range [start, stop). Offsets are clamped to [0, length()]; a negative start clamps to 0, a stop past the string clamps to the length, and stop <= start yields "". A mid-codepoint endpoint rounds down to the nearest char boundary so the slice remains valid UTF-8. It is stop and never end, end being a keyword.
"hello".slice(0, 2) => "he"
"hello".slice(1, 100) => "ello"
"héllo".slice(0, 2) => "h"
slice_from
def slice_from(self, i: int) -> string
The substring from byte offset i to the end: self.slice(i, self.length). A negative i clamps to the whole string; past-the-end yields ""; a mid-codepoint i rounds down to the nearest char boundary.
"hello".slice_from(0) => "hello"
"hello".slice_from(100) => ""
"héllo".slice_from(3) => "llo"
to_bytes
def to_bytes(self) -> bytes
UTF-8 encode into a bytes buffer. Total, every string being valid UTF-8, and this is therefore the infallible counterpart of bytes.to_string.
"hi".to_bytes().length => 2
hash_u64
def hash_u64(self) -> u64
A 64-bit hash of the UTF-8 bytes. Backs Hash<string>; equal strings hash equal.
"hi".hash_u64() == "hi".hash_u64() => true
ends_with?
def ends_with?(self, suffix: string) -> bool
True when self ends with suffix. The empty suffix always matches.
"hello".ends_with?("llo") => true
"hello".ends_with?("hel") => false
strip_prefix
def strip_prefix(self, prefix: string) -> Option<string>
self with prefix removed from the front, or None when self does not start with prefix. The empty prefix strips nothing and gives Some(self).
"foobar".strip_prefix("foo") => Some("bar")
"foobar".strip_prefix("xyz") => None
"foobar".strip_prefix("") => Some("foobar")
strip_suffix
def strip_suffix(self, suffix: string) -> Option<string>
self with suffix removed from the end, or None when self does not end with suffix. The empty suffix strips nothing and gives Some(self).
"foobar".strip_suffix("bar") => Some("foo")
"foobar".strip_suffix("xyz") => None
"foobar".strip_suffix("") => Some("foobar")
find
def find(self, needle: string) -> Option<int>
The byte offset of the first occurrence of needle, or None when it does not occur. The empty needle matches at 0.
"hello".find("ll") => Some(2)
"hello".find("xyz") => None
"hello".find("") => Some(0)
A byte-scanning intrinsic (O(n+m)). A valid UTF-8 needle matches only at a character boundary.
split
def split(self, sep: string) -> List<string>
The pieces of self between occurrences of sep, in order. Adjacent separators yield empty elements, as do leading/trailing ones; a sep that never occurs (or an empty sep) yields self as the single element.
"a,b,c".split(",").length => 3
"a,,b".split(",").get(1) => Some("")
"abc".split(",").get(0) => Some("abc")
The scan and copied result bytes are linear in the input and separator lengths. Splitting is available at compile time and on both runtime tiers.
join
def join(self, parts: List<string>) -> string
The elements of parts joined with self between each pair: ", ".join(parts). It sits on the separator because List<string> cannot carry its own inherent impl next to the generic List<T> one.
parts: List<string> = List.empty().append("a").append("b")
", ".join(parts) => "a, b"
A byte-scanning intrinsic that fuses the pieces in a single pass: it sums the total length, allocates once, and copies each part and separator into place, O(total bytes), where an accumulator loop is O(n²) (a full-copy concat per piece). Producing the pieces contributes separately to the total cost of a renderer.
split_whitespace
def split_whitespace(self) -> List<string>
The words of self: the pieces between runs of ASCII whitespace (space, tab, LF, CR, the same set trim strips), with empty pieces already dropped. Runs collapse, and consecutive separators yield no empty elements; an all-whitespace or empty self gives the empty list.
"a b c".split_whitespace().length => 3
" a\tb\r\nc ".split_whitespace().get(1) => Some("b")
"one".split_whitespace().get(0) => Some("one")
" \t ".split_whitespace().length => 0
"".split_whitespace().length => 0
trim_start
def trim_start(self) -> string
self without leading ASCII whitespace (space, tab, LF, CR).
" hi".trim_start() => "hi"
"hi ".trim_start() => "hi "
The boundary scan is linear and the changed result is copied once.
trim_end
def trim_end(self) -> string
self without trailing ASCII whitespace (space, tab, LF, CR).
"hi ".trim_end() => "hi"
" hi".trim_end() => " hi"
The boundary scan is linear and the changed result is copied once.
trim
def trim(self) -> string
self without leading or trailing ASCII whitespace (space, tab, LF, CR).
" hi ".trim() => "hi"
"hi".trim() => "hi"
replace
def replace(self, from: string, to: string) -> string
self with every occurrence of from replaced by to, scanning left to right without revisiting replacements. An empty from returns self unchanged. Splitting scans the input once and joining copies the result once, linear in the input, pattern and output byte lengths.
"a,b,c".replace(",", "; ") => "a; b; c"
"aaa".replace("aa", "b") => "ba"
repeat
def repeat(self, n: int) -> string
self repeated n times (the empty string for n <= 0 or an empty self). It fuses n copies through join, which makes it O(n·len) and not the O(n²) of a growing-accumulator concat loop.
"ab".repeat(3) => "ababab"
"x".repeat(0) => ""
lines
def lines(self) -> List<string>
self split into lines: a \n ends a line, a \r immediately before it (a CRLF) is stripped, and a single trailing \n does not add a final empty line. A bare CR is preserved, including at the end of an unterminated line. The empty string is no lines. Pairs with io.read_line!.
"a\nb\nc".lines() => List.empty().append("a").append("b").append("c")
"a\r\nb\r\n".lines() => List.empty().append("a").append("b")
pad_start
def pad_start(self, width: int, fill: string) -> string
self left-padded with fill (repeated, then trimmed to length) until it is at least width bytes; unchanged when already wide enough or fill is empty. Byte-width, like the rest of string.
"42".pad_start(5, "0") => "00042"
"hello".pad_start(3, " ") => "hello"
pad_end
def pad_end(self, width: int, fill: string) -> string
self right-padded with fill (repeated, then trimmed to length) until it is at least width bytes; unchanged when already wide enough or fill is empty. Byte-width, like the rest of string.
"42".pad_end(5, ".") => "42..."
"hello".pad_end(3, " ") => "hello"
eqignoreascii_case?
def eq_ignore_ascii_case?(self, other: string) -> bool
true where self equals other ignoring ASCII case (A to Z against a to z alone; non-ASCII bytes must still match). The case-insensitive companion to ==.
"Hello".eq_ignore_ascii_case?("hELLO") => true
"abc".eq_ignore_ascii_case?("abd") => false