Open7

Swift パターンマッチと展開されるSIL命令の関係

kntkymtkntkymt

Swiftでなんかいい感じやってくれているSwitch(パターンマッチ)の挙動を深掘ってみる。
数パターンのSwiftとそのSIL (raw) を見比べて分岐や評価の挙動を探る。

使ったコマンドは

swiftc -emit-silgen Source.swift

SILの全体出力は文字数制限の関係でgistにあります

https://gist.github.com/kntkymt/2ff13aa5eee9eb04b7ea05c88ca35862

kntkymtkntkymt

1 単純なenum

enum Hoge {
  case a
  case b
}

let hoge = Hoge.a
switch hoge {
case .a:
    print("a")
case .b:
    print("b")
}

出力を一部抜粋

// main
// Isolation: unspecified
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s6Source4hogeAA4HogeOvp          // id: %2
  %3 = global_addr @$s6Source4hogeAA4HogeOvp : $*Hoge // users: %7, %6
  %4 = metatype $@thin Hoge.Type
  %5 = enum $Hoge, #Hoge.a!enumelt                // user: %6
  store %5 to [trivial] %3 : $*Hoge               // id: %6
  %7 = load [trivial] %3 : $*Hoge                 // user: %8
  switch_enum %7 : $Hoge, case #Hoge.a!enumelt: bb1, case #Hoge.b!enumelt: bb2 // id: %8

switch_enum という命令が使われており、docを見ると以下の記載があった。

Conditionally branches to one of several destination basic blocks based on the discriminator in a loadable enum value.
ロード可能なenumのdiscriminatorに基づき、複数のデスティネーション基本ブロックの1つに条件分岐する。

enumのdiscriminator (「どのvariantを示すか」の1バイトの整数値)を元に分岐を行う命令のらしい。
つまりパターンマッチにおいてcaseがn個書かれていても、一回の値の評価で対象のブランチを特定できるということだと思う。

kntkymtkntkymt

1.1 単純なenum + if case

switchではなくif caseのパターンマッチを用いて記述したところ、ほぼ同一のSILが得られた。
(このケースにおいては少なくとも)if caseとswitch間のパターンマッチに挙動差はなさそう。

enum Hoge {
  case a
  case b
}

let hoge = Hoge.a
if case .a = hoge {
    print("a")
} else if case .b = hoge {
    print("b")
}
// main
// Isolation: unspecified
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s6Source4hogeAA4HogeOvp          // id: %2
  %3 = global_addr @$s6Source4hogeAA4HogeOvp : $*Hoge // users: %38, %7, %6
  %4 = metatype $@thin Hoge.Type
  %5 = enum $Hoge, #Hoge.a!enumelt                // user: %6
  store %5 to [trivial] %3 : $*Hoge               // id: %6
  %7 = load [trivial] %3 : $*Hoge                 // user: %8
  switch_enum %7 : $Hoge, case #Hoge.a!enumelt: bb2, case #Hoge.b!enumelt: bb1 // id: %8
kntkymtkntkymt

2 enum以外の値

switch Int.random(in: 0..<100) {
case 0:
    print("0")
case 1:
    print("1")
default:
    print("default")
}

抜粋・rawでノイズが多いので中略

// main
// Isolation: unspecified
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  ...
  // function_ref static FixedWidthInteger.random(in:)
  %27 = function_ref @$ss17FixedWidthIntegerPsE6random2inxSnyxG_tFZ : $@convention(method) <τ_0_0 where τ_0_0 : FixedWidthInteger> (@in_guaranteed Range<τ_0_0>, @thick τ_0_0.Type) -> @out τ_0_0 // user: %28
  %28 = apply %27<Int>(%2, %25, %4) : $@convention(method) <τ_0_0 where τ_0_0 : FixedWidthInteger> (@in_guaranteed Range<τ_0_0>, @thick τ_0_0.Type) -> @out τ_0_0
  ...
  %33 = integer_literal $Builtin.IntLiteral, 0    // user: %36
  // function_ref ~= infix<A>(_:_:)
  %41 = function_ref @$ss2teoiySbx_xtSQRzlF : $@convention(thin) <τ_0_0 where τ_0_0 : Equatable> (@in_guaranteed τ_0_0, @in_guaranteed τ_0_0) -> Bool // user: %42
  %42 = apply %41<Int>(%37, %39) : $@convention(thin) <τ_0_0 where τ_0_0 : Equatable> (@in_guaranteed τ_0_0, @in_guaranteed τ_0_0) -> Bool // user: %45
  ...
  %45 = struct_extract %42 : $Bool, #Bool._value  // user: %46
  cond_br %45, bb1, bb2                           // id: %46

bb1:                                              // Preds: bb0
  ...
  %69 = function_ref @$ss5print_9separator10terminatoryypd_S2StF : $@convention(thin) (@guaranteed Array<Any>, @guaranteed String, @guaranteed String) -> () // user: %70
  %70 = apply %69(%64, %66, %68) : $@convention(thin) (@guaranteed Array<Any>, @guaranteed String, @guaranteed String) -> ()
  ...

bb2:                                              // Preds: bb0
  extend_lifetime %32 : $Int                      // id: %76
  br bb3                                          // id: %77

bb3:                                              // Preds: bb2
  ...
  // function_ref ~= infix<A>(_:_:)
  %87 = function_ref @$ss2teoiySbx_xtSQRzlF : $@convention(thin) <τ_0_0 where τ_0_0 : Equatable> (@in_guaranteed τ_0_0, @in_guaranteed τ_0_0) -> Bool // user: %88
  %88 = apply %87<Int>(%83, %85) : $@convention(thin) <τ_0_0 where τ_0_0 : Equatable> (@in_guaranteed τ_0_0, @in_guaranteed τ_0_0) -> Bool // user: %91
  dealloc_stack %85 : $*Int                       // id: %89
  dealloc_stack %83 : $*Int                       // id: %90
  %91 = struct_extract %88 : $Bool, #Bool._value  // user: %92
  cond_br %91, bb4, bb5                           // id: %92

ざっくりまとめると

  • bb0: Int.randomの結果と0~= infix<A>(_:_:)にて評価しbb1 (true) or bb2 (false)へ分岐
    • bb1: print("0")
    • bb2: bb3へ
  • bb3: Int.randomの結果と1~= infix<A>(_:_:)にて評価しbb4 (true) or bb5 (false)へ分岐
    • bb4: print("1")
    • bb5: bb6へ
  • bb6: print("default")

要はswitch_enumのような1回のみの評価ではなく、caseのパターンマッチを上から順に~=演算子にて評価を行い、対象のブランチを探している。
つまりenumではない場合、パターンマッチングは(意味的には)「if-else if + ~=に書き下しているだけ」と捉えても良さそうか?

kntkymtkntkymt

3 associated valueのあるenum

enum Hoge {
  case a(String)
  case b
}

let hoge = Hoge.a("a")
switch hoge {
case .a("empty"):
    print("empty a")
case .a:
    print("a")
case .b:
    print("b")
}

一部抜粋

// main
// Isolation: unspecified
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s6Source4hogeAA4HogeOvp          // id: %2
  %3 = global_addr @$s6Source4hogeAA4HogeOvp : $*Hoge // users: %13, %12
  %4 = metatype $@thin Hoge.Type
  %5 = string_literal utf8 "a"                    // user: %10
  %6 = integer_literal $Builtin.Word, 1           // user: %10
  %7 = integer_literal $Builtin.Int1, -1          // user: %10
  %8 = metatype $@thin String.Type                // user: %10
  // function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
  %9 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %10
  %10 = apply %9(%5, %6, %7, %8) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %11
  %11 = enum $Hoge, #Hoge.a!enumelt, %10 : $String // user: %12
  store %11 to [init] %3 : $*Hoge                 // id: %12
  %13 = load_borrow %3 : $*Hoge                   // users: %123, %95, %65, %14
  switch_enum %13 : $Hoge, case #Hoge.a!enumelt: bb1, case #Hoge.b!enumelt: bb5 // id: %14

// %15                                            // user: %16
bb1(%15 : @guaranteed $String):                   // Preds: bb0
  %16 = copy_value %15 : $String                  // user: %17
  %17 = move_value [var_decl] %16 : $String       // users: %38, %67, %26
  %18 = string_literal utf8 "empty"               // user: %23
  %19 = integer_literal $Builtin.Word, 5          // user: %23
  %20 = integer_literal $Builtin.Int1, -1         // user: %23
  %21 = metatype $@thin String.Type               // user: %23
  // function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
  %22 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %23
  %23 = apply %22(%18, %19, %20, %21) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %25
  %24 = alloc_stack $String                       // users: %35, %34, %30, %25
  store %23 to [init] %24 : $*String              // id: %25
  %26 = begin_borrow %17 : $String                // users: %33, %28
  %27 = alloc_stack $String                       // users: %32, %28
  %28 = store_borrow %26 to %27 : $*String        // users: %31, %30
  // function_ref ~= infix<A>(_:_:)
  %29 = function_ref @$ss2teoiySbx_xtSQRzlF : $@convention(thin) <τ_0_0 where τ_0_0 : Equatable> (@in_guaranteed τ_0_0, @in_guaranteed τ_0_0) -> Bool // user: %30
  %30 = apply %29<String>(%24, %28) : $@convention(thin) <τ_0_0 where τ_0_0 : Equatable> (@in_guaranteed τ_0_0, @in_guaranteed τ_0_0) -> Bool // user: %36
  end_borrow %28 : $*String                       // id: %31
  dealloc_stack %27 : $*String                    // id: %32
  end_borrow %26 : $String                        // id: %33
  destroy_addr %24 : $*String                     // id: %34
  dealloc_stack %24 : $*String                    // id: %35
  %36 = struct_extract %30 : $Bool, #Bool._value  // user: %37
  cond_br %36, bb2, bb3                           // id: %37

1のケースと2のケースを複合したアプローチになっており
まず、bb0にて1のケースと同様にswitch_enumで.a.bかの分岐をしている。
その後、bb1にて2のケースのようにassociated valueを~=演算子用いて評価することで.a.a("empty")かの分岐をしている。

kntkymtkntkymt

4 Bool

switch Bool.random() {
case true:
    print("true")
case false:
    print("false")
}

一部抜粋

// main
// Isolation: unspecified
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  %2 = metatype $@thin Bool.Type                  // user: %4
  // function_ref static Bool.random()
  %3 = function_ref @$sSb6randomSbyFZ : $@convention(method) (@thin Bool.Type) -> Bool // user: %4
  %4 = apply %3(%2) : $@convention(method) (@thin Bool.Type) -> Bool // user: %7
  %5 = integer_literal $Builtin.Int1, -1          // user: %8
  %6 = integer_literal $Builtin.Int1, 0           // user: %8
  %7 = struct_extract %4 : $Bool, #Bool._value    // user: %8
  switch_value %7 : $Builtin.Int1, case %5: bb1, case %6: bb2 // id: %8

switch_enumではなくswitch_valueなる命令を用いている。

Conditionally branches to one of several destination basic blocks based on a value of builtin integer. If the operand value matches one of the case values of the instruction, control is transferred to the corresponding basic block. I
組み込み整数の値に基づいて、複数の宛先基本ブロックのいずれかに条件分岐する。オペランド値が命令のケース値のいずれかと一致する場合、制御は対応する基本ブロックに移行する。

挙動的にはswitch_enumと同様に一回の比較で対象のブランチを特定できるように読める。

(疑問)Bool以外でswitch_valueが使われるケースはあるのだろうか、理論的には2のケースのIntも(この例に限っては)switch_valueに最適化できそうだが...

kntkymtkntkymt

まとめ

出力されるSILから挙動を推測するとSwiftのパターンマッチは

  • 対象がenumの場合、switch_enumという命令によってenumのdiscriminator (「どのvariantを示すか」の1バイトの整数値)を元に分岐を行うため、パターンマッチにcaseがn個書かれていても、一回の値の評価で対象のブランチを特定できる。
    • associated valueを持つenumの場合、switch_enumでenumのvariantを絞り込んだ後、別途associated valueのパターンマッチを行う。
  • 対象がBool型の場合、switch_valueという命令によって (Boolの裏にあるInt1) 整数値を元に分岐を行うため、一回の値の評価で対象のブランチを特定できる。
    • Bool以外でswitch_valueが使われているかは不明
  • 対象がintやその他の型の場合、パターンマッチのcase毎に~=逐次演算子による比較が行われる。上に記述されたcaseから順に実行され、最初にマッチしたブランチに分岐する。
  • switchをif-caseのパターンマッチに置き換えたところswitch_enum, switch_valueの有無の差は確認できなかった。

これ以上SILレベルから挙動を推測するには限界がありそうなので、コンパイラのコードを読んで答え合わせをするのが良さそう、それっぽいSemaのコードは見つけたのでまたの機会に読んでみる

https://github.com/swiftlang/swift/blob/3d6c240aa61eaa3ec2345d0fb5c3ba0ff467f742/lib/Sema/TypeCheckPattern.cpp#L1367-L1381