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 