Postfix 宏和 Let 位置
Postfix Macros and Let Place

原始链接: https://nadrieril.github.io/blog/2025/12/09/postfix-macros-and-let-place.html

这篇帖子探讨了一种在 Rust 中启用后缀宏(如 `x.method!()`)的解决方案,而不会违反“无回溯规则”——防止宏在执行过程中重新评估表达式。核心思想是**“部分求值”**:与其将*表达式*传递给后缀宏,不如传递一个*位置*——代表表达式求值结果的内存位置。 直接将表达式存储在临时变量中会因为不正确的位置到值的转换而失败。作者提出了一种新的语言特性,**`let place p = ;`**,它将 `` 作为位置进行求值,并为其创建别名 `p`,避免不必要的复制或移动。这使得后缀宏可以直接操作该位置,确保副作用可预测地发生。 虽然 `let place` 引入了诸如推断可变性以进行自动解引之类的复杂性,但它出奇地简单,并且不需要更改 Rust 的中间表示 (MIR)。作者甚至建议 `let place` 可以简化模式匹配并解释某些借用检查行为。最终,该提案旨在实现表达力强的后缀宏,并提出 `let place` 即使独立来看也是一项有价值的特性。

Hacker News 新闻 | 过去 | 评论 | 提问 | 展示 | 工作 | 提交 登录 Postfix 宏和 Let Place (nadrieril.github.io) 9 分,by todsacerdoti 1 天前 | 隐藏 | 过去 | 收藏 | 3 评论 kazinator 1 天前 | 下一个 [–] > let place 是一个我看到的想法,在 TXR Lisp (placelet) https://www.nongnu.org/txr/txr-manpage.html#S-9A89A6B8Emacs Lisp (gv-letplace) https://www.gnu.org/software/emacs/manual/html_node/elisp/Ad... 回复 mmastrac 1 天前 | 上一个 | 下一个 [–] 为什么需要新的语法?我们可以有: let &p = &x.field; 或者 let &mut p = &mut x.field; 回复 rilindo 1 天前 | 上一个 [–] 这感觉很像 postfix 的 sendmail 虚构故事。我不知道这是否是好事。编辑:实际上不是,但仍然感觉像是添加了不必要的回复 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

Postfix macros is the feature proposal that would allow something.macro!(x, y, z). It’s been stalled for a long time on some design issues; in this blog post I’m exploring an idea that could answer these issues.

The obvious way to make the feature work is to say that in <expr>.macro!(), the macro gets the tokens for <expr> and does what it wants with them.

This however allows macros to break the so-called “no-backtracking rule” (coined by Tyler Mandry IIRC): in x.is_some().while! { ... }, reading the while makes us realize that the is_some() call wasn’t just a boolean value, it was an expression to be evaluated every loop. So we sort of have to go back and re-read the beginning of the line. For purposes of reducing surprise and code legibility, we’d like to avoid that.

Hence the question that the feature stalled on: can we design postfix macros that always respect the no-backtracking rule? We would need to somehow evaluate <expr> once and pass the result to the macro instead of passing <expr> itself. Apart from that I’ll assume that we want maximal expressiveness.

This post is centrally about places and the implicit operations that surround them; check out my recent blog post on the topic for an overview of that vocabulary.

Partial Place Evaluation

To get the obvious out of the way: we can’t just desugar <expr>.method() to let x = <expr>; x.method(); that may give entirely the wrong behavior, e.g.:

struct Foo { count: Option<u32> }
impl Foo {
    fn take_count(&mut self) -> Option<u32> {
        // That's fine
        self.count.take()
        // That creates a copy
        // let tmp = self.count;
        // tmp.take() // modifies the copy instead of the original
    }
}

In technical terms, that’s because the LHS of a method call is a place expression. Storing <expr> in a temporary adds an incorrect place-to-value coercion. The same applies to postfix macros.

I think that the behavior we ideally want is to pre-evaluate all temporaries (that arise from value-to-place coercion), and pass whatever remains of the expression as-is to the macro. I’ll call that “partial place evaluation”.

Some examples:

let x: Foo = ...;
x.field.macro!()
// becomes (there are no temporaries)
macro!(x.field)

impl .. { fn method(&self) -> Foo { .. } }
x.method().field.macro!()
// becomes
let mut tmp = x.method();
macro!(tmp.field)

Looks easy enough, except for autoderef.

let x: Box<Foo> = ...;
x.field.macro!()

Depending on the contents of macro!(), this may need to expand to a call to deref or deref_mut:

let tmp = Box::deref(&x);
macro!((*tmp).field)
// or
let tmp = Box::deref_mut(&mut x);
macro!((*tmp).field)

At this point it’s hopefully clear that no simple syntactic transformation will give us what we want.

Place aliases, aka let place

What we’re trying to express is “compute a place once and use it many times”. let place is an idea I’ve seen floating around which expresses exactly that: let place p = <expr>; causes <expr> to be evaluated as a place, and then p to become an alias for the place in question. In particular, this does not cause a place-to-value coercion.

let place p = x.field; // no place-to-value, so this does not try to move out of the place
something(&p);
something_else(p); // now this moves out
// would be identical to:
something(&x.field);
something_else(x.field); // now this moves out

let place p = x.method().field;
something(&p);
// would be identical to:
let tmp = x.method();
something(&tmp.field);

This is exactly what we need for postfix macros: <expr>.macro!() would become (using a match to make the temporary lifetimes work as they should 🤞):

match <expr> {
    place p => macro!(p),
}

This would have the effect I propose above: any side-effects are evaluated early, and then we can do what we want with the resulting place.

One of my litmus tests of expressivity for postfix macros is this write! macro, which ends up working pretty straighforwardly:

macro_rules! write {
    ($self:self, $val:expr) => ({
        $self = $val; // assign to the place
        &mut $self // borrow it mutably
    })
}
let mut x; // borrowck understands that `write!` initializes the place!
let _ = x.write!(Some(42)).take();
// desugars to:
let _ = match x {
    place p => write!(p, Some(42)).take(),
};
// desugars to:
let _ = write!(x, Some(42)).take();
// desugars to:
let _ = {
    x = Some(42);
    (&mut x).take()
};

let place and custom autoderef

The hard question is still autoderef :

let mut x: Box<Foo> = ...;
let place p = x.field; // should this use `deref` or `deref_mut`?
something(&p);
something_else(&mut p); // causes `deref_mut` to be called above

For that to work, we infer for each place alias whether it is used by-ref, by-ref-mut or by-move (like closure captures I think), and propagate this information to its declaration so that we can know which Deref variant to call .

let place isn’t too powerful

Turns out let place is a rather simple feature when we play with it:

// Place aliases can't be reassigned:
let place p = x.field;
// Warning, this assigns to `x.field` here! that's what we want place aliases to do
// but it's admittedly surprising.
p = x.other_field;

// You can't end the scope of a place alias by hand:
let place p = x.field;
drop(p); // oops you moved out of `x.field`
// `p` is still usable here, e.g. you can assign to it

// Place aliases can't be conditional.
let place p = if foo() { // value-to-place happens at the assignment
    x.field // place-to-value happens here
} else {
    x.other_field
};
// This won't mutate either of the fields, `p` is fresh from a value-to-place coercion. I propose
// that this should just be an error to avoid sadness.
do_something(&mut p);

In particular it’s easy to statically know what each place alias is an alias for.

The caveat is that all of those are surprising if you think of p as a variable. This is definitely not a beginners feature.

let place doesn’t need to exist in MIR

The big question that let place raises is what this even means in the operational semantics of Rust. Do we need a new notion of “place alias” in MiniRust?

I think not. The reason is that the “store intermediate values in temporaries” happens automatically when we lower to MIR. All place coercions and such are explicit, and MIR place expressions do not cause side-effects. So whenever we lower a let place p to MIR, we can record what mir::Place p stands for and substitute it wherever it’s used.

To ensure that the original place doesn’t get used while the alias is live, we insert a fake borrow where the let place is taken and fake reads when it’s referenced. That’s already a trick we use in MIR lowering for exactly this purpose.

So the only difficulty seems to be the mutability inference mentioned in previous section. The rest of typechecking let place is straighforward: let place p = <expr>; makes a place with the same type as <expr>, and then it behaves pretty much like a local variable.

All in all this is looking like a much simpler feature that I expected when I started playing with it.

let place is fun

I kinda of want it just because it’s cute. It makes explicit something implicit in a rather elegant way. Here are some fun things I discovered about it.

To start with, it kind of subsumes binding modes in patterns: if let Some(ref x) = ... is the same thing as if let Some(place p) = ... && let x = &p. One could even use place x instead of x in patterns everywhere and let autoref set the right binding mode! That’s a funky alternative to match ergonomics.

We can also use it to explain this one weird corner case of borrow-checking. This code is rejected by the borrow-checker, can you tell why?

let mut x: &[_] = &[[0, 1]];
let y: &[_] = &[];
let _ = x[0][{x = y; 1}];
//      ^^^^ value is immutable in indexing expression

What’s happening is that we do the first bound-check on x before we evaluate the second index expression. So we can’t have that expression invalidate the bound-check on pain of UB. We can use let place to explain the situation via a desugaring:

x[0][{x = y; 1}]
// desugars to:
let place p = x[0]; // bounds check happens here
p[{x = y; 1}]
// desugars to:
let place p = x[0];
let index = {x = y; 1}; // x modified here
p[index] // but place alias used again here

Can this be used to explain closure captures? I don’t think so because closures really do carry borrows of places, not just places. It does feel like a related kind of magic though.

Conclusion

I started out writing this blog post not knowing where it would lead, and I’m stoked of how clean this proposal ended up looking. I kinda want let place even independently from postfix macros. The one weird thing about let place is this “mutability inference” for autoderef, hopefully that’s an acceptable complication.

I’m most looking forward to everyone’s feedback on this; let place is rather fresh and I wanna know if I missed anything important (or anything fun!).

联系我们 contact @ memedata.com