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

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

このコミットは、Go言語のコンパイラにおけるnew(T)組み込み関数の挙動に関するバグ修正です。具体的には、Tが名前付きのスライス型(例: type MyIntArray []int)である場合に、new(T)が返す式の型が、名前付き型Tではなく、その基底型(この例では[]int)として扱われてしまう問題を修正しています。これにより、コンパイラが型の情報を正しく保持し、後続の型チェックやコード生成で誤った型推論が行われるのを防ぎます。

コミット

  • コミットハッシュ: af5e16cfd9898e53beb1a8d74bcdf00ad9e6b8ba
  • 作者: Russ Cox rsc@golang.org
  • コミット日時: Sat Dec 20 16:30:44 2008 -0800

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

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

元コミット内容

fix new(T) if type T []int.
make sure type of expr is T not just []int

R=ken
OCL=21688
CL=21688

変更の背景

Go言語のnew組み込み関数は、指定された型のゼロ値へのポインタを返します。例えば、new(int)*int型の値を返し、その値は0に初期化されたintを指します。

このコミットが行われた当時のGoコンパイラ(初期バージョン)では、new(T)が呼び出された際に、Ttype MySlice []intのような「名前付きのスライス型」であった場合、コンパイラの内部処理(特にメモリ割り当てを行う部分)で、その名前付き型情報が失われ、基底型である[]intとして扱われてしまう問題がありました。

この型情報の喪失は、コンパイラの型チェックフェーズや、その後のコード生成フェーズにおいて問題を引き起こす可能性がありました。例えば、特定の名前付き型に特化したメソッド呼び出しや型アサーションが正しく機能しない、あるいはコンパイルエラーになる、といった事態が考えられます。この修正は、newによって生成された式の型が、元の名前付き型Tを正確に反映するようにすることで、コンパイラの型システムの一貫性と正確性を保証することを目的としています。

前提知識の解説

Go言語のnew組み込み関数

new(Type)は、Go言語に組み込まれている関数の一つで、引数として与えられたTypeのゼロ値が格納される新しいメモリ領域を割り当て、その領域へのポインタを返します。例えば、new(int)*int型の値を返し、new(struct{})*struct{}型の値を返します。これは、C++におけるnew Type()やJavaにおけるnew Type()とは異なり、コンストラクタを呼び出すわけではありません。

Go言語の型システムにおける「名前付き型」と「基底型」

Go言語では、既存の型に新しい名前を付けて「名前付き型」を定義できます。例えば、type MyInt intと定義すると、MyIntintとは異なる新しい型として扱われます。MyIntの基底型はintです。同様に、type MySlice []intと定義すると、MySlice[]intとは異なる新しい型となり、その基底型は[]intです。

Goの型システムでは、名前付き型と基底型は厳密に区別されます。異なる名前付き型間では、たとえ基底型が同じであっても、明示的な型変換なしには代入や比較ができません。この厳密さが、コンパイラが名前付き型を正しく保持することの重要性を示しています。

Goコンパイラの構造とsrc/cmd/gc/walk.c

Goコンパイラは、ソースコードを機械語に変換する複雑なプロセスを担っています。その主要部分はsrc/cmd/gcディレクトリに格納されており、gcは "Go compiler" の略です。

コンパイルプロセスは、主に以下のフェーズに分けられます。

  1. 字句解析 (Lexing): ソースコードをトークン(単語)のストリームに変換します。
  2. 構文解析 (Parsing): トークンのストリームから抽象構文木 (AST: Abstract Syntax Tree) を構築します。ASTはプログラムの構造を木構造で表現したものです。
  3. 型チェック (Type Checking): ASTを走査し、各式の型が正しいか、型の一貫性が保たれているかを確認します。
  4. 中間表現 (IR: Intermediate Representation) への変換: ASTを、より機械語に近いがまだプラットフォーム非依存な中間表現に変換します。
  5. 最適化 (Optimization): 中間表現を最適化し、より効率的なコードを生成します。
  6. コード生成 (Code Generation): 最適化された中間表現から、ターゲットアーキテクチャの機械語を生成します。

src/cmd/gc/walk.cは、Goコンパイラの「ウォーカー」フェーズに関連するC言語のソースファイルです。このフェーズでは、構文解析によって生成されたASTを走査("walk")し、型チェック、定数畳み込み、一部の最適化、そして最終的なコード生成のための準備を行います。new組み込み関数の処理も、このwalkフェーズの中でASTノードが処理される際に実行されます。

技術的詳細

このバグは、new(T)が呼び出された際に、コンパイラが内部的に配列やスライスを扱うための低レベルな関数(例えばnewarrayのようなもの)を呼び出す過程で発生していました。この低レベル関数は、メモリを割り当て、その基底型(例: []int)に基づいて初期化を行うことはできますが、元の名前付き型(例: MyIntArray)に関する情報を自動的に保持するわけではありませんでした。

結果として、new(MyIntArray)の呼び出しから返されるASTノードの型が、MyIntArrayではなく、その基底型である[]intとして設定されてしまっていました。これは、コンパイラの内部的な型表現が、ソースコードで意図された型と乖離している状態を意味します。

この問題は、特に以下のようなシナリオで顕在化する可能性があります。

  1. 型アサーションの失敗: v := new(MyIntArray); _ = v.(*MyIntArray)のようなコードが、実際には*[]int型として扱われるため、型アサーションが失敗する。
  2. メソッド呼び出しの不一致: MyIntArray型に定義されたメソッドが、[]int型として扱われるために呼び出せない、あるいは予期せぬ動作をする。
  3. コンパイルエラー: 型の不一致が原因で、後続のコンパイルフェーズでエラーが発生する。

この修正は、new操作の結果として生成されたASTノードに対して、明示的に元の名前付き型を再設定することで、この型情報の喪失を防ぎます。これにより、コンパイラはnew(T)の戻り値の型を正確に認識し、Go言語の型システムのセマンティクスに沿った正しい型チェックとコード生成を行うことができるようになります。

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

変更はsrc/cmd/gc/walk.cファイルの一箇所のみです。

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -2671,6 +2671,7 @@ arrayop(Node *n, int top)\
 	r = nod(OCALL, on, r);\
 
 	walktype(r, top);\
+	r->type = t;	// if t had a name, going through newarray lost it
 	break;
 
 	case OSLICE:

コアとなるコードの解説

変更された行は以下の通りです。

r->type = t;	// if t had a name, going through newarray lost it

この行は、arrayop関数(おそらく配列やスライス関連の操作を処理する部分)内で、new組み込み関数によって生成された式のASTノードrの型を、元の意図された型tに明示的に設定し直しています。

  • r: new(T)の呼び出しによって生成された結果のASTノードを表します。このノードは、メモリ割り当て後のスライス/配列のポインタを保持しています。
  • t: new(T)Tとして渡された元の型を表します。このtが、type MyIntArray []intのような名前付き型である場合に、その名前情報が保持されるべきです。
  • r->type = t;: この代入によって、rの型情報が、低レベルなnewarray操作によって失われた名前付き型tに上書きされます。

コメント// if t had a name, going through newarray lost itは、この修正の意図を明確に示しています。つまり、コンパイラの内部的な配列生成ルーチン(newarrayなど)が、名前付き型を基底型に「格下げ」してしまっていたため、その情報を回復させるための措置であるということです。

この修正により、new(T)の戻り値は、その基底型だけでなく、元の名前付き型情報も正しく保持するようになり、Go言語の型システムの厳密なセマンティクスが維持されます。

関連リンク

参考にした情報源リンク

  • Go言語のnew組み込み関数に関する一般的なドキュメント
  • Goコンパイラの内部構造に関する一般的な情報(src/cmd/gc、AST、型チェックフェーズなど)
  • Go言語の型システムにおける名前付き型と基底型の概念
  • (この解説の生成にあたり、特定の外部Webページを直接参照したわけではありませんが、上記の概念はGo言語の公式ドキュメントや関連する技術記事から得られる一般的な知識に基づいています。)