Scope and Ref and Borrowing

Walter Bright via Digitalmars-d digitalmars-d at puremagic.com
Thu Nov 13 17:20:30 PST 2014


Thought I'd bring this up as deadalnix is working on a related proposal. It uses 
'scope' in conjunction with 'ref' to resolve some long standing @safe issues.
---------------------------------------

**Background

The goal of @safe code is that it is guaranteed to be memory safe. This is mostly
achieved, but there's a gaping hole - returning pointers to stack objects when those
objects are out of scope. This is memory corruption.

The simple cases of this are disallowed:

   T* func(T t) {
     T u;
     return &t; // Error: escaping reference to local t
     return &u; // Error: escaping reference to local u
   }

But are is easily circumvented:

   T* func(T t) {
     T* p = &t;
     return p;  // no error detected
   }

@safe deals with this by preventing taking the address of a local:

   T* func(T t) @safe {
     T* p = &t; // Error: cannot take address of parameter t in @safe function func
     return p;
   }

But this is awfully restrictive. So the 'ref' storage class was introduced which
defines a special purpose pointer. 'ref' can only appear in certain contexts,
in particular function parameters and returns, only applies to declarations,
cannot be stored, and cannot be incremented.

   ref T func(T t) @safe {
     return t; // Error: escaping reference to local variable t
   }

Ref can be passed down to functions:

   void func(ref T t) @safe;
   void bar(ref T t) @safe {
      func(t); // ok
   }

But the following idiom is far too useful to be disallowed:

   ref T func(ref T t) @safe {
     return t; // ok
   }

And if it is misused it can result in stack corruption:

   ref T foo() @safe {
     T t;
     return func(t); // no error detected, despite returning pointer to t
   }

The purpose of this proposal is to detect these cases at compile time and 
disallow them.
Memory safety is achieved by allowing pointers to stack objects be passed down 
the stack,
but those pointers may not be saved into non-stack objects or stack objects 
higher on the stack,
and may not be passed up the
stack past where they are allocated.

The:

     return func(t);

case is detected by all of the following conditions being true:

1. foo() returns by reference
2. func() returns by reference
3. func() has one or more parameters that are by reference
4. 1 or more of the arguments to those parameters are stack objects local to foo()
5. Those arguments can be @safe-ly converted from the parameter to the return type.
    For example, if the return type is larger than the parameter type, the 
return type
    cannot be a reference to the argument. If the return type is a pointer, and the
    parameter type is a size_t, it cannot be a reference to the argument. The larger
    a list of these cases can be made, the more code will pass @safe checks 
without requiring
    further annotation.

**Scope Ref

The above solution is correct, but a bit restrictive. After all, func(t, u) 
could be returning
a reference to non-local u, not local t, and so should work. To fix this, 
introduce the concept
of 'scope ref':

     ref T func(scope ref T t, T u) @safe {
       return t; // Error: escaping scope ref t
       return u; // ok
     }

Scope means that the ref is guaranteed not to escape.

   T u;
   ref T foo() @safe {
     T t;
     return func(t, u); // ok, u is not local
     return func(u, t); // Error: escaping scope ref t
   }

This scheme minimizes the number of 'scope' annotations required.

**Out Parameters

'out' parameters are treated like 'ref' parameters for the purposes of this 
document.

**Inference

Many functions can infer pure, @safe, and @nogc. Those same functions can infer
which ref parameters are 'scope', without needing user annotation.

**Mangling

Scope will require additional name mangling, as it affects the interface of the 
function.

**Nested Functions

Nested functions have more objects available than just their arguments:

   ref T foo() @safe {
     T t;
     ref T func() { return t; }
     return func();  // should be disallowed
   }

On the plus side the body of the nested function is available to the compiler for
examination.

**Delegates and Closures

This one is a little harder; but the compiler can detect that t is taken by 
reference,
and then can assume that dg() is returning t by reference and disallow it.

   ref T foo() @safe {
     T t;
     ref T func() { return t; }
     auto dg = &func;
     return dg();  // should be disallowed
   }


**Overloading

Scope does not affect overloading, i.e.:

    T func(scope ref a);
    T func(ref T b);

are considered the same as far as overloading goes.

**Inheritance

Overriding functions inherit any 'scope' annotations from their antecedents.

**Limitations

Arrays of references are not allowed.
Struct and class fields that are references are not allowed.
Non-parameter variables cannot be references.


More information about the Digitalmars-d mailing list