この記事の要点
- Swift 5.9 では、コンパイラと LLDB デバッガに、日々のデバッグ作業を助けるいくつかの新機能が入りました。本記事はその中から 3 つを紹介します。
p/poコマンドの高速化と整理。pとpoは、重いexpressionではなく新しいdwim-printコマンドのエイリアスになり、変数の表示が高速になりました。pは$R0のような永続結果変数(persistent result variable)を作らなくなり、poは生のアドレスを渡すだけでもオブジェクトを表示できるようになりました。- 式評価でジェネリック型パラメータを参照可能に。 ブレークポイントで止まった場所から
po T.selfのように、ジェネリック関数の型パラメータの具体的な型を調べたり、T.self == String.selfのようなブレークポイント条件を書いたりできます。 - より正確なスコープ情報。 コンパイラがより精密な lexical scope をデバッグ情報に出力するようになり、まだスコープに入っていない変数(初期化前の
letなど)を未初期化のまま表示してしまう問題が解消されました。
p / po の高速化と永続結果変数の抑制
LLDB には、変数を調べる p と、オブジェクトの debugDescription プロパティを呼び出す po という短縮コマンドがあります。これらはもともと、やや重量級の expression(p)と expression -O(po)のエイリアスでした。Swift 5.9 では、p と po は新しい dwim-print コマンドのエイリアスに定義し直されました。
dwim-print は、最もユーザーにとって扱いやすい方法で値を表示します。”DWIM” は “Do What I Mean”(言いたいことを汲み取る)の頭字語です。具体的には、変数を表示するとき、dwim-print は重い式評価器ではなく frame variable(v)と同じ実装を使うため、表示が高速になります。
高速になっただけでなく、p は $R0 のような 永続結果変数(persistent result variable)を作らなくなりました。これらはデバッグ中に使われないことが多いうえに、オーバーヘッドを生み、保持しているオブジェクトを retain し続けるため、プログラムの実行に予期しない副作用を与えることがありました。
ときどき永続結果が欲しい場合は、p の代わりに expression(あるいは expr のような一意な接頭辞)を直接使えます。常に永続結果を有効にしたいなら、LLDB のエイリアス機能を使って ~/.lldbinit に次を書いておけます。
command unalias p
command alias p dwim-print --persistent-result on --
dwim-print は po にも新しい機能を与えます。po は、生のアドレスを渡すだけでも Swift のオブジェクトを表示できるようになりました。po <オブジェクトのアドレス> を実行すると、LLDB に組み込まれた Swift コンパイラが内部的に unsafeBitCast(<オブジェクトのアドレス>, to: AnyObject.self) を評価し、期待どおりの結果を生成します。
Swift 5.9 より前:
(lldb) po 0x00006000025c43d0
(Int) 105553155867600
Swift 5.9 では:
(lldb) po 0x00006000025c43d0
<MyApp.AppDelegate: 0x6000025c43d0>
式の中でジェネリック型パラメータを参照する
LLDB は、式評価の中でジェネリック型パラメータを参照できるようになりました。たとえば次のコードで考えます。
func use<T>(_ t: T) {
print(t) // break here
}
use(5)
use("Hello!")
use で止まった状態で po T.self を実行すると、1 回目の呼び出し経由なら Int、2 回目なら String と表示されます。
ジェネリックの具体的な型を表示できるだけでなく、これを使って具体的な型を条件にすることもできます。たとえば上のブレークポイントに T.self == String.self という条件を付けると、変数 t が String のときだけ use で止まるようになります(この最後の例は Swift 5.9 ツールチェインの nightly ビルドでのみ動作します)。
より精密な lexical scope 情報
Swift コンパイラは、デバッグ情報により正確な lexical scope を出力するようになりました。これにより、デバッガが異なる変数をより正しく区別できます。たとえば次の例には x という名前の変数が複数あります。
func f(x: AnyObject?) {
// 関数パラメータ x: AnyObject?
guard let x else {}
// ローカル変数 x: AnyObject。これは関数引数の x をシャドーする
...
}
Swift 5.9 では、コンパイラがより正確な ASTScope 情報を使って lexical scope の階層をデバッグ情報に生成するようになり、その結果デバッガの挙動にいくつかの変化があります。
次の例では、ローカル変数 a は getInt() の呼び出し時点ではまだスコープに入っておらず、代入されて初めて参照できるようになります。以前のバージョンのコンパイラが生成したデバッグ情報では、getInt() の呼び出し時点で a の中身として未初期化メモリを表示してしまうことがありました。Swift 5.9 では、変数 a は初期化された後にだけ見えるようになります。
1 func getInt() -> Int { return 42 }
2
3 func f() {
4 let a = getInt()
^
5 print(a)
6 }
(lldb) p a
error: <EXPR>:3:1: error: cannot find 'a' in scope
(lldb) n
3 func f() {
4 let a = getInt()
5 print(a)
^
6 }
(lldb) p a
42
デバッグについて参加するには
Swift のデバッグや LLDB についてさらに学んだり、フィードバックを送ったり、ツール自体にコントリビュートしたりしたい場合は、Swift 開発フォーラムの LLDB セクション に参加できます。
関連リンク
- LLDB プロジェクト — LLDB デバッガの公式サイト
- Swift 開発フォーラムの LLDB セクション
- これらの機能を含むリリースの全体像は Swift 5.9 リリース を参照してください。