Implementing typestate

Marc Schütz via Digitalmars-d digitalmars-d at puremagic.com
Wed Sep 16 11:01:27 PDT 2015


On Wednesday, 16 September 2015 at 17:15:55 UTC, Ola Fosheim 
Grøstad wrote:
> On Wednesday, 16 September 2015 at 17:03:14 UTC, Marc Schütz 
> wrote:
>> On Tuesday, 15 September 2015 at 21:44:25 UTC, Freddy wrote:
>>> On Tuesday, 15 September 2015 at 17:45:45 UTC, Freddy wrote:
>>>>  Rust style memory management in a library
>>>
>>> Wait nevermind about that part, it's harder than I thought.
>>
>> Yeah, I thought about type-states as a way of implementing 
>> borrowing, too. I think the biggest difficulty is that the 
>> state of one object (the owner) can be affected by what 
>> happens in other objects (i.e., it becomes mutable again when 
>> those are destroyed).
>
> If the borrowed reference itself follows move semantics, can't 
> you just require it to be swallowed by it's origin as the 
> "close" operation?
>
> pseudocode:
>
> File<Open> f = open();
> (File<OpenLending> f, FileRef<Ready> r) = f.borrow();
>
> dostuff(r);
>
> (File<Open> f, FileRef<Void> r) = f.unborrow(r);
>
> File<Closed> f = f.close()

But the `unborrow` is explicit. What I'd want is to use the 
implicit destructor call:

     struct S {
         static struct Ref {
             private @typestate alias owner;
             private S* p;
             @disable this();
             this()
             typestate(alias owner) {
                 this.owner := owner; // re-alias operator
                 this.owner.refcount++;
             }
             body {
                 this.p = &owner;
             }
             this(this) {
                 this.owner.refcount++;
             }
             ~this() {
                 this.owner.refcount--;
             }
         }
         @typestate size_t refcount = 0;
         S.Ref opUnary(string op : "*")() {
             // overload address operator (not yet supported)
             return S.Ref(@typestate this);
         }
         ~this() static if(refcount == 0) { }
     }

     void foo(scope S.Ref p);
     void bar(-> S.Ref p); // move
     void baz(S.Ref p);

     S a;              // => S<0>
     {
         auto p = &a;  // => S<1>
         foo(p);       // pass-by-scope doesn't copy or destroy
                       // => S<1>
         p.~this();    // (implicit) => S<0>
     }
     {
         auto p = &a;  // => S<1>
         bar(p);       // pass-by-move, no copy or destruction
                       // => S<1>
         p.~this();    // (implicit) => S<0>
     }
     {
         auto p = &a;  // => S<1>
         baz(p);       // compiler sees only the copy,
                       // but no destructor => S<2>
         p.~this();    // (implicit) => S<1>
     }
     a.~this();        // ERROR: a.refcount != 0

The first two cases can be analyzed at the call site. But the 
third one is problematic, because inside `baz()`, the compiler 
doesn't know where the alias actually points to, because it could 
be in an entirely different compilation unit. I guess this can be 
solved by disallowing all operations modifying or depending on an 
alias type-state.

(Other complicated things, like preserving type-state through 
references or array indices, probably shouldn't even be 
attempted.)


More information about the Digitalmars-d mailing list