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

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

このコミットは、Goコンパイラ(cmd/gc)におけるクロージャのコンパイルプロセスを改善するものです。具体的には、クロージャの実体となる「隠されたトップレベル関数」の生成タイミングを、コンパイルの後期フェーズであるwalkから、より早期のtypecheckフェーズへと前倒ししています。これにより、コンパイルシーケンスが簡素化され、全体的な効率と堅牢性が向上しています。

変更された主なファイルは以下の通りです。

  • src/cmd/gc/closure.c: クロージャの生成と型チェックに関する核心的なロジックが含まれています。
  • src/cmd/gc/esc.c: エスケープ解析におけるクロージャの扱いが変更されています。
  • src/cmd/gc/fmt.c: クロージャノードのフォーマットに関する軽微な変更です。
  • src/cmd/gc/inl.c: インライン化とエスケープ解析に関するコメントが更新されています。
  • src/cmd/gc/lex.c: コンパイラのメインループにおけるコンパイルフェーズの順序が調整され、クロージャのコンパイルに関する特定のフェーズが削除されています。
  • src/cmd/gc/sinit.c: 静的初期化に関する軽微な変更です。

コミット

commit 25922c06582a91ae02ca8a632b2a75aeb7cdc887
Author: Russ Cox <rsc@golang.org>
Date:   Mon Jun 4 17:07:59 2012 -0400

    cmd/gc: introduce hidden closure functions earlier
    
    The original implementation of closures created the
    underlying top-level function during walk, which is fairly
    late in the compilation process and caused ordering-based
    complications due to earlier stages that had to be repeated
    any number of times.
    
    Create the underlying function during typecheck, much
    earlier, so that later stages can be run just once.
    
    The result is a simpler compilation sequence.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/6279049

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

https://github.com/golang/go/commit/25922c06582a91ae02ca8a632b2a75aeb7cdc887

元コミット内容

cmd/gc: introduce hidden closure functions earlier

The original implementation of closures created the
underlying top-level function during walk, which is fairly
late in the compilation process and caused ordering-based
complications due to earlier stages that had to be repeated
any number of times.

Create the underlying function during typecheck, much
earlier, so that later stages can be run just once.

The result is a simpler compilation sequence.

変更の背景

Go言語のコンパイラ(cmd/gc)において、クロージャ(匿名関数)は、コンパイル時に「隠された(hidden)」トップレベル関数として内部的に表現されます。このコミット以前のGoコンパイラでは、この隠されたトップレベル関数の生成が、コンパイルプロセスの比較的後期のフェーズであるwalk(ASTの走査と変換を行うフェーズ)で行われていました。

この遅延生成にはいくつかの問題がありました。

  1. 順序依存性の複雑化: クロージャの実体が遅れて生成されるため、それ以前のコンパイルフェーズ(型チェックやエスケープ解析など)が、クロージャの存在を完全に考慮できない状態で行われる必要がありました。これにより、コンパイラの内部ロジックが複雑になり、特定の処理が複数回実行される必要が生じることがありました。
  2. 非効率性: クロージャの実体が生成された後で、再度型チェックやエスケープ解析のような処理を適用する必要がある場合、コンパイルパイプライン全体で冗長な処理が発生し、コンパイル時間の増加につながっていました。
  3. コンパイルシーケンスの複雑性: クロージャの特殊な扱いのために、コンパイラのフェーズ管理が複雑になり、コードの保守性や理解度が低下していました。

このコミットは、これらの問題を解決するために、隠されたクロージャ関数の生成タイミングをtypecheckフェーズ(型チェックを行うフェーズ)へと大幅に前倒しすることを目的としています。これにより、クロージャの実体がコンパイルプロセスのより早い段階で利用可能になり、後続のフェーズ(エスケープ解析やコード生成など)が一度だけ、かつより完全な情報に基づいて実行できるようになります。結果として、コンパイルシーケンスが簡素化され、コンパイラの効率と堅牢性が向上します。

前提知識の解説

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

Go言語におけるクロージャ

Go言語におけるクロージャは、関数リテラル(匿名関数)が、その関数が定義されたスコープ(環境)内の非ローカル変数を「キャプチャ」する能力を持つものです。キャプチャされた変数は、クロージャが呼び出される際に、その定義時の値または参照を保持し続けます。

例:

func outer() func() int {
    x := 0
    return func() int { // この匿名関数がクロージャ
        x++ // outer関数のxをキャプチャ
        return x
    }
}

func main() {
    f := outer()
    fmt.Println(f()) // 1
    fmt.Println(f()) // 2
}

コンパイラは、このようなクロージャを処理するために、内部的に特別なメカニズムを使用します。具体的には、クロージャは、キャプチャされた変数を引数として受け取る「隠された(hidden)」トップレベル関数として変換され、元の関数リテラルは、この隠された関数とキャプチャされた変数を組み合わせた「クロージャ値」を生成するコードに変換されます。

Goコンパイラ(cmd/gc)のコンパイルパイプライン

Goコンパイラcmd/gcは、ソースコードを機械語に変換するために複数のフェーズ(段階)を経て処理を行います。主要なフェーズは以下の通りです。

  1. 字句解析 (Lexing): ソースコードをトークン(キーワード、識別子、演算子など)のストリームに変換します。
  2. 構文解析 (Parsing): トークンのストリームを抽象構文木(AST: Abstract Syntax Tree)に変換します。ASTはプログラムの構造を木構造で表現したものです。
  3. 型チェック (Typecheck): ASTを走査し、各ノードの型を解決し、型の一貫性を検証します。このフェーズで、型推論や型エラーの検出が行われます。
  4. ASTウォーク (Walk): 型チェックが完了したASTを走査し、高レベルな構文を低レベルな中間表現に変換したり、最適化を行ったりします。例えば、forループをgoto文とラベルに変換したり、クロージャの具体的なコードを生成したりすることが含まれます。
  5. エスケープ解析 (Escape Analysis): 変数がスタックに割り当てられるべきか、ヒープに割り当てられるべきかを決定する最適化フェーズです。変数の寿命がその変数を宣言した関数のスコープを超える場合(「エスケープする」と呼ばれる)、その変数はヒープに割り当てられる必要があります。
  6. インライン化 (Inlining): 小さな関数呼び出しを、呼び出し元のコードに直接展開する最適化です。
  7. コード生成 (Code Generation): 中間表現をターゲットアーキテクチャの機械語に変換します。

ASTノードとxtop

Goコンパイラ内部では、プログラムの各要素(変数、関数、式など)がNodeという構造体で表現され、これらが組み合わさってASTを形成します。NodeListNodeのリストです。

xtopは、Goコンパイラが管理するグローバルなNodeListの一つで、トップレベルの宣言(パッケージレベルの変数、関数、型など)を保持します。コンパイルの様々な段階で、新しいトップレベルエンティティ(例えば、クロージャから生成される隠された関数)がこのリストに追加されます。

typecheckwalkフェーズの役割

  • typecheckフェーズ: 主にプログラムの静的な意味論を検証します。変数の型、関数のシグネチャ、式の型の一貫性などを確認し、ASTノードに型情報を付与します。このフェーズは、プログラムがGo言語の型システムに準拠していることを保証します。
  • walkフェーズ: typecheckフェーズの後に実行され、ASTを変換してコード生成の準備をします。このフェーズでは、高レベルなGoの構文要素(例: rangeループ、defer文、クロージャ)が、よりプリミティブな操作のシーケンスに分解されます。以前は、クロージャの隠された関数がこのフェーズで具体的に生成され、xtopに追加されていました。

このコミットの変更は、クロージャの隠された関数をwalkフェーズではなくtypecheckフェーズでxtopに追加することで、コンパイラの内部処理の順序と依存性を根本的に変更しています。

技術的詳細

このコミットの核心は、Goコンパイラがクロージャを内部的にどのように表現し、いつその表現を具体化するかという点にあります。以前は、クロージャの実体である「隠されたトップレベル関数」はwalkフェーズで生成されていましたが、この変更によりtypecheckフェーズで生成されるようになりました。

具体的な変更点は以下の通りです。

  1. closure.cにおけるmakeclosure関数の呼び出しタイミングの変更:

    • typecheckclosure関数(クロージャの型チェックを行う関数)内で、クロージャの型チェックが完了した後、すぐにmakeclosure関数が呼び出されるようになりました。
    • makeclosureは、クロージャに対応する隠されたトップレベル関数(xfunc)を生成し、その関数をxtop(トップレベル宣言のリスト)に追加します。
    • 以前は、このmakeclosureの呼び出しとxtopへの追加はwalkclosure関数(walkフェーズで実行される)内で行われていました。
    • この変更により、クロージャの隠された関数は、その型が確定した直後にコンパイラ全体で認識されるようになります。
  2. closure.cにおけるNode構造体のリンクの追加:

    • makeclosure関数内で、生成された隠された関数ノード(xfunc)と元のクロージャノード(func)の間に双方向のリンクが設定されます。
      • xfunc->closure = func;
      • func->closure = xfunc;
    • これにより、クロージャノードからその実体である隠された関数へ、またその逆への参照が確立され、コンパイラの異なるフェーズ間でクロージャとその実体の関連付けが容易になります。
    • また、元のクロージャノードのボディ(func->nbody)などがnilに設定され、元のクロージャノードが直接コード生成の対象とならないようにします。
  3. closure.cにおけるSymExportedフラグの設定:

    • makeclosureで生成される隠された関数のシンボル(xfunc->nname->sym)にSymExportedフラグが設定されます。これは、この関数が外部にエクスポートされるべきではないことを示し、コンパイラ内部でのみ使用されることを明確にします。
  4. esc.cにおけるエスケープ解析の簡素化:

    • エスケープ解析(escapes関数とescfunc関数)のロジックから、OCLOSUREノード(クロージャを表すASTノード)を特別に処理する部分が削除されました。
    • これは、クロージャがtypecheckフェーズで既に隠されたトップレベル関数としてxtopに追加されているため、エスケープ解析は通常の関数と同様にその隠された関数を処理すればよくなったためです。これにより、エスケープ解析のコードが簡素化されます。
    • 特に、クロージャがキャプチャする変数のアドレスをクロージャにリンクする処理が、OCLOSUREノードのesc関数内のcase OCLOSURE:ブロックに移動され、より一貫した方法で処理されるようになりました。
  5. lex.cにおけるコンパイルフェーズの再編成:

    • main関数内のコンパイルフェーズの定義が変更されました。
    • 以前存在した「Phase 6b: Compile all closures.」という独立したフェーズが削除されました。これは、クロージャがtypecheckフェーズで既にトップレベル関数として扱われるようになったため、特別なバッチ処理が不要になったためです。
    • これにより、コンパイルフェーズの番号付けが調整され、全体的なコンパイルシーケンスがより線形かつシンプルになりました。

これらの変更により、コンパイラはクロージャをより早い段階で「通常の関数」として扱うことができるようになり、後続のフェーズ(エスケープ解析、インライン化、コード生成など)が、クロージャの存在を最初から考慮した上で一度だけ実行できるようになります。これにより、コンパイラの内部ロジックが簡素化され、バグの発生リスクが低減し、コンパイル効率が向上します。

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

src/cmd/gc/closure.c

--- a/src/cmd/gc/closure.c
+++ b/src/cmd/gc/closure.c
@@ -75,6 +75,8 @@ closurebody(NodeList *body)
 	return func;
 }
 
+static Node* makeclosure(Node *func, int nowrap);
+
 void
 typecheckclosure(Node *func, int top)
 {
@@ -85,12 +87,12 @@ typecheckclosure(Node *func, int top)
 	oldfn = curfn;
 	typecheck(&func->ntype, Etype);
 	func->type = func->ntype->type;
-	if(curfn == nil) {
-		xtop = list(xtop, func);
-		return;
-	}
-
-	if(func->type != T) {
+	
+	// Type check the body now, but only if we're inside a function.
+	// At top level (in a variable initialization: curfn==nil) we're not
+	// ready to type check code yet; we'll check it later, because the
+	// underlying closure function we create is added to xtop.
+	if(curfn && func->type != T) {
 		curfn = func;
 		typechecklist(func->nbody, Etop);
 		curfn = oldfn;
@@ -120,18 +122,19 @@ typecheckclosure(Node *func, int top)
 		func->enter = list(func->enter, v->heapaddr);
 		v->heapaddr = N;
 	}
+
+	// Create top-level function 
+	xtop = list(xtop, makeclosure(func, func->cvars==nil || (top&Ecall)));
 }
 
 static Node*
-makeclosure(Node *func, NodeList **init, int nowrap)
+makeclosure(Node *func, int nowrap)
 {
 	Node *xtype, *v, *addr, *xfunc;
 	NodeList *l;
 	static int closgen;
 	char *p;
 
-	USED(init);
-
 	/*
 	 * wrap body in external function
 	 * with extra closure parameters.
@@ -168,8 +171,9 @@ makeclosure(Node *func, NodeList **init, int nowrap)
 
 	// create the function
 	xfunc = nod(ODCLFUNC, N, N);
-	snprint(namebuf, sizeof namebuf, "_func_%.3d", ++closgen);
+	snprint(namebuf, sizeof namebuf, "func·%.3d", ++closgen);
 	xfunc->nname = newname(lookup(namebuf));
+	xfunc->nname->sym->flags |= SymExported; // disable export
 	xfunc->nname->ntype = xtype;
 	xfunc->nname->defn = xfunc;
 	declare(xfunc->nname, PFUNC);
@@ -180,7 +184,13 @@ makeclosure(Node *func, NodeList **init, int nowrap)
 	if(xfunc->nbody == nil)
 		fatal("empty body - won't generate any code");
 	typecheck(&xfunc, Etop);
-	closures = list(closures, xfunc);
+	
+	xfunc->closure = func;
+	func->closure = xfunc;
+	
+	func->nbody = nil;
+	func->list = nil;
+	func->rlist = nil;
 
 	return xfunc;
 }
@@ -194,7 +204,7 @@ walkclosure(Node *func, NodeList **init)
 
 	// no closure vars, don't bother wrapping
 	if(func->cvars == nil)
-		return makeclosure(func, init, 1)->nname;
+		return func->closure->nname;
 
 	/*
 	 * wrap body in external function
@@ -202,7 +212,7 @@ walkclosure(Node *func, NodeList **init)
 	 */
 
 	// create the function
-	xfunc = makeclosure(func, init, 0);
+	xfunc = func->closure;
 	xtype = xfunc->nname->ntype;
 
 	// prepare call of sys.closure that turns external func into func literal value.
@@ -250,7 +260,7 @@ walkcallclosure(Node *n, NodeList **init)
 	// New arg list for n. First the closure-args
 	// and then the original parameter list.\n
 	n->list = concat(n->left->enter, n->list);
-	n->left = makeclosure(n->left, init, 1)->nname;
+	n->left = n->left->closure->nname;
 	dowidth(n->left->type);
 	n->type = getoutargx(n->left->type);
 	// for a single valued function, pull the field type out of the struct

src/cmd/gc/esc.c

--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -74,7 +74,7 @@ escapes(NodeList *all)
 
 	// flow-analyze functions
 	for(l=all; l; l=l->next)
-		if(l->n->op == ODCLFUNC || l->n->op == OCLOSURE)
+		if(l->n->op == ODCLFUNC)
 			escfunc(l->n);
 
 	// print("escapes: %d dsts, %d edges\n", dstcount, edgecount);
@@ -102,23 +102,6 @@ escfunc(Node *func)
 		}
 	}
 
-	// walk will take the address of cvar->closure later and assign it to cvar.
-	// linking a fake oaddr node directly to the closure handles the case
-	// of the closure itself leaking.  Following the flow of the value to th
-	// paramref is done in escflow, because if we did that here, it would look
-	// like the original is assigned out of its loop depth, whereas it's just
-	// assigned to something in an inner function.  A paramref itself is never
-	// moved to the heap, only its original.
-	for(ll=curfn->cvars; ll; ll=ll->next) {
-		if(ll->n->op == OXXX)  // see dcl.c:398
-			continue;
-
-		n = nod(OADDR, ll->n->closure, N);
-		n->lineno = ll->n->lineno;
-		typecheck(&n, Erv);
-		escassign(curfn, n);
-	}
-
 	escloopdepthlist(curfn->nbody);
 	esclist(curfn->nbody);
 	curfn = savefn;
@@ -217,19 +200,16 @@ esc(Node *n)
 {
 	int lno;
 	NodeList *ll, *lr;
+	Node *a;
 
 	if(n == N)
 		return;
 
 	lno = setlineno(n);
 	if(n->op == OFOR || n->op == ORANGE)
 		loopdepth++;
 
-	if(n->op == OCLOSURE) {
-		escfunc(n);
-	} else {
-		esc(n->left);
-		esc(n->right);
-		esc(n->ntest);
-		esc(n->nincr);
-		esclist(n->ninit);
-		esclist(n->nbody);
-		esclist(n->nelse);
-		esclist(n->list);
-		esclist(n->rlist);
-	}
+	esc(n->left);
+	esc(n->right);
+	esc(n->ntest);
+	esc(n->nincr);
+	esclist(n->ninit);
+	esclist(n->nbody);
+	esclist(n->nelse);
+	esclist(n->list);
+	esclist(n->rlist);
+
 	if(n->op == OFOR || n->op == ORANGE)
 		loopdepth--;
 
@@ -388,6 +369,16 @@ esc(Node *n)
 		break;
 	
 	case OCLOSURE:
+		// Link addresses of captured variables to closure.
+		for(ll=n->cvars; ll; ll=ll->next) {
+			if(ll->n->op == OXXX)  // unnamed out argument; see dcl.c:/^funcargs
+				continue;
+			a = nod(OADDR, ll->n->closure, N);
+			a->lineno = ll->n->lineno;
+			typecheck(&a, Erv);
+			escassign(n, a);
+		}
+		// fallthrough
 	case OADDR:
 	case OMAKECHAN:
 	case OMAKEMAP:
@@ -726,11 +717,9 @@ escwalk(int level, Node *dst, Node *src)
 			if(debug['m'])
 				warnl(src->lineno, "leaking param: %hN", src);
 		}
-		// handle the missing flow ref <- orig
-		// a paramref is automagically dereferenced, and taking its
-		// address produces the address of the original, so all we have to do here
-		// is keep track of the value flow, so level is unchanged.\n
-		// alternatively, we could have substituted PPARAMREFs with their ->closure in esc/escassign/flow,\n
+
+		// Treat a PPARAMREF closure variable as equivalent to the
+		// original variable.
 		if(src->class == PPARAMREF) {
 			if(leaks && debug['m'])
 				warnl(src->lineno, "leaking closure reference %hN", src);

src/cmd/gc/lex.c

--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -187,7 +187,7 @@ int
 main(int argc, char *argv[])
 {
 	int i, c;
-	NodeList *l, *batch;
+	NodeList *l;
 	char *p;
 
 #ifdef	SIGBUS	
@@ -335,6 +335,7 @@ main(int argc, char *argv[])
 		frame(1);
 
 	// Process top-level declarations in phases.
+
 	// Phase 1: const, type, and names and types of funcs.
 	//   This will gather all the information about types
 	//   and methods but doesn't depend on any of it.\n
@@ -368,7 +369,7 @@ main(int argc, char *argv[])
 		errorexit();
 
 	// Phase 4: Inlining
-	if (debug['l'] > 1) {
+	if(debug['l'] > 1) {
 		// Typecheck imported function bodies if debug['l'] > 1,
 		// otherwise lazily when used or re-exported.
 		for(l=importlist; l; l=l->next)
@@ -381,7 +382,7 @@ main(int argc, char *argv[])
 		errorexit();
 	}
 
-	if (debug['l']) {
+	if(debug['l']) {
 		// Find functions that can be inlined and clone them before walk expands them.
 		for(l=xtop; l; l=l->next)
 			if(l->n->op == ODCLFUNC)
@@ -393,21 +394,7 @@ main(int argc, char *argv[])
 			inlcalls(l->n);
 	}
 
-	// Phase 5: escape analysis.
+	// Phase 5: Escape analysis.
 	if(!debug['N'])
 		escapes(xtop);
 
@@ -405,21 +392,7 @@ main(int argc, char *argv[])
 	if(nsavederrors+nerrors == 0)
 		fninit(xtop);
 
-	// Phase 6b: Compile all closures.
-	// Can generate more closures, so run in batches.
-	while(closures) {
-		batch = closures;
-		closures = nil;
-		if(debug['l'])
-			for(l=batch; l; l=l->next)
-				inlcalls(l->n);
-		if(!debug['N'])
-			escapes(batch);
-		for(l=batch; l; l=l->next)
-			funccompile(l->n, 1);
-	}
-
-	// Phase 7: check external declarations.
+	// Phase 6: Check external declarations.
 	for(l=externdcl; l; l=l->next)
 		if(l->n->op == ONAME)
 			typecheck(&l->n, Erv);

コアとなるコードの解説

src/cmd/gc/closure.c

  • typecheckclosure関数の変更:

    • 以前は、トップレベルの変数初期化(curfn == nilの場合)でクロージャが使われる際に、xtop = list(xtop, func);としてクロージャノード自体をxtopに追加していました。しかし、これはクロージャの実体ではなく、その宣言ノードでした。
    • 変更後、if(curfn && func->type != T)という条件が追加され、関数内部でクロージャが定義されている場合にのみ、そのボディの型チェックを行うようになりました。トップレベルのクロージャは、後でxtopに追加される隠された関数として処理されるため、ここではボディの型チェックは行いません。
    • 最も重要な変更は、xtop = list(xtop, makeclosure(func, func->cvars==nil || (top&Ecall)));という行です。これにより、typecheckclosureが実行されるとすぐに、makeclosureが呼び出され、クロージャの実体である隠されたトップレベル関数が生成され、xtopに追加されます。これは、以前のwalkフェーズでの生成から大幅な前倒しとなります。
  • makeclosure関数の変更:

    • 関数のシグネチャからNodeList **init引数が削除されました。これは、この関数が初期化リストを直接操作する必要がなくなったためです。
    • 生成される隠された関数の名前が_func_%.3dからfunc·%.3dに変更されました。これはGoの内部的な命名規則に合わせたものです。
    • xfunc->nname->sym->flags |= SymExported; // disable exportという行が追加されました。これは、生成された隠された関数がGoのリンカによって外部にエクスポートされるべきではないことを明示的に示しています。これにより、内部的な関数が誤って外部から参照されることを防ぎます。
    • xfunc->closure = func;func->closure = xfunc;という双方向のリンクが設定されます。これは、元のクロージャノードと、そのクロージャの実体である隠された関数ノードとの間に明確な関連付けを確立します。これにより、コンパイラの他の部分が、クロージャとそれに対応する関数を容易に参照できるようになります。
    • func->nbody = nil; func->list = nil; func->rlist = nil;という行は、元のクロージャノードのボディやリストをクリアします。これは、クロージャの実際のコードは隠された関数の方に移動したため、元のクロージャノードはもはやコード生成の直接の対象ではないことを示しています。
  • walkclosureおよびwalkcallclosure関数の変更:

    • これらの関数では、以前はmakeclosureを呼び出して隠された関数を取得していましたが、変更後はfunc->closure->nnameのように、typecheckフェーズで既に設定されたリンクを通じて隠された関数を直接参照するようになりました。これにより、walkフェーズでの冗長な関数生成が不要になります。

src/cmd/gc/esc.c

  • escapes関数の変更:

    • for(l=all; l; l=l->next) if(l->n->op == ODCLFUNC || l->n->op == OCLOSURE) escfunc(l->n);という行がif(l->n->op == ODCLFUNC)に簡素化されました。これは、OCLOSUREノードがtypecheckフェーズで既にODCLFUNC(関数宣言)ノードとしてxtopに追加されているため、エスケープ解析はODCLFUNCノードのみを処理すればよくなったことを意味します。
  • escfunc関数の変更:

    • 以前は、escfuncの冒頭にクロージャのキャプチャ変数に関するエスケープ解析のロジックが含まれていましたが、これが削除されました。このロジックは、esc関数のOCLOSUREケースに移動され、より一般的なAST走査の一部として処理されるようになりました。
  • esc関数の変更:

    • if(n->op == OCLOSURE) { escfunc(n); } else { ... }という条件分岐が削除され、すべてのノードタイプに対して一般的なAST走査(esc(n->left); esc(n->right); ...)が適用されるようになりました。これは、OCLOSUREノードがもはや特別なエスケープ解析の対象ではなく、その実体であるODCLFUNCノードが通常の関数として処理されるためです。
    • case OCLOSURE:ブロックが追加され、クロージャがキャプチャする変数のアドレスをクロージャにリンクする処理がここで行われるようになりました。これは、キャプチャされた変数がヒープに割り当てられるべきかどうかをエスケープ解析が判断するために必要な情報です。// fallthroughコメントは、この処理の後、OCLOSUREノードがOADDR(アドレス取得)ノードと同様に扱われることを示唆しています。

src/cmd/gc/lex.c

  • main関数の変更:
    • NodeList *l, *batch;からbatch変数の宣言が削除されました。これは、クロージャのバッチコンパイルが不要になったためです。
    • 「Phase 6b: Compile all closures.」というコメントと、それに続くwhile(closures) { ... }のループ全体が削除されました。これは、クロージャがtypecheckフェーズで既にコンパイルの準備が整い、xtopに追加されているため、独立したクロージャコンパイルフェーズが不要になったことを意味します。
    • コンパイルフェーズの番号付けが調整され、「Phase 7: check external declarations.」が「Phase 6: Check external declarations.」に変更されました。これにより、コンパイルパイプラインがよりシンプルで線形なものになりました。

これらの変更は、Goコンパイラの内部構造をよりクリーンにし、クロージャの処理を他の関数と同様に扱うことで、コンパイラの複雑性を軽減し、将来的な最適化や機能追加を容易にすることを目的としています。

関連リンク

  • Go言語のクロージャ:
  • Goコンパイラに関する情報:
    • The Go Programming Language Specification - Function literals: https://go.dev/ref/spec#Function_literals
    • Goコンパイラの内部構造に関する詳細なドキュメントは公式には少ないですが、ソースコード自体が最も正確な情報源です。
  • エスケープ解析:
    • Goにおけるエスケープ解析の概念について解説している記事は多数存在します。例えば、「Go エスケープ解析」などで検索すると多くの情報が見つかります。

参考にした情報源リンク