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

Refactor TraitObject to Slice<ExistentialPredicate> #37965

Merged
merged 7 commits into from
Nov 30, 2016

Conversation

Mark-Simulacrum
Copy link
Member

@Mark-Simulacrum Mark-Simulacrum commented Nov 23, 2016

For reference, the primary types changes in this PR are shown below. They may add in the understanding of what is discussed below, though they should not be required.

We change TraitObject into a list of ExistentialPredicates to allow for a couple of things:

  • Principal (ExistentialPredicate::Trait) is now optional.
  • Region bounds are moved out of TraitObject into TyDynamic. This permits wrapping only the ExistentialPredicate list in Binder.
  • BuiltinBounds and BuiltinBound are removed entirely from the codebase, to permit future non-constrained auto traits. These are replaced with ExistentialPredicate::AutoTrait, which only requires a DefId. For the time being, only Send and Sync are supported; this constraint can be lifted in a future pull request.
  • Binder-related logic is extracted from ExistentialPredicate into the parent (Binder<Slice<EP>>), so PolyXs are inside TraitObject are replaced with X.

The code requires a sorting order for ExistentialPredicates in the interned Slice. The sort order is asserted to be correct during interning, but the slices are not sorted at that point.

  1. ExistentialPredicate::Trait are defined as always equal; This may be wrong; should we be comparing them and sorting them in some way?
  2. ExistentialPredicate::Projection: Compared by ExistentialProjection::sort_key.
  3. ExistentialPredicate::AutoTrait: Compared by TraitDef.def_path_hash.

Construction of ExistentialPredicates is conducted through TyCtxt::mk_existential_predicates, which interns a passed iterator as a Slice. There are no convenience functions to construct from a set of separate iterators; callers must pass an iterator chain. The lack of convenience functions is primarily due to few uses and the relative difficulty in defining a nice API due to optional parts and difficulty in recognizing which argument goes where. It is also true that the current situation isn't significantly better than 4 arguments to a constructor function; but the extra work is deemed unnecessary as of this time.

// before this PR
struct TraitObject<'tcx> {
    pub principal: PolyExistentialTraitRef<'tcx>,
    pub region_bound: &'tcx ty::Region,
    pub builtin_bounds: BuiltinBounds,
    pub projection_bounds: Vec<PolyExistentialProjection<'tcx>>,
}

// after
pub enum ExistentialPredicate<'tcx> {
    // e.g. Iterator
    Trait(ExistentialTraitRef<'tcx>),
    // e.g. Iterator::Item = T
    Projection(ExistentialProjection<'tcx>),
    // e.g. Send
    AutoTrait(DefId),
}

@eddyb eddyb added the S-waiting-on-crater Status: Waiting on a crater run to be completed. label Nov 23, 2016
Copy link
Member

@eddyb eddyb left a comment

Choose a reason for hiding this comment

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

LGTM modulo comments. cc @nikomatsakis about Binder discipline.

ty::TyAdt(def, _) => def.is_fundamental(),
ty::TyDynamic(ref data, ..) => {
match data.principal() {
Some(ref principal) => tcx.has_attr(principal.def_id(), "fundamental"),
Copy link
Member

Choose a reason for hiding this comment

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

You shouldn't need ref in positions by this, the match is on an rvalue anyway.

tt.principal.def_id().is_local()
ty::TyDynamic(ref tt, ..) => {
match tt.principal() {
Some(ref p) => p.def_id().is_local(),
Copy link
Member

Choose a reason for hiding this comment

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

Same extraneous ref. It might be worth using map_or in cases like this, I'm not sure.

match self.tcx().lang_items.to_builtin_kind(obligation.predicate.def_id()) {
Some(ty::BoundCopy) => {
let def_id = obligation.predicate.def_id();
match obligation.predicate.def_id() {
Copy link
Member

Choose a reason for hiding this comment

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

This should be an if-else chain.

}

data.principal.with_self_ty(this.tcx(), self_ty)
match data.principal() {
Some(ref p) => p.with_self_ty(this.tcx(), self_ty),
Copy link
Member

Choose a reason for hiding this comment

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

Same extraneous ref.

data_a.principal.def_id() == data_b.principal.def_id() &&
data_a.builtin_bounds.is_superset(&data_b.builtin_bounds)
match (data_a.principal(), data_b.principal()) {
(Some(ref a), Some(ref b)) => a.def_id() == b.def_id() &&
Copy link
Member

Choose a reason for hiding this comment

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

Same extraneous ref.

@@ -807,11 +807,11 @@ impl<'a, 'gcx, 'tcx> RegionCtxt<'a, 'gcx, 'tcx> {
}

/*From:*/ (_,
/*To: */ &ty::TyTrait(ref obj)) => {
/*To: */ &ty::TyDynamic(.., ref reg)) => {
Copy link
Member

Choose a reason for hiding this comment

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

Same odd reg.

// This is something like impl Trait1 for Trait2. Illegal
// if Trait1 is a supertrait of Trait2 or Trait2 is not object safe.

if !self.tcx.is_object_safe(data.principal.def_id()) {
if data.principal().is_none() ||
!self.tcx.is_object_safe(data.principal().unwrap().def_id()) {
Copy link
Member

Choose a reason for hiding this comment

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

This can be written using map_or.

@@ -1958,7 +1958,7 @@ pub fn compute_bounds<'gcx: 'tcx, 'tcx>(astconv: &AstConv<'gcx, 'tcx>,

Bounds {
region_bounds: region_bounds,
builtin_bounds: builtin_bounds,
auto_traits: auto_traits,
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure this partitioning should even exist until a TyDynamic is created.


let poly_trait_ref = data.principal.with_self_ty(self.tcx(), self.tcx().types.err);
let poly_trait_ref = data.principal().unwrap().with_self_ty(self.tcx(),
self.tcx().types.err);
self.add_constraints_from_trait_ref(generics, poly_trait_ref.0, variance);
Copy link
Member

Choose a reason for hiding this comment

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

This can be wrapped in an if let Some instead of using .unwrap().

v.extend(obj.principal.skip_binder().substs.regions());
TyDynamic(ref obj, region) => {
let mut v = vec![region];
v.extend(obj.principal().unwrap().skip_binder().substs.regions());
Copy link
Member

Choose a reason for hiding this comment

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

This shouldn't .unwrap().

@Mark-Simulacrum Mark-Simulacrum force-pushed the trait-obj-to-exis-predicate branch 2 times, most recently from b475ea0 to 2c1ec2f Compare November 24, 2016 03:54
@bors
Copy link
Contributor

bors commented Nov 24, 2016

☔ The latest upstream changes (presumably #37890) made this pull request unmergeable. Please resolve the merge conflicts.

@Mark-Simulacrum Mark-Simulacrum force-pushed the trait-obj-to-exis-predicate branch 4 times, most recently from 3c63d7c to 8d76e7e Compare November 25, 2016 04:40
@Mark-Simulacrum
Copy link
Member Author

@eddyb Ready for the crater run, I think, but might want to review before?

@Mark-Simulacrum Mark-Simulacrum force-pushed the trait-obj-to-exis-predicate branch 2 times, most recently from 90d69cd to 72f54e0 Compare November 26, 2016 04:22
if let Some(principal) = trait_data.principal() {
self.push_def_path(principal.def_id(), output);
self.push_type_params(principal.skip_binder().substs,
&trait_data.projection_bounds().collect::<Vec<_>>()[..],
Copy link
Member Author

Choose a reason for hiding this comment

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

Shouldn't allocate.

field.ty,
ty::BoundSized,
fcx.tcx.lang_items.require(lang_items::SizedTraitLangItem)
.unwrap_or_else(|msg| fcx.tcx.sess.fatal(&msg[..])),
Copy link
Member Author

Choose a reason for hiding this comment

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

Replace with require_lang_item.

@@ -371,15 +371,17 @@ impl<'a, 'tcx> ConstraintContext<'a, 'tcx> {
variance);
}

ty::TyTrait(ref data) => {
ty::TyDynamic(ref data, ref reg) => {
Copy link
Member Author

Choose a reason for hiding this comment

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

reg

@Mark-Simulacrum
Copy link
Member Author

Expanded the description with discussion and elaboration on what this PR does.

@eddyb Let me know what parts of that should be transferred into documentation comments in the code.

@Mark-Simulacrum
Copy link
Member Author

Squashed the remaining fix commits. r? @eddyb

@eddyb
Copy link
Member

eddyb commented Nov 27, 2016

Started crater run.

@@ -1492,11 +1493,13 @@ impl<'a, 'gcx, 'tcx> InferCtxt<'a, 'gcx, 'tcx> {
}
}

let copy_lang_item = self.tcx.require_lang_item(lang_items::CopyTraitLangItem);
Copy link
Member

Choose a reason for hiding this comment

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

copy_trait or copy_def_id.

match tt.principal() {
Some(p) => p.def_id().is_local(),
None => false,
}
Copy link
Member

Choose a reason for hiding this comment

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

This (and other places) could use map_or for brevity.

} else if self.tcx().lang_items.unsize_trait() == Some(def_id) {
self.assemble_candidates_for_unsizing(obligation, &mut candidates);
} else {
// For non-builtins and Send/Sync
Copy link
Member

Choose a reason for hiding this comment

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

Comment is unnecessary/misleading.


// If the type is `Foo+'a`, ensures that the type
// being cast to `Foo+'a` outlives `'a`:
let outlives = ty::OutlivesPredicate(source, data.region_bound);
let outlives = ty::OutlivesPredicate(source, r);
push(ty::Binder(outlives).to_predicate());
Copy link
Member

Choose a reason for hiding this comment

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

Not necessary in this PR, but these push calls could be replaced with collecting an iterator to a Vec.

self.mk_ty(TyTrait(box obj))
pub fn mk_trait(self,
obj: ty::Binder<&'tcx Slice<ExistentialPredicate<'tcx>>>,
reg: &'tcx ty::Region)
Copy link
Member

Choose a reason for hiding this comment

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

Maybe rename this to mk_dynamic?

// If it could be sized, and is, add the sized predicate
if self.implicitly_sized && tcx.lang_items.sized_trait().is_some() {
let trait_ref = ty::TraitRef {
def_id: tcx.lang_items.sized_trait().unwrap(),
Copy link
Member

Choose a reason for hiding this comment

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

Use if let Some here.

}
let sync_trait = ccx.tcx.require_lang_item(lang_items::SyncTraitLangItem);
let send_trait = ccx.tcx.require_lang_item(lang_items::SendTraitLangItem);
if trait_ref.def_id != sync_trait && trait_ref.def_id != send_trait {
Copy link
Member

Choose a reason for hiding this comment

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

Is this check necessary anymore? AFAIK trait_has_default_impl returns true for Send and Sync.

@@ -1611,7 +1610,7 @@ fn add_unsized_bound<'gcx: 'tcx, 'tcx>(astconv: &AstConv<'gcx, 'tcx>,
if let &hir::TraitTyParamBound(ref ptr, hir::TraitBoundModifier::Maybe) = ab {
if unbound.is_none() {
assert!(ptr.bound_lifetimes.is_empty());
unbound = Some(ptr.trait_ref.clone());
unbound = Some(ptr.trait_ref.ref_id.clone());
Copy link
Member

Choose a reason for hiding this comment

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

No need to .clone().

@@ -1622,25 +1621,26 @@ fn add_unsized_bound<'gcx: 'tcx, 'tcx>(astconv: &AstConv<'gcx, 'tcx>,

let kind_id = tcx.lang_items.require(SizedTraitLangItem);
match unbound {
Some(ref tpb) => {
Some(ref ref_id) => {
Copy link
Member

Choose a reason for hiding this comment

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

Unnecessary ref.

Builtin traits are an exception to this rule: it's possible to have bounds of
one non-builtin type, plus any number of builtin types. For example, the
Send and Sync are an exception to this rule: it's possible to have bounds of
one non-builtin type, plus either or both of Send and Sync. For example, the
Copy link
Member

Choose a reason for hiding this comment

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

s/type/trait

@eddyb
Copy link
Member

eddyb commented Nov 28, 2016

Crater report contains no legitimate regressions (ignoring uses of TyTrait in lints).

@Mark-Simulacrum Mark-Simulacrum force-pushed the trait-obj-to-exis-predicate branch 2 times, most recently from 6b84098 to 17ac55e Compare November 28, 2016 05:07
@bors
Copy link
Contributor

bors commented Nov 28, 2016

☔ The latest upstream changes (presumably #37676) made this pull request unmergeable. Please resolve the merge conflicts.

@Mark-Simulacrum
Copy link
Member Author

@eddyb Rebased, but haven't run tests--started those locally (and Travis will test too, of course).

@eddyb eddyb removed the S-waiting-on-crater Status: Waiting on a crater run to be completed. label Nov 28, 2016
(ty::Substs::empty().types().rev(), None),
};

substs.chain(opt_ty)
Copy link
Member

Choose a reason for hiding this comment

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

My suggestion was to move the .types().rev() here.

@@ -369,27 +365,30 @@ pub fn predicates_for_generics<'tcx>(cause: ObligationCause<'tcx>,
/// `bound` or is not known to meet bound (note that this is
/// conservative towards *no impl*, which is the opposite of the
/// `evaluate` methods).
pub fn type_known_to_meet_builtin_bound<'a, 'gcx, 'tcx>(infcx: &InferCtxt<'a, 'gcx, 'tcx>,
pub fn type_known_to_meet_bound<'a, 'gcx, 'tcx>(infcx: &InferCtxt<'a, 'gcx, 'tcx>,
ty: Ty<'tcx>,
Copy link
Member

Choose a reason for hiding this comment

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

There's a few indentation aberrations, here and in a few other places. Let me know if you want a list.

projection_bounds: data_a.projection_bounds.clone(),
});
let principal = data_a.principal();
let iter = principal.iter().map(|x| ty::ExistentialPredicate::Trait(x.0))
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, to make this nicer I think you should call .skip_binder on data_a and data_b (with a comment that it's reintroduced below).

let mut v = iter::once(ty::ExistentialPredicate::Trait(existential_principal.0))
.chain(auto_traits.into_iter().map(|x| ty::ExistentialPredicate::AutoTrait(x)))
.chain(existential_projections
.map(|x| ty::ExistentialPredicate::Projection(x.0)))
Copy link
Member

Choose a reason for hiding this comment

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

At least use .skip_binder() instead of .0.

@Mark-Simulacrum
Copy link
Member Author

Fixed review comments.

r? @eddyb

@eddyb
Copy link
Member

eddyb commented Nov 29, 2016

@bors r+

@bors
Copy link
Contributor

bors commented Nov 29, 2016

📌 Commit 8b82fd7 has been approved by eddyb

bors added a commit that referenced this pull request Nov 30, 2016
…=eddyb

Refactor TraitObject to Slice<ExistentialPredicate>

For reference, the primary types changes in this PR are shown below. They may add in the understanding of what is discussed below, though they should not be required.

We change `TraitObject` into a list of `ExistentialPredicate`s to allow for a couple of things:
 - Principal (ExistentialPredicate::Trait) is now optional.
 - Region bounds are moved out of `TraitObject` into `TyDynamic`. This permits wrapping only the `ExistentialPredicate` list in `Binder`.
 - `BuiltinBounds` and `BuiltinBound` are removed entirely from the codebase, to permit future non-constrained auto traits. These are replaced with `ExistentialPredicate::AutoTrait`, which only requires a `DefId`. For the time being, only `Send` and `Sync` are supported; this constraint can be lifted in a future pull request.
 - Binder-related logic is extracted from `ExistentialPredicate` into the parent (`Binder<Slice<EP>>`), so `PolyX`s are inside `TraitObject` are replaced with `X`.

The code requires a sorting order for `ExistentialPredicate`s in the interned `Slice`. The sort order is asserted to be correct during interning, but the slices are not sorted at that point.

1. `ExistentialPredicate::Trait` are defined as always equal; **This may be wrong; should we be comparing them and sorting them in some way?**
1. `ExistentialPredicate::Projection`: Compared by `ExistentialProjection::sort_key`.
1. `ExistentialPredicate::AutoTrait`: Compared by `TraitDef.def_path_hash`.

Construction of `ExistentialPredicate`s is conducted through `TyCtxt::mk_existential_predicates`, which interns a passed iterator as a `Slice`. There are no convenience functions to construct from a set of separate iterators; callers must pass an iterator chain. The lack of convenience functions is primarily due to few uses and the relative difficulty in defining a nice API due to optional parts and difficulty in recognizing which argument goes where. It is also true that the current situation isn't significantly better than 4 arguments to a constructor function; but the extra work is deemed unnecessary as of this time.

```rust
// before this PR
struct TraitObject<'tcx> {
    pub principal: PolyExistentialTraitRef<'tcx>,
    pub region_bound: &'tcx ty::Region,
    pub builtin_bounds: BuiltinBounds,
    pub projection_bounds: Vec<PolyExistentialProjection<'tcx>>,
}

// after
pub enum ExistentialPredicate<'tcx> {
    // e.g. Iterator
    Trait(ExistentialTraitRef<'tcx>),
    // e.g. Iterator::Item = T
    Projection(ExistentialProjection<'tcx>),
    // e.g. Send
    AutoTrait(DefId),
}
```
@bors
Copy link
Contributor

bors commented Nov 30, 2016

⌛ Testing commit 8b82fd7 with merge 8e373b4...

@bors bors merged commit 8b82fd7 into rust-lang:master Nov 30, 2016
@Mark-Simulacrum Mark-Simulacrum deleted the trait-obj-to-exis-predicate branch December 2, 2016 01:27
bors added a commit that referenced this pull request Dec 6, 2016
Refactor ty::FnSig to contain a &'tcx Slice<Ty<'tcx>>

We refactor this in order to achieve the following wins:

 - Decrease the size of `FnSig` (`Vec` + `bool`: 32, `&Slice` + `bool`: 24).
 - Potentially decrease total allocated memory due to arena-allocating `FnSig` inputs/output; since they are allocated in the type list arena, other users of type lists can reuse the same allocation for an equivalent type list.
 - Remove the last part of the type system which needs drop glue (#37965 removed the other remaining part). This makes arenas containing `FnSig` faster to drop (since we don't need to drop a Vec for each one), and makes reusing them without clearing/dropping potentially possible.

r? @eddyb
@tbg
Copy link
Contributor

tbg commented Apr 23, 2017

@Mark-Simulacrum: Very late to the party, but I just ran into the the sort order while spelunking for #41444, and I'm curious about:

The sort order is asserted to be correct during interning, but the slices are not sorted at that point.

What does "correct" mean? I can see from the code that the only time the assertion runs is when it is passed something that has previously been sorted using the same cmp. But I'm not sure what the point of this ordering is.

ExistentialPredicate::Trait are defined as always equal; This may be wrong; should we be comparing them and sorting them in some way?

Why not just tie-break by DefId? Or rather, why does it not matter which way two Traits compare? For interning, it seems to matter because when you have the "same" slice interned twice but the order differs, to my untrained eye it seems easy enough to introduce subtle bugs when doing pointer-comparisons, etc.

@Mark-Simulacrum
Copy link
Member Author

What does "correct" mean? I can see from the code that the only time the assertion runs is when it is passed something that has previously been sorted using the same cmp. But I'm not sure what the point of this ordering is.

Since we now represent TraitObject as a slice of ExistentialPredicates, we need an efficient method to access the parts, which means that we sort into a particular order. As long as the ExistentialPredicate variants are in the correct order, we don't care about the specific order of the variants within each "set."

I think I answered the second question as well. You do make a good point about possibly more efficient interning if we sorted with tie-breaking by DefId; that sounds like a potential optimization, though I don't know how good the wins (if any) will be.

Let me know if you have any more questions!

@eddyb
Copy link
Member

eddyb commented Apr 24, 2017

You simply can't have more than one Trait right now, so ignoring DefId is an optimization.

@tbg
Copy link
Contributor

tbg commented Apr 27, 2017

@Mark-Simulacrum: Thanks for the explanation. I have one follow-up question on the following (and it's not really a question about this code in particular any more):

Since we now represent TraitObject as a slice of ExistentialPredicates, we need an efficient method to access the parts, which means that we sort into a particular order. As long as the ExistentialPredicate variants are in the correct order, we don't care about the specific order of the variants within each "set."

The comment on Slice explains:

/// A wrapper for slices with the additional invariant
/// that the slice is interned and no other slice with
/// the same contents can exist in the same context.
/// This means we can use pointer + length for both
/// equality comparisons and hashing.

I guess it depends on what is meant by "same contents", but it would seem to me that at this point you could wind up interning both of the slices &[a,b] and &[b,a]. And, I assume you're fine with that in your use case, but in general if pointer+length is used for equality and you consider things "equal" under certain permutations, isn't there something weird that can happen (afaict, if there's only ever one Trait(_), it seems such permutations can't happen for yo in practice, but still)?

@Mark-Simulacrum
Copy link
Member Author

Slices can be compared by pointer and length because they're always interned, which means that &[a,b] and &[b,a] will not compare equally, which is "correct"; arguably if and when we add multiple Traits in the ExistentialPredicate list we should probably sort them. I'm not sure, though. @eddyb might be able to explain more clearly....

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants