When I get the type of a field and try to call a static method on that type, I encounter issues depending on the lifetime of the field. The call will resolve if the field is borrowed, but will not if the field is unmanaged, owned, or shared.
Source Code:
class Parent {
proc type foo() param {
return 3;
}
}
class Child: Parent {
}
record Wrapper {
var x;
}
var p = new shared Parent(); // works if borrowed or unspecified, otherwise fails to resolve the call
var c = new shared Child(); // works if borrowed or unspecified, otherwise fails to resolve the call
var w1 = new Wrapper(p);
var w2 = new Wrapper(c);
writeln(w1.x.type: string);
writeln(w2.x.type: string);
writeln(w1.x.type.foo());
writeln(w2.x.type.foo());
Compile command:
chpl foo.chpl
Associated Future Test(s):
test/classes/lydia/staticMethodInheritance-whenField-*.chpl #12096
chpl --version: 1.19.0 (pre-release)Note that I didn't double check the behavior on when getting the type from a non-field instance. It could be that that also fails
This doesn't sound very surprising (though I also don't think it's what we want). Specifically, the type methods we create for proc type C.myTypeMethod(...) can essentially be thought of as having the signature:
proc myTypeMethod(type t: C, ...) ...
or maybe (*)
proc myTypeMethod(type t, ...) where t == C ...
so given that C typically means borrowed C, it makes sense that it would match for that case but not others. Presumably to get this to work, we'd need to either change the signatures above to support "any management flavor of C" or change resolution to unwrap these flavors before resolving the type method (where the former sounds preferable, since it's cleaner, to me).
Tagging @mppf as the class management flavor expert.
(* = I'm having trouble remembering or finding where the type constraint is added at this moment)
Most of the logic to do methods for owned/shared/unmanaged relies on coercions.
But we decided not to do coercions for type t: C arguments (and we don't for generic arguments with where clauses).
So it would require something else special/different do to this. We might be able to get the forwarding mechanism to do it, for example.
I was imagining that we'd change the compiler to generate type methods using a signature more like:
proc myTypeMethod(type t: ?/* any */ C, ...)
than to wrestle with forwarding or coercions. I.e., an argument of type t: C would not support any flavor of C, but we'd have some way of expressing "any C" (and, ideally, the user would as well for cases where they needed it).
Yeah, but... say we have type foo() as in the original example in the issue description. Do we actually want the compiler to instantiate foo(borrowed Parent), foo(owned Parent), and foo(unmanaged Parent)? These could then behave differently by working on the type of this, couldn't they? Wouldn't that be more surprising than useful?
Do we actually want the compiler to instantiate foo(borrowed Parent), foo(owned Parent), and foo(unmanaged Parent)?
This would only happen if all three signatures were required by the user's code, wouldn't it? And if the user's code did require it, what choice would we have?
These could then behave differently by working on the type of this, couldn't they? Wouldn't that be more surprising than useful?
Conceptually, I understand what you're saying, but I'm having a hard time coming up with an example where that would be the case (i.e., that it would be surprising).
I ran afoul of this issue in my own work today (i.e., independently of Lydia's context): I've essentially got a record with a generic field:
record R {
var x;
}
where in practice, R.x is likely to be an owned class or an unmanaged class. I'm in the context of a type method on R and trying to call a type method on x's type, but because x isn't borrowed, it's not resolving, giving errors like:
testit.chpl:79: error: unresolved call 'type unmanaged C.foo(int(64))'
...
testit.chpl:17: note: candidates are: C.type foo(x: int)
This was another case where, intuitively, the fact that I'd defined proc type C.foo(x: int) made it seem as though I should be able to call the method on types unmanaged C or owned C, yet I couldn't.
I also haven't been able to find a workaround so far... is there one? (e.g., can I explicitly define type methods on owned C and borrowed C?)
@bradcray - the workaround is to call the type method on _to_borrowed(t).
Cool, that helps me make forward progress with this prototype.
The next challenge is that my type method behaves like a factory method, creating and returning instances of that type. But it doesn't know whether to create an owned, unmanaged, or shared. For the purposes of the experiment I'm working on, I can just hardcode it to what I happen to know I want it to be, but more generally it seems like it might be nice to have this on a class type method be owned C if the original type was owned C, unmanaged C if it had been unmanaged C, etc., which would potentially support writing new this(...) and having the right thing happen (?). It seems like the other alternative would be to support the ability to write type methods on each of the flavors distinctly, but (for this case at least), I think this would result in a lot of repeated code.
I ran back into this issue again today and still tend to intuitively expect that a class type method should be generic w.r.t. different flavors of classes (unmanaged, shared, owned).
Presumably to get this to work, we'd need to either change the signatures above to support "any management flavor of C" or change resolution to unwrap these flavors before resolving the type method (where the former sounds preferable, since it's cleaner, to me).
In other words, we are considering two options:
class C {
proc type typemethod() {
writeln(this:string);
}
}
var x = new owned C();
x.type.typemethod(); // outputs `owned C`
borrow when calling the type method:class C {
proc type typemethod() {
writeln(this:string);
}
}
var x = new owned C();
x.type.typemethod(); // outputs `C` aka `borrowed C`
We have decided to choose Option 1 from the above comment. That is, a type method in a class is generic w.r.t. the receiver's management flavor - owned, shared, borrowed, unmanaged.
Should that decision extend to secondary methods? In the following declaration:
proc type C.typemethod() {....}
A. C is an implicit borrowed C, as is the rule in most cases in Chapel, or
B. This is an exception to the rule, and C is treated as generic over owned/shared/...
Note that in either case the class flavor can be specified explicitly in a secondary method (currently unsupported for primary methods), ex.
proc type (owned C).typemethod() {}
I think that the secondary method case should behave the same as the primary method case because I view secondary methods as simply being an alternate syntax for writing a method rather than something distinct from a primary method (besides which, if it meant "define for borrowed C only, we'd need to invent some way of creating the generic secondary type method version).
I don't view this as an exception per se, at least not any more than I consider class C not meaning class borrowed C to be an exception. In this instance, the C seems to be saying "which class type should this secondary method be associated with?" where the answer C seems like a reasonable default without thinking of it as borrowed C.