I am experiencing some surprising behaviour with an enum containing multiple associated values. The following snippet reproduces the problem:
import SwiftUI
enum Foo {
case one
case two(ScenePhase)
case three
case four(Bool)
case five
case six
case seven
}
print(Foo.seven) // prints "one"
The compiler seems to be emitting one in place of seven and this is causing unexpected failures (using Xcode 13.2.1 and 13.1).
I have also observed that the issue goes away when:
- there are six cases only and not seven
- the
ScenePhase payload is replaced with a different type such as String
- the
Bool payload is removed so there is only a single associated type of ScenePhase
Wondering if this is a compiler bug, or is the definition somehow not well formed?
4 Likes
mattcurtis
(Matt Curtis)
2
This is weird. From what I can tell the only thing being disrupted is how the description for case seven is generated. Also seems unique to ScenePhase as you mentioned. But otherwise pattern matching works as expected:
import SwiftUI
enum Foo {
case one
case two(ScenePhase)
case three
case four(Bool)
case five
case six
case seven
}
switch Foo.seven {
case .one: print("is one")
case .seven: print("is seven") // this is what is executed
default: print("is something else")
}
Hmmmm, might be related to how Swift lays out and pads enums with associated values in memory. Could you give is the different MemoryLayout properties of ScenePhase and your enum Foo?
xwu
(Xiaodi Wu)
4
This is pretty remarkable! Please file a bug over at bugs.swift.org. I can even add another case and have this produce additional bogus results (Xcode 13.2):
import SwiftUI
enum Foo {
case one
case two(ScenePhase)
case three
case four(Bool)
case five
case six
case seven
case eight
}
print(Foo.eight, Foo.seven, Foo.six) // "three one six"
7 Likes
Type Foo has a size of 2 bytes (stride of 2, alignment of 1) and ScenePhase has a size of 1 byte, (stride of 1, alignment of 1).
I tried to reproduce with a custom type instead of ScenePhase that has the shape (i.e. ScenePhase appears to be an enum with 3 cases, conforming to Comparable and Hashable). However, it only triggers with ScenePhase.
Thanks for verifying.
In the code base where this is triggered, pattern matching is impacted. The value of .seven is used at the call-site but the value .one instead is propagated from the call site through a deep chain of function calls to a function that then matches against one producing unexpected results. I can see that the first function on the call stack immediately receives one instead of seven.
Karl
(👑🦆)
7
At a glance, it looks like this occurs for non-frozen resilient enums.
Text.TruncationMode is another example:
enum Foo {
case one
case two(Text.TruncationMode) // <---
case three
case four(Bool)
case five
case six
case seven
}
print(Foo.seven) // "one"
But if you make your own, in-module enum, everything is fine:
enum MyScenePhase : Comparable {
case background
case inactive
case active
}
enum Foo {
case one
case two(MyScenePhase)
case three
case four(Bool)
case five
case six
case seven
}
print(Foo.seven) // "seven"
5 Likes
I opened an issue in feedback assistant, and for now am working around by dropping one of the cases. Thanks for the sanity check, it appears that I was just really unlucky in my specific use case.
Thanks for the insight. This leads to a better work around where I can just map ScenePhase to my own version, and preserve the original semantics I was aiming for.
1 Like
Nevin
10
Some, but not all of them.
Among standard-library non-frozen enums, I confirm the bug for these:
Mirror.DisplayStyle
Unicode.GeneralCategory
Unicode.NumericType
But not for these:
DecodingError
EncodingError
Mirror.AncestorRepresentation
2 Likes
Nevin
11
As a compiler bug, this should be reported in the location @xwu specified: bugs.swift.org.
Worth mentioning in the bug report that it affects standard-library enums.
4 Likes
tera
12
Interestingly indirect on the whole enum or case two(ScenePhase) fixes the bug.
1 Like