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
131 changes: 120 additions & 11 deletions compiler/rustc_builtin_macros/src/deriving/reborrow.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use rustc_ast::{self as ast, AttrArgs, Generics, ItemKind, token};
use rustc_ast::{
self as ast, AngleBracketedArg, AttrArgs, DUMMY_NODE_ID, GenericArg, Generics, ItemKind, token,
};
use rustc_data_structures::fx::FxHashSet;
use rustc_errors::E0802;
use rustc_expand::base::ExtCtxt;
use rustc_macros::Diagnostic;
Expand All @@ -19,7 +22,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 +38,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 +70,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 +97,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 +110,87 @@ 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(cx.lifetime_param(lt.ident, Default::default()));
trait_where_clause.predicates.push(cx.where_outlives(*arg, lt));
// 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 +200,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 +252,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,
}
14 changes: 14 additions & 0 deletions compiler/rustc_expand/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,20 @@ impl<'a> ExtCtxt<'a> {
}
}

/// Create a `where 'left: 'right` predicate.
pub fn where_outlives(&self, left: ast::Lifetime, right: ast::Lifetime) -> ast::WherePredicate {
ast::WherePredicate {
attrs: Default::default(),
kind: ast::WherePredicateKind::RegionPredicate(ast::WhereRegionPredicate {
lifetime: left,
bounds: thin_vec![ast::GenericBound::Outlives(right)],
}),
id: ast::DUMMY_NODE_ID,
span: DUMMY_SP,
is_placeholder: false,
}
}

pub fn const_param(
&self,
ident: Ident,
Expand Down
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
53 changes: 19 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 @@ -776,11 +749,17 @@ fn field_tys_satisfy_relation_after_normalization_and_resolution<'tcx>(
return false;
}

// impl<'a: 'b, 'b> CoerceShared<Target<'b>> for Source<'a> {}
//
// We expect all fields of `Source<'a>` to be one of:
// 1. `CoreceShared<TargetField<'b>>`: this is handled separately
// 2. assignable to target field: this is checked with a covariant .relate() call.
// 3. `&'a mut T` assigned to `&'b T`: this is checked below with .sub_regions() and .sup().
match relation {
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 +773,15 @@ fn field_tys_satisfy_relation_after_normalization_and_resolution<'tcx>(
else {
return false;
};
if source_region != target_region {
return false;
}
// Here we're effectively checking assignability of `&'source_region mut T` to
// `&'target_region T`, so we require `'source_region: 'target_region` using
// `.sub_regions()` and the `T`'s to be unifiable with `.sup()`.
infcx.sub_regions(
SubregionOrigin::Reborrow(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 760 to 787

@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?

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.

yea, there are existing docs in the code, but they don't mention lifetimes yet at all beyond the trivial usage

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.

Added inline comments to this. You might want to also read https://hackmd.io/xl3EvNkZSHqqNi_K2XwdRg regarding this - if we do go back to an associated type on CoerceShared (which I think we should - otherwise we cannot represent &mut T and &T using Reborrow and CoerceShared alone) then the lifetime stuff here will revert to an invariant again :D

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
Loading
Loading