Swiftの型システムを読む その26 - type(of:)の実装と@_semantics

January 12, 2018

たまたまtype(of:) の実装を読んでたら少し知見があったのでメモ。

type(of:)とは?

You can use the type(of:) function to find the dynamic type of a value, particularly when the dynamic type is different from the static type. The static type of a value is the known, compile-time type of the value. The dynamic type of a value is the value’s actual type at run-time, which can be nested inside its concrete type.

https://developer.apple.com/documentation/swift/2885064-type

静的な型ではなく、実行時のDynamic type(のメタタイプ)を返す。 例えば以下のbは静的にはAny型だが、実行時には Int型である。

let a: Int = 1
let b: Any = a
type(of: b) // Int

(正確にはExistentialの中身がInt型)

サブクラスなども同様で、animalの静的な型はAnimalだが、実行時の型はDogである。

class Animal { }
class Dog: Animal { }
let dog = Dog()
let animal: Animal = dog
type(of: animal) // Dog

実装をみてみる

stdlib/core/Builtin.swiftに実装がある…あるのだが、みてみると「この実装は使われていない」と書いてある。どういうことだろう?と思って調べてみたのが今回の記事。

@_inlineable // FIXME(sil-serialize-all)
@_transparent
@_semantics("typechecker.type(of:)")
public func type<T, Metatype>(of value: T) -> Metatype {
  // This implementation is never used, since calls to `Swift.type(of:)` are
  // resolved as a special case by the type checker.
  Builtin.staticReport(_trueAfterDiagnostics(), true._value,
    ("internal consistency error: 'type(of:)' operation failed to resolve"
     as StaticString).utf8Start._rawValue)
  Builtin.unreachable()
}

@_semantics

@_semantics("typechecker.type(of:)")

@_semanticsは名前の通り特別なセマンティクスが与えられている関数に付けられるアトリビュート。簡単に言えばコンパイル時に実際の実装が挿入されるtypechecker.type(of:) という名前でgrepしてみると、どうやらlib/Sema/TypeChecker.cppで拾っているようだ。

DeclTypeCheckingSemantics
TypeChecker::getDeclTypeCheckingSemantics(ValueDecl *decl) {
  // Check for a @_semantics attribute.
  if (auto semantics = decl->getAttrs().getAttribute<SemanticsAttr>()) {
    if (semantics->Value.equals("typechecker.type(of:)"))
      return DeclTypeCheckingSemantics::TypeOf;
    if (semantics->Value.equals("typechecker.withoutActuallyEscaping(_:do:)"))
      return DeclTypeCheckingSemantics::WithoutActuallyEscaping;
    if (semantics->Value.equals("typechecker._openExistential(_:do:)"))
      return DeclTypeCheckingSemantics::OpenExistential;
  }
  return DeclTypeCheckingSemantics::Normal;
}

ConstraintSystem::resolveOverload時にこの特別扱いに当てはまるかをチェックして、当てはまればDeclTypeCheckingSemanticsの種類に応じた処理を行う。 type(of:)の場合はDeclTypeCheckingSemantics::TypeOfになる。

型チェックをみる

改めて特別扱いされてはいるものの、基本的には普通の関数と同じように型チェックされる。シグネチャをもう一度見てみると、TMetatypeの2つの型パラメータをとる。

public func type<T, Metatype>(of value: T) -> Metatype

通常のopenのフローには乗らないものの、いつも通り「型パラメータにフレッシュな型変数を割り当てる」ことによって多相性を実現する。

auto input = CS.createTypeVariable(
  CS.getConstraintLocator(locator, ConstraintLocator::FunctionArgument),
  TVO_CanBindToInOut);
auto output = CS.createTypeVariable(
  CS.getConstraintLocator(locator, ConstraintLocator::FunctionResult),
  TVO_CanBindToInOut);

そしてMetatypeTDynamicTypeであるという制約が追加される。

CS.addConstraint(ConstraintKind::DynamicTypeOf, output, input,
    CS.getConstraintLocator(locator, ConstraintLocator::RvalueAdjustment));

ASTの書き換えをみてみる

ExprRewriter::finishApply の中で@_semantics付きの関数の書き換えを行なっている。ここで実装としての ASTが挿入される。

type(of:)の場合はDynamicTypeExprが挿入される。

auto replacement = new (tc.Context)
  DynamicTypeExpr(apply->getFn()->getLoc(),
                  apply->getArg()->getStartLoc(),
                  arg,
                  apply->getArg()->getEndLoc(),
                  Type());

-dump-astなどで出力した場合はmetatype_exprという名前で出てくる。

SILを見てみる

Animal -> Dogの例

value_metatypeはその名の通りvalueのDynamic Typeをとる命令。

%15 = value_metatype [email protected] Animal.Type, %14

Any -> Intの例

%12 = existential_metatype [email protected] Any.Type, %8

Existentialの場合はexistential_metatypeによってDynamic Typeが取得される。Existentialは(実行時のメモリ上の表現では)、値・メタタイプ・protocol witness tableの3つ組なのでそこからメタタイプを取り出す。

Swift3とSwift4の違い

Swift3ではtype(of:)Parse時に特別扱いされていたが、Swift4の このコミットでTypeCheck時の特別扱いに変わった。

Resolve type(of:) by overload resolution rather than parse hackery. type(of:) has behavior whose type isn’t directly representable in Swift’s type system, since it produces both concrete and existential metatypes. In Swift 3 we put in a parser hack to turn type(of: <expr>) into a DynamicTypeExpr, but this effectively made type(of:) a reserved name. It’s a bit more principled to put Swift.type(of:) on the same level as other declarations, even with its special-case type system behavior, and we can do this by special-casing the type system we produce during overload resolution if Swift.type(of:) shows up in an overload set. This also lays groundwork for handling other declarations we want to ostensibly behave like normal declarations but with otherwise inexpressible types, viz. withoutActuallyEscaping from SE-0110.

まとめ

  • @_semanticsの使われ方がわかった。
  • ConstraintKind::DynamicTypeOfの使われ方がわかった。

IUOとtype(of:)を組み合わせた時の挙動で面白そうなのをDiscordで見かけたので、時間があるときに調べてみる。

このエントリーをはてなブックマークに追加