What's the harm in not knowing that a switch statement's pattern-matching order is undefined?

I just read this in “The Swift Programming Language”: (5.2 beta, p 1005)

Although the actual execution order of pattern-matching operations, and in particular the evaluation order of patterns in cases, is unspecified, pattern matching in a switch statement behaves as if the evaluation is performed in source order—that is, the order in which they appear in source code. As a result, if multiple cases contain patterns that evaluate to the same value, and thus can match the value of the control expression, the program executes only the code within the first matching case in source order.

Is it possible to get ‘something unexpected’ by assuming the ‘evaluation order of patterns’ is performed in source order?

In other words, can a pattern-match have a side-effect, and can the Nth case's pattern match or statements could be affected by the (N+m)th's pattern-match side-effect?

2 Likes

When you use an expression in a case label, Swift will evaluate that expression to get the value to match against. If that evaluation has side effects, they may be visible during the evaluation of other cases. For instance, if you had:

switch () {
case print("foo"): break
case print("bar"): break
}

The compiler could in theory print "bar" before "foo".

3 Likes

For a concrete example of how evaluation order might be different from source order, try this:

enum Foo {
  case a(Int)
  case b(Int)
}

func printAndReturn(_ x: Int) -> Int {
  print(x)
  return(x)
}

let x = Foo.b(2)
switch x {
  case .a(printAndReturn(0)): break
  case .a(printAndReturn(1)): break
  case .b(printAndReturn(2)): break
  case .b(printAndReturn(3)): break
  default: break
}

The printAndReturn(0) and printAndReturn(1) calls inside the .a branches will not execute, because the compiler generates an optimized decision tree where the .a and .b cases are partitioned first before trying to evaluate any subpatterns.

8 Likes

If you're referring to the e-book version, note that page number changes depending on the screen size. It'd be better to use chapter number & section title.

2 Likes

Thanks for the example, Joe. I hadn't imagined calling a method in the pattern-match expression, I normally use constants.

I extended your example slightly to drive the point home, it shows how the compiled code is first checking for .a vs .b, then doing a pared-down comparision.

enum Foo {
    case a(Int)
    case b(Int)
}

func printAndReturn(_ x: Int) -> Int {
    print("pr: \(x)")
    return(x)
}

for i in 0...4 {
    let a_and_b = [ Foo.a(i), Foo.b(i) ]
    for x in a_and_b {
        print("------ \(x) -----")
        switch x {
        case .a(printAndReturn(0)): print ("case 0"); break
        case .a(printAndReturn(1)): print ("case 1"); break
        case .b(printAndReturn(2)): print ("case 2"); break
        case .b(printAndReturn(3)): print ("case 3"); break
        default: print ("case default"); break
        }
    }
}

and got

pr: 0
case 0
------ b(0) -----
pr: 2
pr: 3
case default
------ a(1) -----
pr: 0
pr: 1
case 1
------ b(1) -----
pr: 2
pr: 3
case default
------ a(2) -----
pr: 0
pr: 1
case default
------ b(2) -----
pr: 2
case 2
------ a(3) -----
pr: 0
pr: 1
case default
------ b(3) -----
pr: 2
pr: 3
case 3
------ a(4) -----
pr: 0
pr: 1
case default
------ b(4) -----
pr: 2
pr: 3
case default