[インデックス 1064] ファイルの概要
このコミットは、Go言語のコンパイラ(gc
)における型アサーションの処理に関するものです。具体的には、t, ok = i.(T)
の形式でインターフェース値 i
を型 T
にアサートし、その結果と成功を示すブール値 ok
を同時に受け取る構文(二値返却型アサーション)のコンパイル時の挙動を改善しています。変更は src/cmd/gc/walk.c
ファイルに対して行われており、このファイルはGoコンパイラのバックエンドに近い部分で、抽象構文木(AST)を走査し、低レベルのコード生成に適した形式に変換する「ウォーク」処理を担当しています。
コミット
commit 83a798513cf487f6c5e0af919b9cf03246ded0ca
Author: Ken Thompson <ken@golang.org>
Date: Wed Nov 5 15:33:01 2008 -0800
more on t,ok = I.(T)
R=r
OCL=18599
CL=18599
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/83a798513cf487f6c5e0af919b9cf03246ded0ca
元コミット内容
more on t,ok = I.(T)
このコミットメッセージは非常に簡潔ですが、Go言語の初期開発段階における典型的なものです。「t,ok = I.(T)
について、さらに」という内容は、二値返却型アサーションのコンパイルロジックに対する追加の修正や改善が行われたことを示唆しています。これは、Go言語の型システム、特にインターフェースと型アサーションの堅牢性を高めるための継続的な作業の一部です。
変更の背景
Go言語の初期段階では、言語仕様やコンパイラの挙動が頻繁に調整されていました。このコミットが行われた2008年11月は、Go言語が一般に公開される前の、まさに活発な開発期間中でした。
Goの型アサーション i.(T)
は、インターフェース値 i
が特定の型 T
を保持しているかどうかをチェックし、もしそうであればその型 T
の値として返すための構文です。この操作には2つの形式があります。
- 単一値返却:
t = i.(T)
i
がT
型の値を保持していない場合、ランタイムパニックが発生します。
- 二値返却("comma-ok" idiom):
t, ok = i.(T)
i
がT
型の値を保持している場合、t
にその値が代入され、ok
はtrue
になります。i
がT
型の値を保持していない場合、t
にはT
型のゼロ値が代入され、ok
はfalse
になります。パニックは発生しません。
このコミットは、後者の二値返却形式 t, ok = i.(T)
のコンパイル処理、特にgc
コンパイラのwalk
フェーズにおける挙動を修正・改善することを目的としています。初期のコンパイラでは、このような複雑な構文の処理にバグや最適化の余地があったと考えられます。この修正は、コンパイラがこの構文を正しく、かつ効率的に中間表現に変換できるようにするために必要でした。
前提知識の解説
Goコンパイラ (gc
) の構造
Goコンパイラ gc
は、複数のフェーズに分かれて動作します。
- 字句解析 (Lexing): ソースコードをトークンに分割します。
- 構文解析 (Parsing): トークンから抽象構文木 (AST) を構築します。
- 型チェック (Type Checking): ASTの各ノードの型を解決し、型の一貫性を検証します。
- ウォーク (Walk): ASTを走査し、高レベルなASTノードを低レベルな中間表現(IR)に変換します。このフェーズで、Go特有の構文(例:
defer
,go
,select
, 型アサーションなど)が、より基本的な操作のシーケンスに展開されます。src/cmd/gc/walk.c
はこのフェーズの主要な部分です。 - 最適化 (Optimization): IRに対して様々な最適化を適用します。
- コード生成 (Code Generation): IRからターゲットアーキテクチャの機械語コードを生成します。
このコミットが影響する walk.c
は、特にGo言語のセマンティクスを低レベルな操作に変換する重要な役割を担っています。
型アサーション (i.(T)
)
Goにおける型アサーションは、インターフェースの強力な機能の一つです。インターフェースは、メソッドのセットを定義する型であり、任意の具象型がそのメソッドセットを実装していれば、そのインターフェース型として扱われます。
型アサーションは、実行時にインターフェース値が特定の具象型(または別のインターフェース型)であるかどうかを動的にチェックするために使用されます。
i.(T)
: インターフェース値i
が型T
を保持していることをアサートします。T
が具象型の場合、i
がその具象型の値を保持しているかをチェックします。T
がインターフェース型の場合、i
がT
インターフェースを実装する型を保持しているかをチェックします。
二値返却形式 t, ok = i.(T)
は、エラーハンドリングのGoらしいイディオム「comma-ok idiom」の一例です。これにより、型アサーションの失敗をパニックではなく、プログラムで処理可能なブール値として受け取ることができます。
OCONV
ノード
Goコンパイラの内部では、型変換や型アサーションは OCONV
(Operation CONVersion) という種類のASTノードで表現されます。このノードは、変換元と変換先の型、そして変換の種類(例: インターフェースから具象型へのアサーション)に関する情報を含んでいます。
技術的詳細
このコミットは、src/cmd/gc/walk.c
内の walk
関数と multi
関数の2箇所に修正を加えています。これらの関数は、ASTノードを走査し、必要に応じて変換を行う役割を担っています。
walk
関数内の修正
walk
関数は、ASTノードを再帰的に走査し、各ノードに対して適切な処理を適用します。
@@ -469,6 +469,7 @@ loop:
case OCONV:
if(cl == 2 && cr == 1) {
// a,b = i.(T)
+ walktype(r->left, Erv);
if(r->left == N)
break;
et = isandss(r->type, r->left);
この変更は、OCONV
ノードが二値返却型アサーション (cl == 2 && cr == 1
は、左辺が2つの要素(a, b
)で、右辺が1つの要素(i.(T)
)であることを示唆していると推測されます) を表す場合に適用されます。
walktype(r->left, Erv);
の追加は、型アサーションの対象となるインターフェース値 i
(ASTノード r->left
で表される) の型を、Erv
(Expression for return value) のコンテキストでウォークすることを意味します。これは、i
が評価されるべき式であることをコンパイラに明示し、その型情報が正しく処理されるようにするためのものです。これにより、型アサーションの左辺の評価が適切に行われるようになります。
multi
関数内の修正
multi
関数は、複数の値を返す式(例: 関数呼び出し、チャネル受信、そして型アサーションの二値返却)を処理するために使用されます。
@@ -2964,6 +2965,20 @@ multi:
tn = list(n, a);
break;
+ case OCONV:
+ // a,b := i.(T)
+ if(cl != 2)
+ goto badt;
+ walktype(nr->left, Erv);
+ if(!isinter(nr->left->type))
+ goto badt;
+ // a,b = iface
+ a = old2new(nl->left, nr->type);
+ n = a;
+ a = old2new(nl->right, types[TBOOL]);
+ n = list(n, a);
+ break;
+
case ORECV:
if(cl != 2)
goto badt;
@@ -2975,6 +2990,7 @@ multi:
tn = a;
a = old2new(nl->right, types[TBOOL]);
tn = list(n, a);
+ break;
}
tn = rev(n);
return n;
このセクションは、OCONV
ノードが二値返却型アサーション (a,b := i.(T)
) を表す場合の具体的なコード生成ロジックを定義しています。
if(cl != 2) goto badt;
: 左辺が2つの要素でない場合(つまり、二値返却ではない場合)はエラーとします。これは、このcase OCONV
ブロックが二値返却型アサーション専用であることを保証します。walktype(nr->left, Erv);
:walk
関数での修正と同様に、アサート対象のインターフェース値 (nr->left
) をErv
コンテキストでウォークします。if(!isinter(nr->left->type)) goto badt;
: アサート対象の式がインターフェース型でない場合、それは不正な型アサーションであるためエラーとします。型アサーションはインターフェース値に対してのみ意味があります。a = old2new(nl->left, nr->type); n = a;
: これは、型アサーションの結果として得られる値(t
に相当)を処理する部分です。nl->left
は左辺の最初の要素(t
)、nr->type
はアサートされる型T
を指します。old2new
は、ASTノードを新しい形式に変換するユーティリティ関数で、ここではt
にT
型の値を代入する操作を表現するノードを生成していると考えられます。a = old2new(nl->right, types[TBOOL]); n = list(n, a);
: これは、型アサーションの成功/失敗を示すブール値(ok
に相当)を処理する部分です。nl->right
は左辺の2番目の要素(ok
)、types[TBOOL]
はブール型を指します。同様に、ok
にブール値を代入する操作を表現するノードを生成し、前の操作と連結しています。
これらの変更により、Goコンパイラは t, ok = i.(T)
という構文を、実行時にインターフェースの型情報をチェックし、その結果に基づいて2つの変数に適切に値を代入する一連の低レベルな操作に正確に変換できるようになります。
コアとなるコードの変更箇所
src/cmd/gc/walk.c
ファイルの以下の2箇所が変更されています。
walk
関数内のcase OCONV
ブロック(約469行目付近)multi
関数内のcase OCONV
ブロック(約2964行目付近)
コアとなるコードの解説
walk
関数内の変更
// src/cmd/gc/walk.c
// ...
loop:
case OCONV:
if(cl == 2 && cr == 1) {
// a,b = i.(T)
walktype(r->left, Erv); // 追加行
if(r->left == N)
break;
et = isandss(r->type, r->left);
// ...
この追加は、二値返却型アサーションの右辺(インターフェース値 i
)が、コンパイラの「ウォーク」フェーズで適切に処理されることを保証します。walktype
は、式の型情報を解決し、その式が評価されるべきコンテキスト(ここでは Erv
、つまり返り値として扱われる式)で処理されるようにします。これにより、i
の評価が型アサーションの前に正しく行われ、その結果がアサーション操作に渡されるようになります。
multi
関数内の変更
// src/cmd/gc/walk.c
// ...
multi:
tn = list(n, a);
break;
case OCONV: // 追加されたケース
// a,b := i.(T)
if(cl != 2) // 左辺が2つの要素でない場合はエラー
goto badt;
walktype(nr->left, Erv); // インターフェース値をウォーク
if(!isinter(nr->left->type)) // インターフェース型でない場合はエラー
goto badt;
// a,b = iface
a = old2new(nl->left, nr->type); // 最初の返り値 (t) の処理
n = a;
a = old2new(nl->right, types[TBOOL]); // 2番目の返り値 (ok) の処理
n = list(n, a); // 2つの返り値の処理を連結
break; // 追加されたbreak
case ORECV:
if(cl != 2)
goto badt;
// ...
tn = a;
a = old2new(nl->right, types[TBOOL]);
tn = list(n, a);
break; // 追加されたbreak
}
tn = rev(n);
return tn;
multi
関数内の OCONV
ケースは、二値返却型アサーションの具体的なコンパイル戦略を実装しています。
cl != 2
と!isinter(nr->left->type)
のチェックは、入力が期待される二値返却型アサーションの形式と型制約を満たしているかを確認するガード節です。walktype(nr->left, Erv);
は、アサートされるインターフェース値が適切に評価されることを保証します。old2new
を使用した2つのa = ...
行は、型アサーションの結果(値とブール値)をそれぞれ左辺の変数に代入するためのASTノードを生成しています。list(n, a)
はこれらの操作を連結し、最終的な中間表現を構築します。case ORECV:
の最後にbreak;
が追加されたのは、おそらく以前のコードではフォールスルーが発生し、意図しない挙動を引き起こす可能性があったため、明示的にブロックを終了させるためと考えられます。
これらの変更は、Goコンパイラが t, ok = i.(T)
という構文を、正確かつ堅牢に、そしてGoのセマンティクスに沿って機械語に変換するための基盤を強化するものです。
関連リンク
- Go言語の仕様: https://go.dev/ref/spec (特に「Type assertions」のセクション)
- Goコンパイラのソースコード: https://github.com/golang/go/tree/master/src/cmd/compile
参考にした情報源リンク
- Go言語の公式ドキュメント
- Goコンパイラのソースコード(特に
src/cmd/gc/walk.c
の周辺コード) - Go言語の初期開発に関する議論やコミットログ(GitHubの履歴)
- Go言語の型アサーションに関する一般的な解説記事
- Goコンパイラの内部構造に関する技術ブログや論文(もしあれば)
- Go言語の「comma-ok idiom」に関する解説