Skip to content

Commit 1203e08

Browse files
committed
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 #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.
2 parents 7ac4b82 + a080165 commit 1203e08

File tree

17 files changed

+330
-54
lines changed

17 files changed

+330
-54
lines changed

compiler/rustc_ast/src/ast.rs

+9
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,15 @@ impl Expr {
11841184
expr
11851185
}
11861186

1187+
pub fn peel_parens_and_refs(&self) -> &Expr {
1188+
let mut expr = self;
1189+
while let ExprKind::Paren(inner) | ExprKind::AddrOf(BorrowKind::Ref, _, inner) = &expr.kind
1190+
{
1191+
expr = inner;
1192+
}
1193+
expr
1194+
}
1195+
11871196
/// Attempts to reparse as `Ty` (for diagnostic purposes).
11881197
pub fn to_ty(&self) -> Option<P<Ty>> {
11891198
let kind = match &self.kind {

compiler/rustc_ast/src/format.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ impl FormatArguments {
131131
&self.arguments[..]
132132
}
133133

134-
pub fn all_args_mut(&mut self) -> &mut [FormatArgument] {
135-
&mut self.arguments[..]
134+
pub fn all_args_mut(&mut self) -> &mut Vec<FormatArgument> {
135+
&mut self.arguments
136136
}
137137
}
138138

compiler/rustc_ast_lowering/src/format.rs

+216-22
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,172 @@ use rustc_hir as hir;
77
use rustc_span::{
88
sym,
99
symbol::{kw, Ident},
10-
Span,
10+
Span, Symbol,
1111
};
12+
use std::borrow::Cow;
1213

1314
impl<'hir> LoweringContext<'_, 'hir> {
1415
pub(crate) fn lower_format_args(&mut self, sp: Span, fmt: &FormatArgs) -> hir::ExprKind<'hir> {
15-
expand_format_args(self, sp, fmt)
16+
// Never call the const constructor of `fmt::Arguments` if the
17+
// format_args!() had any arguments _before_ flattening/inlining.
18+
let allow_const = fmt.arguments.all_args().is_empty();
19+
let mut fmt = Cow::Borrowed(fmt);
20+
if self.tcx.sess.opts.unstable_opts.flatten_format_args {
21+
fmt = flatten_format_args(fmt);
22+
fmt = inline_literals(fmt);
23+
}
24+
expand_format_args(self, sp, &fmt, allow_const)
25+
}
26+
}
27+
28+
/// Flattens nested `format_args!()` into one.
29+
///
30+
/// Turns
31+
///
32+
/// `format_args!("a {} {} {}.", 1, format_args!("b{}!", 2), 3)`
33+
///
34+
/// into
35+
///
36+
/// `format_args!("a {} b{}! {}.", 1, 2, 3)`.
37+
fn flatten_format_args(mut fmt: Cow<'_, FormatArgs>) -> Cow<'_, FormatArgs> {
38+
let mut i = 0;
39+
while i < fmt.template.len() {
40+
if let FormatArgsPiece::Placeholder(placeholder) = &fmt.template[i]
41+
&& let FormatTrait::Display | FormatTrait::Debug = &placeholder.format_trait
42+
&& let Ok(arg_index) = placeholder.argument.index
43+
&& let arg = fmt.arguments.all_args()[arg_index].expr.peel_parens_and_refs()
44+
&& let ExprKind::FormatArgs(_) = &arg.kind
45+
// Check that this argument is not used by any other placeholders.
46+
&& fmt.template.iter().enumerate().all(|(j, p)|
47+
i == j ||
48+
!matches!(p, FormatArgsPiece::Placeholder(placeholder)
49+
if placeholder.argument.index == Ok(arg_index))
50+
)
51+
{
52+
// Now we need to mutate the outer FormatArgs.
53+
// If this is the first time, this clones the outer FormatArgs.
54+
let fmt = fmt.to_mut();
55+
56+
// Take the inner FormatArgs out of the outer arguments, and
57+
// replace it by the inner arguments. (We can't just put those at
58+
// the end, because we need to preserve the order of evaluation.)
59+
60+
let args = fmt.arguments.all_args_mut();
61+
let remaining_args = args.split_off(arg_index + 1);
62+
let old_arg_offset = args.len();
63+
let mut fmt2 = &mut args.pop().unwrap().expr; // The inner FormatArgs.
64+
let fmt2 = loop { // Unwrap the Expr to get to the FormatArgs.
65+
match &mut fmt2.kind {
66+
ExprKind::Paren(inner) | ExprKind::AddrOf(BorrowKind::Ref, _, inner) => fmt2 = inner,
67+
ExprKind::FormatArgs(fmt2) => break fmt2,
68+
_ => unreachable!(),
69+
}
70+
};
71+
72+
args.append(fmt2.arguments.all_args_mut());
73+
let new_arg_offset = args.len();
74+
args.extend(remaining_args);
75+
76+
// Correct the indexes that refer to the arguments after the newly inserted arguments.
77+
for_all_argument_indexes(&mut fmt.template, |index| {
78+
if *index >= old_arg_offset {
79+
*index -= old_arg_offset;
80+
*index += new_arg_offset;
81+
}
82+
});
83+
84+
// Now merge the placeholders:
85+
86+
let rest = fmt.template.split_off(i + 1);
87+
fmt.template.pop(); // remove the placeholder for the nested fmt args.
88+
// Insert the pieces from the nested format args, but correct any
89+
// placeholders to point to the correct argument index.
90+
for_all_argument_indexes(&mut fmt2.template, |index| *index += arg_index);
91+
fmt.template.append(&mut fmt2.template);
92+
fmt.template.extend(rest);
93+
94+
// Don't increment `i` here, so we recurse into the newly added pieces.
95+
} else {
96+
i += 1;
97+
}
1698
}
99+
fmt
100+
}
101+
102+
/// Inline literals into the format string.
103+
///
104+
/// Turns
105+
///
106+
/// `format_args!("Hello, {}! {} {}", "World", 123, x)`
107+
///
108+
/// into
109+
///
110+
/// `format_args!("Hello, World! 123 {}", x)`.
111+
fn inline_literals(mut fmt: Cow<'_, FormatArgs>) -> Cow<'_, FormatArgs> {
112+
let mut was_inlined = vec![false; fmt.arguments.all_args().len()];
113+
let mut inlined_anything = false;
114+
115+
for i in 0..fmt.template.len() {
116+
let FormatArgsPiece::Placeholder(placeholder) = &fmt.template[i] else { continue };
117+
let Ok(arg_index) = placeholder.argument.index else { continue };
118+
119+
let mut literal = None;
120+
121+
if let FormatTrait::Display = placeholder.format_trait
122+
&& placeholder.format_options == Default::default()
123+
&& let arg = fmt.arguments.all_args()[arg_index].expr.peel_parens_and_refs()
124+
&& let ExprKind::Lit(lit) = arg.kind
125+
{
126+
if let token::LitKind::Str | token::LitKind::StrRaw(_) = lit.kind
127+
&& let Ok(LitKind::Str(s, _)) = LitKind::from_token_lit(lit)
128+
{
129+
literal = Some(s);
130+
} else if let token::LitKind::Integer = lit.kind
131+
&& let Ok(LitKind::Int(n, _)) = LitKind::from_token_lit(lit)
132+
{
133+
literal = Some(Symbol::intern(&n.to_string()));
134+
}
135+
}
136+
137+
if let Some(literal) = literal {
138+
// Now we need to mutate the outer FormatArgs.
139+
// If this is the first time, this clones the outer FormatArgs.
140+
let fmt = fmt.to_mut();
141+
// Replace the placeholder with the literal.
142+
fmt.template[i] = FormatArgsPiece::Literal(literal);
143+
was_inlined[arg_index] = true;
144+
inlined_anything = true;
145+
}
146+
}
147+
148+
// Remove the arguments that were inlined.
149+
if inlined_anything {
150+
let fmt = fmt.to_mut();
151+
152+
let mut remove = was_inlined;
153+
154+
// Don't remove anything that's still used.
155+
for_all_argument_indexes(&mut fmt.template, |index| remove[*index] = false);
156+
157+
// Drop all the arguments that are marked for removal.
158+
let mut remove_it = remove.iter();
159+
fmt.arguments.all_args_mut().retain(|_| remove_it.next() != Some(&true));
160+
161+
// Calculate the mapping of old to new indexes for the remaining arguments.
162+
let index_map: Vec<usize> = remove
163+
.into_iter()
164+
.scan(0, |i, remove| {
165+
let mapped = *i;
166+
*i += !remove as usize;
167+
Some(mapped)
168+
})
169+
.collect();
170+
171+
// Correct the indexes that refer to arguments that have shifted position.
172+
for_all_argument_indexes(&mut fmt.template, |index| *index = index_map[*index]);
173+
}
174+
175+
fmt
17176
}
18177

19178
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
@@ -189,11 +348,26 @@ fn expand_format_args<'hir>(
189348
ctx: &mut LoweringContext<'_, 'hir>,
190349
macsp: Span,
191350
fmt: &FormatArgs,
351+
allow_const: bool,
192352
) -> hir::ExprKind<'hir> {
353+
let mut incomplete_lit = String::new();
193354
let lit_pieces =
194355
ctx.arena.alloc_from_iter(fmt.template.iter().enumerate().filter_map(|(i, piece)| {
195356
match piece {
196-
&FormatArgsPiece::Literal(s) => Some(ctx.expr_str(fmt.span, s)),
357+
&FormatArgsPiece::Literal(s) => {
358+
// Coalesce adjacent literal pieces.
359+
if let Some(FormatArgsPiece::Literal(_)) = fmt.template.get(i + 1) {
360+
incomplete_lit.push_str(s.as_str());
361+
None
362+
} else if !incomplete_lit.is_empty() {
363+
incomplete_lit.push_str(s.as_str());
364+
let s = Symbol::intern(&incomplete_lit);
365+
incomplete_lit.clear();
366+
Some(ctx.expr_str(fmt.span, s))
367+
} else {
368+
Some(ctx.expr_str(fmt.span, s))
369+
}
370+
}
197371
&FormatArgsPiece::Placeholder(_) => {
198372
// Inject empty string before placeholders when not already preceded by a literal piece.
199373
if i == 0 || matches!(fmt.template[i - 1], FormatArgsPiece::Placeholder(_)) {
@@ -244,6 +418,18 @@ fn expand_format_args<'hir>(
244418

245419
let arguments = fmt.arguments.all_args();
246420

421+
if allow_const && arguments.is_empty() && argmap.is_empty() {
422+
// Generate:
423+
// <core::fmt::Arguments>::new_const(lit_pieces)
424+
let new = ctx.arena.alloc(ctx.expr_lang_item_type_relative(
425+
macsp,
426+
hir::LangItem::FormatArguments,
427+
sym::new_const,
428+
));
429+
let new_args = ctx.arena.alloc_from_iter([lit_pieces]);
430+
return hir::ExprKind::Call(new, new_args);
431+
}
432+
247433
// If the args array contains exactly all the original arguments once,
248434
// in order, we can use a simple array instead of a `match` construction.
249435
// However, if there's a yield point in any argument except the first one,
@@ -290,25 +476,14 @@ fn expand_format_args<'hir>(
290476
let args_ident = Ident::new(sym::args, macsp);
291477
let (args_pat, args_hir_id) = ctx.pat_ident(macsp, args_ident);
292478
let args = ctx.arena.alloc_from_iter(argmap.iter().map(|&(arg_index, ty)| {
293-
if let Some(arg) = arguments.get(arg_index) {
294-
let sp = arg.expr.span.with_ctxt(macsp.ctxt());
295-
let args_ident_expr = ctx.expr_ident(macsp, args_ident, args_hir_id);
296-
let arg = ctx.arena.alloc(ctx.expr(
297-
sp,
298-
hir::ExprKind::Field(
299-
args_ident_expr,
300-
Ident::new(sym::integer(arg_index), macsp),
301-
),
302-
));
303-
make_argument(ctx, sp, arg, ty)
304-
} else {
305-
ctx.expr(
306-
macsp,
307-
hir::ExprKind::Err(
308-
ctx.tcx.sess.delay_span_bug(macsp, format!("no arg at {arg_index}")),
309-
),
310-
)
311-
}
479+
let arg = &arguments[arg_index];
480+
let sp = arg.expr.span.with_ctxt(macsp.ctxt());
481+
let args_ident_expr = ctx.expr_ident(macsp, args_ident, args_hir_id);
482+
let arg = ctx.arena.alloc(ctx.expr(
483+
sp,
484+
hir::ExprKind::Field(args_ident_expr, Ident::new(sym::integer(arg_index), macsp)),
485+
));
486+
make_argument(ctx, sp, arg, ty)
312487
}));
313488
let elements: Vec<_> = arguments
314489
.iter()
@@ -409,3 +584,22 @@ fn may_contain_yield_point(e: &ast::Expr) -> bool {
409584
visitor.visit_expr(e);
410585
visitor.0
411586
}
587+
588+
fn for_all_argument_indexes(template: &mut [FormatArgsPiece], mut f: impl FnMut(&mut usize)) {
589+
for piece in template {
590+
let FormatArgsPiece::Placeholder(placeholder) = piece else { continue };
591+
if let Ok(index) = &mut placeholder.argument.index {
592+
f(index);
593+
}
594+
if let Some(FormatCount::Argument(FormatArgPosition { index: Ok(index), .. })) =
595+
&mut placeholder.format_options.width
596+
{
597+
f(index);
598+
}
599+
if let Some(FormatCount::Argument(FormatArgPosition { index: Ok(index), .. })) =
600+
&mut placeholder.format_options.precision
601+
{
602+
f(index);
603+
}
604+
}
605+
}

compiler/rustc_interface/src/tests.rs

+1
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,7 @@ fn test_unstable_options_tracking_hash() {
744744
tracked!(emit_thin_lto, false);
745745
tracked!(export_executable_symbols, true);
746746
tracked!(fewer_names, Some(true));
747+
tracked!(flatten_format_args, true);
747748
tracked!(force_unstable_if_unmarked, true);
748749
tracked!(fuel, Some(("abc".to_string(), 99)));
749750
tracked!(function_sections, Some(false));

compiler/rustc_session/src/options.rs

+3
Original file line numberDiff line numberDiff line change
@@ -1422,6 +1422,9 @@ options! {
14221422
fewer_names: Option<bool> = (None, parse_opt_bool, [TRACKED],
14231423
"reduce memory use by retaining fewer names within compilation artifacts (LLVM-IR) \
14241424
(default: no)"),
1425+
flatten_format_args: bool = (false, parse_bool, [TRACKED],
1426+
"flatten nested format_args!() and literals into a simplified format_args!() call \
1427+
(default: no)"),
14251428
force_unstable_if_unmarked: bool = (false, parse_bool, [TRACKED],
14261429
"force all crates to be `rustc_private` unstable (default: no)"),
14271430
fuel: Option<(String, u64)> = (None, parse_optimization_fuel, [TRACKED],

compiler/rustc_span/src/symbol.rs

+1
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,7 @@ symbols! {
984984
never_type_fallback,
985985
new,
986986
new_binary,
987+
new_const,
987988
new_debug,
988989
new_display,
989990
new_lower_exp,

library/core/src/fmt/mod.rs

+24-2
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,31 @@ enum FlagV1 {
392392
}
393393

394394
impl<'a> Arguments<'a> {
395+
#[doc(hidden)]
396+
#[inline]
397+
#[unstable(feature = "fmt_internals", issue = "none")]
398+
#[rustc_const_unstable(feature = "const_fmt_arguments_new", issue = "none")]
399+
pub const fn new_const(pieces: &'a [&'static str]) -> Self {
400+
if pieces.len() > 1 {
401+
panic!("invalid args");
402+
}
403+
Arguments { pieces, fmt: None, args: &[] }
404+
}
405+
395406
/// When using the format_args!() macro, this function is used to generate the
396407
/// Arguments structure.
408+
#[cfg(not(bootstrap))]
409+
#[doc(hidden)]
410+
#[inline]
411+
#[unstable(feature = "fmt_internals", reason = "internal to format_args!", issue = "none")]
412+
pub fn new_v1(pieces: &'a [&'static str], args: &'a [ArgumentV1<'a>]) -> Arguments<'a> {
413+
if pieces.len() < args.len() || pieces.len() > args.len() + 1 {
414+
panic!("invalid args");
415+
}
416+
Arguments { pieces, fmt: None, args }
417+
}
418+
419+
#[cfg(bootstrap)]
397420
#[doc(hidden)]
398421
#[inline]
399422
#[unstable(feature = "fmt_internals", reason = "internal to format_args!", issue = "none")]
@@ -417,8 +440,7 @@ impl<'a> Arguments<'a> {
417440
#[doc(hidden)]
418441
#[inline]
419442
#[unstable(feature = "fmt_internals", reason = "internal to format_args!", issue = "none")]
420-
#[rustc_const_unstable(feature = "const_fmt_arguments_new", issue = "none")]
421-
pub const fn new_v1_formatted(
443+
pub fn new_v1_formatted(
422444
pieces: &'a [&'static str],
423445
args: &'a [ArgumentV1<'a>],
424446
fmt: &'a [rt::v1::Argument],

library/core/src/panicking.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ pub const fn panic(expr: &'static str) -> ! {
111111
// truncation and padding (even though none is used here). Using
112112
// Arguments::new_v1 may allow the compiler to omit Formatter::pad from the
113113
// output binary, saving up to a few kilobytes.
114-
panic_fmt(fmt::Arguments::new_v1(&[expr], &[]));
114+
panic_fmt(fmt::Arguments::new_const(&[expr]));
115115
}
116116

117117
/// 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) -> ! {
120120
#[lang = "panic_nounwind"] // needed by codegen for non-unwinding panics
121121
#[rustc_nounwind]
122122
pub fn panic_nounwind(expr: &'static str) -> ! {
123-
panic_nounwind_fmt(fmt::Arguments::new_v1(&[expr], &[]));
123+
panic_nounwind_fmt(fmt::Arguments::new_const(&[expr]));
124124
}
125125

126126
#[inline]

0 commit comments

Comments
 (0)