Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Temporaries and Lifetime Extension

A "value-to-place coercion" occurs when a value expression is used in a context where a place is needed, e.g. because it is borrowed, matched on, or has a field accessed. See this blog post for more details about place/value expressions and place/value contexts.

Whenever that happens, the value will get stored in a temporary variable. In this step, we make these temporaries explicit.

The rules that determine the scope of these temporaries are complex; they're described in the Reference. You may also enjoy this blog post with a more explanatory style.

In this step, for each expression $expr to be coerced, we first add a let tmp; statement, then assign it tmp = $expr; (these two steps can be merged), then use tmp where the expression was. The placement of the let tmp; determines how long the value will live and its drop order. To get the right scope, extra blocks { .. } may be added.

For example:

#![allow(unused)]
fn main() {
let s = if Option::is_some(&Option::clone(&opt)) {
    let _x = &42;
    &String::new()
} else {
    &String::new()
};

// becomes:
let tmp3;
let tmp4;
let s = if { let tmp1 = Option::clone(&opt); Option::is_some(&tmp1) } {
    let tmp2 = 42;
    let _x = &tmp2;
    tmp3 = String::new();
    &tmp3
} else {
    tmp4 = String::new();
    &tmp4
};
}

Or:

#![allow(unused)]
fn main() {
let opt: RwLock<Option<u32>> = ...
if let Some(x) = Option::as_ref(&*Result::unwrap(RwLock::read(&opt))) {
    ...
} else {
    ...
}

// becomes (in edition 2024):
if let tmp = Result::unwrap(RwLock::read(&opt)) && let Some(x) = Option::as_ref(&*tmp) {
    ...
} else {
    ...
}
}

Note how in let chains we may introduce the temporaries as part of the let chain to get the right scope. Our Extended Let Chains allow forward declarations let x; in the middle of a let chain for that purpose.

Taking an example from the edition book:

#![allow(unused)]
fn main() {
fn f() -> usize {
    let c = RefCell::new("..");
    c.borrow().len()
}

// Becomes, after method resolution:
fn f() -> usize {
    let c = RefCell::new("..");
    str::len(*<Ref<_> as Deref>::deref(&RefCell::borrow(&c)))
}

// Before 2024, this becomes:
fn f() -> usize {
    let tmp1; // Added at the start of scope so that it drops after the other locals.
    let tmp2;
    let c = RefCell::new("..");
    tmp1 = RefCell::borrow(&c); // error[E0597]: `c` does not live long enough
    tmp2 = <Ref<_> as Deref>::deref(&tmp1);
    str::len(*tmp2)
}

// After 2024, this becomes:
fn f() -> usize {
    let c = RefCell::new("..");
    let tmp1; // drops before other locals
    let tmp2;
    tmp1 = RefCell::borrow(&c);
    tmp2 = <Ref<_> as Deref>::deref(&tmp1);
    str::len(*tmp2)
}
}

There is an exception to the above: temporaries can, when sensible, become statics instead of local variables. This is called "constant promotion":

#![allow(unused)]
fn main() {
let x = &1 + 2;

// becomes:
static TMP: u32 = 1 + 2;
let x = &TMP; // this allows `x` to have type `&'static u32`
}

After this step, all place contexts contain place expressions.

Implementation

This is a completely fake PoC implementation, as a placeholder to try out my test harness.

#![allow(unused)]
fn main() {
//! Entirely silly PoC desugaring pass.

pub struct ValueToPlaceDesugarer<'tcx, 'a> {
    body: &'a mut Body<'tcx>,
}

impl<'tcx, 'a> ValueToPlaceDesugarer<'tcx, 'a> {
    pub fn new(body: &'a mut Body<'tcx>) -> Self {
        Self { body }
    }

    pub fn run(&mut self) {
        // Only iterate over the original expressions; newly created ones don't need to be
        // revisited.
        for expr_id in self.body.thir.exprs.indices() {
            if let ExprKind::Borrow { borrow_kind, arg } =
                self.body.thir.exprs[expr_id].kind.clone()
            {
                if self.should_temp(arg) {
                    self.introduce_temp(expr_id, borrow_kind, arg);
                }
            }
        }
    }

    fn should_temp(&self, arg: thir::ExprId) -> bool {
        let peeled = self.peel_value_expr(arg);
        matches!(self.body.thir.exprs[peeled].kind, ExprKind::Call { .. })
    }

    fn peel_value_expr(&self, mut id: thir::ExprId) -> thir::ExprId {
        loop {
            match self.body.thir.exprs[id].kind {
                ExprKind::Scope { value, .. }
                | ExprKind::Use { source: value }
                | ExprKind::NeverToAny { source: value }
                | ExprKind::PlaceTypeAscription { source: value, .. }
                | ExprKind::ValueTypeAscription { source: value, .. }
                | ExprKind::PlaceUnwrapUnsafeBinder { source: value }
                | ExprKind::ValueUnwrapUnsafeBinder { source: value }
                | ExprKind::WrapUnsafeBinder { source: value } => {
                    id = value;
                }
                _ => break id,
            }
        }
    }

    fn fresh_scope(&mut self) -> region::Scope {
        region::Scope {
            local_id: self.body.new_hir_id().local_id,
            data: ScopeData::Node,
        }
    }

}

Some markdown explanation:

#![allow(unused)]
fn main() {
    fn introduce_temp(
        &mut self,
        expr_id: thir::ExprId,
        borrow_kind: rustc_middle::mir::BorrowKind,
        arg: thir::ExprId,
    ) {
        let hir_id = self.body.new_hir_id();
        let tmp_name = Symbol::intern("tmp");
        self.body.insert_synthetic_local_name(hir_id, tmp_name);

        let var_expr_id = self.body.thir.exprs.push(Expr {
            kind: ExprKind::VarRef {
                id: thir::LocalVarId(hir_id),
            },
            ..self.body.thir.exprs[arg]
        });

        let borrow_expr = Expr {
            kind: ExprKind::Borrow {
                borrow_kind,
                arg: var_expr_id,
            },
            ..self.body.thir.exprs[expr_id]
        };
        let borrow_expr_id = self.body.thir.exprs.push(borrow_expr);

        let arg_expr = &self.body.thir.exprs[arg];
        let pat = Pat {
            ty: arg_expr.ty,
            span: arg_expr.span,
            kind: PatKind::Binding {
                name: tmp_name,
                mode: hir::BindingMode(hir::ByRef::No, Mutability::Not),
                var: thir::LocalVarId(hir_id),
                ty: arg_expr.ty,
                subpattern: None,
                is_primary: true,
                is_shorthand: false,
            },
        };
        let assign_stmt = Stmt {
            kind: StmtKind::Let {
                remainder_scope: self.fresh_scope(),
                init_scope: self.fresh_scope(),
                pattern: Box::new(pat),
                initializer: Some(arg),
                else_block: None,
                lint_level: thir::LintLevel::Inherited,
                span: self.body.thir.exprs[arg].span,
            },
        };
        let stmt_id = self.body.thir.stmts.push(assign_stmt);

        let block = thir::Block {
            targeted_by_break: false,
            region_scope: self.fresh_scope(),
            span: self.body.thir.exprs[expr_id].span,
            stmts: Box::from([stmt_id]),
            expr: Some(borrow_expr_id),
            safety_mode: thir::BlockSafety::Safe,
        };
        let block_id = self.body.thir.blocks.push(block);
        self.body.thir.exprs[expr_id].kind = ExprKind::Block { block: block_id };
    }
}
}