[Discussion] A perspective on super let: could a related “lift” capability ever belong in Rust’s macro system?
Thank you for the thoughtful reply!
I realize I should clarify my Racket analogy a bit. My core point isn't merely that "super let reminds me of macro lifting," but rather a structural observation: If Rust's macro system possessed a Racket-likelift primitive, super let wouldn't need to be a core language feature at all; it could simply be reinvented as macro-provided syntax.
1. The Racket Analogy Clarified
To see exactly how this relates to scoping, let’s look at what syntax-local-lift-expression actually does in Racket. The macro does not merely paste text outward. It asks the expander to evaluate an expression in an enclosing context and returns a fresh, hygienic identifier bound to it.
#lang racket
(require (for-syntax syntax/parse))
(define-syntax (compute-once stx)
(syntax-parse stx[(_ expr)
;; The expander hoists `expr` to the module's top level,
;; and returns a hygienic identifier for the local code to use.
(define lifted-id (syntax-local-lift-expression #'expr))
lifted-id]))
(define (local-function)
(displayln "Local function started.")
(define val (compute-once (begin (displayln "Heavy computation!") 42)))
(+ val val))
(local-function)
(local-function)
Output:
Heavy computation! <-- Lifted! Evaluated exactly once at the top level.
Local function started.
84
Local function started.
84
2. Translating to Rust: Reinventing super let
Today, Rust procedural macros are strictly local token-replacers (TokenStream -> TokenStream). The hypothetical capability I have in mind would be context-sensitive, looking closer to (TokenStream, &mut Context) -> TokenStream.
If a macro could tell the compiler: "Create a fresh hygienic binding for this expression in the enclosing placement/drop scope, then give me back the identifier," then super_let could be implemented purely as a library macro:
#[proc_macro]
pub fn super_let(input: TokenStream, cx: &mut Context) -> TokenStream {
let expr = parse_expr(input);
// Ask the expansion context to place this expression in the
// enclosing drop scope and return a hygienic identifier.
let lifted_id = cx.lift_expr(expr);
// Hand the output tokens back to the compiler using standard quotes.
// The macro locally uses the hygienic handle provided by the compiler.
quote! {
{
#lifted_id
}
}.into()
}
Then user code could write:
let writer = {
println!("opening file...");
let filename = "hello.txt";
Writer::new(&super_let!(File::create(filename).unwrap()))
};
writer.something(); // no error
3. Addressing the "Temporary Lifetime Rules" Complication
You rightly pointed out that Rust has very subtle mechanisms to determine the drop scope of temporary values, and mapping macro expressions to these rules could be complicated.
However, I believe this complication actually highlights exactly why a lift primitive is conceptually so powerful: it eliminates the need to interact with temporary extension rules altogether.
My perspective is that a reference should always point to a well-defined "place", and temporaries should be no exception. The current temporary lifetime extension rules are notoriously complex precisely because temporaries are "ghost" values. The compiler uses rigid syntactic heuristics to guess when to extend their lifetimes, which is exactly why things break down inside macros or function calls.
By lifting an expression, the expander physically generates a canonical let binding (e.g., let __hygienic_id = expr;) in the targeted drop scope.
The temporary ceases to be a temporary—it is transformed into a standard, orthodox local variable (a canonical "place"). Because of this, all lifetime and drop semantics become strictly canonical. The macro doesn't need to specify or micromanage how a temporary interacts with the extension rules, because the temporary has simply become a named local variable. The Borrow Checker then evaluates it using the most basic, universally understood rules of Rust.
The Design-Space Question
I am absolutely not claiming this exists today, nor that it would be trivial to implement. Breaking the pure-function TokenStream -> TokenStream model is a massive architectural shift for rustc.
But if such an operation did exist, the conceptual role of super let changes. It becomes just one possible built-in spelling of a much more fundamental metaprogramming operation: "place this value in an enclosing drop scope, while exposing only a hygienic local handle to it."
That is the design-space question I am exploring: Should this capability fundamentally belong only to the core language syntax, or is it, in principle, a macro-system capability, assuming Rust ever gained a controlled, hygienic way for macros to interact with their surrounding context?
Thanks again for engaging with this idea!
Discussion in the ATmosphere