Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 18854] ファイルの概要

このコミットは、Goコンパイラのcmd/gcパッケージにおけるselectステートメントの処理に関するバグ修正です。具体的には、select句の引数に暗黙的な型変換が関与する場合に発生していた、誤った型エラー("spurious type errors")を解決します。

コミット

commit fcc10bc0f1bed00e951f26bd32aaeea5d6d691b3
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Thu Mar 13 08:14:05 2014 +0100

    cmd/gc: fix spurious type errors in walkselect.
    
    The lowering to runtime calls introduces hidden pointers to the
    arguments of select clauses. When implicit conversions were
    involved it could end up with incompatible pointers. Since the
    pointed-to types have the same representation, we can introduce a
    forced conversion.
    
    Fixes #6847.
    
    LGTM=rsc
    R=rsc, iant, khr
    CC=golang-codereviews
    https://golang.org/cl/72380043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/fcc10bc0f1bed00e951f26bd32aaeea5d6d691b3

元コミット内容

cmd/gc: fix spurious type errors in walkselect.

select句の引数に対するランタイム呼び出しへの変換(lowering)が、隠れたポインタを導入する際に、暗黙的な型変換が関与すると互換性のないポインタになる可能性があった。ポインタが指す型が同じ表現を持つ場合、強制的な型変換を導入することでこの問題を解決できる。

Issue #6847 を修正。

変更の背景

Go言語のselectステートメントは、複数のチャネル操作を待機し、準備ができた最初の操作を実行するための強力な並行処理プリミティブです。コンパイラ(cmd/gc)は、Goのソースコードを機械語に変換する過程で、selectステートメントのような高レベルな構文を、より低レベルなランタイム関数呼び出しに「lowering」(変換)します。この変換プロセスにおいて、チャネル操作の引数(送受信される値やチャネル自体)は、ランタイム関数に渡される際に内部的にポインタとして扱われることがあります。

このコミットが修正しようとしている問題は、selectステートメント内で暗黙的な型変換(例えば、chan intからchan<- intへの変換や、インターフェース型への変換など)が発生する場合に顕在化しました。コンパイラがこれらの変換を処理する際、ランタイム呼び出しのために生成される「隠れたポインタ」が、期待される型と異なる型を持つと誤って判断されることがありました。具体的には、ポインタが指す基底の型は互換性がある(例えば、同じメモリ表現を持つ)にもかかわらず、コンパイラが型チェックの段階で「互換性のないポインタ」としてエラーを報告してしまうという「spurious type errors」(見かけ上の型エラー)が発生していました。

この問題は、Goの型システムとランタイムの連携における微妙な不整合を示しており、開発者が正当なコードを書いたにもかかわらず、コンパイラによって不当なエラーに直面するというユーザーエクスペリエンスの低下を引き起こしていました。Issue #6847として報告され、このコミットによって解決されました。

前提知識の解説

  • Goコンパイラ (cmd/gc): Go言語の公式コンパイラです。Goのソースコードを解析し、抽象構文木(AST)を構築し、型チェック、最適化、そして最終的に実行可能なバイナリコードを生成します。cmd/gcは、Goのランタイムと密接に連携して動作します。
  • selectステートメント: Goにおける並行処理の重要な要素で、複数のチャネル操作(送受信)を同時に待機し、準備ができた最初の操作を実行します。デッドロックの回避やタイムアウトの実装にも利用されます。
  • チャネル (Channels): Goのゴルーチン間で値を送受信するための通信メカニズムです。チャネルは型付けされており、特定の型の値のみを送受信できます。
    • 双方向チャネル (chan T): 送受信両方に使用できるチャネル。
    • 送信専用チャネル (chan<- T): 送信のみ可能なチャネル。
    • 受信専用チャネル (<-chan T): 受信のみ可能なチャネル。 これらのチャネル型の間には、特定の規則に基づいた暗黙的な型変換(例えば、双方向チャネルから送信専用または受信専用チャネルへの変換)が許可されています。
  • 型チェック (Type Checking): コンパイルプロセスの段階の一つで、プログラムがGoの型規則に準拠しているかを確認します。これにより、型に関するエラーが実行時ではなくコンパイル時に検出されます。
  • Lowing (低レベル化): コンパイラが、高レベルな言語構造(例: selectステートメント、forループ)を、より低レベルな中間表現やランタイム関数呼び出しに変換するプロセスです。これにより、複雑な構文が機械が理解しやすい単純な操作に分解されます。
  • OCONVNOP (Convert No-Op): Goコンパイラの内部表現におけるノードタイプの一つです。これは「何もしない変換」を意味し、型は異なるがメモリ表現が同じであるため、実質的なコード生成を伴わない型変換を示すために使用されます。これは、コンパイラが型システムの一貫性を保ちつつ、実行時にオーバーヘッドなしで異なる型を扱えるようにするために重要です。

技術的詳細

このコミットの核心は、src/cmd/gc/select.cファイル内のwalkselect関数にあります。walkselectは、selectステートメントの抽象構文木を走査し、それをGoランタイムのselect関連関数(例: runtime.selectgo)への呼び出しに変換する役割を担っています。

問題は、チャネルの送受信操作の引数(例えば、case ch <- val:valcase val = <-ch:val)が、ランタイム関数に渡される際に、そのアドレス(ポインタ)が使用される点にありました。コンパイラはこれらの引数に対して一時的な変数を作成し、そのアドレスをランタイムに渡すことで、ランタイムが直接値を操作できるようにします。

しかし、チャネルの型変換(例: chan intからchan<- intへの暗黙的な変換)が絡む場合、コンパイラが生成する一時変数の型と、ランタイムが期待するポインタの型との間で、見かけ上の不一致が生じることがありました。コミットメッセージが述べているように、「pointed-to types have the same representation」(ポインタが指す型が同じ表現を持つ)にもかかわらず、コンパイラの型チェックロジックがこれをエラーと判断していました。これは、例えばchan intchan<- intは異なる型ですが、内部的には同じチャネルオブジェクトを指しており、そのメモリ表現は同じである、といった状況を指します。

この修正は、このような状況を検出し、OCONVNOPノードを挿入することで解決します。OCONVNOPは、コンパイラに対して「この型変換は安全であり、実行時に特別なコードを生成する必要はない」と指示します。これにより、コンパイラは型チェックをパスし、ランタイムは期待通りに動作します。

具体的には、select.cの変更箇所では、チャネルの送受信操作の引数に対してOADDR(アドレス取得)ノードを生成した後、そのポインタの型がチャネルの要素型と異なる場合に、OCONVNOPノードを介して強制的な型変換を行うようにしています。これにより、ランタイムに渡されるポインタの型が、ランタイムが期待する型と一致するようになります。

コアとなるコードの変更箇所

変更は主にsrc/cmd/gc/select.cファイルに集中しています。

  1. selectの受信ケース (case val = <-ch) の修正: walkselect関数内のcase ORECV:ブロックにおいて、受信操作の左辺(値を受け取る変数)のアドレスを取得する際に、以下のコードが追加されました。

    if(!eqtype(ch->type->type, n->left->type->type)) {
        n->left = nod(OCONVNOP, n->left, N);
        n->left->type = ptrto(ch->type->type);
        n->left->typecheck = 1;
    }
    

    これは、チャネルの要素型(ch->type->type)と、受信する値の型(n->left->type->type)が異なる場合に、n->left(受信変数のアドレスを表すノード)に対してOCONVNOPを適用し、チャネルの要素型へのポインタ型に強制的に変換しています。

  2. selectの送信ケース (case ch <- val) の修正: walkselect関数内のcase OSEND:ブロックにおいて、送信操作の右辺(送信する値)のアドレスを取得する際に、以下のコードが追加されました。

    // cast to appropriate type if necessary.
    if(!eqtype(n->right->type->type, n->left->type->type) &&
        assignop(n->right->type->type, n->left->type->type, nil) == OCONVNOP) {
        n->right = nod(OCONVNOP, n->right, N);
        n->right->type = ptrto(n->left->type->type);
        n->right->typecheck = 1;
    }
    

    これは、送信する値の型(n->right->type->type)と、チャネルの要素型(n->left->type->type、ここではチャネルの型から導出される)が異なり、かつその変換がOCONVNOPで処理できる場合(つまり、メモリ表現が同じである場合)に、n->right(送信する値のアドレスを表すノード)に対してOCONVNOPを適用し、チャネルの要素型へのポインタ型に強制的に変換しています。

また、test/fixedbugs/issue6847.goという新しいテストファイルが追加され、このバグが修正されたことを検証しています。このテストファイルには、チャネルの暗黙的な型変換(双方向チャネルから送信/受信専用チャネル、インターフェース型への変換など)を含む様々なselectステートメントのパターンが含まれており、これらがコンパイルエラーにならないことを確認しています。

コアとなるコードの解説

追加されたコードは、GoコンパイラのAST(抽象構文木)変換フェーズの一部であるwalkselect関数内で動作します。この関数は、selectステートメントを処理する際に、チャネル操作の引数(送受信される値)をランタイム関数に渡すための準備を行います。

Goのランタイムは、チャネル操作を行う際に、送受信される値のメモリ上のアドレスを必要とします。そのため、コンパイラはOADDRノード(アドレス取得演算子)を使用して、これらの値へのポインタを生成します。

問題は、Goの型システムが厳密であるため、たとえメモリ表現が同じであっても、異なる型を持つポインタを直接扱うことを許さない点にありました。例えば、chan intchan<- intは異なる型ですが、これらが指す基底のチャネルオブジェクトは同じメモリレイアウトを持つことがあります。同様に、具体的な型とそれが実装するインターフェース型の間でも、このような状況が発生し得ます。

追加されたifブロックは、この特定のシナリオを検出します。 eqtype(type1, type2)関数は、二つの型が完全に等しいかどうかをチェックします。 assignop(type1, type2, nil) == OCONVNOPは、type1からtype2への代入が、実質的なコード生成を伴わないOCONVNOP変換で可能かどうかをチェックします。これは、型は異なるがメモリ表現が互換性がある場合に真となります。

これらの条件が満たされた場合、nod(OCONVNOP, original_node, N)によって新しいOCONVNOPノードが作成されます。このノードは、original_node(アドレスを取得したノード)をラップし、その型をptrto(target_type)target_typeへのポインタ型)に設定します。n->left->typecheck = 1;またはn->right->typecheck = 1;は、このノードが既に型チェック済みであることをコンパイラに示します。

これにより、コンパイラは、型チェックの段階で「互換性のないポインタ」という誤ったエラーを報告することなく、ランタイムが期待する正しいポインタ型でチャネル操作の引数を処理できるようになります。これは、Goの型システムが提供する安全性と、ランタイムの効率的な動作を両立させるための重要な修正です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (Channels, Select): https://go.dev/tour/concurrency/5
  • Goコンパイラの内部構造に関する一般的な情報 (Go compiler internals): https://go.dev/doc/articles/go-compiler-internals (これは一般的な情報源であり、特定のコミットに直接関連するものではありませんが、背景知識として有用です。)
  • Goの型システムに関する情報 (Go type system): https://go.dev/blog/go-type-system (これも一般的な情報源であり、特定のコミットに直接関連するものではありませんが、背景知識として有用です。)
  • Goのselectステートメントのランタイム実装に関する情報 (Go runtime select implementation): Goのソースコード(src/runtime/select.goなど)が最も正確な情報源ですが、これはより深いレベルの分析が必要となります。