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 RFC 1238: nonparametric dropck. #28861

Merged
merged 16 commits into from
Oct 10, 2015

Conversation

pnkfelix
Copy link
Member

@pnkfelix pnkfelix commented Oct 6, 2015

implement RFC 1238: nonparametric dropck.

cc #28498

cc @nikomatsakis

Implement cannot-assume-parametricity (CAP) from RFC 1238, and add the
UGEH attribute.

----

Note that we check for the attribute attached to the dtor method, not
the Drop impl.

(This is just to match the specification of RFC and the tests; I am
not wedded to this approach.)
I needed it in `RawVec`, `Vec`, and `TypedArena` for `rustc` to
bootstrap; but of course that alone was not sufficient for `make
check`.

Later I added `unsafe_destructor_blind_to_params` to collections, in
particular `LinkedList` and `RawTable` (the backing representation for
`HashMap` and `HashSet`), to get the regression tests exercising
cyclic structure from PR rust-lang#27185 building.

----

Note that the feature is `dropck_parametricity` (which is not the same
as the attribute's name). We will almost certainly vary our strategy
here in the future, so it makes some sense to have a not-as-ugly name
for the feature gate. (The attribute name was deliberately selected to
be ugly looking.)
Illustrates cases that worked before and must continue to work, and a
case that shows how to use the `unsafe_destructor_blind_to_params`
attribute (aka "the UGEH attribute") to work around
cannot-assume-parametricity.
One just checks that we are feature-gating the UGEH attribute (as
usual for attributes associated with unstable features).

The other is adapted from the RFC 1238 text, except that it has been
extended somewhat to actually *illustrate* the scenario that we are
trying to prevent, namely observing the state of data, from safe code,
after the destructor for that data has been executed.
@rust-highfive
Copy link
Collaborator

r? @nrc

(rust_highfive has picked a reviewer for you, use r? to override)

@pnkfelix
Copy link
Member Author

pnkfelix commented Oct 6, 2015

r? @arielb1

@rust-highfive rust-highfive assigned arielb1 and unassigned nrc Oct 6, 2015
@pnkfelix
Copy link
Member Author

pnkfelix commented Oct 6, 2015

hmm, weird about the travis failure, I thought I ran make check but maybe I only ran make check-rpass check-cfail on this go-round.

@pnkfelix
Copy link
Member Author

pnkfelix commented Oct 6, 2015

@arielb1 responding to your IRC comments here, since you logged out of IRC AFAICT:

arielb1 wrote:

pnkfelix: I don't like your test
to gh28861
it exploits a soundness bug

preventing that and dropck-specialization is different
from what the test is trying to show
I would have a trivial container
with a destructor

I infer based on your response that your problem here is with compile-fail/issue28498-reject-ex1.rs? Would the original example from the RFC be a suitable "trivial container" to illustrate this?

(I ask because I view compile-fail/issue28498-reject-ex1.rs as an expansion of such a trivial container, so I am a little surprised to see you react so strongly to it. But switching to the example from the RFC is certainly easy; I just wanted to better understand the problem you have, which I assume is that it may confuse other people reading the commits about how truly simple-minded the new dropck is...)

  • Update: On re-reading compile-fail/issue28498-reject-ex1.rs, I now retract my claim that it is merely an "expansion" of a trivial container. I have come around to @arielb1 's POV that it really is demonstrating something different, and I should just go back to the example from the RFC.

I would also add a test for the interaction of UGEH with param bounds

Good idea; will do.


pnkfelix: I don't like the "unreachable" parametricity-checking code

Yeah that had a "bad code smell" to me as well. The reason I left it in:

  • other work has led me to think that the unsafe_destructor_blind_to_params attribute is not fine-grain enough (it essentially tags all type formals as parametric, when I think we actually want to specify parametricity for individual type parameters for some cases, if not now then in the near future),
  • and I was/am not sure if a finer-grain attribute will end up needing the other code, so I didn't want to immediately delete it.

I am in the midst of investigating the finer-grain attribute now, so that should help inform me as to whether the other code should indeed be deleted

…1.rs

(It is not *exactly* the text from the RFC, but the only thing it adds
is a call to a no-op function that is just an attempt to make it clear
where the potential for impl specialization comes from.)
@@ -578,6 +578,16 @@ impl<'tcx> ty::ctxt<'tcx> {
});
let generics = adt.type_scheme(self).generics;

// RFC 1238: if the destructor method is tagged with the
Copy link
Contributor

Choose a reason for hiding this comment

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

This whole method should be !self.has_attr(dtor_method, "unsafe_destructor_blind_to_params") - unless you have some other plan.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, yes, this code as it stands could be reduced to that.

My intention had originally been to continue returning true from is_adt_dtorck if the adt has region params.

Looking now, I see that the RFC directly says in its description of UGEH that the attribute "will allow a destructor to side-step the dropck's constraints." For some reason while writing the RFC I had thought that was only talking about the type parameters (that's why there's the explicit alternative of having UGEH for lifetime parameters).

But at this point, I don't see much reason to stick with my original interpretation. This one (where UGEH causes all constraints to be ignored) is easier to explain and implement.

@arielb1
Copy link
Contributor

arielb1 commented Oct 6, 2015

I would prefer to either have a test that UGEH works with a "real" bound - like my fmt::Debug example, but made non-UB - or to still have the parametricity check. Documenting the unsafety rules somewhere would be nice (i.e. not using lifetime parameters, not using trait bounds, not calling function pointers accessible from self whose parameters contain a type parameter).

@pnkfelix
Copy link
Member Author

pnkfelix commented Oct 8, 2015

Okay I agree with both of @arielb1's points in that last comment:

  1. I need more tests to illustrate some of the other scenarios that arise here, and
  2. I need to improve the documentation -- not sure where that should go, maybe in a PR on the rustonomicon doc.

@pnkfelix
Copy link
Member Author

pnkfelix commented Oct 8, 2015

@arielb1 do you want to review the new tests and docs, or shall I hand that off to someone else?

@bstrie
Copy link
Contributor

bstrie commented Oct 8, 2015

@pnkfelix does this close #26656?

@arielb1
Copy link
Contributor

arielb1 commented Oct 8, 2015

@bstrie

It essentially works around it.


The precise rules that govern drop checking may be less restrictive in
the future.

Copy link
Contributor

Choose a reason for hiding this comment

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

I wouldn't really call the current analysis conservative (even though it is), given that it just doesn't care about the destructor.

Copy link
Member Author

Choose a reason for hiding this comment

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

... it cares that the Drop impl exists, and is not attempting any sort of parametricity analysis of how the type parameters are used in the type structure.

To me, that consists of a more conservative analysis.

Are you arguing that I should be also pointing out that it is now a trivial analysis?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that is a better way to put it - we require the programmer to specify whether a destructor is safe to call given only destruction-safety.

@arielb1
Copy link
Contributor

arielb1 commented Oct 8, 2015

@gankro: could you take a look at the docs?

/// safe for destruction requires it to be alive
/// Returns true if this ADT is a dtorck type, i.e. whether it
/// being safe for destruction requires all borrowed pointers
/// reachable by it to have lifetimes strictly greater than self.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I used "alive" intentionally - phantom lifetimes are just as important as borrowed pointers here. "Greater than self" is also not entirely accurate - you can totally have a &'a mut MyTrait+'a (or even a &'a mut TypeWithDestructor<'a>, if you are willing to use unsafe means to actually create and destroy it), you just can't call its destructor outside 'a.

Maybe

    /// Returns whether this ADT is a dtorck type. If not, it
    /// is safe to call this type's destructor even when it does
    /// not actually outlive the call, as long as its contents
    /// remain destruction-safe.

Copy link
Member Author

Choose a reason for hiding this comment

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

hmm, well that's better than the previous use of "alive", which I thought was too easy to misinterpret.

Copy link
Contributor

Choose a reason for hiding this comment

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

I wrote it before the "outlives" reform.

Copy link
Member Author

Choose a reason for hiding this comment

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

(I still have some doubts about the wording. E.g. the phrase "even when it does not actually outlive the call [to the destructor]" troubles me, probably because I am having problems choosing a consistent denotation for "it" and "outlives".)

I'll try to come up with something better that still satisfies the constraints you listed above.

@arielb1
Copy link
Contributor

arielb1 commented Oct 8, 2015

A point I want to document, not necessarily in this PR:

  • In most cases, when a method is called, its receiver is required to outlive the call.
  • If this was required by drop glue (or destructors), it would not be possible to create circular reference graphs.
  • Therefore, we have the (weaker) notion of "destruction-safety". Types without destructors are destruction-safe when their contents are.
  • To allow for library-defined containers, it is possible to create types with destructors that are destruction-safe when their contents are.
  • However, these types must be careful as they can be destroyed with their contents not being alive.

@eefriedman
Copy link
Contributor

Is there some logic behind the specific types in the standard library you chose to mark #[unsafe_destructor_blind_to_params]? For example, VecDeque is marked, but HashSet isn't.

@eefriedman
Copy link
Contributor

Nevermind, didn't see the comment on pnkfelix@d778e57 .

Would it make sense to mark Rc/Arc?

@pnkfelix
Copy link
Member Author

pnkfelix commented Oct 9, 2015

@eefriedman marking Rc / Arc seems sound, but I'd prefer not to expand the scope of marked types without seeing a motivating example...

Update: I constructed an example. :) I'll add it as a test and put the marker on Rc (and probably Arc too).

pnkfelix added a commit to pnkfelix/rust that referenced this pull request Oct 9, 2015
1. Added big comment block explaining the test framework.

2. Added tests exericising Rc and Arc. This was inspired by a comment
   from eefriedman on PR rust-lang#28861.

3. Made the cycle-detection not issue false-positives on acyclic dags.

   Doing this efficiently required revising the framework; instead of
   visiting all children (i.e. doing a traversal), now each test is
   responsible for supplying the path that will act as a witness to
   the cycle.

   Luckily for me, all of the pre-existing tests worked with a trivial
   path built from "always tke your first left", but new tests I added
   did require other input paths (i.e., "first turn right, then left".

   (The path representation is a bit-string and its branches are
    n-ary, not word phrases and binary branches as you might think
    from the outline above.)
This was proven necessary after I added `Rc` and `Arc` to the rpass
test `dropck_legal_cycles.rs`; see PR rust-lang#28929.
bors added a commit that referenced this pull request Oct 9, 2015
Major revision to the dropck_legal_cycles test.

1. Added big comment block explaining the test framework.

2. Added tests exericising Rc and Arc. This was inspired by a comment
   from eefriedman on PR #28861.

3. Made the cycle-detection not issue false-positives on acyclic dags.

   Doing this efficiently required revising the framework; instead of
   visiting all children (i.e. doing a traversal), now each test is
   responsible for supplying the path that will act as a witness to
   the cycle.

   Luckily for me, all of the pre-existing tests worked with a trivial
   path built from "always tke your first left", but new tests I added
   did require other input paths (i.e., "first turn right, then left".

   (The path representation is a bit-string and its branches are
    n-ary, not word phrases and binary branches as you might think
    from the outline above.)

cc PR #27185
@arielb1
Copy link
Contributor

arielb1 commented Oct 9, 2015

@bors r+

@bors
Copy link
Contributor

bors commented Oct 9, 2015

📌 Commit a445f23 has been approved by arielb1

@arielb1
Copy link
Contributor

arielb1 commented Oct 9, 2015

The code is desugared into

use std::cell::Cell;
struct C<'a> { val: Cell<Option<&'a C<'a>>> }

fn main() {
    'd: {
        let t: (C<'a>, C<'a>);
        'a: {
             t = (C { val: Cell::new(None) }, C { val: Cell::new(None) });
            Cell::set(&t.0.val, Some(&t.1));
            Cell::set(&t.1.val, Some(&t.0));
        }
        del t;
    }
}

This program is somewhat suspect, because t is not alive during the call to del t, but as the types involved are non-dtorck types, there is no such actual constraint (if they were dtorck types, the constraint would cause the lifetime parameters, and the borrows, to be inferred to 'd, which would cause a borrowck error).

@bors
Copy link
Contributor

bors commented Oct 10, 2015

⌛ Testing commit a445f23 with merge 87cd2c0...

bors added a commit that referenced this pull request Oct 10, 2015
…elb1

implement RFC 1238: nonparametric dropck.

cc #28498 

cc @nikomatsakis
@bors bors merged commit a445f23 into rust-lang:master Oct 10, 2015
@Gankra
Copy link
Contributor

Gankra commented Oct 10, 2015

A bit late to the party for that review...

Easy to fix in Post.

@pnkfelix
Copy link
Member Author

@gankro yeah I'll make a follow-up PR with further revisions to nomincon.

@pnkfelix
Copy link
Member Author

@arielb1 I have a question:

Your most recent comment above, that starts with: "The code is desugared into ..."

What is the context of that note?

Is that text that you think would be useful to add somewhere in the nomicon? Or are you pointing out something about one of the tests (in which case I am not able to infer what test you are referencing).

My best guess is that it is an example that you want added to the nomicon, but I am not certain.

pnkfelix added a commit to pnkfelix/rust that referenced this pull request Oct 14, 2015
1. Added big comment block explaining the test framework.

2. Added tests exericising Rc and Arc. This was inspired by a comment
   from eefriedman on PR rust-lang#28861.

3. Made the cycle-detection not issue false-positives on acyclic dags.

   Doing this efficiently required revising the framework; instead of
   visiting all children (i.e. doing a traversal), now each test is
   responsible for supplying the path that will act as a witness to
   the cycle.

   Luckily for me, all of the pre-existing tests worked with a trivial
   path built from "always tke your first left", but new tests I added
   did require other input paths (i.e., "first turn right, then left".

   (The path representation is a bit-string and its branches are
    n-ary, not word phrases and binary branches as you might think
    from the outline above.)
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.

8 participants