Introduction

Let's start with a funny number "83", try to guess what that means before i tell you

That's how many different nightly features are enabled in compiler at the commit I checked

(I counted every distinct name inside feature attr in the lib.rs of each crate in the compiler at commit f45772e)

Important

I should mention that these features are not required for the compiler to function. I'm pretty sure it could work the same way without about 90% of them. The point is that they make the code simpler and more nicer to read and write.

And i don't mean some exotic internal stuff that only makes sense inside rustc (well, that too, but not today). A lot of these are just regular and very useful features, ones you can make great use of right now (on nightly)!

Note

Wait, how does a stable compiler use nightly features? rustc is written in Rust, so to build it you need another Rust compiler first. The build downloads the latest beta, called stage0, and uses it to build the new compiler

Beta normally forbids usage of any unstable features, so the build sets a special variable called RUSTC_BOOTSTRAP to 1, which allows using nightly features. It's meant only for building the compiler itself (more in the rustc-dev-guide), so please don't set it in your own projects: it turns off the stability guarantees, and your code can break with any update

Here are the top 5 by how many compiler crates turn them on:

#FeatureCratesWhat it does
1deref_patterns21match through Box, String, Vec and friends
2try_blocks16try { ... } blocks, so you can use ? without a separate function
3option_into_flat_iter14turn an Option of something iterable into an iterator, empty if None
4default_field_values13default values right in the struct definition
5iter_intersperse, file_buffered, rustc_attrs11put a separator between iterator items, open or create a file already wrapped in BufReader/BufWriter, and compiler-internal attributes

#1 and #4 get the full treatment today, the rest will have to wait for another post :p

So... let's start!

deref_patterns

I picked this one first because it's enabled in 21 crates in the compiler, which is the highest number, and you will see why :D

this feature fixes an actual pain point you've probably all run into. for example you're writing a small CLI and want to match on its arguments:

#![feature(deref_patterns)]

fn run(args: Vec<String>) -> String {
    match &args {
        ["help"] => "usage: app <command>".to_string(),
        ["open", file] => format!("opening {file}"),
        ["rm", files @ ..] => format!("removing {} files", files.len()),
        _ => "unknown command".to_string(),
    }
}
Warning

This is just to show the feature, not how you'd write a real CLI!

Looks like it should just work, right? On stable it doesn't, and it trips twice. First, the pattern is being matched against a Vec, but slice patterns require a slice or an array here:

error[E0529]: expected an array or slice, found `Vec<String>`
 --> src/main.rs:3:9
  |
3 |         ["help"] => "usage: app <command>".to_string(),
  |         ^^^^^^^^ pattern cannot match with input type `Vec<String>`
  |
help: consider slicing here
  |
2 |     match &args[..] {
  |                ++++

Now the slice pattern matches the slice, but the "help" pattern still doesn't match a String element, because Rust doesn't automatically dereference the String to match it as a &str:

error[E0308]: mismatched types
 --> src/main.rs:3:10
  |
2 |     match &args[..] {
  |           --------- this expression has type `&[String]`
3 |         ["help"] => "usage: app <command>".to_string(),
  |          ^^^^^^ expected `String`, found `&str`

So on stable you end up building a second vector just for this:

fn run(args: Vec<String>) -> String {
    let args: Vec<&str> = args.iter().map(String::as_str).collect();
    match args.as_slice() {
        ["help"] => "usage: app <command>".to_string(),
        // ...
    }
}

With deref patterns the compiler looks through both the Vec and the String for you, and the first version just works

Boxes Are a Barrier No More

The compiler's problem is the same, just with Box instead of a String. rustc data structures are stuffed with boxes, mostly to keep enums small. For example, every assignment in MIR is stored as Assign(Box<(Place, Rvalue)>).

With deref patterns, code that looks for "an assignment of a reference" can match it in one go, straight through the box:

// roughly it says: does this line of code look like `x = &y`?
// if so, give me lifetime, mutability and `y` itself
let StatementKind::Assign((_, Rvalue::Ref(region, borrow_kind, place))) =
    &mut statement.kind
else {
    bug!()
};

Without the feature that's two steps: first get the box out, then dereference it and match what's inside

let StatementKind::Assign(assign) = &mut statement.kind else { bug!() };
let (_, Rvalue::Ref(region, borrow_kind, place)) = &mut **assign else {
    bug!()
};

box Patterns Are Gone

The compiler used to do this with a different feature, box_patterns, which needed an explicit keyword: Assign(box (lhs, ref rhs)). box_patterns was never going to be stabilized, since deref_patterns does the same thing and more. But it couldn't simply be deleted either, because the compiler itself used it all over the place. So after an MCP, in May 2026 a series of PRs, starting with #156193, went through the compiler crate by crate and replaced them

Once nothing depended on it anymore, #156749 removed box_patterns from the compiler completely. It landed in August 2026, and i was the assigned reviewer on that one, with @fmease helping out :3

The only note left for the feature now says "superseded by deref_patterns"

Not Every Deref

It works for Box, String, Vec, Rc, Arc, Cow and a few more, but not for every type with a Deref. To match through a pointer, the compiler has to assume that dereferencing it gives the same thing every time, otherwise checking that a match covers every case would be unsound. But Deref is just a trait with regular code inside, and nothing stops someone from writing one that returns something different on every call. So it only works for types that implement the unsafe marker trait DerefPure. The standard library implements it for its own pointers, but your own type doesn't get it for free:

#![feature(deref_patterns)]
use std::ops::Deref;

struct Name(String);

impl Deref for Name {
    type Target = String;
    fn deref(&self) -> &String { &self.0 }
}

fn main() {
    match Name("ferris".into()) {
        "ferris" => println!("hi!"),
        _ => {}
    }
}
error[E0277]: the trait bound `Name: DerefPure` is not satisfied
  --> src/main.rs:13:9
   |
13 |         "ferris" => println!("hi!"),
   |         ^^^^^^^^ unsatisfied trait bound
   |
help: the nightly-only, unstable trait `DerefPure` is not implemented for `Name`
  --> src/main.rs:4:1
   |
 4 | struct Name(String);
   | ^^^^^^^^^^^

Your own type can implement DerefPure with an unsafe impl, but doing so requires another nightly feature called deref_pure_trait


So why isn't it on stable yet? Mostly because there's no RFC for it yet, the people behind it are still writing one up. And the DerefPure part, letting your own types opt in, isn't planned to be stabilized at this stage at all

If you want to follow along: tracking issue #87121, on nightly since 1.79

default_field_values

The next feature is turned on in 13 compiler crates, and once you see it you'll want it everywhere

Here's the parser. Not a toy parser, the parser, the one that reads your code every day. Here's its definition:

pub struct Parser<'a> {
    pub psess: &'a ParseSess,
    /// The current token.
    pub token: Token = Token::dummy(),
    /// The spacing for the current token.
    token_spacing: Spacing = Spacing::Alone,
    /// The previous token.
    pub prev_token: Token = Token::dummy(),
    pub capture_cfg: bool = false,
    restrictions: Restrictions = Restrictions::empty(),
    expected_token_types: TokenTypeSet = TokenTypeSet::new(),
    token_cursor: TokenCursor,
    num_bump_calls: u32 = 0,
    // ...
    recovery: Recovery = Recovery::Allowed,
    // ...
}

See those = something after the types? Those are default values, written right in the struct definition! And here's how a parser gets created:

let mut parser = Parser {
    psess,
    token_cursor: TokenCursor::new(stream),
    subparser_name,
    capture_state: CaptureState { /* ... */ },
    ..
};

That lonely .. at the end means "everything i didn't mention - take it from the defaults". Parser has 20 fields, 16 of them have a default, so you only spell out the 4 that actually depend on the input

And the parser isn't even the biggest case. the resolver has 96 fields, and 57 of them have a default. Imagine reading that constructor without ..

It's Not Just Syntax Sugar for Default

What I like most is that required fields stay required and if you miss one, compiler will give you a small reminder:

#![feature(default_field_values)]
struct P<'a> { src: &'a str, pos: usize = 0 }
fn main() { let _ = P { .. }; }
error: missing field `src` in initializer
 --> src/main.rs:3:25
  |
3 | fn main() { let _ = P { .. }; }
  |                         ^ fields that do not have a defaulted value must be provided explicitly

On stable the usual trick is ..Default::default(), but that only works if the whole struct implements Default, and it builds a complete throwaway value just to take the remaining fields from it. Parser couldn't even do that: there's no sensible default for psess: &ParseSess, so you'd be back to writing every field by hand

This feature also combines with #[derive(Default)] pretty well

The derived Default implementation uses the declared field defaults where present, rather than the type's usual default values:

#![feature(default_field_values)]

#[derive(Default, Debug)]
struct Config { name: String, retries: u32 = 3, verbose: bool = true }

fn main() {
    println!("{:?}", Config::default());
    // Config { name: "", retries: 3, verbose: true }
}

Default expressions must be usable in a const context. For trait-based defaults such as Default::default(), this also involves the compiler's const_trait_impl feature


This one already has an accepted RFC and a finished implementation, so what's left is mostly paperwork: docs in the Reference, the Book and Rust By Example, and formatting rules in the style guide. There's also an ongoing discussion about whether defaults should have to be const at all, so that you could write something like seed: u64 = rand()

If you want to follow along: RFC 3681, tracking issue #132162, on nightly since 1.85

Conclusion

So that's what stable Rust stands on: a compiler built with 83 nightly features, two of which you've now seen up close. One lets you match straight through a Box or a String, the other lets you stop writing the same defaults over and over. None of it is compiler magic, rustc just gets to use them first :D

And that's kind of the nice part. By the time a feature like this reaches stable, it has already been battle-tested on one of the biggest Rust codebases out there. And sometimes the test goes the other way: box_patterns lived in the compiler for years and still didn't make it, because something better came along. Either way, what you eventually get on stable has already had a lot of its rough edges filed off

There are plenty of interesting things in the compiler, so I'll bring some of them next time!