Let's see if we can make this work in two steps: first, making the existing scope storage class work, and second, but considering making it the default.

First, let's define it. A scope reference may never escape its scope. This means:

0) Note that scope is irrelevant on value types. I believe it is also mostly irrelevant on references to immutable data (such as strings) since they are de facto value types. (A side effect of this: immutable stack data is wrong.... which is an arguable point, since correct enforcement of slices into it would let you maintain the immutable illusion. Hmm, both sides have good points.)

Nevertheless, while the immutable reference can be debated, scope definitely doesn't matter on value types. While it might be there, I think it should just be a no-op.

1) It or its address must never be assigned to a higher scope. (The compiler currently disallows rebinding scope variables, which I think does achieve this, but is more blunt than it needs to be. If we want to disable rebinding, let's do that on a type-by-type basis e.g. disabling postblit on a unique ptr.)

void foo() {
   int[] outerSlice;
   {
      scope int[] innerSlice = ...;
      outerSlice = innerSlice; // error
innerSlice = innerSlice[1 .. $]; // I think this should be ok
   }
}

Parameters and return values are considered the same level for this, since the parameter and return value both belong to the caller. So:

int[] foo() {
   int[15] staticBuffer;
   scope int[] slice = staticBuffer[];
return slice; // illegal, return value is one level higher than inner function
}

// OK, you aren't giving the caller anything they don't already have
scope char[] strchr(scope char[] s, char[]) { return s; }

It is acceptable to pass it to a lower scope.

int average(in int[]); // in == const scope

void foo() {
    int[15] staticBuffer;
    scope int[] slice = staticBuffer[];
int avg = average(slice); // OK, passing to inner scope is fine
}


scope slice.ptr and &scope slice's return values themselves must be scope. Yes, scope MUST work on function return values as well as parameters and variables. This is an absolute necessity for any degree of sanity, which I'll talk about more in my next numbered point.


BTW I keep using slices into static buffers here because that's the main real-world concern we should keep in mind. A static buffer is a strictly-scoped owned container built right into the language. We know it is wrong to return a reference to stack data, we know why. Conversely, we have a pretty good idea about what *can* work with it. Scope, if we do it right, should statically catch misuses of static array slices while allowing proper uses.

So when in doubt about something, ask: does this make sense when referring to a static buffer slice?

2) scope must be carried along with the variable at every step of its life. (In this sense, it starts to look more like a type constructor than a storage class, but I think it is slightly different still.)

void foo() {
   int[] outerSlice;
   {
       int[16] staticBuffer;
       scope int[] innerSlice = staticBuffer[]; // OK
int[] cheatingSlice = innerSlice; // uh oh, no good because...
       outerSlice = cheatingSlice; // ...it enables this
   }
}


A potential workaround is to require every assignment to also be scope.

       scope int[] cheatingSlice = innerSlice; // OK
outerSlice = cheatingSlice; // this is still disallowed, so cool

It is very important that this also applies through function return values, since otherwise:

T identity(T)(scope T t) { return t; }

can and will escape references. Consider strchr on a static stack array. We do NOT want that to return a pointer to the stack memory after it ceases to exist.

This, that identity function should be illegal with cannot return scope from a non-scope function. We'll allow it by marking the return value as scope as well. (Again, this sounds a lot like a type constructor.)


3) structs are considered reference types if ANY of their members are reference types (unless specifically noted otherwise, see my following post about default and encapsulation for details). Thus, the scope rules may apply to them:

struct Holder {
   int[] foo;
}

Holder h;
void test(scope int[] f) {
h.foo = f; // must be an error, f is escaping to global scope directly h = Holder(f); // this must also be an error, f is escaping indirectly
}

The constructed Holder inside would have to inherit the scopiness of f. This might be the trickiest part of getting this right (though it is kinda neatly solved if scope is default :) )

a) A struct constructed with a scope variable itself must be scope, and thus all the rules apply to it.

b) Assigning to a struct which is not scope, even if it is a local variable, must not be permitted.

Holder h2;
h2.foo = f; // this isn't escaping the scope, but is dropping scope

Just as if we had a local variable of type int[].

We may make the struct scope:

scope Holder h2;
h2.foo = f; // OK

c) Calling methods on a struct which may escape the scope is wrong. Ideally, `this` would always be scope... in fact, I think that's the best way to go. An alternative though might be to restrict calling of non-pure functions. Pure functions don't allow mutation of non-scope data in the first place, so they shouldn't be able to escape references.




I think that covers what I want. Note that this is not necessarily @safe:

struct C_Array { /* grows with malloc */ scope T* borrow() {} }

C_Array!int i;
int* b = i.borrow;
i ~= 10; // might realloc...
// leaving b dangling


So it isn't necessarily @safe. I think it *would* be @safe with static arrays. BTW static array slicing should return scope as should most user defined containers. But with user-defined types, @safety is still in the hands of the programmer. Reallocing with a non-sealed reference should always be considered @trusted.


Stand by for my next post which will discuss making it default, with a few more points relevant to the whole concept.

Reply via email to