"Copy of noncopyable typed value" bug

The following code hits a compiler bug:

struct A: ~Copyable {}

struct B: ~Copyable {
    var a: A?
}

func make() -> sending B {
    B(a: nil)
}

func f() {
    let b = make().a // Error: Copy of noncopyable typed value. This is a compiler bug. Please file a bug with a small example of the bug
    _ = b
}

I've filed an issue, but I like to also post these things here out of curiosity to hear a bit about what's going on under the hood.

2 Likes

Yeah, if anyone wants to contribute, this seems to belong to the same compiler bug family as #85576, #87141, #73156, #80724, #82261, #76775, and #72651

curiosity to hear a bit about what's going on under the hood.

I’m not at all an expert on this, but curious too so we can take a peek at the SIL in a similar passing case, then compare:

If it’s written to make the ā€˜consuming nature’ of the situation clear, it seems to compile fine:

struct A: ~Copyable {}

struct B: ~Copyable {
  var a: A?
  
  consuming func takeA() -> A? { a }
}

func make() -> sending B {
    B(a: nil)
}

func f() {
  let b = make().takeA()
  _ = b
}

In the above version the core SILGen emitted for f:

   %0 = alloc_box ${ let Optional<A> }, let, name "b" // users: %13, %1
   %1 = begin_borrow [lexical] [var_decl] %0       // users: %12, %2
   %2 = project_box %1, 0                          // users: %8, %7
   // function_ref make()
   %3 = function_ref @$s8snippet24makeAA1BVyF : $@convention(thin) () -> @sil_sending @owned B // user: %4
   %4 = apply %3() : $@convention(thin) () -> @sil_sending @owned B // user: %6
   // function_ref B.takeA()
   %5 = function_ref @$s8snippet21BV5takeAAA1AVSgyF : $@convention(method) (@owned B) -> @owned Optional<A> // user: %6
   %6 = apply %5(%4) : $@convention(method) (@owned B) -> @owned Optional<A> // user: %7
   store %6 to [init] %2                           // id: %7
   %8 = mark_unresolved_non_copyable_value [no_consume_or_assign] %2 // user: %9
   %9 = load_borrow %8                             // users: %11, %10
   ignored_use %9                                  // id: %10
   end_borrow %9                                   // id: %11
   end_borrow %1                                   // id: %12
   destroy_value %0                                // id: %13

Note here there’s no explicit copies in the IR. We make a stack slot for the Optional<A>, invoke make() -> B, then call takeA() → A? on the returned value, and store it into that original stack slot (store %6 to [init] %2). That is a pretty direct ā€œmove make().takeA() into bā€ I assume.

One more maybe relevant passing case to compare to:

func f() {
  let b = make()
  let a = b.a
  _ = a
}

This one works and is semantically almost identical to what you wrote. The SILGen is:

  %0 = alloc_stack [lexical] [var_decl] $B, let, name "b", type $B // users: %3, %5, %13
  // function_ref make()
  %1 = function_ref @$s8snippet34makeAA1BVyF : $@convention(thin) () -> @sil_sending @owned B // user: %2
  %2 = apply %1() : $@convention(thin) () -> @sil_sending @owned B // user: %3
  store %2 to %0                                  // id: %3
  %4 = alloc_stack [lexical] [var_decl] $Optional<A>, let, name "a", type $Optional<A> // users: %9, %8, %11, %12
  %5 = struct_element_addr %0, #B.a               // user: %6
  %6 = load %5                                    // user: %8
  debug_value undef : $*B, let, name "b"          // id: %7
  store %6 to %4                                  // id: %8
  %9 = load %4
  debug_value undef : $*Optional<A>, let, name "a" // id: %10
  destroy_addr %4                                 // id: %11
  dealloc_stack %4                                // id: %12
  dealloc_stack %0                                // id: %13

In this one again no explicit copy. It puts b on the stack, grabs the address of b.a , loads it (not sure how the compiler thinks about this in terms of whether or not it constitutes a ā€˜copy’, but it’s not a literal copy_value), and then stores it to a.

Now in the failing source (so make().a instead of make().takeA()), SILGen emits:

   %0 = alloc_box ${ let Optional<A> }, let, name "b" // users: %16, %1
   %1 = begin_borrow [lexical] [var_decl] %0       // users: %15, %2
   %2 = project_box %1, 0                          // users: %11, %10
   // function_ref make()
   %3 = function_ref @$s8snippet14makeAA1BVyF : $@convention(thin) () -> @sil_sending @owned B // user: %4
   %4 = apply %3() : $@convention(thin) () -> @sil_sending @owned B // users: %9, %5
   %5 = begin_borrow %4                            // users: %8, %6
   %6 = struct_extract %5, #B.a                    // user: %7
   %7 = copy_value %6                              // user: %10
   end_borrow %5                                   // id: %8
   destroy_value %4                                // id: %9
   store %7 to [init] %2                           // id: %10
   %11 = mark_unresolved_non_copyable_value [no_consume_or_assign] %2 // user: %12
   %12 = load_borrow %11                           // users: %14, %13
   ignored_use %12                                 // id: %13
   end_borrow %12                                  // id: %14
   end_borrow %1                                   // id: %15
   destroy_value %0                                // id: %16

The IR is pretty similar here, except now to extract the a from the returned B it does:

   %4 = apply %3() : $@convention(thin) () -> @sil_sending @owned B // users: %9, %5
   %5 = begin_borrow %4                            // users: %8, %6
   %6 = struct_extract %5, #B.a                    // user: %7
   %7 = copy_value %6                              // user: %10
   end_borrow %5                                   // id: %8
   destroy_value %4                                // id: %9
   store %7 to [init] %2                           // id: %10

It does an explicit copy of make().a with the copy_value, then stores the copy into the original stack slot. I’m assuming that is (part of?) the issue.

As someone with very little knowledge on the compiler architecture, I can’t comment on whether it’s that SILGen shouldn’t have emitted the copy, or some downstream pass should realize it can eliminate the copy to make it legal, or some other option altogether. But anyway there’s some of the IR out of interest :smiley:

4 Likes

This compiles:

struct A: ~Copyable {}

struct B: ~Copyable {
    var a: A?
}

func make() -> sending B {
    B(a: nil)
}

func g() {
    let b = make()
    _ = b.a
}

What is the difference?

one difference i think is that in your case the 'B' variable is an l-value and in the original example it's an r-value. perhaps that has something to do with it? as Ben pointed out though, if you make the 'consumingness' explicit the r-value version also works.

1 Like