Unexpected behaviour using enum with associated values

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

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?

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.

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

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

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

Interestingly indirect on the whole enum or case two(ScenePhase) fixes the bug.

1 Like