Hi everyone,
As I’ve been closely following the exciting progress of #139076 (super let), I want to share a perspective inspired by the macro systems of Lisp/Racket. While super let beautifully solves the temporary lifetime and hygiene issues, I’ve been wondering if this capability could—or should—be generalized as a feature of the Rust macro system itself.
1. The Core Observation: super let is essentially a hygienic AST Lift
If we look strictly at the Borrow Checker rules, super let doesn't actually introduce new lifetime semantics. It essentially performs two tasks:
-
Relocation (Drop Scope) : It hoists the variable declaration to the outer scope to extend its extent/survival time.
-
Hygiene (Lexical Scope) : It locks the visibility of the variable strictly to the inner block, preventing external access.
In other words, super let is a mechanism to "hygienically leak a variable's drop scope to the outside without leaking its lexical visibility."
This is also why it seems closely related to temporary lifetime extension: in that case, the compiler already performs a limited, implicit version of this idea for certain syntactic forms, effectively placing an intermediate temporary in a scope where the resulting borrow can remain valid. From this perspective, super let looks like a way to make that kind of placement explicit and programmable, while preserving hygiene.
2. The Racket Inspiration: syntax-local-lift-expression
In the Racket/Scheme macro ecosystem, this exact problem is solved without introducing core language keywords. Instead, the macro system is empowered with a lift capability (e.g., syntax-local-lift-expression).
When a macro in Racket needs to generate an expression that should be evaluated/stored in an outer context (while keeping its identifier perfectly hygienic locally), the macro "lifts" it. The macro expander physically moves the declaration to the outer scope, but leaves behind a hygienic identifier for the local code to use.
3. What if Rust Macros had lift capabilities?
If Rust's macro system (e.g., Proc-macros) were extended to support lift, macros could natively solve the temporary lifetime issues. For example, a macro like pin!() or format_args!() could use a hypothetical TokenStream::lift() API. The macro would tell the compiler: "Please place this generated temporary variable in the parent block's scope, but only give me back the hygienic identifier to use in this local expansion."
Potential advantages of this approach:
-
More Powerful Metaprogramming : Macros could conditionally lift various constructs, offering developers more flexibility without being limited to let bindings.
-
Keeping Core Language Smaller : It keeps the complexity inside the metaprogramming domain rather than adding a new feature (super) to the core language. After all, the temporary lifetime issue is mostly felt when authoring or using macros. In ordinary day-to-day code, I think this issue is often not catastrophic.
4. The Trade-offs
I understand that Rust and Racket have fundamentally different architectures. I can see a few reasons why super let might be preferred over macro lift:
-
In-place Expansion : Rust macros currently operate on strict in-place token replacement. Modifying the AST outside the macro's invocation span would require a massive architectural shift in the compiler's expansion engine.
-
Everyday Ergonomics : super let isn't just for macro authors. It dramatically improves the ergonomics for regular developers facing "delayed initialization" patterns in daily coding.
-
Explicitness : Rust values explicit control flow. super let makes the extend ed drop scope visibly explicit to the reader, whereas a macro lift would hide this side-effect.
5. Summary and Questions
My current intuition is that super let is best understood not as “lifetime magic,” but as an explicit, hygienic variable-placement mechanism. In that sense, it feels closely connected to temporary lifetime extension, except that it turns an implicit and syntax-restricted behavior into something explicit and controllable.
That in turn makes me wonder whether super let might be viewed as one specialized language construct for a more general capability: hygienically placing a generated binding in an outer drop scope while keeping its lexical visibility local.
So I’d be very interested to hear the lang/compiler team’s thoughts on a few questions:
- Was a macro-level “lift” capability ever considered during the early design discussions around this space?
- Is there a fundamental reason this kind of outward placement should remain a core-language feature rather than something macros could also express?
- Even if
super letis the right design for Rust, could a related lift-like capability ever make sense for future macro evolution?
Looking forward to hearing your thoughts!