about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2023-03-16 13:46:52 +0000
committerbors <bors@rust-lang.org>2023-03-16 13:46:52 +0000
commit1203e0866e6c3659775efcb8aecad21dc13ef38b (patch)
tree649280a25f8efd4058120de4a4e104959b83e3f4
parent7ac4b82ddd596a218cac8cd6b88a91b54fcdcf13 (diff)
parenta08016514847d39ae3096ff41f98759abf59bee8 (diff)
downloadrust-1203e0866e6c3659775efcb8aecad21dc13ef38b.tar.gz
rust-1203e0866e6c3659775efcb8aecad21dc13ef38b.zip
Auto merge of #106824 - m-ou-se:format-args-flatten, r=oli-obk
Flatten/inline format_args!() and (string and int) literal arguments into format_args!()

Implements https://github.com/rust-lang/rust/issues/78356

Gated behind `-Zflatten-format-args=yes`.

Part of #99012

This change inlines string literals, integer literals and nested format_args!() into format_args!() during ast lowering, making all of the following pairs result in equivalent hir:

```rust
println!("Hello, {}!", "World");
println!("Hello, World!");
```

```rust
println!("[info] {}", format_args!("error"));
println!("[info] error");
```

```rust
println!("[{}] {}", status, format_args!("error: {}", msg));
println!("[{}] error: {}", status, msg);
```

```rust
println!("{} + {} = {}", 1, 2, 1 + 2);
println!("1 + 2 = {}", 1 + 2);
```

And so on.

This is useful for macros. E.g. a `log::info!()` macro could just pass the tokens from the user directly into a `format_args!()` that gets efficiently flattened/inlined into a `format_args!("info: {}")`.

It also means that `dbg!(x)` will have its file, line, and expression name inlined:

```rust
eprintln!("[{}:{}] {} = {:#?}", file!(), line!(), stringify!(x), x); // before
eprintln!("[example.rs:1] x = {:#?}", x); // after
```

Which can be nice in some cases, but also means a lot more unique static strings than before if dbg!() is used a lot.
-rw-r--r--compiler/rustc_ast/src/ast.rs9
-rw-r--r--compiler/rustc_ast/src/format.rs4
-rw-r--r--compiler/rustc_ast_lowering/src/format.rs238
-rw-r--r--compiler/rustc_interface/src/tests.rs1
-rw-r--r--compiler/rustc_session/src/options.rs3
-rw-r--r--compiler/rustc_span/src/symbol.rs1
-rw-r--r--library/core/src/fmt/mod.rs26
-rw-r--r--library/core/src/panicking.rs4
-rw-r--r--library/core/tests/fmt/mod.rs8
-rw-r--r--src/tools/clippy/clippy_utils/src/macros.rs21
-rw-r--r--tests/pretty/issue-4264.pp10
-rw-r--r--tests/rustdoc-ui/z-help.stdout1
-rw-r--r--tests/ui/borrowck/issue-64453.stderr2
-rw-r--r--tests/ui/consts/const-eval/format.rs3
-rw-r--r--tests/ui/consts/const-eval/format.stderr29
-rw-r--r--tests/ui/unpretty/flattened-format-args.rs8
-rw-r--r--tests/ui/unpretty/flattened-format-args.stdout16
17 files changed, 330 insertions, 54 deletions
diff --git a/compiler/rustc_ast/src/ast.rs b/compiler/rustc_ast/src/ast.rs
index 6503bf2bab7..5d164bc4b3c 100644
--- a/compiler/rustc_ast/src/ast.rs
+++ b/compiler/rustc_ast/src/ast.rs
@@ -1184,6 +1184,15 @@ impl Expr {
         expr
     }
 
+    pub fn peel_parens_and_refs(&self) -> &Expr {
+        let mut expr = self;
+        while let ExprKind::Paren(inner) | ExprKind::AddrOf(BorrowKind::Ref, _, inner) = &expr.kind
+        {
+            expr = inner;
+        }
+        expr
+    }
+
     /// Attempts to reparse as `Ty` (for diagnostic purposes).
     pub fn to_ty(&self) -> Option<P<Ty>> {
         let kind = match &self.kind {
diff --git a/compiler/rustc_ast/src/format.rs b/compiler/rustc_ast/src/format.rs
index d021bea5eca..356b9bb6371 100644
--- a/compiler/rustc_ast/src/format.rs
+++ b/compiler/rustc_ast/src/format.rs
@@ -131,8 +131,8 @@ impl FormatArguments {
         &self.arguments[..]
     }
 
-    pub fn all_args_mut(&mut self) -> &mut [FormatArgument] {
-        &mut self.arguments[..]
+    pub fn all_args_mut(&mut self) -> &mut Vec<FormatArgument> {
+        &mut self.arguments
     }
 }
 
diff --git a/compiler/rustc_ast_lowering/src/format.rs b/compiler/rustc_ast_lowering/src/format.rs
index 4095e225a80..72352b138cb 100644
--- a/compiler/rustc_ast_lowering/src/format.rs
+++ b/compiler/rustc_ast_lowering/src/format.rs
@@ -7,13 +7,172 @@ use rustc_hir as hir;
 use rustc_span::{
     sym,
     symbol::{kw, Ident},
-    Span,
+    Span, Symbol,
 };
+use std::borrow::Cow;
 
 impl<'hir> LoweringContext<'_, 'hir> {
     pub(crate) fn lower_format_args(&mut self, sp: Span, fmt: &FormatArgs) -> hir::ExprKind<'hir> {
-        expand_format_args(self, sp, fmt)
+        // Never call the const constructor of `fmt::Arguments` if the
+        // format_args!() had any arguments _before_ flattening/inlining.
+        let allow_const = fmt.arguments.all_args().is_empty();
+        let mut fmt = Cow::Borrowed(fmt);
+        if self.tcx.sess.opts.unstable_opts.flatten_format_args {
+            fmt = flatten_format_args(fmt);
+            fmt = inline_literals(fmt);
+        }
+        expand_format_args(self, sp, &fmt, allow_const)
+    }
+}
+
+/// Flattens nested `format_args!()` into one.
+///
+/// Turns
+///
+/// `format_args!("a {} {} {}.", 1, format_args!("b{}!", 2), 3)`
+///
+/// into
+///
+/// `format_args!("a {} b{}! {}.", 1, 2, 3)`.
+fn flatten_format_args(mut fmt: Cow<'_, FormatArgs>) -> Cow<'_, FormatArgs> {
+    let mut i = 0;
+    while i < fmt.template.len() {
+        if let FormatArgsPiece::Placeholder(placeholder) = &fmt.template[i]
+            && let FormatTrait::Display | FormatTrait::Debug = &placeholder.format_trait
+            && let Ok(arg_index) = placeholder.argument.index
+            && let arg = fmt.arguments.all_args()[arg_index].expr.peel_parens_and_refs()
+            && let ExprKind::FormatArgs(_) = &arg.kind
+            // Check that this argument is not used by any other placeholders.
+            && fmt.template.iter().enumerate().all(|(j, p)|
+                i == j ||
+                !matches!(p, FormatArgsPiece::Placeholder(placeholder)
+                    if placeholder.argument.index == Ok(arg_index))
+            )
+        {
+            // Now we need to mutate the outer FormatArgs.
+            // If this is the first time, this clones the outer FormatArgs.
+            let fmt = fmt.to_mut();
+
+            // Take the inner FormatArgs out of the outer arguments, and
+            // replace it by the inner arguments. (We can't just put those at
+            // the end, because we need to preserve the order of evaluation.)
+
+            let args = fmt.arguments.all_args_mut();
+            let remaining_args = args.split_off(arg_index + 1);
+            let old_arg_offset = args.len();
+            let mut fmt2 = &mut args.pop().unwrap().expr; // The inner FormatArgs.
+            let fmt2 = loop { // Unwrap the Expr to get to the FormatArgs.
+                match &mut fmt2.kind {
+                    ExprKind::Paren(inner) | ExprKind::AddrOf(BorrowKind::Ref, _, inner) => fmt2 = inner,
+                    ExprKind::FormatArgs(fmt2) => break fmt2,
+                    _ => unreachable!(),
+                }
+            };
+
+            args.append(fmt2.arguments.all_args_mut());
+            let new_arg_offset = args.len();
+            args.extend(remaining_args);
+
+            // Correct the indexes that refer to the arguments after the newly inserted arguments.
+            for_all_argument_indexes(&mut fmt.template, |index| {
+                if *index >= old_arg_offset {
+                    *index -= old_arg_offset;
+                    *index += new_arg_offset;
+                }
+            });
+
+            // Now merge the placeholders:
+
+            let rest = fmt.template.split_off(i + 1);
+            fmt.template.pop(); // remove the placeholder for the nested fmt args.
+            // Insert the pieces from the nested format args, but correct any
+            // placeholders to point to the correct argument index.
+            for_all_argument_indexes(&mut fmt2.template, |index| *index += arg_index);
+            fmt.template.append(&mut fmt2.template);
+            fmt.template.extend(rest);
+
+            // Don't increment `i` here, so we recurse into the newly added pieces.
+        } else {
+            i += 1;
+        }
     }
+    fmt
+}
+
+/// Inline literals into the format string.
+///
+/// Turns
+///
+/// `format_args!("Hello, {}! {} {}", "World", 123, x)`
+///
+/// into
+///
+/// `format_args!("Hello, World! 123 {}", x)`.
+fn inline_literals(mut fmt: Cow<'_, FormatArgs>) -> Cow<'_, FormatArgs> {
+    let mut was_inlined = vec![false; fmt.arguments.all_args().len()];
+    let mut inlined_anything = false;
+
+    for i in 0..fmt.template.len() {
+        let FormatArgsPiece::Placeholder(placeholder) = &fmt.template[i] else { continue };
+        let Ok(arg_index) = placeholder.argument.index else { continue };
+
+        let mut literal = None;
+
+        if let FormatTrait::Display = placeholder.format_trait
+            && placeholder.format_options == Default::default()
+            && let arg = fmt.arguments.all_args()[arg_index].expr.peel_parens_and_refs()
+            && let ExprKind::Lit(lit) = arg.kind
+        {
+            if let token::LitKind::Str | token::LitKind::StrRaw(_) = lit.kind
+                && let Ok(LitKind::Str(s, _)) = LitKind::from_token_lit(lit)
+            {
+                literal = Some(s);
+            } else if let token::LitKind::Integer = lit.kind
+                && let Ok(LitKind::Int(n, _)) = LitKind::from_token_lit(lit)
+            {
+                literal = Some(Symbol::intern(&n.to_string()));
+            }
+        }
+
+        if let Some(literal) = literal {
+            // Now we need to mutate the outer FormatArgs.
+            // If this is the first time, this clones the outer FormatArgs.
+            let fmt = fmt.to_mut();
+            // Replace the placeholder with the literal.
+            fmt.template[i] = FormatArgsPiece::Literal(literal);
+            was_inlined[arg_index] = true;
+            inlined_anything = true;
+        }
+    }
+
+    // Remove the arguments that were inlined.
+    if inlined_anything {
+        let fmt = fmt.to_mut();
+
+        let mut remove = was_inlined;
+
+        // Don't remove anything that's still used.
+        for_all_argument_indexes(&mut fmt.template, |index| remove[*index] = false);
+
+        // Drop all the arguments that are marked for removal.
+        let mut remove_it = remove.iter();
+        fmt.arguments.all_args_mut().retain(|_| remove_it.next() != Some(&true));
+
+        // Calculate the mapping of old to new indexes for the remaining arguments.
+        let index_map: Vec<usize> = remove
+            .into_iter()
+            .scan(0, |i, remove| {
+                let mapped = *i;
+                *i += !remove as usize;
+                Some(mapped)
+            })
+            .collect();
+
+        // Correct the indexes that refer to arguments that have shifted position.
+        for_all_argument_indexes(&mut fmt.template, |index| *index = index_map[*index]);
+    }
+
+    fmt
 }
 
 #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
@@ -189,11 +348,26 @@ fn expand_format_args<'hir>(
     ctx: &mut LoweringContext<'_, 'hir>,
     macsp: Span,
     fmt: &FormatArgs,
+    allow_const: bool,
 ) -> hir::ExprKind<'hir> {
+    let mut incomplete_lit = String::new();
     let lit_pieces =
         ctx.arena.alloc_from_iter(fmt.template.iter().enumerate().filter_map(|(i, piece)| {
             match piece {
-                &FormatArgsPiece::Literal(s) => Some(ctx.expr_str(fmt.span, s)),
+                &FormatArgsPiece::Literal(s) => {
+                    // Coalesce adjacent literal pieces.
+                    if let Some(FormatArgsPiece::Literal(_)) = fmt.template.get(i + 1) {
+                        incomplete_lit.push_str(s.as_str());
+                        None
+                    } else if !incomplete_lit.is_empty() {
+                        incomplete_lit.push_str(s.as_str());
+                        let s = Symbol::intern(&incomplete_lit);
+                        incomplete_lit.clear();
+                        Some(ctx.expr_str(fmt.span, s))
+                    } else {
+                        Some(ctx.expr_str(fmt.span, s))
+                    }
+                }
                 &FormatArgsPiece::Placeholder(_) => {
                     // Inject empty string before placeholders when not already preceded by a literal piece.
                     if i == 0 || matches!(fmt.template[i - 1], FormatArgsPiece::Placeholder(_)) {
@@ -244,6 +418,18 @@ fn expand_format_args<'hir>(
 
     let arguments = fmt.arguments.all_args();
 
+    if allow_const && arguments.is_empty() && argmap.is_empty() {
+        // Generate:
+        //     <core::fmt::Arguments>::new_const(lit_pieces)
+        let new = ctx.arena.alloc(ctx.expr_lang_item_type_relative(
+            macsp,
+            hir::LangItem::FormatArguments,
+            sym::new_const,
+        ));
+        let new_args = ctx.arena.alloc_from_iter([lit_pieces]);
+        return hir::ExprKind::Call(new, new_args);
+    }
+
     // If the args array contains exactly all the original arguments once,
     // in order, we can use a simple array instead of a `match` construction.
     // However, if there's a yield point in any argument except the first one,
@@ -290,25 +476,14 @@ fn expand_format_args<'hir>(
         let args_ident = Ident::new(sym::args, macsp);
         let (args_pat, args_hir_id) = ctx.pat_ident(macsp, args_ident);
         let args = ctx.arena.alloc_from_iter(argmap.iter().map(|&(arg_index, ty)| {
-            if let Some(arg) = arguments.get(arg_index) {
-                let sp = arg.expr.span.with_ctxt(macsp.ctxt());
-                let args_ident_expr = ctx.expr_ident(macsp, args_ident, args_hir_id);
-                let arg = ctx.arena.alloc(ctx.expr(
-                    sp,
-                    hir::ExprKind::Field(
-                        args_ident_expr,
-                        Ident::new(sym::integer(arg_index), macsp),
-                    ),
-                ));
-                make_argument(ctx, sp, arg, ty)
-            } else {
-                ctx.expr(
-                    macsp,
-                    hir::ExprKind::Err(
-                        ctx.tcx.sess.delay_span_bug(macsp, format!("no arg at {arg_index}")),
-                    ),
-                )
-            }
+            let arg = &arguments[arg_index];
+            let sp = arg.expr.span.with_ctxt(macsp.ctxt());
+            let args_ident_expr = ctx.expr_ident(macsp, args_ident, args_hir_id);
+            let arg = ctx.arena.alloc(ctx.expr(
+                sp,
+                hir::ExprKind::Field(args_ident_expr, Ident::new(sym::integer(arg_index), macsp)),
+            ));
+            make_argument(ctx, sp, arg, ty)
         }));
         let elements: Vec<_> = arguments
             .iter()
@@ -409,3 +584,22 @@ fn may_contain_yield_point(e: &ast::Expr) -> bool {
     visitor.visit_expr(e);
     visitor.0
 }
+
+fn for_all_argument_indexes(template: &mut [FormatArgsPiece], mut f: impl FnMut(&mut usize)) {
+    for piece in template {
+        let FormatArgsPiece::Placeholder(placeholder) = piece else { continue };
+        if let Ok(index) = &mut placeholder.argument.index {
+            f(index);
+        }
+        if let Some(FormatCount::Argument(FormatArgPosition { index: Ok(index), .. })) =
+            &mut placeholder.format_options.width
+        {
+            f(index);
+        }
+        if let Some(FormatCount::Argument(FormatArgPosition { index: Ok(index), .. })) =
+            &mut placeholder.format_options.precision
+        {
+            f(index);
+        }
+    }
+}
diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs
index 18d84a7023a..014810dba9c 100644
--- a/compiler/rustc_interface/src/tests.rs
+++ b/compiler/rustc_interface/src/tests.rs
@@ -744,6 +744,7 @@ fn test_unstable_options_tracking_hash() {
     tracked!(emit_thin_lto, false);
     tracked!(export_executable_symbols, true);
     tracked!(fewer_names, Some(true));
+    tracked!(flatten_format_args, true);
     tracked!(force_unstable_if_unmarked, true);
     tracked!(fuel, Some(("abc".to_string(), 99)));
     tracked!(function_sections, Some(false));
diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs
index b466a3fcdee..0548379dc2f 100644
--- a/compiler/rustc_session/src/options.rs
+++ b/compiler/rustc_session/src/options.rs
@@ -1422,6 +1422,9 @@ options! {
     fewer_names: Option<bool> = (None, parse_opt_bool, [TRACKED],
         "reduce memory use by retaining fewer names within compilation artifacts (LLVM-IR) \
         (default: no)"),
+    flatten_format_args: bool = (false, parse_bool, [TRACKED],
+        "flatten nested format_args!() and literals into a simplified format_args!() call \
+        (default: no)"),
     force_unstable_if_unmarked: bool = (false, parse_bool, [TRACKED],
         "force all crates to be `rustc_private` unstable (default: no)"),
     fuel: Option<(String, u64)> = (None, parse_optimization_fuel, [TRACKED],
diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs
index 0154c719ef6..abe5af8f9e0 100644
--- a/compiler/rustc_span/src/symbol.rs
+++ b/compiler/rustc_span/src/symbol.rs
@@ -984,6 +984,7 @@ symbols! {
         never_type_fallback,
         new,
         new_binary,
+        new_const,
         new_debug,
         new_display,
         new_lower_exp,
diff --git a/library/core/src/fmt/mod.rs b/library/core/src/fmt/mod.rs
index c9821bf8109..6d764237dc8 100644
--- a/library/core/src/fmt/mod.rs
+++ b/library/core/src/fmt/mod.rs
@@ -392,8 +392,31 @@ enum FlagV1 {
 }
 
 impl<'a> Arguments<'a> {
+    #[doc(hidden)]
+    #[inline]
+    #[unstable(feature = "fmt_internals", issue = "none")]
+    #[rustc_const_unstable(feature = "const_fmt_arguments_new", issue = "none")]
+    pub const fn new_const(pieces: &'a [&'static str]) -> Self {
+        if pieces.len() > 1 {
+            panic!("invalid args");
+        }
+        Arguments { pieces, fmt: None, args: &[] }
+    }
+
     /// When using the format_args!() macro, this function is used to generate the
     /// Arguments structure.
+    #[cfg(not(bootstrap))]
+    #[doc(hidden)]
+    #[inline]
+    #[unstable(feature = "fmt_internals", reason = "internal to format_args!", issue = "none")]
+    pub fn new_v1(pieces: &'a [&'static str], args: &'a [ArgumentV1<'a>]) -> Arguments<'a> {
+        if pieces.len() < args.len() || pieces.len() > args.len() + 1 {
+            panic!("invalid args");
+        }
+        Arguments { pieces, fmt: None, args }
+    }
+
+    #[cfg(bootstrap)]
     #[doc(hidden)]
     #[inline]
     #[unstable(feature = "fmt_internals", reason = "internal to format_args!", issue = "none")]
@@ -417,8 +440,7 @@ impl<'a> Arguments<'a> {
     #[doc(hidden)]
     #[inline]
     #[unstable(feature = "fmt_internals", reason = "internal to format_args!", issue = "none")]
-    #[rustc_const_unstable(feature = "const_fmt_arguments_new", issue = "none")]
-    pub const fn new_v1_formatted(
+    pub fn new_v1_formatted(
         pieces: &'a [&'static str],
         args: &'a [ArgumentV1<'a>],
         fmt: &'a [rt::v1::Argument],
diff --git a/library/core/src/panicking.rs b/library/core/src/panicking.rs
index 805a1e51ae9..dd0105c0eb4 100644
--- a/library/core/src/panicking.rs
+++ b/library/core/src/panicking.rs
@@ -111,7 +111,7 @@ pub const fn panic(expr: &'static str) -> ! {
     // truncation and padding (even though none is used here). Using
     // Arguments::new_v1 may allow the compiler to omit Formatter::pad from the
     // output binary, saving up to a few kilobytes.
-    panic_fmt(fmt::Arguments::new_v1(&[expr], &[]));
+    panic_fmt(fmt::Arguments::new_const(&[expr]));
 }
 
 /// Like `panic`, but without unwinding and track_caller to reduce the impact on codesize.
@@ -120,7 +120,7 @@ pub const fn panic(expr: &'static str) -> ! {
 #[lang = "panic_nounwind"] // needed by codegen for non-unwinding panics
 #[rustc_nounwind]
 pub fn panic_nounwind(expr: &'static str) -> ! {
-    panic_nounwind_fmt(fmt::Arguments::new_v1(&[expr], &[]));
+    panic_nounwind_fmt(fmt::Arguments::new_const(&[expr]));
 }
 
 #[inline]
diff --git a/library/core/tests/fmt/mod.rs b/library/core/tests/fmt/mod.rs
index 61807635813..c1c80c46c78 100644
--- a/library/core/tests/fmt/mod.rs
+++ b/library/core/tests/fmt/mod.rs
@@ -22,11 +22,11 @@ fn test_pointer_formats_data_pointer() {
 #[test]
 fn test_estimated_capacity() {
     assert_eq!(format_args!("").estimated_capacity(), 0);
-    assert_eq!(format_args!("{}", "").estimated_capacity(), 0);
+    assert_eq!(format_args!("{}", {""}).estimated_capacity(), 0);
     assert_eq!(format_args!("Hello").estimated_capacity(), 5);
-    assert_eq!(format_args!("Hello, {}!", "").estimated_capacity(), 16);
-    assert_eq!(format_args!("{}, hello!", "World").estimated_capacity(), 0);
-    assert_eq!(format_args!("{}. 16-bytes piece", "World").estimated_capacity(), 32);
+    assert_eq!(format_args!("Hello, {}!", {""}).estimated_capacity(), 16);
+    assert_eq!(format_args!("{}, hello!", {"World"}).estimated_capacity(), 0);
+    assert_eq!(format_args!("{}. 16-bytes piece", {"World"}).estimated_capacity(), 32);
 }
 
 #[test]
diff --git a/src/tools/clippy/clippy_utils/src/macros.rs b/src/tools/clippy/clippy_utils/src/macros.rs
index e135bd9feee..c0e32068eca 100644
--- a/src/tools/clippy/clippy_utils/src/macros.rs
+++ b/src/tools/clippy/clippy_utils/src/macros.rs
@@ -533,6 +533,14 @@ struct FormatArgsValues<'tcx> {
 }
 
 impl<'tcx> FormatArgsValues<'tcx> {
+    fn new_empty(format_string_span: SpanData) -> Self {
+        Self {
+            value_args: Vec::new(),
+            pos_to_value_index: Vec::new(),
+            format_string_span,
+        }
+    }
+
     fn new(args: &'tcx Expr<'tcx>, format_string_span: SpanData) -> Self {
         let mut pos_to_value_index = Vec::new();
         let mut value_args = Vec::new();
@@ -997,12 +1005,13 @@ impl<'tcx> FormatArgsExpn<'tcx> {
             .find(|&name| matches!(name, sym::const_format_args | sym::format_args | sym::format_args_nl))?;
         let newline = macro_name == sym::format_args_nl;
 
+        // ::core::fmt::Arguments::new_const(pieces)
         // ::core::fmt::Arguments::new_v1(pieces, args)
         // ::core::fmt::Arguments::new_v1_formatted(pieces, args, fmt, _unsafe_arg)
-        if let ExprKind::Call(callee, [pieces, args, rest @ ..]) = expr.kind
+        if let ExprKind::Call(callee, [pieces, rest @ ..]) = expr.kind
             && let ExprKind::Path(QPath::TypeRelative(ty, seg)) = callee.kind
             && let TyKind::Path(QPath::LangItem(LangItem::FormatArguments, _, _)) = ty.kind
-            && matches!(seg.ident.as_str(), "new_v1" | "new_v1_formatted")
+            && matches!(seg.ident.as_str(), "new_const" | "new_v1" | "new_v1_formatted")
         {
             let format_string = FormatString::new(cx, pieces)?;
 
@@ -1026,7 +1035,7 @@ impl<'tcx> FormatArgsExpn<'tcx> {
                 return None;
             }
 
-            let positions = if let Some(fmt_arg) = rest.first() {
+            let positions = if let Some(fmt_arg) = rest.get(1) {
                 // If the argument contains format specs, `new_v1_formatted(_, _, fmt, _)`, parse
                 // them.
 
@@ -1042,7 +1051,11 @@ impl<'tcx> FormatArgsExpn<'tcx> {
                 }))
             };
 
-            let values = FormatArgsValues::new(args, format_string.span.data());
+            let values = if let Some(args) = rest.first() {
+                FormatArgsValues::new(args, format_string.span.data())
+            } else {
+                FormatArgsValues::new_empty(format_string.span.data())
+            };
 
             let args = izip!(positions, parsed_args, parser.arg_places)
                 .map(|(position, parsed_arg, arg_span)| {
diff --git a/tests/pretty/issue-4264.pp b/tests/pretty/issue-4264.pp
index e0fa1fe2824..4020a433d62 100644
--- a/tests/pretty/issue-4264.pp
+++ b/tests/pretty/issue-4264.pp
@@ -32,13 +32,11 @@ fn bar() ({
         ({
                 let res =
                     ((::alloc::fmt::format as
-                            for<'a> fn(Arguments<'a>) -> String {format})(((<#[lang = "format_arguments"]>::new_v1
+                            for<'a> fn(Arguments<'a>) -> String {format})(((<#[lang = "format_arguments"]>::new_const
                                 as
-                                fn(&[&'static str], &[core::fmt::ArgumentV1<'_>]) -> Arguments<'_> {Arguments::<'_>::new_v1})((&([("test"
-                                            as &str)] as [&str; 1]) as &[&str; 1]),
-                            (&([] as [core::fmt::ArgumentV1<'_>; 0]) as
-                                &[core::fmt::ArgumentV1<'_>; 0])) as Arguments<'_>)) as
-                        String);
+                                fn(&[&'static str]) -> Arguments<'_> {Arguments::<'_>::new_const})((&([("test"
+                                            as &str)] as [&str; 1]) as &[&str; 1])) as Arguments<'_>))
+                        as String);
                 (res as String)
             } as String);
     } as ())
diff --git a/tests/rustdoc-ui/z-help.stdout b/tests/rustdoc-ui/z-help.stdout
index 79e6b94f1ac..5ad38e4fd98 100644
--- a/tests/rustdoc-ui/z-help.stdout
+++ b/tests/rustdoc-ui/z-help.stdout
@@ -44,6 +44,7 @@
     -Z             export-executable-symbols=val -- export symbols from executables, as if they were dynamic libraries
     -Z                 extra-const-ub-checks=val -- turns on more checks to detect const UB, which can be slow (default: no)
     -Z                           fewer-names=val -- reduce memory use by retaining fewer names within compilation artifacts (LLVM-IR) (default: no)
+    -Z                   flatten-format-args=val -- flatten nested format_args!() and literals into a simplified format_args!() call (default: no)
     -Z            force-unstable-if-unmarked=val -- force all crates to be `rustc_private` unstable (default: no)
     -Z                                  fuel=val -- set the optimization fuel quota for a crate
     -Z                     function-sections=val -- whether each function should go in its own section
diff --git a/tests/ui/borrowck/issue-64453.stderr b/tests/ui/borrowck/issue-64453.stderr
index 245c3a40e05..f032ea779dd 100644
--- a/tests/ui/borrowck/issue-64453.stderr
+++ b/tests/ui/borrowck/issue-64453.stderr
@@ -1,4 +1,4 @@
-error: `Arguments::<'a>::new_v1` is not yet stable as a const fn
+error: `Arguments::<'a>::new_const` is not yet stable as a const fn
   --> $DIR/issue-64453.rs:4:31
    |
 LL | static settings_dir: String = format!("");
diff --git a/tests/ui/consts/const-eval/format.rs b/tests/ui/consts/const-eval/format.rs
index 0d8b7c12d8a..5bdb2bf1954 100644
--- a/tests/ui/consts/const-eval/format.rs
+++ b/tests/ui/consts/const-eval/format.rs
@@ -1,12 +1,13 @@
 const fn failure() {
     panic!("{:?}", 0);
     //~^ ERROR cannot call non-const formatting macro in constant functions
+    //~| ERROR cannot call non-const fn `Arguments::<'_>::new_v1` in constant functions
 }
 
 const fn print() {
     println!("{:?}", 0);
     //~^ ERROR cannot call non-const formatting macro in constant functions
-    //~| ERROR `Arguments::<'a>::new_v1` is not yet stable as a const fn
+    //~| ERROR cannot call non-const fn `Arguments::<'_>::new_v1` in constant functions
     //~| ERROR cannot call non-const fn `_print` in constant functions
 }
 
diff --git a/tests/ui/consts/const-eval/format.stderr b/tests/ui/consts/const-eval/format.stderr
index 4bf39db5874..c39920d444d 100644
--- a/tests/ui/consts/const-eval/format.stderr
+++ b/tests/ui/consts/const-eval/format.stderr
@@ -7,8 +7,17 @@ LL |     panic!("{:?}", 0);
    = note: calls in constant functions are limited to constant functions, tuple structs and tuple variants
    = note: this error originates in the macro `$crate::const_format_args` which comes from the expansion of the macro `panic` (in Nightly builds, run with -Z macro-backtrace for more info)
 
+error[E0015]: cannot call non-const fn `Arguments::<'_>::new_v1` in constant functions
+  --> $DIR/format.rs:2:5
+   |
+LL |     panic!("{:?}", 0);
+   |     ^^^^^^^^^^^^^^^^^
+   |
+   = note: calls in constant functions are limited to constant functions, tuple structs and tuple variants
+   = note: this error originates in the macro `$crate::const_format_args` which comes from the expansion of the macro `panic` (in Nightly builds, run with -Z macro-backtrace for more info)
+
 error[E0015]: cannot call non-const formatting macro in constant functions
-  --> $DIR/format.rs:7:22
+  --> $DIR/format.rs:8:22
    |
 LL |     println!("{:?}", 0);
    |                      ^
@@ -16,17 +25,17 @@ LL |     println!("{:?}", 0);
    = note: calls in constant functions are limited to constant functions, tuple structs and tuple variants
    = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
 
-error: `Arguments::<'a>::new_v1` is not yet stable as a const fn
-  --> $DIR/format.rs:7:5
+error[E0015]: cannot call non-const fn `Arguments::<'_>::new_v1` in constant functions
+  --> $DIR/format.rs:8:5
    |
 LL |     println!("{:?}", 0);
    |     ^^^^^^^^^^^^^^^^^^^
    |
-   = help: add `#![feature(const_fmt_arguments_new)]` to the crate attributes to enable
+   = note: calls in constant functions are limited to constant functions, tuple structs and tuple variants
    = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
 
 error[E0015]: cannot call non-const fn `_print` in constant functions
-  --> $DIR/format.rs:7:5
+  --> $DIR/format.rs:8:5
    |
 LL |     println!("{:?}", 0);
    |     ^^^^^^^^^^^^^^^^^^^
@@ -63,19 +72,19 @@ LL |     panic!("{:?}", 0);
    = note: this note originates in the macro `$crate::const_format_args` which comes from the expansion of the macro `panic` (in Nightly builds, run with -Z macro-backtrace for more info)
 
 note: erroneous constant used
-  --> $DIR/format.rs:7:14
+  --> $DIR/format.rs:8:14
    |
 LL |     println!("{:?}", 0);
    |              ^^^^^^
 
 note: erroneous constant used
-  --> $DIR/format.rs:7:14
+  --> $DIR/format.rs:8:14
    |
 LL |     println!("{:?}", 0);
    |              ^^^^^^
 
 note: erroneous constant used
-  --> $DIR/format.rs:7:22
+  --> $DIR/format.rs:8:22
    |
 LL |     println!("{:?}", 0);
    |                      ^
@@ -83,13 +92,13 @@ LL |     println!("{:?}", 0);
    = note: this note originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
 
 note: erroneous constant used
-  --> $DIR/format.rs:7:22
+  --> $DIR/format.rs:8:22
    |
 LL |     println!("{:?}", 0);
    |                      ^
    |
    = note: this note originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
 
-error: aborting due to 4 previous errors
+error: aborting due to 5 previous errors
 
 For more information about this error, try `rustc --explain E0015`.
diff --git a/tests/ui/unpretty/flattened-format-args.rs b/tests/ui/unpretty/flattened-format-args.rs
new file mode 100644
index 00000000000..705dded169a
--- /dev/null
+++ b/tests/ui/unpretty/flattened-format-args.rs
@@ -0,0 +1,8 @@
+// compile-flags: -Zunpretty=hir -Zflatten-format-args=yes
+// check-pass
+
+fn main() {
+    let x = 1;
+    // Should flatten to println!("a 123 b {x} xyz\n"):
+    println!("a {} {}", format_args!("{} b {x}", 123), "xyz");
+}
diff --git a/tests/ui/unpretty/flattened-format-args.stdout b/tests/ui/unpretty/flattened-format-args.stdout
new file mode 100644
index 00000000000..a8fe8da0024
--- /dev/null
+++ b/tests/ui/unpretty/flattened-format-args.stdout
@@ -0,0 +1,16 @@
+#[prelude_import]
+use ::std::prelude::rust_2015::*;
+#[macro_use]
+extern crate std;
+// compile-flags: -Zunpretty=hir -Zflatten-format-args=yes
+// check-pass
+
+fn main() {
+        let x = 1;
+        // Should flatten to println!("a 123 b {x} xyz\n"):
+        {
+                ::std::io::_print(<#[lang = "format_arguments"]>::new_v1(&["a 123 b ",
+                                    " xyz\n"],
+                        &[<#[lang = "format_argument"]>::new_display(&x)]));
+            };
+    }