Swift Digest
Blog | Swift.org Blog

新しい診断アーキテクチャの概要

New Diagnostic Architecture Overview

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

この記事の要点

背景: なぜ良い診断は難しいのか

Swift はクラス継承・プロトコル適合・ジェネリクス・オーバーロードといった豊富な機能を持つ表現力の高い言語です。コンパイラはどの Swift コードが妥当でどれが不正かを正確に知っていますが、難しいのは「何が」「どこで」間違っていて「どう直せばよいか」を、いかに分かりやすく伝えるかです。

この取り組みの焦点は 型チェッカ(type checker) の改善にあります。型チェッカは型の使われ方に関する規則を強制し、規則が破られたときにそれを知らせる役割を担います。

たとえば次のコードを考えます。

struct S<T> {
  init(_: [T]) {}
}

var i = 42
_ = S<Int>([i!])

従来はこれが次のように診断されていました。

error: type of expression is ambiguous without more context

これは本当にエラーではあるものの、具体性がなく、どう直せばよいかが分かりません。原因は、従来の型チェッカがエラーの正確な位置を 推測 していたことにあります。新しいアプローチではこれが次のように診断されます。

error: cannot force unwrap value of non-optional type 'Int'
_ = S<Int>([i!])
            ~^

オプショナルでない Int! で強制アンラップしている、という実際の問題を正確に指摘できています。

型推論のしくみ

新しい診断は型チェッカと密接に結びついているため、まず型推論を簡単に押さえます。Swift は制約ベースの型チェッカによる双方向の型推論を行い、これは古典的な Hindley-Milner 型推論アルゴリズムに着想を得たものです。

診断にとって重要なのは、制約の生成(Constraint Generation)と求解(Constraint Solving)の 2 段階です。入力の式から、型チェッカは各部分式の抽象的な型を表す型変数の集合と、それらの関係を表す型制約の集合を生成します。よく使われる制約には次のようなものがあります。

例: str + 1

次の関数を考えます。

func foo(_ str: String) {
  str + 1
}

str + 1 という式に対し、型チェッカは次のような型変数を作ります。$Strstr の型)、$One(リテラル 1 の型)、$Result+ の結果型)、$Plus(演算子 + 自体の型。複数のオーバーロード候補の集合)です。そして次のような制約が付きます。

ソルバはまず $Plus の disjunction の選択肢を 1 つずつ試し、各型変数の取り得る型を絞り込んでいきます。たとえば $Plus を最初の候補 (String, String) -> String に束縛すると、$Str <convertible to> String$One <convertible to> String$Result = String という制約が導かれます。ここで $One の候補(Int / Double / String)はどれも残りの制約をすべて満たせません(IntDoubleString に変換できず、StringExpressibleByIntegerLiteral に適合しません)。そのため $One がエラーの位置だと判定でき、ソルバは次の disjunction の候補へバックトラックします。

このようにエラーの位置自体はソルバが特定できます。問題は、こうした行き詰まった状況でソルバを前進させ、完全な解を導く方法でした。

新しいアプローチ: constraint fix

新しい診断基盤は、ソルバが他に試す型がなく行き詰まった状況を解消するために constraint fix(制約の修正) という仕組みを使います。先ほどの例での修正は、「StringExpressibleByIntegerLiteral に適合しないこと」をいったん無視することです。修正の目的は、エラー位置に関する有用な情報をソルバから残らず取り出し、あとで診断に使えるようにすることにあります。

これが従来との本質的な違いです。従来はエラーの位置を 推測 していたのに対し、新しいアプローチではソルバと協調し、ソルバがエラーの位置をすべて提供してくれます。型変数や制約はどの部分式に由来するかの情報を保持しているため、その関係と型情報を組み合わせれば、問題に応じた診断と Fix-It を素直に組み立てられます。

先の例では $One がエラー位置だと分かっており、$One+ の第 2 引数であること、StringExpressibleByIntegerLiteral に適合しないことが分かっています。これらから次のような診断を生成できます。

error: binary operator '+' cannot be applied to arguments 'String' and 'Int'

診断の組み立てかた

制約の失敗が検出されると、constraint fix が作られ、失敗の種類・ソースコード上の位置・関係する型や宣言を記録します。ソルバはこれらの修正を蓄積し、解にたどり着いたら、解の一部となった修正を見て、実際の修正につながるエラーや警告を生成します。

func foo(_: inout Int) {}

var x: Int = 0
foo(x)

ここでは xinout 引数に明示的な & なしで渡せないことが問題です。ソルバは ($X) -> $Result(inout Int) -> Void に合わせようとし、Int <convertible to> inout Int$Result <equal to> Void を導きます。Intinout Int に変換できないため、ソルバはこの失敗を「& が足りない(missing &)」として記録し、その変換制約を無視します。残りの制約は無事に解け、型チェッカは記録した修正を見て、& を挿入する Fix-It 付きのエラーを生成します。

error: passing value of type 'Int' to an inout parameter requires explicit '&'
foo(x)
    ^
    &

このアーキテクチャは、1 つの式の中の 複数の独立したエラー も扱えます。

func foo(_: inout Int, bar: String) {}

var x: Int = 0
foo(x, "bar")

ここでは「& が足りない」失敗に加えて「引数ラベル bar: が足りない」失敗も記録されます。両方を記録したうえで残りの制約を解き、2 つの問題それぞれに Fix-It 付きの診断を出せます。

error: passing value of type 'Int' to an inout parameter requires explicit '&'
error: missing argument label 'bar:' in call

個々の失敗を記録してから残りを解き続けるということは、それらの失敗を直せば型の整合した解が得られる、ということを意味します。だからこそ、しばしば修正候補を伴った、実際にコードを正しくする方向へ導く診断を出せるのです。

改善された診断の例

この仕組みにより、通常のコードでも SwiftUI のコードでも、従来は分かりにくかった多くの診断が具体的になりました。たとえば次のような改善があります。

引数ラベル不足は、従来 argument labels '(_:)' do not match any available overloads(利用可能なオーバーロードに一致しない)と出ていたものが、missing argument label 'answer:' in call のように足りないラベルそのものを指摘し、Fix-It を提示します。

ジェネリックの推論失敗は、従来は generic parameter 'T' could not be inferred(型パラメータ T を推論できない)としか言えませんでしたが、原因がプロトコル適合の欠如なら argument type 'T' does not conform to expected type 'P' のように、本当の原因を直接指摘します。

SwiftUI では効果が顕著です。たとえば次のコードでは、従来は無関係な 'Double' is not convertible to 'CGFloat?' という的外れな診断が出ていました。

import SwiftUI

struct S: View {
  var body: some View {
    ZStack {
      Rectangle().frame(width: 220.0, height: 32.0)
                 .foregroundColor(.systemRed)
      // ...
    }
  }
}

新しい診断は、本当の原因である「systemRed という色は存在しない」ことを正しく指摘します。

error: type 'Color?' has no member 'systemRed'
                   .foregroundColor(.systemRed)
                                    ~^~~~~~~~~

まとめ

新しい診断基盤は、エラー位置を推測していた従来手法の弱点を克服するよう設計されています。既存の診断の改善・移植が容易で、新機能の実装者が最初から良い診断を提供しやすい構造になっています。すでに移植された診断はいずれも有望な結果を示しており、Swift 5.2 に向けて移植が進められていました。

関連リンク