Skip to content

format_args deduplicates consts with interior mutability or destructor #145739

Description

@theemathas

I'm not sure if this is a bug or not.

I tried this code:

use std::fmt::{self, Display, Formatter};
use std::cell::Cell;

struct PrintCounter(Cell<usize>);

impl Display for PrintCounter {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        let counter = 1 + self.0.get();
        self.0.set(counter);
        write!(f, "{counter}")
    }
}

const ZERO: PrintCounter = PrintCounter(Cell::new(0));

fn main() {
    println!("{ZERO}, {ZERO}");
}

I expected the code to print 1, 1, since I am creating two separate PrintCounter values and printing them separately.

Instead, it printed 1, 2. The compiler seemed to have deduplicated the two mentions of ZERO into one mention.

Note that if the code is modified to be println!("{}, {}", ZERO, ZERO);, then it correctly prints 1, 1.

@rustbot labels +A-fmt

Meta

Reproducible on the playground with version 1.91.0-nightly (2025-08-20 040a98af70f0a7da03f3)

Related

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    A-fmtArea: `core::fmt`
    on Aug 22, 2025
  2. theemathas commented on Aug 22, 2025

    @theemathas
    ContributorAuthor

    It is possible that this is working as intended, although this behavior is very strange.

    RFC 2795 says:

    If this proposal were captured, the following (currently invalid) macro invocation: format_args!("hello {person}") would become a valid macro invocation, and would be equivalent to a shorthand for the already valid: format_args!("hello {person}", person=person)

    This desugaring would naturally result in deduplicating consts (the current behavior), since having multiple named arguments with the same name doesn't make much sense.

  3. theemathas commented on Aug 22, 2025

    @theemathas
    ContributorAuthor

    This also affects consts with destructors.

    The following code prints drop only once, instead of twice:

    #[derive(Debug)]
    struct LoudDrop;
    
    impl Drop for LoudDrop {
        fn drop(&mut self) {
            println!("drop");
        }
    }
    
    fn main() {
        println!("{LoudDrop:?}, {LoudDrop:?}");
    }

    @rustbot labels +A-const-eval +A-destructors

  4. added
    A-const-evalArea: Constant evaluation, covers all const contexts (static, const fn, ...)
    A-destructorsArea: Destructors (`Drop`, …)
    on Aug 22, 2025
  5. changed the title [-]`format_args` deduplicates consts with interior mutability[/-] [+]`format_args` deduplicates consts with interior mutability or destructor[/+] on Aug 22, 2025
  6. added
    T-langRelevant to the language team
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    on Aug 22, 2025
  7. kpreid commented on Aug 22, 2025

    @kpreid
    Contributor

    As I see it, this surprise comes from conflict between two different interpretations of the meaning of the syntax. Here are citations for both of these interpretations:

    1. RFC 2785 says:

      If this proposal were captured, the following (currently invalid) macro invocation:

      format_args!("hello {person}")

      would become a valid macro invocation, and would be equivalent to a shorthand for the already valid:

      format_args!("hello {person}", person=person)

      Under this interpretation, the current behavior is exactly correct: there can only be one key-value pair with a given key, so there is only one value expression to evaluate, too.

    2. On the other hand, the standard library documentation says:

      If a named parameter does not appear in the argument list, format! will reference a variable with that name in the current scope.

      This sentence can be taken as saying that captures should not be translated into key-value pairs, but are their own distinct thing which occurs only when there is not a matching key in the arguments. (Or, extremely pedantically, we could say that a const item is not a variable, and so this sentence gives no meaning to mentioning a const item in a format string. But, I think, nobody wants that.)

      Under this interpretation, the current behavior is incorrect.

    A factor worth noting is that interpretation 2 is more friendly to the possibility of expanding format strings to allow more kinds of expressions than single identifiers. (I personally think doing so would be unwise, but I don’t care to argue that point.)

  8. removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Aug 23, 2025
  9. added
    C-discussionCategory: Discussion or questions that doesn't represent real issues.
    I-lang-radarItems that are on lang's radar and will need eventual work or consideration.
    and removed
    C-bugCategory: This is a bug.
    on Aug 25, 2025
  10. 18 remaining items

  11. added
    I-lang-nominatedNominated for discussion during a lang team meeting.
    P-lang-drag-1Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang
    on Nov 24, 2025
  12. removed
    E-help-wantedCall for participation: Help is requested to fix this issue.
    on Nov 24, 2025
  13. added a commit that references this issue on Nov 25, 2025
  14. joshtriplett commented on Nov 26, 2025

    @joshtriplett
    Member

    @ShoyuVanilla Awesome, let us know if you need help, either here, on #149291, or on Zulip #t-lang. Right now we're going to wait on results from your crater experiment; if at any point you feel blocked please let us know so you don't have to wait on us.

  15. removed
    I-lang-nominatedNominated for discussion during a lang team meeting.
    P-lang-drag-1Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang
    on Nov 26, 2025
  16. ShoyuVanilla commented on Dec 14, 2025

    @ShoyuVanilla
    Member

    The crater result looks fine, so I've opened #149926

  17. m-ou-se commented on Dec 24, 2025

    @m-ou-se
    Member

    I don't think we should change this, but instead accept this as expected behaviour. See #149926 (comment):

    I don't think we should do this. It will make the generated code for println!("{x} {x}"); less efficient, as it will get two separate arguments instead of one.

    I don't want to end up in a situation where it would make sense for Clippy to suggest something like:

    warning: using the same placeholder multiple times is inefficient as of Rust 1.94.0
     --> src/main.rs:3:5
      |
    3 |     println!("{x} {x}");
      |     ^^^^^^^^^^^^^^^^^^^
      |
    help: change this to
      |
    3 -     println!("{x} {x}");
    3 +     println!("{x} {x}", x = x);
      |
    

    Adding , x = x shouldn't make a difference. If adding that makes the resulting code more efficient, I strongly feel like we've done something wrong.

  18. m-ou-se commented on Dec 24, 2025

    @m-ou-se
    Member

    Note that clippy today already warns about the code example at the top of this issue:

    warning: a `const` item should not be interior mutable
      --> src/main.rs:14:1
       |
    14 | const ZERO: PrintCounter = PrintCounter(Cell::new(0));
       | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       |
    
    warning: a `const` item with interior mutability should not be borrowed
      --> src/main.rs:17:16
       |
    17 |     println!("{ZERO}, {ZERO}");
       |                ^^^^
       |
    
  19. workingjubilee commented on Dec 25, 2025

    @workingjubilee
    Member

    The line in the documentation that misunderstands the behavior that was implemented for format_args! was written by @joshtriplett in #90473 which was also its stabilization PR.

    So it does not surprise me that it differs from what was implemented and RFCed, seeing as how it was not written by the person who wrote the RFC and implemented it initially, @davidhewitt.

    It does surprise me that T-lang would propose something for stabilization by adding documentation that disagrees with what was actually RFCed and implemented.

    I am aware that we have made them easier to encounter over time, but when did

    • non-idempotent Deref
    • interior mutability in consts
    • Drop impls in consts

    become good ideas that we wish to make everyone else's code worse for the sake of?

    The original RFC that added argument capturing said that it was intentionally avoiding expression interpolation, and there was considerable apprehension towards adding such, especially any form that has arbitrary extension to only limited expressions. I assume, given the author was @davidhewitt, that this was not due to a lack of experience with expression interpolation in a language, but rather because of a surplus of it.

    Unsurprisingly, if you try to smuggle some side-effectful code into the format arguments using existing mechanisms for smuggling side-effectful code, it is not exceptionally compatible with the semantics specified in his RFC. However, the code generated is what is preferable.

    This behavior seems fairly intentional to me, but perhaps the author will think it merely incidental.

  20. joshtriplett commented on Jan 21, 2026

    @joshtriplett
    Member

    The line in the documentation that misunderstands the behavior that was implemented for format_args! was written by @joshtriplett in #90473 which was also its stabilization PR.

    It is not clear to me what line you are referring to. All of the documentation at https://github.com/rust-lang/rust/pull/90473/changes#diff-f299871a8b9896a7ad00ea2a572067b625a3323d34c3152d9ab44da65b491e0c appears to be accurate by either interpretation of format_args!; none of it depends on the interpretation.

    The original RFC that added argument capturing said that it was intentionally avoiding expression interpolation, and there was considerable apprehension towards adding such, especially any form that has arbitrary extension to only limited expressions. I assume, given the author was @davidhewitt, that this was not due to a lack of experience with expression interpolation in a language, but rather because of a surplus of it.

    My impression was that that apprehension was not from the original author, but rather from the lang team not wanting to extend to arbitrary expressions for reasons of complexity.

  21. joshtriplett commented on Jan 21, 2026

    @joshtriplett
    Member

    The previous outcome of the lang discussion, to remove this deduplication (for consistency with function arguments) would continue to be my preference, and I'd likewise propose @traviscross's solution of only deduplicating when not observable. But for the record, I would also not object to adjusting rust-lang/rfcs#3626 to be consistent with deduplication; I don't want to block that RFC on this change, and I only updated that RFC in anticipation of this change because lang did not anticipate this change being contentious.

  22. workingjubilee commented on Jan 28, 2026

    @workingjubilee
    Member

    @joshtriplett:

    It is not clear to me what line you are referring to. All of the documentation at https://github.com/rust-lang/rust/pull/90473/changes#diff-f299871a8b9896a7ad00ea2a572067b625a3323d34c3152d9ab44da65b491e0c appears to be accurate by either interpretation of format_args!; none of it depends on the interpretation.

    I am not going to repeat @kpreid's argument: #145739 (comment)

    My impression was that that apprehension was not from the original author, but rather from the lang team not wanting to extend to arbitrary expressions for reasons of complexity.

    The author also specifically distanced his proposal from any that involved inclusion of arbitrary expressions (broadly called "interpolation" in the RFC discussion) and suggested they can result in poor style: rust-lang/rfcs#2795 (comment)

    This was raised before anyone on T-lang did so.

    Indeed the discussion involved quite a bit on the matter of "what if interpolation?" by almost everyone who was commenting who wasn't on T-lang before T-lang weighed in with any significance. Perhaps that was merely in the interest of avoiding scope creep, but I don't think it is accurate to characterize that part as even mostly T-lang's idea.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-const-evalArea: Constant evaluation, covers all const contexts (static, const fn, ...)A-destructorsArea: Destructors (`Drop`, …)A-fmtArea: `core::fmt`C-discussionCategory: Discussion or questions that doesn't represent real issues.I-lang-radarItems that are on lang's radar and will need eventual work or consideration.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-langRelevant to the language team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions