New Issue: Adding support for context managers

17592, "dlongnecke-cray", "Adding support for context managers", "2021-04-23T06:28:40Z"

This issue proposes to add context managers to Chapel, and suggests a potential implementation that can be used as a starting point for discussion.

Introducing the context manager

A context manager is a syntactic construct that makes a series of actions occur at the beginning and end of a block. It can be used to:

  • Control access to a resource (e.g. a critical section)
  • Create and clean up a resource (e.g. a file)
  • Introduce scope-bound state changes (e.g. context sensitive parsing)

The context manager from Python looks a bit like this:


# If our context manager does not return a resource, or we do not care about what it returns,
# we can drop the resource instead of binding it to a name.
with createAContext():
  do_something()

# Many times, the manager will return a resource. In that case, we can bind the resource to
# a name so that we can use it within the scope of the managed block.
with createAContext() as handleToResource:
  handleToResource.do_something()

A first-pass translation of this construct to Chapel might look like:

manage createAContext() {
  do_something();
}

manage createAContext() as handleToResource {
  handleToResource.do_something();
}

The keyword manage was chosen to avoid confusing context managers with task and forall intents. If we want to avoid adding new keywords to the language, we could use:

with createAContext() {
  do_something();
}

with createAContext() as handleToResource {
  handleToResource.do_something();
}

As an alternative, but I worry about potential confusion (though to the parser, the syntax is quite clear, as managers will likely only appear at the statement level).

Requirements for a type to be used as a context manager

To be used as a context manager, a type must define two methods. The first method is called upon entry to the managed block. It may or may not return a value. The second method is called upon exiting the managed block. It does not return a value.

class MyContextManager(object):
  # In Python, the magic method __enter__ is called on entry to a block.
  def __enter__(self):
    return 1
  # The magic method __exit__ is called upon exiting a block.
  def __exit__(self):
    pass

A first-pass translation of these methods to Chapel might look like:

record myContextManager {
  proc enterThis() { return 1; }

  proc leaveThis() {}

The names enterThis and leaveThis were chosen due to symmetry and because other magic methods tend to append "this" such as readThis, writeThis, and readWriteThis.

Alternatively, the names from Python could be used instead:

record myContextManager {
  proc enterThis() { return 1; }

  proc exitThis() {}

What does the desugared form of the context manager look like?

Regardless of the chosen syntax, the desugared form of the context manager will probably end up looking like:

manage new myContextManager() as x {
  x.doSomething();
}

Desugars to...

{
  var x = (new myContextManager()).enterThis();
  {
    x.doSomething();
  }
  tmp.leaveThis();
}

Dealing with overloaded return intents for enterThis()

In Chapel, return intents are considered when selecting which overload of a function to call.

Consider the following:


record storeNumber {
  var x = 0;
  proc enterThis() { return x; }
  proc enterThis() ref { return x; }
  proc leaveThis() {}
}

var myManager = new storeNumber();
manage myManager as number {
  number = 5;
}
writeln(myManager.x); // What does this print?
  • What is the variable declaration type of number? Is it var or ref? Is it const?
  • Which overload of enterThis() is called?

One option is to require the user to specify the variable declaration type of the managed resource.

If we wanted the ref overload of enterThis() to be preferred, we could declare number as ref:

var myManager = new storeNumber();
manage myManager as ref number {
  number = 5;
}
writeln(myManager.x); // Prints '5'

And if we wanted the var overload of enterThis() to be preferred, we could declare number as var:

var myManager = new storeNumber();
manage myManager as var number {
  number = 5;
}
writeln(myManager.x); // Prints '0'

And we could have the var option be a reasonable default (or ref):

var myManager = new storeNumber();
manage myManager as number {
  number = 5;
}
writeln(myManager.x); // Prints '0'

Another option is to disallow overloading the return intent of enterThis(). This way, the variable declaration type of number is obvious, because there is nothing to disambiguate.


record storeNumber {
  var x = 0;
  proc enterThis() { return x; }
  proc enterThis() ref { return x; }
  proc leaveThis() {}
}

var myManager = new storeNumber();

// Error: call to 'proc storeNumber.enterThis()' is ambiguous due to multiple return intents
manage myManager as number {
  number = 5;
}
writeln(myManager.x);

record storeNumber {
  var x = 0;
  proc enterThis() ref { return x; }
  proc leaveThis() {}
}

var myManager = new storeNumber();

// Here `number` will be `ref`
manage myManager as number {
  number = 5;
}
writeln(myManager.x); // OK, prints '5'

Alternative syntax that could support overloaded return intents for enterThis()

Here is an alternative syntax that would also allow disambiguating calls to enterThis():

record storeNumber {
  var x = 0;
  proc enterThis() { return x; }
  proc enterThis() ref { return x; }
  proc leaveThis() {}
}

var myManager = new storeNumber();

manage ref number = myManager {
  number = 5;
}
writeln(myManager.x);

This could work, however I worry that ref number = myManager is indistinguishable from assignment, when in fact, its meaning is ref number = (myManager).enterThis().