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)); split and replace build on it, so the whole search surface is linear. needle is valid UTF-8, and it can only match at a char boundary; a probe never straddles a codepoint.
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")
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). Every stdlib renderer builds a List<string> and fuses it here, and Display, replace and the JSON and HTTP renderers are all linear.
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 "
trim_end
def trim_end(self) -> string
self without trailing ASCII whitespace (space, tab, LF, CR).
"hi ".trim_end() => "hi"
" hi".trim_end() => " hi"
trim
def trim(self) -> string
self without leading or trailing ASCII whitespace.
" 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.
"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 and is therefore O(n·len) and not than 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. 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.
"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