Skip to content

Commit 1bd2bdc

Browse files
committed
Discuss hygiene with format_args!(expr)
When format_args! recieves an expression as the first argument, it attempts to expand it to a string literal. If successful, this is used as the format string and format_args! macro expansion continues as usual. This has subtle interactions with macro hygiene when implicit named arguments come into play. This commit adds dicussion around this case.
1 parent 270bada commit 1bd2bdc

File tree

1 file changed

+90
-19
lines changed

1 file changed

+90
-19
lines changed

text/0000-format-args-implicit-identifiers.md

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
# Summary
88
[summary]: #summary
99

10-
Add implicit named arguments to `std::format_args!`, inferred from the format string.
10+
Add implicit named arguments to `std::format_args!`, inferred from the format string literal.
1111

1212
This would result in downstream macros based on `format_args!` to accept implicit named arguments, for example:
1313

@@ -52,42 +52,51 @@ This identifier `person` would be known as an **implicit named argument** to the
5252
Should `person` not exist in the scope, the usual error E0425 would be emitted by the compiler:
5353

5454
error[E0425]: cannot find value `person` in this scope
55-
--> .\foo.rs:X:Y
55+
--> .\foo.rs:X:Y
5656
|
5757
X | println!("hello {person}");
5858
| ^^^^^^^^ not found in this scope
5959

60+
As a result of this change, downstream macros based on `format_args!` would also be able to accept implicit named arguments in the same way. This would provide ergonomic benefit to many macros across the ecosystem, including:
61+
62+
- `format!`
63+
- `print!` and `println!`
64+
- `eprint!` and `eprintln!`
65+
- `write!` and `writeln!`
66+
- `panic!`, `unreachable!` and `unimplemented!`
67+
- `assert!` and similar
68+
- macros in the `log` crate
69+
- macros in the `failure` crate
70+
71+
(This is not an exhaustive list of the many macros this would affect. In discussion of this RFC if any further commonly-used macros are noted, they may be added to this list.)
72+
73+
## Precedence
74+
6075
Implicit arguments would have lower precedence than the existing named arguments `format_args!` already accepts. For example, in the example below, the `person` named argument is explicit, and so the `person` variable in the same scope would not be captured:
6176

62-
let person = "David";
77+
let person = "Charlie";
6378

6479
// Person is an explicit named argument, so this
65-
// expands to "hello Matt".
66-
println!("hello {person}", person="Matt");
80+
// expands to "hello Snoopy".
81+
println!("hello {person}", person="Snoopy");
6782

6883
Indeed, in this example above the `person` variable would be unused, and so in this case the unused varible warning will apply, like the below:
6984

7085
warning: unused variable: `person`
71-
--> src/foo.rs:X:Y
86+
--> src/foo.rs:X:Y
7287
|
73-
X | let person = "David";
88+
X | let person = "Charlie";
7489
| ^^^^^^ help: consider prefixing with an underscore: `_person`
7590
|
7691
= note: `#[warn(unused_variables)]` on by default
7792

7893
Because implicit named arguments would have lower precedence than explicit named arguments, it is anticipated that no breaking changes would occur to existing code by implementing this RFC.
7994

80-
As a result of this change, downstream macros based on `format_args!` would also be able to accept implicit named arguments in the same way. This would provide ergonomic benefit to many macros across the ecosystem, including:
95+
## Generated Format Strings
8196

82-
- `format!`
83-
- `print!` and `println!`
84-
- `eprint!` and `eprintln!`
85-
- `write!` and `writeln!`
86-
- `panic!`, `unreachable!` and `unimplemented!`
87-
- `assert!` and similar
88-
- macros in the `log` crate
97+
`format_args!` can accept an expression instead of a string literal as its first argument. `format_args!` will attempt to expand any such expression to a string literal. If successful then the `format_args!` expansion will continue as if the user had passed that string literal verbatim.
8998

90-
(This is not an exhaustive list of the many macros this would affect. In discussion of this RFC if any further commonly-used macros are noted, they should be added to this list.)
99+
No implicit named argument capture will be performed if the format string is generated from an expansion. See the [macro hygiene](#macro-hygiene) discussion for the motivation behind this decision.
91100

92101

93102
# Reference-level explanation
@@ -101,18 +110,80 @@ The implementation pathway is directly motivated by the guide level explanation
101110

102111
error: there is no argument named `person`
103112
--> src/foo.rs:X:Y
104-
|
105-
20 | println!("hello {person}");
106-
| ^^^^^^^^
113+
|
114+
X | println!("hello {person}");
115+
| ^^^^^^^^
107116

108117
If this RFC were implemented, instead of this resulting in an error, this named argument would be treated as an **implicit named argument** and the final result of the expansion of the `format_args!` macro would be the same as if a named argument, with name equivalent to the identifier, had been provided to the macro invocation.
109118

110119
Because `person` is only treated as an implicit named argument if no exisiting named argument can be found, this ensures that implicit named arguments have lower precedence than explicit named arguments.
111120

112121
## Macro Hygiene
122+
[macro-hygiene]: #macro-hygiene
123+
113124

114125
Expanding the macro in this fashion will need to generate an identifier which corresponds to the implicit named argument. The hygiene of this generated identifier would be inherited from the format string, with location information reduced to the section of the format string which contains the implicit named argument.
115126

127+
An interesting case to consider is that `format_args!`-based macros can accept any expression in the format string position. The macro then attempts to expand this expression to a string literal.
128+
129+
This means the below examples of `format!` invocations could compile successfully in stable Rust today:
130+
131+
format!(include_str!("README.md"), foo=1)
132+
format!(concat!("hello ", "{bar}")), bar=2)
133+
134+
This RFC argues that `format_args!` should not attempt to expand any implicit named arguments if the macro is provided with an expression instead of a verbatim string literal.
135+
136+
The following are motivations why this RFC argues this case:
137+
138+
* This RFC's motivation for implicit named arguments is to give users a concise syntax for string formatting. When the format string is generated from some other expression this motivation for concise syntax is irrelevant.
139+
140+
* The hygienic context of the string literal generated by the expansion is entirely dependent on the expression. For example, the string literal produced by the `concat!` macro resides in a separate hygienic context. In combination with implicit named arguments using hygiene inherited from the format string, this would lead to puzzling errors like the below:
141+
142+
error[E0425]: cannot find value `person` in this scope
143+
--> scratch/test.rs:4:14
144+
|
145+
| let person = "Charlie";
146+
4 | println!(concat!("hello {person}"));
147+
| ^^^^^^^^^^^^^^^^^^^^^^^^^ not found in this scope
148+
149+
* The expression may expand to a format string which contains new identifiers not written by the users, bypassing macro hygiene in suprising ways. For example, if the `concat!` macro did not have the hygiene issue described above, it could be to "splice together" an implicit named argument like so:
150+
151+
let person = "Charlie";
152+
println!(concat!("hello {p", "er", "son", "}"));
153+
154+
The RFC author argues that it appears highly undesirable that implicit capture of the `person` identifier should occur in this example given above.
155+
156+
* Using the hygienic context of the format string for implicit named arguments can have potentially suprising results even just with `macro_rules!` macros.
157+
158+
For example, the RFC author found that with a proof-of-concept implementation of implicit named arguments the invocation below would print `"Snoopy"`:
159+
160+
const PERSON: &'static str = "Charlie";
161+
162+
fn main() {
163+
macro_rules! bar {
164+
() => { "{PERSON}" };
165+
}
166+
167+
const PERSON: &'static str = "Snoopy";
168+
println!(bar!());
169+
}
170+
171+
However, by merely changing to `let` bindings and moving the `"Charlie"` declaration three lines down to be inside the `main()` function, as below, the invocation would instead print `"Charlie"`:
172+
173+
fn main() {
174+
let person = "Charlie";
175+
macro_rules! bar {
176+
() => { "{person}" };
177+
}
178+
179+
let person = "Snoopy";
180+
println!(bar!());
181+
}
182+
183+
While it can be argued that this example is very contrived, the RFC author believes that it is undesirable to add such subtle interactions to the `format_args!` family of macros.
184+
185+
These appear to give strong motivation to disable implicit argument capture when `format_args!` expands an expression instead of a verbatim string literal.
186+
116187
# Drawbacks
117188
[drawbacks]: #drawbacks
118189

0 commit comments

Comments
 (0)