Skip to content

Commit b23f8c4

Browse files
author
Alexander Regueiro
committed
Created RFC for hygiene opt-out (escaping) in macros.
1 parent e978a8d commit b23f8c4

File tree

1 file changed

+221
-0
lines changed

1 file changed

+221
-0
lines changed

text/0000-macro-hygiene-optout.md

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
- Feature Name: macro_hygiene_optout
2+
- Start Date: 2018-07-05
3+
- RFC PR: (leave this empty)
4+
- Rust Issue: (leave this empty)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
This feature introduces the ability to "opt-out" of the usual macro hygiene rules within definitions of [declarative macros][decl-macro], for designated identifiers or occurrences of identifiers. In other words, the feature will enable one to annotate occurrences of identifiers with macro call-site hygiene rather than the default definition-site hygiene.
10+
11+
# Motivation
12+
[motivation]: #motivation
13+
14+
The use of [hygienic macros] in Rust is justified by much prior research and experience, and solves several common issues that programmers would otherwise encounter with macros due to the nature of syntactical substitution. The principal deficit of this approach is that it requires that names/identifiers of any items generated by a macro be *explicitly passed to* the macro as arguments. This both requires the logic for name selection to remain entirely external to the macro, and even if that is not a problem, the passing of all identifiers-to-export into a macro can quickly become unwieldy for macros that generate many identifiers.
15+
16+
# Guide-level explanation
17+
[guide-level-explanation]: #guide-level-explanation
18+
19+
Escaping of hygiene for identifiers within macros allows one to define identifiers with syntax contexts (**hygiene**) corresponding to the place the macro is invoked (the **call-site**) rather than the place it is defined (**definition-site**). It also enables one to use/reference existing identifiers from the call-site from within macro definitions, though this is not the true aim of the feature, but rather a side-effect, and will be discussed later.
20+
21+
Note that for the purposes of this RFC, an **identifier** can roughly be considered to be an textual name (e.g. `foo_bar`) of any sort (for a variable, function, trait, etc.) or a lifetime (e.g. `'a`).
22+
23+
To escape an identifier in code, one simply prefixes an identifier with the [sigil] `#`. This changes the syntax context (hygiene) of the identifier from the usual definition-site to the call-site.
24+
25+
## Guide: Example A
26+
[guide-example-a]: #guide-example-a
27+
28+
```rust
29+
#![feature(decl_macro)]
30+
#![feature(macro_hygiene_optout)]
31+
32+
macro m() {
33+
pub mod #foo {
34+
pub const #BAR: u32 = 123;
35+
}
36+
}
37+
38+
fn main() {
39+
m!(); // `foo` and `foo::BAR` both behave as if they were defined directly here.
40+
assert_eq!(123, foo::BAR);
41+
}
42+
```
43+
44+
## Guide: Example B
45+
[guide-example-b]: #guide-example-b
46+
47+
```rust
48+
#![feature(decl_macro)]
49+
#![feature(macro_hygiene_optout)]
50+
51+
macro m($mod_name:ident) {
52+
pub mod $mod_name {
53+
pub const #BAR: u32 = 123;
54+
}
55+
}
56+
57+
fn main() {
58+
m!(foo); // `foo` and `foo::BAR` both behave as if they were defined directly here.
59+
assert_eq!(123, foo::BAR);
60+
}
61+
```
62+
63+
## Guide: Example C
64+
[guide-example-c]: #guide-example-c
65+
66+
```rust
67+
#![feature(decl_macro)]
68+
#![feature(macro_hygiene_optout)]
69+
70+
macro m($mod_name:ident) {
71+
pub mod $mod_name {
72+
pub const BAR: u32 = 123;
73+
}
74+
}
75+
76+
fn main() {
77+
m!(foo);
78+
let _ = foo::BAR;
79+
//~^ ERROR cannot find value `BAR` in module `foo`
80+
}
81+
```
82+
83+
## Guide: Example D
84+
[guide-example-d]: #guide-example-d
85+
86+
```rust
87+
#![feature(decl_macro)]
88+
#![feature(macro_hygiene_optout)]
89+
90+
macro m() {
91+
pub mod #foo {
92+
pub const BAR: u32 = 123;
93+
}
94+
}
95+
96+
fn main() {
97+
m!();
98+
let _ = foo::BAR;
99+
//~^ ERROR cannot find value `BAR` in module `foo`
100+
}
101+
```
102+
103+
## Meta-variables
104+
[meta-variables]: #meta-variables
105+
106+
Hygiene escaping of meta-variables (i.e. `#$foo` and `$#foo`) does not have immediately obvious semantics or usefulness, so is explicitly disallowed for the present, and yields error messages.
107+
108+
## Usage Notes
109+
[usage-notes]: #usage-notes
110+
111+
While the motivation of this feature stems from defining or "exporting" new identifiers from macros to their call-site, where it is appropriate for the macro itself to choose/compute the name, it is clear from the above semantics that this feature allows for other potential uses cases. Most notably, one can use or "import" an identifier from their call-site. This, however, is *not* recommended, since this purpose is already fulfilled well by macro parameters. On the other hand, it is not explicitly disallowed, for two reasons:
112+
113+
- Defining an identifier with call-site hygiene within that macro and then using it is a perfectly reasonable scenario.
114+
- Macro expansion is performed at the syntactical (token stream) level, before parsing, so definitions and uses cannot be easily distinguished.
115+
116+
# Reference-level explanation
117+
[reference-level-explanation]: #reference-level-explanation
118+
119+
The macro parser routine first parses the macro definition into a token stream (as before), but now also tags tokens and meta-variables with an enum value representing the kind of hygiene (definition-site or call-site). This is only enabled for new-style `macro!` macros (i.e. *decl_macro* or macros 2.0); for `macro_rules!` macros, the call-site sigil `#` is not handled specially, and gives rise to an error. The sigil is always treated as a separate token outside of macros, on the LHS of macro rules, and when not followed by an identifier on the RHS.
120+
121+
When the macro is invoked (expanded), each token tree is transcribed according to the following rules, depending on its hygiene tag.
122+
123+
- *definition-site*: a normal mark is applied for the current expansion
124+
- *call-site*: a transparent mark is applied for the current expansion and the syntax context for every identifier in the token tree is changed to the syntax context of the call site.
125+
126+
## Reference: Example A
127+
[reference-example-a]: #reference-example-a
128+
129+
In [example A][guide-example-a], the identifiers `foo` (the name of the module) and `BAR` (the name of the constant within the module) are hygiene-escaped, giving them the syntax context of the call site. Thus, `foo::BAR` resolves fine, since `foo` has the same syntax context as the body of the `main` function.
130+
131+
## Reference: Example B
132+
[reference-example-b]: #reference-example-b
133+
134+
In [example B][guide-example-b], the module is named using the identifier passed into the macro, which as a macro argument has the syntax context of the call site. Furthermore, the constant `BAR` within the module is hygiene-escaped, so likewise has the syntax context of the call site. Thus, `foo::BAR` resolves fine, since `foo` has the same syntax context as the body of the `main` function.
135+
136+
## Reference: Example C
137+
[reference-example-c]: #reference-example-c
138+
139+
In [example B][guide-example-b], the situation is similar to [example B][reference-example-b], except that the constant `BAR` is not hygiene-escaped, and thus retains the default definite-site syntaxt context. Thus, when one tries to access `foo::BAR` within the `main` function, `foo` resolves fine, but the constant `BAR` within it is not visible due to hygiene rules, since it does not have a syntax context of the `main` function (or any parent context).
140+
141+
## Reference: Example D
142+
[reference-example-d]: #reference-example-d
143+
144+
In [example B][guide-example-b], the situation is almost identical to [example C][reference-example-c], except that the name of the module is defined within the macro as `foo`, and hygiene-escaped, so that it has the call-site syntax context.
145+
146+
```rust
147+
#![feature(decl_macro)]
148+
#![feature(macro_hygiene_optout)]
149+
150+
macro m() {
151+
pub mod #foo {
152+
pub const BAR: u32 = 123;
153+
}
154+
}
155+
156+
fn main() {
157+
m!();
158+
let _ = foo::BAR;
159+
//~^ ERROR cannot find value `BAR` in module `foo`
160+
}
161+
```
162+
163+
# Drawbacks
164+
[drawbacks]: #drawbacks
165+
166+
- Introducing a new sigil such as `#` can be seen as increasing the syntactical complexity of the language, and potentially obfuscating code slightly.
167+
- The ability to mark some occurences of an identifier with call-site hygiene and leave others with default definition-site hygiene is perhaps more fine-grained than necessary.
168+
- It is not immediately obvious from a macro definition which (occurences of) identifiers take their syntax context from the call site. One has to read through the whole definition to figure it out.
169+
- The syntax permits marking identifiers with call-site hygiene purely for "use" or "import" scenarios (as opposed to "defining" or "exporting" scenarios). Parameters are intended for this purpose, and accomplish the task much better, since they self-document uses of identifiers. However, this ability may actually be desirable more than problematic, as mentioned in the [usage notes][usage-notes].
170+
171+
# Rationale and alternatives
172+
[alternatives]: #alternatives
173+
174+
The design in this RFC was chosen because of its simple syntax and semantics, and the fact it offers a good way to get experience with hygiene opt-out in general, due to its fine-grainedness.
175+
176+
The main alternative considered was having an `escapes` attribute for macros and not using a sigil.
177+
178+
```rust
179+
#[escapes(S, T)]
180+
macro m() {
181+
struct S; // Defines `S` at the call-site.
182+
T // Resolves at the call-site.
183+
}
184+
```
185+
186+
The above would then be equivalent to the following, using the sigil syntax.
187+
188+
```rust
189+
macro m() {
190+
struct #S; // Defines `S` at the call-site.
191+
#T // Resolves at the call-site.
192+
}
193+
```
194+
195+
The obvious benefit of this is that is manifest which identifiers (`S` and `T` in the above example) are hygiene-escaped. A downside, which may or may not be significant, is that these identifiers are then *always* escaped within the macro definition, and thus can never be used with definition-site hygiene.
196+
197+
Going beyond a single `escapes` attribute, one can also imagine having two separate attributes: `defines`, for defining (exporting) identifiers, and `uses`, for using (importing) identifiers. The main issue here is the complexity of the semantics and implementation; indeed, it is not even clear whether one could clearly demarcate cases of definition and use at the syntactical level. As implied by the [usage notes][usage-notes], however, the `uses` attribute would largely overlap with the purpose of macro parameters.
198+
199+
In the end, the approach taken by this RFC was chosen due to the fact it has the most prior art, including an [existing working implementation][pr-47992]. It is also the most flexible in that it allows different hygiene to be applied to different *occurrences* of the same identifier. This will allow us to learn more about the use of hygiene opt-out in practice, while the feature is unstable.
200+
201+
# Prior art
202+
[prior-art]: #prior-art
203+
204+
Extended discussion on this subject was carried out in a [pull request][pr-47992] for this feature, which was closed due to the decision that an RFC such as this one be accepted first. [Alternatives][pr-47992-alternatives] were originally evaluated there, with discussion initiated by @jseyfried, and [continued][pr-47992-alternatives-eval] by @petrochenkov.
205+
206+
Further back, the initial sigil syntax was mentioned in [this comment][pr-40848-comment], and some discussion occrred in the [declarative macros 2.0 tracking issue][decl-macro].
207+
208+
# Unresolved questions
209+
[unresolved]: #unresolved-questions
210+
211+
- Do we want to somehow disallow pure importing of identifiers within macros aside from via parameters, as mentioned in the [drawbacks] section?
212+
- Do we also want to implement the attribute-based approach as an alternative or in addition to the sigil-based approach?
213+
214+
[sigil]: https://en.wikipedia.org/wiki/Sigil_(computer_programming)
215+
[hygienic macros]: https://doc.rust-lang.org/1.7.0/book/macros.html#hygiene
216+
217+
[decl-macro]: https://github.com/rust-lang/rust/issues/39412
218+
[pr-40848-comment]: https://github.com/rust-lang/rust/pull/40847#issuecomment-291186518
219+
[pr-47992]: https://github.com/rust-lang/rust/pull/47992
220+
[pr-47992-alternatives]: https://github.com/rust-lang/rust/pull/47992#issuecomment-364729651
221+
[pr-47992-alternatives-eval]: https://github.com/rust-lang/rust/pull/47992#issuecomment-370268136

0 commit comments

Comments
 (0)