[インデックス 175] ファイルの概要
このコミットは、Go言語のコンパイラとランタイムに複数の重要な変更を導入しています。主な変更点として、式のlvalue/rvalueコンテキストのチェックの導入、組み込み関数呼び出しのためのジェネリクスの初期段階、map
型の導入、そしてany
が予約語から型へと降格されたことが挙げられます。これらの変更は、Go言語の型システム、コンパイラのセマンティック解析、およびランタイムの機能において、言語の進化の初期段階における重要なステップを示しています。
コミット
このコミットは、Go言語のコンパイラ(gc
および6g
)とランタイム(runtime
)にわたる広範な変更を含んでいます。具体的には、式の評価コンテキスト(lvalue/rvalue)の厳密化、map
データ構造の基本的なサポートの追加、そしてany
キーワードの役割の再定義が行われました。これらの変更は、Go言語がより堅牢な型システムと表現力豊かなデータ構造を持つ方向へと進むための基盤を築いています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e1a06ccc80159cf2b6a3cd86493c6a53b5a6f9e8
元コミット内容
commit e1a06ccc80159cf2b6a3cd86493c6a53b5a6f9e8
Author: Ken Thompson <ken@golang.org>
Date: Sun Jun 15 20:24:30 2008 -0700
now checks for lvalue/rvalue context of
expressions.
start of generics for calling builtin
functions
start of map type
'any' demoted from reserved word to type
SVN=122808
変更の背景
このコミットが行われた2008年6月は、Go言語がまだ初期開発段階にあった時期です。言語の設計者たちは、型安全性、効率性、そしてプログラマの生産性を高めるための様々な実験と改善を行っていました。
-
lvalue/rvalueコンテキストの導入: プログラミング言語において、式の評価コンテキストを明確にすることは、コンパイラがコードの意図を正確に解釈し、適切なコードを生成するために不可欠です。特に、代入操作や関数呼び出しにおいて、式が値として評価されるべきか(rvalue)、それともメモリ上の場所を指すべきか(lvalue)を区別することは、バグの防止とコードの予測可能性を高めます。この変更は、Go言語のセマンティック解析の厳密性を向上させることを目的としています。
-
組み込み関数呼び出しのためのジェネリクスの開始: Go言語は、その設計哲学として「シンプルさ」を重視しており、C++のようなテンプレートやJavaのようなジェネリクスを当初は採用していませんでした。しかし、組み込み関数(例:
len
,cap
,new
,make
など)は、異なる型に対して汎用的に動作する必要があります。このコミットにおける「ジェネリクスの開始」は、これらの組み込み関数が型に依存しない形で動作するための内部的なメカニズムの初期実装を指していると考えられます。これは、後のGo言語におけるインターフェースや、より現代的なジェネリクス機能の萌芽とも言えるでしょう。 -
map
型の導入:map
(ハッシュマップまたは連想配列)は、キーと値のペアを効率的に格納・検索するための基本的なデータ構造であり、現代の多くのプログラミング言語において不可欠な要素です。Go言語の初期段階でmap
型が導入されたことは、言語が実用的なアプリケーション開発をサポートするための重要な一歩でした。これにより、開発者は柔軟なデータ構造をGoで表現できるようになりました。 -
any
の予約語から型への降格:any
は、Go言語の初期の実験的な段階で、任意の型を表すための予約語として存在していました。しかし、言語の設計が進むにつれて、より明確で型安全なメカニズム(例えば、空インターフェースinterface{}
、後のany
型)が求められるようになりました。この変更は、any
を予約語から特定の型(TANY
)として扱うことで、言語のキーワードセットを整理し、将来的な型システムの拡張に備えるためのものです。
これらの変更は、Go言語がその後の安定版リリースに向けて、より堅牢で表現力豊かな言語へと成長していく過程における、重要なマイルストーンと言えます。
前提知識の解説
このコミットを理解するためには、以下のプログラミング言語とコンパイラの基本的な概念を理解しておく必要があります。
-
lvalueとrvalue:
- lvalue (locator value): メモリ上の場所を指す式です。代入の左辺に来ることができる式(例: 変数名、配列の要素、ポインタの参照外し)。lvalueはアドレスを持ちます。
- rvalue (read value): 値として評価される式です。代入の右辺に来ることができる式(例: リテラル、計算結果)。rvalueは一時的な値であり、通常はアドレスを持ちません。
- Go言語のコンパイラは、式の使用コンテキストに応じて、それがlvalueとして扱われるべきか、rvalueとして扱われるべきかを判断し、適切なコードを生成します。
-
コンパイラのフェーズ:
- 字句解析 (Lexical Analysis): ソースコードをトークン(予約語、識別子、リテラルなど)のストリームに変換します。
- 構文解析 (Syntax Analysis): トークンのストリームを解析し、言語の文法規則に従って抽象構文木 (AST) を構築します。
- セマンティック解析 (Semantic Analysis): ASTを走査し、型チェック、名前解決、式の評価コンテキストの決定など、意味的な正当性を検証します。このコミットのlvalue/rvalueチェックは、このフェーズで行われます。
- コード生成 (Code Generation): セマンティック解析されたASTから、ターゲットマシンコードを生成します。
-
Go言語の型システム:
- Go言語は静的型付け言語であり、変数の型はコンパイル時に決定されます。
- 基本型:
int
,float64
,bool
,string
など。 - 複合型:
array
,slice
,struct
,pointer
,function
,interface
,map
,channel
など。 any
型 (旧interface{}
): Go言語における「任意の型」を表すための特別な型です。これは、型が不明な場合や、異なる型の値を扱う必要がある場合に使用されます。このコミットでは、any
が予約語からこの特別な型へと降格されました。
-
組み込み関数 (Built-in Functions):
- Go言語には、コンパイラによって特別に扱われる一連の組み込み関数があります(例:
len
,cap
,new
,make
,append
,panic
,print
など)。これらは、通常の関数とは異なり、言語のコア機能の一部として直接サポートされます。
- Go言語には、コンパイラによって特別に扱われる一連の組み込み関数があります(例:
-
ハッシュマップ (Hash Map / Map):
- キーと値のペアを格納するデータ構造です。キーは一意であり、値に高速にアクセスするために使用されます。Go言語の
map
は、このハッシュマップの実装です。
- キーと値のペアを格納するデータ構造です。キーは一意であり、値に高速にアクセスするために使用されます。Go言語の
-
抽象構文木 (AST):
- ソースコードの構文構造を木構造で表現したものです。コンパイラのセマンティック解析やコード生成の段階で利用されます。
技術的詳細
このコミットは、Goコンパイラの内部構造、特にセマンティック解析とコード生成に深く関わる部分に影響を与えています。
lvalue/rvalueコンテキストの導入
src/cmd/gc/walk.c
のwalktype
関数は、ASTを走査し、各ノードの型チェックとセマンティック解析を行うGoコンパイラの中心的な部分です。このコミットでは、walktype
関数にtop
という新しい引数が追加されました。このtop
引数は、現在のノードが評価されるべきコンテキスト(lvalue、rvalue、またはステートメントレベル)を示します。
Etop
: ステートメントレベルでの評価(例:for
,if
,switch
, 代入文全体)。Elv
: lvalueコンテキストでの評価(例: 代入の左辺)。Erv
: rvalueコンテキストでの評価(例: 代入の右辺、関数引数)。
これにより、コンパイラはより正確に式の意味を解釈し、例えば代入の左辺にrvalueが来るような不正なコードを検出できるようになります。
map
型の導入
map
型は、Go言語の複合型として導入されました。
src/cmd/gc/go.h
:TMAP
という新しい型定数が追加され、Type
構造体内でマップのキーと値の型を指すためのフィールド(down
とtype
)が定義されました。src/cmd/gc/export.c
: コンパイラが型情報をエクスポートする際に、TMAP
型を正しく処理するためのロジックが追加されました。これにより、異なるパッケージ間でマップ型が正しく共有できるようになります。src/cmd/gc/sys.go
:newmap
,mapaccess1
,mapaccess2
,mapdelete
,mapassign
といった、マップ操作のための組み込み関数が宣言されました。これらはGoランタイムによって提供される低レベルの関数です。src/cmd/gc/sysimport.c
: 上記のマップ操作関数の型情報が、コンパイラが認識できるようにインポート文字列として追加されました。src/cmd/gc/walk.c
:mapop
という新しい関数が導入され、NEW
演算子(make
の初期段階)やINDEX
演算子(マップの要素アクセス)がマップ型に対してどのように動作するかを定義しています。mapop
は、マップのキーと値のサイズ、アルゴリズムタイプ(algtype
関数で決定される)などの情報を使って、ランタイムのnewmap
関数を呼び出します。
any
の予約語から型への降格
src/cmd/gc/go.y
: 構文解析器の定義ファイルで、LANY
トークンが削除されました。これは、any
がもはや特別な予約語として扱われないことを意味します。src/cmd/gc/lex.c
: 字句解析器の定義ファイルで、any
がLANY
(予約語)ではなく、LBASETYPE
(基本型)としてTANY
にマッピングされるように変更されました。これにより、any
は他の組み込み型と同様に扱われるようになります。src/cmd/gc/subr.c
:etnames
配列にTANY
が追加され、デバッグ出力などでany
型が正しく表示されるようになりました。
その他の変更
src/runtime/runtime.c
/src/runtime/runtime.h
:mcpy
(メモリコピー)とmal
(メモリ割り当て)関数がstatic
から非static
に変更され、外部から呼び出し可能になりました。これは、コンパイラがこれらのランタイム関数を直接利用できるようにするためです。また、sys_printpointer
などの新しいシステムコールが追加されました。test/switch.go
:switch
文が文字列型をサポートするようになったことを示すテストケースが追加されました。これは、switch
文のセマンティック解析が拡張されたことを示唆しています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は多岐にわたりますが、特に以下のファイルと関数が重要です。
-
src/cmd/gc/walk.c
:walktype
関数のシグネチャ変更と、top
引数(Etop
,Elv
,Erv
)の導入。mapop
関数の追加と、NEW
およびINDEX
演算子におけるマップ処理のロジック。stringop
関数の変更(top
引数の追加)。
-
src/cmd/gc/go.h
:Etop
,Elv
,Erv
といった評価コンテキストを示す列挙型の追加。TMAP
型定数の追加。argtype
,syslook
,mapop
などの新しい関数のプロトタイプ宣言。
-
src/cmd/gc/sys.go
:newmap
,mapaccess1
,mapaccess2
,mapdelete
,mapassign
といったマップ操作のための組み込み関数の宣言。
-
src/cmd/gc/sysimport.c
:- マップ操作関数の型定義がインポート文字列に追加。
any
型がsys.any
として定義されるように変更。
-
src/cmd/gc/go.y
:LANY
トークンの削除。
-
src/cmd/gc/lex.c
:any
がLBASETYPE
としてTANY
にマッピングされるように変更。
コアとなるコードの解説
walktype
関数 (src/cmd/gc/walk.c
)
void
walktype(Node *n, int top)
{
// ...
if(top == Exxx || top == Eyyy) {
dump("", n);
fatal("walktype: top=%d", top);
}
// ...
switch(n->op) {
case OPRINT:
if(top != Etop)
goto nottop;
walktype(n->left, Erv); // OPRINTの引数はrvalue
// ...
case OAS: // 代入
if(top != Etop)
goto nottop;
walktype(l, Elv); // 左辺はlvalue
walktype(r, Erv); // 右辺はrvalue
// ...
// ...
nottop:
dump("bad top", n);
fatal("walktype: top=%d %O", top, n->op);
}
// ...
}
walktype
関数は、ASTノードn
と評価コンテキストtop
を受け取ります。この変更により、各演算子(OPRINT
, OAS
など)の処理において、そのオペランドがどのコンテキストで評価されるべきかを明示的に指定できるようになりました。例えば、代入演算子OAS
では、左辺はElv
(lvalue)、右辺はErv
(rvalue)としてwalktype
が再帰的に呼び出されます。もし、期待されるコンテキストと異なる場合(例: OPRINT
がEtop
以外で呼び出された場合)、nottop
ラベルにジャンプし、致命的なエラーを発生させます。これは、コンパイラがコードのセマンティックな正当性をより厳密にチェックするための重要なメカニズムです。
mapop
関数 (src/cmd/gc/walk.c
)
Node*
mapop(Node *n, int top)
{
// ...
switch(n->op) {
case ONEW: // make(map[K]V) のような操作
// ...
on = syslook("newmap", 1); // ランタイムのnewmap関数を検索
// ...
argtype(on, t->down); // キーの型をany-1として引数型チェック
argtype(on, t->type); // 値の型をany-2として引数型チェック
// ...
r = nod(OCALL, on, r); // newmapを呼び出すASTノードを生成
walktype(r, top);
r->type = n->type;
break;
case OINDEX: // map[key] のような要素アクセス
case OINDEXPTR:
// ...
on = syslook("mapaccess1", 1); // ランタイムのmapaccess1関数を検索
// ...
argtype(on, t->down); // キーの型をany-1として引数型チェック
argtype(on, t->type); // 値の型をany-2として引数型チェック
argtype(on, t->down); // キーの型をany-3として引数型チェック
argtype(on, t->type); // 値の型をany-4として引数型チェック
// ...
r = nod(OCALL, on, r); // mapaccess1を呼び出すASTノードを生成
walktype(r, Erv); // rvalueコンテキストで評価
r->type = ptrto(t->type);
r = nod(OIND, r, N); // ポインタの参照外し
r->type = t->type;
break;
}
// ...
return r;
}
mapop
関数は、map
型に関連する操作(NEW
によるマップの作成、INDEX
による要素アクセスなど)を処理します。この関数は、ランタイムに実装されている低レベルのマップ操作関数(newmap
, mapaccess1
など)を呼び出すためのASTノードを生成します。argtype
関数は、呼び出されるランタイム関数の引数型と、実際に渡される引数の型が一致するかどうかをチェックするために使用されます。これは、Go言語の初期の「ジェネリクス」の概念が、組み込み関数やランタイム関数を通じてどのように実現されていたかを示す良い例です。any
型が引数として使われているのは、マップのキーと値が任意の型を取りうるため、汎用的なインターフェースを提供するためです。
syslook
関数 (src/cmd/gc/subr.c
)
Node*
syslook(char *name, int copy)
{
Sym *s;
Node *n;
s = pkglookup(name, "sys"); // "sys"パッケージからシンボルを検索
if(s == S || s->oname == N)
fatal("looksys: cant find sys.%s", name);
if(!copy)
return s->oname; // コピー不要なら既存のノードを返す
n = nod(0, N, N);
*n = *s->oname; // シンボルに対応するノードをコピー
n->type = deep(s->oname->type); // 型も深くコピー
return n;
}
syslook
関数は、Goランタイムが提供する組み込み関数(sys
パッケージ内の関数)のシンボルを検索し、それに対応するASTノードを返します。copy
引数がtrue
の場合、ノードとその型情報を深くコピーして返します。これは、ASTの変換中に元のノードが変更されることを防ぐため、または新しいコンテキストでノードを使用するために重要です。mapop
関数などでsyslook
が使われているのは、マップ操作がランタイムの低レベル関数に依存しているためです。
関連リンク
- Go言語の初期のコミット履歴: https://github.com/golang/go/commits?author=ken%40golang.org
- Go言語の
map
型に関する公式ドキュメント: https://go.dev/blog/maps (これは現代のGoに関するものですが、概念は共通です) - Go言語の
any
型に関する公式ドキュメント: https://go.dev/doc/go1.18#any (Go 1.18で導入されたany
キーワードに関するものですが、interface{}
のエイリアスとしての歴史的背景を理解するのに役立ちます)
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/gc/
ディレクトリ): https://github.com/golang/go - コンパイラの設計に関する一般的な情報源 (lvalue/rvalue、ASTなど):
- "Compilers: Principles, Techniques, and Tools" (通称 "Dragon Book")
- オンラインのコンピュータサイエンスの教科書や講義資料
これらの情報源は、Go言語のコンパイラの内部動作、特に型システム、セマンティック解析、およびランタイムとの連携について深く理解するために参照されました。