Mutable Strings
This section describes the mutable string capabilities, building on the core String type from section 3.7.
Preview Feature: Mutable strings are a preview feature requiring
--preview mutable_strings. See ADR-0014 for the design.
String Representation
A String value consists of three components: a pointer to the string data, the length in bytes, and the allocated capacity.
When capacity is zero, the string data points to read-only memory (a string literal). When capacity is greater than zero, the string data is heap-allocated and can be mutated.
This representation allows string literals to remain cheap (no allocation) while enabling mutation when needed. Mutation methods automatically promote read-only strings to the heap.
String Ownership
String is an affine type: a String value is consumed when used and cannot be used again unless explicitly cloned.
String is not @copy. Passing a string to a function or assigning it to another binding moves the string.
fn takes_string(s: String) -> i32 { 0 }
fn main() -> i32 {
var s = "hello";
takes_string(s); // s is moved
// takes_string(s); // ERROR: use of moved value
0
}
Construction
String::new() returns an empty string with no allocation.
String::with_capacity(cap: u64) returns an empty string with pre-allocated capacity for cap bytes.
fn main() -> i32 {
let empty = String::new();
let prealloc = String::with_capacity(1024);
0
}
Query Methods
fn len(borrow self) -> u64 returns the length of the string in bytes.
fn capacity(borrow self) -> u64 returns the allocated capacity of the string. Returns zero for string literals.
fn is_empty(borrow self) -> bool returns true if the string length is zero.
Query methods use borrow self to access the string without consuming it, leaving the string valid after the call.
fn main() -> i32 {
let s = "hello";
if s.len() == 5 && !s.is_empty() {
0
} else {
1
}
}
Mutation Methods
fn push_str(inout self, other: String) appends the contents of other to the string. If the string is a literal (capacity zero), it is first promoted to the heap.
fn push(inout self, byte: u8) appends a single byte to the string.
fn clear(inout self) removes all content from the string but retains the allocated capacity.
fn reserve(inout self, additional: u64) ensures the string has capacity for at least additional more bytes.
Mutation methods use inout self to modify the string in place. The variable must be declared with var to allow mutation.
fn main() -> i32 {
var s = String::new();
s.push_str("hello");
s.push_str(" world");
s.push(33); // '!' character
0
}
Heap Promotion
When a mutation method is called on a string literal (capacity zero), the string is promoted to the heap:
- A heap buffer is allocated with capacity for the existing content plus the new content
- The existing content is copied from read-only memory to the heap buffer
- The string's pointer and capacity are updated
- The mutation is performed
Heap promotion is transparent to the user. There is no separate "owned" vs "borrowed" string distinction.
fn main() -> i32 {
var s = "hello"; // literal: capacity = 0
s.push_str("!"); // promotes to heap, then appends
// s is now "hello!" with capacity > 0
0
}
Growth Strategy
When appending would exceed the current capacity, a new buffer is allocated with double the current capacity (minimum 16 bytes). The existing content is copied and the old buffer is freed.
The doubling growth strategy amortizes allocation cost over many appends, providing O(1) amortized time per append.
Clone
fn clone(borrow self) -> String creates a deep copy of the string, allocating a new heap buffer with the same content.
Clone borrows self so the original string remains valid. Cloning always allocates, even for string literals.
fn main() -> i32 {
let a = "hello";
let b = a.clone(); // deep copy
// Both a and b are valid
0
}
Destructor
When a String value is dropped:
- If capacity is zero (literal), no action is taken
- If capacity is greater than zero (heap-allocated), the heap buffer is freed
The destructor automatically distinguishes between string literals and heap-allocated strings, ensuring correct cleanup.
fn main() -> i32 {
var s = "hello";
s.push_str("!"); // promotes to heap
0
} // destructor frees the heap buffer
Byte String Semantics
Rue strings are conventionally UTF-8 rather than strictly validated:
- String literals are valid UTF-8 (validated at compile time)
- At runtime, strings are byte sequences
- Methods like
push_straccept any bytes - No runtime UTF-8 validation overhead
This approach matches Go's string and Rust's bstr crate: UTF-8 is the convention, but the type does not enforce it at runtime.