Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Macro reform #453

Merged
merged 27 commits into from
Dec 19, 2014
Merged
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
33f9f6d
RFC: Macro reform
kmcallister Nov 8, 2014
4b5e09f
Add disclaimer
kmcallister Nov 8, 2014
79c74f9
New syntax proposal
kmcallister Nov 12, 2014
193e2ab
Add item macro sugar
kmcallister Nov 13, 2014
82f7c16
Unspecify order of procedural macro side effects
kmcallister Nov 13, 2014
3ad1ce3
Add note about $crate lint
kmcallister Nov 13, 2014
df4bb87
Spellcheck
kmcallister Nov 13, 2014
2ef9932
Add acknowledgements
kmcallister Nov 13, 2014
01563f6
Demonstrate alternative procedural macro sugar
kmcallister Nov 13, 2014
1fc4d1e
Add a note about deprecating use macro
kmcallister Nov 13, 2014
f517cd1
Add lazy-static.rs as an example
kmcallister Nov 13, 2014
fea0c6d
Add table of contents for design
kmcallister Nov 13, 2014
3dd2ee9
Add #[plugin(... args ...)]
kmcallister Nov 13, 2014
6f7ea0c
Add more acknowledgements
kmcallister Nov 13, 2014
9bbb13f
Update for new syntax
kmcallister Nov 13, 2014
ef13bdc
Reorganize and clarify
kmcallister Nov 17, 2014
7a25233
Remove leftover partial sentence
kmcallister Nov 17, 2014
0471d61
Stick with macro_rules! syntax for definitions
kmcallister Nov 19, 2014
ee9e2e8
Remove item macro sugar
kmcallister Nov 19, 2014
0c10871
Remove section about procedural macro order
kmcallister Nov 19, 2014
ee22f7f
Clarify the "crate ident" concept
kmcallister Nov 19, 2014
c6cdf8c
Reorganize #[plugin] section
kmcallister Nov 19, 2014
45ff6ec
More acknowledgements
kmcallister Nov 19, 2014
3e5861d
New revision
kmcallister Nov 26, 2014
a503fb4
Changes from the meeting
kmcallister Dec 5, 2014
ebc0a85
Rename attributes per meeting
kmcallister Dec 5, 2014
2f6bdc8
Add one more unresolved question
kmcallister Dec 5, 2014
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions text/0000-macro-reform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
- Start Date: 2014-11-05
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)

# Summary

Various enhancements to `macro_rules!` ahead of its standardization in 1.0.

# Motivation

`macro_rules!` has [many rough
edges](https://github.com/rust-lang/rfcs/issues/440). A few of the big ones:

- You can't re-export macros
- Even if you could, names produced by the re-exported macro won't follow the re-export
- You can't use the same macro in-crate and exported, without the "curious inner-module" hack
- There's no namespacing at all
- You can't control which macros are imported from a crate
- You need the feature-gated `#[phase(plugin)]` to import macros

These issues in particular are things we have a chance of addressing for 1.0.
This RFC contains plans to do so.

# Detailed design

## Rename

Rename `macro_rules!` to `macro!`, analogous to `fn`, `struct`, etc.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I stated on #440, I don't like taking this nice name for this imperfect macro system. It seems unfortunate to me that one would have to explicitly opt-in to some new improved macro system if/when we create a parallel one that incorporates the backwards-incompatible improvements to macro_rules!. (That is, it is extremely unfortunate that the obvious macro! foo { ... } invocation would give the deprecated form.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename trait to crappy_trait because we don't have HKT yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, maybe that's an unfair comparison because HKT are supposed to be back-compat. But every macro system we ever have in Rust is going to be "imperfect" in some way. At some point we have to live with the imperfections and improve them in non-breaking ways, which is what 1.0 is all about, but why back away from that idea just in the case of macros?

The practical outcome of your alternative is that Rust programmers are stuck with the ugly name macro_rules! for perhaps years and the unstable alternative will be something not descriptively different, like macro_case!.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With macros, we already know before 1.0 that the ways we want to improve them in the future are not backwards compatible, and the stabilization of the current system of macros is itself a compromise. I don't know of another Rust language feature in a similar situation, but maybe there is an example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically I see it as

  • force most people to write macro_rules!, for years
  • force some people to write #![feature(improved_macros)] or #[version(2)] macro!, if and when such back-incompat enhancements are developed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Long term, improved_macros are what people are going to use,

But long term, they'll be on Rust 2.x or Rust 3.x, which gives us another chance to make breaking changes, especially fairly superficial ones like naming.

I'm not sure how long 1.x is meant to last but it would seem relevant to this discussion.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to note: some of the ways that macros will change, like hygiene, will be (a) a change to the entire system, preventing backwards compatibility from even being possible (i.e., define_a_macro_in_the_old_way! won't recover the old semantics), and (b) make things almost strictly better (i.e., the old semantics mainly gets in people's way).

However, the reason it is called macro_rules! is that it's not able to define all kinds of macros, only rule-based ones. A construct named macro! should be capable of defining procedural macros, and I think that the arrival of procedural macros is an appropriate time to make the name change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was expecting that we would have two essentially independent hygiene systems inside the compiler (i.e. identifiers would go from storing just ctxt: SyntaxContext to storing ctxt: SyntaxContext, improved_ctxt: SyntaxContext); it's pretty sad, but it seems to me to be the only way we can possibly aim to improve hygiene without breaking backwards compatibility.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@huonw this is horrifying! I was expecting that we would be able to have a single hygiene system in rustc and any macro system could do what they like - i.e., rustc::resolve would expect all lexical context ids to be 'correct'. macro! would output correct lexical contexts, macro_rules! would output lexical contexts that correspond to the current level of hygiene, etc. I'm not entirely sure if this is possible, because I don't exactly know how hygiene is currently broken.

As far as I see it, being able to do this backwards compatibly after 1.0 is the most important issue for macros pre-1.0.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nick29581 I agree 100% that it is not nice, but I believe that it is an existence proof that we can have a macro system with improved hygiene post-1.0 without being forced to break what we already have. (Your approach does sound better, if it works.)


## `#[visible(...)]`

`#[macro_export]` and `#[macro_escape]` are replaced with a new attribute
applied to a `macro!` invocation:

* `#[visible(this_crate)]` — this macro "escapes" up the module hierarchy to
the crate root, so it can be used anywhere in the crate after the definition
(according to a depth-first traversal). This is like putting `#[macro_escape]`
on the module and all its ancestors, but applies *only* to the macro with the
`#[visible]` attribute.

* `#[visible(other_crates)]` — same meaning as `#[macro_export]` today

These can be combined as `#[visible(this_crate, other_crates)]` (in either
order).

The default (as today) is that the macro is only visible within the lexical
scope where it is defined.

## `$crate`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be a minor and incremental change, which is helpful, so +1 to it.

I don't think this is something we'd want in the eventual perfect macro world, but seems like a good band aid fix for macro_rules.


Add a special metavar `$crate` which expands to `::foo` when the macro was
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bikeshed: I would prefer something like $_crate, or $$crate.

imported from crate `foo`, and to nothing when it was defined in-crate.
`$crate::bar::baz` will be an absolute path either way.

This feature eliminates the need for the "curious inner-module" and also
enables macro re-export (see below). It is [implemented and
tested](https://github.com/kmcallister/rust/commits/macro-reexport) but needs a
rebase.

## Crate scope for macros
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm keen for this in general, but I don't like the proposed syntax. I would prefer using attributes for this.


Instead of a single global namespace for macro definitions, we now have one
namespace per crate. We introduce an attribute to `use` macros with optional
renaming:

```rust
#[use_macros(std::vec, std::panic as fail)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post 1.0, this could expand to something like use std::{vec!, panic! as fail!};

Of course the syntax and expansion semantics are out of scope of this PR - just illustrating a possible migration path.

```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should introduce language syntax for macros - it is clear at the moment that everything macro-like uses its own syntax which is separate from macro-free Rust. I prefer to keep it that way, it makes macro systems more pluggable and I believe will make things easier to understand.


(We'd need to extend attribute syntax, or change this to be compatible.)

This can be applied to a module, a function, or anywhere else attributes are
allowed.

There's no way to invoke a macro with a qualified name; this obviates the need
for changes to the grammar / parser.

This isn't an actual `use` item because macro expansion happens before name
resolution, and libsyntax knows nothing about the latter.

This change applies to `macro!` and to all other syntax extensions, even
decorators that are used as attributes.

Many macros expand using other "private macros" as an implementation detail.
For example, librustc's `declare_lint!` uses `lint_initializer!`. The client
should not know about this macro, although it still needs to be exported for
cross-crate use. The solution is to allow `use_macros` on `macro!` itself, and
allow `$crate` in that context:

```rust
/// Not to be imported directly.
#[visible(other_crates)]
macro! lint_initializer ( ... )

/// Declare a lint.
#[visible(other_crates)]
#[use_macros($crate::lint_initializer)]
macro! declare_lint (
($name:ident, $level:ident, $desc:expr) => (
static $name: &'static $crate::lint::Lint
= &lint_initializer!($name, $level, $desc);
)
)
```

The macro `lint_initializer!` will be visible only during further expansion of
the result of invoking `declare_lint!`.

Procedural macros need their own way to manipulate the expansion context, but
that's an unstable internal API, so it's outside the scope of this RFC.
Ideally the implementation of `macro!` will use the same API.

## Macro re-export

With `$crate` we can easily re-export macros that were imported from another
crate.

For example, libcollections defines a `vec!` macro, which would now look like:

```rust
#[visible(other_crates)]
macro! vec (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this could expand to pub macro! vec { ... } post 1.0...

($($e:expr),*) => ({
let mut _temp = $crate::vec::Vec::new();
$(_temp.push($e);)*
_temp
})
)
```

Currently, libstd duplicates this macro in its own `macros.rs`. Now it could
do

```rust
#![reexport_macros(collections::vec)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this expand to something like pub use collections::vec!; post 1.0?

```

as long as the module `std::vec` is interface-compatible with
`collections::vec`.

(Actually the current libstd `vec!` is completely different for efficiency, but
it's just an example.)

## Auto-load macros

Since macros are now crate-scoped, we can load macros from every `extern crate`
without a special attribute. (Probably we should exclude `extern crate`s that
aren't at the crate root, because there's no way `$crate` paths will be
correct.)

`#[phase(plugin)]` becomes simply `#[plugin]` and is still feature-gated. It
only controls whether to search for and run a plugin registrar function. The
plugin itself will decide whether it's to be linked at runtime, by calling a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would a syntax extension want to link itself in at runtime? It injects a runtime dependency on librustc, so most (all?) syntax extensions have a separate crate for runtime support (e.g. regex and regex_macros, or phf and phf_mac).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't expect it to be a common situation. One example would be quasiquoting as a library. quote_foo!() expands to calls into syntax::ext::quote::rt::.... And there's no need for this to live in a separate crate from the quote macro, because the user of quasiquoting is also going to link librustc, or libsyntax anyway.

Basically it's a capability we have today with #[phase(plugin, link)] and I didn't want to throw it away. Though, the Registry API is unstable and we'd only be adding capabilities to it, so maybe we should throw this out for the time being and see if anyone complains. It might be good to nudge people towards doing thing the two-crates way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My gut says that #[phase(plugin, link)] is only ever used for imports of macro_rules plugins, so I'd vote for axing the functionality until people start complaining :).

`Registry` method.

In the future I would like to support `#[plugin(... any metas ...)]` where
these "arguments" are available in the registrar and can be used to configure
how the plugin works. This RFC does not cover that feature; I just want to
make sure our design is compatible.


# Drawbacks

This is big churn on a major feature, not long before 1.0.

We can ship improved versions of `macro!` in a back-compat way (in theory; I
would like to smoke test this idea before 1.0). So we could defer all this
reform until after 1.0. The main reason not to is macro import/export. Right
now every macro you import will be expanded using your local copy of `macro!`,
regardless of what the macro author had in mind.

# Alternatives

We could try to implement proper hygienic capture of crate names in macros.
This would be wonderful, but I don't think we can get it done for 1.0.

We would have to actually parse the macro RHS when it's defined, find all the
paths it wants to emit (somehow), and then turn each crate reference within
such a path into a globally unique thing that will still work when expanded in
another crate. Right now libsyntax is oblivious to librustc's name resolution
rules, and those rules can't be applied until macro expansion is done, because
(for example) a macro can expand to a `use` item.

nrc suggested dropping the `#![macro_escape]` functionality as part of this
reform. Two ways this could work out:

- *All* macros are visible throughout the crate. This seems bad; I depend on
module scoping to stay (marginally) sane when working with macros. You can
have private helper macros in two different modules without worrying that
the names will clash.

- Only macros at the crate root are visible throughout the crate. I'm also
against this because I like keeping `lib.rs` as a declarative description
of crates, modules, etc. without containing any actual code. Forcing the
user's hand as to which file a particular piece of code goes in seems
un-Rusty.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are adding syntax for macro import, why not rely on that to import a macro from another module into where you want it, rather than on macro_escape?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like the difficult "proper name resolution for macros" solution, but I'll think more about it. In the proposal as written all macros live directly under a crate so it's not clear how to extend it to intra-crate import.

# Unresolved questions

Do we support globs in `use_macros`? How does prelude injection work?
How do I get all of libcore's macros if my crate is `no_std`?

Does `use_macros` also define the name in child modules? In a sense this is the
more natural thing to do in libsyntax, but it's inconsistent with normal `use`
items.

All of the syntax is bikeshedable. For example, should `use_macros` include
the exclamation point? What about when it applies to item decorator attributes?

Allowing `$crate` in attributes is weird. Maybe we should use some other
syntax that fits with existing attribute parsing.