Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions compiler/rustc_const_eval/src/util/type_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ impl<'tcx> PrettyPrinter<'tcx> for TypeNamePrinter<'tcx> {
// `std::any::type_name` should never print verbose type names
false
}

// `type_name` output is user-observable; keep it byte-stable here even
// though the docs permit drift.
fn add_disambiguating_parens_in_prefix_position(&self) -> bool {
false
}

@cjgillot cjgillot May 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the docs permit drift, I don't see why we should bother keeping wrong code.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

}

impl Write for TypeNamePrinter<'_> {
Expand Down
130 changes: 128 additions & 2 deletions compiler/rustc_middle/src/ty/print/pretty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,15 @@ pub trait PrettyPrinter<'tcx>: Printer<'tcx> + fmt::Write {
/// will always be printed.)
fn should_print_optional_region(&self, region: ty::Region<'tcx>) -> bool;

/// Whether `pretty_print_type` should wrap a multi-bound `impl` / `dyn`
/// inner in parens after a prefix type constructor (`&`, `&mut`, `*const`,
/// `*mut`), so the output is `&(impl A + B)` rather than the parser-
/// ambiguous `&impl A + B`. Byte-stable printers (mangling, `type_name`)
/// override this to `false`.
fn add_disambiguating_parens_in_prefix_position(&self) -> bool {
true
}

fn reset_type_limit(&mut self) {}

// Defaults (should not be overridden):
Expand Down Expand Up @@ -723,15 +732,17 @@ pub trait PrettyPrinter<'tcx>: Printer<'tcx> + fmt::Write {
}
ty::RawPtr(ty, mutbl) => {
write!(self, "*{} ", mutbl.ptr_str())?;
ty.print(self)?;
self.print_inner_with_disambiguating_parens(ty)?;
}
ty::Ref(r, ty, mutbl) => {
write!(self, "&")?;
if self.should_print_optional_region(r) {
r.print(self)?;
write!(self, " ")?;
}
ty::TypeAndMut { ty, mutbl }.print(self)?;
// `&mut (impl A + B)`, not `&(mut impl A + B)`: emit `mut ` before the parens.
write!(self, "{}", mutbl.prefix_str())?;
self.print_inner_with_disambiguating_parens(ty)?;
}
ty::Never => write!(self, "!")?,
ty::Tuple(tys) => {
Expand Down Expand Up @@ -1071,6 +1082,121 @@ pub trait PrettyPrinter<'tcx>: Printer<'tcx> + fmt::Write {
Ok(())
}

/// Prints `ty` after a prefix type constructor, wrapping in parens iff
/// [`Self::inner_needs_disambiguating_parens`] returns `true`.
fn print_inner_with_disambiguating_parens(&mut self, ty: Ty<'tcx>) -> Result<(), PrintError> {
let need_paren = self.inner_needs_disambiguating_parens(ty);
if need_paren {
write!(self, "(")?;
}
ty.print(self)?;
if need_paren {
write!(self, ")")?;
}
Ok(())
}

/// Whether `ty`, sitting right after a prefix type constructor, would
/// print with a top-level `+`. Without the wrap, `&impl A + B` parses as
/// the ambiguous `(&impl A) + B`.
fn inner_needs_disambiguating_parens(&self, ty: Ty<'tcx>) -> bool {
if !self.add_disambiguating_parens_in_prefix_position() || self.should_print_verbose() {
return false;
}
match ty.kind() {
// RPITIT trait-side appears as `Projection`, not `Opaque`.
ty::Alias(ty::AliasTy { kind: ty::Opaque { def_id }, args, .. }) => {
self.opaque_has_multiple_bounds(*def_id, args)
}
ty::Alias(ty::AliasTy { kind: ty::Projection { def_id }, args, .. })
if self.tcx().is_impl_trait_in_trait(*def_id) =>
{
self.opaque_has_multiple_bounds(*def_id, args)
}
ty::Dynamic(predicates, region) => {
if self.should_print_optional_region(*region) {
// `ty::Dynamic` self-wraps when it has an explicit region.
false

@cjgillot cjgillot May 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we return true here, and remove the self-wrap from ty::Dynamic?

View changes since the review

@onehr onehr May 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my understanding (correct me if I'm wrong):

First: Fn(..) -> T + B hits the same +-ambiguity as &T + B -- the parser refuses to commit and emits error: ambiguous + in a type. So the same disambiguation hook also runs from pretty_print_fn_sig's output position, and the trait method is renamed add_disambiguating_parens since it now covers both prefix and return slots. Concrete: Box<dyn Fn() -> dyn Trait> defaults the inner dyn's lifetime to 'static, so dropping the self-wrap alone would print it as Box<dyn Fn() -> dyn Trait + 'static> -- parser-rejected. The fn-sig wrap restores Box<dyn Fn() -> (dyn Trait + 'static)>, which round-trips.

Second: the cleanup is broader than the prefix-position fix. Box<dyn A + 'a>, [dyn A + 'a; N], top-level dyn etc. drop the parens that the self-wrap used to add defensively in positions where the parser does not need them. The last commit touches 100+ files -- 8 stderr/fixed gain prefix or fn-return parens (the actual correctness fix), 3 carry both kinds of change, ~100 stderr/fixed drop defensive parens, and 26 .rs files get inline //~ ERROR annotation updates to match.

I put the whole change in a single commit on top of the commits for type_name and identity-instantiation ones, so if the scope is too wide for this PR it can be dropped

} else {
// Projections inline into the principal as `<Item = X>`;
// only principal/auto traits produce a top-level `+`.
predicates
.iter()
.filter(|pred| {
matches!(
pred.skip_binder(),
ty::ExistentialPredicate::Trait(_)
| ty::ExistentialPredicate::AutoTrait(_)
)
})
.count()
> 1
}
}
_ => false,
}
}

/// Whether `Alias(Opaque)` would print with more than one top-level
/// `+`-joined component. Must stay in sync with
/// `pretty_print_opaque_impl_type`'s sized-bound handling. `?Sized` and
/// the synthetic `Sized` / `?Sized` / `MetaSized` / `PointeeSized` suffix
/// each contribute one top-level joinable component. Regressions land in
/// `tests/ui/impl-trait/in-trait/refine-rustfix-parens.rs`.
fn opaque_has_multiple_bounds(&self, def_id: DefId, args: ty::GenericArgsRef<'tcx>) -> bool {
let tcx = self.tcx();
let bounds = tcx.explicit_item_bounds(def_id);

// Mirror `pretty_print_opaque_impl_type`'s sized-bound handling:
// positive `Sized` and `MetaSized` are absorbed into the synthetic
// suffix below; negative `Sized` (`?Sized`) falls through and is
// printed inline.
let mut trait_emits = 0usize;
let mut lifetimes_count = 0usize;
let mut has_sized_bound = false;
let mut has_negative_sized_bound = false;
let mut has_meta_sized_bound = false;

for (predicate, _) in
bounds.iter_instantiated_copied(tcx, args).map(Unnormalized::skip_norm_wip)

@cjgillot cjgillot May 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to instantiate? As we are looking at the clause kind, is identity instantiation enough?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, identity is enough -- only the clause kinds (Trait def_id + polarity, TypeOutlives) are inspected, all invariant under instantiation. Switched to iter_identity_copied() and dropped the args parameter

{
match predicate.kind().skip_binder() {
ty::ClauseKind::Trait(pred) => match tcx.as_lang_item(pred.def_id()) {
Some(LangItem::Sized) => match pred.polarity {
ty::PredicatePolarity::Positive => {
has_sized_bound = true;
}
ty::PredicatePolarity::Negative => {
has_negative_sized_bound = true;
trait_emits += 1;
}
},
Some(LangItem::MetaSized) => {
has_meta_sized_bound = true;
}
Some(LangItem::PointeeSized) => {}
_ => trait_emits += 1,
},
ty::ClauseKind::TypeOutlives(_) => lifetimes_count += 1,
_ => {}
}
}

// The synthetic suffix in `pretty_print_opaque_impl_type` emits at most
// one extra bound (`Sized` / `?Sized` / `MetaSized` / `PointeeSized`).
let using_sized_hierarchy = tcx.features().sized_hierarchy();
let add_sized = has_sized_bound && (trait_emits == 0 || has_negative_sized_bound);
let add_maybe_sized =
has_meta_sized_bound && !has_negative_sized_bound && !using_sized_hierarchy;
let has_pointee_sized =
!has_sized_bound && !has_meta_sized_bound && !has_negative_sized_bound;
let synthetic = add_sized
|| add_maybe_sized
|| (using_sized_hierarchy && (has_meta_sized_bound || has_pointee_sized));

trait_emits + lifetimes_count + usize::from(synthetic) > 1
}

fn pretty_print_opaque_impl_type(
&mut self,
def_id: DefId,
Expand Down
6 changes: 6 additions & 0 deletions compiler/rustc_symbol_mangling/src/legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,12 @@ impl<'tcx> PrettyPrinter<'tcx> for LegacySymbolMangler<'tcx> {
false
}

// Mangled symbols are ABI-stable; don't pick up the default printer's
// parser-disambiguating parens.
fn add_disambiguating_parens_in_prefix_position(&self) -> bool {
false
}

// Identical to `PrettyPrinter::comma_sep` except there is no space after each comma.
fn comma_sep<T>(&mut self, mut elems: impl Iterator<Item = T>) -> Result<(), PrintError>
where
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
error: Undefined Behavior: constructing invalid value of type *const dyn std::fmt::Debug + std::marker::Send + std::marker::Sync: wrong trait in wide pointer vtable: expected `std::fmt::Debug + std::marker::Send + std::marker::Sync`, but encountered `std::fmt::Display`
error: Undefined Behavior: constructing invalid value of type *const (dyn std::fmt::Debug + std::marker::Send + std::marker::Sync): wrong trait in wide pointer vtable: expected `std::fmt::Debug + std::marker::Send + std::marker::Sync`, but encountered `std::fmt::Display`
--> tests/fail/dyn-upcast-nop-wrong-trait.rs:LL:CC
|
LL | let ptr: *const (dyn fmt::Debug + Send + Sync) = unsafe { std::mem::transmute(ptr) };
Expand Down
8 changes: 4 additions & 4 deletions src/tools/miri/tests/fail/tail_calls/cc-mismatch.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ LL | extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
at RUSTLIB/std/src/sys/backtrace.rs:LL:CC
2: std::rt::lang_start::<()>::{closure#0}
at RUSTLIB/std/src/rt.rs:LL:CC
3: std::ops::function::impls::<impl std::ops::FnOnce<()> for &dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe>::call_once
3: std::ops::function::impls::<impl std::ops::FnOnce<()> for &(dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe)>::call_once
at RUSTLIB/core/src/ops/function.rs:LL:CC
4: std::panicking::catch_unwind::do_call::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>
4: std::panicking::catch_unwind::do_call::<&(dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe), i32>
at RUSTLIB/std/src/panicking.rs:LL:CC
5: std::panicking::catch_unwind::<i32, &dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe>
5: std::panicking::catch_unwind::<i32, &(dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe)>
at RUSTLIB/std/src/panicking.rs:LL:CC
6: std::panic::catch_unwind::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>
6: std::panic::catch_unwind::<&(dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe), i32>
at RUSTLIB/std/src/panic.rs:LL:CC
7: std::rt::lang_start_internal::{closure#0}
at RUSTLIB/std/src/rt.rs:LL:CC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
at RUSTLIB/std/src/sys/backtrace.rs:LL:CC
3: std::rt::lang_start::<()>::{closure#0}
at RUSTLIB/std/src/rt.rs:LL:CC
4: std::ops::function::impls::<impl std::ops::FnOnce<()> for &dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe>::call_once
4: std::ops::function::impls::<impl std::ops::FnOnce<()> for &(dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe)>::call_once
at RUSTLIB/core/src/ops/function.rs:LL:CC
5: std::panicking::catch_unwind::do_call::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>
5: std::panicking::catch_unwind::do_call::<&(dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe), i32>
at RUSTLIB/std/src/panicking.rs:LL:CC
6: std::panicking::catch_unwind::<i32, &dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe>
6: std::panicking::catch_unwind::<i32, &(dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe)>
at RUSTLIB/std/src/panicking.rs:LL:CC
7: std::panic::catch_unwind::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>
7: std::panic::catch_unwind::<&(dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe), i32>
at RUSTLIB/std/src/panic.rs:LL:CC
8: std::rt::lang_start_internal::{closure#0}
at RUSTLIB/std/src/rt.rs:LL:CC
Expand Down
8 changes: 4 additions & 4 deletions src/tools/miri/tests/pass/backtrace/backtrace-std.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
at RUSTLIB/std/src/sys/backtrace.rs:LL:CC
7: std::rt::lang_start::<()>::{closure#0}
at RUSTLIB/std/src/rt.rs:LL:CC
8: std::ops::function::impls::<impl std::ops::FnOnce<()> for &dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe>::call_once
8: std::ops::function::impls::<impl std::ops::FnOnce<()> for &(dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe)>::call_once
at RUSTLIB/core/src/ops/function.rs:LL:CC
9: std::panicking::catch_unwind::do_call::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>
9: std::panicking::catch_unwind::do_call::<&(dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe), i32>
at RUSTLIB/std/src/panicking.rs:LL:CC
10: std::panicking::catch_unwind::<i32, &dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe>
10: std::panicking::catch_unwind::<i32, &(dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe)>
at RUSTLIB/std/src/panicking.rs:LL:CC
11: std::panic::catch_unwind::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>
11: std::panic::catch_unwind::<&(dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe), i32>
at RUSTLIB/std/src/panic.rs:LL:CC
12: std::rt::lang_start_internal::{closure#0}
at RUSTLIB/std/src/rt.rs:LL:CC
Expand Down
6 changes: 3 additions & 3 deletions tests/ui/argument-suggestions/display-is-suggestable.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ error[E0061]: this function takes 1 argument but 0 arguments were supplied
--> $DIR/display-is-suggestable.rs:6:5
|
LL | foo();
| ^^^-- argument #1 of type `&dyn std::fmt::Display + Send` is missing
| ^^^-- argument #1 of type `&(dyn std::fmt::Display + Send)` is missing
|
note: function defined here
--> $DIR/display-is-suggestable.rs:3:4
Expand All @@ -11,8 +11,8 @@ LL | fn foo(x: &(dyn Display + Send)) {}
| ^^^ ------------------------
help: provide the argument
|
LL | foo(/* &dyn std::fmt::Display + Send */);
| +++++++++++++++++++++++++++++++++++
LL | foo(/* &(dyn std::fmt::Display + Send) */);
| +++++++++++++++++++++++++++++++++++++

error: aborting due to 1 previous error

Expand Down
2 changes: 1 addition & 1 deletion tests/ui/cast/casts-differing-anon.stderr
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
error[E0606]: casting `*mut impl Debug + ?Sized` as `*mut impl Debug + ?Sized` is invalid
error[E0606]: casting `*mut (impl Debug + ?Sized)` as `*mut (impl Debug + ?Sized)` is invalid
--> $DIR/casts-differing-anon.rs:21:13
|
LL | b_raw = f_raw as *mut _;
Expand Down
Loading
Loading