We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Collections, Text and First Iterators step 6 of 21
Zero-copy parsing: borrow, don't allocate
Parse an INI-style config into (section, key, value) triples — where all
three are borrowed slices of the input, not new Strings.
pub fn parse_ini(src: &str) -> Vec<(&str, &str, &str)>
The rules:
-
[name]on a line by itself starts a section. The name is trimmed. -
key = valueproduces a triple with the current section. Key and value are trimmed; only the first=splits, sourl = http://x?a=bkeeps its second=in the value. -
Lines starting with
#or;are comments. Blank lines are skipped. A line with no=and no brackets is ignored. -
Pairs appearing before any section header get the section
"".
The hidden case parses a 10 000-line config and asserts at most 20 heap
allocations for the whole parse. That budget covers the output Vec growing
by doubling and nothing else. One to_string() per field would be 27 000.
The signature is the whole idea
Look at the return type again: Vec<(&str, &str, &str)>. Not String.
Those &strs point into src — they are a pointer and a length, sixteen
bytes, no ownership, no copy. Rust lets you hand out interior views of data
you do not own and still guarantees at compile time that nobody keeps one
past the lifetime of the thing it points into.
Where does the lifetime come from? Elision. The function has exactly one input reference, so every elided output lifetime is tied to it. Written out in full, the signature is:
pub fn parse_ini<'a>(src: &'a str) -> Vec<(&'a str, &'a str, &'a str)>
Try to write a version with no src parameter at all and you get E0106,
missing lifetime specifier — there is nothing for the output to borrow from,
and the compiler asks you where the data is supposed to come from.
The error the starter gives you
The starter’s algorithm is already correct. Exactly one decision in it is
wrong: it stores the current section in an owned String. The compiler
produces two errors from that one mistake:
error[E0506]: cannot assign to `section` because it is borrowed
error[E0515]: cannot return value referencing local variable `section`
E0515 is the important one, and it is worth reading slowly. section is a
String owned by the function. Its buffer is freed when the function
returns. Pushing section.as_str() into out puts a pointer to that buffer
into the return value — so the moment the caller looks at the result, it is
reading freed memory. In C this is the classic returning-a-pointer-to-a-local
bug and it compiles fine. Here it is a compile error with your name on it.
E0506 falls out of the same thing: once out holds a borrow of section,
you are no longer allowed to reassign section, because that would drop the
buffer the borrow points at.
The fix is not a clone and not a lifetime annotation. It is to stop owning
the section at all — src.lines() yields &str slices that live as long as
src, and a section name is a slice of one of those lines. Change
let mut section = String::new() to let mut section = "" and the two
errors and the 10 000 allocations all disappear at once.
::: question If the starter had written out.push((section.clone(), ...)) with an owned String in the tuple, would it compile?
You would have to change the return type to Vec<(String, &str, &str)>
first, and then yes, it compiles and it is correct. That is the escape hatch
people take, and it is why this problem measures allocations: the borrow
checker will not stop you from writing the slow version, it only stops you
from writing the unsound one.
Cloning is the right answer when the parsed data must outlive the input — if
you are going to drop(src) and keep the config, you need owned strings.
The decision is about lifetime, not about performance, and you should be able
to say which one applies before you type either version.
:::
Three std methods that do most of the work
-
split_once(pat)returnsOption<(&str, &str)>— everything before the first match and everything after. This replaces thesplitn(2, '=')two-step dance, it is clearer, and a surprising number of Rust programmers have never noticed it exists. clippy hasmanual_split_oncefor the old spelling. -
strip_prefix/strip_suffixreturnOption<&str>with the affix removed, so[section]is one chained call instead ofstarts_withplus slicing plus an off-by-one. -
trimreturns a sub-slice. It allocates nothing; it just moves the pointer forward and shortens the length.
The measurement that kills a reflex
From a 300 000-field CSV benchmark, three ways to parse the same input:
split(',').map(to_string).collect::<Vec<String>>() then parse 6.07 ms
split(',').filter_map(|s| s.parse().ok()) 2.01 ms 3x
a hand-written byte-scanner state machine 2.70 ms
The middle row is the idiomatic iterator version. The bottom row is the version written by someone who “knows iterators are abstraction overhead” and hand-rolled a scanner — and it is slower. The iterator chain compiles down to a tight loop with bounds checks elided; the hand-written machine has a match on state that the optimiser has a harder time with.
Keep that number. The reflex that manual equals fast is wrong often enough that you should measure before believing it, and the idiomatic version is also the one you can still read next year.
One related fact: str::split reports size_hint == (0, None), so collecting
it never pre-sizes the destination. If you are collecting a split of something
huge, with_capacity genuinely helps there — unlike collecting a slice
iterator, which already knows its length.
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.