scope escaping
Adam D. Ruppe
destructionator at gmail.com
Thu Feb 6 07:47:43 PST 2014
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.
More information about the Digitalmars-d
mailing list