module strings
module lib/strings.mu
import "strings"
strings provides a collection of string manipulation functions.
Imports
Values
whitespace_chars#
whitespace_chars is the whitespace table trim and split use, filled on first use by ensure_whitespace_chars. It is a list of single characters, so a multi-byte entry such as U+00A0 is one element rather than two bytes.
Source lib/strings.mu:8
whitespace_chars := []Functions
contains#
contains checks whether segment appears in text.
Source lib/strings.mu:242
fn contains(text, segment) {
found := index_of(text, segment)
if is_error(found) {
return found
}
return found >= 0
}ends_with#
ends_with reports whether text concludes with suffix.
Source lib/strings.mu:271
fn ends_with(text, suffix) {
if !typing.is_str(text) {
return error("strings.ends_with expects string text, got " + type(text))
}
if !typing.is_str(suffix) {
return error("strings.ends_with expects string suffix, got " + type(suffix))
}
value := text
needle := suffix
needle_len := len(needle)
if needle_len == 0 {
return true
}
total := len(value)
if needle_len > total {
return false
}
return ends_with_at(value, needle, total)
}ends_with_at#
ends_with_at checks if suffix ends at end_index within text.
Source lib/strings.mu:79
fn ends_with_at(text, suffix, end_index) {
suf_len := len(suffix)
if suf_len == 0 {
return false
}
if end_index < suf_len {
return false
}
start := end_index - suf_len
i := 0
while i < suf_len {
if text[start+i] != suffix[i] {
return false
}
i = i + 1
}
return true
}ensure_whitespace_chars#
ensure_whitespace_chars lazily populates whitespace_chars before any checks run.
Source lib/strings.mu:11
fn ensure_whitespace_chars() {
if len(whitespace_chars) != 0 {
return
}
append(whitespace_chars, chr(9))
append(whitespace_chars, chr(10))
append(whitespace_chars, chr(11))
append(whitespace_chars, chr(12))
append(whitespace_chars, chr(13))
append(whitespace_chars, " ")
// Non-ASCII whitespace is appended as real UTF-8 via chr(): mu has no \u
// escape, so "\u00a0" was never U+00A0 — it was the literal text u00a0,
// which is_whitespace could never match against a single character.
append(whitespace_chars, chr(0x0085))
append(whitespace_chars, chr(0x00a0))
append(whitespace_chars, chr(0x1680))
append(whitespace_chars, chr(0x180e))
append(whitespace_chars, chr(0x2000))
append(whitespace_chars, chr(0x2001))
append(whitespace_chars, chr(0x2002))
append(whitespace_chars, chr(0x2003))
append(whitespace_chars, chr(0x2004))
append(whitespace_chars, chr(0x2005))
append(whitespace_chars, chr(0x2006))
append(whitespace_chars, chr(0x2007))
append(whitespace_chars, chr(0x2008))
append(whitespace_chars, chr(0x2009))
append(whitespace_chars, chr(0x200a))
append(whitespace_chars, chr(0x2028))
append(whitespace_chars, chr(0x2029))
append(whitespace_chars, chr(0x202f))
append(whitespace_chars, chr(0x205f))
append(whitespace_chars, chr(0x3000))
}find_newline#
find_newline returns [index, width] for the first line ending in text, where width is 1 for a bare LF or CR and 2 for CRLF. It answers [-1, 0] when there is no complete line ending -- including a trailing CR, which cannot be classified until the next byte arrives, so an incremental reader must keep it and ask again.
Source lib/strings.mu:139
fn find_newline(text) {
i := 0
total := len(text)
while i < total {
ch := text[i]
if ch == "\n" {
if i > 0 && text[i-1] == "\r" {
return [i-1, 2]
}
return [i, 1]
}
if ch == "\r" {
if i == total-1 {
return [-1, 0]
}
if text[i+1] == "\n" {
return [i, 2]
}
return [i, 1]
}
i = i + 1
}
return [-1, 0]
}index_of#
index_of returns the byte offset of the first occurrence of segment in text, or -1 when it does not occur. An empty segment is found at 0.
Source lib/strings.mu:198
fn index_of(text, segment) {
if !typing.is_str(text) {
return error("strings.index_of expects string text, got " + type(text))
}
if !typing.is_str(segment) {
return error("strings.index_of expects string segment, got " + type(segment))
}
if len(segment) == 0 {
return 0
}
limit := len(text) - len(segment)
i := 0
while i <= limit {
if matches_at(text, segment, i) {
return i
}
i = i + 1
}
return -1
}is_alnum#
is_alnum reports whether ch is a single ASCII letter or digit.
Source lib/strings.mu:488
fn is_alnum(ch) {
return is_alpha(ch) || is_digit(ch)
}is_alpha#
is_alpha reports whether ch is a single ASCII letter.
Source lib/strings.mu:483
fn is_alpha(ch) {
return _is_ascii_class(ch, "a", "z") || _is_ascii_class(ch, "A", "Z")
}is_digit#
is_digit reports whether ch is a single ASCII decimal digit.
Source lib/strings.mu:478
fn is_digit(ch) {
return _is_ascii_class(ch, "0", "9")
}is_whitespace#
is_whitespace checks whether ch is recognized as a whitespace rune. ch is a whole character: a single byte for ASCII, or a complete multi-byte UTF-8 sequence (as rune_at yields). A lone continuation byte matches nothing.
Source lib/strings.mu:344
fn is_whitespace(ch) {
ensure_whitespace_chars()
i := 0
while i < len(whitespace_chars) {
if whitespace_chars[i] == ch {
return true
}
i = i + 1
}
return false
}join#
join concatenates string parts using sep, enforcing string arguments.
It joins by halves rather than by accumulating left to right. Strings are immutable, so out = out + part copies everything joined so far on every step -- quadratic in the total length, which is ruinous for the thousands of chunks a whole-file read produces. Halving copies each byte log2(n) times instead.
Source lib/strings.mu:105
fn join(parts, sep) {
if !typing.is_list(parts) {
return error("strings.join expects list parts, got " + type(parts))
}
if !typing.is_str(sep) {
return error("strings.join expects string sep, got " + type(sep))
}
i := 0
while i < len(parts) {
if !typing.is_str(parts[i]) {
return error("strings.join expects all list elements to be strings, got " + type(parts[i]))
}
i = i + 1
}
return _join_range(parts, sep, 0, len(parts))
}last_index_of#
last_index_of returns the byte offset of the LAST occurrence of segment in text, or -1 when it does not occur.
Source lib/strings.mu:221
fn last_index_of(text, segment) {
if !typing.is_str(text) {
return error("strings.last_index_of expects string text, got " + type(text))
}
if !typing.is_str(segment) {
return error("strings.last_index_of expects string segment, got " + type(segment))
}
if len(segment) == 0 {
return len(text)
}
i := len(text) - len(segment)
while i >= 0 {
if matches_at(text, segment, i) {
return i
}
i = i - 1
}
return -1
}lower#
lower returns text with every ASCII letter folded to lower case.
ASCII only, deliberately. mu strings are byte-indexed UTF-8, and case folding the rest of Unicode needs tables that do not belong in a module this size -- and would still be wrong for the locale-sensitive cases. Bytes outside A-Z are passed through untouched, so multi-byte sequences survive.
Source lib/strings.mu:448
fn lower(text) {
return _map_ascii_case(text, "strings.lower", 65, 90, 32)
}matches_at#
matches_at reports whether segment matches text starting exactly at index.
Source lib/strings.mu:60
fn matches_at(text, segment, index) {
seg_len := len(segment)
if seg_len == 0 {
return false
}
if index+seg_len > len(text) {
return false
}
i := 0
while i < seg_len {
if text[index+i] != segment[i] {
return false
}
i = i + 1
}
return true
}pad_left#
pad_left returns text padded on the LEFT with pad until it is width long, so the text ends up right-aligned. Text already that long is returned as it is -- padding never truncates.
Source lib/strings.mu:507
fn pad_left(text, width, pad) {
return _pad(text, width, pad, "strings.pad_left", true)
}pad_right#
pad_right returns text padded on the RIGHT with pad until it is width long, so the text ends up left-aligned.
Source lib/strings.mu:513
fn pad_right(text, width, pad) {
return _pad(text, width, pad, "strings.pad_right", false)
}repeat#
repeat returns text repeated count times (empty string when count<=0).
Source lib/strings.mu:292
fn repeat(text, count) {
if !typing.is_str(text) {
return error("strings.repeat expects string text, got " + type(text))
}
value := text
if count <= 0 {
return ""
}
result := ""
i := 0
while i < count {
result = result + value
i = i + 1
}
return result
}replace#
replace substitutes every occurrence of old in text with new.
Source lib/strings.mu:310
fn replace(text, old, new) {
if !typing.is_str(text) {
return error("strings.replace expects string text, got " + type(text))
}
if !typing.is_str(old) {
return error("strings.replace expects string old, got " + type(old))
}
value := text
needle := old
if len(needle) == 0 {
return error("strings.replace old segment must not be empty")
}
if !typing.is_str(new) {
return error("strings.replace expects string new, got " + type(new))
}
result := ""
i := 0
total := len(value)
needle_len := len(needle)
while i < total {
if i+needle_len <= total && matches_at(value, needle, i) {
result = result + new
i = i + needle_len
continue
}
result = result + value[i]
i = i + 1
}
return result
} rune_at#
rune_at returns the whole character beginning at index i.
Source lib/strings.mu:396
fn rune_at(text, i) {
w := rune_width(text, i)
if i + w > len(text) {
w = 1
}
return substring(text, i, i + w)
}rune_start#
rune_start returns the index at which the UTF-8 sequence ending just before end begins, by walking back over continuation bytes (0x80-0xbf). It never steps back more than the three continuations a sequence can have, so malformed input still makes progress.
Source lib/strings.mu:381
fn rune_start(text, end) {
i := end - 1
steps := 0
while i > 0 && steps < 3 {
b := ord(text[i])
if b < 0x80 || b >= 0xc0 {
return i
}
i = i - 1
steps = steps + 1
}
return i
}rune_width#
rune_width returns the length in bytes of the UTF-8 sequence beginning at index i. Strings are byte-indexed, so scanning by rune means stepping by this width. A byte that is not a well-formed lead byte reports 1 so that callers always advance over malformed input instead of looping.
Source lib/strings.mu:360
fn rune_width(text, i) {
b := ord(text[i])
if b < 0x80 {
return 1
}
if b >= 0xf0 && b <= 0xf7 {
return 4
}
if b >= 0xe0 && b <= 0xef {
return 3
}
if b >= 0xc0 && b <= 0xdf {
return 2
}
return 1
}split#
split divides text by sep, returning all substrings (sep must not be empty).
Source lib/strings.mu:165
fn split(text, sep) {
if !typing.is_str(text) {
return error("strings.split expects string text, got " + type(text))
}
if !typing.is_str(sep) {
return error("strings.split expects string sep, got " + type(sep))
}
value := text
separator := sep
sep_len := len(separator)
if sep_len == 0 {
return error("strings.split expects a non-empty separator")
}
result := []
current := ""
i := 0
total := len(value)
while i < total {
if i+sep_len <= total && matches_at(value, separator, i) {
append(result, current)
current = ""
i = i + sep_len
continue
}
current = current + value[i]
i = i + 1
}
append(result, current)
return result
}starts_with#
starts_with reports whether text begins with prefix.
Source lib/strings.mu:251
fn starts_with(text, prefix) {
if !typing.is_str(text) {
return error("strings.starts_with expects string text, got " + type(text))
}
if !typing.is_str(prefix) {
return error("strings.starts_with expects string prefix, got " + type(prefix))
}
value := text
needle := prefix
needle_len := len(needle)
if needle_len == 0 {
return true
}
if needle_len > len(value) {
return false
}
return matches_at(value, needle, 0)
}substring#
substring returns text[start:end] without extra checks so callers can compose safely.
Source lib/strings.mu:49
fn substring(text, start, end) {
res := ""
i := start
while i < end {
res = res + text[i]
i = i + 1
}
return res
}trim#
trim removes leading and trailing whitespace runes. It advances a whole UTF-8 sequence at a time: trimming byte by byte could only ever match the ASCII entries in the whitespace table, and could split a multi-byte character in half.
Source lib/strings.mu:408
fn trim(text) {
if !typing.is_str(text) {
return error("strings.trim expects string text, got " + type(text))
}
value := text
length := len(value)
start := 0
end := length
while start < end {
w := rune_width(value, start)
if start + w > end {
w = 1
}
if !is_whitespace(substring(value, start, start + w)) {
break
}
start = start + w
}
while end > start {
s := rune_start(value, end)
if s < start {
s = start
}
if !is_whitespace(substring(value, s, end)) {
break
}
end = s
}
if start >= end {
return ""
}
return substring(value, start, end)
}upper#
upper returns text with every ASCII letter folded to upper case. See lower for why this stops at ASCII.
Source lib/strings.mu:454
fn upper(text) {
return _map_ascii_case(text, "strings.upper", 97, 122, -32)
}Internal helpers
Underscore-prefixed names are implementation detail. They are listed so the module's source reads without surprises, not as API — they may change at any time.
| _is_ascii_class(ch, low, high) | _is_ascii_class reports whether ch is exactly one byte within [low, high]. |
| _join_range(parts, sep, lo, hi) | _join_range joins parts[lo:hi], which the caller has already type-checked. |
| _map_ascii_case(text, caller, low, high, delta) | _map_ascii_case shifts every byte within [low, high] by delta. |
| _pad(text, width, pad, caller, on_left) | _pad builds the filler once and puts it on the requested side. |