Rust-1.95

#111 Apr 2026

111. Vec::insert_mut — Splice In and Edit Without Reindexing

You insert a placeholder, then index back to fix it up. Two lookups, two bounds checks, one wobble. Vec::insert_mut — stable in 1.95 — hands you the &mut T directly.

The classic dance:

1
2
3
4
let mut v = vec![1, 2, 4, 5];
v.insert(2, 0);
v[2] = 3; // recompute the index, second bounds check
assert_eq!(v, [1, 2, 3, 4, 5]);

insert_mut returns the slot:

1
2
3
4
let mut v = vec![1, 2, 4, 5];
let slot = v.insert_mut(2, 0);
*slot = 3;
assert_eq!(v, [1, 2, 3, 4, 5]);

This shines when the value is built in pieces — push a default, then fill it in based on where it landed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#[derive(Default, Debug, PartialEq)]
struct Job { id: u32, label: String }

let mut jobs: Vec<Job> = vec![Job { id: 1, label: "build".into() }];
let j = jobs.insert_mut(0, Job::default());
j.id = 0;
j.label = "setup".into();

assert_eq!(jobs[0], Job { id: 0, label: String::from("setup") });
assert_eq!(jobs[1].label, "build");

Rust 1.95 grew the whole _mut family alongside Vec::push_mut: Vec::insert_mut, VecDeque::push_front_mut, VecDeque::push_back_mut, VecDeque::insert_mut, LinkedList::push_front_mut, and LinkedList::push_back_mut. Same idea everywhere — the place-it method now returns a mutable reference to the slot it just placed.

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

let mut q: VecDeque<i32> = VecDeque::from([2, 3]);
let head = q.push_front_mut(0);
*head += 1; // now 1
assert_eq!(q, VecDeque::from([1, 2, 3]));

Quietly useful, no churn — just fewer indices floating around.

94. ControlFlow::is_break and is_continue — Ask the Flow Which Way It Went

Got a ControlFlow back from try_fold or a visitor and just want to know which variant you’re holding? Before 1.95 you either pattern-matched or reached for matches!. Rust 1.95 adds straightforward .is_break() and .is_continue() methods.

The old pain

ControlFlow<B, C> is the enum that powers short-circuiting iterator methods like try_for_each and try_fold. Once you had one, checking which arm it was took more ceremony than you’d expect:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::ops::ControlFlow;

fn first_over(v: &[i32], n: i32) -> ControlFlow<i32> {
    v.iter().try_for_each(|&x| {
        if x > n { ControlFlow::Break(x) } else { ControlFlow::Continue(()) }
    })
}

fn main() {
    let flow = first_over(&[1, 2, 3, 10, 4], 5);
    // Want a bool? Reach for matches!
    let found = matches!(flow, ControlFlow::Break(_));
    assert!(found);
}

The name ControlFlow::Break also collides visually with loop break, so code like this reads a bit awkwardly.

The fix: inherent is_break / is_continue

Rust 1.95 stabilises two one-line accessors, mirroring Result::is_ok / is_err and Option::is_some / is_none:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use std::ops::ControlFlow;

fn first_over(v: &[i32], n: i32) -> ControlFlow<i32> {
    v.iter().try_for_each(|&x| {
        if x > n { ControlFlow::Break(x) } else { ControlFlow::Continue(()) }
    })
}

fn main() {
    let hit = first_over(&[1, 2, 3, 10, 4], 5);
    let miss = first_over(&[1, 2, 3], 5);

    assert!(hit.is_break());
    assert!(!hit.is_continue());

    assert!(miss.is_continue());
    assert!(!miss.is_break());
}

No pattern, no import of a macro, no wildcards.

Handy in counting and filtering

Because the methods take &self, you can pipe results straight through iterator adapters:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::ops::ControlFlow;

fn main() {
    let flows: Vec<ControlFlow<&'static str, i32>> = vec![
        ControlFlow::Continue(1),
        ControlFlow::Break("boom"),
        ControlFlow::Continue(2),
        ControlFlow::Break("fire"),
        ControlFlow::Continue(3),
    ];

    let breaks = flows.iter().filter(|f| f.is_break()).count();
    let continues = flows.iter().filter(|f| f.is_continue()).count();

    assert_eq!(breaks, 2);
    assert_eq!(continues, 3);
}

Previously you’d write |f| matches!(f, ControlFlow::Break(_)) — shorter, but noisier and requires you to name the variant (which means a use or a fully-qualified path).

It’s just a bool — but it’s the right bool

Nothing here is groundbreaking: you could always write a matches!. But when a method exists on Result and Option and Poll and hash iterators and even Peekable, not having one on ControlFlow meant reaching for a different idiom for no good reason. 1.95 closes that small gap.

Stabilised in Rust 1.95 (April 2026).

93. MaybeUninit Array Conversions — Build Fixed Arrays Without transmute

Ever tried to build a [T; N] element-by-element and ended up reaching for mem::transmute because [MaybeUninit<T>; N] refused to convert? Rust 1.95 stabilises safe conversions between [MaybeUninit<T>; N] and MaybeUninit<[T; N]> — no transmute, no tricks.

The old pain

You want a fully-initialised [T; N], but T isn’t Default (or the init is fallible, or expensive). The canonical pattern is:

  1. Allocate [MaybeUninit<T>; N] uninitialised.
  2. Fill each slot.
  3. Get a [T; N] out the other end.

Step 3 is where it got ugly. MaybeUninit::assume_init only works on MaybeUninit<T>, not [MaybeUninit<T>; N]. To flip the array-of-uninits into uninit-of-array, you reached for mem::transmute — which works, but leans on layout assumptions and carries a big “here be dragons” vibe.

The fix: From conversions

Rust 1.95 stabilises both directions of the conversion, so no transmute is needed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::mem::MaybeUninit;

fn main() {
    let arr: [MaybeUninit<u32>; 4] = [
        MaybeUninit::new(1),
        MaybeUninit::new(2),
        MaybeUninit::new(3),
        MaybeUninit::new(4),
    ];

    // [MaybeUninit<T>; N] -> MaybeUninit<[T; N]>
    let packed: MaybeUninit<[u32; 4]> = MaybeUninit::from(arr);
    let init: [u32; 4] = unsafe { packed.assume_init() };

    assert_eq!(init, [1, 2, 3, 4]);
}

MaybeUninit::from takes the array-of-uninits and gives you an uninit-of-array ready to assume_init. Safe, obvious, no layout assumption on your part.

The reverse direction

You can also go the other way — from MaybeUninit<[T; N]> back to [MaybeUninit<T>; N] — useful when you want to touch elements individually:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::mem::MaybeUninit;

fn main() {
    let packed: MaybeUninit<[u32; 3]> = MaybeUninit::new([10, 20, 30]);

    // MaybeUninit<[T; N]> -> [MaybeUninit<T>; N]
    let unpacked: [MaybeUninit<u32>; 3] = <[MaybeUninit<u32>; 3]>::from(packed);

    let values: [u32; 3] = unsafe {
        [
            unpacked[0].assume_init(),
            unpacked[1].assume_init(),
            unpacked[2].assume_init(),
        ]
    };
    assert_eq!(values, [10, 20, 30]);
}

Plus AsRef / AsMut for free

The same release adds AsRef<[MaybeUninit<T>; N]> and AsMut<[MaybeUninit<T>; N]> (plus slice versions) for MaybeUninit<[T; N]>. That means you can borrow the uninit array as a slice without any conversion ceremony:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::mem::MaybeUninit;

fn fill_evens(buf: &mut [MaybeUninit<u32>]) {
    for (i, slot) in buf.iter_mut().enumerate() {
        slot.write(i as u32 * 2);
    }
}

fn main() {
    let mut packed: MaybeUninit<[u32; 4]> = MaybeUninit::uninit();

    // AsMut<[MaybeUninit<T>]> — borrow as a mutable slice of slots
    fill_evens(packed.as_mut());

    let init: [u32; 4] = unsafe { packed.assume_init() };
    assert_eq!(init, [0, 2, 4, 6]);
}

The helper writes through the AsMut slice view; the caller gets a fully-initialised [u32; 4] after assume_init. No transmute, no pointer casting.

When to reach for it

This is niche — you don’t need it until you’re writing generic collection code, FFI wrappers, or allocator-like APIs that build arrays without Default. But when you do, the new conversions delete an uncomfortable transmute from your codebase and make the intent explicit.

Stabilised in Rust 1.95 (April 2026).

92. core::range — Range Types You Can Actually Copy

Ever tried to reuse a 0..=10 range and hit “use of moved value”? Rust 1.95 stabilises core::range, a new module of range types that implement Copy.

The old pain

The classic range types in std::opsRange, RangeInclusive — are iterators themselves. That means iterating them consumes them, and they can’t be Copy:

1
2
3
4
let r = 0..=5;
let a: Vec<_> = r.clone().collect();
let b: Vec<_> = r.collect(); // consumes r
// r is gone here

Every time you want to reuse a range you reach for .clone() or rebuild it. Annoying for something that looks like a pair of integers.

The fix: core::range

Rust 1.95 adds new range types in core::range (re-exported as std::range). They’re plain data — Copy, no iterator state baked in — and you turn them into an iterator on demand with .into_iter():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use core::range::RangeInclusive;

fn main() {
    let r: RangeInclusive<i32> = (0..=5).into();

    let a: Vec<i32> = r.into_iter().collect();
    let b: Vec<i32> = r.into_iter().collect(); // r is Copy, still usable

    assert_eq!(a, vec![0, 1, 2, 3, 4, 5]);
    assert_eq!(a, b);
}

No .clone(), no rebuild. r is just two numbers behind the scenes, so copying it is free.

Passing ranges around

Because the new types are Copy, you can pass them into helpers without worrying about move semantics:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use core::range::RangeInclusive;

fn sum_range(r: RangeInclusive<i32>) -> i32 {
    r.into_iter().sum()
}

fn main() {
    let r: RangeInclusive<i32> = (1..=4).into();

    assert_eq!(sum_range(r), 10);
    assert_eq!(sum_range(r), 10); // reuse: r is Copy
}

The old std::ops::RangeInclusive would have been moved by the first call.

Field access, not method calls

The new types expose their bounds as public fields — no more .start() / .end() accessors. For RangeInclusive, the upper bound is called last (emphasising that it’s included):

1
2
3
4
5
6
7
8
use core::range::RangeInclusive;

fn main() {
    let r: RangeInclusive<i32> = (10..=20).into();

    assert_eq!(r.start, 10);
    assert_eq!(r.last, 20);
}

Exclusive Range has start and end in the same way. Either one is simple plain-old-data that you can pattern-match, destructure, or read directly.

When to reach for it

Use core::range whenever you want to store, pass, or reuse a range as a value. The old std::ops ranges are still everywhere (literal syntax, slice indexing, for loops), so there’s no rush to migrate — but for library APIs that take ranges as parameters, the new types are the friendlier choice.

Stabilised in Rust 1.95 (April 2026).

91. bool::try_from — Convert Integers to Booleans Safely

Need to turn a 0 or 1 from a database, config file, or FFI boundary into a bool? bool::try_from does it safely — rejecting anything that isn’t 0 or 1. Stabilized in Rust 1.95.

The old way: match or panic

When you get integer flags from external sources — database columns, binary protocols, C APIs — you need to convert them to bool. Before 1.95, you’d write your own:

1
2
3
4
5
6
7
fn int_to_bool(v: u8) -> Result<bool, String> {
    match v {
        0 => Ok(false),
        1 => Ok(true),
        other => Err(format!("invalid bool value: {other}")),
    }
}

Or worse, you’d just do v != 0 and silently treat 42 as true, hiding data corruption.

The new way: bool::try_from

Rust 1.95 stabilizes TryFrom<{integer}> for bool across all integer types — u8, i32, u64, you name it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn main() {
    let t = bool::try_from(1u8);
    let f = bool::try_from(0i32);
    let e = bool::try_from(2u8);

    assert_eq!(t, Ok(true));
    assert_eq!(f, Ok(false));
    assert!(e.is_err());

    println!("{t:?}"); // Ok(true)
    println!("{f:?}"); // Ok(false)
    println!("{e:?}"); // Err(TryFromIntError(()))
}

Only 0 maps to false and 1 maps to true. Everything else returns Err(TryFromIntError). No silent coercion, no panics.

Real-world usage: parsing a config

Here’s where it shines — reading flags from external data:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::collections::HashMap;

fn main() {
    // Simulating values from a config file or database row
    let config: HashMap<&str, i64> = HashMap::from([
        ("verbose", 1),
        ("dry_run", 0),
        ("debug", 3),  // invalid!
    ]);

    for (key, &val) in &config {
        match bool::try_from(val) {
            Ok(flag) => println!("{key} = {flag}"),
            Err(_) => eprintln!("warning: {key} has invalid bool value: {val}"),
        }
    }
}

Instead of silently treating 3 as true, you get a clear warning and a chance to handle bad data properly.

Why not just val != 0?

The C convention of “zero is false, everything else is true” works fine when you control the data. But when parsing untrusted input — database rows, wire protocols, config files — a value of 255 probably means something went wrong. bool::try_from catches that; != 0 hides it.

One method call, no custom helpers, no silent bugs.

89. cold_path — Tell the Compiler Which Branch Won't Happen

Your error-handling branch fires once in a million calls, but the compiler doesn’t know that. core::hint::cold_path lets you mark unlikely branches so the optimiser can focus on the hot path.

Why branch layout matters

Modern CPUs predict which way a branch will go and speculatively execute instructions ahead of time. When the prediction is right, execution flies. When it’s wrong, the pipeline stalls.

Compilers already try to guess which branches are hot, but they don’t always get it right — especially when both sides of an if look equally plausible from a static analysis perspective. That’s where cold_path comes in.

The basics

Call cold_path() at the start of a branch that is rarely taken:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::hint::cold_path;

fn process(value: Option<u64>) -> u64 {
    if let Some(v) = value {
        v * 2
    } else {
        cold_path();
        log_miss();
        0
    }
}

fn log_miss() {
    // imagine logging or metrics here
}

The compiler now knows the else arm is unlikely. It can lay out the machine code so the hot path (the Some arm) has no jumps, keeping it in the instruction cache and the branch predictor happy.

In match expressions

cold_path works well in match arms too — mark the rare variants:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::hint::cold_path;

fn handle_status(code: u16) -> &'static str {
    match code {
        200 => "ok",
        301 => "moved",
        404 => { cold_path(); "not found" }
        500 => { cold_path(); "server error" }
        _   => { cold_path(); "unknown" }
    }
}

Only the branches you expect to be common stay on the fast track.

Building likely and unlikely helpers

If you’ve used C/C++, you might miss __builtin_expect. With cold_path you can build the same thing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use std::hint::cold_path;

#[inline(always)]
const fn likely(b: bool) -> bool {
    if !b { cold_path(); }
    b
}

#[inline(always)]
const fn unlikely(b: bool) -> bool {
    if b { cold_path(); }
    b
}

fn check_bounds(index: usize, len: usize) -> bool {
    if unlikely(index >= len) {
        panic!("out of bounds: {} >= {}", index, len);
    }
    true
}

Now you can annotate conditions directly instead of marking individual branches.

A word of caution

Misusing cold_path on a branch that actually runs often can hurt performance — the compiler will deprioritise it, and you’ll get more pipeline stalls, not fewer. Always benchmark before sprinkling hints around. Profile first, hint second.

The bottom line

cold_path is a zero-cost, zero-argument function that tells the optimiser what you already know: this branch is the exception, not the rule. It’s a small tool, but in hot loops and latency-sensitive code, it can make a measurable difference.

Stabilised in Rust 1.95 (April 2026).

88. Vec::push_mut — Push and Modify in One Step

Tired of pushing a default into a Vec and then grabbing a mutable reference to fill it in? Rust 1.95 stabilises push_mut, which returns &mut T to the element it just inserted.

The old dance

A common pattern is to push a placeholder value and then immediately mutate it. Before push_mut, you had to do an awkward two-step:

1
2
3
4
5
6
7
8
9
let mut names = Vec::new();

// Push, then index back in to mutate
names.push(String::new());
let last = names.last_mut().unwrap();
last.push_str("hello");
last.push_str(", world");

assert_eq!(names[0], "hello, world");

That last_mut().unwrap() is boilerplate — you know the element is there because you just pushed it. Worse, in more complex code the compiler can’t always prove the reference is safe, forcing you into index gymnastics.

Enter push_mut

push_mut pushes the value and hands you back a mutable reference in one shot:

1
2
3
4
5
6
7
let mut scores = Vec::new();

let entry = scores.push_mut(0);
*entry += 10;
*entry += 25;

assert_eq!(scores[0], 35);

No unwraps, no indexing, no second lookup. The borrow checker is happy because there’s a clear chain of ownership.

Building structs in-place

This really shines when you’re constructing complex values piece by piece:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#[derive(Debug, Default)]
struct Player {
    name: String,
    score: u32,
    active: bool,
}

let mut roster = Vec::new();

let p = roster.push_mut(Player::default());
p.name = String::from("Ferris");
p.score = 100;
p.active = true;

assert_eq!(roster[0].name, "Ferris");
assert_eq!(roster[0].score, 100);
assert!(roster[0].active);

Instead of building the struct fully before pushing, you push a default and fill it in. This can be handy when the final field values depend on context you only have after insertion.

Also works for insert_mut

Need to insert at a specific index? insert_mut follows the same pattern:

1
2
3
4
5
let mut v = vec![1, 3, 4];
let inserted = v.insert_mut(1, 2);
*inserted *= 10;

assert_eq!(v, [1, 20, 3, 4]);

Both methods are also available on VecDeque (push_front_mut, push_back_mut, insert_mut) and LinkedList (push_front_mut, push_back_mut).

When to reach for it

Use push_mut whenever you’d otherwise write push followed by last_mut().unwrap(). It’s one less unwrap, one fewer line, and clearer intent: push this, then let me tweak it.

Stabilised in Rust 1.95 (April 2026) — update your toolchain and give it a spin.

72. if let Guards — Pattern Match Inside Match Guards

Match guards are great for adding conditions to arms, but until now you couldn’t destructure inside them. Rust 1.95 stabilizes if let guards, letting you pattern-match right in the guard position.

Suppose you’re matching commands and need to parse a value from one of the variants. Before if let guards, you had to nest an if let inside the arm body:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
enum Command {
    Set(String, String),
    Get(String),
    Quit,
}

fn execute(cmd: Command) -> String {
    match cmd {
        Command::Set(key, value) => {
            if let Ok(n) = value.parse::<i32>() {
                format!("SET {key} = {n} (as integer)")
            } else {
                format!("SET {key} = {value} (as string)")
            }
        }
        Command::Get(key) => format!("GET {key}"),
        Command::Quit => "BYE".to_string(),
    }
}

The Set arm does two different things depending on whether the value parses as an integer — but that logic is buried inside a nested if let. With if let guards, the condition moves into the guard itself:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fn execute_v2(cmd: Command) -> String {
    match cmd {
        Command::Set(key, value) if let Ok(n) = value.parse::<i32>() => {
            format!("SET {key} = {n} (as integer)")
        }
        Command::Set(key, value) => {
            format!("SET {key} = {value} (as string)")
        }
        Command::Get(key) => format!("GET {key}"),
        Command::Quit => "BYE".to_string(),
    }
}

Now each arm handles exactly one case. The guard destructures the parse result, and n is available in the arm body. If the guard fails, Rust falls through to the next Set arm — no nested if/else needed.

You can combine if let guards with regular boolean conditions using &&:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn execute_v3(cmd: Command) -> String {
    match cmd {
        Command::Set(key, value)
            if let Ok(n) = value.parse::<i32>()
            && n > 0 =>
        {
            format!("SET {key} = {n} (positive integer)")
        }
        Command::Set(key, value) => {
            format!("SET {key} = {value}")
        }
        Command::Get(key) => format!("GET {key}"),
        Command::Quit => "BYE".to_string(),
    }
}

This feature lands stable in Rust 1.95 (April 2026). If you’ve been nesting if let inside match arms, this is your cleanup opportunity.