Pre-RFC: pub(restricted for action)


#1

Just writing down some ideas since the Sealed traits topic is bumped up again and RFC 2008 (#[non_exhaustive]) also talked about sealed traits.

I will probably not turn it into a real RFC in the near future since I haven’t encountered the motivation cases yet. But if anyone is interested, welcome to push it through…


  • Feature name: pub_access_control
  • Start date: not today
  • RFC PR: (empty)
  • Rust issue: (empty)

Summary

Enable fine-grained privacy check based on action.

pub struct File {
    pub(self for mut, extern for else) fd: i32,
}

pub(crate for impl, extern for else) trait IsFile {}

impl IsFile for File {}

Motivation

  1. Read-only properties: Fields which can be accessed by public but can only be modified in private.
  2. Sealed traits: Traits which can be used as bounds but should not be extended by public.

Detailed design

General semantics

Extend the pub(restricted) syntax to read:

pub
// everyone can do anything
pub(extern)
// equivalent to above
// alternative syntax: pub(pub), pub(::), pub(*), pub(🌎), ...

pub(in path)    
// code inside `path` can do anything, 
// code outside `path` cannot use it (same as RFC 1422)

// assume `mod x { mod y { ... } }`
pub(in x for play, in y for record, …) 
// in x: allowed access, cannot "record", can "play"
// in y: allowed access, can "record", can "play" (inherited from `in x for play`)
// outside: private

pub(self for record, extern for else)
// only this module can "record"
// everyone have the remaining permissions ("else")
//
// alternative syntax are rejected:
//    pub(for else, self for record) <- ignoring the module path is awkward
//    pub(self for record) <- does not tell reader that
//                            the item is still accessible for everyone

Allowing access to some actions imply access to another. For instance, if a module can “mut” (take mutable reference) to a struct field, it can certainly do everything else for that memory location, so

pub(crate for mut, self for else)
// Error. Outer module (crate) given stronger permission (mut) 
//        than inner module (self)

Every action must be covered, so below would be an error

pub(in x for record)
// Error. Does not make it clear to reader who will get the remaining permission.

pub(in x for play, in y for record)
// Still error. If the item gained a new permission this will no longer compile.

pub(in x for else, in y for record)
// The only future-compatible way.

Struct/union fields

Struct/union fields will have one permission:

  • “mut”: allows taking a mutable reference and assigning to the field in constructor/FRU.

The remaining actions must be all weaker than “mut”, including taking a shared reference and moving the field out

#[derive(Default)]
pub struct S {
    pub(self for mut, extern for else) a: u32,
    pub b: u32,
}
let s = S { a: 1, .. S::default() } 
// only allowed in current module which have "set" permission for field "a"
let t = S { b: 1, .. S::default() }
// allowed everywhere.
let u = S { a: 1, b: 2 } 
// only allowed in current module which have "set" permission for field "a"

Trait items

Trait items will have one permission:

  • “impl”: allows implementing the trait.

The remaining actions must be all weaker than “impl”, including using the trait as a bound, as a trait object, as impl Trait, and use the methods and associated things inside the trait.

Reexports

Access control can be reduced in reexported items (i.e. become more private), e.g.

mod a {
    pub trait Normal {}
    pub(self for impl, extern for else) trait Sealed {}
}
mod b {
    pub(self for impl, extern for else) use ::a::Normal; 
    // b::Normal cannot be impl since that access is not reexported
    pub(extern for impl, extern for else) use ::a::Sealed;
    // b::Sealed still cannot be impl since mod b doesn't have it
    // this one should be an error.
    pub use ::a::Sealed as AlsoSealed;
    // b::AlsoSealed is also not impl (same as b::Sealed, but without error)
}

This also applies to type alias and trait alias (RFC 1733).

How We Teach This

TODO. Probably teach together with pub(restricted).

The error message for access control violation may be updated to read:

error: field `z` of struct `y::x::S` is private for mutation
  --> <anon>:21:5
   |
21 |     e.z = 4;
   |     ^^^

Drawbacks

  • Makes the full pub syntax extremely long. Impairs code readability.

  • Introduced a new syntax for some pretty niche use case.

  • There have been working alternatives — read-only fields can already be approximated by getter methods, sealed traits is already solved (?) using private supertrait.

Alternatives

  • Use another syntax instead of <path> for <action>.

  • pub(in x for a) pub(in y for b) instead of pub(in x for a, in y for b) (similar to Swift)

  • Use attributes (i.e. #[readonly]/#[sealed]/final) instead of modifying pub(restricted)

  • Do nothing. (See last point of the drawbacks section)

Unresolved questions

  • How would this affect RFC 1546 (fields in traits; currently using the syntax x: T and mut x: T), RFC Issue 275 (private trait items) and RFC 2028 (privacy in enum variants and trait items)?

  • Will pub(extern cause parsing problem since struct T(pub (extern fn(),)) is already a valid syntax?

  • If this is accepted, should the long visibility still formatted in line, or put it in a new line (like attributes)?


#2

path for action feels to me like a very generic open-ended syntax, but it’s not the same feature used in multiple places, but a mini query language for selecting one of a few different specific language features.

So personally I’d prefer specific keywords for specific behaviors, e.g. pub(self for mut, extern for else) is a long way of saying pub(readonly). The latter looks familiar to me from ObjC.


#3

pub(readonly) is fine if you only want universal-read/private-write, but it has to be able to combine with pub(restricted). Something like pub(crate & readonly). Which I think is no better than just saying #[readonly] pub(crate).