Yet Another named arguments prototype

Inspired by @Yoric's interesting experiments I set out to see how un-obtrusive and zero-cost I could make a procedural macro implementation. The best I got was a single attribute, and having to throw a ! in at each call-site.

I put together the named crate. Sample code:

use named::named;

#[named]
fn or(a: bool, b: bool) -> bool {
    a || b
}

fn main() {
    or!(a = true, b = false);
}

It also supports optional default values:

use named::named;

#[named(defaults(b = false))]
fn or(a: bool, b: bool) -> bool {
    a || b
}

fn main() {
    or!(a = true);

    or!(a = true, b = true);
}

The error messages are decent, too; see some trybuild test cases for examples.

I think this shows that such a feature could reasonably live outside the language. Interesting limitations we would want to overcome if we wanted to turn this into something more real:

  • No support for functions defined in impl blocks. Attribute macros aren't allowed on fns inside impl blocks. This could probably be worked around by having the attribute macro on the containing item, and a helper attribute on the function, but really, it would be nicer to not need to jump through those hoops. I'm not sure why this restriction is in place, I guess to avoid needing to provide surrounding context to the macro? Adding this support could probably help with other use-cases, too.
  • No support for functions which take receivers (i.e. self). Postfix macros could potentially help here.
  • Macros can't be defined as members of Structs - you can't define a macro such that Foo::macro!() can be called. This feels reasonably easily fixable if we wanted to.
  • Error messages are a bit limited in terms of context collection. To avoid a combinatorial explosion of match conditions on the macro, error messages if you give arguments in the wrong order, or give multiple unknown arguments, only mention the first problem they encounter. This is the only thing that pushes me towards thinking if we wanted to make this something real it should be done in the language, where more complex matching logic can be written. That said, there are probably more clever things this macro could do, or features we could introduce to the macro system to support this better, and also the error messages aren't terrible, just not as rich as they could be.

But in general, I dislike this interface less than I was expecting to...! I'd be interested to know what other people think.

3 Likes

That's awesome. Thanks for implementing it :grinning:

If I understand correctly, macros are resolved before type inference which unfortunately makes that difficult :frowning:

What type of macro do you generate?

I'm betting the macro impl block limitation has to do with generics, but I'm not sure.

No reason that $path::$ident!() can't desugar to $ident!($path); when $path doesn't name a module; macros are AFAIK resolved after some amount of HIR name resolution.

Ambiguous syntax for parsing.

In particular, we don't want parsing to in any way rely on semantics. In formal terms, we want the grammar to be Context-Free (modulo "mildly context sensitive" tokens, which iirc is just raw strings (and maybe nested block comments I forget the specifics off the top of my head)).

1 Like

I don't think this affects parsing. By the time we're in HIR and evaluating macros, we know that a macro is requesting its path:

macro_rules! {
  ($path:self_path) => {...}
}

use somewhere::else::baz;
foo::Bar::baz!(); // foo::Bar is not a module.
// Expanded as if it were:
::somewhere::else::baz!(foo::Bar);
actually::a::module::baz!(); // Expansion is as-is-written, even
                             // if `actually::a::module::baz`
                             // turns out to not be a macro.

This is about expansion and name-resolution after parsing. The production is still $path ! ( $tokens ) at the grammar level. Whether these semantics are reasonable is anyone's guess, but parsing is still context-free. Of course, if name resolution and macro expansion are not sequenced in the way I think they are, this doesn't work, but I'm pretty sure they have to be so that qualified macros work at all.

Macros are expanded before the desugaring to hir. Instead it happens before the full name resolution. Instead a name resolution is used that only considers macros.

It generates a macro_rules macro... Ideally it would generate a procedural function-like macro, but as far as I can tell these can only be created in crates which are marked proc-macro = true which is very restrictive.