A level of access control between fileprivate and internal, for code partitioning

Continuing the discussion from Access control below public [not `protected`]:

I suggested this from a resurrected thread.

If all the parts of a module being able to pick apart all the other parts of the same module is too much, while at the same time there does need to be limited sharing between certain files within a module, so using private or fileprivate is wrong in the other direction, then we need something to partition off a module into clusters of functionality. Code in the same module, but a different partition, can only see the internal (or above) items from other partitions. This maintains encapsulation without needing to create multiple modules or creating a sub-module concept and figuring out its semantics. These partitions are not visible to code outside the module.

The only syntax I can guess is to use:

cluster "ClusterName"

near the top of the file to indicate which partition the current file is part of, then use

clusterprivate func MyFunc() { /*...*/ }

This function can only be seen within the same file, and any other file in the module that uses "ClusterName" as its partition. Files that don't use the cluster directive aren't part of any partition. Using the clusterprivate access level within said files is an error.

A file can have only one cluster directive. (Should we let it be controlled by conditional compilation?)

As an idea for (what I think is) better syntax:
cluster -> mod "small module" / the keyword Rust uses
clusterprivate -> internal
(the old) internal -> external

All old "large" modules contain a single implicit "small" module
This means changing the meaning of internal is not source breaking (I think)

New "large" modules can contain several "small" modules

1 Like

There is a sort of semi-security problem with this. A file can insert itself into a cluster, which sort of negates the closed-borders protection that private/fileprivate gives. Currently, you can only "invade" the borders by modifying a source file or type that's inside, which is likely to be noticed. With cluster you could do it stealthily.

Of course, it's not really a security problem, but a procedural one. But what good is privacy protection you're relying on within your implementation, if any client of your (source) code can simply ignore the protection?

The safer way is for something inside the border to grant access to something outside. That's heading in the direction of C++ friend.

1 Like

One of us is misunderstanding something. The access levels are:

private → fileprivate → clusterprivate → internal → public → open

All files within the same cluster must be part of the same module. Clusters are completely private and enclosed to their module; they are a hierarchical step between a single file and the entire module, multi-file without having to manage subdirectories and/or sub-module concepts.

A given module that imports another has no idea what the second's cluster graph is, or even if it had any. Reusing a cluster name in the first module does not give it access to a cluster of the same name in the second module. After whatever part of the AST or SIL confirms that there are no access-level violations, the existence of clusters is purged before IR-Gen and never makes it to the object code.

My opening post was off-the-cuff, but I imagine that a type can straddle multiple clusters. If we have a type X that is spread into clusters A and B, the only way X's code in one cluster can call the other's code is via the other cluster's parts of X that are internal/public/open. This implies that every clusterprivate stored property shares the same cluster, that of the primary definition. (Cases have to have the same publicity level as their type, so the only way cases would be hidden from another cluster is if the entire type was sub-internal to begin with.)

Would submodules be a good alternative here? I personally use multiple modules while developing code so I can make sure I enforce proper layering, but then conditionally compile (by default) to build a single module for release.

The point of this proposal is as a lighter alternative to submodules. It would have the effect that you already use for development, ensure proper layering between internal sub-sections while looking like a unified whole from the outside.

I'm talking about within a module too. This is for APIs that are used elsewhere in the same module, whether or not they're exposed publicly outside the module.

Currently, if I implement a class or group of classes in a single file, and I choose to hide implementation details behind private/fileprivate barriers, then the only way a co-worker working within the same module can bypass my privacy decisions is to visibly modify my code.

If I use cluster instead, I can't prevent a co-worker from reaching into my implementation and using information I don't want to expose. Since that's not "my source code", chances are I won't even know about it.