Checking Variable Lifetimes¶
As of Chapel 1.18, Chapel includes a compiler component called the lifetime checker. The lifetime checker produces errors at compile time to reveal potential memory errors. Note that the Chapel lifetime checker is not complete - that is, there are programs with memory errors that it will not detect. However, we hope that it offers a good balance between being easy to work with and catching common memory errors at compile-time. See also Checking for Nil Dereferences which discusses a related component that discovers nil dereferences at compile time.
Defining Scope and Lifetime¶
Scope¶
Variables in Chapel have a lexical scope within which it is legal to access the variable. For example:
module DemonstrateScopes {
proc function() {
var f: int;
// scope of `f` includes the body of this function
{
// and any nested blocks (including loops, conditionals, etc).
var x: int;
// `x` scope ends here, but `f` scope does not
}
// it would be an error to access `x` here
}
// it would be an error to access `f` here
{
var b: int;
// `b`s scope extends to the end of this block
}
// it would be an error to try to use `b` here
var g: int;
// `g` is a global variable and its scope extends
// to any code using this module.
}
A scope can be contained in another scope. For example, the scope of x
is contained within the scope of f
in the above example. In other
words, anywhere that x
can be accessed, f
can also be accessed.
In such a case we say that the scope of x
is smaller than the scope of
f
.
Lifetime¶
The lifetime of a variable indicates when that variable can be safely used.
Variables that cannot refer to another value, such as numeric variables, have a lifetime equal to their lexical scope.
Variables that can refer to other variables include
borrowed
class instances andref
variables. These variables get their lifetime from the lifetime of the variable that they refer to.
owned
andshared
variables have lifetime equal to their scope.
Note that owned
and shared
variables can be returned or assigned
without impacting their lifetime. The lifetime checker just checks that a
borrow
from such a variable does not outlive the variable itself.
module DemonstrateLifetimes {
proc function() {
var i: int;
// `i`s lifetime extends to the end of this block
ref r = i;
// `r` refers to `i`, so it's lifetime == `i`s lifetime
var own = new owned SomeClass();
// `own`s lifetime extends to the end of this block,
// (at which point the class instance may be deleted)
var borrow = own.borrow();
// `borrow`s lifetime extends to the end of this block
// because its lifetime matches `own`
}
var global: owned SomeClass;
proc settingGlobal() {
var x = new owned SomeClass();
// lifetime of x extends for entire function body
global = x; // transfers the instance from x to global
// leaving x storing `nil`
// lifetime of `x` extends to here, but an attempt
// to use `x` would result in an error from
// compile-time nil checking.
}
}
Similarly to scopes, lifetimes may be contained within each other. Ultimately, a lifetime is just the scope of some variable, and so we can say that one lifetime is smaller or larger than another, just as we can say that a scope is smaller or larger than another scope.
Example Errors¶
The lifetime checker is designed to catch errors such as:
returning a reference to or borrow from a function-local variable
assigning a value with a shorter lifetime to something with a larger scope
When the lifetime for a variable is smaller than its scope, that usually means that there is some point in the program where accessing that variable could lead to a memory error. There are some cases where the analysis indicates a memory error could occur, but a human programmer might know that it cannot for other reasons.
Returning a Reference to a Local Variable¶
// returnsref.chpl
proc refTo(ref x) ref {
return x;
}
proc returnsRefLocal() ref // note `ref` return intent
{
var i: int;
return refTo(i); // returns `i` by reference
// but `i` goes out of scope here
}
ref r = returnsRefLocal();
var val = r; // accesses invalid memory
returnsref.chpl:6: In function 'returnsRefLocal':
returnsref.chpl:9: error: Reference to scoped variable cannot be returned
returnsref.chpl:8: note: consider scope of i
Returning a Borrow From a Local Owned Instance¶
// returnsborrow.chpl
class SomeClass { var field: int; }
proc borrowLocal() {
var obj = new owned SomeClass;
return obj.borrow(); // returns borrow of `obj`
// but `obj` goes out of scope (and `delete`s the instance) here
}
var b = borrowLocal();
var y = b.field; // accesses deleted memory
returnsborrow.chpl:3: In function 'borrowLocal':
returnsborrow.chpl:5: error: Scoped variable cannot be returned
returnsborrow.chpl:4: note: consider scope of obj
Assigning a Borrow to something with Longer Scope¶
// assignsborrow.chpl
class SomeClass { }
{
var bor: borrowed SomeClass;
{
var obj = new owned SomeClass();
bor = obj.borrow(); // borrow of `obj` escapes
// but `obj` goes out of scope (and `delete`s the instance) here
}
writeln(bor); // uses freed memory
}
assignsborrow.chpl:8: error: Scoped variable bor would outlive the value it is set to
assignsborrow.chpl:7: note: consider scope of obj
Lifetime Inference¶
The lifetime checker starts by inferring the lifetime of each variable. It considers the ways that the variable is set:
if the variable is a reference to another variable, then its lifetime will be the scope of that variable
if a borrow is assigned or initialized from another variable, then its lifetime will be at most the lifetime of the other variable
if the variable is set by a function call, then the lifetime is inferred according to rules described below
Inference proceeds until the minimum inferred lifetime of each variable is established.
Inferred Lifetimes of Arguments¶
For methods, the this
argument is assumed to have longer lifetime than the
actual arguments and only the this
argument is assumed to have a lifetime
that can be returned.
For non-methods, all formals are considered to have a lifetime that can be returned.
Inferred Lifetime of Function Call Results¶
For x = f(a, b, c)
, the lifetime of x
is inferred to be the
minimum lifetime of the arguments a
, b
, c
that have lifetimes
that could be returned.
For a method call, such as y = receiver.f(a, b, c)
, the lifetime will
be inferred to be the lifetime of receiver
.
If these inferred lifetimes are not appropriate for a function, the lifetimes can be specified with a lifetime annotation.
Lifetime Annotations¶
Certain functions need to override the default lifetime inference rules.
This can be accomplished by placing a lifetime
clause after the
return type. These lifetime
clauses share some similarities with
where
clauses. For example:
class C { var x: int; }
var globalOwned = new owned C(1);
var globalBorrow = globalOwned.borrow();
// Default lifetime inference assumes that the
// returned lifetime is the lifetime of arg,
// but that's not appropriate here.
//
// The lifetime annotation indicates that the returned value
// has the lifetime of globalBorrow.
proc returnsGlobalBorrow(arg: borrowed C)
lifetime return globalBorrow
{
return globalBorrow;
}
Other functions need to assert a relationship between the lifetimes of their arguments. This pattern comes up with functions that append some data to a data structure.
record Collection {
type elementType;
var element: elementType;
}
// Without lifetime annotation, the compiler will raise an error,
// because `this` is assumed to have larger lifetime than `arg`,
// and so the assignment will set something with a longer lifetime
// to something with a shorter lifetime.
//
// The lifetime clause `lifetime this < arg` avoids that error
// by informing the compiler that `this` (and by extension, `this.element`)
// need to have lifetime no longer than `arg`.
proc Collection.addElement(arg: elementType)
lifetime this < arg
{
this.element = arg;
}
Note that the lifetime clause needs to be written in terms of formal
arguments, including this
for methods, and possible outer variables.
In particular, in the above, the constraint is between this
and
arg
rather than this.element
and arg
. this.element
will
have its lifetime inferred to be the lifetime of this
, so these are
equivalent.
In some cases, it is more natural to write the lifetime annotation in terms of what assignments the function may make. For example:
proc myswap(ref lhs: borrowed MyClass, ref rhs: borrowed MyClass)
lifetime lhs=rhs, rhs=lhs
{
var tmp = lhs;
lhs = rhs;
rhs = tmp;
}
Here the lifetime checker ensures that the lifetimes of the actual
arguments are suitable for performing the assignments between formals
that are indicated in the lifetime clause lifetime lhs=rhs, rhs=lhs
.