Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement #[rustc_must_implement_one_of] attribute #92164

Merged
merged 9 commits into from
Jan 18, 2022
6 changes: 6 additions & 0 deletions compiler/rustc_feature/src/builtin_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,12 @@ pub const BUILTIN_ATTRIBUTES: &[BuiltinAttribute] = &[
"the `#[rustc_skip_array_during_method_dispatch]` attribute is used to exclude a trait \
from method dispatch when the receiver is an array, for compatibility in editions < 2021."
),
rustc_attr!(
rustc_must_implement_one_of, Normal, template!(List: "function1, function2, ..."), ErrorFollowing,
"the `#[rustc_must_implement_one_of]` attribute is used to change minimal complete \
definition of a trait, it's currently in experimental form and should be changed before \
being exposed outside of the std"
),

// ==========================================================================
// Internal attributes, Testing:
Expand Down
2 changes: 2 additions & 0 deletions compiler/rustc_metadata/src/rmeta/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,7 @@ impl<'a, 'tcx> CrateMetadataRef<'a> {
data.skip_array_during_method_dispatch,
data.specialization_kind,
self.def_path_hash(item_id),
data.must_implement_one_of,
)
}
EntryKind::TraitAlias => ty::TraitDef::new(
Expand All @@ -831,6 +832,7 @@ impl<'a, 'tcx> CrateMetadataRef<'a> {
false,
ty::trait_def::TraitSpecializationKind::None,
self.def_path_hash(item_id),
None,
),
_ => bug!("def-index does not refer to trait or trait alias"),
}
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_metadata/src/rmeta/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1514,6 +1514,7 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
is_marker: trait_def.is_marker,
skip_array_during_method_dispatch: trait_def.skip_array_during_method_dispatch,
specialization_kind: trait_def.specialization_kind,
must_implement_one_of: trait_def.must_implement_one_of.clone(),
};

EntryKind::Trait(self.lazy(data))
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_metadata/src/rmeta/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ struct TraitData {
is_marker: bool,
skip_array_during_method_dispatch: bool,
specialization_kind: ty::trait_def::TraitSpecializationKind,
must_implement_one_of: Option<Box<[Ident]>>,
}

#[derive(TyEncodable, TyDecodable)]
Expand Down
8 changes: 7 additions & 1 deletion compiler/rustc_middle/src/ty/trait_def.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::traits::specialization_graph;
use crate::ty::fast_reject::{self, SimplifiedType, SimplifyParams, StripReferences};
use crate::ty::fold::TypeFoldable;
use crate::ty::{Ty, TyCtxt};
use crate::ty::{Ident, Ty, TyCtxt};
use rustc_hir as hir;
use rustc_hir::def_id::DefId;
use rustc_hir::definitions::DefPathHash;
Expand Down Expand Up @@ -44,6 +44,10 @@ pub struct TraitDef {
/// The ICH of this trait's DefPath, cached here so it doesn't have to be
/// recomputed all the time.
pub def_path_hash: DefPathHash,

/// List of functions from `#[rustc_must_implement_one_of]` attribute one of which
/// must be implemented.
pub must_implement_one_of: Option<Box<[Ident]>>,
}

/// Whether this trait is treated specially by the standard library
Expand Down Expand Up @@ -87,6 +91,7 @@ impl<'tcx> TraitDef {
skip_array_during_method_dispatch: bool,
specialization_kind: TraitSpecializationKind,
def_path_hash: DefPathHash,
must_implement_one_of: Option<Box<[Ident]>>,
) -> TraitDef {
TraitDef {
def_id,
Expand All @@ -97,6 +102,7 @@ impl<'tcx> TraitDef {
skip_array_during_method_dispatch,
specialization_kind,
def_path_hash,
must_implement_one_of,
}
}

Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,7 @@ symbols! {
rustc_macro_transparency,
rustc_main,
rustc_mir,
rustc_must_implement_one_of,
rustc_nonnull_optimization_guaranteed,
rustc_object_lifetime_default,
rustc_on_unimplemented,
Expand Down
29 changes: 29 additions & 0 deletions compiler/rustc_typeck/src/check/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,10 @@ fn check_impl_items_against_trait<'tcx>(
if let Ok(ancestors) = trait_def.ancestors(tcx, impl_id.to_def_id()) {
// Check for missing items from trait
let mut missing_items = Vec::new();

let mut must_implement_one_of: Option<&[Ident]> =
trait_def.must_implement_one_of.as_deref();

for &trait_item_id in tcx.associated_item_def_ids(impl_trait_ref.def_id) {
let is_implemented = ancestors
.leaf_def(tcx, trait_item_id)
Expand All @@ -987,12 +991,37 @@ fn check_impl_items_against_trait<'tcx>(
if !is_implemented && tcx.impl_defaultness(impl_id).is_final() {
missing_items.push(tcx.associated_item(trait_item_id));
}

if let Some(required_items) = &must_implement_one_of {
// true if this item is specifically implemented in this impl
Copy link
Contributor

@danielhenrymantilla danielhenrymantilla Jan 10, 2022

Choose a reason for hiding this comment

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

Suggested change
// true if this item is specifically implemented in this impl
// true if this item is specifically implemented in this impl ≠ a default implementation

Speaking of which: how does this interact with specialization / partial impls?

#[rustc_must_implement_one_of(foo, bar)]
trait Trait {
    fn foo() { Self::bar() }
    fn bar() { Self::foo() }
}

default /* EDIT */
// impl Trait for () {
impl<T> Trait for T {
    fn foo() {}
}

impl Trait for () {}

Copy link
Member Author

Choose a reason for hiding this comment

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

This example doesn't compile even without rustc_must_implement_one_of though...

error[E0119]: conflicting implementations of trait `Trait` for type `()`
  --> ./what.rs:15:1
   |
10 | / default
11 | | impl Trait for () {
12 | |     fn foo() {}
13 | | }
   | |_- first implementation here
14 |
15 |   impl Trait for () {}
   |   ^^^^^^^^^^^^^^^^^ conflicting implementation for `()`

Copy link
Contributor

@danielhenrymantilla danielhenrymantilla Jan 10, 2022

Choose a reason for hiding this comment

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

Argh, I hate default impl not meaning partial impl but partial impl { default members } (not only is it redundant w.r.t. to default fn being usable inside a default impl, examples such as this one showcase that's it's also too restrictive, and thus, unnecessarily restrictive).
Okay, replace default impl Trait for () with default impl<T> Trait for T { fn foo() {} }.

And let's also test with default impl Trait for T {} 🙏

Copy link
Member Author

Choose a reason for hiding this comment

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

So

  • default impl<T> Trait for T { fn foo() {} } & impl Trait for () {} compiles
  • default impl<T> Trait for T {} errors

I'm honestly very confused about what default impls mean, so not sure if this is correct 😅

Snippets

#![feature(specialization)]
#![feature(rustc_attrs)]

#[rustc_must_implement_one_of(foo, bar)]
trait Trait {
    fn foo() { Self::bar() }
    fn bar() { Self::foo() }
}

default impl<T> Trait for T { 
    fn foo() {} 
}

impl Trait for () {}

fn main() {}
#![feature(specialization)]
#![feature(rustc_attrs)]

#[rustc_must_implement_one_of(foo, bar)]
trait Trait {
    fn foo() { Self::bar() }
    fn bar() { Self::foo() }
}

default impl<T> Trait for T {}

fn main() {}
error[E0046]: not all trait items implemented, missing one of: `foo`, `bar`
  --> ./what.rs:10:1
   |
10 | default impl<T> Trait for T {}
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing one of `foo`, `bar` in implementation
   |
note: required because of this annotation
  --> ./what.rs:4:1
   |
4  | #[rustc_must_implement_one_of(foo, bar)]
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Copy link
Contributor

@danielhenrymantilla danielhenrymantilla Jan 10, 2022

Choose a reason for hiding this comment

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

Thanks for having run the tests (I don't have access to a computer with a rust/ repo setup today)

what default impls means

My view on the matter

So, default impl … { items… } means partial impl … { default items… }, as in one is then writing part of a trait (from 0 to all associated items), but for the trait itself. And I personally see partial impl, alone, as sugar for super-traits.

Let's take Any upcasting as an example:

trait SubAny : Any {
    fn as_any(&self) -> &dyn Any;
}

Problem: implementors of SubAny would need to feature an impl of as_any.

trait SubAny : Any {
    fn as_any(&self) -> &dyn Any { self }
}

Problem: Self : Sized does not hold.

trait SubAny : Any {
    fn as_any(&self) -> &dyn Any
    where Self : Sized,
    { self }
}

Problem: &dyn SubAny can't use as_any.

trait SubAny : Any + AsAny {}
trait AsAny : Any {
    fn as_any(&self) -> &dyn Any;
}
impl<T : /* Sized + */ Any> AsAny for T {
    fn as_any(&self) -> &dyn Any { self }
}

This finally works, but is cumbersome, plus the AsAny trait does not read as well in the docs to let the user know of a .as_any() method available to it.

With partial impls, however, we can both feature a method with no trait-def impl / body (where we couldn't because of lack of Sized) and yet not force implementors to provide, each time, the same body again and again:

trait SubAny : Any {
    fn as_any(&self) -> &dyn Any;
}

partial
impl<T : /* Sized + */ Any> SubAny for T {
    fn as_any(&self) -> &dyn Any { self }
}

Now, from here, your challenge would be to somehow make it if you are inside a default impl, not error there (since even if some methods are implemented, the trait itself isn't and thus, its methods aren't callable, so there is no risk of infinite recursion of, e.g., eq calling ne calling eq, etc.).

Given the body of the function, it may be as simple as disabling your check if tcx.impl_defaultness(impl_id).is_final() does not hold, but take that suggestion with a big grain of salt 😅

Copy link
Member

Choose a reason for hiding this comment

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

Personally, I think it's fine to address any issues with default in a follow-up PR. Specialization is a long ways from stabilization, so I don't think it needs to block this PR

let is_implemented_here = ancestors
.leaf_def(tcx, trait_item_id)
.map_or(false, |node_item| !node_item.defining_node.is_from_trait());

if is_implemented_here {
let trait_item = tcx.associated_item(trait_item_id);
if required_items.contains(&trait_item.ident) {
must_implement_one_of = None;
}
}
}
}

if !missing_items.is_empty() {
let impl_span = tcx.sess.source_map().guess_head_span(full_impl_span);
missing_items_err(tcx, impl_span, &missing_items, full_impl_span);
}

if let Some(missing_items) = must_implement_one_of {
let impl_span = tcx.sess.source_map().guess_head_span(full_impl_span);
let attr_span = tcx
.get_attrs(impl_trait_ref.def_id)
.iter()
.find(|attr| attr.has_name(sym::rustc_must_implement_one_of))
.map(|attr| attr.span);

missing_items_must_implement_one_of_err(tcx, impl_span, missing_items, attr_span);
}
}
}

Expand Down
25 changes: 25 additions & 0 deletions compiler/rustc_typeck/src/check/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,31 @@ fn missing_items_err(
err.emit();
}

fn missing_items_must_implement_one_of_err(
tcx: TyCtxt<'_>,
impl_span: Span,
missing_items: &[Ident],
annotation_span: Option<Span>,
) {
let missing_items_msg =
missing_items.iter().map(Ident::to_string).collect::<Vec<_>>().join("`, `");

let mut err = struct_span_err!(
tcx.sess,
impl_span,
E0046,
"not all trait items implemented, missing one of: `{}`",
missing_items_msg
);
err.span_label(impl_span, format!("missing one of `{}` in implementation", missing_items_msg));

if let Some(annotation_span) = annotation_span {
err.span_note(annotation_span, "required because of this annotation");
}

err.emit();
}

/// Resugar `ty::GenericPredicates` in a way suitable to be used in structured suggestions.
fn bounds_from_generic_predicates<'tcx>(
tcx: TyCtxt<'tcx>,
Expand Down
111 changes: 108 additions & 3 deletions compiler/rustc_typeck/src/collect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1198,9 +1198,11 @@ fn super_predicates_that_define_assoc_type(
fn trait_def(tcx: TyCtxt<'_>, def_id: DefId) -> ty::TraitDef {
let item = tcx.hir().expect_item(def_id.expect_local());

let (is_auto, unsafety) = match item.kind {
hir::ItemKind::Trait(is_auto, unsafety, ..) => (is_auto == hir::IsAuto::Yes, unsafety),
hir::ItemKind::TraitAlias(..) => (false, hir::Unsafety::Normal),
let (is_auto, unsafety, items) = match item.kind {
hir::ItemKind::Trait(is_auto, unsafety, .., items) => {
(is_auto == hir::IsAuto::Yes, unsafety, items)
}
hir::ItemKind::TraitAlias(..) => (false, hir::Unsafety::Normal, &[][..]),
Copy link
Member

Choose a reason for hiding this comment

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

I think this can just be &[] instead of &[][..]

Copy link
Member Author

Choose a reason for hiding this comment

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

screenshot of a compilation error that happens without [..] - in short, expected &[TraitItemRef], found &[_; 0]

Copy link
Contributor

Choose a reason for hiding this comment

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

Very disappointing; minimal repro: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=875b8d707eb70e293fc06d51eb6f26e0 — a type annotation at line 1201 does fix it, but without that a "wide pointer" field on one branch is not sufficient for a coercion to happen on other branches.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, even a one element tuple is enough to demonstrate this: (play)

_ => span_bug!(item.span, "trait_def_of_item invoked on non-trait"),
};

Expand All @@ -1227,6 +1229,108 @@ fn trait_def(tcx: TyCtxt<'_>, def_id: DefId) -> ty::TraitDef {
ty::trait_def::TraitSpecializationKind::None
};
let def_path_hash = tcx.def_path_hash(def_id);

let must_implement_one_of = tcx
.get_attrs(def_id)
.iter()
.find(|attr| attr.has_name(sym::rustc_must_implement_one_of))
// Check that there are at least 2 arguments of `#[rustc_must_implement_one_of]`
// and that they are all identifiers
.and_then(|attr| match attr.meta_item_list() {
Some(items) if items.len() < 2 => {
tcx.sess
.struct_span_err(
attr.span,
"the `#[rustc_must_implement_one_of]` attribute must be \
used with at least 2 args",
)
.emit();

None
}
Some(items) => items
.into_iter()
.map(|item| item.ident().ok_or(item.span()))
.collect::<Result<Box<[_]>, _>>()
.map_err(|span| {
tcx.sess
.struct_span_err(span, "must be a name of an associated function")
.emit();
})
.ok()
.zip(Some(attr.span)),
// Error is reported by `rustc_attr!`
None => None,
})
// Check that all arguments of `#[rustc_must_implement_one_of]` reference
// functions in the trait with default implementations
.and_then(|(list, attr_span)| {
let errors = list.iter().filter_map(|ident| {
let item = items.iter().find(|item| item.ident == *ident);

match item {
Some(item) if matches!(item.kind, hir::AssocItemKind::Fn { .. }) => {
if !item.defaultness.has_value() {
tcx.sess
.struct_span_err(
item.span,
"This function doesn't have a default implementation",
)
.span_note(attr_span, "required by this annotation")
.emit();

return Some(());
}

return None;
}
WaffleLapkin marked this conversation as resolved.
Show resolved Hide resolved
Some(item) => tcx
.sess
.struct_span_err(item.span, "Not a function")
.span_note(attr_span, "required by this annotation")
.note(
"All `#[rustc_must_implement_one_of]` arguments \
must be associated function names",
)
.emit(),
None => tcx
.sess
.struct_span_err(ident.span, "Function not found in this trait")
.emit(),
}

Some(())
});

(errors.count() == 0).then_some(list)
})
// Check for duplicates
.and_then(|list| {
let mut set: FxHashSet<&Ident> = FxHashSet::default();
let mut no_dups = true;

for ident in &*list {
if let Some(dup) = set.replace(ident) {
WaffleLapkin marked this conversation as resolved.
Show resolved Hide resolved
let dup2 = set.get(&dup).copied().unwrap(); // We've just inserted it

tcx.sess
.struct_span_err(
vec![dup.span, dup2.span],
WaffleLapkin marked this conversation as resolved.
Show resolved Hide resolved
"Functions names are duplicated",
)
.note(
"All `#[rustc_must_implement_one_of]` arguments \
must be unique",
)
.emit();

no_dups = false;
}
}

no_dups.then_some(list)
});

ty::TraitDef::new(
def_id,
unsafety,
Expand All @@ -1236,6 +1340,7 @@ fn trait_def(tcx: TyCtxt<'_>, def_id: DefId) -> ty::TraitDef {
skip_array_during_method_dispatch,
spec_kind,
def_path_hash,
must_implement_one_of,
)
}

Expand Down
44 changes: 44 additions & 0 deletions src/test/ui/traits/default-method/rustc_must_implement_one_of.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#![feature(rustc_attrs)]

#[rustc_must_implement_one_of(eq, neq)]
trait Equal {
fn eq(&self, other: &Self) -> bool {
!self.neq(other)
}

fn neq(&self, other: &Self) -> bool {
!self.eq(other)
}
}

struct T0;
struct T1;
struct T2;
struct T3;

impl Equal for T0 {
fn eq(&self, _other: &Self) -> bool {
true
}
}

impl Equal for T1 {
fn neq(&self, _other: &Self) -> bool {
false
}
}

impl Equal for T2 {
fn eq(&self, _other: &Self) -> bool {
true
}

fn neq(&self, _other: &Self) -> bool {
false
}
}

impl Equal for T3 {}
//~^ not all trait items implemented, missing one of: `eq`, `neq`

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
error[E0046]: not all trait items implemented, missing one of: `eq`, `neq`
--> $DIR/rustc_must_implement_one_of.rs:41:1
|
LL | impl Equal for T3 {}
| ^^^^^^^^^^^^^^^^^ missing one of `eq`, `neq` in implementation
|
note: required because of this annotation
--> $DIR/rustc_must_implement_one_of.rs:3:1
|
LL | #[rustc_must_implement_one_of(eq, neq)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0046`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#![feature(rustc_attrs)]

#[rustc_must_implement_one_of(a, a)]
//~^ Functions names are duplicated
trait Trait {
fn a() {}
}

#[rustc_must_implement_one_of(b, a, a, c, b, c)]
//~^ Functions names are duplicated
//~| Functions names are duplicated
//~| Functions names are duplicated
trait Trait1 {
fn a() {}
fn b() {}
fn c() {}
}

fn main() {}
Loading