#259 Jul 2026

259. str::lines — Split Into Lines Without Dragging \r Along

split('\n') works fine — until a Windows-saved file hands you lines that all end in an invisible \r. lines() was built for exactly this.

The trap

Same story as bite 258: split takes your separator literally. A file saved on Windows uses \r\n line endings, so every “line” keeps a carriage return — and the trailing newline produces a bonus empty string:

1
2
3
4
5
6
7
let text = "alpha\r\nbeta\r\ngamma\r\n";

let naive: Vec<&str> = text.split('\n').collect();
assert_eq!(
    naive,
    ["alpha\r", "beta\r", "gamma\r", ""]
);

That stray \r is invisible in most debug output, so it surfaces as "gamma" != "gamma" mysteries: failed comparisons, HashMap misses, parse errors on the last field.

The fix

lines() splits on \n and strips a trailing \r if one is there — so the same code handles Unix and Windows files:

1
2
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines, ["alpha", "beta", "gamma"]);

No trailing empty string either — like split_terminator (bite 233), the final newline is treated as a terminator, not a separator.

One caveat

Only \r\n and \n count as line endings. A lone \r (classic Mac OS, some protocol payloads) does not split:

1
2
3
let old_mac = "alpha\rbeta";
let lines: Vec<&str> = old_mac.lines().collect();
assert_eq!(lines, ["alpha\rbeta"]);

And a \r in the middle of a line stays untouched — only one directly before the \n is stripped.

Reading a file? BufRead::lines() gives you the same semantics over owned Strings. Either way: for “give me the lines”, it’s lines() every time — save split('\n') for when you truly mean raw bytes-between-newlines.

#258 Jul 2026

258. split_whitespace — Split on Runs, Not on Every Single Space

split(' ') hands you empty strings for every doubled space — and silently ignores tabs. split_whitespace is what you actually meant.

The trap

User input is messy: leading spaces, double spaces, a stray tab. split(' ') takes all of that literally:

1
2
3
4
5
6
7
let line = "  alpha\tbeta   gamma ";

let naive: Vec<&str> = line.split(' ').collect();
assert_eq!(
    naive,
    ["", "", "alpha\tbeta", "", "", "gamma", ""]
);

Two bugs in one line: every consecutive-space pair produces an empty string, and "alpha\tbeta" sails through as a single “word” because a tab isn’t a space.

The fix

split_whitespace splits on runs of any whitespace and never yields empty strings:

1
2
let words: Vec<&str> = line.split_whitespace().collect();
assert_eq!(words, ["alpha", "beta", "gamma"]);

Leading and trailing whitespace disappear too — no trim() needed first.

Unicode-aware, with an ASCII fast path

“Whitespace” here means the Unicode White_Space property, so a non-breaking space (\u{00A0}) splits words just like a regular one:

1
2
3
let fancy = "alpha\u{00A0}beta";
let words: Vec<&str> = fancy.split_whitespace().collect();
assert_eq!(words, ["alpha", "beta"]);

If your input is guaranteed ASCII (log files, protocol lines), split_ascii_whitespace does the same thing with a cheaper per-byte check — same no-empty-strings guarantee:

1
2
3
let words: Vec<&str> =
    " 42  7\t9 ".split_ascii_whitespace().collect();
assert_eq!(words, ["42", "7", "9"]);

Keep split(' ') for formats where empty fields are meaningful (CSV-like, fixed positions). For “give me the words”, it’s split_whitespace every time.

#257 Jul 2026

257. VecDeque::rotate_left — Where the Ring Buffer Finally Pays Rent

slice::rotate_left touches every element, every time. The same call on a VecDeque moves at most half of them — often far fewer.

Rotation on a slice is O(len)

Bite 133 covered slice::rotate_left: in-place, no allocation, but every rotation is O(len) — all elements physically move through memory.

This morning’s bite 256 showed the cost of VecDeque’s ring buffer: no single slice to hand out. Rotation is where that layout pays you back.

The deque version

VecDeque has its own rotate_left / rotate_right, and the ring buffer turns rotation into pointer arithmetic plus a short move:

1
2
3
4
5
6
7
8
9
use std::collections::VecDeque;

let mut buf: VecDeque<i32> = (0..10).collect();

buf.rotate_left(3);
assert_eq!(buf, [3, 4, 5, 6, 7, 8, 9, 0, 1, 2]);

buf.rotate_right(3);
assert_eq!(buf, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);

It’s the loop you’d write by hand — pop one end, push the other — but done as a bulk move:

1
2
3
4
5
6
// rotate_left(3), spelled out:
for _ in 0..3 {
    let x = buf.pop_front().unwrap();
    buf.push_back(x);
}
assert_eq!(buf, [3, 4, 5, 6, 7, 8, 9, 0, 1, 2]);

Because elements may wrap around the allocation’s end, “moving” the front to the back mostly means shifting the head index. The documented bound is O(min(mid, len − mid)) time and no extra space — rotate_left(3) on a million-element deque moves 3 elements, not a million. The same call on a slice moves all of them.

Where it shines: round-robin

A scheduler that cycles through tasks is one rotate_left(1) per turn — O(1):

1
2
3
4
5
let mut tasks: VecDeque<&str> =
    ["fetch", "parse", "render"].into();

tasks.rotate_left(1);
assert_eq!(tasks, ["parse", "render", "fetch"]);

Both methods panic if mid > len(), so a full-cycle rotate_left(len) is legal (and a no-op) but len + 1 is not — use k % len if k can exceed the length.

If you’re rotating a Vec in a hot loop, that’s the signal to switch containers: VecDeque::from(vec) is O(1), and every rotation after that is the cheap kind.

#256 Jul 2026

256. VecDeque::make_contiguous — Turn a Wrapped Ring Buffer Into One Sortable Slice

VecDeque has no .sort(), and any API that wants &[T] rejects it. The catch is the ring buffer underneath — and one call flattens it.

Why a VecDeque isn’t a slice

A VecDeque is a ring buffer: pushes and pops at both ends are O(1) because the contents are allowed to wrap around the end of the allocation. After a few pops and pushes, the elements may sit in memory as two separate runs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::collections::VecDeque;

let mut buf: VecDeque<i32> = VecDeque::with_capacity(4);
buf.extend([3, 1, 4, 1]);
buf.pop_front();
buf.pop_front();
buf.push_back(5);
buf.push_back(9); // wraps around the buffer's end

let (front, back) = buf.as_slices();
assert_eq!(front, [4, 1]);
assert_eq!(back,  [5, 9]); // two pieces, not one

That’s why there’s no VecDeque::sort, and why you can’t pass one to anything expecting a &[T] — there is no single slice to hand out.

The built-in

make_contiguous rotates the elements back into one run and returns the whole thing as &mut [T]:

1
2
3
let slice = buf.make_contiguous();
slice.sort_unstable();
assert_eq!(slice, [1, 4, 5, 9]);

The deque itself is unchanged as a collection — same elements, same order you left them in — but now it’s backed by a single run:

1
2
3
let (front, back) = buf.as_slices();
assert_eq!(front, [1, 4, 5, 9]);
assert!(back.is_empty()); // one piece

Sorting was the classic motivation, but any slice-only API works after the call: windows, chunks, bite 247’s sort_unstable, or an FFI boundary that wants a pointer and a length.

What it costs

If the deque is already contiguous — which it always is fresh after new() or from(vec) — the call is free. Otherwise it moves elements within the existing buffer: no allocation, worst case O(n) moves. Do it once, then take slices as often as you like.

One nuance: binary_search and friends exist directly on VecDeque, so you don’t need this call just to search. Reach for make_contiguous when the API you’re feeding — or the mutation you want, like an in-place sort — demands one contiguous &mut [T].

#255 Jul 2026

255. rotate_left / rotate_right — Bit Rotation Without the Shift-Overflow Trap

Shifting throws bits away. The manual “wrap them around” idiom panics on n == 0. Rotation has been one method call the whole time.

The problem with shifts

A left shift pushes the top bits off the edge — they’re gone:

1
2
3
let x = 0b1000_0001_u8;

assert_eq!(x << 1, 0b0000_0010); // top bit lost

When you need the bits to wrap around — hashing, checksums, circular counters — the textbook idiom combines two shifts:

1
2
3
let n = 1;
let rotated = (x << n) | (x >> (8 - n));
assert_eq!(rotated, 0b0000_0011);

Which works right up until n == 0: then x >> 8 is a shift by the full bit width — a panic in debug builds, and a masked, silently-wrong result in release. A correct version needs masking both shift amounts, and now you’re writing a code comment again.

The built-in

Every integer type has rotate_left and rotate_right. Bits that fall off one end come back on the other:

1
2
3
4
let x = 0b1000_0001_u8;

assert_eq!(x.rotate_left(1),  0b0000_0011);
assert_eq!(x.rotate_right(1), 0b1100_0000);

No edge cases: the rotation amount is taken modulo the bit width, so n == 0, n == 8, even n == 1000 are all fine —

1
2
3
assert_eq!(x.rotate_left(0), x);
assert_eq!(x.rotate_left(8), x);           // full circle
assert_eq!(x.rotate_left(9), x.rotate_left(1));

— and like bite 254’s isolate_lowest_one, it compiles to a single instruction (ROL/ROR on x86) instead of the three ops the manual idiom costs.

Round trips for free

Rotation never destroys information, so it’s trivially reversible — handy for mixing bits in a hash and for tests:

1
2
3
let v = 0xDEAD_u16;

assert_eq!(v.rotate_left(5).rotate_right(5), v);

Note this rotates the bit pattern, not bytes: for endianness work you want swap_bytes or bite 240’s to_le_bytes. But when the task is “slide bits around a circle”, rotate_left says exactly that — with no (8 - n) waiting to panic.

#254 Jul 2026

254. isolate_lowest_one — The x & x.wrapping_neg() Hack Finally Has a Name

Every bitmask codebase has an unexplained x & x.wrapping_neg() in it somewhere. Rust 1.97 gives the trick a name — and a sibling for the other end.

The folklore version

To keep only the lowest set bit of an integer, the two’s-complement trick is to AND the value with its own negation:

1
2
3
4
let x = 0b0101_0100_u8;

// lowest set bit, the folklore way
assert_eq!(x & x.wrapping_neg(), 0b0000_0100);

It works, it compiles to one instruction (BLSI on x86) — and it explains nothing to the next reader. For the highest set bit there isn’t even a one-liner: you shift 1 by leading_zeros arithmetic and special-case zero.

Named, on every integer type

Rust 1.97 stabilizes isolate_lowest_one and isolate_highest_one. They return the isolated bit as a mask — the value with all other bits cleared:

1
2
3
4
5
6
7
8
let x = 0b0101_0100_u8;

assert_eq!(x.isolate_lowest_one(),  0b0000_0100);
assert_eq!(x.isolate_highest_one(), 0b0100_0000);

// zero just stays zero — no panic, no sentinel
assert_eq!(0_u8.isolate_lowest_one(),  0);
assert_eq!(0_u8.isolate_highest_one(), 0);

Where bite 250’s lowest_one / highest_one answer “at which position?” (as an Option), the isolate_ pair answers “which bit?” — same information, shaped for masking instead of indexing.

The pattern: walk the set bits

The mask shape is exactly what you want for iterating over flags — grab the lowest bit, handle it, XOR it away:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let mut mask = 0b0101_0100_u8;
let mut seen = vec![];

while mask != 0 {
    let bit = mask.isolate_lowest_one();
    seen.push(bit);   // handle one flag
    mask ^= bit;      // clear it
}

assert_eq!(seen, [0b0000_0100,
                  0b0001_0000,
                  0b0100_0000]);

No positions, no shifting back and forth — each iteration hands you a ready-to-use single-bit mask. Signed types work too ((-8_i8).isolate_lowest_one() == 8), since the methods operate on the raw bit pattern.

If your code review comments still include “this ANDs x with its negation to isolate the lowest set bit…”, Rust 1.97 lets the method name say it for you.

#253 Jul 2026

253. overflowing_add — Wrap, But Know It Happened

This morning’s Wrapping<T> (bite 252) wraps silently — but multiword arithmetic needs the carry bit too. overflowing_add returns both: the wrapped result and whether it wrapped.

The sum < a trick

The classic way to detect a carry after unsigned addition compares the result to an input:

1
2
3
4
5
fn add_with_carry(a: u64, b: u64) -> (u64, u64) {
    let sum = a.wrapping_add(b);
    let carry = (sum < a) as u64; // wrapped iff smaller
    (sum, carry)
}

It works — unsigned overflow means the sum came back smaller — but it’s a puzzle for the reader, it’s easy to compare against the wrong operand, and the signed version of the trick is different and wrong in edge cases.

overflowing_add says it directly

Every integer type has overflowing_add (and _sub, _mul, _neg, _shl…): it returns (wrapped_value, overflowed: bool) in one call:

1
2
assert_eq!(u64::MAX.overflowing_add(1), (0, true));
assert_eq!(1_u64.overflowing_add(1), (2, false));

That makes carry chains — the core of any 128-bit-or-wider addition — read like what they are:

1
2
3
4
5
6
7
8
// [lo, hi] little-endian u128 as two u64 limbs
fn add128(a: [u64; 2], b: [u64; 2]) -> [u64; 2] {
    let (lo, carry) = a[0].overflowing_add(b[0]);
    let hi = a[1]
        .wrapping_add(b[1])
        .wrapping_add(carry as u64);
    [lo, hi]
}
1
2
3
let max = [u64::MAX, 0];      // u64::MAX
let one = [1, 0];
assert_eq!(add128(max, one), [0, 1]); // 2^64

One family, four answers to overflow

Where checked_add (and bite 219’s checked_add_signed) bails out with None and Wrapping<T> (bite 252) wraps silently, overflowing_add is the “do both” option: you always get the mod-2^n result, plus the fact you’d otherwise have to reverse-engineer. (The dedicated carrying_add that takes and returns a carry is still nightly-only — on stable, overflowing_add is how the limbs get added.)

Compilers recognize the pattern, too: the carry chain above compiles down to an add + adc pair on x86-64 — the same code the manual trick produces, minus the puzzle.

#252 Jul 2026

252. Wrapping<T> — Modular Arithmetic Without the .wrapping_add() Noise

Hash functions, checksums, and PRNGs want arithmetic mod 2^n — but writing .wrapping_mul() on every single operation buries the math. std::num::Wrapping<T> puts the semantics in the type so you can use plain operators again.

Method-call soup

In wrap-heavy code every operation needs the method form, or debug builds panic on overflow:

1
2
3
4
5
6
7
8
fn fnv1a(data: &[u8]) -> u32 {
    let mut h: u32 = 0x811c9dc5;
    for &b in data {
        h ^= b as u32;
        h = h.wrapping_mul(0x01000193);
    }
    h
}

One call is fine. But real hash/PRNG code chains them — x.wrapping_mul(a).wrapping_add(c) — and the actual formula disappears under the method names. Forget one and a debug build panics while release silently wraps.

Wrapping moves the choice into the type

Wrap the values once; every operator on them wraps from then on:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::num::Wrapping;

fn fnv1a(data: &[u8]) -> u32 {
    let mut h = Wrapping(0x811c9dc5_u32);
    for &b in data {
        h ^= Wrapping(b as u32);
        h *= Wrapping(0x01000193);
    }
    h.0
}

*, +, -, <<, the assign forms, even unary - — all defined to wrap, in every build profile. .0 unwraps back to the plain integer at the boundary.

1
2
3
4
5
use std::num::Wrapping;

let x = Wrapping(u32::MAX);
assert_eq!(x + Wrapping(1), Wrapping(0));
assert_eq!(-Wrapping(1_u32), Wrapping(u32::MAX));

Declare intent once, not per call

The win is the same one Saturating<T> gives you (bite 167): overflow behavior is a property of the data, declared once, instead of a per-callsite decision you can fumble. The type keeps you honest, too — you can’t accidentally mix a wrapping value into checked arithmetic without an explicit .0.

Stable since Rust 1.0, Copy, zero-cost: Wrapping<u32> compiles to exactly the same instructions as the wrapping_* calls. If a function is more math than method names, wrap the inputs and let the operators speak.

#251 Jul 2026

251. include_str! — Ship the File Inside the Binary, Skip the Runtime Read

A missing template or SQL file in the deploy takes your app down at startup. include_str! bakes the file into the binary at compile time — a missing file becomes a compile error, not a production incident.

The runtime way — and its failure mode

1
2
3
// Runs at startup: I/O, error handling, and the
// file must be shipped alongside the executable.
let template = std::fs::read_to_string("greeting.txt")?;

This works until someone forgets to copy greeting.txt into the container, or the working directory isn’t what you assumed. The failure shows up at runtime, on someone else’s machine.

The compile-time way

1
2
// Embedded in the executable at compile time.
static TEMPLATE: &str = include_str!("greeting.txt");

The file’s contents become a &'static str inside your binary. No I/O, no Result, nothing to deploy alongside. If the file is missing or isn’t valid UTF-8, cargo build fails — the mistake never leaves your machine.

For binary assets there is include_bytes!, which gives you a &'static [u8]:

1
static LOGO: &[u8] = include_bytes!("logo.png");

Path gotcha

The path is relative to the current source file, not the crate root or working directory. For paths that survive refactoring into submodules, anchor them to the manifest:

1
2
3
static QUERY: &str = include_str!(
    concat!(env!("CARGO_MANIFEST_DIR"), "/queries/get_user.sql")
);

When to reach for it

SQL queries, HTML templates, license text, shader source, test fixtures — anything small and fixed at build time. Skip it for files that must be user-editable after deployment, or big enough to bloat the binary noticeably: embedding means every change requires a recompile. That’s the trade — and for config that should never drift from the code, it’s exactly what you want.

#250 Jul 2026

250. lowest_one / highest_one — Set-Bit Positions as an Option, Not a Sentinel

trailing_zeros() on 0 returns the type’s width — a sentinel you must remember to special-case. Rust 1.97 adds lowest_one and highest_one, which return an Option and make “no bits set” impossible to forget.

The sentinel problem

This morning’s bite 249 covered bit_width from today’s Rust 1.97 release. The same release fixes another sharp edge: finding the position of a set bit.

The classic tools each handle “no bits set” badly:

1
2
3
4
5
6
7
8
let x = 0b0101_0100_u8;

// Position of the lowest set bit... usually.
assert_eq!(x.trailing_zeros(), 2);

// On zero it returns the type width — a magic
// number you must remember to check for:
assert_eq!(0_u8.trailing_zeros(), 8);

For the highest bit it’s worse: ilog2() panics on zero, and the leading_zeros arithmetic bakes the type width into your formula — the same trap bite 249 described.

An Option is honest

Stable since Rust 1.97 on all integer types:

1
2
3
4
5
6
7
8
let x = 0b0101_0100_u8;

assert_eq!(x.lowest_one(),  Some(2));
assert_eq!(x.highest_one(), Some(6));

// Zero has no set bits — and the type says so:
assert_eq!(0_u8.lowest_one(),  None);
assert_eq!(0_u8.highest_one(), None);

No sentinel, no panic. The compiler forces you to decide what “no bits” means for your code, instead of letting 8 masquerade as a bit position:

1
2
3
4
5
6
7
8
// e.g. first free slot in an allocation bitmap,
// where a full mask means "grow"
let free_mask = 0b0000_0000_u8;

match free_mask.lowest_one() {
    Some(slot) => println!("use slot {slot}"),
    None       => println!("all full, grow"),
}

The 1.97 bit family

Together with bite 249, the release completes a tidy family: bit_width (how many bits a value needs), isolate_lowest_one / isolate_highest_one (the bit as a mask), and lowest_one / highest_one (the bit as a position). They agree with each other, too:

1
2
3
4
let x = 0b0101_0100_u8;

assert_eq!(x.highest_one().map(|p| p + 1)
            .unwrap_or(0), x.bit_width());

If you still write x & x.wrapping_neg() or 31 - n.leading_zeros() from muscle memory, Rust 1.97 is your cue to stop.