Explicitly capturing memory management when implementing cast between two generic types

I'm trying to implement the cast : operator between two generic class types in a way that explicitly copies the management strategy from the fromType.

I know that : has the signature below (though I forget whether I found that in a doc or inferred it through trial and error).

operator :(from : fromType, type to : toType) {..}

I want to both restrict fromType to only be instantiations of generic class FooFrom, but also be able to query the memory management from it. So I'm using a queryable formal, paired with a where clause for the type restriction.

I currently have

class FooFrom {
  param commonParam;
  ... // other common non-param members
}
class FooTo {
  param commonParam;
  ... // other common non-param members
  operator :( from : ?fromType, type to: this.type) where isSubtype(fromType, FooFrom(?)) {
    assert(to.commonParam == fromType.commonParam, "CAST FAILED");
    var tmp = new fromType:class? to(
      ... // other common non-param members
    );
    return tmp;
  }
}

But I'm getting

<line that new is on>: syntax error: 'new' expression is missing its argument list
<line that new is on>: syntax error: near 'to'

I don't think I'm correctly capturing the memory management of from, which is what's ultimately breaking the new expression.

(I came up with the fromType:class? syntax from here Conversions — Chapel Documentation 1.30, but within a new expression is clearly not the right place to use it.)

If I remove the attempt to make it explicit, and just do new to(...) then I actually implicitly get the behavior I want. An unmanaged fromType becomes an unmanaged FooTo, a shared becomes a shared. But it's a little uncomfortable that this behavior isn't made explicit anywhere I've found (yet).

Am I overcomplicating by trying to control management within the : operator? And instead should just do any explicit management control in the cast's caller? (Perfectly reasonable if that's the case, or I've missed a doc somewhere which guarantees the copied management semantics.)

Perhaps I just want a param proc that directly returns the management strategy of a class type? I know there are boolean ones to check if it is/n't one of the four strategies, but nothing like numBits that would return the specific strategy, at least not on Types — Chapel Documentation 1.30

While it's not querying it from the type itself, one thing that you
could use is these queries from the Types module:

which I believe will allow you to pass either from itself or fromType and get a bool result. You could then use that to explicitly set the matching memory management in the new declaration (probably by taking advantage of split initialization so that tmp can live outside of the if statement making these checks

Hope that helps,
Lydia
(edit for removed links)

Sorry, looks like that got cut off. I've edited the original message with what was missing.

Lydia

Thanks Lydia!

Doh, I always forget about split init so I hadn't even thought of this pattern.

      var tmp : to;
      if (isOwnedClass(from)) {
        tmp= new owned to();
      } else if (isSharedClass(from)) {
        tmp= new shared to();
      } else if (isUnmanagedClass(from)) {
        tmp= new unmanaged to();
      } else if (isBorrowedClass(from)) {
        tmp= new borrowed to();
      }
    ... // Assign the common non-param members
    return tmp;

The above is choking on a duplicate decorator when to already has a management strategy (in the branch matching the existing strategy), but it feels close.

Edit: I guess that's because the caller is assuming some strategy for the cast, which it seems to be grabbing from the instance I'm casting from. Which ultimately is the behavior I want, but it's uncomfortable that it's implicit. If anything I would expect the to type to revert to the default managed strategy.

Gotta quit for today, I can move forward with the implicit behavior, but it's a curious one.

To avoid the duplicate decorator issue, you can apply a type cast.

tmp = new owned to();
tmp = new shared to();
tmp = new unmanaged to();

becomes

tmp = new (to:owned)();
tmp = new (to:shared)();
tmp = new (to:unmanaged)();

Also note that new (to:borrowed)() (or new borrowed ...) is deprecated and in this case will fail the lifetime checker anyways.

As a side note, the calls like isOwnedClass(from) will never return true. Casting a managed class instance will always result in an implicit borrow, e.g. myOwned:someOtherType implicitly borrows and looks like myOwned.borrow():someOtherType

-Jade

Yes casting the type is the way we have to override the decorator for a class type.

About the side note:

There might be a bug here somewhere, but I don't think that's quite accurate, at least not in all cases:

class Parent { }
class Child : Parent { }

proc main() {
  var x = new Child();
  var y = x: owned Parent;
  writeln(y, ":", y.type:string);
}
{}:owned Parent

Also, I also don't see the problem when defining a custom operator or calling isOwnedClass:

class MyClass { var field: int; }

operator MyClass.: (x: MyClass, type t: int) {
  writeln("In cast, x.type is ", x.type:string);
  writeln("isOwnedClass(x) is ", isOwnedClass(x));
  writeln("isOwnedClass(x.type) is ", isOwnedClass(x.type));
  return x.field;
}

proc main() {
  var x = new MyClass(1);
  writeln(x : int);
}

In cast, x.type is owned MyClass
isOwnedClass(x) is true
isOwnedClass(x.type) is true
1

So, I'd be curious what case appeared to have the implicit borrow on a cast.

The specific edge case is when casting from managed to not managed. So the above example with int works, but changing type t to be type t: unmanaged causes x.type to be borrowed and isOwnedClass(x) to return false.