Skip to content

Instantly share code, notes, and snippets.

@manicaesar
Created September 24, 2021 19:10
Show Gist options
  • Save manicaesar/984ea25673b5dc412ce3fb05ea4deaa2 to your computer and use it in GitHub Desktop.
Save manicaesar/984ea25673b5dc412ce3fb05ea4deaa2 to your computer and use it in GitHub Desktop.
Swift property overloading
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
@manicaesar
Copy link
Author

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 👍

@manicaesar
Copy link
Author

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.

@Losiowaty
Copy link

Losiowaty commented Sep 26, 2021

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment