Difference between revisions of "User:Schuetzm/scope2"

From D Wiki
Jump to: navigation, search
(Scope inference)
(scope inference)
 
(20 intermediate revisions by the same user not shown)
Line 1: Line 1:
== Overview ==
+
== Introduction ==
  
<code>scope</code> is a storage class. It applies to function parameters (including <code>this</code>), local variables, the return value (treated as if it were an <code>out</code> parameter), and member variables of aggregates. It participates in overloading.
+
The current D language specification reserves the '''scope''' keyword in function signatures to specify that a parameter will not be escaped by the function, making it @safe to pass references to local variables or manually managed memory to it, among other things. This feature is currently unimplemented, apart from its use with lambdas where it guarantees the closure will be allocated on the stack instead of the GC. This proposal intends to change that. It will allow the safe and efficient implementation of various memory management strategies (including reference counting), as well as unified handling of references to GC, reference counted data, local variables, containers, and others.
  
With every variable, a particular lifetime (called '''scope''') is associated.
+
The proposal is mostly a superset of [[DIP25]], but is generalized to all types of references and adds inference to alleviate the need for explicit annotations. Credits are due to the authors of that DIP, ''Andrei'' and ''Walter'', then to ''Zach the Mystic'' who had the idea to generalize DIP25 as well as provided inspiration for the inference algorithm, ''deadalnix'' for his many valuable arguments, for example pointing out the intricacies of handling multiple indirections safely, and various other members of the community who provided useful contributions in past discussions in the news groups.
It usually refers to a local variable or parameter; additionally, an infinite scope is defined that corresponds to global/static variables or GC managed memory.
 
Scopes and lifetimes are defined purely based on lexical scope and order of declaration of their corresponding variables. Therefore, for any two lifetimes, one is either completely contained in the other, or they are disjoint.
 
By annotating a variable with <code>scope</code>, it's scope is defined to be equal to the variable's lifetime, instead of the default (infinity).
 
  
For any expression involving at least one scope value, two lifetimes (''LHS lifetime'' and ''RHS lifetime'') are computed in a way that ensures that the resulting RHS lifetime will not be greater than that of any of the expression's parts, and the LHS lifetime will not be smaller.
+
== Overview ==
The exact rules will be defined in one of the following sections.
 
An '''assignment''' (i.e., <code>=</code> operator, passing to a function, returning from a function, capturing in a closure, throwing, etc.) involving scope values is only permissible, if the destination's LHS lifetime is fully contained in the source's RHS lifetime.
 
Throwing is considered assignment to a variable with static lifetime.
 
  
The following invariant is enforced to always be true on every assignment: A location with scope ''a'' will never contain references pointing to values with a lifetime shorter than ''a''.
+
=== Basics ===
  
To allow a function to return a value it received as a parameter, the parameter can be annotated with the <code>return</code> keyword,
+
'''scope''' is a storage class; it will only be applicable to parameters in function signatures (which include the implicit '''this''' parameter for methods, as well as the context pointer for delegates). It will have the semantics one expects: when a function with a scope parameter returns, the corresponding argument will not have been stored in a global variable or on the heap, etc:
as in [[DIP25]].
 
It's also possible to express that a parameter escapes through another parameter (including <code>this</code>) by using the <code>return!identifier</code> syntax.
 
Multiple such annotations can appear for each parameter.
 
When a function is called, the compiler checks (for each argument and the return value) that only expressions with a lifetime longer than those of all the corresponding <code>return</code> annotations are passed in, and that the return value is used in a conforming way.
 
  
Because all relevant information about lifetimes is contained in the function signature, no explicit <code>scope</code> annotations for local variables
+
<source lang="D">
are necessary; the compiler can figure them out by itself.
+
void sendData(scope ubyte[] data);
Additionally, inference of annotations is done in the usual situations, i.e. nested functions and templates.
+
void someOtherFunction(ubyte[] data);
In a subsequent section, an algorithm is presented that can be used for inference of scopes of local variables as well as annotations of parameters.
 
  
In parameters of <code>@safe</code> functions, all reference types (class references, pointers, slices, <code>ref</code> parameters) or aggregates containing references are implicitly treated as <code>scope</code> unless the parameter is annotated with the keyword <code>static</code>. This does however not apply to the return value.
+
void main() {
 +
    ubyte[1024] chunkOfData = ...;
 +
    sendData(chunkOfData);
 +
    // this is @safe: no reference to the local has escaped
 +
    someOtherFunction(chunkOfData);
 +
    // this is @system: the callee gives no guarantees about the param
 +
}
 +
</source>
  
<code>ref</code> and <code>out</code> parameters can also be treated as implicitly scoped, but this has the potential to break lots of code and needs to be considered carefully.
+
As we can see, certain operations, like taking the address of a local (or slicing of a fixed-size array, which is equivalent), no longer need to be @system per se. Instead, it's what is done with the resulting reference that decides whether it's @system or @safe.
  
A <code>scope</code> annotation on a member variable shall be equivalent to a <code>@property</code> function returning a reference to that member, scoped to <code>this</code>.
+
=== Implicit '''scope''' and opt-out ===
  
'''Borrowing''' is the term used for assignment of a value to a variable with narrower scope. This typically happens on function calls, when a value is passed as an argument to a parameter annotated with (or inferred as) <code>scope</code>.
+
To reduce the need for manual annotations, @safe functions take all their reference parameters as '''scope'''. '''ref''' implies '''scope''' even in @system functions. Because sometimes a @safe function may actually want to accept non-scope params, there is an opt-out in the form of '''static'''. Coupled with scope inference for templates, and an optional change like "@safe by default" (currently being discussed), this will get rid of most explicit scope annotations:
  
== Implementation ==
+
<source lang="D">
 +
void doSomething(int[] data) @safe;
 +
// equivalent to:
 +
void doSomething(scope int[] data) @safe;
  
=== Scope inference ===
+
void foo(int[] input, static int* output) @safe;
 +
// `input` is scope, `output` isn't
  
This algorithm works at the function level. Scope inference for variables and parameters in one function is independent from all other functions.
+
void bar(ref MyStruct s) @safe;
 +
// equivalent to:
 +
void bar(scope ref MyStruct s) @safe;
 +
</source>
  
It takes as input a list of variables whose scope is already fixed (by explicit annotations) and another list whose scopes are to be inferred. It will choose the narrowest possible scopes for them for which the function will still compile. This is based on the observation that variables that are read from need to have a scope at least as large as their destination. Therefore, we can start with the smallest possible scope <code>[]</code> and extend it to the scope of the destination, if it isn't already larger.
+
=== '''scope''' for value types & overloading ===
  
----
+
'''scope''' applies to all types with indirections: pointers, slices, class references, '''ref''' parameters, delegates, and aggregates containing such. Functions can be overloaded on '''scope'''. This allows efficient passing of RC wrappers for instance:
  
1. Let <code>Q</code> be a list of all variables whose scopes are to be inferred. This includes template function parameters not otherwise annotated and all local variables.
 
 
2. Assign all elements of <code>Q</code> an initial scope of <code>[]</code>:
 
 
<source lang="D">
 
<source lang="D">
     foreach(var; Q) {
+
struct RC(T) if(is(T == class)) {
         var.scope := [];
+
    // ...
 +
     this(this) static {
 +
        // increment refcount
 +
        count++;
 +
    }
 +
    ~this() static {
 +
         // decrement refcount
 +
        if(--count == 0)
 +
            destroy(payload);
 +
    }
 +
    this(this) scope {
 +
        // DON'T increment refcount
 +
    }
 +
    ~this() scope {
 +
        // DON'T decrement refcount
 
     }
 
     }
 +
    // magic, to be explained later
 +
    alias borrow this;
 +
}
 +
 +
void foo(scope MyClass object);
 +
 +
RC!MyClass global;
 +
void bar(scope RC!MyClass object) {
 +
    if(some_condition)
 +
        global = object; // make a copy, adjust refcount
 +
}
 +
 +
void main() {
 +
    RC!MyClass x = ...;
 +
    // auto conversion to MyClass, no refcount update:
 +
    foo(x);
 +
    // no refcount update at call site,
 +
    // no needless double indirection with `ref`:
 +
    bar(x);
 +
}
 
</source>
 
</source>
  
3. For each variable in <code>Q</code> that is accessed at least once, set its lifetime to itself:
+
All of this can be implemented in user code or in the standard library. The compiler doesn't need to be aware of reference counting.
 +
 
 +
The rules for overloading are:
 +
* If only an overload accepting '''scope''' is defined, it is selected.
 +
* If only an overload accepting '''static''' (the default) is defined, it can only be called if the argument also has static scope.
 +
* If both overloads are defined, the static one is called for arguments with static scope, and the scope one for all others.
 +
 
 +
Because scope is inferred for templates, we must explicitly specify '''static''' and '''scope''' if we want to overload on them.
 +
 
 +
=== Implicit conversions ===
 +
 
 +
A '''scope''' parameter doesn't care how the data it refers to has been allocated. All it requires is that the reference stays valid for the duration of the function call. Therefore, it's a perfect fit for library functions. They don't need to be templated to support different resource management strategies of the library's user. It acts as a bridge between different types of strategies, just like '''const''' acts as a bridge between mutable and immutable data.
 +
 
 
<source lang="D">
 
<source lang="D">
    foreach(var; Q) {
+
// no template bloat, no knowledge about RC etc.:
        if(variable_is_accessed(var))
+
double computeAverage(scope int[] data);
            var.scope := [var];
+
 
     }
+
void main() {
 +
    int[20] local = [1,2,3,...];
 +
    writeln(computeAverage(local));    // OK
 +
    int[] heap = ...;
 +
    writeln(computeAverage(heap));    // OK
 +
    RC!(int[]) rc = ...;
 +
     writeln(computeAverage(rc));      // OK
 +
}
 
</source>
 
</source>
  
4. For each <code>ASSIGNMENT</code> whose <code>RHS_SCOPE</code> depends on a variable in <code>Q</code>, expand that variable's scope to at least the <code>LHS_SCOPE</code>. For all variables the <code>LHS_SCOPE</code> depends on and that are in <code>Q</code>, record a dependency:
+
This is achieved by allowing non-scoped types to convert to '''scope''' implicitly. For builtin references, the language does this automatically. User-defined types must opt in by defining an appropriate '''alias this'''.
<source lang="D">
 
    foreach(ass; ASSIGNMENTS) {
 
        if(ass.rhs_scope.depends_on(Q)) {
 
            foreach(rhs_var; ass.rhs_scope.vars) {
 
                if(not rhs_var in Q)
 
                    continue;
 
                foreach(lhs_var; ass.lhs_scope.vars) {
 
                    rhs_var.scope |= ass.lhs_scope;
 
                    if(lhs_var in Q)
 
                        rhs_var.deps ~= lhs_var;
 
                }
 
            }
 
        }
 
    }
 
</source>
 
  
5. Remove all variables from <code>Q</code> that have no dependencies:
+
=== Returning scoped parameters ===
<source lang="D">
 
    foreach(var; Q) {
 
        if(var.deps.empty)
 
            Q.remove(var);
 
    }
 
</source>
 
  
6. If <code>Q</code> is empty, terminate, else remember length of <code>Q</code>:
+
Some functions want to return a parameter that is passed in, or something reachable through one, e.g. a member of '''this'''. They can express this by annotating the parameter with the keyword '''return''', just as in [[DIP25]]:
<source lang="D">
 
    if(Q.empty)
 
        return;
 
    old_Q_len := Q.length;
 
</source>
 
  
7. Expand all variables' scopes to at least that of their dependencies; if a dependency has no dependencies itself, remove it from the variable's dependencies:
 
 
<source lang="D">
 
<source lang="D">
    foreach(var; Q) {
+
struct RC(T) if(is(T == class)) {
        foreach(dep; var.deps) {
+
    scope T payload;
            var.scope |= dep.scope;
+
    T borrow() return {    // `return` applies to `this`
            if(dep.deps.empty)
+
        return payload;
                var.deps.remove(dep);
 
        }
 
 
     }
 
     }
 +
}
 
</source>
 
</source>
  
8. If the length changed, we made progress. We can repeat from step 5. Otherwise we have a dependency loop. Find a cycle (for example using [//en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm Tarjan's algorithm]). Collect all elements in the cycle, remove their dependencies from <code>DEPENDENCIES</code>, and assign them all the union of their scopes:
+
To specify that the value is returned through another parameter, the '''return!ident''' syntax can be used. If necessary, these annotations can be used multiple times per parameter, when the reference can be returned through several other parameters:
 +
 
 
<source lang="D">
 
<source lang="D">
    if(Q.length != old_Q_len)
+
int* foo(
        goto step5;
+
     scope int* input return return!output return!output2,
     cycle := tarjan(DEPENDENCIES);
+
     int** output,
     new_scope := []
+
     out int* output2
    foreach(var; cycle) {
+
);
        new_scope |= var.scope;
 
        var.deps.remove_each(cycle);
 
     }
 
    foreach(var; cycle) {
 
        var.scope := new_scope;
 
    }
 
 
</source>
 
</source>
  
9. Go to step 5.
+
To prevent accidental non-scoped access to a member (e.g. ''payload'' in the above example), the member can be annotated with '''scope'''. The compiler will then treat it as if it were always accessed through an appropriately annotated property that returns a (scoped) reference to it.
 
 
----
 
 
 
At this point, each variable will have a scope assigned. Now, all assignment can be checked to verify that they never place a reference in a location that outlives the reference's target.
 
  
== Examples ==
+
The compiler will make sure the returned value is not used in any way that is un-@safe. In particular, it will verify that the returned references' lifetimes won't exceed the lifetimes of the arguments they're coming from.
  
=== RCArray ===
+
=== '''scope''' inference ===
  
Walter's <code>RCArray</code>, adjusted to this proposal:
+
For templates and nested functions, the compiler will infer the scope annotations, just as it infers purity and @safe-ty. Generic code therefore rarely needs any explicit annotations:
  
 
<source lang="D">
 
<source lang="D">
@safe:
+
T* foo(T)(T* a, T* b) {
 +
    static T* cache;
 +
    cache = b;
 +
    return a;
 +
}
  
struct RCArray(E) {
+
// `foo!int` will be inferred as:
    this(E[] a)
+
int* foo_int(scope int* a return, static int* b);
    {
+
// (`static` is the default anyway, only here for clarity)
        array = a.dup;
+
</source>
        count = new int;
 
        *count = 1;
 
    }
 
  
    ~this() @trusted
+
=== Multiple indirections ===
    {
 
        if (count && --*count == 0)
 
        {
 
            // either `delete` in `@system` code will accept `scope`:
 
            delete array;
 
            // or a helper needs to be used to remove `scope`:
 
            delete assumeStatic(array);
 
        }
 
    }
 
  
    this(this)
+
Multiple indirections are also handled in a way that preserves the guarantees about lifetimes. Because '''scope''' is not a type modifier, it cannot encode information about the lifetimes of objects behind more than one indirection. Therefore, the compiler must be conservative. For the left-hand side of assignments, it must assume that the destination has infinite lifetime, while for the right-hand side, it must assume that the destination will vanish as soon as the reference through which it is accessed goes out of scope.
    {
 
        if (count)
 
            ++*count;
 
    }
 
  
    @property size_t length()
+
=== @safe-ty violations with borrowing ===
    {
 
        return array.length;
 
    }
 
  
    ref E opIndex(size_t i)
+
When borrowing is combined with explicit, non lexical-scope based memory management (of which reference counting is one form), there will inevitably be problems as the one discussed in [http://forum.dlang.org/post/huspgmeupgobjubtsmfe@forum.dlang.org this forum thread]. To deal with them in a safe way requires some kind of data flow and aliasing analysis. Rust is an example of a language that uses very sophisticated analysis algorithms for that. This proposal will include a simplified algorithm to detect potentially unsafe uses at compile time, at the cost of detecting some false positives, for which there will however be workarounds. Instead of disallowing these operations, they will be treated as @system. It is therefore up to the end user to decide how to deal with them: they can make their code @trusted if they verify that it is indeed safe, but the compiler just can't know it, or they can rewrite it in a way that allows the compiler to proof the safety.
    {
 
        return array[i];
 
    }
 
  
    E[] opSlice(size_t lwr, size_t upr)
+
The operations that are potentially unsafe are:
    {
+
* borrowing from a mutable global variable
        return array[lwr .. upr];
+
: Global variables can be accessed and therefore be mutated from anywhere.
    }
+
* re-borrowing from a mutable variable to which another borrowed reference is currently accessible
 
+
: A @safe function can then assume that its parameters don't alias in a dangerous way.
    E[] opSlice()
 
    {
 
        return array[];
 
    }
 
 
 
private:
 
    scope E[] array;    // this is the only explicit annotation
 
    int* count;
 
}
 
</source>
 

Latest revision as of 13:19, 25 March 2015

Introduction

The current D language specification reserves the scope keyword in function signatures to specify that a parameter will not be escaped by the function, making it @safe to pass references to local variables or manually managed memory to it, among other things. This feature is currently unimplemented, apart from its use with lambdas where it guarantees the closure will be allocated on the stack instead of the GC. This proposal intends to change that. It will allow the safe and efficient implementation of various memory management strategies (including reference counting), as well as unified handling of references to GC, reference counted data, local variables, containers, and others.

The proposal is mostly a superset of DIP25, but is generalized to all types of references and adds inference to alleviate the need for explicit annotations. Credits are due to the authors of that DIP, Andrei and Walter, then to Zach the Mystic who had the idea to generalize DIP25 as well as provided inspiration for the inference algorithm, deadalnix for his many valuable arguments, for example pointing out the intricacies of handling multiple indirections safely, and various other members of the community who provided useful contributions in past discussions in the news groups.

Overview

Basics

scope is a storage class; it will only be applicable to parameters in function signatures (which include the implicit this parameter for methods, as well as the context pointer for delegates). It will have the semantics one expects: when a function with a scope parameter returns, the corresponding argument will not have been stored in a global variable or on the heap, etc:

void sendData(scope ubyte[] data);
void someOtherFunction(ubyte[] data);

void main() {
    ubyte[1024] chunkOfData = ...;
    sendData(chunkOfData);
    // this is @safe: no reference to the local has escaped
    someOtherFunction(chunkOfData);
    // this is @system: the callee gives no guarantees about the param
}

As we can see, certain operations, like taking the address of a local (or slicing of a fixed-size array, which is equivalent), no longer need to be @system per se. Instead, it's what is done with the resulting reference that decides whether it's @system or @safe.

Implicit scope and opt-out

To reduce the need for manual annotations, @safe functions take all their reference parameters as scope. ref implies scope even in @system functions. Because sometimes a @safe function may actually want to accept non-scope params, there is an opt-out in the form of static. Coupled with scope inference for templates, and an optional change like "@safe by default" (currently being discussed), this will get rid of most explicit scope annotations:

void doSomething(int[] data) @safe;
// equivalent to:
void doSomething(scope int[] data) @safe;

void foo(int[] input, static int* output) @safe;
// `input` is scope, `output` isn't

void bar(ref MyStruct s) @safe;
// equivalent to:
void bar(scope ref MyStruct s) @safe;

scope for value types & overloading

scope applies to all types with indirections: pointers, slices, class references, ref parameters, delegates, and aggregates containing such. Functions can be overloaded on scope. This allows efficient passing of RC wrappers for instance:

struct RC(T) if(is(T == class)) {
    // ...
    this(this) static {
        // increment refcount
        count++;
    }
    ~this() static {
        // decrement refcount
        if(--count == 0)
            destroy(payload);
    }
    this(this) scope {
        // DON'T increment refcount
    }
    ~this() scope {
        // DON'T decrement refcount
    }
    // magic, to be explained later
    alias borrow this;
}

void foo(scope MyClass object);

RC!MyClass global;
void bar(scope RC!MyClass object) {
    if(some_condition)
        global = object; // make a copy, adjust refcount
}

void main() {
    RC!MyClass x = ...;
    // auto conversion to MyClass, no refcount update:
    foo(x);
    // no refcount update at call site,
    // no needless double indirection with `ref`:
    bar(x);
}

All of this can be implemented in user code or in the standard library. The compiler doesn't need to be aware of reference counting.

The rules for overloading are:

  • If only an overload accepting scope is defined, it is selected.
  • If only an overload accepting static (the default) is defined, it can only be called if the argument also has static scope.
  • If both overloads are defined, the static one is called for arguments with static scope, and the scope one for all others.

Because scope is inferred for templates, we must explicitly specify static and scope if we want to overload on them.

Implicit conversions

A scope parameter doesn't care how the data it refers to has been allocated. All it requires is that the reference stays valid for the duration of the function call. Therefore, it's a perfect fit for library functions. They don't need to be templated to support different resource management strategies of the library's user. It acts as a bridge between different types of strategies, just like const acts as a bridge between mutable and immutable data.

// no template bloat, no knowledge about RC etc.:
double computeAverage(scope int[] data);

void main() {
    int[20] local = [1,2,3,...];
    writeln(computeAverage(local));    // OK
    int[] heap = ...;
    writeln(computeAverage(heap));     // OK
    RC!(int[]) rc = ...;
    writeln(computeAverage(rc));       // OK
}

This is achieved by allowing non-scoped types to convert to scope implicitly. For builtin references, the language does this automatically. User-defined types must opt in by defining an appropriate alias this.

Returning scoped parameters

Some functions want to return a parameter that is passed in, or something reachable through one, e.g. a member of this. They can express this by annotating the parameter with the keyword return, just as in DIP25:

struct RC(T) if(is(T == class)) {
    scope T payload;
    T borrow() return {    // `return` applies to `this`
        return payload;
    }
}

To specify that the value is returned through another parameter, the return!ident syntax can be used. If necessary, these annotations can be used multiple times per parameter, when the reference can be returned through several other parameters:

int* foo(
    scope int* input return return!output return!output2,
    int** output,
    out int* output2
);

To prevent accidental non-scoped access to a member (e.g. payload in the above example), the member can be annotated with scope. The compiler will then treat it as if it were always accessed through an appropriately annotated property that returns a (scoped) reference to it.

The compiler will make sure the returned value is not used in any way that is un-@safe. In particular, it will verify that the returned references' lifetimes won't exceed the lifetimes of the arguments they're coming from.

scope inference

For templates and nested functions, the compiler will infer the scope annotations, just as it infers purity and @safe-ty. Generic code therefore rarely needs any explicit annotations:

T* foo(T)(T* a, T* b) {
    static T* cache;
    cache = b;
    return a;
}

// `foo!int` will be inferred as:
int* foo_int(scope int* a return, static int* b);
// (`static` is the default anyway, only here for clarity)

Multiple indirections

Multiple indirections are also handled in a way that preserves the guarantees about lifetimes. Because scope is not a type modifier, it cannot encode information about the lifetimes of objects behind more than one indirection. Therefore, the compiler must be conservative. For the left-hand side of assignments, it must assume that the destination has infinite lifetime, while for the right-hand side, it must assume that the destination will vanish as soon as the reference through which it is accessed goes out of scope.

@safe-ty violations with borrowing

When borrowing is combined with explicit, non lexical-scope based memory management (of which reference counting is one form), there will inevitably be problems as the one discussed in this forum thread. To deal with them in a safe way requires some kind of data flow and aliasing analysis. Rust is an example of a language that uses very sophisticated analysis algorithms for that. This proposal will include a simplified algorithm to detect potentially unsafe uses at compile time, at the cost of detecting some false positives, for which there will however be workarounds. Instead of disallowing these operations, they will be treated as @system. It is therefore up to the end user to decide how to deal with them: they can make their code @trusted if they verify that it is indeed safe, but the compiler just can't know it, or they can rewrite it in a way that allows the compiler to proof the safety.

The operations that are potentially unsafe are:

  • borrowing from a mutable global variable
Global variables can be accessed and therefore be mutated from anywhere.
  • re-borrowing from a mutable variable to which another borrowed reference is currently accessible
A @safe function can then assume that its parameters don't alias in a dangerous way.