Circular init pattern not caught by compiler

class Location: NSManagedObject, Decodable {
  var country: String = ""

  required convenience init(from decoder: Decoder) throws {
    try self.init(from: decoder)
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.country = try container.decode(String.self,
                                        forKey: .country)
  }

  enum CodingKeys: String, CodingKey {
    case country
  }
}

I am not sure why this is compiling, the initializer is calling itself in an infinite loop. I am trying to combine Core Data with Decodable and came across this issue, not sure if this is a known issue with Swift or not.

1 Like

Interesting! This appears to be an issue with the initializer being marked as required convenience instead of just required. I can reproduce without NSManagedObject or Decodable (Swift 5.5.2):

class Foo {
    required convenience init(x: Int) {
        self.init(x: x)
    }
}

compiles with no warnings or errors. Dropping required yields a warning:

convenience init(x: Int) {
    self.init(x: x) // warning: Function call causes an infinite recursion
}

while dropping convenience yields a warning (because the initializer cannot call other initializers):

required init(x: Int) { // error: Designated initializer for 'Foo' cannot delegate (with 'self.init'); did you mean for this to be a convenience initializer?
    self.init(x: x) // note: Delegation occurs here
}

I don't know if this is a known issue (a rudimentary search didn't bring anything up, but my JIRA-searching-fu may be lacking), but seems worthy of reporting a bug on bugs.swift.org.


In the case of your specific Location subclass, you should be able to drop the convenience specifier, unless you're looking to actually delegate to another initializer; at least that will get you an error.

3 Likes

Thanks I've filed my first Swift bug now: [SR-15847] Circular required convenience init pattern not caught by compiler · Issue #58121 · apple/swift · GitHub

4 Likes

To add to this, interestingly, I'm actually seeing a warning for this when compiling with swift directly:

$ swift Test.swift
Test.swift:3:14: warning: function call causes an infinite recursion
        self.init(x: x)
             ^
$

However, in Xcode, I only see this warning when compiling in Release mode, not in Debug mode. I also don't see this error when compiling with the compiler directly:

$ swiftc Test.swift
$

I'll add this info to your bug report.

2 Likes

I wonder if for some reason DiagnoseInfiniteRecursion pass in SILOptimizer is running only in Release mode? It is a mandatory pass though, so it's supposed to run in all configurations. But that pass is where I'd start digging for a fix for this bug.

1 Like

It should be running in -Onone too swift/PassPipeline.cpp at aa0f5d5deb6f6d76d0ca5b013ecff9cba8f2516e · apple/swift · GitHub, but yeah something's gone wrong somewhere!

2 Likes

One potential explanation could be that some other pass earlier in the pipeline is misbehaving in some configurations and emits different SIL, which DiagnoseInfiniteRecursion pass no longer detects as infinite recursion? (I may be completely wrong, I'm just spitballing here)

Yeah, to be clear, I do still see this with -Onone:

$ swift -Onone Test.swift
Test.swift:3:14: warning: function call causes an infinite recursion
        self.init(x: x)
             ^

Possibly the more interesting thing to me is why I don't see it when invoking swiftc directly. But, I don't know enough about the front-end to know what it would be passing (or not) to the compiler to trigger this warning.

If you pass -Xllvm -sil-print-pass-name=true you can see the list of passes running and I can see the DiagnoseInfiniteRecursion pass running when invoking swiftc. I don't see the warning though, at least on main. On 5.0-release I do see it.

I wish we had something like manyclangs, but for Swift. That would allow one to bisect it much more easily to a specific commit. But even pinpointing to a first Swift release where it started happening would help.

That would be Swift 5.1, that's when it first regressed (but I haven't tested any point releases between 5.0 and 5.1). Looking at the output it's running the pass on the following methods:

xcrun swift-demangle s6output3FooC1xACSi_tcfC
$s6output3FooC1xACSi_tcfC ---> output.Foo.__allocating_init(x: Swift.Int) -> output.Foo

xcrun swift-demangle s6output3FooCfd
$s6output3FooCfd ---> output.Foo.deinit

xcrun swift-demangle s6output3FooCfD
$s6output3FooCfD ---> output.Foo.__deallocating_deinit

xcrun swift-demangle s6output3FooCACycfc
$s6output3FooCACycfc ---> output.Foo.init() -> output.Foo

xcrun swift-demangle s6output3FooCACycfC
$s6output3FooCACycfC ---> output.Foo.__allocating_init() -> output.Foo

Needs more investigation, but the pass does seem to be running and on the right methods. s6output3FooC1xACSi_tcfC aka output.Foo.__allocating_init(x: Swift.Int) -> output.Foo is the one that the warning is attached to.

4 Likes