-
-
Save manicaesar/984ea25673b5dc412ce3fb05ea4deaa2 to your computer and use it in GitHub Desktop.
struct StructWithTwoObjects { | |
let object: String | |
/* | |
var object: A { // Stupid, but causes compilation error - OK! | |
return self | |
} | |
*/ | |
} | |
// --------------------- | |
protocol SomeProtocol {} | |
protocol ObjectProviding { | |
associatedtype ObjectType: SomeProtocol | |
var object: ObjectType { get } | |
} | |
extension ObjectProviding where | |
Self: SomeProtocol, // It is not needed, can be commented out | |
Self.ObjectType == Self { | |
var object: ObjectType { | |
return self | |
} | |
} | |
struct ObjectContainer: SomeProtocol, ObjectProviding { | |
let object: String | |
init(object: String) { | |
self.object = object | |
} | |
} | |
struct ObjectPrinter<T: ObjectProviding> { | |
let objectContainer: T | |
init(objectContainer: T) { | |
self.objectContainer = objectContainer | |
} | |
func printTypeOfContainedObject() { | |
return print(type(of: objectContainer.object)) | |
} | |
} | |
let objectContainer = ObjectContainer(object: "string") | |
let printer = ObjectPrinter<ObjectContainer>(objectContainer: objectContainer) | |
print(type(of: objectContainer.object)) // Prints: String | |
printer.printTypeOfContainedObject() // Prints: ObjectContainer |
Many thanks for the thorough investigation.
It is interesting that the issue can be reproduced with the simple struct & protocol example you provided. I assumed the same as you - that it would not compile - and didn't even try to simplify the example further - thanks for proving me wrong! 😀
Yeah, it reminded me of the behaviour you mentioned at the end of your comment as well... but then I realised it is a little bit different, because property signatures (if there is such thing as property signature, not sure 😅) in terms of returned type are different, while foo
and bar
are the same in terms of returned type and parameter types.
I think the culprit here is that from SIL perspective, getters are equivalent to parameterless methods (more on that below). So if we modify the example to use parameterless functions instead of getters, then I guess you won't be surprised that the code below compiles - as method overloads are legal:
protocol Provider {
func fvalue() -> Int
}
extension Provider {
func fvalue() -> Int { 1 }
}
struct Struct: Provider {
func fvalue() -> String { "11" }
}
let s = Struct()
let fint: Int = s.fvalue()
let fstr: String = s.fvalue()
Now, why I think that:
from SIL perspective, getters are equivalent to parameterless methods
?
Well, merging the two examples together into:
protocol Provider {
var value: Int { get }
func fvalue() -> Int
}
extension Provider {
var value: Int { 0 }
func fvalue() -> Int { 1 }
}
struct Struct: Provider {
var value: String { "10" }
func fvalue() -> String { "11" }
}
let s = Struct()
let int: Int = s.value
let str: String = s.value
let fint: Int = s.fvalue()
let fstr: String = s.fvalue()
and looking at the SIL generated, it basically almost the same for value
and fvalue
, e.g.:
// Provider.value.getter
sil hidden [ossa] @$s7SILTest8ProviderPAAE5valueSivg : $@convention(method) <Self where Self : Provider> (@in_guaranteed Self) -> Int {
// %0 "self" // user: %1
bb0(%0 : $*Self):
debug_value_addr %0 : $*Self, let, name "self", argno 1 // id: %1
%2 = integer_literal $Builtin.IntLiteral, 0 // user: %5
%3 = metatype $@thin Int.Type // user: %5
// function_ref Int.init(_builtinIntegerLiteral:)
%4 = function_ref @$sSi22_builtinIntegerLiteralSiBI_tcfC : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %5
%5 = apply %4(%2, %3) : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %6
return %5 : $Int // id: %6
} // end sil function '$s7SILTest8ProviderPAAE5valueSivg'
// Int.init(_builtinIntegerLiteral:)
sil [transparent] [serialized] @$sSi22_builtinIntegerLiteralSiBI_tcfC : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int
// Provider.fvalue()
sil hidden [ossa] @$s7SILTest8ProviderPAAE6fvalueSiyF : $@convention(method) <Self where Self : Provider> (@in_guaranteed Self) -> Int {
// %0 "self" // user: %1
bb0(%0 : $*Self):
debug_value_addr %0 : $*Self, let, name "self", argno 1 // id: %1
%2 = integer_literal $Builtin.IntLiteral, 1 // user: %5
%3 = metatype $@thin Int.Type // user: %5
// function_ref Int.init(_builtinIntegerLiteral:)
%4 = function_ref @$sSi22_builtinIntegerLiteralSiBI_tcfC : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %5
%5 = apply %4(%2, %3) : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %6
return %5 : $Int // id: %6
} // end sil function '$s7SILTest8ProviderPAAE6fvalueSiyF'
// protocol witness for Provider.value.getter in conformance Struct
sil private [transparent] [thunk] [ossa] @$s7SILTest6StructVAA8ProviderA2aDP5valueSivgTW : $@convention(witness_method: Provider) (@in_guaranteed Struct) -> Int {
// %0 // user: %2
bb0(%0 : $*Struct):
// function_ref Provider.value.getter
%1 = function_ref @$s7SILTest8ProviderPAAE5valueSivg : $@convention(method) <τ_0_0 where τ_0_0 : Provider> (@in_guaranteed τ_0_0) -> Int // user: %2
%2 = apply %1<Struct>(%0) : $@convention(method) <τ_0_0 where τ_0_0 : Provider> (@in_guaranteed τ_0_0) -> Int // user: %3
return %2 : $Int // id: %3
} // end sil function '$s7SILTest6StructVAA8ProviderA2aDP5valueSivgTW'
// protocol witness for Provider.fvalue() in conformance Struct
sil private [transparent] [thunk] [ossa] @$s7SILTest6StructVAA8ProviderA2aDP6fvalueSiyFTW : $@convention(witness_method: Provider) (@in_guaranteed Struct) -> Int {
// %0 // user: %2
bb0(%0 : $*Struct):
// function_ref Provider.fvalue()
%1 = function_ref @$s7SILTest8ProviderPAAE6fvalueSiyF : $@convention(method) <τ_0_0 where τ_0_0 : Provider> (@in_guaranteed τ_0_0) -> Int // user: %2
%2 = apply %1<Struct>(%0) : $@convention(method) <τ_0_0 where τ_0_0 : Provider> (@in_guaranteed τ_0_0) -> Int // user: %3
return %2 : $Int // id: %3
} // end sil function '$s7SILTest6StructVAA8ProviderA2aDP6fvalueSiyFTW'
sil_witness_table hidden Struct: Provider module SILTest {
method #Provider.value!getter: <Self where Self : Provider> (Self) -> () -> Int : @$s7SILTest6StructVAA8ProviderA2aDP5valueSivgTW // protocol witness for Provider.value.getter in conformance Struct
method #Provider.fvalue: <Self where Self : Provider> (Self) -> () -> Int : @$s7SILTest6StructVAA8ProviderA2aDP6fvalueSiyFTW // protocol witness for Provider.fvalue() in conformance Struct
}
So yeah, I guess the best idea would be to ask on Swift forums whether this is actually expected behaviour (as I am not going to read compiler's code either 😝). Thanks again and I am glad you enjoyed diving into this topic 👍
Ok, I have found this thread: https://forums.swift.org/t/concerned-about-this-protocol-extension-conformance-bug/9687
And I guess we can assume Ben Cohen's response is official explanation (not a bug, if we want to change it, it is a feature request 😀)
https://forums.swift.org/t/concerned-about-this-protocol-extension-conformance-bug/9687/5
Though looks like this never got enough traction to at least provide "near-miss warning" from the compiler.
O wow! That's a super interesting thread, glad you found it ❤️
While it's (a little) reassuring that this is expected and well-defined behavior, I don't think I personally like the potential implications of all of this, especially when we move this discussion to consider cross-framework implications.
Since in your merged sample we see that we can force different value
to be used by giving some hints to the compiler than this becomes possible :
// Assume this is in External Module
protocol Provider {
var value: Int { get }
func fvalue() -> Int
}
extension Provider {
var value: Int { 0 }
}
// -------
// And now in Our Module
struct Struct: Provider {
var value: String { "1" }
// Provider conformance
func fvalue() -> Int { return 1 }
}
func takeInt(_ i: Int) { print(i) }
takeInt(Struct().value)
This can be unexpected from the External Module
consumer perspective - the compiler obviously told them that they need to implement func fvalue()
to fulfil the requirements but in no other way hinted them that there are other requirements that have default values - so they aren't aware that they "silently" have an Int
property and would (rightfully so!) expect this to fail, or maybe assume that there is a conversion happening under the hood (conversion from Int
to String
would be more natural, but still)
Not to mention the samples that Xiaodi Wu and Marc Palmer mentioned in their posts 🤯
It can easily lead to a whole lot of errors that may be confusing to track down.
I think I kinda know what and how is happening 😅
The explanation is super lengthy, mainly because I didn't know how to explain it in a shorter way so it would still be organised and readable 🙈
For reference - SIL docs
It seems it has to do with static dispatch and how this is done based on the variable type (or what type information we have).
To try to understand everything let's start from this super simple and straightforward sample :
And the important/interesting parts of SIL are :
What is important to notice is that the
Struct
itself at this point has no notion of it having avalue
property - it was purely inherited from the protocol and its default implementation - it is "injected" into thestruct
via the witness table#3
as something (a method) with a signature(Self) -> () -> Int
.Now the call sites for
value
look a little bit different depending on what we know about the type :In
#4
we know the exact type of the object so we are able to directly find the exact implementation we want to call (function_ref @$s4main8ProviderPAAE5valueSivg
- the default implementation from the protocol).In
#5
we know that we have "something" of typeProvider
so we need to reach into the actual existential and in its witness table find a method that fits our signature - and the only candidate is the method that points to the implementation that triggers the default implementation.Let's adjust our sample by simply providing a dedicated implementation of the protocol requirements:
This little source change has some interesting implications in SIL :
The struct itself now actually knows about the
value
:which caused the implementation of
// protocol witness for Provider.value.getter in conformance Struct
to change and now call the dedicated value getter provided in thestruct
:Notice how the
sil_witness_table hidden Struct: Provider module main
didn't change at all! It still points to the same method as before, just its implementation changed.The change in call sites accessing the property changed in an expected way - the direct access now calls the proper implementation directly, and accessing it via
Provider
variable still needs to reach out into the existential.Up to this point everything looks and works as expected 👍 So far so good 😆
Let's shake things up! 🌪️
With this change we reached a simplified version of your sample - just without the generics (we'll get to them later).
Before seeing your post on Twitter I'd have said with full conviction that at this moment Swift should start throwing errors - I'd expect either
invalid redeclaration of value
orStruct does not conform to protocol Provider
- I guess either would make sense for me.But as we already know, Swift is happy with it (well, I guess the compiler more than Swift per se).
Let's dive deep into SIL again 🤿
Just to see that actually not much has changed 😅
Which actually kinda explains the entire situation 😅
One of the most important part that didn't change is still the
sil_witness_table hidden Struct: Provider module main
:Now we enter a, more or less "educated", guessing territory, which most likely could be confirmed by looking at Swift/compiler source code.
I believe this is what happens :
Struct
is compiled and has thevalue: String
propertyProvider
conformance gets compiled and the witness table is generatedStruct
, doesn't find it.// protocol witness for Provider.value.getter in conformance Struct
The other important part that didn't change are the call sites :
Struct
we have direct static dispatchProvider
we reach into the existential and find the method pointed to by its witness table - which in its implementation calls the default implementation!Instinctively the entire process should error out somewhere between steps 3 and 4 described above.
Now there are two questions left.
Dunno 😆 Apart from actually reading Swift/compiler source code (not saying I'm not gonna do it 😅 ) the best would be to ask on Swift forums ;)
I've found those two threads (maybe the search term needs to be refined) :
They aren't precisely about what you are asking but there is some similar behaviours and interesting knowledge between the lines.
The basics are the same. The only extra thing that happens is that the compiler, trying to be super extra helpful is also figuring out that since
ObjectContainer
does conform toSomeProtocol
itself it can infertypealias ObjectType = ObjectContainer
in that case and then happily use the default implementation you provided.This inference can obviously bee seen in SIL :
and seems to be the crux of the issue in this sample - as soon as you explicitly state that
typealias ObjectType = String
or remove the conformance ofObjectContainer
toSomeProtocol
we get expected failures.Alternatively providing
extension String: SomeProtocol {}
pushes the compiler in the expected direction when it sees that the custom providedlet object: String
can fulfil the protocols requirements so its best to use it.After this lengthy text (which I immensely enjoyed researching and writing) my conclusion is that this is the result and a mix of the compiler trying to be super helpful (by trying to figure out the
associatedtype
by itself in the generic case) and the fact that the default implementation "lives in a different place" - in terms of static dispatch - so it doesn't clash when you use the same names but otherwise don't fulfil the protocols requirements.All of this kinda reminds me of this behaviour :
In this case both
func bar()
methods, while having the same signature and name don't clash because they both "live in different contexts".