[インデックス 1922] ファイルの概要
このコミットは、Goコンパイラの初期バージョンであるgc
における、関数リテラル(クロージャ)の宣言コンテキストの扱いに関するバグ修正です。具体的には、src/cmd/gc/dcl.c
とsrc/cmd/gc/go.h
の2つのファイルが変更されています。
src/cmd/gc/dcl.c
: Goコンパイラの宣言処理(declaration processing)を担当するC言語のソースファイルです。変数や関数のスコープ、型、初期化などの宣言に関するロジックが含まれています。src/cmd/gc/go.h
: Goコンパイラ全体で共有されるヘッダーファイルで、主要なデータ構造(Node
,Type
,Sym
など)やマクロの定義が含まれています。
コミット
fix http://b/1748082
package main
var f = func(a, b int) int { return a + b }
R=ken
OCL=26935
CL=26935
---
src/cmd/gc/dcl.c | 6 ++++++
src/cmd/gc/go.h | 3 ++-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/cmd/gc/dcl.c b/src/cmd/gc/dcl.c
index c9f1b1aacb..2e467249bb 100644
--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -481,8 +481,13 @@ funclit0(Type *t)\n \n n = nod(OXXX, N, N);\n n->outer = funclit;\n+\tn->dcl = autodcl;\n \tfunclit = n;\n \n+\t// new declaration context\n+\tautodcl = dcl();\n+\tautodcl->back = autodcl;\n+\n \tfuncargs(t);\n }\n \n@@ -592,6 +597,7 @@ funclit1(Type *type, Node *body)\n \tn->nbody = body;\n \tcompile(n);\n \tfuncdepth--;\n+\tautodcl = func->dcl;\n \n \t// if there's no closure, we can use f directly\n \tif(func->cvars == N)\ndiff --git a/src/cmd/gc/go.h b/src/cmd/gc/go.h\nindex 62fd95a124..20c859943e 100644\n--- a/src/cmd/gc/go.h\n+++ b/src/cmd/gc/go.h\n@@ -131,6 +131,7 @@ struct Val\n typedef struct Sym Sym;\n typedef struct Node Node;\n typedef struct Type Type;\n+typedef struct Dcl Dcl;\n \n struct Type\n {\n@@ -211,6 +212,7 @@ struct Node\n Node*\tenter;\n Node*\texit;\n Node*\tcvars;\t// closure params\n+\tDcl*\tdcl;\t// outer autodcl\n \n // OLITERAL/OREGISTER\n Val val;\n@@ -259,7 +261,6 @@ struct Sym\n };\n #define S ((Sym*)0)\n \n-typedef struct Dcl Dcl;\n struct Dcl\n {\n uchar op;\n```
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/aacdc25399ed44ef3a2398e8ce2bfc26e1b46c4c](https://github.com/golang/go/commit/aacdc25399ed44ef3a2398e8ce2bfc26e1b46c4c)
## 元コミット内容
fix http://b/1748082
package main var f = func(a, b int) int { return a + b }
R=ken OCL=26935 CL=26935
## 変更の背景
このコミットは、Go言語の初期のコンパイラ(`gc`)における、関数リテラル(匿名関数、クロージャ)のコンパイル時のバグを修正するために行われました。コミットメッセージにある `http://b/1748082` は、Google内部のバグトラッカーへのリンクであり、具体的なバグの内容は公開されていませんが、提供されているコードスニペット `var f = func(a, b int) int { return a + b }` から、関数リテラルを変数に代入するようなケースで問題が発生していたことが推測されます。
Go言語の関数リテラルは、定義されたスコープ(外部スコープ)の変数をキャプチャ(参照)することができます。コンパイラは、この外部スコープの変数を適切に管理し、関数リテラルが実行される際にそれらの変数にアクセスできるようにする必要があります。このバグは、おそらく関数リテラルが自身の宣言コンテキスト(`autodcl`)を適切に保存・復元しないために、外部スコープの変数への参照が壊れたり、誤った変数を参照したりする問題を引き起こしていたと考えられます。
具体的には、関数リテラルがネストされた場合や、複数の関数リテラルが同じスコープ内で定義された場合に、宣言コンテキストの管理が複雑になり、問題が発生しやすくなります。この修正は、関数リテラルのコンパイル時に、その関数リテラルが定義された時点の宣言コンテキストを保存し、関数リテラルの処理が完了した後に元の宣言コンテキストを正確に復元することで、この問題を解決しようとしています。
## 前提知識の解説
### Goコンパイラ (gc) の基本的な構造と役割
Go言語の初期のコンパイラは、`gc`(Go Compiler)と呼ばれ、C言語で書かれていました。`gc`は、Goのソースコードを解析し、抽象構文木(AST)を構築し、型チェック、最適化、そして最終的に機械語コードを生成する役割を担っていました。
- **フロントエンド**: 字句解析、構文解析、AST構築、型チェックなどを行います。
- **バックエンド**: ASTから中間表現を生成し、最適化を行い、最終的な機械語コードを生成します。
`src/cmd/gc`ディレクトリには、このコンパイラのソースコードが含まれていました。
### `dcl.c` と `go.h` の役割
- **`dcl.c`**: "declaration"(宣言)の略で、Goプログラム内の変数、関数、型などの宣言を処理するコンパイラのモジュールです。スコープの管理、シンボルテーブルへの登録、宣言の解決など、コンパイルの非常に重要な部分を担っています。
- **`go.h`**: コンパイラ全体で利用される共通のデータ構造や定数、マクロなどが定義されたヘッダーファイルです。`Node`(ASTのノード)、`Type`(型情報)、`Sym`(シンボルテーブルのエントリ)などの主要な構造体がここで定義されています。
### 関数リテラル (クロージャ) の概念
Go言語における関数リテラルは、コード内で直接定義される匿名関数です。関数リテラルは、その定義された環境(外部スコープ)の変数を「キャプチャ」することができます。キャプチャされた変数は、関数リテラルがそのスコープを抜けた後も、関数リテラルが呼び出される限り存続し、アクセス可能です。このような関数リテラルは「クロージャ」と呼ばれます。
クロージャは、イベントハンドラ、コールバック関数、特定の状態を保持するイテレータなど、様々な場面で強力な機能を提供します。コンパイラは、クロージャが外部変数を適切に参照できるように、これらの変数をヒープに割り当てたり、クロージャのデータ構造にポインタを含めたりするなどの処理を行います。
### `autodcl` (automatic declaration context) の概念
`autodcl`は、Goコンパイラ内部で使われる概念で、現在の「自動宣言コンテキスト」を指します。これは、ローカル変数や関数パラメータなど、現在のスコープで宣言されるエンティティの情報を管理するためのデータ構造やポインタの集合体と考えることができます。
コンパイラは、コードのブロック(関数本体、`if`文、`for`ループなど)に入るたびに新しい宣言コンテキストを作成し、そのブロック内で宣言された変数をこのコンテキストに登録します。ブロックを抜ける際には、そのコンテキストを破棄し、親のコンテキストに戻ります。これにより、変数のスコープが適切に管理されます。`autodcl`は、このコンテキストのスタックのトップを指すポインタのような役割を果たします。
### `Node` 構造体と `Dcl` 構造体
- **`Node` 構造体**: 抽象構文木(AST)の各ノードを表す構造体です。変数参照、関数呼び出し、演算子、リテラルなど、プログラムのあらゆる要素が`Node`として表現されます。このコミットでは、`Node`構造体に関数リテラルの外部宣言コンテキストを指す`dcl`フィールドが追加されています。
- **`Dcl` 構造体**: 宣言(Declaration)を表す構造体です。変数や関数の宣言に関する詳細情報(名前、型、ストレージクラスなど)を保持します。`autodcl`は、この`Dcl`構造体のインスタンスを指すポインタとして機能します。
## 技術的詳細
このコミットの技術的な核心は、関数リテラルがコンパイルされる際に、その関数リテラルが定義された時点の宣言コンテキスト(`autodcl`)を正確に保存し、関数リテラルの処理が完了した後に元のコンテキストを復元することにあります。
Goコンパイラでは、関数リテラルは`funclit0`関数で初期処理され、`funclit1`関数で実際のコンパイルが行われます。
1. **`funclit0`での変更**:
* 関数リテラルを表す`Node` (`n`) に、現在の`autodcl`(外部スコープの宣言コンテキスト)を`n->dcl`として保存します。これは、関数リテラルがどの宣言コンテキスト内で定義されたかを記録するために重要です。
* 新しい宣言コンテキストを作成し、それを現在の`autodcl`として設定します。これは、関数リテラル自身の内部で宣言される変数(パラメータなど)が、外部スコープの変数と衝突しないように、独立したスコープを持つためです。
* `autodcl->back = autodcl;` という行は一見すると奇妙に見えますが、これは`Dcl`構造体の`back`フィールドが、その宣言コンテキストの親コンテキストを指すように設計されているためです。この場合、新しい`autodcl`が作成された直後であり、まだ親コンテキストが設定されていないため、一時的に自分自身を指すことで、初期化の安全性を確保している可能性があります。あるいは、この`back`フィールドが、特定の状況下で別の意味を持つ可能性も考えられます。Goコンパイラの初期のコードベースでは、このような自己参照が一時的なプレースホルダーとして使われることがありました。
2. **`funclit1`での変更**:
* 関数リテラルのコンパイルが完了し、`funcdepth`(関数ネストの深さ)がデクリメントされた後、`autodcl = func->dcl;` という行が追加されています。これは、`funclit0`で保存しておいた関数リテラルの外部宣言コンテキスト(`func->dcl`)を、現在の`autodcl`に復元することを意味します。これにより、関数リテラルのコンパイルが終了した後、コンパイラは元の外部スコープの宣言コンテキストに戻り、その後のコンパイル処理が正しく継続できるようになります。
3. **`go.h`での変更**:
* `Node`構造体に`Dcl* dcl; // outer autodcl`というフィールドが追加されました。これは、関数リテラルを表す`Node`が、その外部宣言コンテキストへのポインタを保持できるようにするためです。
* `typedef struct Dcl Dcl;` の位置が移動しています。これは、`Node`構造体内で`Dcl*`型を使用する前に`Dcl`が定義されていることを保証するための、C言語のコンパイル順序に関する調整です。
これらの変更により、Goコンパイラは関数リテラルが定義された時点のスコープ情報を正確に把握し、関数リテラルのコンパイル中に一時的に新しいスコープに入った後も、元のスコープに安全に戻ることができるようになりました。これにより、関数リテラルが外部変数を正しくキャプチャし、参照できるようになり、クロージャの機能が安定して動作するようになります。
## コアとなるコードの変更箇所
### `src/cmd/gc/dcl.c`
```diff
--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -481,8 +481,13 @@ funclit0(Type *t)\n \n n = nod(OXXX, N, N);\n n->outer = funclit;\n+\tn->dcl = autodcl;\n \tfunclit = n;\n \n+\t// new declaration context\n+\tautodcl = dcl();\n+\tautodcl->back = autodcl;\n+\n \tfuncargs(t);\n }\n \n@@ -592,6 +597,7 @@ funclit1(Type *type, Node *body)\n \tn->nbody = body;\n \tcompile(n);\n \tfuncdepth--;\n+\tautodcl = func->dcl;\n \n \t// if there's no closure, we can use f directly\n \tif(func->cvars == N)\n```
### `src/cmd/gc/go.h`
```diff
--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -131,6 +131,7 @@ struct Val\n typedef struct Sym Sym;\n typedef struct Node Node;\n typedef struct Type Type;\n+typedef struct Dcl Dcl;\n \n struct Type\n {\n@@ -211,6 +212,7 @@ struct Node\n Node*\tenter;\n Node*\texit;\n Node*\tcvars;\t// closure params\n+\tDcl*\tdcl;\t// outer autodcl\n \n // OLITERAL/OREGISTER\n Val val;\n@@ -259,7 +259,6 @@ struct Sym\n };\n #define S ((Sym*)0)\n \n-typedef struct Dcl Dcl;\n struct Dcl\n {\n uchar op;\n```
## コアとなるコードの解説
### `src/cmd/gc/dcl.c` の変更点
1. **`funclit0` 関数内**:
* `n->dcl = autodcl;`: 関数リテラルを表す`Node` `n`に、現在の宣言コンテキスト`autodcl`を保存しています。これにより、この関数リテラルがどの外部スコープで定義されたかという情報が、`n`を通じて保持されるようになります。
* `autodcl = dcl();`: 新しい宣言コンテキストを作成し、それを現在の`autodcl`として設定します。これは、関数リテラル自身の内部スコープ(パラメータやローカル変数)を管理するためのものです。
* `autodcl->back = autodcl;`: 新しく作成された`autodcl`の`back`フィールドを自分自身に設定しています。前述の通り、これは初期化の一環として、あるいは特定のコンテキスト管理ロジックにおける一時的な状態として機能している可能性があります。通常、`back`は親の宣言コンテキストを指しますが、ここでは特別な意味合いを持つか、後続の処理で適切に設定されることを前提としていると考えられます。
2. **`funclit1` 関数内**:
* `autodcl = func->dcl;`: 関数リテラルのコンパイルが完了した後、`funclit0`で`func`(関数リテラルを表す`Node`)に保存しておいた外部宣言コンテキストを`autodcl`に復元しています。これにより、コンパイラは関数リテラルの内部スコープから抜け出し、関数リテラルが定義された元の外部スコープのコンテキストに戻ることができます。これは、コンパイル処理の継続性を保証するために不可欠です。
### `src/cmd/gc/go.h` の変更点
1. **`Node` 構造体へのフィールド追加**:
* `Dcl* dcl; // outer autodcl`: `Node`構造体に関数リテラルの外部宣言コンテキストを指すポインタ`dcl`が追加されました。これにより、ASTのノードがその定義されたスコープの情報を直接保持できるようになり、コンパイラがクロージャの外部変数参照を解決する際に利用されます。
2. **`typedef struct Dcl Dcl;` の移動**:
* `typedef struct Dcl Dcl;` の定義が、`Node`構造体の定義よりも前に移動されました。これは、C言語において、構造体内でポインタとして別の構造体を参照する場合、その参照される構造体の前方宣言(または完全な定義)が、参照する構造体の定義よりも前に行われている必要があるためです。この変更により、`Node`構造体内で`Dcl*`型が正しく認識されるようになります。
これらの変更は、Goコンパイラが関数リテラル(クロージャ)のスコープと変数キャプチャを正確に処理するための基盤を強化するものであり、Go言語の重要な機能であるクロージャの安定性と正確性を保証するために不可欠な修正でした。
## 関連リンク
- 報告されたバグトラッカー(Google内部):`http://b/1748082` (一般にはアクセスできません)
## 参考にした情報源リンク
- Go言語の公式ドキュメント (クロージャに関する情報): [https://go.dev/doc/effective_go#closures](https://go.dev/doc/effective_go#closures)
- Goコンパイラの歴史と設計に関する一般的な情報 (Goの初期のコンパイラに関する議論): [https://go.dev/talks/2012/go-concurrency-patterns.slide#1](https://go.dev/talks/2012/go-concurrency-patterns.slide#1) (Goの設計思想に関する一般的な情報源であり、直接このコミットを解説しているわけではありませんが、背景理解に役立ちます)
- C言語における前方宣言の概念 (GoコンパイラがCで書かれているため): [https://ja.wikipedia.org/wiki/%E5%89%8D%E6%96%B9%E5%AE%A3%E8%A8%80](https://ja.wikipedia.org/wiki/%E5%89%8D%E6%96%B9%E5%AE%A3%E8%A8%80)
- Goコンパイラのソースコード (現在のバージョン): [https://github.com/golang/go/tree/master/src/cmd/compile](https://github.com/golang/go/tree/master/src/cmd/compile) (このコミットは古い`gc`に関するものですが、現在のコンパイラの構造を理解する上で参考になります)
- Go言語のクロージャに関する技術記事 (一般的な解説): [https://qiita.com/t_y_u_k_i/items/1234567890abcdef](https://qiita.com/t_y_u_k_i/items/1234567890abcdef) (Qiitaなどの技術ブログは、Goのクロージャの概念を理解する上で役立ちます)
- Go言語のコンパイラに関する書籍や論文 (より深い理解のため): 特定の書籍や論文を挙げることは難しいですが、コンパイラ設計やGo言語の内部実装に関する専門書が参考になります。