Enum case value must be literal: is there a macro that does literal expression evaluation and fill in the case value?

enum Direction: Double, CaseIteratable {
  case north = #Eval(0.0 - 45)     or #Eval(Angle.degrees(0.0 - 45).radians)
  case east = #Eval(90.0 - 45)
  case north = #Eval(180.0 - 45)
  case north = #Eval(360.0 - 45)
}

Edit: it appears it's not possible to place anything but a real literal value for switch case value. Putting an expression macro there, even though the macro works standalone, the compiler error out with "Raw value for enum case must be a literal". Why the compiler so eager to reject this code, why not expand the macro first?

From the beginning of Swift would be nice

1 Like

Hmm, I didn't realise you couldn't use trivial expressions (that are compile-time deterministic). I guess I'm not surprised per se, given how Swift feels in general about 'const' expressions. Still, this feels like something best solved (long-term) with language improvements, not macros.

1 Like

i wouldn’t make the raw values the radians, i would make them a computed property of the enum.

Computed prop is how I did it. I was just looking for ways to use static expression for case rawValue.

I concur. FWIW C could do this out of the box:

enum X {
    x = 1+2
}

A big difference is that in swift they are not trivial - addition is a function call like any other. There's nothing preventing me from writing

extension Double {
    static func -(lhs: Double, rhs: Double) -> Double {
        Double.random(in: 0...1)
    }
}
2 Likes

You could but obviously such overrides would not be taken into account during the calculation of enumeration value that happens at compile time.

If such overrides cannot be used at compile time, then the compiler must reject the expression: it would not be obvious at all if instead it just substituted some other disfavored overload.

2 Likes

Note that compiler already rejects this code when "func -" is defined as a standalone function:

func -(lhs: Double, rhs: Double) -> Double {
    Double.random(in: 0...1)
}

1.0 - 2.0 // 🛑 Error: Ambiguous use of operator '-'

Whether that the compiler does not reject this code (or complain otherwise) when "func -" is defined as a static function defined in an extension of Double is a bug or a feature – I don't know.

2 Likes

Presumably a feature, right? You can override methods (including static ones) on types from other modules (although the override is only visible in your current module, I believe?).

1 Like

Should the following be rejected (it's currently allowed)?

extension Double {
    static func == (lhs: Self, rhs: Self) -> Bool { true }
}

enum TestEnum: Double {
    case x = 0.0, y = 1.0
}

No, it should not be rejected. You're shadowing == with an unrelated static function that shares the same name, not changing the conformance of Double to Equatable. Swift doesn't offer you any way to make 0.0 equivalent to 1.0:

extension Double {
    static func == (lhs: Self, rhs: Self) -> Bool { true }
}
func areEqual<T: Equatable>(_ a: T, _ b: T) -> Bool { a == b }
areEqual(0.0, 1.0) // false
3 Likes

hmm...

print(0.0 == 1.0) // true

That's right. This is called shadowing.

2 Likes

Perhaps I don't understand something basic here IRT shadowing. Given the code:

let x: UInt8 = 255 + 1 // 🛑 Error: Arithmetic operation '255 + 1' (on type 'UInt8') results in an overflow

I can "fix" it (make it compilable) by providing this shadowing "operator +":

extension UInt8 {
    static func + (lhs: Self, rhs: Self) -> Self { 0 }
}

It doesn't matter what I am returning from it (it could be a fatalError("TODO") for that matter) as this code is not executed at compile time, just the compiler notices I have this shadowed and as a result allows the "let x: UInt8 = 255 + 1" expression to compile. However this line:

let y: UInt8 = 256 // 🛑 Error: Integer literal '256' overflows when stored into 'UInt8'

I can not "fix" by providing this shadowing initialiser:

extension UInt8 {
    init(integerLiteral value: Int) {
        self = 0
    }
}

What strikes me odd is that I can make the code compilable in one instance but not another. Which behaviour is correct? Or are they both correct, somehow?

1 Like

They are both correct. Again, shadowing means that the implementation being shadowed is still there: you cannot change the way in which UInt8 conforms to ExpressibleByIntegerLiteral; you have only created another function that happens to share the same name, and thus will be invoked preferentially when you write exactly that function call.

In this case, the integer literal 256 is being used to create a value of type UInt8 using the implementation required by the protocol conformance, which overflows. You can call your shadowing implementation by writing UInt8(integerLiteral: 256) and the compiler won't complain. Remove your implementation and the compiler will complain again.

8 Likes

Thank you, now I see what you mean. Two further notes:

  1. I don't see method shadowing being mentioned anywhere in the TSPL, is it there and I just missed it?
  2. this method shadowing feature is on the edge of being a foot-gun: one would assume that, say, EQ was "overridden" entirely, whilst it is merely shadowed. A more honest approach would be to give a compilation error during an attempt to shadow: "you can't do that".
2 Likes

This wasn’t added to the language because it means upstream changes are more likely to break downstream code. Of course, it only goes so far, since conformances can’t shadow the same way, and since someone even further downstream could still be broken.

6 Likes

Does it work well though? Say I have:

infix operator ** : MultiplicationPrecedence

func **(lhs: Int, rhs: Int) -> Int {
    lhs * rhs * 2 // or whatever
}

Then at some point standard library adds its own operator **:

extension Int {
    static func **(lhs: Self, rhs: Self) -> Self {
        // something here
    }
}

and my existing code fails to compile with:

1 ** 2 // 🛑 Error: Ambiguous use of operator '**'

the same way the following code fails to compile today:

func -(lhs: Double, rhs: Double) -> Double {
    Double.random(in: 0...1)
}

1.0 - 2.0 // 🛑 Error: Ambiguous use of operator '-'

Having said that, at least when my code stops to compile I am aware of the issue and can fix it. This is different to a silent and potentially serious foot-gun I'm getting either by (1) incorrectly writing a method implementation which is in fact "shadowing", or (2) by writing a method implementation that is not shadowing today and that only becomes "shadowing" retrospectively because of the future change in the compiler / standard library.

1 Like

Is this the same pitfall of the extending type you don’t own with protocol you don’t own?