New Issue: should it be clearer when a class/record is generic?

19120, "mppf", "should it be clearer when a class/record is generic?", "2022-01-26T14:54:04Z"

Related to #18665 and #16508.

Also related to a common error in declaring recursive class types (in details)

I think there have been issues about that but I haven't found a link to one yet.

Coming from something like C++ it is common to write code like this:

class IntListNode {
  var value: int;
  var next: IntListNode;
}

which is probably error because now IntListNode is generic. (It is generic because just IntListNode means the class with generic/unknown management).

The question posed by this issue is - should it be clearer when a class/record is generic?

Today we have a problem that whether or not a class/record is generic depends upon how the field types are resolved. I don't think this makes sense.

In Generics — Chapel Documentation 1.26 , the spec says a generic field is:

  • a specified or unspecified type alias,
  • a parameter field, or
  • a var or const field that has no type and no initialization expression.

Notably absent from this is "a var or const field that is declared with a generic type" even though it is how the compiler is behaving today. The fact that it behaves this way is due to my PR #9489 where I thought I was increasing generality of a feature in an obvious manner. However now I think that was not a good idea.

In particular, for the compiler today, the record A is generic in the below:

record GR { type t; field: t; }

record A {
  var field: GR;
}

However one has to look at / study the definition of GR in order to know that A is generic. It can be worse than that, because we allow type functions, the type of field could be computed by a type function. So, in other words, it is not clear from the record A declaration itself whether the programmer intended to create a generic record.

I think it is nice that writing generic code in Chapel is easy and Python-like. However, for a type declarations specifically, it makes a big difference to how the type is used whether or not it is generic. In particular, the generic fields form part of the API for the type since they are used in type construction (e.g. A(int) is a type construction call). So, I think that:

  • the programmer already knows whether or not they are trying to create a generic type
  • they should communicate that clearly to both the compiler and people reading the definition of the type

Beyond the question about whether the current situation is good enough at communicating programmer intent, quite a bit of the compiler code behaves differently depending on whether a type is generic or not, and it would definitely make the compiler easier to implement if this property was obvious from the syntax.

Speaking of compiler bugs, here is a more difficult example. As long as the generic-ness of a type depends on the field types, it can only be computed during resolution, but that presents some challenge when the type is recursive. This example has surprising behavior today, in that the type seems to be both concrete (according to warnings) and generic (according to a later compilation error). Is the challenge in implementing the behavior correctly an indication that programs using this feature can be difficult to understand?

record GR { type t=int; var field: t; }

proc computeTypeY(type xType, type rType) type {
  compilerWarning("In computeTypeY, rType=" + rType:string +
                  " isGenericType(rType)=" + isGenericType(rType):string,
                  " isGenericType(R)=" + isGenericType(R):string);
  return unmanaged C?;
}
proc computeTypeZ(type yType, type rType) type {
  compilerWarning("In computeTypeZ, rType=" + rType:string +
                  " isGenericType(rType)=" + isGenericType(rType):string,
                  " isGenericType(R)=" + isGenericType(R):string);

  return GR;
}


class C {
  var field: R;
}

record R {
  var x: unmanaged C?;
  var y: computeTypeY(x.type, R);
  var z: computeTypeZ(y.type, R);
}

compilerWarning("At top level, isGenericType(R)=" +
                isGenericType(R):string);

var x:R;
ee.chpl:1: In module 'ee':
ee.chpl:24: warning: In computeTypeY, rType=R isGenericType(rType)=false isGenericType(R)=false
ee.chpl:25: warning: In computeTypeZ, rType=R isGenericType(rType)=false isGenericType(R)=false
ee.chpl:28: warning: At top level, isGenericType(R)=false
ee.chpl:24: warning: In computeTypeY, rType=R isGenericType(rType)=false isGenericType(R)=false
ee.chpl:25: warning: In computeTypeZ, rType=R isGenericType(rType)=false isGenericType(R)=false
ee.chpl:25: error: Cannot default-initialize a variable with generic type
ee.chpl:25: note: 'z' has generic type 'GR'

How can R be not generic but have a generic field z?

In particular I can see two possible approaches to make it clear when a type is generic:

Approach 1

Building on the proposal in #18665, a user wishing to make a field generic can do so with the ? syntax.

So in the original example, we could write

record GR { type t; field: t; }

record A {
  var field: GR(?);
}

Approach 2

We could undo the change in PR #9489 and move instead to requiring no type specification (e.g. var field;) or separate type fields, which is the situation described in the spec and the situation before that PR.