Edited at

-emit-silgenで見るSwift4.1からのenumとEquatable

※バージョンが間違えてたので修正・・・すいません


EquatableがSwift4.1から使いやすくなりました 🎉🎉🎉🎉

みなさんご存知の通り、Swift4.1からEquatableが使いやすくなりました。

Swift4までは、Equatableプロトコルに準拠するときは、以下のようにstatic public func ==(lhs:rhs:) -> Boolを自分で書き上げないといけませんでしたね。

enum Nyan: Equatable {

case hoge
case fuga

static public func ==(lhs: Nyan, rhs: Nyan) -> Bool {
switch (lhs, rhs) {
case (.hoge, .hoge), (.fuga, .fuga):
return true
default:
return false
}
}
}

もしメンバ変数やcaseの数が膨大になれば、switch文中の比較はその分必要になりますので、当然コードの量も膨大になります。

値付きのcase(e.g. case hoge(Int))になると、中の値まで比較しないといけなくなりますね・・・。

でも大丈夫!Swift4.1から、static public func == (lhs:rhs:) -> Boolを自分で書かなくて良くなりました。

わーい!コード量が減るよ!劇的BeforeAfterだね!

enum Nyan: Equatable { // Equatableに準拠しますよ〜ということだけを言えばいい

case hoge
case fuga
}


・・・なんで書かないでよくなったの?

さて、このときですがNyanでは何が起こっているのでしょうか?:thinking::thinking::thinking:

今まで書くべきだったstatic public func ==(lhs:rhs:) -> Boolは果たしてどこに?!

では、以下のコードをつかって、Swift4.1からの新しいEquatableを覗いてみましょう!

enum Nyan: Equatable {

case hoge
case fuga
}


enumによるEquatableを、-emit-silgenで覗いてみる

こういうときはSILを見るのがいいかもしれません。

では、enumによるEquatableを、早速-emit-silgenでraw SILを生成して覗いてみましょう!

swiftc -emit-silgen nyan.swift

で、出力されたraw SILがこちらです!(この時点でraw SILは読み流して結構です)

sil_stage raw

import Builtin
import Swift
import SwiftShims

enum Nyan : Equatable {
case hoge
case fuga
var hashValue: Int { get }
func hash(into hasher: inout Hasher)
@_implements(Equatable, ==(_:_:)) static func __derived_enum_equals(_ a: Nyan, _ b: Nyan) -> Bool
}

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
%2 = integer_literal $Builtin.Int32, 0 // user: %3
%3 = struct $Int32 (%2 : $Builtin.Int32) // user: %4
return %3 : $Int32 // id: %4
} // end sil function 'main'

// Nyan.hashValue.getter
sil hidden @$S1A4NyanO9hashValueSivg : $@convention(method) (Nyan) -> Int {
// %0 // users: %3, %1
bb0(%0 : $Nyan):
debug_value %0 : $Nyan, let, name "self", argno 1 // id: %1
%2 = alloc_stack $Nyan // users: %6, %5, %3
store %0 to [trivial] %2 : $*Nyan // id: %3
// function_ref _hashValue<A>(for:)
%4 = function_ref @$Ss10_hashValue3forSix_tSHRzlF : $@convention(thin) <τ_0_0 where τ_0_0 : Hashable> (@in_guaranteed τ_0_0) -> Int // user: %5
%5 = apply %4<Nyan>(%2) : $@convention(thin) <τ_0_0 where τ_0_0 : Hashable> (@in_guaranteed τ_0_0) -> Int // user: %7
dealloc_stack %2 : $*Nyan // id: %6
return %5 : $Int // id: %7
} // end sil function '$S1A4NyanO9hashValueSivg'

// _hashValue<A>(for:)
sil [serialized] [always_inline] @$Ss10_hashValue3forSix_tSHRzlF : $@convention(thin) <τ_0_0 where τ_0_0 : Hashable> (@in_guaranteed τ_0_0) -> Int

// Nyan.hash(into:)
sil hidden @$S1A4NyanO4hash4intoys6HasherVz_tF : $@convention(method) (@inout Hasher, Nyan) -> () {
// %0 // users: %29, %2
// %1 // users: %7, %3
bb0(%0 : $*Hasher, %1 : $Nyan):
debug_value_addr %0 : $*Hasher, var, name "hasher", argno 1 // id: %2
debug_value %1 : $Nyan, let, name "self", argno 2 // id: %3
%4 = alloc_box ${ var Int }, var, name "discriminator" // user: %5
%5 = mark_uninitialized [var] %4 : ${ var Int } // users: %34, %6
%6 = project_box %5 : ${ var Int }, 0 // users: %24, %20, %12
switch_enum %1 : $Nyan, case #Nyan.hoge!enumelt: bb1, case #Nyan.fuga!enumelt: bb2 // id: %7

bb1: // Preds: bb0
%8 = metatype $@thin Int.Type // user: %11
%9 = integer_literal $Builtin.Int2048, 0 // user: %11
// function_ref Int.init(_builtinIntegerLiteral:)
%10 = function_ref @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %11
%11 = apply %10(%9, %8) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %13
%12 = begin_access [modify] [unknown] %6 : $*Int // users: %14, %13
assign %11 to %12 : $*Int // id: %13
end_access %12 : $*Int // id: %14
br bb3 // id: %15

bb2: // Preds: bb0
%16 = metatype $@thin Int.Type // user: %19
%17 = integer_literal $Builtin.Int2048, 1 // user: %19
// function_ref Int.init(_builtinIntegerLiteral:)
%18 = function_ref @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %19
%19 = apply %18(%17, %16) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %21
%20 = begin_access [modify] [unknown] %6 : $*Int // users: %22, %21
assign %19 to %20 : $*Int // id: %21
end_access %20 : $*Int // id: %22
br bb3 // id: %23

bb3: // Preds: bb2 bb1
%24 = begin_access [read] [unknown] %6 : $*Int // users: %26, %25
%25 = load [trivial] %24 : $*Int // user: %28
end_access %24 : $*Int // id: %26
%27 = alloc_stack $Int // users: %33, %31, %28
store %25 to [trivial] %27 : $*Int // id: %28
%29 = begin_access [modify] [unknown] %0 : $*Hasher // users: %32, %31
// function_ref Hasher.combine<A>(_:)
%30 = function_ref @$Ss6HasherV7combineyyxSHRzlF : $@convention(method) <τ_0_0 where τ_0_0 : Hashable> (@in_guaranteed τ_0_0, @inout Hasher) -> () // user: %31
%31 = apply %30<Int>(%27, %29) : $@convention(method) <τ_0_0 where τ_0_0 : Hashable> (@in_guaranteed τ_0_0, @inout Hasher) -> ()
end_access %29 : $*Hasher // id: %32
dealloc_stack %27 : $*Int // id: %33
destroy_value %5 : ${ var Int } // id: %34
%35 = tuple () // user: %36
return %35 : $() // id: %36
} // end sil function '$S1A4NyanO4hash4intoys6HasherVz_tF'

// Int.init(_builtinIntegerLiteral:)
sil [transparent] [serialized] @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int

// Hasher.combine<A>(_:)
sil [serialized] [always_inline] @$Ss6HasherV7combineyyxSHRzlF : $@convention(method) <τ_0_0 where τ_0_0 : Hashable> (@in_guaranteed τ_0_0, @inout Hasher) -> ()

// static Nyan.__derived_enum_equals(_:_:)
sil hidden @$S1A4NyanO21__derived_enum_equalsySbAC_ACtFZ : $@convention(method) (Nyan, Nyan, @thin Nyan.Type) -> Bool {
// %0 // users: %9, %3
// %1 // users: %29, %4
// %2 // user: %5
bb0(%0 : $Nyan, %1 : $Nyan, %2 : $@thin Nyan.Type):
debug_value %0 : $Nyan, let, name "a", argno 1 // id: %3
debug_value %1 : $Nyan, let, name "b", argno 2 // id: %4
debug_value %2 : $@thin Nyan.Type, let, name "self", argno 3 // id: %5
%6 = alloc_box ${ var Int }, var, name "index_a" // user: %7
%7 = mark_uninitialized [var] %6 : ${ var Int } // users: %56, %8
%8 = project_box %7 : ${ var Int }, 0 // users: %47, %22, %14
switch_enum %0 : $Nyan, case #Nyan.hoge!enumelt: bb1, case #Nyan.fuga!enumelt: bb2 // id: %9

bb1: // Preds: bb0
%10 = metatype $@thin Int.Type // user: %13
%11 = integer_literal $Builtin.Int2048, 0 // user: %13
// function_ref Int.init(_builtinIntegerLiteral:)
%12 = function_ref @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %13
%13 = apply %12(%11, %10) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %15
%14 = begin_access [modify] [unknown] %8 : $*Int // users: %16, %15
assign %13 to %14 : $*Int // id: %15
end_access %14 : $*Int // id: %16
br bb3 // id: %17

bb2: // Preds: bb0
%18 = metatype $@thin Int.Type // user: %21
%19 = integer_literal $Builtin.Int2048, 1 // user: %21
// function_ref Int.init(_builtinIntegerLiteral:)
%20 = function_ref @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %21
%21 = apply %20(%19, %18) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %23
%22 = begin_access [modify] [unknown] %8 : $*Int // users: %24, %23
assign %21 to %22 : $*Int // id: %23
end_access %22 : $*Int // id: %24
br bb3 // id: %25

bb3: // Preds: bb2 bb1
%26 = alloc_box ${ var Int }, var, name "index_b" // user: %27
%27 = mark_uninitialized [var] %26 : ${ var Int } // users: %55, %28
%28 = project_box %27 : ${ var Int }, 0 // users: %50, %42, %34
switch_enum %1 : $Nyan, case #Nyan.hoge!enumelt: bb4, case #Nyan.fuga!enumelt: bb5 // id: %29

bb4: // Preds: bb3
%30 = metatype $@thin Int.Type // user: %33
%31 = integer_literal $Builtin.Int2048, 0 // user: %33
// function_ref Int.init(_builtinIntegerLiteral:)
%32 = function_ref @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %33
%33 = apply %32(%31, %30) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %35
%34 = begin_access [modify] [unknown] %28 : $*Int // users: %36, %35
assign %33 to %34 : $*Int // id: %35
end_access %34 : $*Int // id: %36
br bb6 // id: %37

bb5: // Preds: bb3
%38 = metatype $@thin Int.Type // user: %41
%39 = integer_literal $Builtin.Int2048, 1 // user: %41
// function_ref Int.init(_builtinIntegerLiteral:)
%40 = function_ref @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %41
%41 = apply %40(%39, %38) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %43
%42 = begin_access [modify] [unknown] %28 : $*Int // users: %44, %43
assign %41 to %42 : $*Int // id: %43
end_access %42 : $*Int // id: %44
br bb6 // id: %45

bb6: // Preds: bb5 bb4
%46 = metatype $@thin Int.Type // user: %54
%47 = begin_access [read] [unknown] %8 : $*Int // users: %49, %48
%48 = load [trivial] %47 : $*Int // user: %54
end_access %47 : $*Int // id: %49
%50 = begin_access [read] [unknown] %28 : $*Int // users: %52, %51
%51 = load [trivial] %50 : $*Int // user: %54
end_access %50 : $*Int // id: %52
// function_ref static Int.== infix(_:_:)
%53 = function_ref @$SSi2eeoiySbSi_SitFZ : $@convention(method) (Int, Int, @thin Int.Type) -> Bool // user: %54
%54 = apply %53(%48, %51, %46) : $@convention(method) (Int, Int, @thin Int.Type) -> Bool // user: %57
destroy_value %27 : ${ var Int } // id: %55
destroy_value %7 : ${ var Int } // id: %56
return %54 : $Bool // id: %57
} // end sil function '$S1A4NyanO21__derived_enum_equalsySbAC_ACtFZ'

// static Int.== infix(_:_:)
sil [transparent] [serialized] @$SSi2eeoiySbSi_SitFZ : $@convention(method) (Int, Int, @thin Int.Type) -> Bool

// protocol witness for Hashable.hashValue.getter in conformance Nyan
sil private [transparent] [thunk] @$S1A4NyanOSHAASH9hashValueSivgTW : $@convention(witness_method: Hashable) (@in_guaranteed Nyan) -> Int {
// %0 // user: %1
bb0(%0 : $*Nyan):
%1 = load [trivial] %0 : $*Nyan // user: %3
// function_ref Nyan.hashValue.getter
%2 = function_ref @$S1A4NyanO9hashValueSivg : $@convention(method) (Nyan) -> Int // user: %3
%3 = apply %2(%1) : $@convention(method) (Nyan) -> Int // user: %4
return %3 : $Int // id: %4
} // end sil function '$S1A4NyanOSHAASH9hashValueSivgTW'

// protocol witness for Hashable.hash(into:) in conformance Nyan
sil private [transparent] [thunk] @$S1A4NyanOSHAASH4hash4intoys6HasherVz_tFTW : $@convention(witness_method: Hashable) (@inout Hasher, @in_guaranteed Nyan) -> () {
// %0 // user: %4
// %1 // user: %2
bb0(%0 : $*Hasher, %1 : $*Nyan):
%2 = load [trivial] %1 : $*Nyan // user: %4
// function_ref Nyan.hash(into:)
%3 = function_ref @$S1A4NyanO4hash4intoys6HasherVz_tF : $@convention(method) (@inout Hasher, Nyan) -> () // user: %4
%4 = apply %3(%0, %2) : $@convention(method) (@inout Hasher, Nyan) -> ()
%5 = tuple () // user: %6
return %5 : $() // id: %6
} // end sil function '$S1A4NyanOSHAASH4hash4intoys6HasherVz_tFTW'

// protocol witness for static Equatable.== infix(_:_:) in conformance Nyan
sil private [transparent] [thunk] @$S1A4NyanOSQAASQ2eeoiySbx_xtFZTW : $@convention(witness_method: Equatable) (@in_guaranteed Nyan, @in_guaranteed Nyan, @thick Nyan.Type) -> Bool {
// %0 // user: %3
// %1 // user: %4
bb0(%0 : $*Nyan, %1 : $*Nyan, %2 : $@thick Nyan.Type):
%3 = load [trivial] %0 : $*Nyan // user: %7
%4 = load [trivial] %1 : $*Nyan // user: %7
%5 = metatype $@thin Nyan.Type // user: %7
// function_ref static Nyan.__derived_enum_equals(_:_:)
%6 = function_ref @$S1A4NyanO21__derived_enum_equalsySbAC_ACtFZ : $@convention(method) (Nyan, Nyan, @thin Nyan.Type) -> Bool // user: %7
%7 = apply %6(%3, %4, %5) : $@convention(method) (Nyan, Nyan, @thin Nyan.Type) -> Bool // user: %8
return %7 : $Bool // id: %8
} // end sil function '$S1A4NyanOSQAASQ2eeoiySbx_xtFZTW'

sil_witness_table hidden Nyan: Hashable module A {
base_protocol Equatable: Nyan: Equatable module A
method #Hashable.hashValue!getter.1: <Self where Self : Hashable> (Self) -> () -> Int : @$S1A4NyanOSHAASH9hashValueSivgTW // protocol witness for Hashable.hashValue.getter in conformance Nyan
method #Hashable.hash!1: <Self where Self : Hashable> (Self) -> (inout Hasher) -> () : @$S1A4NyanOSHAASH4hash4intoys6HasherVz_tFTW // protocol witness for Hashable.hash(into:) in conformance Nyan
}

sil_witness_table hidden Nyan: Equatable module A {
method #Equatable."=="!1: <Self where Self : Equatable> (Self.Type) -> (Self, Self) -> Bool : @$S1A4NyanOSQAASQ2eeoiySbx_xtFZTW // protocol witness for static Equatable.== infix(_:_:) in conformance Nyan
}

むむ!なにやらたくさんソースがあるぞ!では、随所随所抜粋して、読んで考えていきましょう。

まず、もとのenumっぽいソースがありますね。抜粋してみます!

enum Nyan : Equatable {

case hoge
case fuga
var hashValue: Int { get }
func hash(into hasher: inout Hasher)
@_implements(Equatable, ==(_:_:)) static func __derived_enum_equals(_ a: Nyan, _ b: Nyan) -> Bool
}

この中に、今まで実装していたstatic func == (lhs:rhs:)と同じと思われる@_implements(Equatable, ==(_:_:)) static func __derived_enum_equals(_ a: Nyan, _ b: Nyan) -> Boolがありました。

では、SILをもうちょっと覗いてみます。最後の方に、Nyan型の引数を2つとってBoolを返す関数のように見えるコードが見えました!

// protocol witness for static Equatable.== infix(_:_:) in conformance Nyan

sil private [transparent] [thunk] @$S1A4NyanOSQAASQ2eeoiySbx_xtFZTW : $@convention(witness_method: Equatable) (@in_guaranteed Nyan, @in_guaranteed Nyan, @thick Nyan.Type) -> Bool {
// %0 // user: %3
// %1 // user: %4
bb0(%0 : $*Nyan, %1 : $*Nyan, %2 : $@thick Nyan.Type):
%3 = load [trivial] %0 : $*Nyan // user: %7
%4 = load [trivial] %1 : $*Nyan // user: %7
%5 = metatype $@thin Nyan.Type // user: %7
// function_ref static Nyan.__derived_enum_equals(_:_:)
%6 = function_ref @$S1A4NyanO21__derived_enum_equalsySbAC_ACtFZ : $@convention(method) (Nyan, Nyan, @thin Nyan.Type) -> Bool // user: %7
%7 = apply %6(%3, %4, %5) : $@convention(method) (Nyan, Nyan, @thin Nyan.Type) -> Bool // user: %8
return %7 : $Bool // id: %8
} // end sil function '$S1A4NyanOSQAASQ2eeoiySbx_xtFZTW'

// protocol witness for static Equatable.== infix(_:_:) in conformance Nyanというコメントから、これはEquatableの準拠によって生成された==のコードだと考えられます。

ここで__derived_enum_equals

%6 = function_ref @$S1A4NyanO21__derived_enum_equalsySbAC_ACtFZ : $@convention(method) (Nyan, Nyan, @thin Nyan.Type) -> Bool // user: %7

で呼ばれているのがわかりますね。実際に__derived_enum_equalsのコードと思われる場所を見てみると以下のようになっていることがわかります。

// static Nyan.__derived_enum_equals(_:_:)

sil hidden @$S1A4NyanO21__derived_enum_equalsySbAC_ACtFZ : $@convention(method) (Nyan, Nyan, @thin Nyan.Type) -> Bool {
// %0 // users: %9, %3
// %1 // users: %29, %4
// %2 // user: %5
bb0(%0 : $Nyan, %1 : $Nyan, %2 : $@thin Nyan.Type):
debug_value %0 : $Nyan, let, name "a", argno 1 // id: %3
debug_value %1 : $Nyan, let, name "b", argno 2 // id: %4
debug_value %2 : $@thin Nyan.Type, let, name "self", argno 3 // id: %5
%6 = alloc_box ${ var Int }, var, name "index_a" // user: %7
%7 = mark_uninitialized [var] %6 : ${ var Int } // users: %56, %8
%8 = project_box %7 : ${ var Int }, 0 // users: %47, %22, %14
switch_enum %0 : $Nyan, case #Nyan.hoge!enumelt: bb1, case #Nyan.fuga!enumelt: bb2 // id: %9

最後の行を見てみましょう。switch_enumで1つめの引数%0によって処理を分岐させており、



  • case #Nyan.hoge!enumelt:(つまり case hoge)の場合はラベルbb1へ、


  • case #Nyan.fuga!enumelt:(つまり case fuga)の場合はラベルbb2へ、

処理を引き継いでいます。bb1bb2では、各処理の最後にラベルbb3へ飛び、最後にswitch_enumで2つめの引数%1を比較して処理を分岐している事がわかります。

bb1:                                              // Preds: bb0

%10 = metatype $@thin Int.Type // user: %13
%11 = integer_literal $Builtin.Int2048, 0 // user: %13
// function_ref Int.init(_builtinIntegerLiteral:)
%12 = function_ref @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %13
%13 = apply %12(%11, %10) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %15
%14 = begin_access [modify] [unknown] %8 : $*Int // users: %16, %15
assign %13 to %14 : $*Int // id: %15
end_access %14 : $*Int // id: %16
br bb3 // id: %17

bb2: // Preds: bb0
%18 = metatype $@thin Int.Type // user: %21
%19 = integer_literal $Builtin.Int2048, 1 // user: %21
// function_ref Int.init(_builtinIntegerLiteral:)
%20 = function_ref @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %21
%21 = apply %20(%19, %18) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %23
%22 = begin_access [modify] [unknown] %8 : $*Int // users: %24, %23
assign %21 to %22 : $*Int // id: %23
end_access %22 : $*Int // id: %24
br bb3 // id: %25

bb3: // Preds: bb2 bb1
%26 = alloc_box ${ var Int }, var, name "index_b" // user: %27
%27 = mark_uninitialized [var] %26 : ${ var Int } // users: %55, %28
%28 = project_box %27 : ${ var Int }, 0 // users: %50, %42, %34
switch_enum %1 : $Nyan, case #Nyan.hoge!enumelt: bb4, case #Nyan.fuga!enumelt: bb5 // id: %29

これ以降の解説は割愛しますが、これらから私達が以前まで書いていたswitchによる==の実装と同様の実装を、コンパイラが生成していると考えられます!


まとめ

以上のことから以下のことがわかりました!


  • Swift4.1では、Equatableのstatic public func ==(lhs:rhs:) -> Boolはコンパイラによって生成されている。


  • enumの場合、今まで作っていたswitch-caseと同じような比較をコンパイラが生成してくれていると考えられる。

新しいEquatableでは、従来私達が書いていた==による比較をコンパイラが生成してくれていることがわかりました!

これから先、このEquatableで私達のコーディングライフがもっと楽しくなればいいですね!

今回のこの機能の実装と解説はSynthesizing Equatable and Hashable conformanceで見れますので、チェックしてみるのも良いかもしれません!

間違い等があれば、コメントをよろしくおねがいします!