std.strings
str is the built-in counted UTF-8 shared view; &str is mutable and ^str owns its buffer. U+0000 is ordinary text and no role promises a terminator. Shared-view operations (iter, slice_bytes, split, lines, repeat, and clone) and the size field are available directly from text. The owner additionally exposes readonly capacity and consuming append, reserve, and clear. std.strings supplies opaque RuneCursor, SplitCursor, and LinesCursor types plus free functions without a single receiver; today the sole free function is join.
Contract
- A hole-free literal such as
"text"is a static sharedstr; expected type never turns it into an owner. Any hole-free owning literal whose decoded content is statically empty is allocation-free and infallible. Every statically nonempty owning literal is written explicitly astry ^"text", and interpolation is accepted only in the owning form, for exampletry ^"name={name}". Nonempty construction and interpolation reportAllocError; there is no hidden stack or contextual allocation. ^stris a move value.clone,repeat, andjoinbuild exact-capacity owners; a nonempty result performs one allocation and reportsAllocError. Every empty result is the canonical allocation-free owner, although the general function signatures remain checked and are still called withtry.appendandreserveconsume an owning receiver and return its successor. Taking a named receiver is always explicit:(<-s).append(rest)or(<-s).reserve(total). They reuse the allocation when its logical capacity is sufficient and otherwise perform at most one growth call.clearis the corresponding infallible consuming transform and retains the allocation.itercreates an opaque affineRuneCursorwithout allocating or scanning. It yieldsrunevalues and advances by decoded UTF-8 byte width until the stored byte end; it never treats U+0000 as exhaustion. This is the sole direct scalar traversal and the path used byfor scalar in text.slice_bytes(start, size)returns a zero-copy source-regionstr?. It returnsnullwhen the range is out of bounds or either endpoint splits a UTF-8 scalar. There is no integer subscript, ordinal-scalar index, or grapheme index.splitbreaks the receiver at each non-overlapping occurrence of the separator. It returns an opaque affineSplitCursorwithout allocating or throwing; eachnextlends astrview into the original source. An empty or too-long separator yields the complete source exactly once. Otherwisennon-overlapping left-to-right matches yieldn + 1pieces, so a leading, trailing, or doubled separator produces an empty view. Splitting an empty source also yields one empty view.linessplits on\n, treating an immediately preceding\ras part of the terminator. It likewise returns an allocation-free, infallible opaque affineLinesCursorthat lends source views. Exactly one immediate preceding CR is absorbed; a lone CR remains content. A trailing terminator adds no final view, an empty receiver yields none, and interior blank lines are preserved. Thus"\n"yields one empty view,"\n\n"yields two, and"a\r\r\n"yields"a\r".join(parts, separator)concatenates the pieces with the separator between consecutive elements.partsis an explicit borrowed[^str]slice and is left intact. A cursor is not a slice, sojoin(s.split(sep), sep)is rejected; materialize each lent piece explicitly when an owning collection is wanted.strhas no raw-byte, scalar-position, or grapheme-positionpush/pop/insert/remove/replacefamily. Build a distinct owner withreserveandappend(str | rune)so source loans and byte offsets cannot be invalidated by hidden in-place editing. Convert explicitly to an owning byte container only when the result is binary data.- RFC 0064 reserves public text
appendfor concatenating one completestrorrunesuffix. Text does not gain sequencepush, collectionextend, or mutation aliases such asdelete,erase, ortake. sizeis the byte-extent field on every text role.capacityis a readonly owner-only field. Empty testing has one spelling,text.size == 0; there is nois_emptymethod or compatibility alias.
Quick Example
split lends source views one at a time. Cursor construction and traversal are infallible and allocation-free:
// RFC 0050 target; cursor implementation pending.
import std.strings.{ SplitCursor };
fn main() {
let parts: ^SplitCursor in _ = ("a,b,c").split(",");
assert parts.next() == "a";
assert parts.next() == "b";
assert parts.next() == "c";
assert parts.next() == null;
assert parts.next() == null; // sticky exhaustion
}Materialization names every allocation. Clone each lent piece, grow the owning slice explicitly, then call join:
// RFC 0050 target; cursor implementation pending.
import std.strings.{ join };
import std.mem.{ AllocError };
fn round_trip(text: str, separator: str) throws(AllocError) -> ^str {
var parts: ^[^str] = ^[];
for part: str in _ in text.split(separator) {
let copy: ^str = try part.clone();
parts = try (<-parts).push(<-copy);
}
return try join(parts, separator);
}Lines And Repeat
lines handles LF and CRLF terminators uniformly, dropping the terminator bytes from each line. It produces a cursor rather than an owning collection. repeat builds an exact-capacity owner of count back-to-back copies; a zero-size result normalizes to the canonical empty owner without allocating.
// RFC 0050 target; cursor implementation pending.
import std.mem.{ AllocError };
import std.strings.{ LinesCursor };
fn main() throws(AllocError) {
let text = "first\r\nsecond\nthird";
let rows: ^LinesCursor in _ = text.lines();
assert rows.next() == "first";
assert rows.next() == "second";
assert rows.next() == "third";
assert rows.next() == null;
let bar = try ("ab").repeat(3);
assert bar == "ababab";
assert bar.size == 6 as usize;
}Building And Growing
clone makes an independent exact-capacity copy of borrowed text. Owner growth is a consuming transformation because borrowing a ^str as &str exposes only the counted text range, not the owner's {ptr, size, capacity} descriptor. reserve(total) requests a total logical capacity, never an additional amount; append reuses that capacity; and clear sets size to zero while retaining the allocation.
// RFC 0051 target; consuming owner-growth implementation pending.
import std.mem.{ AllocError };
fn main() throws(AllocError) {
var greeting = try ("Hello").clone();
greeting = try (<-greeting).reserve(32);
greeting = try (<-greeting).append(", world");
assert greeting == "Hello, world";
assert greeting.size == 12 as usize;
assert greeting.capacity >= 32 as usize;
let blank = try ("").clone();
assert blank.size == 0 as usize;
assert blank.capacity == 0 as usize;
greeting = (<-greeting).clear();
assert greeting.size == 0 as usize;
assert greeting.capacity >= 32 as usize;
}Call a method on a bare string literal through parentheses — ("Hello").clone(), not "Hello".clone() — because the unparenthesized form does not parse.
Ownership, Allocation, And Errors
An owning nonempty ^str owns its heap buffer and drops it deterministically; the canonical empty owner allocates nothing. Its logical capacity counts UTF-8 bytes. For example, try ^"text" has size == 4 and capacity == 4. Every role guarantees exactly its size readable bytes and makes no claim about ptr[size]; only the owner exposes capacity. The logical capacity range is not reduced to reserve a physical terminator slot.
reserve(total) is an exact no-op when total <= capacity, including zero. After success, capacity >= total; the rounding strategy and resulting spare capacity are deliberately unspecified. It never shrinks. append first checks size + suffix.size without wrapping. An empty suffix returns the exact input owner. Sufficient spare capacity preserves the allocation and capacity; otherwise the operation makes at most one allocator growth call. Both operations publish only the exact counted range. clear performs no allocation or deallocation, preserves pointer and capacity, and sets size to zero. It does not securely erase bytes outside the new logical range.
For one successful owner lineage, a sequence of appends has amortized-linear total growth work: writing appended bytes, bytes copied because storage grows, and the aggregate storage requested by growth calls are O(initial_size + total_appended_size). Producing the suffix values and any work the caller performs to read them are outside this bound. The implementation must satisfy the bound but exposes no fixed growth factor, minimum capacity, capacity sequence, or allocation addresses.
The consuming receiver is evaluated before explicit arguments. Taking it is incompatible with every outstanding view derived from the same owner, and the taken binding is already moved before the suffix is evaluated. Consequently safe code cannot use the receiver or one of its subviews as its own suffix; no runtime self-alias exception exists.
If evaluating the suffix itself fails, the receiver has already become an owned argument temporary. Cleanup drops that temporary exactly once and the source binding remains moved. Bind a fallible suffix first when it must succeed before the receiver is taken.
If append or reserve reports AllocError, its already-taken owner is dropped exactly once and the caller's binding remains moved. This is intentionally different from ordinary non-consuming replacement, whose failed right-hand side leaves the old value installed. There is no recovery payload, rollback, hidden clone, or owner-preserving overload. A caller that needs an independently recoverable value must create that owner explicitly before the consuming call.
SplitCursor<region Life> and LinesCursor<region Source> are package-created, opaque, affine, non-Copy, non-Clone, and next-only. They own their cursor state but allocate no heap storage and do not own the viewed text. Split uses one common Life region for both source and separator:
split<region Life>(self in Life, separator: str in Life) -> ^SplitCursor in Life;
SplitCursor.next<region Step>(&self in Step) -> str? in Life;
lines<region Source>(self in Source) -> ^LinesCursor in Source;
LinesCursor.next<region Step>(&self in Step) -> str? in Source;The source and, for split, separator must therefore outlive the cursor and all yielded views. Constructing a split cursor evaluates the receiver once and then the separator once, left to right; constructing either cursor is O(1), performs no scan, allocation, copy, or user call, and cannot throw. Each successful next scans only as far as its next result, commits the new cursor position before publishing that result, and returns a view into the source. Normal exhaustion is sticky and every later next is effect-free. There is no cursor clone, reset, size hint, indexing, collection terminal, or implicit materialization. Dropping a cursor does not scan or drain the remainder; it only discards cursor state. Already yielded views remain valid after cursor drop for their declared Life or Source region. A named cursor can resume after break from a loop. join returns an exact-capacity owner for nonempty output and the canonical empty owner otherwise. The borrowing operations — the size field and reading a [str] element through equality — do not transfer ownership. Interpolation is not a borrowing operation: its result is a fresh owning ^str.
Send and Sync derive only from the cursor's stored loans and scalar state; neither cursor receives a blanket capability. Moving a borrowed cursor to a child requires a scope whose mandatory join ends before its source loans, and next(&self) still requires exclusive access to the single cursor position, so Sync never permits concurrent advancement.
Every potentially allocating operation reports overflow or out-of-memory as AllocError.OutOfMemory and is called with try. clone, repeat, and join size once up front; a failed pure construction leaves every borrowed input unchanged and publishes no partial owner. Their empty output performs no allocation but retains the checked effect of the general signature. split, lines, their next methods, clear, size, and reading owner capacity never allocate or throw.
Nonempty literals, interpolation, clone, repeat, and join produce exact capacity. Spare capacity arises only through explicit reserve or growth by append. There is no reserve_exact, shrink operation, builder, concatenation operator, byte/rune/grapheme-position mutator, or formatting writer in this surface. The two append overloads accept borrowed str and scalar rune.
Related Modules
Use std.slice when explicitly materializing the owning ^[^str] that join consumes. Bind an explicit comparator that delegates to ordering.compare_str, then pass it by & to the sole slice.sort or slice.binary_search operation. Use std.ordering for that lexicographic comparator and std.mem for the AllocError thrown by allocating string operations.
API Reference
str Methods
Shared methods operate on the built-in text view. Owner methods select only a consuming ^self receiver. A named receiver must therefore be visibly taken, for example s = try (<-s).append(rest).
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
iter | iter<region Source>(self in Source) -> ^RuneCursor in Source | None | allocation-free cursor | Traverses the counted source as Unicode scalar rune values; this is the direct for path. |
slice_bytes | slice_bytes<region Source>(self in Source, start: usize, size: usize) -> str? in Source | start, size: explicit byte range | region-tied nullable view | Lends a zero-copy text range, or null for bounds or UTF-8-boundary failure. |
split | split<region Life>(self in Life, separator: str in Life) -> ^SplitCursor in Life | separator: delimiter text in the common source lifetime | allocation-free cursor | Breaks the receiver at non-overlapping left-to-right separators and lends each source view. Empty/too-long separator yields the whole source once. |
lines | lines<region Source>(self in Source) -> ^LinesCursor in Source | None | allocation-free cursor | Lends LF-delimited source views, absorbs one immediate CR, preserves interior blank lines, and adds no view after a trailing terminator. |
repeat | repeat(count: usize) throws(AllocError) -> ^str | count: number of copies | ^str throws(AllocError) | Builds a fresh string of count back-to-back copies; count == 0 yields an empty string. |
clone | clone() throws(AllocError) -> ^str | None | ^str throws(AllocError) | Copies borrowed text into a new owning string. |
append | append(^self, suffix: str) throws(AllocError) -> ^str | suffix: distinct borrowed text to append | ^str throws(AllocError) | Consumes the owner, reuses sufficient capacity or grows at most once, and returns its successor. Empty suffix is an exact no-op. |
append | append(^self, suffix: rune) throws(AllocError) -> ^str | suffix: one Unicode scalar, including U+0000 | ^str throws(AllocError) | Encodes and appends one proven scalar while preserving counted UTF-8. |
reserve | reserve(^self, total: usize) throws(AllocError) -> ^str | total: requested total logical content capacity | ^str throws(AllocError) | Consumes the owner and returns one with capacity >= total; an already-satisfied request is an exact no-op. |
clear | clear(^self) -> ^str | None | ^str | Consumes the owner, retains its allocation and capacity, and returns it with size == 0. |
size is a field, not a method: s.size reads the byte extent as a usize without consuming or allocating. capacity is likewise a readonly field, but only on the owning ^str role. Projecting an owner to str or &str forgets that field. Test emptiness with s.size == 0.
Functions
| Function | Signature | Returns | Description |
|---|---|---|---|
join | join(parts: [^str], separator: str) throws(AllocError) -> ^str | ^str throws(AllocError) | Concatenates an explicitly materialized owner slice with separator; cursors are not accepted. |
Cursor Types
| Type | Contract |
|---|---|
RuneCursor<region Source> | Opaque affine cursor borrowing its source and byte end; sole operation next<region Step>(&self in Step) -> rune?, with sticky exhaustion. |
SplitCursor<region Life> | Opaque affine cursor borrowing source and separator in one Life; sole operation next<region Step>(&self in Step) -> str? in Life. |
LinesCursor<region Source> | Opaque affine cursor borrowing its source; sole operation next<region Step>(&self in Step) -> str? in Source. |
Status
^str is the owning-string type, with invariant-preserving per-receiver operations exposed as methods; std.strings holds the free functions that lack a single receiver. RFC 0049 makes ownership explicit at literal sites and removes contextual/interpolated hidden allocation. RFC 0050 makes bare text a strictly counted, not necessarily terminated view and replaces allocating split/lines collections with opaque borrowed cursors. RFC 0051 selects one consuming capacity-growth path, canonicalizes pure empty results, and removes the redundant is_empty. RFC 0046 removes the checked-in raw-byte mutators and TakenByte from the accepted target. RFC 0061 removes every text-level terminator and no-NUL promise, makes ordinary iteration yield rune, adds checked byte-bound views and scalar append, and keeps grapheme segmentation explicit. RFC 0064 gives the removed byte push, pop, insert, and remove spellings no aliases and fixes append as the text-suffix verb. For the full surface inventory, see Standard Library Inventory.