Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
154 changes: 141 additions & 13 deletions compiler/rustc_builtin_macros/src/deriving/reborrow.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
use rustc_ast::{self as ast, AttrArgs, Generics, ItemKind, token};
use rustc_ast::{
self as ast, AngleBracketedArg, AttrArgs, DUMMY_NODE_ID, GenericArg, GenericBound,
GenericParam, GenericParamKind, Generics, ItemKind, WherePredicate, WherePredicateKind,
WhereRegionPredicate, token,
};
use rustc_data_structures::fx::FxHashSet;
use rustc_errors::E0802;
use rustc_expand::base::ExtCtxt;
use rustc_macros::Diagnostic;
use rustc_span::{Ident, Span, Symbol, sym};
use thin_vec::ThinVec;
use rustc_span::{DUMMY_SP, Ident, Span, Symbol, sym};
use thin_vec::{ThinVec, thin_vec};

use crate::deriving::generic::*;
use crate::deriving::new_path;
Expand All @@ -19,7 +24,10 @@ pub(crate) fn expand_deriving_reborrow(
return;
};

push_marker_impl(cx, span, ident, generics, sym::Reborrow, Vec::new(), push);
let self_args: Vec<_> =
generics.params.iter().map(|p| generic_param_to_arg(cx, p, p.span())).collect();

push_marker_impl(cx, span, ident, generics, sym::Reborrow, Vec::new(), self_args, push);
}

pub(crate) fn expand_deriving_coerce_shared(
Expand All @@ -32,11 +40,13 @@ pub(crate) fn expand_deriving_coerce_shared(
let Some((ident, generics)) = struct_def(cx, span, item, sym::CoerceShared) else {
return;
};
let Some(target) = coerce_shared_target(cx, span, item) else {
let self_args: Vec<_> =
generics.params.iter().map(|p| generic_param_to_arg(cx, p, p.span())).collect();
let Some((target, generics)) = coerce_shared_target(cx, span, item, generics) else {
return;
};

push_marker_impl(cx, span, ident, generics, sym::CoerceShared, vec![target], push);
push_marker_impl(cx, span, ident, &generics, sym::CoerceShared, vec![target], self_args, push);
}

fn struct_def<'a>(
Expand All @@ -62,8 +72,14 @@ fn struct_def<'a>(
}
}

fn coerce_shared_target(cx: &ExtCtxt<'_>, span: Span, item: &ast::Item) -> Option<Box<ast::Ty>> {
let mut attrs = item.attrs.iter().filter(|attr| attr.has_name(sym::coerce_shared));
fn coerce_shared_target(
cx: &ExtCtxt<'_>,
span: Span,
coerce_shared_item: &ast::Item,
source_generics: &Generics,
) -> Option<(Box<ast::Ty>, Generics)> {
let mut attrs =
coerce_shared_item.attrs.iter().filter(|attr| attr.has_name(sym::coerce_shared));
let Some(attr) = attrs.next() else {
cx.dcx().emit_err(MissingTarget { span });
return None;
Expand All @@ -83,7 +99,7 @@ fn coerce_shared_target(cx: &ExtCtxt<'_>, span: Span, item: &ast::Item) -> Optio
}

let mut parser = cx.new_parser_from_tts(args.tokens.clone());
let target = match parser.parse_ty() {
let mut target = match parser.parse_ty() {
Ok(target) => target,
Err(err) => {
err.cancel();
Expand All @@ -96,7 +112,104 @@ fn coerce_shared_target(cx: &ExtCtxt<'_>, span: Span, item: &ast::Item) -> Optio
return None;
}

Some(target)
let rustc_ast::TyKind::Path(_, path) = &mut target.kind else {
cx.dcx().emit_err(MalformedTargetType { span: target.span });
return None;
};

let Some(last) = path.segments.last_mut() else {
// It shouldn't be possible for segments to be empty.
cx.dcx().emit_err(MalformedTargetType { span: path.span });
return None;
};

let Some(rustc_ast::GenericArgs::AngleBracketed(target_args)) = last.args.as_deref_mut() else {
// FIXME(reborrow): same as above.
cx.dcx().emit_err(NoGenericsOnTargetType { span: last.span() });
return None;
};

// Map to check generated lifetime arg names against.
let mut lt_names: FxHashSet<Symbol> = target_args
.args
.iter()
.filter_map(|arg| {
if let AngleBracketedArg::Arg(GenericArg::Lifetime(arg)) = arg {
Some(arg.ident.name)
} else {
None
}
})
.collect();

// struct Source<'a, 'b, T, const U> {} + coerce_shared(Target<'a>) =>
// impl<'a, 'a_, T, const U> CoerceShared<Target<'a_>> for Source<'a, T, const U>
// where 'a: 'a_ {}
let coerce_shared_trait_param_count = source_generics.params.len() + lt_names.len();
let mut trait_params = ThinVec::with_capacity(coerce_shared_trait_param_count);
let mut trait_where_clause = source_generics.where_clause.clone();

// First add in the existing lifetimes.
trait_params.extend(
source_generics
.params
.iter()
.take_while(|p| matches!(p.kind, rustc_ast::GenericParamKind::Lifetime))
.cloned(),
);

// Remember how many lifetime parameters there were.
let source_generics_lt_count = trait_params.len();

// Replace all lifetime parameters 'a with a new 'a_ where 'a: 'a_ in the Target definition, and
// push the new lifetimes into trait params.
for arg in target_args.args.iter_mut() {
let AngleBracketedArg::Arg(GenericArg::Lifetime(arg)) = arg else {
continue;
};

// Eagerly intern the generated lifetime name - it is unlikely we have a naming conflict.
let mut name_string = format!("{}_", arg.ident.as_str());
let mut name = Symbol::intern(&name_string);
while lt_names.contains(&name) {
// Just keep piling on the underscores.
name_string.push('_');
name = Symbol::intern(&name_string);
}
// Now that we generated a unique lifetime name, add it into the set.
lt_names.insert(name);

// Create our lifetime, add it into the trait parameters and create a where-clause predicate
// for it.
let lt = rustc_ast::Lifetime { id: DUMMY_NODE_ID, ident: Ident::with_dummy_span(name) };
trait_params.push(GenericParam {
id: DUMMY_NODE_ID,
ident: lt.ident,
attrs: Default::default(),
bounds: Default::default(),
is_placeholder: false,
kind: GenericParamKind::Lifetime,
colon_span: None,
});
Comment on lines +185 to +193

@cyrgani cyrgani Oct 3, 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.

Suggested change
trait_params.push(GenericParam {
id: DUMMY_NODE_ID,
ident: lt.ident,
attrs: Default::default(),
bounds: Default::default(),
is_placeholder: false,
kind: GenericParamKind::Lifetime,
colon_span: None,
});
trait_params.push(cx.lifetime_param(lt.ident, Default::default()));

View changes since the review

trait_where_clause.predicates.push(WherePredicate {
attrs: Default::default(),
kind: WherePredicateKind::RegionPredicate(WhereRegionPredicate {
lifetime: *arg,
bounds: thin_vec![GenericBound::Outlives(lt)],
}),
id: DUMMY_NODE_ID,
span: DUMMY_SP,
is_placeholder: false,
});
// Note: this mutates `target` from `Target<'a>` to `Target<'a_>`.
*arg = lt;
}

// Finally add in any non-lifetime generics to the trait parameters: they must follow lifetimes
// hence this ordering. This is also why we made note of lifetime parameter count.
trait_params.extend_from_slice(&source_generics.params[source_generics_lt_count..]);

Some((target, Generics { params: trait_params, where_clause: trait_where_clause, span }))
}

fn push_marker_impl(
Expand All @@ -106,14 +219,13 @@ fn push_marker_impl(
generics: &Generics,
trait_name: Symbol,
trait_args: Vec<Box<ast::Ty>>,
self_args: Vec<GenericArg>,
push: &mut dyn FnMut(Box<ast::Item>),
) {
let trait_path = new_path(cx, span, &[sym::core, sym::marker, trait_name], trait_args);
let trait_ref = cx.trait_ref(trait_path);

let self_params: Vec<_> =
generics.params.iter().map(|p| generic_param_to_arg(cx, p, p.span())).collect();
let self_ty = cx.ty_path(cx.path_all(span, false, vec![ident], self_params));
let self_ty = cx.ty_path(cx.path_all(span, false, vec![ident], self_args));

push(cx.item_trait_impl(
span,
Expand Down Expand Up @@ -159,3 +271,19 @@ struct MalformedTarget {
#[primary_span]
span: Span,
}

#[derive(Diagnostic)]
#[diag("malformed `#[coerce_shared(Target)]` attribute for `derive(CoerceShared)`", code = E0802)]
#[note("expected target type to be a user-defined type")]
struct MalformedTargetType {
#[primary_span]
span: Span,
}

#[derive(Diagnostic)]
#[diag("malformed `#[coerce_shared(Target)]` attribute for `derive(CoerceShared)`", code = E0802)]
#[note("expected target type to have generics")]
struct NoGenericsOnTargetType {
#[primary_span]
span: Span,
}
22 changes: 2 additions & 20 deletions compiler/rustc_hir_analysis/src/coherence/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,10 +476,8 @@ pub(crate) fn reborrow_info<'tcx>(
let source = tcx.type_of(impl_did).instantiate_identity().skip_norm_wip();
let trait_ref = tcx.impl_trait_ref(impl_did).instantiate_identity().skip_norm_wip();

if trait_impl_lifetime_params_count(tcx, impl_did) != 1 {
return Err(tcx
.dcx()
.emit_err(diagnostics::CoerceSharedNotSingleLifetimeParam { span, trait_name }));
if trait_impl_lifetime_params_count(tcx, impl_did) == 0 {
return Err(tcx.dcx().emit_err(diagnostics::ReborrowNoLifetimes { span, trait_name }));
}

assert_eq!(trait_ref.def_id, reborrow_trait);
Expand All @@ -496,20 +494,8 @@ pub(crate) fn reborrow_info<'tcx>(
}
};

let lifetimes_count = generic_lifetime_params_count(args);
let data_fields = collect_reborrow_data_fields(tcx, def, args);

if lifetimes_count != 1 {
let item = tcx.hir_expect_item(impl_did);
let _span = if let ItemKind::Impl(hir::Impl { of_trait: Some(of_trait), .. }) = &item.kind {
of_trait.trait_ref.path.span
} else {
tcx.def_span(impl_did)
};

return Err(tcx.dcx().emit_err(diagnostics::CoerceSharedMulti { span, trait_name }));
}

if data_fields.is_empty() {
return Ok(());
}
Expand Down Expand Up @@ -561,10 +547,6 @@ fn trait_impl_lifetime_params_count(tcx: TyCtxt<'_>, did: LocalDefId) -> usize {
.count()
}

fn generic_lifetime_params_count(args: &[ty::GenericArg<'_>]) -> usize {
args.iter().filter(|arg| arg.as_region().is_some()).count()
}

#[derive(Clone, Copy)]
struct ReborrowDataField<'tcx> {
ident: Ident,
Expand Down
44 changes: 10 additions & 34 deletions compiler/rustc_hir_analysis/src/coherence/builtin/coerce_shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use rustc_errors::ErrorGuaranteed;
use rustc_hir as hir;
use rustc_hir::ItemKind;
use rustc_hir::def_id::{DefId, LocalDefId};
use rustc_infer::infer::{DefineOpaqueTypes, InferCtxt, TyCtxtInferExt};
use rustc_infer::infer::{DefineOpaqueTypes, InferCtxt, SubregionOrigin, TyCtxtInferExt};
use rustc_infer::traits::{Obligation, TraitErrors};
use rustc_middle::ty::{self, Ty, TyCtxt, TypeVisitableExt, TypingMode, Unnormalized};
use rustc_span::Span;
Expand Down Expand Up @@ -239,10 +239,8 @@ pub(super) fn coerce_shared_info<'tcx>(
let source = tcx.type_of(impl_did).instantiate_identity().skip_norm_wip();
let trait_ref = tcx.impl_trait_ref(impl_did).instantiate_identity().skip_norm_wip();

if trait_impl_lifetime_params_count(tcx, impl_did) != 1 {
return Err(tcx
.dcx()
.emit_err(diagnostics::CoerceSharedNotSingleLifetimeParam { span, trait_name }));
if trait_impl_lifetime_params_count(tcx, impl_did) == 0 {
return Err(tcx.dcx().emit_err(diagnostics::ReborrowNoLifetimes { span, trait_name }));
}

assert_eq!(trait_ref.def_id, coerce_shared_trait);
Expand All @@ -266,25 +264,6 @@ pub(super) fn coerce_shared_info<'tcx>(
(&ty::Adt(def_a, args_a), &ty::Adt(def_b, args_b))
if def_a.is_struct() && def_b.is_struct() =>
{
let a_lifetime = single_region_arg(args_a);
let b_lifetime = single_region_arg(args_b);

if a_lifetime.is_none() || b_lifetime.is_none() {
return Err(tcx.dcx().emit_err(diagnostics::CoerceSharedMulti {
span: diagnostic_context.trait_span,
trait_name,
}));
}

if a_lifetime != b_lifetime {
return Err(tcx.dcx().emit_err(diagnostics::CoerceSharedLifetimeMismatch {
span: diagnostic_context.trait_span,
source_lifetime_span: diagnostic_context.source_lifetime_span,
target_lifetime_span: diagnostic_context.target_lifetime_span,
trait_name,
}));
}

validate_reborrow_field_access(
tcx,
impl_did,
Expand Down Expand Up @@ -341,12 +320,6 @@ enum CoerceSharedFieldPairError<'tcx> {
MissingSourceField { target: ReborrowDataField<'tcx> },
}

fn single_region_arg<'tcx>(args: ty::GenericArgsRef<'tcx>) -> Option<ty::Region<'tcx>> {
let mut lifetimes = args.iter().filter_map(|arg| arg.as_region());
let lifetime = lifetimes.next()?;
lifetimes.next().is_none().then_some(lifetime)
}

// This is a coherence/WF check only. It verifies that the CoerceShared impl
// describes a structurally valid field-wise relation. Runtime lowering of the
// operation is not modeled here.
Expand Down Expand Up @@ -780,7 +753,7 @@ fn field_tys_satisfy_relation_after_normalization_and_resolution<'tcx>(
FieldRelation::Equal => {
if infcx
.at(&cause, param_env)
.relate(DefineOpaqueTypes::Yes, source_ty, ty::Variance::Invariant, target_ty)
.relate(DefineOpaqueTypes::Yes, source_ty, ty::Variance::Covariant, target_ty)
.is_err()
{
return false;
Expand All @@ -794,9 +767,12 @@ fn field_tys_satisfy_relation_after_normalization_and_resolution<'tcx>(
else {
return false;
};
if source_region != target_region {
return false;
}
infcx.sub_regions(
SubregionOrigin::RelateObjectBound(span),
target_region,
source_region,
ty::VisibleForLeakCheck::Yes,
);
if ocx.sup(&cause, param_env, target_referent_ty, source_referent_ty).is_err() {
return false;
}
Comment on lines 754 to 778

@oli-obk oli-obk Sep 28, 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.

I couldn't find any in-tree docs about the lifetime handling. Should probably add some now that you aren't requiring equality anymore. I had to grab a variance table and poke at the code and tests here until I was sure it was all in the right direction 😆

View changes since the review

@aapoalas aapoalas Oct 3, 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.

Where should they be located in? Just here in the code?

Expand Down
31 changes: 2 additions & 29 deletions compiler/rustc_hir_analysis/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1356,37 +1356,10 @@ pub(crate) struct CoerceMulti {
}

#[derive(Diagnostic)]
#[diag(
"implementing `{$trait_name}` requires that a single lifetime parameter is passed between source and target"
)]
pub(crate) struct CoerceSharedNotSingleLifetimeParam {
#[primary_span]
pub span: Span,
pub trait_name: &'static str,
}

#[derive(Diagnostic)]
#[diag(
"implementing `{$trait_name}` requires exactly one lifetime argument in the reborrowed type"
)]
pub(crate) struct CoerceSharedMulti {
#[primary_span]
pub span: Span,
pub trait_name: &'static str,
}

#[derive(Diagnostic)]
#[diag(
"implementing `{$trait_name}` requires source and target to use the same reborrow lifetime \
argument"
)]
pub(crate) struct CoerceSharedLifetimeMismatch {
#[diag("implementing `{$trait_name}` requires that at least one lifetime parameter")]
pub(crate) struct ReborrowNoLifetimes {
#[primary_span]
pub span: Span,
#[label("source reborrow lifetime")]
pub source_lifetime_span: Option<Span>,
#[label("target reborrow lifetime")]
pub target_lifetime_span: Option<Span>,
pub trait_name: &'static str,
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct CustomMarkerRef<'a>(PhantomData<(Debug, Clone, Copy)>);
//~| ERROR

impl<'a> Reborrow for CustomMarker<'a> {}
impl<'a> CoerceShared<CustomMarkerRef<'a>> for CustomMarker<'a> {}
impl<'a: 'b, 'b> CoerceShared<CustomMarkerRef<'b>> for CustomMarker<'a> {}
//~^ ERROR

fn method<'a>(_a: CustomMarkerRef<'a>) -> 'a () {
Expand Down
Loading
Loading