User-defined non-copy record initializer on other type breaks resolution of compiler's default copy-initializer

Using Chapel 1.29

  record CSR_descriptor {
    var isWeighted : bool;
    var isVertexT64 : bool;
    var isEdgeT64 : bool;
    var isWeightT64 : bool;
    proc init=(rhs : 4*bool) {
      this.isWeighted = rhs(0);
      this.isVertexT64 = rhs(1);
      this.isEdgeT64 = rhs(2);
      this.isWeightT64 = rhs(3);
    }
  }
  //Opaque handle
  record CSR_handle { 
    var desc : CSR_descriptor;  //Error points here
    var data : c_void_ptr;
  }
  //test
  proc copyDesc() {
  var desc1 : CSR_descriptor;
  var desc2 : CSR_descriptor;
  desc1.isWeighted = true;
  desc1.isVertexT64 = false;
  desc1.isEdgeT64 = false;
  desc1.isWeightT64 = true;
  writeln(desc1);
  writeln(desc2);
  desc2 = desc1;
  writeln(desc2);
  }

https://chapel-lang.org/docs/technotes/initequals.html?highlight=operator#the-init-method-for-non-generic-types
Says that

In order to override this compiler-generated implementation, the user must implement an init= method that can accept an argument of the same type. Other user-defined init= methods will not prevent the compiler from generating a default implementation for init= .

(emphasis mine)

Since my init= is on a 4*bool rather than CSR_descriptor, it shouldn't get in the way of the default copy-initializer.

Without the init= method, it compiles and I get the expected output from copyDesc()

(isWeighted = true, isVertexT64 = false, isEdgeT64 = false, isWeightT64 = true)
(isWeighted = false, isVertexT64 = false, isEdgeT64 = false, isWeightT64 = false)
(isWeighted = true, isVertexT64 = false, isEdgeT64 = false, isWeightT64 = true)

But with the init= function, it suddenly can't resolve the default copy initializer

CSR.chpl:52: error: could not find a copy initializer ('init=') for type 'CSR_descriptor' from type 'CSR_descriptor'
CSR.chpl:43: note: this candidate did not match: CSR_descriptor.init=(rhs: 4*bool) [184935]
CSR.chpl:52: note: because actual argument #1 with type 'CSR_descriptor'
CSR.chpl:43: note: is passed to formal 'rhs: 4*bool [184945]'
CSR.chpl:52: note: unresolved call had id 901713

I can write the trivial copy initializer below (which in turn requires the explicit assignment operator, else I get error: Type 'CSR_descriptor' uses compiler-generated default '=' but has a custom 'init=' method. Please add a '=' function.) and then it works again, but given the documentation this feels like a regression that I shouldn't have to do?

   proc init=(rhs : CSR_descriptor) {
      this.isWeighted = rhs.isWeighted;
      this.isVertexT64 = rhs.isVertexT64;
      this.isEdgeT64 = rhs.isEdgeT64;
      this.isWeightT64 = rhs.isWeightT64;
    }
    operator =(ref lhs : CSR_descriptor, rhs : CSR_descriptor) :void {
      lhs.isWeighted = rhs.isWeighted;
      lhs.isVertexT64 = rhs.isVertexT64;
      lhs.isEdgeT64 = rhs.isEdgeT64;
      lhs.isWeightT64 = rhs.isWeightT64;
    }

Welcome to Chapel's Discourse, Paul! I agree that this looks like either a bug in the documentation or a regression/mistake in the implementation. Let's see whether we can get someone on the team more knowledgeable than me about init= to comment.

Thanks for raising this,
-Brad

Hi Paul. Unfortunately this is an error in our documentation. At one point it was true that a compiler-generated copy initializer would always be generated, but that behavior has since changed and the documentation was not updated to reflect the new behavior.

Part of the motivation behind this change was to allow users to indicate that a type cannot be copy-initialized. It was also found in practice that always compiler-generating the copy initializer could lead to some compilation or execution errors that became difficult to diagnose due to the invisible nature of the compiler-generated copy initializer.

I will work on updating the documentation to reflect the current behavior.

As Ben reminded me offline, it's worth mentioning that for cases like this where the presence of a user method disables the compiler-provided version of the method, we've wanted to provide a way to have the type author opt into preserving the compiler-generated initializer without having to re-implement it manually themselves. For example, the compiler-generated initializer for a large record can be very handy, but sometimes you need a second initializer with a different signature as well. Today, adding that initializer would cause the compiler-generated initializer not to be created, but there ought to be a way to say "I want both" without reimplementing the compiler's version manually.

This is an idea that hasn't had detractors that I'm aware of, and is just looking for a good proposal for how it should be expressed.

-Brad

Following up on this topic, I've merged a PR that will update the relevant documentation for the 1.30 release.

1 Like