Skip to content

Instantly share code, notes, and snippets.

@vasslitvinov
Last active June 4, 2020 20:06
Show Gist options
  • Save vasslitvinov/af2af39239cba9dce0efd3780eadd7be to your computer and use it in GitHub Desktop.
Save vasslitvinov/af2af39239cba9dce0efd3780eadd7be to your computer and use it in GitHub Desktop.
Converting POI examples to CG

Update 2020-06-04: replaced with this comment on #6252.

This gist investigates the examples presented here. In particular, how can they be converted to Constrained Generics (CG)? What can we learn from them about POI?

test/functions/generic/poi/canresolve-in-sort.chpl

canResolve and similar Reflection functions are more likely to be affected by #6252. However, that is an artifact of how they are implemented. A different implementation - ex. similar to compilerError() - can remove reliance of canResolve on POI. Which would have no user-observable impact, except for restored correctness in corner cases.

test/functions/generic/poi/forwarding-wrapper.chpl

Similar scenario: test/functions/generic/poi/init-in-private-array.chpl and test/functions/generic/poi/zmq-initequals.chpl The test forwarding-wrapper.chpl adds forwarding to the call chain that POI needs to consult. The following discusses the scenario without forwarding.

Consider this simple example from #10358 :

record R {}
var bag = new DistBag(R);  // in DistributedBag
bag.add(new R());
a highlight of DistributedBag
module DistributedBag {
  record DistBag {
    type eltType;
    var seg = new BagSegmentBlock(eltType); // a simplification
    proc add(elt: eltType) {
      this.seg.push(elt);
    }
  }
  class BagSegmentBlock {
    type eltType;
    var elems: eltType; // a simplification
    proc push(elt: eltType) {
      elems = elt;      // invokes R.init= or assignment
    }
  }
}

The library implementation of DistBag.add() needs assignment between R records. Collection libraries in general may also need other predefined methods on their elements, such as init, init=, and deinit. These methods for a user-defined record will be defined in the application code and so will not be immediately visible to the library.

The design question here is: do we treat these special methods the same as any other user-defined function, or do we do something different?

For example, every record must have a deinit, and in most cases it is easily discoverable by the compiler from the record type. It looks superfluous to have the library writer discuss it in a constraint.

The initializers and the assignment are less clear-cut, as some of them can be missing or overloaded for different argument types. For example, does the collection require a default initializer? If no, is a call to a default initializer within the collection implementation an error? One option is for the user to specify this in a constraint; another is for the compiler to infer it -- which is closer to how unconstrained generics work.

test/functions/generic/poi/writethis-user-record.chpl

In a sentence, the implementation ofwriteThis or similar for a user record needs to be accessed by the library implementation of writeln().

The twist here is that writeThis is available for every type. So a CG constraint would not actually restrict any call to writeln() from getting compiled.

Still, if the IO library is switched to CG, the compiler will need the "witness" for writeThis because otherwise it will not be visible to the writeln implementation in the case of a user-defined type. So we may as well add a "writeThis is provided" constraint where needed in the IO library.

test/functions/generic/poi/hashed-dist-with-mapper.chpl

Converting it to CG is straightforward. For example:

// HashedDist module introduces an interface for the mapper object
interface HashedMappable(type Self) {
  proc Self.this(elm /*what is the type of 'elm'?*/, targetLocs: [] locale);
}

// This interface is required when creating a 'Hashed'
proc Hashed.init(..., mapper: HashedMappable) {
  ......
}

// The compiler infers that the mapper argument to Hashed.init
// implements this interface. The application code does not change.
var myMapper = .....;
var D: domain(string) dmapped Hashed(idxType=string,
                                     mapper=myMapper);

Here is an interesting piece. The type Hashed will need to know that its field mapper implements the interface HashedMappable in the above example. Perhaps that can be inferred from Hashed.init() ? Such inference might be complicated by the presence of a copy-like initializer - used for privatization - where ideally the library writer would not need to indicate this constraint for the source of the copy.

From Hashed.mapper, the "implements HashedMappable" knowledge probably needs to be transferred to fields of other classes that Hashed creates, such as UserMapAssocDom.mapperType. Even if such transfer is not needed in this particular case, there are other cases where it IS needed, for example in the module DistributedBag from DistBag.eltType through to BagSegmentBlock.eltType.

test/functions/generic/poi/reduce-user-record.chpl

The (internal) implementation of product reduction needs to impose a requirement on ProductReduceScanOp.eltType to implement the following interface:

interface Multipliable(type Self) {
  proc identity(type Self): Self;
  proc *(a: Self, b: Self): Self;
}

Cf. the current implementation actually requires a cast from 1 to eltType (see _prod_id() in Types.chpl) and *=(eltType,eltType). We will probably prefer something closer to the above and so adjust the code in ProductReduceScanOp in trival ways.

Again, the application code needs not change, as long as the compiler infers interface compliance at call site.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment