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
- Read-only properties: Fields which can be accessed by public but can only be modified in private.
- 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 ofpub(in x for a, in y for b)
(similar to Swift) -
Use attributes (i.e.
#[readonly]
/#[sealed]
/final
) instead of modifyingpub(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
andmut x: T
), RFC Issue 275 (private trait items) and RFC 2028 (privacy in enum variants and trait items)? -
Will
pub(extern
cause parsing problem sincestruct 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)?