Skip to content

Instantly share code, notes, and snippets.

@omochi
Created Jun 1, 2020
Embed
What would you like to do?

slidenumber: true autoscale: true

CSFixとラベルマッチ

わいわいswiftc #20

@omochimetaru


自己紹介

  • 新米Swiftコミッター

inline


今日の方針

  • (自分は)オンライン発表を聞くのは集中度が下がる😐

  • 話す側からしても、リアクションが少なめで微妙🤨

  • 手を動かしてもらいます💡

  • ターミナルとエディタを開いておいてね💻


Swift 5.2 の新機能


新診断機構

(New Diagnostic Architecture)

(CSFix)


公式の解説ブログ記事

New Diagnostic Architecture Overview

https://swift.org/blog/new-diagnostic-arch-overview/


新診断機構

  • 言語機能ではないのであまり注目されていない(と思う)

  • だけど、コンパイラの機能としては強力!



ミスのあるコード1

func f(aa: Int, bb: Int) {}

func main() {
    // ❌ `bb:` を付け忘れている
    f(aa: 1, 2)
}

Swift 5.1.5, Swift 5.2.2 (Xcode11.4.1)

$ alias swift515='xcrun --toolchain org.swift.515120200323a swift'

$ swift515 a.swift

a.swift:4:6: error: missing argument label 'bb:' in call
    f(aa: 1, 2)
     ^
             bb: 

😀


ミスのあるコード2

func f(aa: Int, bb: Int) {}

// fのオーバーロードが追加されている
func f(cc: Int, dd: Int) {}

func main() {
    // ❌ `bb:` を付け忘れている
    f(aa: 1, 2)
}

Swift 5.1.5

$ swift515 a.swift

a.swift:6:5: error: argument labels '(aa:, _:)' do not match any available overloads
    f(aa: 1, 2)
    ^~~~~~~~~~~
a.swift:6:5: note: overloads for 'f' exist with these partially matching parameter 
lists: (aa: Int, bb: Int), (cc: Int, dd: Int)
    f(aa: 1, 2)
    ^

😩


Swift 5.2.2 (Xcode11.4.1)

$ swift a.swift

やってみて!


Swift 5.2.2 (Xcode11.4.1)

$ swift a.swift

a.swift:6:6: error: missing argument label 'bb:' in call
    f(aa: 1, 2)
     ^
             bb: 

🤩


何が起きているか

  • コンパイラが、ユーザがどっちのオーバーロードを呼ぶつもりだったのか、予測している

旧挙動と型推論


  • 型推論においてオーバーロードはDisjunction Constraintとして扱われる
<disjunction> {
    $T1 <bind OL> (aa: Int, bb: Int) -> Void
    $T1 <bind OL> (cc: Int, dd: Int) -> Void
}

$T1 <appfn> (aa: Int, _: Int) -> Void

Disjunction空間の探索

  • 候補制約を一つずつ推論器に試しに投入する

  • 全部失敗 → エラー: マッチするオーバーロードが無い

  • 一つ成功 → それを型推論結果として採用

  • 複数成功 → 成功した解同士を比較して、最良のものを選択

  • 比較結果が同率一位 → エラー: 曖昧なコード


解の比較(復習)

func f(_ a: Int?) { print("Int?") }

func f(_ a: Any) { print("Any") }

func main() {
    // 1 は Int? でもあるし、 Any でもある。
    f(1)
}

main()

解の比較

$ swift b.swift

やってみて!


解の比較

$ swift b.swift

Any

解の比較の状況の観察

$ swift -Xfrontend -debug-constraints b.swift

Comparing 2 viable solutions
--- Solution #0 ---
Fixed score: 0 0 0 0 0 0 0 0 1 0 0 0
Type variables:
  $T0 as (Int?) -> () @ locator@0x7ff8a0808800 [OverloadedDeclRef@b.swift:6:5]

--- Solution #1 ---
Fixed score: 0 0 0 0 0 0 0 0 0 1 0 0
Type variables:
  $T0 as (Any) -> () @ locator@0x7ff8a0808800 [OverloadedDeclRef@b.swift:6:5]

解のスコア(コスト)

0 0 0 0 0 0 0 0 1 0 0 0
                ^ SK_ValueToOptional は Level4

0 0 0 0 0 0 0 0 0 1 0 0
                  ^ SK_EmptyExistentialConversion は Level3
  • Anyへの変換の方が安い

参考: 過去の発表

Swiftのオーバーロード選択のスコア規則12種類

https://speakerdeck.com/omochi/swiftfalseobarodoxuan-ze-falsesukoagui-ze-12zhong-lei


新挙動の実現方法


新挙動の実現方法

もう察した人は、挙手🙋‍♂️


新挙動の実現方法

  • 間違いがあったが修復した、というスコア(SK_Fix)を定義する

  • 関数呼び出しでラベルに間違いがあっても、修復スコアを積んで処理をすすめる

  • 最終的に修復スコアの付いた解が選択された場合、 コンパイルエラーにしつつ、その解に対する診断を出力する


修復の状況の観察

// (都合上、定義順を入れ替え)
func f(cc: Int, dd: Int) {}
func f(aa: Int, bb: Int) {}
func main() {
    f(aa: 1, 2)
}
$ swift -Xfrontend -debug-constraints a.swift

修復の状況の観察

$ swift -Xfrontend -debug-constraints a.swift

--- Solution #0 ---
Fixed score: 4 0 0 0 0 0 0 0 0 0 0 0
Fixes:
  [fix: re-label argument(s)] @ locator@0x7ff8e08cf100 [Call@a.swift:4:5 -> apply argument]

--- Solution #1 ---
Fixed score: 1 0 0 0 0 0 0 0 0 0 0 0
Fixes:
  [fix: re-label argument(s)] @ locator@0x7ff8e08cf100 [Call@a.swift:4:5 -> apply argument]

Fixed score: 4 0 0 0 0 0 0 0 0 0 0 0
             ^ Level12のSK_Fixが4点 =
                 ラベルエラーの基本点が1点 + 
                 aa: → cc: のwrongが3点 +
                 _: → dd: のmissingが0点

Fixed score: 1 0 0 0 0 0 0 0 0 0 0 0
             ^ Level12のSK_Fixが1点 =
                 ラベルエラーの基本点が1点 +
                 _: → bb: のmissingが0点
  • (aa:_:)からの修復は(aa:bb:)の方が(cc:dd:)より安い

型エラーも修復できる

func f(aa: String) {}

func f(bb: Int) {}

func main() {
    // ラベルの間違い具合は同率だけど・・・
    f(cc: 1)
}

$ swift c.swift

c.swift:5:6: error: incorrect argument label in call (have 'cc:', expected 'bb:')
    f(cc: 1)
     ^~~
      bb
  • (bb: Int) の方を呼びたかっただろうと判断している

型エラー修復の観察

--- Solution #0 ---
Fixed score: 6 0 0 0 0 0 0 0 0 0 0 0
Fixes:
  [fix: re-label argument(s)] @ locator@0x7fc7638cedf8 [Call@c.swift:5:5 -> apply argument]
  [fix: allow argument to parameter type conversion mismatch] @ locator@0x7fc7638cee48 
    [Call@c.swift:5:5 -> apply argument -> comparing call argument #0 to parameter #0]

--- Solution #1 ---
Fixed score: 4 0 0 0 0 0 0 0 0 0 0 0
Fixes:
  [fix: re-label argument(s)] @ locator@0x7fc7638cedf8 [Call@c.swift:5:5 -> apply argument]

  • (cc: 1) to (aa: String) は ラベルエラー基本点1点 + cc:→aa: wrong 3点 + 型エラー2点 で合計6点
  • (cc: 1) to (bb: Int) は ラベルエラー基本点1点 + cc:→bb: wrong 3点 で合計4点でこちらが安い

ここまでのまとめ

  • Swift5.2では、エラーを修復しながら型推論を進めるCSFix機構が入ったことで、 オーバーロードが関わるコンパイルエラーがより親切になった

  • その実現には、複数の推論解を優先度付けする既存の仕組みをうまく活用した


ラベルマッチング


ラベルマッチング

  • 関数呼び出しにおいて、呼び出し側の実引数と、 関数側の仮引数の対応関係を決める処理を ラベルマッチングと呼ぶっぽい (コミットメッセージとか見てると)

  • Swiftのこのあたりの言語仕様はかなり複雑

  • 修復する場合も考えるとさらに複雑になる


ラベル指定

func f(aa: Int, bb: Int, cc: Int) {}

f(aa: 1, bb: 2, cc: 3)

ラベル間違いと順番間違い

func f(aa: Int, bb: Int, cc: Int) {}

f(aax: 1, bb: 2, cc: 3)
// incorrect argument label in call (have 'aax:bb:cc:', expected 'aa:bb:cc:')

f(bb: 2, cc: 3, aa: 1)
// argument 'aa' must precede argument 'bb'

ラベルなし引数

func f(_ aa: Int, _ bb: Int, cc: Int) {}

f(1, 2, cc: 3)

ラベル忘れとラベル過剰

func f(_ aa: Int, _ bb: Int, cc: Int) {}

f(1, 2, 3)
// missing argument label 'cc:' in call

f(1, bb: 2, cc: 3)
// extraneous argument label 'bb:' in call

引数過剰と引数不足

func f(aa: Int, bb: Int, cc: Int) {}

f(aa: 1, bb: 2, cc: 3, dd: 4)
// extra argument 'dd' in call

f(aa: 1, bb: 2)
// missing argument for parameter 'cc' in call

ラベルなし引数と引数不足の組み合わせ

func f(_ aa: Int, bb: Int, _ cc: Int) {}

f(bb: 2, 3)
// missing argument for parameter #1 in call
  • bb:を基準に3_ cc:にマッチ、_ aa:が不足と判断

可変長引数

func f(aa: Int, bb: Int..., cc: Int) {}

f(aa: 1, bb: 21, 22, cc: 3)

f(aa: 1, cc: 3)

可変長引数とラベルなし引数

func f(aa: Int, bb: Int..., _ cc: Int) {}
// a parameter following a variadic parameter requires a label
  • 可変長引数の後続はラベルが必須

  • 以下の場合に3が曖昧になるからだろう

f(aa: 1, bb: 2, 3)

可変長引数は1つまで

func f(aa: Int..., bb: Int...) {}
// only a single variadic parameter '...' is permitted
  • この制限を外す提案がある

  • Lifting the 1 variadic param per function restriction https://forums.swift.org/t/lifting-the-1-variadic-param-per-function-restriction/33787


デフォルト付き引数

func f(aa: Int, bb: Int = 0, _ cc: Int) {}

f(aa: 1, bb: 2, 3)
f(aa: 1, 3)

ラベル無しデフォルト付き引数

func f(aa: Int, _ bb: Int = 0, _ cc: Int) {}

f(aa: 1, 2, 3)
// OK

f(aa: 1, 3)

やってみて!


機能しないラベル無しデフォルト付き引数

func f(aa: Int, _ bb: Int = 0, _ cc: Int) {}

f(aa: 1, 2, 3)
// OK

f(aa: 1, 3)
// missing argument for parameter #3 in call
  • デフォルト付きラベル無し引数に、ラベル無し引数が続くと、 このデフォルト値が使われる事はない

  • 後続のラベル無し引数だけ渡したくても、 手前のデフォルト付き引数に吸い込まれる


ラベル付きデフォルト付き引数

func f(aa: Int, bb: Int = 0, bb cc: Int) {}

f(aa: 1, bb: 2, bb: 3)
// OK

f(aa: 1, bb: 3)

やってみて!


機能しないラベル付きデフォルト付き引数

func f(aa: Int, bb: Int = 0, bb cc: Int) {}

f(aa: 1, bb: 2, bb: 3)
// OK

f(aa: 1, bb: 3)
// missing argument for parameter 'bb' in call
  • 同じラベルを使う事ができるので、 デフォルト付きラベル付き引数に、同じラベル引数が続くと、 やはり使われないデフォルト値が生じる

可変長引数の後続の機能しないラベル付きデフォルト付き引数

func f(aa: Int, bb: Int..., cc: Int = 0, _ dd: Int) {}

f(aa: 1, bb: 2, cc: 3, 4)
// OK

f(aa: 1, bb: 2, 4)
// missing argument for parameter #4 in call
  • 可変長引数の後にデフォルト付き引数を置いて、 さらにその後にラベル無し引数を置くと、 機能しないデフォルト値が生じる

  • デフォルト付き引数を省略した場合、 可変長引数の後続の値も可変長引数に吸い込まれるため、 ラベル無し引数に値を与える事ができない


末尾クロージャ

func f(aa: Int, bb: Int, cc: () -> Void) {}

f(aa: 1, bb: 2) { }

不正な末尾クロージャ渡し

func f(aa: Int, bb: Int, cc: Int) {}

f(aa: 1, bb: 2) { }
// trailing closure passed to parameter of type 'Int' that does not accept a closure

ジェネリクスと末尾クロージャ

func f<T>(type: T.Type, aa: Int, bb: Int, cc: T) {}

f(type: (() -> Void).self, aa: 1, bb: 2) { }

ジェネリクスと不正な末尾クロージャ渡し

func f<T>(type: T.Type, aa: Int, bb: Int, cc: T) {}

f(type: Int.self, aa: 1, bb: 2) { }
// cannot convert value of type '() -> ()' to expected argument type 'Int'
  • 末尾クロージャ自体は許容されてしまって、その先で型エラーになる

末尾クロージャと後続のデフォルト付き引数

func f(aa: Int, bb: () -> Void, cc: Int = 0) {}

f(aa: 1) { }

やってみて!


末尾クロージャと後続のデフォルト付き引数

func f(aa: Int, bb: () -> Void, cc: Int = 0) {}

f(aa: 1) { }
  • 最近できるようになった

末尾クロージャと後続のデフォルト付き引数の順番間違い

func f(aa: Int, bb: () -> Void, cc: Int = 0) {}

f(aa: 1, cc: 3) { }

やってみて!


末尾クロージャと後続のデフォルト付き引数の順番間違い

func f(aa: Int, bb: () -> Void, cc: Int = 0) {}

f(aa: 1, cc: 3) { }
// OK
  • バグ?

末尾クロージャと後続の複数のデフォルト付き引数

func f(aa: Int, bb: () -> Void, cc: Int = 0, dd: Int = 0) {}

f(aa: 1) { }

やってみて!


末尾クロージャと後続の複数のデフォルト付き引数

func f(aa: Int, bb: () -> Void, cc: Int = 0, dd: Int = 0) {}

f(aa: 1) { }
// trailing closure passed to parameter of type 'Int' that does not accept a closure
// missing argument for parameter 'bb' in call
  • できない(なんでや)

  • 僕がPRを出してマージされ、実装された https://github.com/apple/swift/pull/29845

  • しかし、いろいろあってrevertされた😔 https://github.com/apple/swift/pull/30656


可変長引数と末尾クロージャ

func f(aa: Int, bb: Int, cc: () -> Void...) {}

f(aa: 1, bb: 2) {}
// OK

f(aa: 1, bb: 2, cc: {}, {})
// OK

f(aa: 1, bb: 2, cc: {}) {}

やってみて!


可変長引数と末尾クロージャ

func f(aa: Int, bb: Int, cc: () -> Void...) {}

f(aa: 1, bb: 2) {}
// OK

f(aa: 1, bb: 2, cc: {}, {})
// OK

f(aa: 1, bb: 2, cc: {}) {}
// extra argument 'cc' in call
  • 末尾クロージャのマッチがされると可変長機能が効かない

  • バグ?


ラベルのtypo検出マッチ

func f(aa: Int, bb: Int, cc: Int) {}

f(aa: 1, dd: 4, bbx: 2, cc: 3)
// extra argument 'dd' in call

f(aa: 1, dd: 4, ee: 5, cc: 3)
// extra arguments at positions #2, #3 in call
// missing argument for parameter 'bb' in call
  • 編集距離がある条件を満たす時、typo扱いする

  • 1件目はbbx:bb:のtypoだと判定されているので、2件目とは異なるエラーになる


複数の末尾クロージャ

UIView.animate(withDuration: 0.3) {
  self.view.alpha = 0
} completion: { _ in
  self.view.removeFromSuperview()
}
  • 最近AcceptされたSE-0279

  • さらに複雑になったね!😇


不足引数の特殊挙動

func f(_ aa: Int, _ bb: Int) {}

f(1)
// missing argument for parameter #2 in call
  • これは普通

不足引数の特殊挙動

func f(_ aa: String, _ bb: Int) {}

f(1)

やってみて!


不足引数の特殊挙動

func f(_ aa: String, _ bb: Int) {}

f(1)
//  missing argument for parameter #1 in call
  • 引数の1_ bb:に渡された事になっている

  • マッチングで型を見ている?


不足引数の特殊挙動

  • マッチングは通常通りやっている

  • 診断フェーズで後から解釈変更している (型エラーなど他のFixスコアと矛盾した振る舞いを生じうる・・・)

  • [Diagnostics] Port missing argument(s) diagnostics #27362 https://github.com/apple/swift/pull/27362


不足引数の特殊挙動

func f(_ aa: String, _ bb: Int, cc: Int) {}

f(1, cc: 3)

やってみて!


不足引数の特殊挙動

func f(_ aa: String, _ bb: Int, cc: Int) {}

f(1, cc: 3)
// missing argument for parameter #2 in call
// cannot convert value of type 'Int' to expected argument type 'String'
  • 3引数だと発動しない(なんでや)

不足引数の特殊挙動

func f(aa: String, bb: Int) {}

f(aa: 1)

やってみて!


不足引数の特殊挙動

func f(aa: String, bb: Int) {}

f(aa: 1)
// missing argument for parameter 'aa' in call
  • ラベルがあっても発動する

  • 意味不明な振る舞いになる、バグ?


不足引数の特殊挙動

func f(aa: String = "", bb: Int) {}

f(aa: 1)

やってみて!


不足引数の特殊挙動

func f(aa: String = "", bb: Int) {}

f(aa: 1)
// missing argument for parameter 'aa' in call
  • デフォルト引数を与える事で完全に意味不明に

バグや謎挙動をみつけよう

  • 複雑なので、仕様が組み合わさる部分などで怪しい挙動があったりする

  • クラッシュバグもあった(ワシが直した) https://github.com/apple/swift/pull/30348

  • SE-0279は新しい爆弾かも💣


まとめ

  • Swift5.2では、エラーを修復しながら型推論を進めるCSFix機構が入ったことで、 オーバーロードが関わるコンパイルエラーがより親切になった

  • その実現には、複数の推論解を優先度付けする既存の仕組みをうまく活用した

  • ラベルマッチはとても複雑

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment