コンテンツにスキップ

Go 1.26:型構築とサイクル検出の改善(コンパイラの安定性向上)

概要

Go 1.26でコンパイラの型チェッカー内の「型構築(type construction)」処理を大幅改善。一般的なGoプログラマーには直接影響しないが、循環型定義(サイクル)に関する複数のコンパイラクラッシュが修正され、安定性が向上した。型チェッカーが「完全性(completeness)」を追跡しながら型を深さ優先で構築する仕組みを解説する。

詳細

型チェッカーが行う2つの検証

1. AST 内の型が有効であること
   例:マップのキー型は comparable である必要がある

2. 型に対する操作が有効であること
   例:int と string は加算できない

型構築:シンプルなケース

type T []U
type U *int
型チェッカーの動作(深さ優先):
  T を構築しようとする
    → U が必要
    → U を構築 → *int
  → T = [](*int)  完了

型構築:再帰的なケース

type T []U
type U *T
T を構築しようとする
  → U が必要
  → U を構築するには *T が必要
  → T はまだ「不完全」だが、ポインタ経由なので参照可能
  → T が完全になった後で U も完全になる

ポイント:不完全な型への参照を「後で完全になる」前提で許容する。

問題:循環サイクルの検出

// コンパイルエラーになるべきコード
type T [unsafe.Sizeof(T{})]int
問題の構造:
  T を完成させる → Array のサイズが必要
  Array のサイズ → T のサイズが必要
  T のサイズ → T を分解(deconstruct)する必要がある
  → T はまだ不完全 → 無限ループ

サイクルが発生しうる「上流(upstream)」のパターン

// 変換
type T [unsafe.Sizeof(T(42))]int

// 関数呼び出し
func f() T
type T [unsafe.Sizeof(f())]int

// 型アサーション
var i interface{}
type T [unsafe.Sizeof(i.(T))]int

// チャネル受信
type T [unsafe.Sizeof(<-(make(<-chan T)))]int

// マップアクセス
type T [unsafe.Sizeof(make(map[int]T)[42])]int

// ポインタ逆参照
type T [unsafe.Sizeof(*new(T))]int

いずれも「T の結果の型」が必要になる場所。型チェッカーは 結果の型が完全であることを確認し、そうでなければサイクルエラーを報告する

型チェッカーの実装(変換ケースの擬似コード)

func callExpr(call *syntax.CallExpr) operand {
    x := typeOrValue(call.Fun)
    switch x.mode() {
    case typeExpr:
        // T() の場合は型変換
        T := x.typ()
        if !isComplete(T) {
            reportCycleErr(T)  // サイクルを報告
            return invalid
        }
        // ここから先は T を安全に分解できる
    }
}

Go 1.26 で修正されたバグ

Issue 内容
#75918 循環型定義によるコンパイラクラッシュ
#76383 同上(別パターン)
#76384 同上
#76478 同上

これらは複雑な循環型定義に関する稀なエッジケースだが、コンパイラの堅牢性向上につながる。

なぜ重要か / いつ使うか

  • Go コンパイラの挙動について深く理解したいとき
  • unsafe.Sizeof を使った定数式で不思議なコンパイルエラーが出たとき
  • Go のジェネリクスや複雑な型定義を扱うライブラリを実装するとき
  • 「なぜこの型定義はコンパイルエラーになるのか」の原理を追うとき