Hoisting Expressions — Yosh Wuyts — Blog
Author<br>Yosh Wuyts
Published on<br>2026-06-25
Samples licensed as<br>Apache 2.0
Citation
Hoisting Expressions
Introduction<br>There is an RFC open on Rust which proposes what I’m calling hoisting<br>expressions into the language. These are expressions which can be introduced<br>inside of closures-only (for now), and are hoisted by the compiler to run<br>before the rest of the closure does. To illustrate how they work, consider this<br>example:
rust<br>print!("hello ");<br>hoist { print!("world!") };Copy<br>Even though in the code we declared "hello " first and "world!" second, this<br>will print: world!hello . In a way, you can think of hoist as the<br>inverse of defer:
defer in languages declare statements which are run after exiting the scope.
hoist in Rust would declare expressions which are run before entering the scope.
If you’re familiar with variable hoisting from JavaScript: this feature goes<br>beyond that, not just hoisting variable (var) declarations to the top of the<br>function, but hoisting the entire expression-evaluation to run before the rest<br>of the closure.
The proposal does not call it hoist<br>RFC 3968 proposes the addition of<br>move($expr), not hoist-expressions. But that’s only a syntactic difference; functionally it is identical. If I’m not mistaken this is an evolution of an earlier notation based on postfix .use. Here is our earlier example written using the experimental move-expressions feature on nightly (playground):
rust<br>#![allow(incomplete_features, unused)]<br>#![feature(move_expr)]
fn main() {<br>(|| {<br>print!("hello ");<br>move({ print!("world!")});<br>})();<br>}Copy<br>It’s not hard to see how you could add (|| move({ $expr }))(); to the inside of any function to be able to use move({ $expr }) anywhere. With some macro magic, we can even convert move($expr) to be written as hoist! { ... } instead:
rust<br>// Enables: `hoist! { println!("world!") };`<br>macro_rules! hoist { ($x:expr) => { move($x) };}Copy<br>Also on the semantics: the way move desugars is not actually to the<br>top of the closure, but instead to the outside of where the closure is initially created. That<br>means that if we zoom out a level, the earlier code actually looks like this:
rust<br>#![allow(incomplete_features, unused)]<br>#![feature(move_expr)]
fn main() {<br>print!("world!");<br>(|| {<br>print!("hello ");<br>})();<br>}Copy<br>That means that even if we were to never execute the closure, the hoisted<br>expression will still be unconditionally run. However from the perspective of the<br>closure body, all we need to know is that expressions which are defined later in<br>the body are now executed before1 the remainder of the<br>body .
1happens-before, anyone?
Arbitrary lookahead<br>My main worry here is that hoisting expressions change the evaluation<br>order away from the source order. Closures defer execution of something to a<br>later point in time. move($expr)-expressions travel backwards to<br>execute something before they were ever declared. And that’s a problem,<br>because that means we must know facts about code we haven’t even read yet to accurately reason about code we are evaluating in the present.
To illustrate this point, consider a 100-line closure. This takes a reference to a file and writes lines out to it. A question for the reader: what are the first three lines we’ve written to the file?
rust<br>let file = File::open("./my-file.md")?;<br>(|| {<br>write!(&file, "first")?;<br>write!(&file, "second")?;<br>write!(&file, "third")?;<br>// 97 more lines here<br>})();Copy<br>Once we introduce hoisting expressions, the only correct answer becomes: “I<br>cannot say, I would have to read the remaining 97 lines to be able to answer<br>that.” That means that to accurately answer questions you have to look ahead<br>and actually scan whether any non-local effects are being applied in the<br>remainder of the scope.
It’s hard to gauge how common this will be, and how much people would actually<br>end up abusing this in practice. Maybe Rust users will only use it to add the<br>odd in-line .clone() into their code and that’s that. Maybe Rust’s borrow<br>checking rules will prevent most bad cases, and this only becomes a hazard for<br>operations which rely on internal mutability or are inherently shared (like<br>files and stdio).
But if something can happen, with sufficient exposure it usually means it<br>eventually will happen. Internal mutability is readily available, and All it<br>takes is for people to start putting moderately complex things in hoisting<br>expressions and all of a sudden what I’m describing becomes a reality. Likely?<br>Hard to say. Possible? Definitely.
Defer is a library feature<br>In Rust the primitive for running items at the end of scopes are the Drop and Destruct traits, not a dedicated defer {} language feature. My operating theory is that a majority of the benefit of a hypothetical defer language feature could be captured by adding a library API that takes a closure which is run on drop at the end of the scope. That's why I started the process of adding std::mem::DropGuard last year. Using it looks like...