Series: The Rust Annals

Vol. I Issue 66 nlopes.dev

Announcing Rust 1.65.0

Generic associated types and two new control-flow forms make this a substantial language release.

Rust 1.65 stabilizes generic associated types, enabling associated types parameterized by lifetimes, types, and constants. It also adds let-else for refutable bindings that remain in the surrounding scope, permits value-producing break expressions from labeled blocks, and brings stable split debug information to Linux. Compiler and Cargo scheduling improvements reduce build work, while the standard library gains backtrace capture and new pointer, I/O, and bound APIs.

Generic associated types express lending and type-family patterns

Lifetime, type, and const generics can now be defined on associated types:

trait Foo {
    type Bar<'x>;
}

This makes it possible to write patterns that could not previously be expressed, including an iterator-like trait whose item borrows from Self, abstractions over smart-pointer families such as Rc or Arc, and borrowing fixed-size arrays from storage that is not necessarily contiguous:

/// An `Iterator`-like trait that can borrow from `Self`
trait LendingIterator {
    type Item<'a> where Self: 'a;

    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

/// Can be implemented over smart pointers, like `Rc` or `Arc`,
/// in order to allow being generic over the pointer type
trait PointerFamily {
    type Pointer<T>: Deref<Target = T>;

    fn new<T>(value: T) -> Self::Pointer<T>;
}

/// Allows borrowing an array of items. Useful for
/// `NdArray`-like types that don't necessarily store
/// data contiguously.
trait BorrowArray<T> {
    type Array<'x, const N: usize> where Self: 'x;

    fn borrow_array<'a, const N: usize>(&'a self) -> Self::Array<'a, N>;
}

The stabilization announcement describes known limitations of this stabilization. The earlier stabilization push, the associated-types reference, and the original RFC, initially opened more than 6.5 years before this release, provide further detail.

Refutable let bindings keep successful names in scope

A new form of let accepts a refutable pattern and runs a diverging else block when the pattern does not match:

let PATTERN: TYPE = EXPRESSION else {
    DIVERGING_CODE;
};

Ordinary let statements require irrefutable patterns, which are statically known to match. With let-else, a refutable pattern can bind variables in the surrounding scope, while a failed match must diverge with an operation such as break, return, or panic!:

fn get_count_item(s: &str) -> (u64, &str) {
    let mut it = s.split(' ');
    let (Some(count_str), Some(item)) = (it.next(), it.next()) else {
        panic!("Can't segment count item pair: '{s}'");
    };
    let Ok(count) = u64::from_str(count_str) else {
        panic!("Can't parse integer: '{count_str}'");
    };
    (count, item)
}
assert_eq!(get_count_item("3 chairs"), (3, "chairs"));

The scope of the bindings is the main difference from match or if let-else. Previously, the same flow required repetition and an outer let:

    let (count_str, item) = match (it.next(), it.next()) {
        (Some(count_str), Some(item)) => (count_str, item),
        _ => panic!("Can't segment count item pair: '{s}'"),
    };
    let count = if let Ok(count) = u64::from_str(count_str) {
        count
    } else {
        panic!("Can't parse integer: '{count_str}'");
    };

Labeled blocks can finish early with a value

Plain block expressions can now be labeled as break targets. The jump is not arbitrary: it can only go from inside the block to its end. Although this was already possible with loop, it sometimes required a loop that always ran exactly once.

A labeled break can carry an expression value, allowing a multi-statement block to produce an early result:

let result = 'block: {
    do_thing();
    if condition_not_met() {
        break 'block 1;
    }
    do_next_thing();
    if condition_not_met() {
        break 'block 2;
    }
    do_last_thing();
    3
};

Linux builds gain split DWARF and optimized compilations do less work

Following macOS support in Rust 1.51, split debug information is now stable on Linux. -Csplit-debuginfo=unpacked writes debug information to multiple .dwo DWARF object files; -Csplit-debuginfo=packed produces one .dwp DWARF package alongside the output binary; and -Csplit-debuginfo=off remains the default, retaining DWARF data in the objects' and final binary's .debug_* ELF sections.

Keeping debug information out of the object files being linked lets the linker avoid processing it, which can speed up link times. Other targets now accept -Csplit-debuginfo as a stable option with their platform-specific default value, but specifying other values on those targets remains unstable.

MIR inlining is now enabled for optimized compilations. The release announcement reports a 3–10% improvement in compile times for real-world crates. Cargo also now sorts its queue of pending jobs when scheduling builds to improve performance.

Backtraces and pointer operations join the stable library surface

std::backtrace::Backtrace can capture a stack backtrace at any time using the same platform-specific implementation that usually serves panic backtraces. This may be useful, for example, when adding runtime context to error types.

The release also stabilizes Bound::as_ref, std::io::read_to_string, <*const T>::cast_mut, and <*mut T>::cast_const. The pointer methods <*const T>::offset_from and <*mut T>::offset_from are newly usable in const contexts.

Reproduced from the Rust blog under its publication licence. Typeset in Literata.