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

Semantic "private in public" enforcement. #1671

Closed
wants to merge 1 commit into from

Conversation

eddyb
Copy link
Member

@eddyb eddyb commented Jul 9, 2016

# Motivation
[motivation]: #motivation

The "private-in-public" rules ensure the transitivity of abstraction. That is, one must be able to name types and bounds used in public APIs, from anywhere else.
Copy link
Contributor

@petrochenkov petrochenkov Jul 9, 2016

Choose a reason for hiding this comment

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

"able to name" is not entirely correct, "able to name after addition of arbitrary number of reexports" is closer to the truth. E.g.

mod m {
    mod detail {
        pub struct S;
    }
    pub fn f() -> detail::S { detail::S }
}

is allowed because S is potentially nameable outside of m (if reexported), but not actually nameable (private-in-public checker doesn't analyze reexport chains to determine actual nameability by design).
I'd like to avoid promising namebility in docs, including RFCs, because it's already one of the most common misconceptions about our privacy system.
EDIT: The definition of "less public" below also uses nameability ("can be referred to by any name").

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, I remember reading about this before, but I wasn't clear on what decisions were taken.
It does weaken the "transitivity of abstraction" argument in that case, sadly.

Copy link
Contributor

Choose a reason for hiding this comment

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

How odd, link to rational for this?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks so much!

@eddyb
Copy link
Member Author

eddyb commented Jul 9, 2016

I want to have this outside of diff comments too: @petrochenkov pointed out that name, type alias, UFCS and associated type resolution combined (i.e. everything the compiler does to go from the syntactic to the semantic type-system) are enough to reveal all possible private type leaks in public APIs.
We then wouldn't even need to look at bounds, if all we care about is preventing private type leaks, and not the ability to write perfect proxies or anything else.

I agree this would greatly simplify the RFC and the implementation work and allow deriving to work even in dubious situations where the set of implementations available to a bound is completely hidden.

Does anyone else (e.g. from @rust-lang/lang) have anything against that?
If not, I'll update (or rewrite, even) the RFC to reflect the simpler solution.

@Ericson2314
Copy link
Contributor

@eddyb sounds great to me! I'd ideally like the docs to evaluate/resolve types as little as possible---just enough not leak. Maybe we can someday leverage incremental compilation for this. (Type language is pure and thus can be safely lazily evaluated.)

@petrochenkov
Copy link
Contributor

@eddyb

We then wouldn't even need to look at bounds

The main argument against this change probably lies outside of the language - documentation.
A function like fn f<T: PrivateBounds>(arg: T) { .... } is a black box, unless it's well documented in prose. It's not clear what arguments it can accept. If wrong arguments are provided then users see error messages like "PrivateTrait is not satisfied", what can they do with them? (more detailed comment about this).
On the other hand, derived impls - the main beneficiary from private bounds - don't care about documenting bounds.

@petrochenkov
Copy link
Contributor

petrochenkov commented Jul 10, 2016

"nothing private can be leaked" is the core guarantee from the current privacy system (#1671 (comment))

I should write this out in more detail. The guarantee is that entities with visibility pub(m) "can't escape" from the module m.
What "can't escape" means for different entities?

  1. For imports, extern crates, modules, type aliases, functions, statics, constants, foreign functions, foreign statics, struct fields, associated functions/types/constants this simply means that they can't be named outside of module m. This is ensured by the usual privacy checker ("can't access private X") and limitations on reexports ("X is private, and cannot be reexported").

  2. For types (structs and enums) and traits-as-types this additionally means that values of those types cannot be obtained. This is ensured by the private-in-public checker. The private-in-public has to check bounds and where clauses due to situations like this, unless types are fully transformed into their "semantic" form (ty::Ty) before checking.

  3. Traits. When should a trait be considered escaping from its module (In addition to conditions from 1. and 2.)? This is the key question to answer before relaxing the private-in-public rules.

    Regardless of the discussion about predicates above, I think a trait should be considered leaked if its items are available outside of its module. This can happen if the private trait has public sub-traits (pub trait PubTr: PrivTr { .... }), then its methods are available through its sub-traits. Returning a public subtrait like a trait object fn f() -> Box<PubTr> { .... } also kind of makes PrivTr leaked as a type too in some sense.
    So I think private-in-public checker still needs to check bounds on trait definitions (i.e. supertraits).

@nrc nrc added the T-lang Relevant to the language team, which will review and decide on the RFC. label Jul 10, 2016
@crumblingstatue
Copy link

Does this aim to address rust-lang/rust#30905?
More specifically, I am very interested in resolving this type of API weirdness:

// This type is inacessible outside of the crate, but "public" to the entire
// crate, since it's at the crate root.
struct CratePrivType;

mod private {
    // This function is inacessible outside of the crate, so it should be okay
    // to return a crate-private type
    pub fn gimme_crate_priv() -> super::CratePrivType { unimplemented!() }
}

fn main() {}
error: private type in public interface [--explain E0446]
 --> <anon>:8:34
  |>
8 |>     pub fn gimme_crate_priv() -> super::CratePrivType { unimplemented!() }
  |>                                  ^^^^^^^^^^^^^^^^^^^^

To work around this without exposing the crate-private type, one can put it in a private module.

mod crate_priv {
    pub struct CratePrivType;
}

mod private {
    pub fn gimme_crate_priv() -> ::crate_priv::CratePrivType { unimplemented!() }
}

fn main() {}

But this once again shows the stupidity of the current analysis.
You can now use this type in your public API, even though it's supposed to be private.

mod crate_priv {
    pub struct CratePrivType;
}

mod private {
    pub fn gimme_crate_priv() -> ::crate_priv::CratePrivType { unimplemented!() }
}

pub fn pub_api_func() -> ::crate_priv::CratePrivType { unimplemented!() }

fn main() {}

Even rustdoc gets confused by this, and the crate private type will not get documented, even though
it will appear in the signature of pub_api_func.

@eddyb
Copy link
Member Author

eddyb commented Jul 12, 2016

@crumblingstatue We could restrict the check to the same module, which would allow that code to compile.
It's orthogonal to how the check is done though, i.e. it could be implemented with the current system too.

@petrochenkov
Copy link
Contributor

petrochenkov commented Jul 12, 2016

@crumblingstatue
This was resolved by RFC 1422, your example is supposed to be written like this:

#![feature(pub_restricted)]

// This type is inacessible outside of the crate, but "public" to the entire
// crate, since it's at the crate root.
struct CratePrivType;

mod private {
    // This function is inacessible outside of the crate, so it should be okay
    // to return a crate-private type
    pub(crate) fn gimme_crate_priv() -> super::CratePrivType { unimplemented!() }
}

fn main() {}

Playpen: https://is.gd/TFqG8o

@crumblingstatue
Copy link

@petrochenkov You're right, pub(restricted) does solve the issue I was having. Thanks, and apologies for invading this thread.

@nrc
Copy link
Member

nrc commented Jul 14, 2016

@nrc
Copy link
Member

nrc commented Jul 14, 2016

cc @pnkfelix do you remember the ins and outs of this from the last RFC discussion?

@strega-nil
Copy link

ping @eddyb

status?

@eddyb
Copy link
Member Author

eddyb commented Dec 21, 2016

@ubsan See the latest comments on rust-lang/rust#34537. We're more likely to switch to a different scheme, checking types as they appear after inference, as opposed to only checking direct uses.
Not sure what the next step is, though.

@strega-nil
Copy link

strega-nil commented Dec 21, 2016

@eddyb sounds like a close to me. Or at least a rewrite.

@eddyb
Copy link
Member Author

eddyb commented Dec 21, 2016

@ubsan Oh right, it's my PR so I can just do this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants