rune-rs / rune

An embeddable dynamic programming language for Rust.
https://rune-rs.github.io
Apache License 2.0
1.66k stars 84 forks source link

Support attributes #43

Open udoprog opened 3 years ago

udoprog commented 3 years ago

Support rust style attributes;

#[hello]
fn test() {
}

#[hello]
struct Foo {
}

This will be invoked as macros with the ast of the thing that it annotates as input, and anything produced by the macro will be appended to the source (to work the same as Rust procedural macros).

Implementation note: If you need inspiration for how to structure the AST, look into Attribute in the syn project.

killercup commented 3 years ago

FYI, this is one of the main things I dislike in Rust's syntax because of the typing overhead and the fact that pretty much any tokens are valid in #[here]. Using @Foo(param=foo) (or #[Foo(param=foo)] if you like typing braces) and supporting exactly this style of calling a macro would reduce complexity quite a bit.

udoprog commented 3 years ago

FYI, this is one of the main things I dislike in Rust's syntax because of the typing overhead and the fact that pretty much any tokens are valid in #[here]. Using @Foo(param=foo) (or #[Foo(param=foo)] if you like typing braces) and supporting exactly this style of calling a macro would reduce complexity quite a bit.

I appreciate the point of view. But this is another instance when I want to maintain feature parity with Rust to make it possible to more easily transfer ideas. A note is that attributes are pretty much macro calls with different call syntax (and are associated with the AST they are tacked on to). You can see this in syn, with the Macro (macro call) and Attribute. They both take a Path and a raw TokenStream.

syn also provides a convenience function to parse its content as meta items. Something like this can be done to unobtrusively enforce more opinion. Or something like darling can be ported to Rune.

ModProg commented 1 year ago

Might look into this in the next few days, what should be the input of a function handling a macro? everything after the path? or should it only be the "input" as it is in rust: https://doc.rust-lang.org/reference/procedural-macros.html#attribute-macros i.e. if you call #[attribute(some_input)] the macro implementation would only get some_input and not (some_input).

udoprog commented 1 year ago

Doing it in the same way as Rust sounds good to me.

ModProg commented 1 year ago

this would mean, that a macro could not detect whether they got #[path(...)], #[path[...]], #[path={...} or #[path = ...] . Though I guess that should be fine, as it would be strange to have a different behavior depending on the exact syntax used to invoke (similar to how normal macros also don't differentiate`.

udoprog commented 1 year ago

I think so, all though I must admit I'm not sure what happens with the #[path = ..] variant 😅. I'm guessing .. is what's being fed in? The = is mentioned in the reference as part of meta syntax. Or is it not supported for proc-macro's?

ModProg commented 1 year ago

IIRC it is not supported for the actual macro, only for it's helper macros.

I guess this information could also just be added to the AttributeMacroContext. I.e. outer vs inner, what kind of delimiter was used etc., though I could also see an argument be made that this is just preference of the macro user, and the macro dev shouldn't try to enforce anything.

AFAICT we need to have a bunch of seperate types for the attribute macros anyway, as they e.g. in the AttributeMacroContext would need to have input_span and item_span instead of only stream_span.

As we have builtin ast which is different from rust's proc-macros, we could also have the attribute macro input be something like syn's https://docs.rs/syn/latest/syn/enum.Meta.html instead of a plain token_stream.

udoprog commented 1 year ago

As we have builtin ast which is different from rust's proc-macros, we could also have the attribute macro input be something like syn's https://docs.rs/syn/latest/syn/enum.Meta.html instead of a plain token_stream.

I'd be a bit careful with that. Even in Rust a meta item is only defined to the extent that it is used as a convention for most built-in macros. I frequently include things which is not valid meta type syntax in macros (types, patterns, bounds, ...). If a macro wants the meta syntax, they can opt to parse it out and there's really no difference in diagnostics.

If I'm being honest, I'd probably prefer specialized AST constructs derived in many cases instead of using generic containers such as syn::Meta (which you'd need to decompose / parse further anyway). Many attribute parsing needs can then be done by derives, like:

// Parses `= "<string>"` in an a `#[path]` attribute.
#[derive(Parse, ToTokens, Spanned)]
pub struct PathValue(T![=], LitStr);

The benefit here is that instead of having to match out syn::Meta::NameValue and match out its expression to a literal string or raise an error, that happens during the first round of parsing instead.

ModProg commented 1 year ago

I'd be a bit careful with that. Even in Rust a meta item is only defined to the extent that it is used as a convention for most built-in macros. I frequently include things which is not valid meta type syntax in macros (types, patterns, bounds, ...). If a macro wants the meta syntax, they can opt to parse it out and there's really no difference in diagnostics.

Actually in syn2, the MetaList variant contains unparsed tokenstreams, so it would only enforce that the top level macro matches meta which (at least in rust) is enforced by the syntax itself, but I agree that having the derived Parse is quite nice, and you would lose that when preparsed in some way.

Another place where this question arises is with the item, as an attribute macro can be applied only to valid items (and attributes themselves also to modules, statements and fields/params) the item could also be forwarded in it's parsed form, though the same argument could be applied here, that one could want to call ItemFn::parse directly instead of matching on the Item.

ModProg commented 1 year ago

One future question is, should macros be able to have helper attributes, like rust derive macros have? I.e. attributes that will be ignored when not in scope instead of raising an error.

In rust, only derive macros can have them, attribute macros need to clean them up themselves.

udoprog commented 1 year ago

Derive macros should definitely declare and have their helper attributes cleaned up by the compiler. The reason there would be that other than removing those attributes the original item remains untouched. Anything added is added as separate items after it. It can safely be punted on, since I don't think there's any particular infrastructure overlap in the compiler between the two, but I might be wrong! Unless you want to implement derives as well.

For attribute macros, do what Rust does basically 😄 (unless there's a really good reason). Which I think amounts to "follow a top to bottom expansion order, unless nested macros are present". But no helper attributes.