Capsules - Spicery/Nutmeg GitHub Wiki

This page outlines a proposal for a compiler/runtime-enforced discipline for defining a new kind of class. Classes that obey this discipline are called capsule classes (or 'capsules') and have the attractive property that their mutable state is fully isolated from all other state. This means that capsules are an ideal basis for safe, threaded computation.

Capsule Definitions

Like regular class definitions, capsules have fields called 'slots' that appear inside the capsule definition which are val (read-only) by default but may have the var, val or const modifiers applied. Slots may have initial values.

Leakproof methods that appear inside the capsule can have the modifiers private and public, modifiers that may only appear in class definitions. Private methods may only be called from other private or public methods. Public methods may be called from anywhere but their inputs and outputs are restricted. The input arguments to public methods must either be const or an instance of the capsule class; the same condition applies to the results. Slots count as methods but instead default to private.

Note that methods inside the capsule that are not declared private or public are independent. Independent methods have no access to private methods - and in fact are ordinary procedures - although they are required to be leakproof. Independence is the default. These methods are very useful for enriching the minimal interface established by the public methods.

An Example of Capsule Syntax

Just to give an idea of what capsules (might) look like, here's an example that uses the proposed syntax.

capsule CLASSNAME:

    var slot alpha = 99         ### slots default to private
    val slot beta = MutableList()

    @private
    def ^addItem( x ):          ### ^ is simply syntactic sugar for 'this.'
        if x > ^alpha:
            ^beta.Add( x )
        end
    end

    @public
    def ^bumpAndAdd( n ):
        ^alpha <- ^alpha + 1
        ^addItem( n )
    end

    @independent               ### Methods that are neither private or public faces of the class
    def ^bumpAndAddTwice( n ):  ### are independent. This is the default declaration. 
        ^bumpAndAdd( n )
        ^bumpAndAdd( n )
    end

endcapsule

How Capsules Protect their State

The key principle is that all state S that is reachable from an encapsulated object E is only reachable from the outside by going through E. This is illustrated below.

Diagram showing capsule as gatekeeper to capsule state

This is achieved by:

  1. Constraining public methods so that there is no import of shared mutable state into the capsule's state, because the arguments are const.
  2. Constraining public methods from exporting mutable state from the capsule, because the results are const.
  3. Constraining both public and private methods to be leakproof (this is a side-condition that prevents them leaking store 'sideways').
  4. Constraining private methods, which may freely access the state of an object E, to return the values back through public methods of E.
  5. The extremely paranoid reader will note this leaves a loophole - see the next section for how the loophole is closed.

Cracking Down on State Smugglers

The above constraints leave an easy-to-exploit loophole, which is that a public method might be able to return a 'smuggler' procedure that uses private methods to dig out some supposedly encapsulated state. Such a smuggler is potentially permitted because it doesn't leak sideways and hence is a permissible return value. The problem is that the smuggler ingeniously bypasses a key requirement that private methods are dynamically called within the context of the public method of the same object.

capsule Broken:
    slot myPrivateList = MutableList()

    @public
    def ^getSmuggler():
        myPrivateList                 ### Export an abstraction breaker.
    end
endcapsule

val broken = Broken()
val smuggler = broken.getSmuggler()   ### smuggler can now be used to get access to encapsulated private store!
val muahaha = smuggler( broken )      ### The smuggler passes back the private state.
muahaha.add( 'Now you are mine, all mine!' )

Smuggling completely breaks encapsulation because it allows a private method to be called outside the dynamic-scope of the public method. It highlights the fact that, in order for encapsulation to work, private methods can only operate inside a public methods scope. One way to see this rule is to imagine using a debugger and putting a break point on a private method. When the private method is invoked on object E, we go into the debugger and can walk back up the call-stack and we encounter a series of non-public, leakproof procedures that are capped by an invocation of a public method on the same object E. In other words, the nearest enclosing public method is for the same object and none of the intervening calls can leak.

This also tells us how to beat the smugglers - by arranging that private methods check the call-stack and ensure they are only called in the right circumstances. Of course, ordinary calls to private methods can dispense with the checking. If the compiler can see that the private method is being called in the right context, which they almost always can, then no runtime check is required.

Ruptures and End-of-life

Quite often we only need a capsule to perform a single job and, after that, it will never be used again. In this case we can reuse the internal store of the capsule. This is what ruptures are for. A rupture is a special kind of public method. It has access to private methods but none of the other limitations associated with public methods.

Calling a method marked as a @rupture will immediately mark a capsule as 'dead' and this will block all subsequent public method calls. Ruptures cannot be called from other public methods and, if necessary, will scan the call-stack to verify that they are being used correctly. Aside: if a rupture needs to call a public method, that method needs be additionally marked as private.

Typically the rupture is used to return a portion of the capsule's mutable state that has been carefully prepared. Rather than needlessly copy the internal state, the rupture is used to 'kill' the capsule and raid the contents freely.

Public Returns

Up to this point, we have stated that public methods only return const values (or the capsule itself). However there is a way that a public can return mutable store via the return statement - hence the phrase 'public return'. The idea is simple enough: within a return statement we allow the use of a category of procedures called allocators. Allocators are leakproof and can only be passed const values but are allowed to return mutable state; the alert reader should be able to see they are a slightly relaxed finesse.

Although it would be a little unusual, you can put if statements and for loops inside return statements. However, any code inside the return statement may only refer to const variables. In other words, the return statement is a boundary through which no mutable state can be smuggled. However, once you are inside, you're free to allocate new mutable state. And because everything is leakproof, we know that the returned state is guaranteed to be unshared.