Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upRFC: Eager Macro Expansion #2320
Conversation
pierzchalski
referenced this pull request
Feb 3, 2018
Open
Tracking issue: declarative macros 2.0 #39412
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Feb 3, 2018
|
So the idea would be to implement the |
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Feb 3, 2018
|
@pierzchalski Incidentally, you probably want to CC/assign @jseyfried to this PR. |
This comment has been minimized.
This comment has been minimized.
|
@alexreg Whoops! Done. |
alexreg
referenced this pull request
Feb 4, 2018
Closed
Hygiene opt-out for idents in expansion of declarative macros #47992
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Feb 4, 2018
|
On second thought, maybe better to CC @petrochenkov given @jseyfried's long-term absence? |
This comment has been minimized.
This comment has been minimized.
Sorry, can't say anything useful here, I haven't written a single procedural macro in my life and didn't touch their implementation in the compiler either. |
This comment has been minimized.
This comment has been minimized.
|
This is a language/compiler RFC so I guess @nikomatsakis and @nrc are two other people to CC, anyone else who would be interested? |
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Feb 5, 2018
•
|
@petrochenkov Oh, sorry. I gathered from your comments on the declarative macros 2.0 RFC that you knew something of the macros system in general. My bad. |
Centril
reviewed
Feb 5, 2018
|
|
||
| * Greatly increases the potential for hairy interactions between macro calls. This opens up more of the implementation to be buggy (that is, by restricting how macros can be expanded, we might keep implementation complexity in check). | ||
|
|
||
| * Relies on proc macros being in a separate crate, as discussed in the reference level explanation [above](#reference-level-explanation). This makes it harder to implement any future plans of letting proc macros be defined and used in the same crate. |
This comment has been minimized.
This comment has been minimized.
Centril
Feb 5, 2018
Contributor
I'd like to highlight this drawback. Are the gains in this RFC enough to outweigh this drawback?
This comment has been minimized.
This comment has been minimized.
alexreg
Feb 5, 2018
Indeed, why does it require a separate crate for proc macros? Can you elaborate?
This comment has been minimized.
This comment has been minimized.
pierzchalski
Feb 5, 2018
Author
Thinking about it more, this expansion API doesn't add any extra constraints to where a proc macro can be defined, so I guess this shouldn't really be here.
Originally I was worried about macro name resolution (I thought having proc macros in a separate crate at the call site would make that easier but given that there are other issues involving macro paths this seems redundant to worry about), and collecting definitions in an 'executable' form.
Declarative macros can basically be run immediately after they're parsed because they're all compositions of pre-existing built-in purely-syntactic compiler magic. Same-crate procedural macros would need to be 'pre-compiled' like they're tiny little inline build.rss scattered throughout your code. I thought this would interact poorly in situations line this:
#[macro_use]
extern crate some_crate;
#[proc_macro]
fn my_proc_macro(ts: TokenStream) -> TokenStream { ... }
fn main() {
some_crate::a_macro!(my_proc_macro!(foo));
}How does some_crate::a_macro! know how to expand my_proc_macro!?
In hindsight, this is just a roundabout way of hitting an existing problem with same-crate proc macros:
// Not a proc-macro.
fn helper(ts: TokenStream) -> TokenStream { ... }
#[proc_macro]
fn a_macro(ts: TokenStream) -> TokenStream {
let helped_ts = helper(ts);
...
}
fn main() {
a_macro!(foo);
}Same question: how does a_macro! know how to evaluate helper? I think whatever answer we find there will translate to this macro expansion problem.
Anyway, I'm now slightly more confident that that particular drawback isn't introduced by this RFC. Should I remove it?
This comment has been minimized.
This comment has been minimized.
alexreg
Feb 5, 2018
Yeah, I'd tend to agree with that assessment. Is there an RFC open for same-crate proc macros currently? If so, I'd be curious to read it over.
This comment has been minimized.
This comment has been minimized.
pierzchalski
Feb 6, 2018
Author
I remember reading some fleeting comments about it, but I just had a quick look around and I can't find anything about plans for it.
This comment has been minimized.
This comment has been minimized.
Centril
Feb 6, 2018
Contributor
I'm no expert wrt. proc macros.. I'd also be interested in any resources wrt. same-crate macros.
Thanks for the detailed review and changes =)
This comment has been minimized.
This comment has been minimized.
alexreg
Feb 6, 2018
@pierzchalski On a related note, my WIP PR can be found here: rust-lang/rust#47992 (comment). I'm going to make another big commit & push in an hour I think.
pierzchalski
changed the title
Add macro expansion API to proc macros
RFC: Add macro expansion API to proc macros
Feb 5, 2018
sgrif
added
the
T-lang
label
Feb 8, 2018
jseyfried
reviewed
Feb 9, 2018
|
|
||
| Built-in macros already look more and more like proc macros (or at the very least could be massaged into acting like them), and so they can also be added to the definition map. | ||
|
|
||
| Since proc macros and `macro` definitions are relative-path-addressable, the proc macro call context needs to keep track of what the path was at the call site. I'm not sure if this information is available at expansion time, but are there any issues getting it? |
This comment has been minimized.
This comment has been minimized.
jseyfried
Feb 9, 2018
•
Yeah, this information is available at expansion time. Resolving the macro shouldn't be a problem.
This comment has been minimized.
This comment has been minimized.
|
I just realised that one of the motivations for this feature (the #[proc_macro]
fn lift(ts: TokenStream) -> TokenStream {
let mut mac_c = ...;
mac_c.call_from(...);
// ^^^
// This needs to be the span/scope/context of, in this
// example, `main`: the caller of `m`, which is the caller of `lift!`.
...
}
macro m() {
lift!(m_helper!()); // Should set the caller context of `m_helper!` to
// caller context of `m!`.
}
fn main() {
m!();
}But the current |
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Feb 9, 2018
•
|
@pierzchalski Yeah, it looks like either we'd have to bake this |
pietroalbini
referenced this pull request
Feb 20, 2018
Open
Macro in path attribute on module results in file not found error #48250
llogiq
referenced this pull request
Mar 9, 2018
Open
Procedural Macros expanding macros by example vs. __rust_unstable_column() #48781
This comment has been minimized.
This comment has been minimized.
|
Good job! I've wanted a solution for this for some time. I see but two possible problem with the solution this RFC PR suggests:
|
This comment has been minimized.
This comment has been minimized.
|
@llogiq sorry for the late reply! I'm not sure what point you're trying to make in (1) - if I change the order of two macro calls, I don't really expect the same result in general, similar to if I change the order of two function calls. Do you have a concrete example of a proc macro which wants to ignore/pass-through macro nodes but which also cares if an expression comes from a macro expansion? Also re. (1), I'm not overly familiar with the expansion process but as far as I understand and recall, the current setup is recursive fixpoint expansion, which makes it hard to have cleanly delineated pre- and post-expansion phases for macros to register themselves for. Can you clarify how these would work in that context? Regarding (2), one dodgy solution is to have the macro expansion utility functions be internals-aware by having a blacklist of "do not expand" macros, but that's pretty close to outright stabilising them. |
This comment has been minimized.
This comment has been minimized.
|
To answer (2), in mutagen, I'd like to avoid mutating I'm OK with getting the resulting code if I also get expansion info, and also get a way of expanding macros so I can look into them. |
This comment has been minimized.
This comment has been minimized.
|
So I don't know what changes @jseyfried is making to how contexts and scopes are handled, but I agree that sounds like the right place to put this information (about how a particular token was created or expanded). Putting it in spans definitely sounds more workable than trying to wrangle invocations to guarantee you see things pre- or post-expansion, but it also means doing a lot more design work to identify what information you need and in what form. |
This comment has been minimized.
This comment has been minimized.
|
One thing I think we need is a way for proc macros to mark what they changed (and for |
nrc
self-assigned this
Apr 30, 2018
This comment has been minimized.
This comment has been minimized.
iiuc, |
This comment has been minimized.
This comment has been minimized.
|
re compiler internals and inspection, I would expect that the results of expansion would be a TokenStream and that could be inspected to see what macro was expanded (one could also inspect the macro before expansion to get some details too). I would expect that 'stability hygiene' would handle access to compiler internals, and that the implementation of that would not allow macro authors to arbitrarily apply that tokens. |
This comment has been minimized.
This comment has been minimized.
|
Thanks for this RFC @pierzchalski! I agree that this is definitely a facility we want to provide for macro authors. My primary concern is that this is a surprisingly complex feature and it might be better to try and handle a more minimal version as a first iteration. It might be a good idea to try and avoid any hygiene stuff in a first pass (but keep the API future-compatible in this direction), that would work well with the macros 1.2 work. It is worth considering how to handle expansion order (although it might be worth just making sure we are future-compatible, rather than spec'ing this completely). Consider the following macros uses:
If Then consider a macro that wants to expand two macros where one is defined by the other - it might be nice if the macro could try different expansion orders. I think all that is needed is for the compiler to tell the macro why expansion failed - is it due to a failed name lookup, or something going wrong during the actual expansion stage. Which brings to mind another possible problem - what happens if the macro we're expanding panics? Should that be caught by the compiler or the macro requesting expansion? |
This comment has been minimized.
This comment has been minimized.
|
Is there prior art for this? What do the Scheme APIs for this look like? |
nrc
reviewed
May 2, 2018
| The full API provided by `proc_macro` and used by `syn` is more flexible than suggested by the use of `parse_expand` and `parse_meta_expand` above. To begin, `proc_macro` defines a struct, `MacroCall`, with the following interface: | ||
|
|
||
| ```rust | ||
| struct MacroCall {...}; |
This comment has been minimized.
This comment has been minimized.
nrc
May 2, 2018
Member
Without getting too deep into a bikeshed, I think something like ExpansionBuilder would be a better name
| fn new_attr(path: TokenStream, args: TokenStream, body: TokenStream) -> Self; | ||
| fn call_from(self, from: Span) -> Self; |
This comment has been minimized.
This comment has been minimized.
| fn call_from(self, from: Span) -> Self; | ||
| fn expand(self) -> Result<TokenStream, Diagnostic>; |
This comment has been minimized.
This comment has been minimized.
nrc
May 2, 2018
Member
The error type should probably be an enum of different ways things can go wrong, and where there are compile errors we probably want a Vec of Diagnostics, rather than just one.
| ``` | ||
|
|
||
| The functions `new_proc` and `new_attr` create a procedural macro call and an attribute macro call, respectively. Both expect `path` to parse as a [path](https://docs.rs/syn/0.12/syn/struct.Path.html) like `println` or `::std::println`. The scope of the spans of `path` are used to resolve the macro definition. This is unlikely to work unless all the tokens have the same scope. | ||
|
|
This comment has been minimized.
This comment has been minimized.
nrc
May 2, 2018
Member
Overall, I really like the idea of using a Builder API - it keeps things simple and is future-proof
pierzchalski
added some commits
Mar 9, 2019
This comment has been minimized.
This comment has been minimized.
|
Alright, after some long and fruitful discussions with @alexreg we've got an updated RFC (no longer an eRFC) that focuses on and justifies what we think is the "least exciting" proposal that sill gets us useful eager expansion. |
pierzchalski
changed the title
eRFC: Macro Expansion for Macro Input
RFC: Macro Expansion for Macro Input
Mar 10, 2019
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Mar 10, 2019
|
Indeed, @pierzchalski has worked long and hard over this proposal, and having reviewed some drafts and had some nice discussions with him about this, we've settled on what we think is a reasonably conservative but still powerful extension to the language in terms of eager expansion. Why this RFC does not of course go into implementation details, I think the idea is that this can be implemented mainly within the @pnkfelix Would you mind taking a look and FCPing this for merge, if you're reasonably happy with the state of things? |
This comment has been minimized.
This comment has been minimized.
|
I've in the meantime had discussions with @matklad who leads the rust-analyzer effort and he prefers a less powerful approach (basically just mark proc_macros with an attribute so they get their arguments already expanded) that is easier to implement and more cache-friendly. Perhaps we should use that proposal as a stepping stone to a more full-fledged implementation? (Caveat: I'm just a proc_macro author who wants expanded macros somehow) |
pierzchalski
changed the title
RFC: Macro Expansion for Macro Input
RFC: Eager Macro Expansion
Mar 10, 2019
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Mar 10, 2019
|
@llogiq This is already quite toned-down I feel, and has several notable advantages over that proposal, above all the ability to have eager expansion in declarative macros, apart from greater flexibility. A lot of thought and planning has gone into this already, and several iterations (including inspiration from nrc's prior work). I personally think what we have here represents the best current state of work on the problem, though I of course encourage comments and feedback (now or at FCP stage), and I would imagine @pierzchalski would appreciate that too. |
This comment has been minimized.
This comment has been minimized.
|
On an entirely separate note, I'm currently hunting for prior art for Rust's 'macros 2.0' expansion order (there's plenty of prior art for the hygiene system, and from the right viewpoint Racket looks similar to macros 2.0, but I'm having trouble finding work on eager expansion). There's some discussion on the subreddit, if anyone wants to contribute there or here. |
pierzchalski
force-pushed the
pierzchalski:macro-expansion-api
branch
from
e4f3d04
to
0669405
Mar 12, 2019
pierzchalski
force-pushed the
pierzchalski:macro-expansion-api
branch
from
0669405
to
3dd9545
Mar 12, 2019
This comment has been minimized.
This comment has been minimized.
samth
commented
Mar 13, 2019
•
|
@pierzchalski (I think) asked me to comment on Reddit, so here are some thoughts as someone who's thought deeply about macros but is an outsider to Rust. (I also talked to @nikomatsakis about this a long time ago but I don't think I had as coherent thoughts then.) Here's what I think the motivation is, phrased in a less-Rust-specific way, mostly to help me understand/talk about it.
I would add some other use cases:
How do people solve these problems in other languages with macrosHere I'll mostly describe Racket, which has not only the fanciest macro system but is what I know best. Computing argumentsI think there are two main solutions here. One is just write another macro that expands into the macro call you want. That is, for the Here's an example macro showing how I'd do Adding computation to rewriting-only macrosI think this is mostly a bad idea. The underlying problem is usually that procedural macros are too hard/complex/unwieldy, and that should be fixed directly. In Racket, the transition is very smooth and Partial re-useThis doesn’t seem like a good idea. Analyzing code with macros in itThis is done in Racket by fully expanding the relevant form using the Cooperating macrosThis is the use Having just covered the motivation section, I feel like I should stop here and put my thoughts on the actual proposal in the RFC in a separate comment. Hopefully this is helpful for someone. :) |
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Mar 13, 2019
|
@samth Thanks for sharing your perspective as a Racket developer. Thoughts on this specific RFC would also be appreciated, if you're reasonably familiar with the Rust macro system, but that's already useful for @pierzchalski to maybe amend the Prior Art section with, I think. :-) |
This comment has been minimized.
This comment has been minimized.
samth
commented
Mar 13, 2019
•
|
Ok, some thoughts on the rest of the RFC. First, another useful piece of related work is the concept of continuation-passing-style macros, described here by Oleg Kiselyov with application to exactly this problem. I also added a link to an example of how I'd do
|
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Mar 14, 2019
|
Thanks a lot for your feedback, @samth.
I don't quite see how there are really any special considerations about hygiene beyond what we already have with the Rust hygiene system (which uses set-of-scopes along with macro expansion "marks", as you may know). Well, that's not 100% true. The one thing that we should probably note is that whenever an eager expansion is performed (i.e., the macro expansion routine is invoked for a metavariable like eager! {
#a = concat!("foo", "_", "b");
let #a = 42;
}then the hygiene of all the tokens in the output (
The thing is, a major use case for this is being able to write ident-position macros, hence the declarative
That may help explain; I'm not sure. We had a fair bit of discussion on how best to express this, and I was reasonably happy with how it ended up, but admittedly I do think of things in a slightly different way myself. Now, when it comes to "modifying existing definitions", I'm personally not a big fan of that terminology, even though @pierzchalski somewhat likes it. Personally I don't like to think of macros as "modifying" their token streams, but rather as functions on token streams -- black boxes, even, where there can be anything from an identity relation between (single-argument) input and output, to complete disconnect. |
This comment has been minimized.
This comment has been minimized.
This isn't strictly true. There's nothing stopping us from coming at the 'ident-position macros' problem with a procedural macro (for instance, we could implement something like The reason why I chose to focus on a declarative macro API is that I think declarative expansion semantics are better understood in Rust and easier to extend cleanly. In fact, for my original use-case (expanding arguments to attribute macros), @samth, thanks for the feedback!
Ah, in a previous draft of the RFC I referred to CPS at the start of this section, but ended up removing the term in favour of walking through an example (in fact, CPS was a pretty direct inspiration for
I'm afraid I don't understand. What part of the description of |
This comment has been minimized.
This comment has been minimized.
|
In my previous comment, I was about to say something along the lines of " cps!(foo!(<args to foo>); bar);Would expand into this: bar!(<result of expanding `foo!(<args to foo>)`>);This sidesteps all the issues that In this light, one compromise would be to restrict This idea essentially 'punts' the expansion order question by making it a library problem. I'm not sure how I feel about that. Keeping expansion order under the control of the compiler means we get to say "there's no expansion order that you can rely on"; the alternative is an ecosystem of eager macros that occasionally do depend on expansion order, which sounds unappealingly fragile. Edit: yeah, making expansion order a library choice is worse than I thought. For example: how does a library figure out the expansion order in the following? foo!();
id(mk_macro!(foo)); |
This comment has been minimized.
This comment has been minimized.
|
Regarding the examples in appendices B and C:
Unfortunately, this narrows down the 'interesting' expansion order cases to the admittedly hard-to-motivate examples in the appendices. Fortunately, this is a pretty niche problem! Given the various hoops you have to go through to get to this point (you need your identifier to have the 'correct' hygiene, and you need nested eager macros that provide different definitions for that identifier as they expand), we could probably get away with ignoring the issue for a long time. |
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Mar 14, 2019
My point was it can be used in declarative macro code (and not just as a function within procedural macro libraries). I think there's a bit of confusion around terminology here. Sure, we could implement it like that, and we still might. The RFC doesn't dictate implementation. |
This comment has been minimized.
This comment has been minimized.
samth
commented
Mar 14, 2019
|
A few responses:
|
This comment has been minimized.
This comment has been minimized.
As far as I'm aware, both types of declarative macros (ones defined with One thing that might clear up the point of the B and C appendices: from one point of view, they're about hygiene. Each eager expansion introduces a new scope (the 'expansion contexts' mentioned here) in which discovered definitions are made available; we modify the hygiene resolution rules so that a prospective definition needs to be in the same hygiene scope and also be in a suitable expansion context.
I agree that having a fully-featured procedural API is 'the' 'correct' long-term goal, but that's a long way off (for instance, the API for modifying hygiene information on token streams is currently in flux and far from stabilisation). We can implement something like
I'm rather curious what those reasons are. I would be very surprised and annoyed if this code: // foo.rs
makes_a_struct! {};
// bar.rs
makes_a_module! {};Depended on the order that those macros were expanded in any way. Currently, the only expansion order guarantee that the compiler makes is that a macro gets expanded after its definition can be resolved, which is very minimal, but which also seems to have worked fine so far.
I know that at this point I'm asking for a lot of examples for a lot of things, but... do you have an example of this difference? |
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Mar 14, 2019
•
I'm pretty sure it's been in place for a while now, but I may be mistaken.
I'm yet to see justification for this assertion. I think you're expecting this RFC to cover everything about the macro system and hygiene, when that's far beyond its scope (har har). In fact, that's already been fleshed out before and has been working well in Rust for a while now.
Again, having an actual |
This comment has been minimized.
This comment has been minimized.
|
Has there ever been a detailed public explanation of why hygiene is a hard problem to solve at a design level, not just fiddly to implement? I feel like either we forgot to ever do a proper write-up, or there is one out there I've simply never seen, but either way only half of the people in this discussion seem to have this context. To clarify what I'm asking for: For the hardness of specialization, aturon wrote this. For the hardness of NLL, nikomatsakis wrote this post and a dozen others and the RFC. For the hardness of self-referential structs, withoutboats wrote this post and its sequels. For the hardness of name resolution, RFC 1560 gets the point across. And so on. But for whatever reason, I've never seen any blog post or RFC or forum thread or other public explanation of why hygiene is harder than it seems. Did I simply miss it? |
This comment has been minimized.
This comment has been minimized.
|
reassigning to self, since I told @alexreg that I would try to look more carefully at this. |
pnkfelix
assigned
pnkfelix
and unassigned
nrc
Mar 15, 2019
This comment has been minimized.
This comment has been minimized.
|
@Ixrec: have you looked at @nrc's series on macros? By Rust's standards it's an old series (from 2015), but part 3 talks about some of the rough edges of hygiene at the time (it also mentions the 'macros that work together' paper that @samth referred to in one of their comments). I'm not sure how much of that series is still accurate. |
pierzchalski commentedFeb 2, 2018
•
edited
Rendered.