- Start Date: 2014-11-05
- RFC PR: rust-lang/rfcs#453
- Rust Issue: rust-lang/rust#20008
Summary
Various enhancements to macros ahead of their standardization in 1.0.
Note: This is not the final Rust macro system design for all time. Rather, it addresses the largest usability problems within the limited time frame for 1.0. It’s my hope that a lot of these problems can be solved in nicer ways in the long term (there is some discussion of this below).
Motivation
macro_rules!
has many rough
edges. 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.
Semantic changes
These are the substantial changes to the macro system. The examples also use the improved syntax, described later.
$crate
The first change is to disallow importing macros from an extern crate
that is
not at the crate root. In that case, if
extern crate "bar" as foo;
imports macros, then it’s also introducing ordinary paths of the form
::foo::...
. We call foo
the crate ident of the extern crate
.
We introduce a special macro metavar $crate
which expands to ::foo
when a
macro was imported through crate ident foo
, and to nothing when it was
defined in the crate where it is being expanded. $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 but needs a rebase.
We can add a lint to warn about cases where an exported macro has paths that
are not absolute-with-crate or $crate
-relative. This will have some
(hopefully rare) false positives.
Macro scope
In this document, the “syntax environment” refers to the set of syntax
extensions that can be invoked at a given position in the crate. The names in
the syntax environment are simple unqualified identifiers such as panic
and
vec
. Informally we may write vec!
to distinguish from an ordinary item.
However, the exclamation point is really part of the invocation syntax, not the
name, and some syntax extensions are invoked with no exclamation point, for
example item decorators like deriving
.
We introduce an attribute macro_use
to specify which macros from an external
crate should be imported to the syntax environment:
#[macro_use(vec, panic="fail")]
extern crate std;
#[macro_use]
extern crate core;
The list of macros to import is optional. Omitting the list imports all macros,
similar to a glob use
. (This is also the mechanism by which std
will
inject its macros into every non-no_std
crate.)
Importing with rename is an optional part of this proposal that will be implemented for 1.0 only if time permits.
Macros imported this way can be used anywhere in the module after the
extern crate
item, including in child modules. Since a macro-importing
extern crate
must appear at the crate root, and view items come before
other items, this effectively means imported macros will be visible for
the entire crate.
Any name collision between macros, whether imported or defined in-crate, is a hard error.
Many macros expand using other “helper 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. For this reason we allow #[macro_use]
on a macro
definition.
/// Not to be imported directly.
#[macro_export]
macro_rules! lint_initializer { ... }
/// Declare a lint.
#[macro_export]
#[macro_use(lint_initializer)]
macro_rules! declare_lint {
($name:ident, $level:ident, $desc:expr) => (
static $name: &'static $crate::lint::Lint
= &lint_initializer!($name, $level, $desc);
)
}
The macro lint_initializer!
, imported from the same crate as declare_lint!
,
will be visible only during further expansion of the result of invoking
declare_lint!
.
macro_use
on macro_rules
is an optional part of this proposal that will be
implemented for 1.0 only if time permits. Without it, libraries that use
helper macros will need to list them in documentation so that users can import
them.
Procedural macros need their own way to manipulate the syntax environment, but that’s an unstable internal API, so it’s outside the scope of this RFC.
New syntax
We also clean up macro syntax in a way that complements the semantic changes above.
#[macro_use(...)] mod
The macro_use
attribute can be applied to a mod
item as well. The
specified macros will “escape” the module and become visible throughout the
rest of the enclosing module, including any child modules. A crate might start
with
#[macro_use]
mod macros;
to define some macros for use by the whole crate, without putting those
definitions in lib.rs
.
Note that #[macro_use]
(without a list of names) is equivalent to the
current #[macro_escape]
. However, the new convention is to use an outer
attribute, in the file whose syntax environment is affected, rather than an
inner attribute in the file defining the macros.
Macro export and re-export
Currently in Rust, a macro definition qualified by #[macro_export]
becomes
available to other crates. We keep this behavior in the new system. A macro
qualified by #[macro_export]
can be the target of #[macro_use(...)]
, and
will be imported automatically when #[macro_use]
is given with no list of
names.
#[macro_export]
has no effect on the syntax environment for the current
crate.
We can also re-export macros that were imported from another crate. For
example, libcollections defines a vec!
macro, which would now look like:
#[macro_export]
macro_rules! vec {
($($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
#[macro_reexport(vec)]
extern crate collections;
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.)
Because macros are exported in crate metadata as strings, macro re-export “just
works” as soon as $crate
is available. It’s implemented as part of the
$crate
branch mentioned above.
#[plugin]
attribute
#[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
Registry
method.
#[plugin]
can optionally take any meta
items as “arguments”,
e.g.
#[plugin(foo, bar=3, baz(quux))]
extern crate myplugin;
rustc itself will not interpret these arguments, but will make them available
to the plugin through a Registry
method. This facilitates plugin
configuration. The alternative in many cases is to use interacting side
effects between procedural macros, which are harder to reason about.
Syntax convention
macro_rules!
already allows { }
for the macro body, but the convention is
( )
for some reason. In accepting this RFC we would change to a { }
convention for consistency with the rest of the language.
Reserve macro
as a keyword
A lot of the syntax alternatives discussed for this RFC involved a macro
keyword. The consensus is that macros are too unfinished to merit using the
keyword now. However, we should reserve it for a future macro system.
Implementation and transition
I will coordinate implementation of this RFC, and I expect to write most of the code myself.
To ease the transition, we can keep the old syntax as a deprecated synonym, to be removed before 1.0.
Drawbacks
This is big churn on a major feature, not long before 1.0.
We can ship improved versions of macro_rules!
in a back-compatible way (in
theory; I would like to smoke test this idea before 1.0). So we could defer
much of 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_rules!
, 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.
Unresolved questions
Should we forbid $crate
in non-exported macros? It seems useless, however I
think we should allow it anyway, to encourage the habit of writing $crate::
for any references to the local crate.
Should #[macro_reexport]
support the “glob” behavior of #[macro_use]
with
no names listed?
Acknowledgements
This proposal is edited by Keegan McAllister. It has been refined through many engaging discussions with:
- Brian Anderson, Shachaf Ben-Kiki, Lars Bergstrom, Nick Cameron, John Clements, Alex Crichton, Cathy Douglass, Steven Fackler, Manish Goregaokar, Dave Herman, Steve Klabnik, Felix S. Klock II, Niko Matsakis, Matthew McPherrin, Paul Stansifer, Sam Tobin-Hochstadt, Erick Tryzelaar, Aaron Turon, Huon Wilson, Brendan Zabarauskas, Cameron Zwarich
- GitHub:
@bill-myers
@blaenk
@comex
@glaebhoerl
@Kimundi
@mitchmindtree
@mitsuhiko
@P1Start
@petrochenkov
@skinner
- Reddit:
gnusouth
ippa
!kibwen
Mystor
Quxxy
rime-frost
Sinistersnare
tejp
UtherII
yigal100
- IRC:
bstrie
ChrisMorgan
cmr
Earnestly
eddyb
tiffany
My apologies if I’ve forgotten you, used an un-preferred name, or accidentally categorized you as several different people. Pull requests are welcome :)