Idea: Experimental attribute based support for partial borrows

My old thread on macro fn is the most complete writeup of what they'd entail, I believe. There are various levels of transparency that can be argued for — my original proposal basically wanted to permit _ in the function signature with semantics similar to C++20 abbreviated function templates with auto, meaning field-precise borrowing still wouldn't happen — but I still hold that they're a strong kernel of an idea that I'd like to see developed.

A key bit of functionality of macro fn to me is that unlike macro_rules!, it should instantiate an actual function once per signature; that in every way other than signature transparency it acts like any other function item. I'm looking at it both from the lens of "less constrained function" and "more constrained macro," but imo it should be more like fn than macro since bang macros still exist and work well for what they do. macro fn should be more than just what a carefully written functionlike macro can do, not just the same but easier.

I think the first implementation step would be one of:

  • $:value and/or $:place fragment matchers for macros, which accept the same syntax as $:expr but evaluate the expression before any of the macro's expansion (with any temporary lifetimes treating the macro as a single unit), and the fragment expansion naming the result of that expression evaluation as a value or as a place, respectively.
  • The ability to express and check field-precise borrows across function boundaries in MIR, without any (or with placeholder) surface language syntax.

The former isn't directly useful for macro fn, but it is directly related in that it makes well behaved bang macros more straightforward to write, helping to address a decent portion of the need which is satisfied with each invocation being separately inlined and reanalyzed at each call site. $:value for imitating functions, and $:place to permit partial place usage, including field-precise borrowing. The latter is necessary in order to express what a macro fn is after type inference information from the caller is provided to fill in any type holes, if macro fn is to permit field-precise borrowing.

Although I must say I'm not the most fond of either f(&place) or f(&mut place) at the call site potentially being field-precise borrows. It's a lot easier to accept for method call syntax, extending the existing place autoref to include field-precise partial borrowing of the receiver. But I'm a lot less fond of f(place) potentially receiving place by reference instead of moving (copying) from it. Leave that behavior to syntactically noted bang macros.

This reminds me of a proposal from a good while back for generalizing type ascription in function signatures, allowing you to replace the somewhat repetitive fn f(Newtype(x): Newtype<i32>) with instead just fn f(Newtype(x: i32)), or even leaving type ascription out entirely if the patterns uniquely constrain the argument type.

Tbh I don't know how well it would interact with default binding modes, since Newtype(x: &i32) could be a pattern for type Newtype<&i32> or &Newtype<i32>. Or how the unique fn(&self) sugar that isn't a pattern like other function arguments but some special third thing. And yet another difficulty of course being that in a named struct pattern Widget { a: b }, a is the field name and b is the pattern.

All this to say I'm not against fn f(Widget { &id, .. }) as a syntax for field-precise argument capture in function signatures by analogy to the &self sugar, but I am at least somewhat wary of it. fn f(Widget { &id, .. }: Widget) is thankfully invalid, needing to be written as fn f(Widget { id: &id, .. }: Widget) instead, but fn f(Widget { ref id, .. }: Widget) is valid. (Which honestly doesn't quite vibe with my lazy mental model for binding modes that wants & to switch from the by-ref binding mode to by-move in the same way ref switches from by-move to by-ref, even though I know it doesn't actually work like that[1][2].)


  1. "Fun" fact! Given struct S { a: &'static i32 } and a pattern scrutinee of type &mut S, a pattern of S { a: x } binds x: &mut &i32, S { a: ref x } binds x: &&i32, S { x: mut a } binds x: &i32, and S { a: &x } binds x: i32. If you peel back the curtain, they actually bind ref mut x: &i32, ref x: &i32, mut x: &i32, and &x: &i32, respectively, so there is straightforward predictable logic driving this, but match ergonomics is certainly relying heavily on deref coercion and never mixing in explicit binding modes for it's "just works" target. ↩︎

  2. That a &_ pattern switches the default binding mode back to by-move (but still only is allowed when "destructuring" a reference) whereas named type patterns project the binding mode almost feels like a bug, and my mental model would be happier if &_ universally switched out of a by-ref binding mode if present before attempting to destructure a reference scrutinee. I.e. that default binding modes worked more as if the subpattern scrutinized a temporary value of type &mut Field, instead of still scrutinizing a place of type Field and automatically applying the ref mut mode, but only if no explicit binding mode is used. I realize actually creating a temporary field reference to scrutinize would impact otherwise unscrutinized fields, which shouldn't actually happen, and it shouldn't be possible to bind to the "temporary" by-ref either, but the synthetic field-precise reference scrutinee illustrates how my mental model wants default binding modes to behave. ↩︎

3 Likes