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

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

このコミットは、Go言語のコンパイラにおけるスライス(動的配列)とnilの比較に関する挙動を修正・改善するものです。具体的には、[](空のスライスまたはスライス型)とnilの比較が正しく行われるように、コンパイラのコード生成、定数処理、および型チェックの各フェーズにわたる変更が加えられています。

コミット

commit dcc064fe941b17dad67a13cd635c76a04cde7a69
Author: Ken Thompson <ken@golang.org>
Date:   Thu Dec 18 21:33:45 2008 -0800

    cmp [] to nil
    
    R=r
    OCL=21567
    CL=21567

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

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

元コミット内容

このコミットの元のメッセージは「cmp [] to nil」です。これは「スライスとnilの比較」を意味し、Go言語においてスライスがnilと比較された際のコンパイラの挙動を修正・実装する意図が示されています。

変更の背景

Go言語の初期開発段階において、スライスは言語の重要な要素として導入されました。スライスは、配列の一部を参照する軽量なデータ構造であり、その柔軟性から頻繁に利用されます。他の多くのデータ型と同様に、スライスもnilというゼロ値を持つことができます。nilスライスは、基盤となる配列へのポインタがnilであり、長さと容量が0である状態を指します。

このコミットが作成された2008年12月は、Go言語がまだ一般公開される前の初期段階でした。当時のコンパイラ(6gなど)は、スライスとnilの比較(例: mySlice == nil)を適切に処理するためのロジックが完全に実装されていなかった可能性があります。このような比較は、スライスが初期化されていないか、意図的にnilに設定されているかをチェックする上で非常に一般的かつ重要な操作です。

このコミットの背景には、以下の問題意識があったと考えられます。

  1. 比較の欠如または不正確な挙動: スライスとnilの比較がコンパイラによってサポートされていないか、あるいは誤ったコードが生成される可能性があった。
  2. 言語仕様の明確化: スライスとnilの比較がGo言語のセマンティクスとして明確に定義され、コンパイラがそれに従う必要があった。
  3. 開発者の利便性: 開発者がスライスの状態を簡単にチェックできるように、slice == nilという自然な構文をサポートする必要があった。

このコミットは、これらの課題に対処し、スライスとnilの比較がGo言語の期待されるセマンティクスに沿って正しく機能するようにするための基盤を構築したものです。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語およびコンパイラに関する前提知識が必要です。

  1. Go言語のスライス (Slice):

    • スライスは、Go言語における可変長シーケンス型です。内部的には、基盤となる配列へのポインタ、長さ(len)、容量(cap)の3つの要素から構成されます。
    • nilスライス: スライスのゼロ値はnilです。nilスライスは、基盤となる配列へのポインタがnilであり、長さと容量が0です。var s []intのように宣言されたスライスは初期値としてnilになります。
    • スライスの比較: Go言語では、スライス同士を直接比較することはできません(要素の比較は可能)。しかし、スライスがnilであるかどうかをチェックするために、slice == nilまたはslice != nilという比較は可能です。この比較は、スライスの内部ポインタがnilであるかどうかをチェックします。
  2. Goコンパイラの構造: Goコンパイラ(当時の6gなど)は、大きく分けて以下のフェーズで動作します。

    • フロントエンド (Frontend): ソースコードの字句解析、構文解析、抽象構文木 (AST) の構築、型チェック、定数畳み込み (constant folding) などを行います。src/cmd/gc/const.csrc/cmd/gc/walk.cはこのフェーズに関連します。
    • バックエンド (Backend): ASTを中間表現に変換し、ターゲットアーキテクチャ(この場合はamd64、当時の6g)の機械語コードを生成します。src/cmd/6g/cgen.cはこのフェーズに関連します。
  3. 抽象構文木 (AST):

    • ソースコードはコンパイラによってASTに変換されます。ASTはプログラムの構造を木構造で表現したものです。コンパイラはASTを走査(walk)しながら、型チェックやコード生成を行います。
  4. nilリテラル (Wlitnil):

    • コンパイラ内部でnilというキーワードがリテラルとして扱われる際の表現です。
  5. Array構造体 (内部表現):

    • Go言語のスライスは、コンパイラ内部では通常、基盤となる配列へのポインタ、長さ、容量を保持する構造体として表現されます。このコミットのコードでは、offsetof(Array, array)という記述があり、これはスライスの内部表現であるArray構造体の中に、実際のデータが格納されている配列へのポインタを指すarrayというフィールドが存在することを示唆しています。offsetofはC言語のマクロで、構造体のメンバのオフセット(先頭からのバイト数)を計算します。

技術的詳細

このコミットは、Goコンパイラの複数のコンポーネントにわたる変更を通じて、スライスとnilの比較を正しく処理できるようにしています。

1. src/cmd/6g/cgen.c の変更 (コード生成)

このファイルは、amd64アーキテクチャ(当時の6gコンパイラ)向けのコード生成を担当しています。変更の核心は、スライスとnilの比較が行われた際に、スライスの基盤となる配列へのポインタnil(アドレス0)であるかどうかをチェックする機械語コードを生成する点にあります。

追加されたコードブロックは以下の処理を行います。

  • if(isdarray(nl->type)): 比較の左辺(nl)の型が動的配列(スライス)であるかをチェックします。
  • a = optoas(a, types[tptr]): 比較演算(例: ==)をポインタ型に対するアセンブリ命令に変換します。
  • regalloc(&n1, types[tptr], N); agen(nl, &n1);: スライスのアドレスを保持するためのレジスタn1を割り当て、スライスの値(スライスヘッダ)をn1にロードするコードを生成します。
  • n2 = n1; n2.op = OINDREG; n2.xoffset = offsetof(Array,array);: ここが重要です。n1にはスライスヘッダのアドレスが格納されています。OINDREGは間接参照(ポインタのデリファレンス)を意味し、offsetof(Array,array)はスライスヘッダ構造体内のarrayフィールド(基盤となる配列へのポインタ)のオフセットを示します。これにより、n2はスライスの基盤となる配列へのポインタを指すようになります。
  • nodconst(&tmp, types[tptr], 0);: ポインタ型のnilを表す定数0を作成します。
  • gins(optoas(OCMP, types[tptr]), &n2, &tmp);: n2(スライスの基盤配列ポインタ)とtmpnilを表す0)を比較するアセンブリ命令(CMP)を生成します。
  • patch(gbranch(a, types[tptr]), to);: 比較結果に基づいて条件分岐命令を生成し、適切なターゲット(to)へジャンプするようにパッチを適用します。
  • regfree(&n1);: 使用したレジスタを解放します。

この変更により、mySlice == nilというGoのコードは、内部的にmySliceの基盤配列ポインタが0であるかどうかの比較に変換され、正しい機械語コードが生成されるようになります。

2. src/cmd/gc/const.c の変更 (定数処理)

このファイルは、コンパイラの定数畳み込み(constant folding)と型変換を担当しています。convlit1関数は、リテラルを特定の型に変換する際のルールを定義しています。

変更点:

  • case Wlitnil:: nilリテラルが処理される部分です。
  • 以前は、nilリテラルはポインタ型 (isptr[et]) またはインターフェース型 (et == TINTER) にのみ変換可能でした。
  • 新しいコードでは、switch(et)文が導入され、TPTR32 (32ビットポインタ), TPTR64 (64ビットポインタ), TINTER (インターフェース), そして**TARRAY (配列型)** がnilリテラルの有効な変換先として明示的に追加されました。

Go言語において、スライスは配列の上に構築されており、nilスライスは基盤となる配列へのポインタがnilである状態です。この変更は、コンパイラがnilを配列型(ひいてはスライス型)の文脈で正しく解釈し、比較などの操作においてnilが有効な値として扱われるようにするために不可欠です。これにより、nilがスライスと比較される際に、型システムがその操作を許可するようになります。

3. src/cmd/gc/walk.c の変更 (ASTウォーカー)

このファイルは、コンパイラのASTウォーカーの一部であり、ASTを走査しながら型チェックやその他のセマンティック分析を行います。

変更点:

  • case ONE:: このケースは、単項演算子や比較演算子などの特定のノードタイプを処理する際に使用されます。
  • if(isdarray(n->left->type)) { t = types[TBOOL]; break; }: 比較の左辺(n->left)の型が動的配列(スライス)である場合、その比較式全体の型をTBOOL(ブーリアン型)に設定します。

この変更は、mySlice == nilのような比較式の結果が常にブーリアン値(trueまたはfalse)になることを保証します。コンパイラがこの比較を正しく型付けすることで、後続のコード生成フェーズで適切な命令が生成されるようになります。

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

src/cmd/6g/cgen.c (L750付近)

 	if(isdarray(nl->type)) {
 		a = optoas(a, types[tptr]);
 		regalloc(&n1, types[tptr], N);
 		agen(nl, &n1);
 		n2 = n1;
 		n2.op = OINDREG;
 		n2.xoffset = offsetof(Array,array);
 		nodconst(&tmp, types[tptr], 0);
 		gins(optoas(OCMP, types[tptr]), &n2, &tmp);
 		patch(gbranch(a, types[tptr]), to);
 		regfree(&n1);
 		break;
 	}

src/cmd/gc/const.c (L63付近)

 	case Wlitnil:
 		if(isptrto(t, TSTRING))
 			goto bad1;
 		switch(et) {
 		default:
 			goto bad1;

 		case TPTR32:
 		case TPTR64:
 		case TINTER:
 		case TARRAY: // <-- 追加
 			break;
 		}
 		break;

src/cmd/gc/walk.c (L1033付近)

 	case ONE:
 		if(n->left->type == T)
 			goto ret;
 		if(isdarray(n->left->type)) { // <-- 追加
 			t = types[TBOOL]; // <-- 追加
 			break; // <-- 追加
 		}
 		et = n->left->type->etype;
 		if(!okforeq[et])
 			goto badt;

コアとなるコードの解説

これらの変更は、Goコンパイラがスライスとnilの比較をエンドツーエンドで処理できるようにするためのものです。

  1. src/cmd/6g/cgen.c: このコードは、スライスとnilの比較が行われた際に、実際に実行される機械語命令を生成する部分です。スライスの内部構造(Array構造体)から基盤となる配列へのポインタを取り出し、そのポインタがnil(アドレス0)であるかどうかを比較する命令を生成します。これにより、mySlice == nilというGoのコードが、効率的かつ正確な低レベルの比較に変換されます。

  2. src/cmd/gc/const.c: この変更は、コンパイラの型システムがnilリテラルを配列型(スライス型)の文脈で正しく認識できるようにします。以前は、nilはポインタやインターフェースにしか関連付けられませんでしたが、この変更により、スライスがnilと比較される際に型エラーが発生しなくなります。これは、コンパイラがnilスライスの概念をより深く理解したことを示しています。

  3. src/cmd/gc/walk.c: このコードは、ASTを走査する際に、スライスとnilの比較式の型をboolに設定します。これにより、コンパイラはこのような比較がブーリアン結果を生成することを認識し、後続のコンパイルフェーズで適切な処理が行われることを保証します。これは、Go言語の型システムがスライスとnilの比較を有効な操作として受け入れるための重要なステップです。

これらの変更が連携することで、Go言語においてmySlice == nilという比較が、言語仕様に準拠し、かつ効率的にコンパイル・実行されるようになります。

関連リンク

  • Go言語の公式ドキュメント - Slices: https://go.dev/blog/slices-intro (Go言語のスライスに関する基本的な情報)
  • Go言語の公式ドキュメント - The Go Programming Language Specification (Slices): https://go.dev/ref/spec#Slice_types (Go言語のスライスに関する公式仕様)
  • Go言語の公式ドキュメント - The Go Programming Language Specification (Comparison operators): https://go.dev/ref/spec#Comparison_operators (Go言語の比較演算子に関する公式仕様、特にスライスとnilの比較について言及されているはずです)

参考にした情報源リンク