はじめに
SwiftにはOptional
型がありますよね。
たとえばDictionary
のsubscript(key: Key) -> Value? { get set }
の返り値はOptional<Value>
(その糖衣構文のValue?
)になってますね。
値があるかどうか保証できない場合にOptional
型で返すということは一般的です。
そのOptiona
型のインスタンスについて、自信を持って「値がある!」と言える場合には、強制アンラップをすることがあるかと思います:
let maybeInt: Int? = someFunction()
if maybeInt != nil {
// 実際はOptional bindingを使うと思いますが、あくまで例ということで…
print(maybeInt!)
}
この、強制アンラップ(forced unwrap)をするときの演算子ってどこで定義されているんでしょう?
それを探っていきましょう。
予想
最初は次のようなコードが標準ライブラリのどこかにあるのではないかと想像していました:
postfix operator !
extension Optional {
public static postfix func !(optional: Optional) -> Wrapped {
guard let wrapped = optional else {
fatalError("nilだよ。")
}
return wrapped
}
}
でも、そんなコードはどこにもありませんでした。
上記のコードを実行しようとしてみれば分かると思いますが、
error: postfix operator names starting with '?' or '!' are disallowed to avoid collisions with built-in unwrapping operators
というエラーが表示されると思います。
つまりSwiftのコードでは!
から始まる演算子(もちろん「!
だけ」も含む)を定義することはできないのです。
!
の正体を探す
検証コード
検証のために次のようなコードを用意しました:
let _ = Int?.none!
1行でクラッシュするコードです。
今回は!
の正体を知りたいだけなのでこれで十分です。
AST
とりあえずAST(抽象構文木)をダンプしてみましょう。
swiftc -dump-ast force-unwrap.swift
を実行します。
(source_file "force-unwrap.swift"
(top_level_code_decl range=[force-unwrap.swift:1:1 - line:1:18]
(brace_stmt implicit range=[force-unwrap.swift:1:1 - line:1:18]
(pattern_binding_decl range=[force-unwrap.swift:1:1 - line:1:18]
(pattern_any type='Int')
Original init:
(optional_evaluation_expr implicit type='<null>'
(force_value_expr type='Int' location=force-unwrap.swift:1:18 range=[force-unwrap.swift:1:9 - line:1:18]
(dot_syntax_call_expr type='Int?' location=force-unwrap.swift:1:14 range=[force-unwrap.swift:1:9 - line:1:14] nothrow
(declref_expr type='(Optional<Int>.Type) -> Optional<Int>' location=force-unwrap.swift:1:14 range=[force-unwrap.swift:1:14 - line:1:14] decl=Swift.(file).Optional.none [with (substitution_map generic_signature=<Wrapped> (substitution Wrapped -> Int))] function_ref=unapplied)
(type_expr type='Int?.Type' location=force-unwrap.swift:1:9 range=[force-unwrap.swift:1:9 - line:1:12] typerepr='Int?'))))
Processed init:
(force_value_expr type='Int' location=force-unwrap.swift:1:18 range=[force-unwrap.swift:1:9 - line:1:18]
(dot_syntax_call_expr type='Int?' location=force-unwrap.swift:1:14 range=[force-unwrap.swift:1:9 - line:1:14] nothrow
(declref_expr type='(Optional<Int>.Type) -> Optional<Int>' location=force-unwrap.swift:1:14 range=[force-unwrap.swift:1:14 - line:1:14] decl=Swift.(file).Optional.none [with (substitution_map generic_signature=<Wrapped> (substitution Wrapped -> Int))] function_ref=unapplied)
(type_expr type='Int?.Type' location=force-unwrap.swift:1:9 range=[force-unwrap.swift:1:9 - line:1:12] typerepr='Int?'))))
)))
なんか、もうここで「!
がどこで定義されているか」の答えは出ちゃってる気がしますけどね。
!
(という後置演算子)を見つけると、force_value_expr
(強制アンラップした値を表す式)として解釈されるということのようです。
つまり**!
はSwiftのコードで定義されている訳ではなく構文解析の段階で評価される**ということでした。
…というだけでは当たり前すぎる結果なので、もう少し掘り下げてみましょう。
だって、**「nil
を強制アンラップしたらfatal errorが起きる」**という実装が見えてきていません。
まずパーサにおける!
の解釈を辿るため、 https://github.com/apple/swift/blob/main/lib/Parse/ParseExpr.cpp を見てみましょう。
どうやら、まず、後置演算子の式はParser::parseExprPostfix
という関数で処理されます。その関数は内部でParser::parseExprPostfixSuffix
という関数を呼んでおり、!
を見つけるとSyntaxKind::ForcedValueExpr
というノードを作ります。
これがASTで見たforce_value_expr
のようです。
…?
nil
を強制アンラップしたときのことは(当然と言えば当然ですが)構文解析の段階では全く触れられません。
「nil
を強制アンラップしたらfatal errorが起きる」動作を探るため、次はSILを見てみましょう。
SIL
SIL(Swift Intermediate Language)をダンプしてみましょう。
swiftc -emit-sil force-unwrap.swift
の出番です。
長いので全部を見たい人だけ開いてみてください:
SILの出力結果
sil_stage canonical
import Builtin
import Swift
import SwiftShims
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
%2 = metatype $@thin Optional<Int>.Type
br bb1 // id: %3
bb1: // Preds: bb0
%4 = string_literal utf8 "main/force-unwrap.swift" // user: %13
%5 = integer_literal $Builtin.Word, 23 // user: %17
%6 = integer_literal $Builtin.Word, 18
br bb2 // id: %7
bb2: // Preds: bb1
%8 = string_literal utf8 "Unexpectedly found nil while unwrapping an Optional value" // user: %10
%9 = integer_literal $Builtin.Word, 57 // user: %12
%10 = builtin "ptrtoint_Word"(%8 : $Builtin.RawPointer) : $Builtin.Word // user: %12
%11 = integer_literal $Builtin.Int8, 2 // user: %12
%12 = struct $StaticString (%10 : $Builtin.Word, %9 : $Builtin.Word, %11 : $Builtin.Int8) // user: %31
%13 = builtin "ptrtoint_Word"(%4 : $Builtin.RawPointer) : $Builtin.Word // user: %17
br bb3 // id: %14
bb3: // Preds: bb2
%15 = integer_literal $Builtin.Int8, 2 // user: %17
br bb4 // id: %16
bb4: // Preds: bb3
%17 = struct $StaticString (%13 : $Builtin.Word, %5 : $Builtin.Word, %15 : $Builtin.Int8) // user: %31
%18 = integer_literal $Builtin.Int64, 1 // user: %19
%19 = struct $UInt (%18 : $Builtin.Int64) // user: %31
br bb5 // id: %20
bb5: // Preds: bb4
%21 = string_literal utf8 "Fatal error" // user: %23
%22 = integer_literal $Builtin.Word, 11 // user: %25
%23 = builtin "ptrtoint_Word"(%21 : $Builtin.RawPointer) : $Builtin.Word // user: %25
%24 = integer_literal $Builtin.Int8, 2 // user: %25
%25 = struct $StaticString (%23 : $Builtin.Word, %22 : $Builtin.Word, %24 : $Builtin.Int8) // user: %31
br bb6 // id: %26
bb6: // Preds: bb5
%27 = integer_literal $Builtin.Int32, 1 // user: %28
%28 = struct $UInt32 (%27 : $Builtin.Int32) // user: %31
br bb7 // id: %29
bb7: // Preds: bb6
// function_ref _assertionFailure(_:_:file:line:flags:)
%30 = function_ref @$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_A2HSus6UInt32VtF : $@convention(thin) (StaticString, StaticString, StaticString, UInt, UInt32) -> Never // user: %31
%31 = apply %30(%25, %12, %17, %19, %28) : $@convention(thin) (StaticString, StaticString, StaticString, UInt, UInt32) -> Never
unreachable // id: %32
} // end sil function 'main'
// _diagnoseUnexpectedNilOptional(_filenameStart:_filenameLength:_filenameIsASCII:_line:_isImplicitUnwrap:)
sil public_external [transparent] [serialized] @$ss30_diagnoseUnexpectedNilOptional14_filenameStart01_E6Length01_E7IsASCII5_line17_isImplicitUnwrapyBp_BwBi1_BwBi1_tF : $@convention(thin) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, Builtin.Word, Builtin.Int1) -> () {
// %0 // users: %46, %13
// %1 // users: %53, %20
// %2 // users: %47, %14
// %3 // users: %54, %21
// %4 // user: %5
bb0(%0 : $Builtin.RawPointer, %1 : $Builtin.Word, %2 : $Builtin.Int1, %3 : $Builtin.Word, %4 : $Builtin.Int1):
cond_br %4, bb13, bb1 // id: %5
bb1: // Preds: bb0
%6 = string_literal utf8 "Unexpectedly found nil while unwrapping an Optional value" // user: %8
%7 = integer_literal $Builtin.Word, 57 // user: %12
%8 = builtin "ptrtoint_Word"(%6 : $Builtin.RawPointer) : $Builtin.Word // user: %12
br bb2 // id: %9
bb2: // Preds: bb1
%10 = integer_literal $Builtin.Int8, 2 // user: %12
br bb3 // id: %11
bb3: // Preds: bb2
%12 = struct $StaticString (%8 : $Builtin.Word, %7 : $Builtin.Word, %10 : $Builtin.Int8) // user: %37
%13 = builtin "ptrtoint_Word"(%0 : $Builtin.RawPointer) : $Builtin.Word // user: %20
cond_br %2, bb5, bb4 // id: %14
bb4: // Preds: bb3
%15 = integer_literal $Builtin.Int8, 0 // user: %16
br bb6(%15 : $Builtin.Int8) // id: %16
bb5: // Preds: bb3
%17 = integer_literal $Builtin.Int8, 2 // user: %18
br bb6(%17 : $Builtin.Int8) // id: %18
// %19 // user: %20
bb6(%19 : $Builtin.Int8): // Preds: bb5 bb4
%20 = struct $StaticString (%13 : $Builtin.Word, %1 : $Builtin.Word, %19 : $Builtin.Int8) // user: %37
%21 = builtin "zextOrBitCast_Word_Int64"(%3 : $Builtin.Word) : $Builtin.Int64 // user: %22
%22 = struct $UInt (%21 : $Builtin.Int64) // user: %37
br bb7 // id: %23
bb7: // Preds: bb6
br bb8 // id: %24
bb8: // Preds: bb7
%25 = string_literal utf8 "Fatal error" // user: %27
%26 = integer_literal $Builtin.Word, 11 // user: %31
%27 = builtin "ptrtoint_Word"(%25 : $Builtin.RawPointer) : $Builtin.Word // user: %31
br bb9 // id: %28
bb9: // Preds: bb8
%29 = integer_literal $Builtin.Int8, 2 // user: %31
br bb10 // id: %30
bb10: // Preds: bb9
%31 = struct $StaticString (%27 : $Builtin.Word, %26 : $Builtin.Word, %29 : $Builtin.Int8) // user: %37
br bb11 // id: %32
bb11: // Preds: bb10
%33 = integer_literal $Builtin.Int32, 1 // user: %34
%34 = struct $UInt32 (%33 : $Builtin.Int32) // user: %37
br bb12 // id: %35
bb12: // Preds: bb11
// function_ref _assertionFailure(_:_:file:line:flags:)
%36 = function_ref @$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_A2HSus6UInt32VtF : $@convention(thin) (StaticString, StaticString, StaticString, UInt, UInt32) -> Never // user: %37
%37 = apply %36(%31, %12, %20, %22, %34) : $@convention(thin) (StaticString, StaticString, StaticString, UInt, UInt32) -> Never
unreachable // id: %38
bb13: // Preds: bb0
%39 = string_literal utf8 "Unexpectedly found nil while implicitly unwrapping an Optional value" // user: %41
%40 = integer_literal $Builtin.Word, 68 // user: %45
%41 = builtin "ptrtoint_Word"(%39 : $Builtin.RawPointer) : $Builtin.Word // user: %45
br bb14 // id: %42
bb14: // Preds: bb13
%43 = integer_literal $Builtin.Int8, 2 // user: %45
br bb15 // id: %44
bb15: // Preds: bb14
%45 = struct $StaticString (%41 : $Builtin.Word, %40 : $Builtin.Word, %43 : $Builtin.Int8) // user: %70
%46 = builtin "ptrtoint_Word"(%0 : $Builtin.RawPointer) : $Builtin.Word // user: %53
cond_br %2, bb17, bb16 // id: %47
bb16: // Preds: bb15
%48 = integer_literal $Builtin.Int8, 0 // user: %49
br bb18(%48 : $Builtin.Int8) // id: %49
bb17: // Preds: bb15
%50 = integer_literal $Builtin.Int8, 2 // user: %51
br bb18(%50 : $Builtin.Int8) // id: %51
// %52 // user: %53
bb18(%52 : $Builtin.Int8): // Preds: bb17 bb16
%53 = struct $StaticString (%46 : $Builtin.Word, %1 : $Builtin.Word, %52 : $Builtin.Int8) // user: %70
%54 = builtin "zextOrBitCast_Word_Int64"(%3 : $Builtin.Word) : $Builtin.Int64 // user: %55
%55 = struct $UInt (%54 : $Builtin.Int64) // user: %70
br bb19 // id: %56
bb19: // Preds: bb18
br bb20 // id: %57
bb20: // Preds: bb19
%58 = string_literal utf8 "Fatal error" // user: %60
%59 = integer_literal $Builtin.Word, 11 // user: %64
%60 = builtin "ptrtoint_Word"(%58 : $Builtin.RawPointer) : $Builtin.Word // user: %64
br bb21 // id: %61
bb21: // Preds: bb20
%62 = integer_literal $Builtin.Int8, 2 // user: %64
br bb22 // id: %63
bb22: // Preds: bb21
%64 = struct $StaticString (%60 : $Builtin.Word, %59 : $Builtin.Word, %62 : $Builtin.Int8) // user: %70
br bb23 // id: %65
bb23: // Preds: bb22
%66 = integer_literal $Builtin.Int32, 1 // user: %67
%67 = struct $UInt32 (%66 : $Builtin.Int32) // user: %70
br bb24 // id: %68
bb24: // Preds: bb23
// function_ref _assertionFailure(_:_:file:line:flags:)
%69 = function_ref @$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_A2HSus6UInt32VtF : $@convention(thin) (StaticString, StaticString, StaticString, UInt, UInt32) -> Never // user: %70
%70 = apply %69(%64, %45, %53, %55, %67) : $@convention(thin) (StaticString, StaticString, StaticString, UInt, UInt32) -> Never
unreachable // id: %71
} // end sil function '$ss30_diagnoseUnexpectedNilOptional14_filenameStart01_E6Length01_E7IsASCII5_line17_isImplicitUnwrapyBp_BwBi1_BwBi1_tF'
// _assertionFailure(_:_:file:line:flags:)
sil [noinline] [_semantics "programtermination_point"] @$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_A2HSus6UInt32VtF : $@convention(thin) (StaticString, StaticString, StaticString, UInt, UInt32) -> Never
// Mappings from '#fileID' to '#filePath':
// 'main/force-unwrap.swift' => 'force-unwrap.swift'
SILの中身を見た方は分かると思いますが、ここに来ていきなり"Unexpectedly found nil while implicitly unwrapping an Optional value"というエラーメッセージを用意しています(@main
のbb2:
)。そしてnil
をアンラップしようとしたらクラッシュするようになっています。
どうやら、構文を解析したのちSILに変換する段階で「nilを強制アンラップしたらfatal errorが起きる」動作を挿入しているようです。
これは https://github.com/apple/swift/blob/main/lib/SILGen/SILGenConvert.cpp の中の SILGenFunction::emitPreconditionOptionalHasValue
という関数でおこなっています。たぶん。
そして実際のエラー処理は https://github.com/apple/swift/blob/main/stdlib/public/core/Optional.swift の中にあります。_diagnoseUnexpectedNilOptional
という関数がその処理を担っているのです。
ということで、「nil
を強制アンラップしたらfatal errorが起きる」は周りまわって"Optional.swift"で定義されていたのでした。
おわりに
というわけで、!
という強制アンラップの演算子を少しだけ掘り下げてみた記事でした。
実は筆者はC++が読めないので、間違いがあったらコメントをお願いします。