i happened to notice this recent bug report, which i believe is a duplicate of this older one regarding a confusing/broken interaction between implicitly opened existentials, inout and default (non-nil) parameters. the problem appears to be when you have a pattern like this, in which a parameter with a default value precedes an inout generic argument:
protocol P {}
func fn<Value: P>(a: Bool = false, b: inout Value) {}
when you try to call such a method by passing an implicitly opened existential and do not provide a value for the default parameter:
func example(_ input: any P) {
var local = input
fn(b: &local)
}
you get a SIL verification failure as the opened existential type is seemingly referenced by the default parameter initializer before it's actually been opened:
SIL verification failed: instruction isn't dominated by its operand: properlyDominates(valueI, I)
Verifying instruction:
// function_ref default argument 0 of fn<A>(a:b:)
%6 = function_ref @$s6output2fn1a1bySb_xztAA1PRzlFfA_ : $@convention(thin) <τ_0_0 where τ_0_0 : P> () -> Bool // user: %7
%9 = open_existential_addr mutable_access %8 : $*any P to $*@opened("0AD2AB08-9C5E-11F0-963F-9BA727167DA4", any P) Self // users: %11, %11, %7
-> %7 = apply %6<@opened("0AD2AB08-9C5E-11F0-963F-9BA727167DA4", any P) Self>() : $@convention(thin) <τ_0_0 where τ_0_0 : P> () -> Bool // type-defs: %9; user: %11
%11 = apply %10<@opened("0AD2AB08-9C5E-11F0-963F-9BA727167DA4", any P) Self>(%7, %9) : $@convention(thin) <τ_0_0 where τ_0_0 : P> (Bool, @inout τ_0_0) -> () // type-defs: %9
In function:
// example(_:)
// Isolation: unspecified
sil hidden [ossa] @$s6output7exampleyyAA1P_pF : $@convention(thin) (@in_guaranteed any P) -> () {
// %0 "input" // users: %5, %1
bb0(%0 : $*any P):
debug_value %0, let, name "input", argno 1, expr op_deref // id: %1
%2 = alloc_box ${ var any P }, var, name "local" // users: %14, %3
%3 = begin_borrow [lexical] [var_decl] %2 // users: %13, %4
%4 = project_box %3, 0 // users: %8, %5
copy_addr %0 to [init] %4 // id: %5
// function_ref default argument 0 of fn<A>(a:b:)
%6 = function_ref @$s6output2fn1a1bySb_xztAA1PRzlFfA_ : $@convention(thin) <τ_0_0 where τ_0_0 : P> () -> Bool // user: %7
%7 = apply %6<@opened("0AD2AB08-9C5E-11F0-963F-9BA727167DA4", any P) Self>() : $@convention(thin) <τ_0_0 where τ_0_0 : P> () -> Bool // type-defs: %9; user: %11
%8 = begin_access [modify] [unknown] %4 // users: %12, %9
%9 = open_existential_addr mutable_access %8 to $*@opened("0AD2AB08-9C5E-11F0-963F-9BA727167DA4", any P) Self // users: %11, %11, %7
// function_ref fn<A>(a:b:)
%10 = function_ref @$s6output2fn1a1bySb_xztAA1PRzlF : $@convention(thin) <τ_0_0 where τ_0_0 : P> (Bool, @inout τ_0_0) -> () // user: %11
%11 = apply %10<@opened("0AD2AB08-9C5E-11F0-963F-9BA727167DA4", any P) Self>(%7, %9) : $@convention(thin) <τ_0_0 where τ_0_0 : P> (Bool, @inout τ_0_0) -> () // type-defs: %9
end_access %8 // id: %12
end_borrow %3 // id: %13
destroy_value %2 // id: %14
%15 = tuple () // user: %16
return %15 // id: %16
} // end sil function '$s6output7exampleyyAA1P_pF'
my impression of the problem is this:
- default parameter initializers of generic functions have access to the function's generic parameters. in this case they receive the type of the opened existential value.
- default parameter initializer(s) must be evaluated in both 'left-to-right' order and resolved before their corresponding function is called.
- per ownership rules, the
inoutgeneric parameter requires that there be exclusive access to the opened existential value that spans the duration of the function call. - due to... something, in the case that a default parameter initializer precedes the generic
inoutparameter, the generated SIL ends up somehow producing instructions such that the default parameter initializer has access to the opened existential value before it actually 'exists'.
the issue seems fairly sensitive to the particular combination of preconditions, and so there appear to be a number of workarounds, including:
- re-ordering the default parameters to occur after the
inoutgeneric (if the function signature is under developer control). - providing explicit values for all parameters that precede the
inoutgeneric parameter. - performing the implicit existential opening 'manually', and forward the opened value through to the original function.
however, having to do any of these things does feel a bit awkward, particularly with the custom TextOutputStream + print() examples from the bug reports (seems unfortunate that those cases don’t 'just work').
i'm wondering what possible solutions to this problem might be (reasonably) implemented. is this something that could be straightforwardly handled within SILGen? could the open existential instruction somehow be 'hoisted' such that it occurs before any default argument initializers? if a 'proper' solution isn't particularly tractable, should/could this case be diagnosed instead? given that this hasn't been fixed in multiple years, i'm assuming it's probably not as simple to address as i'm hoping (in which case i'd be interested to know why), but curious to know if anyone has any thoughts on the matter.