28616, "DanilaFe", "(How) should we allow 'type' fields instantiated with generic types?", "2026-03-30T18:53:31Z"
All of this was motivated by https://github.com/Cray/chapel-private/issues/7701, which in turn was motivated by test/types/type_variables/nelson/dependentTypeOverClasses.chpl. Specifically, we currently lock down that the following program works:
class A {
proc hello() { writeln("hello from A!"); }
}
class N {
type X;
var x = new owned X();
}
proc main() {
var n = new owned N(X=A);
n.x.hello();
}
Above, A is a class; so on its own, A is generic (it has generic ownership). The class N (which should be able to be a record, but can't be; see [Bug]: cannot create records with management-generic fields · Issue #28470 · chapel-lang/chapel · GitHub) has a type field X, which is instantiated with (anymanaged) A. This is then used to create an owned instance of A, stored in the variable x. The code demonstrates that T = owned N(X = /* anymanaged */ A) is concrete and functional, by invoking n.x.hello().
(note: Dyno balks at this, because it considers T to be generic, and thus not-fully-instantiated.)
One possible intuition is that we shouldn't count T as generic, because all its var fields are concrete. But then, a formal of type owned N(A) is counted as generic:
proc foo(x: owned N(A)) {
compilerWarning(x.X : string);
}
foo(new owned N(X=A)); // prints 'anymanaged A'
foo(new owned N(X=shared A)); // prints 'shared A'
foo(new owned N(X=owned A)); // prints 'owned A'
In fact, the production compiler itself hasn't quite made up its mind as to whether T is generic. isGeneric(T) returns true when no declaration like var n above exists, and false when it does. I believe the compiler commits to one of these answers when it's first asked, which leads to weird behavior:
// compiles and prints 'true'
writeln(isGeneric(owned N(X=A)));
// compiles and prints 'false'
var n = new owned N(X=A);
writeln(isGeneric(owned N(X=A)));
// doesn't compile; returns could not determine the concrete type for the generic return type 'unmanaged N(anymanaged A)'
writeln(isGeneric(owned N(X=A)));
var n = new owned N(X=A);
I know of two approaches to resolve this issue.
Approach A: Refine our Thinking
@bradcray's thinking here (and I'm paraphrasing) is to separate whether something is 'generic' and 'instantiatable'. I'm going to use 'creatable' for 'instantiatable' to avoid confusing terminology with 'generic instantiations'.
Something is 'generic' when there exist types that are type-based refinements of it. For instance, owned N(A) is generic because owned N(owned A) is such a refinement.
Something is 'creatable' when we have enough type information to create a value of this type. owned N(A) is creatable because its only "real" field has type owned A, which we have no problem creating.
These two properties are more-or-less orthogonal.
T Generic
T Not Generic
T Creatable
class N {
type X;
var x = new owned X();
}
T = owned N(X = A);
class N {
type X;
var x = new X();
}
T = owned N(X = owned A);
T Not Creatable
class N {
type X;
var x: X;
}
T = owned N(numeric);
// 'x' not concrete, can't create
N/A
The idea, then, is to distinguish the properties.
Additionally, to solve a problem in which .type can behave strangely:
proc foo(x: owned N(A), y: x.type) {
// should 'y' be able to be instantiated with 'owned N(shared A)'?
}
We can make it so that types of variables, even those created from generic types, are concrete. Then,
var x: owned N(A); // fine, type is creatable even though generic
writeln(isGeneric(owned N(A))); // true
writeln(isGeneric(x.type)); // false; 'x' is an existing value, can't refine it further.
I'm not sure how this should behave with x.type == owned N(A) (presumably this should return false, since the two types behave differently with isGeneric?).
Approach B: Just don't do it
All of this is a lot of effort for a relatively niche pattern. Do we really need to be able to allocate types with generic fields, even if it comes at the cost of all this extra machinery?
The pattern in the original test can be easily adjusted to work without generic instantiations as follows:
class A {
proc hello() { writeln("hello from A!"); }
}
class N {
type X;
var x = new (X : owned)();
}
proc main() {
var n = new owned N(X=borrowed A);
n.x.hello();
}
In other words: we can cast class types to any other ownership without them being generic. borrowed A can become owned A with a cast, but borrowed A is concrete, so there's no magic genericity. So, we can disallow the pattern.