I'm getting a warning about infinite recursion that seems incorrect. Here's a demo of the issue:
protocol P {
associatedtype Base: P
}
extension P {
static var path: [any P.Type] {
if Base.self == Self.self {
return [Self.self]
}
else {
return Base.path + [Self.self]
// ^--- Warning: Function call causes an infinite recursion
}
}
}
struct A: P {
typealias Base = Self
}
struct B: P {
typealias Base = A
}
struct C: P {
typealias Base = B
}
print(C.path)
Running this code correctly prints out [main.A, main.B, main.C]. I would think the conditional would be enough to tell the compiler there's a way out of the recursion. Is there a way to disable the warning?
Using swift.godbolt.org to try your code with older versions of the compiler, it appears the spurious warning first appeared in Swift 5.9. Compiling under Swift 5.8 does not give a warning.
If you change typealias Base = Self to typealias Base = B, you get the infinite recursion at runtime.
I wouldn't expect the compiler to evaluate all possible chains of P/Base declarations to ensure they terminate in a type that refers to itself.
Indeed, perhaps it's best to always warn on self-call. I don't think Swift guarantees cases where it will reduce tail recursion to a loop (though it might do so in some cases...)
It's a pity Swift doesn't currently support 'negative' constraints, like where Base != Self, as that would presumably address this spurious warning (by properly defining away its possibility).
Since the code runs correctly I would say no, the warning isn’t correct. It’s possible there’s an infinite recursion, but there’s plenty of ways to cause one that doesn’t produce the warning. Why should this one be different?
I agree that this could be defined away with some type system features we don’t have. In the meantime, I think the compiler should defer to the programmer’s intent.
This looks like a bug to me. Only one of the branches in the getter actually calls itself, and the conditional is not optimized away. However, the getter calls itself with a different substitution map than it was called with, and I suspect this confuses the analysis. Please file a issue for this!
Worth noting that as a solution this would change the semantics, though. When it’s not a requirement, the structure of path is driven entirely by the associatedtype relationships, but once it’s a requirement arbitrary conformers could subvert this and supply a custom implementation that deviates from the default.
true. but i think an implementation that dispatches through the protocol instead of manually inspecting metatypes just makes a lot more sense to me.
protocol P
{
associatedtype Base:P
static
var path:[any P.Type] { get }
}
extension P where Base == Self
{
static
var path:[any P.Type]
{
[Self.self]
}
}
extension P
{
static
var path:[any P.Type]
{
Base.path + [Self.self]
}
}
struct A:P
{
typealias Base = Self
}
struct B:P
{
typealias Base = A
}
struct C:P
{
typealias Base = B
}
print(C.path)
[test.A, test.B, test.C]
arguably, the potential for customization itself can be useful too.
Perhaps, but figured it should be called out that if you’re relying on properties like “the nth item in path is the type of the nth type in the associatedtype chain” then allowing the semantics to deviate from that is probably not desirable.
Try it with class A instead of struct A. You’ll also have to change typealias Base = Self to typealias Base = A, but that’s fine.
After making those changes, your version goes into an infinite loop, whereas the original version by @seanmrich still works correctly.
The reason is that, since A is non-final, it may have subclasses where Base is not Self, so the conformance of A to P must use the unconditional extension.
The original version checks the types at runtime, so it still works correctly.
• • •
We might expect subclasses to set Base to their superclass, but in fact neither version allows this to work properly. If we make class B: A and class C: B, then the line typealias Base = B inside the body of C does not “count” for the associated type, so C still uses A as its Base for purposes of P.
good catch! but this is a consequence of requiring Base:P, which might not actually be needed.
we can make the recursive behavior conditional on Base:P, and eliminate the need to have typealias Base = Self at all.
protocol P
{
associatedtype Base = Void
static
var path:[any P.Type] { get }
}
extension P
{
static
var path:[any P.Type]
{
[Self.self]
}
}
extension P where Base:P
{
static
var path:[any P.Type]
{
Base.path + [Self.self]
}
}
class A:P
{
}
struct B:P
{
typealias Base = A
}
struct C:P
{
typealias Base = B
}
print(A.path)
print(B.path)
print(C.path)