From eaa41de2a5e2fd002cc8fdd433b2ad160adac64c Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Mon, 6 May 2024 12:20:30 +0200 Subject: [PATCH 1/9] RFC to extend format_args implicit arguments to allow field access --- text/0000-format-args-implicit-dot.md | 190 ++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 text/0000-format-args-implicit-dot.md diff --git a/text/0000-format-args-implicit-dot.md b/text/0000-format-args-implicit-dot.md new file mode 100644 index 00000000000..7f911c530b6 --- /dev/null +++ b/text/0000-format-args-implicit-dot.md @@ -0,0 +1,190 @@ +- Feature Name: `format_args_implicit_dot` +- Start Date: 2023-10-01 +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- Rust Issue: [rust-lang/rust#00000](https://github.com/rust-lang/rust/issues/00000) + +# Summary +[summary]: #summary + +This RFC extends the "implicit named arguments" mechanism to allow accessing +field names with `var.field` syntax: `format!("{self.x} {var.another_field}")`. + +# Motivation +[motivation]: #motivation + +[RFC 2795](https://github.com/rust-lang/rfcs/pull/2795) added "implicit named +arguments" to `std::format_args!` (and other macros based on it such as +`format!` and `println!` and `panic!`), allowing the format string to reference +variables in scope using identifiers. For instance, `println!("Hello {name}")` +is now equivalent to `println!("Hello {name}", name=name)`. + +The original implicit named arguments mechanism only permitted single +identifiers, to avoid the complexity of embedding arbitrary expressions into +format strings. The implicit named arguments mechanism is widely used, and one +of the most common requests and most common reasons people cannot use that +syntax is when they need to access a struct field. Adding struct field syntax +does not conflict with any other format syntax, and unlike allowing *arbitrary* +expressions, allowing struct field syntax does not substantially increase +complexity or decrease readability. + +This proposal has the same advantages as the original implicit named arguments +proposal: making more formatting expressions easy to read from left-to-right +without having to jump back and forth between the format string and the +arguments. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +With this proposal accepted, the following (currently invalid) macro +invocation: + +```rust +format_args!("hello {person.name}") +``` + +would become a valid macro invocation, and would be equivalent to a shorthand +for the already valid: + +```rust +format_args!("hello {name}", name=person.name) +``` + +The identifier at the beginning of the chain (`person` in this case) must be an +identifier which existed in the scope in which the macro is invoked, and must +have a field of the appropriate name (`name` in this case). + +This syntax works for fields within fields as well: + +```rust +format_args!("{obj.field.nested_field.another_field}") +``` + +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: + + - `format!` + - `print!` and `println!` + - `eprint!` and `eprintln!` + - `write!` and `writeln!` + - `panic!`, `unreachable!`, `unimplemented!`, and `todo!` + - `assert!`, `assert_eq!`, and similar + - macros in the `log` and `tracing` crates + +(This is not an exhaustive list of the many macros this would affect.) + +## Additional formatting parameters + +As a result of this RFC, formatting parameters can also use implicit named +argument capture: + + println!("{self.value:self.width$.self.precision$}"); + +This is slightly complex to read, but unambiguous thanks to the `$`s. + +## Compatibility + +This syntax is not currently accepted, and results in a compiler error. Thus, +adding this syntax should not cause any breaking changes in any existing Rust +code. + +## No field access from named arguments + +This syntax only permits referencing fields from identifiers in scope. It does +not permit referencing fields from named arguments passed into the macro. For +instance, the following syntax is not valid, and results in an error: + +```rust +println!("{x.field}", x=expr()); // Error +``` + +If there is an ambiguity between an identifier in scope and an identifier used +for a named argument, the compiler emits an error. + +```rust +let x = SomeStruct::new(); +println!("{x.field}", x=expr()); // Error +``` + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +The implementation captures the first identifier in the chain using the same +mechanism as implicit format arguments, and then uses normal field accesses to +obtain the value, just as if the field were accessed within a named argument. +Thus, the following two expressions are semantically equivalent: + +```rust +format_args!("{name.field1.field2}") + +format_args!("{unique_identifier}", unique_identifier=name.field1.field2) +``` + +Any `Deref` operations associated with the `.` in each format argument are +evaluated from left-to-right as they appear in the format string, at the point +where the format string argument is evaluated, before the positional or named +arguments are evaluated. (In general, `Deref` operations should be idempotent, +so the evaluation order should not matter.) + +If the identifier at the start of the chain does not exist in the scope, the +usual error E0425 would be emitted by the compiler, with the span of that +identifier: + +``` +error[E0425]: cannot find value `person` in this scope + --> src/main.rs:X:Y + | +X | format_args!("hello {person.name}"); + | ^^^^^^ not found in this scope +``` + +If one of the field references refers to a field not contained in the +structure, the usual error E0609 would be emitted by the compiler, with the +span of the field identifier: + +``` +error[E0609]: no field `name` on type `person` + --> src/main.rs:X:Y + | +5 | format_args!("hello {person.name}"); + | ^^^^ unknown field +``` + +# Drawbacks +[drawbacks]: #drawbacks + +This adds incremental additional complexity to format strings. + +Having `x.y` available may make people assume other types of expressions work +as well. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +The null alternative is to avoid adding this syntax, and let users continue to +pass named arguments or bind local temporary names rather than performing +inline field accesses within format strings. This would continue to be +inconvenient but functional. + +This functionality could theoretically be implemented in a third-party crate, +but would then not be automatically and consistently available within all of +Rust's formatting macros, including those in the standard library and those +throughout the ecosystem. + +We could omit support for other formatting parameters (width, precision). +However, this would introduce an inconsistency that people have to remember; +people would *expect* this to work. + +# Prior art +[prior-art]: #prior-art + +Rust's existing implicit format arguments serve as prior art, and discussion +around that proposal considered the possibility of future (cautious) extension +to additional types of expressions. + +The equivalent mechanisms in some other programming languages (e.g. Python +f-strings, Javascript backticks, C#, and various other languages) allow +arbitrary expressions. This RFC does *not* propose adding arbitrary +expressions, nor should this RFC serve as precedent for arbitrary expressions, +but nonetheless these other languages provide precedent for permitting more +than just single identifiers. From ad43d2a4e480a2b65f77d03c84b214a0325d681a Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Mon, 6 May 2024 12:23:32 +0200 Subject: [PATCH 2/9] RFC 3626 --- ...at-args-implicit-dot.md => 3626-format-args-implicit-dot.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename text/{0000-format-args-implicit-dot.md => 3626-format-args-implicit-dot.md} (98%) diff --git a/text/0000-format-args-implicit-dot.md b/text/3626-format-args-implicit-dot.md similarity index 98% rename from text/0000-format-args-implicit-dot.md rename to text/3626-format-args-implicit-dot.md index 7f911c530b6..0cf14e95612 100644 --- a/text/0000-format-args-implicit-dot.md +++ b/text/3626-format-args-implicit-dot.md @@ -1,6 +1,6 @@ - Feature Name: `format_args_implicit_dot` - Start Date: 2023-10-01 -- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- RFC PR: [rust-lang/rfcs#3626](https://github.com/rust-lang/rfcs/pull/3626) - Rust Issue: [rust-lang/rust#00000](https://github.com/rust-lang/rust/issues/00000) # Summary From 6bbf9bca1d2cddd9432597a18d44133ea6822c74 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Mon, 6 May 2024 13:04:48 +0200 Subject: [PATCH 3/9] Clarify the example desugaring --- text/3626-format-args-implicit-dot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/3626-format-args-implicit-dot.md b/text/3626-format-args-implicit-dot.md index 0cf14e95612..952169f0c74 100644 --- a/text/3626-format-args-implicit-dot.md +++ b/text/3626-format-args-implicit-dot.md @@ -46,7 +46,7 @@ would become a valid macro invocation, and would be equivalent to a shorthand for the already valid: ```rust -format_args!("hello {name}", name=person.name) +format_args!("hello {unique_ident}", unique_ident=person.name) ``` The identifier at the beginning of the chain (`person` in this case) must be an From a84773be180430810bc2f45769a2fdcbf185ec98 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Mon, 6 May 2024 14:40:20 +0200 Subject: [PATCH 4/9] Fix formatting --- text/3626-format-args-implicit-dot.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/text/3626-format-args-implicit-dot.md b/text/3626-format-args-implicit-dot.md index 952169f0c74..62fc058697d 100644 --- a/text/3626-format-args-implicit-dot.md +++ b/text/3626-format-args-implicit-dot.md @@ -78,7 +78,9 @@ provide ergonomic benefit to many macros across the ecosystem, including: As a result of this RFC, formatting parameters can also use implicit named argument capture: - println!("{self.value:self.width$.self.precision$}"); +```rust +println!("{self.value:self.width$.self.precision$}"); +``` This is slightly complex to read, but unambiguous thanks to the `$`s. From de4deb1d72e9623fb429895340a48bb57add5620 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Mon, 6 May 2024 14:40:27 +0200 Subject: [PATCH 5/9] Allow `.await` --- text/3626-format-args-implicit-dot.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/text/3626-format-args-implicit-dot.md b/text/3626-format-args-implicit-dot.md index 62fc058697d..823e7518212 100644 --- a/text/3626-format-args-implicit-dot.md +++ b/text/3626-format-args-implicit-dot.md @@ -84,6 +84,14 @@ println!("{self.value:self.width$.self.precision$}"); This is slightly complex to read, but unambiguous thanks to the `$`s. +## `await` + +Formatting can use `.await`, as well: + +```rust +println!("{future1.await} {future2.await}"); +``` + ## Compatibility This syntax is not currently accepted, and results in a compiler error. Thus, @@ -122,11 +130,10 @@ format_args!("{name.field1.field2}") format_args!("{unique_identifier}", unique_identifier=name.field1.field2) ``` -Any `Deref` operations associated with the `.` in each format argument are -evaluated from left-to-right as they appear in the format string, at the point -where the format string argument is evaluated, before the positional or named -arguments are evaluated. (In general, `Deref` operations should be idempotent, -so the evaluation order should not matter.) +Any `Deref` operations or `.await` operations associated with the `.` in each +format argument are evaluated from left-to-right as they appear in the format +string, at the point where the format string argument is evaluated, before the +positional or named arguments are evaluated. If the identifier at the start of the chain does not exist in the scope, the usual error E0425 would be emitted by the compiler, with the span of that @@ -177,6 +184,9 @@ We could omit support for other formatting parameters (width, precision). However, this would introduce an inconsistency that people have to remember; people would *expect* this to work. +We could omit support for `.await`. However, to users this may seem like an +arbitrary restriction. + # Prior art [prior-art]: #prior-art From 09626b5d99140b56ae24e68a8dc14c23edad4275 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Mon, 6 May 2024 17:10:20 +0200 Subject: [PATCH 6/9] Evaluation happens exactly once --- text/3626-format-args-implicit-dot.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/3626-format-args-implicit-dot.md b/text/3626-format-args-implicit-dot.md index 823e7518212..c62125745b2 100644 --- a/text/3626-format-args-implicit-dot.md +++ b/text/3626-format-args-implicit-dot.md @@ -131,9 +131,9 @@ format_args!("{unique_identifier}", unique_identifier=name.field1.field2) ``` Any `Deref` operations or `.await` operations associated with the `.` in each -format argument are evaluated from left-to-right as they appear in the format -string, at the point where the format string argument is evaluated, before the -positional or named arguments are evaluated. +format argument are evaluated exactly once, from left-to-right as they appear +in the format string, at the point where the format string argument is +evaluated, before the positional or named arguments are evaluated. If the identifier at the start of the chain does not exist in the scope, the usual error E0425 would be emitted by the compiler, with the span of that From 2e3d1b8a6c75bcd2a1e03bc9de5d03f4125dcefd Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Tue, 7 May 2024 10:01:41 +0200 Subject: [PATCH 7/9] No deduplication --- text/3626-format-args-implicit-dot.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/text/3626-format-args-implicit-dot.md b/text/3626-format-args-implicit-dot.md index c62125745b2..8d6e2c6104d 100644 --- a/text/3626-format-args-implicit-dot.md +++ b/text/3626-format-args-implicit-dot.md @@ -133,7 +133,9 @@ format_args!("{unique_identifier}", unique_identifier=name.field1.field2) Any `Deref` operations or `.await` operations associated with the `.` in each format argument are evaluated exactly once, from left-to-right as they appear in the format string, at the point where the format string argument is -evaluated, before the positional or named arguments are evaluated. +evaluated, before the positional or named arguments are evaluated. No +deduplication occurs: if `name.field` or `name.await` is mentioned multiple +times, it will be evaluated multiple times. If the identifier at the start of the chain does not exist in the scope, the usual error E0425 would be emitted by the compiler, with the span of that From 9f2e777985ad4a52b84c285765fead19823c2861 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Tue, 7 May 2024 10:03:53 +0200 Subject: [PATCH 8/9] Mention side effects --- text/3626-format-args-implicit-dot.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/text/3626-format-args-implicit-dot.md b/text/3626-format-args-implicit-dot.md index 8d6e2c6104d..8705c43b951 100644 --- a/text/3626-format-args-implicit-dot.md +++ b/text/3626-format-args-implicit-dot.md @@ -169,6 +169,10 @@ This adds incremental additional complexity to format strings. Having `x.y` available may make people assume other types of expressions work as well. +This introduces an additional mechanism to allow side-effects while evaluating +a format string. However, format strings could already cause side effects while +evaluating, if a `Display` or `Debug` implementation has side effects. + # Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives From 1fdd26025cd2c0c73792f82df1b7076794be9599 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Tue, 7 May 2024 10:15:35 +0200 Subject: [PATCH 9/9] Clarify that allowing `.` is a purely syntactic choice --- text/3626-format-args-implicit-dot.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/text/3626-format-args-implicit-dot.md b/text/3626-format-args-implicit-dot.md index 8705c43b951..4eca900d042 100644 --- a/text/3626-format-args-implicit-dot.md +++ b/text/3626-format-args-implicit-dot.md @@ -191,7 +191,8 @@ However, this would introduce an inconsistency that people have to remember; people would *expect* this to work. We could omit support for `.await`. However, to users this may seem like an -arbitrary restriction. +arbitrary restriction. The rationale for this RFC is purely *syntactic*, on the +basis that we can allow expressions using `.` without requiring delimiters. # Prior art [prior-art]: #prior-art