[インデックス 1444] ファイルの概要
このコミットは、Goコンパイラの内部における型変換の挙動、特にスライス型エイリアス間の変換に関するバグ修正です。src/cmd/6g/cgen.c
とsrc/cmd/gc/walk.c
の2つのファイルが変更されています。
src/cmd/6g/cgen.c
: 6gコンパイラのコード生成部分。agen
関数は、式のメモリアドレスを生成する役割を担います。src/cmd/gc/walk.c
: GoコンパイラのAST (Abstract Syntax Tree) ウォーカー/オプティマイザの一部。ASTを走査し、最適化や型チェック、コード生成前の変換を行います。
コミット
commit 1b7881adb4715e337375ae3a2668237c569ff946
Author: Russ Cox <rsc@golang.org>
Date: Thu Jan 8 15:01:22 2009 -0800
fix:
type T []int
var a []int
var t = T(a)
R=ken
OCL=22341
CL=22341
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1b7881adb4715e337375ae3a2668237c569ff946
元コミット内容
fix:
type T []int
var a []int
var t = T(a)
R=ken
OCL=22341
CL=22341
変更の背景
このコミットは、Go言語のコンパイラが特定の型変換(特に、基底型が同じであるスライス型エイリアス間の変換)を誤って処理していたバグを修正するために導入されました。コミットメッセージに示されている例:
type T []int
var a []int
var t = T(a)
ここで、T
は[]int
の型エイリアスです。a
は[]int
型の変数であり、t
はT
型の変数です。T(a)
という変換は、Goの型システムにおいて、基底型が同じであるため「非自明でない(non-trivial)」変換として扱われるべきです。つまり、実行時に特別なコードを生成する必要がなく、単に型情報を変更するだけで済む変換です。
しかし、このバグが存在するバージョンでは、コンパイラがこのようなケースを正しく認識せず、誤ったコード生成や型推論を行っていた可能性があります。これにより、プログラムが期待通りに動作しない、あるいはコンパイルエラーが発生するといった問題が生じていました。この修正は、コンパイラがこのような「非自明でない」型変換を正しく処理し、効率的かつ正確なコードを生成できるようにすることを目的としています。
前提知識の解説
Go言語の型システムと型変換
Go言語は静的型付け言語であり、厳格な型システムを持っています。異なる型間の操作は、明示的な型変換(型アサーションや型キャストとは異なる)を必要とします。
- 基底型 (Underlying Type): Goの型システムにおいて、ある型が別の型のエイリアスである場合、元の型を「基底型」と呼びます。例えば、
type MyInt int
と定義した場合、MyInt
の基底型はint
です。 - 型エイリアス (Type Alias):
type NewType OldType
のように定義される型。NewType
はOldType
と異なる型として扱われますが、基底型は同じです。 - スライス (Slice): Goのスライスは、配列のセグメントを参照するデータ構造です。内部的には、ポインタ、長さ、容量の3つの要素で構成されます。
[]int
はint
型の要素を持つスライス型です。 - 型変換 (Type Conversion):
T(expression)
の形式で記述され、expression
の値を型T
に変換します。Goの型変換は、C/C++のような暗黙的な型変換とは異なり、明示的に記述する必要があります。基底型が同じである型間の変換は、多くの場合、実行時のコストを伴いません。
Goコンパイラの内部構造
Goコンパイラは、ソースコードを機械語に変換する過程で、いくつかのフェーズを経ます。
- 字句解析 (Lexing): ソースコードをトークンに分割します。
- 構文解析 (Parsing): トークンから抽象構文木 (AST) を構築します。
- 型チェックとASTウォーク (Type Checking and AST Walk): ASTを走査し、型の整合性をチェックし、最適化やコード生成のための変換を行います。
walk.c
: このファイルは、ASTを走査し、様々な最適化や変換を行う「ウォーカー」のロジックを含んでいます。例えば、定数畳み込み、不要なコードの削除、特定の操作の正規化などが行われます。型変換もこのフェーズで処理されることがあります。ONAME
: ASTノードの操作コード(Opcode)の一つで、名前(変数名、関数名など)を表します。OCONV
: ASTノードの操作コードの一つで、型変換を表します。
- コード生成 (Code Generation): 変換されたASTから、ターゲットアーキテクチャの機械語コードを生成します。
cgen.c
: このファイルは、特定のアーキテクチャ(この場合は6g
、つまりamd64アーキテクチャ)向けのコード生成ロジックを含んでいます。agen
関数は、式の「アドレス」を生成する役割を担います。これは、変数のメモリ上の位置を特定したり、ポインタを扱う際に重要になります。
eqtype
関数
eqtype(type1, type2, flag)
は、Goコンパイラ内部で使用される関数で、2つの型が等しいかどうかを比較します。flag
引数によって比較の厳密さが変わることがあります。このコミットでは、基底型が同じであるかどうかの比較に用いられています。
技術的詳細
この修正は、GoコンパイラのAST処理とコード生成のフェーズにおける、型変換の取り扱いを改善します。
src/cmd/6g/cgen.c
の変更
agen
関数は、ASTノードn
のアドレスを生成する役割を担っています。OCONV
(型変換)の場合、以前は特別な処理がありませんでした。このコミットでは、OCONV
ケースが追加され、型変換が「非自明でない(non-trivial)」かどうかをeqtype
関数でチェックします。
!eqtype(n->type, nl->type, 0)
: これは、変換後の型n->type
と変換元の型nl->type
が、基底型レベルで等しくない場合にtrue
となります。つまり、実行時に何らかの操作が必要な「自明な(trivial)」変換(例:int
からfloat64
への変換)の場合です。fatal("agen: non-trivial OCONV")
: もしagen
が「非自明な」型変換に遭遇した場合、それはエラーと見なされ、コンパイラが異常終了します。これは、agen
がアドレス生成のみを担当し、複雑な型変換のロジックは別の場所(例えばwalk.c
)で処理されるべきであるという設計思想を反映しています。agen(nl, res); return;
: もし型変換がeqtype
によって「非自明でない」(つまり、基底型が同じで、実行時の操作が不要)と判断された場合、agen
は単に変換元の式nl
のアドレスを生成し、その結果をres
に格納して終了します。これは、T(a)
のようなケースで、a
のアドレスをそのままt
のアドレスとして使用できることを意味します。これにより、不要なコード生成が回避され、コンパイルされたコードの効率が向上します。
src/cmd/gc/walk.c
の変更
walk.c
のloop
関数内では、nil
変換(nil
を特定の型に変換する操作)が処理されています。
if(eqtype(t, l->type, 0))
: 変換後の型t
と変換元の型l->type
が基底型レベルで等しい場合、つまり「非自明でない」変換の場合の処理ブロックです。if(l->op != ONAME)
: 変換元の式l
が単なる名前(変数名など)でない場合、つまりより複雑な式である場合に、追加の処理が必要になります。indir(n, l);
:indir
関数は、間接参照(ポインタのデリファレンスなど)を処理する可能性があります。この文脈では、l
の値をn
に間接的に割り当てるような操作を示唆しています。n->type = t;
: この行が追加された重要な変更点です。 変換後のノードn
の型を、正しくターゲット型t
に設定します。以前は、l->op != ONAME
の条件が真の場合にn->type
が更新されず、型情報が不整合になる可能性がありました。特に、T(a)
のようなケースで、a
がONAME
でない場合に、t
の型が正しくT
として認識されない問題があったと考えられます。goto ret;
: 処理が完了したら、関数の終了点にジャンプします。
このwalk.c
の変更は、nil
変換の文脈で、基底型が同じである型変換において、変換後のASTノードの型が正しく設定されることを保証します。これにより、後続のコンパイラフェーズ(特にコード生成)が正しい型情報に基づいて処理を進めることができるようになります。
コアとなるコードの変更箇所
diff --git a/src/cmd/6g/cgen.c b/src/cmd/6g/cgen.c
index 0ab1be5c10..228916417f 100644
--- a/src/cmd/6g/cgen.c
+++ b/src/cmd/6g/cgen.c
@@ -415,6 +415,12 @@ agen(Node *n, Node *res)\n \tfatal(\"agen: unknown op %N\", n);\n \tbreak;\n \n+\tcase OCONV:\n+\t\tif(!eqtype(n->type, nl->type, 0))\n+\t\t\tfatal(\"agen: non-trivial OCONV\");\n+\t\tagen(nl, res);\n+\t\treturn;\n+\n \tcase OCALLMETH:\n \t\tcgen_callmeth(n, 0);\n \t\tcgen_aret(n, res);\ndiff --git a/src/cmd/gc/walk.c b/src/cmd/gc/walk.c
index e0eb648175..cfc3154fdf 100644
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -563,8 +563,10 @@ loop:\n \n \t\t// nil conversion\n \t\tif(eqtype(t, l->type, 0)) {\n-\t\t\tif(l->op != ONAME)\n+\t\t\tif(l->op != ONAME) {\n \t\t\t\tindir(n, l);\n+\t\t\t\tn->type = t;\n+\t\t\t}\n \t\t\tgoto ret;\n \t\t}\n \n```
## コアとなるコードの解説
### `src/cmd/6g/cgen.c` の変更点
`agen`関数は、Goコンパイラのバックエンド(この場合は`6g`、amd64アーキテクチャ向け)で、式のメモリアドレスを生成する役割を担います。
- **追加された `case OCONV:` ブロック**:
- `case OCONV:` は、ASTノードが型変換(`OCONV`オペレーション)を表す場合に実行されるコードです。
- `if(!eqtype(n->type, nl->type, 0))`:ここで重要なチェックが行われます。`n->type`は変換後の型、`nl->type`は変換元の型です。`eqtype`関数は2つの型が等しいかどうかを比較します。`0`は比較のフラグで、この場合は基底型が同じであれば等しいと見なすことを意味します。
- もし`!eqtype(...)`が`true`、つまり変換後の型と変換元の型が基底型レベルで異なる場合(例: `int`から`float64`への変換など、実行時に実際のデータ変換が必要な「自明な」変換)、`fatal("agen: non-trivial OCONV")`が呼び出され、コンパイラはエラーで終了します。これは、`agen`がアドレス生成のみを担当し、複雑な型変換のロジックは別のコンパイラフェーズ(通常は`walk.c`のようなASTウォーカー)で処理されるべきであるという設計原則を強制するためです。`agen`は、データそのものの変換ではなく、そのデータがメモリ上のどこにあるか、というアドレス情報に関心があります。
- もし`!eqtype(...)`が`false`、つまり変換後の型と変換元の型が基底型レベルで同じ場合(例: `type T []int; var a []int; var t = T(a)` のようなケース)、これは「非自明な」変換と見なされます。この場合、実行時にデータそのものを変換する必要はなく、単に型情報を変更するだけで済みます。
- `agen(nl, res);`: 「非自明な」型変換の場合、`agen`は変換元の式`nl`のアドレスを再帰的に生成し、その結果を`res`に格納します。これは、`T(a)`のような変換において、`t`のアドレスは`a`のアドレスと物理的に同じであるため、`a`のアドレスをそのまま利用できることを意味します。これにより、不要なコード生成が回避され、コンパイルされたコードの効率が向上します。
- `return;`: 処理が完了したため、関数を終了します。
この変更により、`6g`コンパイラは、基底型が同じである型エイリアス間の変換(例: スライス型エイリアス間の変換)を、実行時のオーバーヘッドなしに正しく処理できるようになりました。
### `src/cmd/gc/walk.c` の変更点
`walk.c`の`loop`関数は、GoコンパイラのASTウォーカーの一部であり、ASTを走査しながら様々な最適化や変換を行います。この変更は、特に`nil`変換の文脈で、型情報の整合性を保証します。
- **変更された `if(eqtype(t, l->type, 0))` ブロック**:
- このブロックは、変換後の型`t`と変換元の型`l->type`が基底型レベルで等しい場合(つまり「非自明な」変換の場合)に実行されます。
- `if(l->op != ONAME)`: この条件は、変換元の式`l`が単なる名前(変数名など)ではない場合、つまりより複雑な式(例: 関数呼び出しの結果、構造体のフィールドアクセスなど)である場合に真となります。
- `indir(n, l);`: この行は以前から存在しており、`l`の値を`n`に間接的に割り当てるような操作を示唆しています。具体的な動作は`indir`関数の実装に依存しますが、一般的にはポインタのデリファレンスやアドレス操作に関連します。
- `n->type = t;`: **この行が追加された最も重要な変更点です。** 変換後のASTノード`n`の型を、正しくターゲット型`t`に設定します。以前は、`l->op != ONAME`の条件が真の場合に`n->type`が更新されず、その結果、AST内の型情報が不整合になる可能性がありました。特に、`T(a)`のようなケースで、`a`が`ONAME`でない場合に、`t`の型が正しく`T`として認識されない問題があったと考えられます。この修正により、後続のコンパイラフェーズ(特にコード生成)が正しい型情報に基づいて処理を進めることができるようになります。
- `}`: `if(l->op != ONAME)`ブロックの閉じ括弧が追加され、`n->type = t;`がこのブロック内に含まれるようになりました。
- `goto ret;`: 処理が完了したら、関数の終了点にジャンプします。
この`walk.c`の変更は、`nil`変換の文脈で、基底型が同じである型変換において、変換後のASTノードの型が正しく設定されることを保証します。これにより、コンパイラが型エイリアス間の変換をより堅牢に処理し、正確なコードを生成できるようになります。
## 関連リンク
特になし。
## 参考にした情報源リンク
- [https://github.com/golang/go/commit/1b7881adb4715e337375ae3a2668237c569ff946](https://github.com/golang/go/commit/1b7881adb4715e337375ae3a2668237c569ff946)