{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreidinyowfuzs4r7ioffy7nkudw555wu3357b7pz7tfviksoqy53o6a",
"uri": "at://did:plc:ivbknywyskln22er3nkssdhl/app.bsky.feed.post/3mke36ilzhre2"
},
"path": "/t/discussion-a-perspective-on-super-let-could-a-related-lift-capability-ever-belong-in-rust-s-macro-system/24193#post_3",
"publishedAt": "2026-04-25T21:35:54.000Z",
"site": "https://internals.rust-lang.org",
"textContent": "Thank you for the thoughtful reply!\n\nI 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-like`lift` primitive, `super let` wouldn't need to be a core language feature at all; it could simply be reinvented as macro-provided syntax.**\n\n### 1. The Racket Analogy Clarified\n\nTo 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.\n\n\n #lang racket\n (require (for-syntax syntax/parse))\n\n (define-syntax (compute-once stx)\n (syntax-parse stx[(_ expr)\n ;; The expander hoists `expr` to the module's top level,\n ;; and returns a hygienic identifier for the local code to use.\n (define lifted-id (syntax-local-lift-expression #'expr))\n lifted-id]))\n\n (define (local-function)\n (displayln \"Local function started.\")\n (define val (compute-once (begin (displayln \"Heavy computation!\") 42)))\n (+ val val))\n\n (local-function)\n (local-function)\n\n\n**Output:**\n\n\n Heavy computation! <-- Lifted! Evaluated exactly once at the top level.\n Local function started.\n 84\n Local function started.\n 84\n\n\n### 2. Translating to Rust: Reinventing `super let`\n\nToday, 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`.\n\nIf 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:\n\n\n #[proc_macro]\n pub fn super_let(input: TokenStream, cx: &mut Context) -> TokenStream {\n let expr = parse_expr(input);\n\n // Ask the expansion context to place this expression in the\n // enclosing drop scope and return a hygienic identifier.\n let lifted_id = cx.lift_expr(expr);\n\n // Hand the output tokens back to the compiler using standard quotes.\n // The macro locally uses the hygienic handle provided by the compiler.\n quote! {\n {\n #lifted_id\n }\n }.into()\n }\n\n\nThen user code could write:\n\n\n let writer = {\n println!(\"opening file...\");\n let filename = \"hello.txt\";\n\n Writer::new(&super_let!(File::create(filename).unwrap()))\n };\n\n writer.something(); // no error\n\n\n### 3. Addressing the \"Temporary Lifetime Rules\" Complication\n\nYou 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.\n\nHowever, 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.**\n\nMy 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.\n\nBy lifting an expression, the expander physically generates a canonical `let` binding (e.g., `let __hygienic_id = expr;`) in the targeted drop scope.\n\nThe 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.\n\n### The Design-Space Question\n\nI 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`.\n\nBut 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.\"**_\n\nThat 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?\n\nThanks again for engaging with this idea!",
"title": "[Discussion] A perspective on super let: could a related “lift” capability ever belong in Rust’s macro system?"
}