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

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

このコミットは、Go言語のコンパイラ(6gおよびgc)における関数に関するルールを更新し、関数をより第一級の市民として扱うための重要な変更を導入しています。具体的には、関数の型システム、パーシング、コード生成、およびセマンティクスが広範囲にわたって修正されています。これにより、Go言語における関数の取り扱いが一貫性のある、より現代的なものへと進化しました。

コミット

commit 4a431983906fb1bcb7d8b95b2e1cb497c799b76a
Author: Russ Cox <rsc@golang.org>
Date:   Fri Jan 30 14:39:42 2009 -0800

    update compiler to new func rules

    R=ken
    OCL=23958
    CL=23961

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

https://github.com/golang/go/commit/4a431983906fb1bcb7d8b95b2e1cb497c799b76a

元コミット内容

update compiler to new func rules

R=ken
OCL=23958
CL=23961

変更の背景

このコミットが行われた2009年1月は、Go言語がまだ初期開発段階にあった時期です。Go言語の設計目標の一つに、並行処理を容易にし、関数を第一級の市民として扱うことがありました。初期のコンパイラ実装では、関数の取り扱いがまだ完全に洗練されていなかった可能性があります。

この「新しい関数ルール」への更新は、Go言語の設計思想をコンパイラレベルでより深く反映させるためのものです。具体的には、関数を単なるコードブロックではなく、他のデータ型と同様に変数に代入したり、引数として渡したり、戻り値として返したりできる「値」として扱うための基盤を強化しています。これにより、Goの関数型プログラミングの側面がより明確になり、クロージャや高階関数といった機能がより自然に扱えるようになります。

特に、C言語のような「関数ポインタ」とは異なる、Go独自の「関数値」のセマンティクスを確立することが目的であったと考えられます。Goでは、関数はポインタのようにアドレスを持つものの、そのアドレスを直接操作したり、関数自体を再代入したりすることはできません。これは、関数が不変な値として扱われることを意味します。

前提知識の解説

このコミットを理解するためには、以下の概念が重要です。

  1. 第一級関数 (First-Class Functions): プログラミング言語において、関数が他の変数と同様に扱われる特性を指します。具体的には、以下のことが可能です。

    • 変数に代入できる。
    • 関数の引数として渡せる(高階関数)。
    • 関数の戻り値として返せる。
    • データ構造に格納できる。 Go言語は、この第一級関数をサポートしています。
  2. コンパイラのフェーズ:

    • 字句解析 (Lexical Analysis): ソースコードをトークン(単語)に分解する。
    • 構文解析 (Syntax Analysis): トークンの並びが文法的に正しいかチェックし、抽象構文木 (AST) を構築する。Go言語のコンパイラでは、go.y(Yacc/Bisonの入力ファイル)が構文解析を担当します。
    • 意味解析 (Semantic Analysis): ASTを走査し、型のチェック、名前解決、スコープの検証など、意味的な正しさを検証する。
    • 中間コード生成 (Intermediate Code Generation): ASTから、機械語に近いがまだ特定のCPUに依存しない中間表現を生成する。
    • コード生成 (Code Generation): 中間表現から、ターゲットCPUの機械語を生成する。6gは64-bitアーキテクチャ向けのコード生成を担当します。
  3. Go言語の型システム: Goは静的型付け言語であり、すべての変数と式には型があります。関数もまた、特定の引数と戻り値の型を持つ「関数型」として扱われます。

  4. Goコンパイラの内部構造:

    • src/cmd/gc/: Go言語のフロントエンド(パーサー、型チェッカー、意味解析など)を担当する汎用コンパイラ部分。
    • src/cmd/6g/: 64-bitアーキテクチャ(x86-64)向けのバックエンド(コード生成、アセンブラなど)を担当するコンパイラ部分。
    • Node: 抽象構文木 (AST) のノードを表す内部データ構造。
    • Type: 型情報を表す内部データ構造。
    • Sym: シンボル(変数名、関数名など)を表す内部データ構造。
    • PEXTERN, PAUTO, PPARAM など: シンボルの「クラス」を示す定数。このコミットでPFUNCが追加されます。

技術的詳細

このコミットの技術的詳細は、Goコンパイラの複数の層にわたる変更を含んでいます。

  1. PFUNC クラスの導入:

    • src/cmd/gc/go.hPFUNC という新しいシンボルクラスが導入されました。これは、関数を表すシンボルを他の外部シンボル (PEXTERN) や自動変数 (PAUTO) と明確に区別するために使用されます。
    • src/cmd/gc/dcl.cfunchdr および addvar 関数が更新され、関数宣言時にシンボルに PFUNC クラスを割り当てるようになりました。これにより、コンパイラはシンボルが関数であることをより正確に認識できます。
  2. 関数型の取り扱い強化:

    • src/cmd/6g/align.cdowidth 関数が修正され、TFUNC (関数型) の幅が wptr (ポインタの幅) として計算されるようになりました。これは、関数が内部的にポインタとして扱われることを示唆しています。
    • src/cmd/6g/gg.h および src/cmd/6g/gsubr.cafunclit 関数が追加されました。これは、関数リテラル(匿名関数)のアドレスをコード生成時に適切に処理するためのものです。
    • src/cmd/gc/subr.cTpretty 関数が修正され、関数型の文字列表現に func キーワードが含まれるようになりました。これにより、デバッグ出力やエラーメッセージで関数型がより明確に表示されます。
  3. 構文解析の変更 (go.y):

    • src/cmd/gc/go.y (Go言語の文法定義ファイル) が大幅に修正されました。
    • fntype ルールにおいて、関数型を定義する際に LFUNC (funcキーワード) が必須となりました。これは、func(int) int のような関数型リテラルを明示的に記述する必要があることを意味します。
    • fnliteral (関数リテラル、匿名関数) の定義から LFUNC が削除されました。これは、関数リテラルが fnlitdcl (関数リテラル宣言) の後に直接 {} ブロックが続く形式になったことを示唆しています。また、関数リテラルが PFUNC クラスで addvar されるようになりました。
    • 関数リテラルのアドレスを明示的に取得する nod(OADDR, $$, N); の行が削除されました。これは、関数リテラルが生成されると、その値が直接関数オブジェクトとして扱われ、そのアドレスはコンパイラが内部的に管理することを意味します。
  4. コード生成の変更 (6g/gen.c, 6g/cgen.c):

    • ginscall という新しいヘルパー関数が導入され、関数呼び出しのコード生成を抽象化しました。これにより、通常の関数呼び出し、go ステートメントによるゴルーチン起動、defer ステートメントによる遅延呼び出しなど、様々な種類の関数呼び出しが統一的に扱えるようになりました。
    • sysfunc ヘルパー関数が導入され、newproc, deferproc, throwindex などのランタイム関数への参照をより簡潔に生成できるようになりました。
    • ACALL (関数呼び出し命令) のターゲットが、関数リテラルの場合は afunclit で処理されるようになりました。
  5. セマンティクスの変更 (gc/walk.c):

    • OADDR (アドレス演算子 &) の処理に、関数型 (TFUNC) かつ PFUNC クラスのノードに対して & 演算子を使用した場合に「cannot take address of function」というエラーを出すチェックが追加されました。これは、Go言語において関数は「値」であり、C言語の関数ポインタのようにそのアドレスを直接操作するものではないというセマンティクスを強制するものです。関数はすでにそれ自体が参照可能な値であるため、明示的にアドレスを取る必要がない、という設計思想を反映しています。
    • OAS (代入演算子 =) の処理に、関数 (PFUNC クラスのノード) への代入を禁止するチェックが追加されました。これは、関数が不変な値として扱われることを意味します。一度定義された関数は、その参照先を変更できないというGoの設計原則を反映しています。

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

このコミットにおけるコアとなる変更は、主に以下のファイルに集中しています。

  • src/cmd/gc/go.y: Go言語の文法定義。関数型と関数リテラルの構文が変更されました。
  • src/cmd/gc/dcl.c: 宣言処理。関数シンボルに新しい PFUNC クラスを割り当てるようになりました。
  • src/cmd/gc/go.h: 共通ヘッダー。PFUNC クラスの定義と、関連する関数のシグネチャ変更が含まれます。
  • src/cmd/gc/walk.c: 意味解析とASTウォーク。関数へのアドレス取得や代入を禁止するセマンティックチェックが追加されました。
  • src/cmd/6g/gen.c: コード生成。関数呼び出しの新しいヘルパー関数 ginscall が導入され、ランタイム関数の初期化が整理されました。
  • src/cmd/6g/align.c: 型のサイズ計算。関数型のサイズがポインタサイズとして扱われるようになりました。

コアとなるコードの解説

src/cmd/gc/go.y (抜粋)

--- a/src/cmd/gc/go.y
+++ b/src/cmd/gc/go.y
@@ -1231,15 +1231,15 @@ fntype:
 |\tBfntype
 
 Afntype:
-\t'(' oarg_type_list ')' Afnres
+\tLFUNC '(' oarg_type_list ')' Afnres
 	{
-\t\t$$ = functype(N, $2, $4);
+\t\t$$ = functype(N, $3, $5);
 	}
 
 Bfntype:
-\t'(' oarg_type_list ')' Bfnres
+\tLFUNC '(' oarg_type_list ')' Bfnres
 	{
-\t\t$$ = functype(N, $2, $4);
+\t\t$$ = functype(N, $3, $5);
 	}
 
 fnlitdcl:
@@ -1251,7 +1251,7 @@ fnlitdcl:
 	}
 
 fnliteral:
-\tLFUNC fnlitdcl '{' ostmt_list '}'
+\tfnlitdcl '{' ostmt_list '}'
 	{
 		popdcl();
 
 		snprint(namebuf, sizeof(namebuf), "_f%.3ld", vargen);
 
 		$$ = newname(lookup(namebuf));
-\t\taddvar($$, $2, PEXTERN);
+\t\taddvar($$, $1, PFUNC);
 
 		{
 			Node *n;
 
 			n = nod(ODCLFUNC, N, N);
 			n->nname = $$;
-\t\t\tn->type = $2;
-\t\t\tn->nbody = $4;
+\t\t\tn->type = $1;
+\t\t\tn->nbody = $3;
 			if(n->nbody == N)
 				n->nbody = nod(ORETURN, N, N);
 			compile(n);
 		}
-\n-\t\t$$ = nod(OADDR, $$, N);\n 	}

この変更は、Go言語の構文における関数型の定義と関数リテラルの扱いを根本的に変えています。

  • fntype ルールに LFUNC (funcキーワード) が追加されたことで、func(int) string のような関数型を記述する際に func キーワードが必須になりました。これは、型としての関数をより明示的に表現するための変更です。
  • fnliteral ルールから LFUNC が削除され、関数リテラルが fnlitdcl の後に直接 {} ブロックが続く形式になりました。これは、関数リテラルがより簡潔に記述できるようになったことを示します。
  • addvar($$, $1, PFUNC); の行は、新しく定義された関数リテラルが PFUNC クラスとしてコンパイラに登録されることを意味します。
  • $$ = nod(OADDR, $$, N); の行が削除されたことは、関数リテラルが生成されると、その値が直接関数オブジェクトとして扱われ、そのアドレスを明示的に取得する必要がなくなったことを示唆しています。関数はそれ自体が値であり、その値が呼び出し可能であることを意味します。

src/cmd/gc/dcl.c (抜粋)

--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -384,9 +384,9 @@ funchdr(Node *n)
 		// declare fun name, argument types and argument names
 		n->nname->type = n->type;
 		if(n->type->thistuple == 0)
-\t\t\taddvar(n->nname, n->type, PEXTERN);
+\t\t\taddvar(n->nname, n->type, PFUNC);
 		else
-\t\t\tn->nname->class = PEXTERN;
+\t\t\tn->nname->class = PFUNC;
 	} else {
 		// identical redeclaration
 		// steal previous names
@@ -742,7 +739,7 @@ addvar(Node *n, Type *t, int ctxt)
 
 	s = n->sym;
 
-\tif(ctxt == PEXTERN) {
+\tif(ctxt == PEXTERN || ctxt == PFUNC) {
 		r = externdcl;
 		gen = 0;
 	} else {
@@ -773,6 +770,8 @@ addvar(Node *n, Type *t, int ctxt)
 	if(dflag()) {
 		if(ctxt == PEXTERN)
 			print("extern var-dcl %S G%ld %T\n", s, s->vargen, t);
+\t\telse if(ctxt == PFUNC)
+\t\t\tprint("extern func-dcl %S G%ld %T\n", s, s->vargen, t);
 		else
 			print("auto   var-dcl %S G%ld %T\n", s, s->vargen, t);
 	}

funchdr 関数は、関数宣言のヘッダーを処理する部分です。ここで、関数の名前を表すノードのクラスが PEXTERN から PFUNC に変更されました。これにより、コンパイラは関数を一般的な外部シンボルではなく、専用の PFUNC クラスとして認識し、より特化した処理を適用できるようになります。 addvar 関数も PFUNC クラスを認識するように更新され、デバッグ出力も追加されています。

src/cmd/gc/walk.c (抜粋)

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -1020,6 +1020,12 @@ loop:
 			indir(n, nvar);
 			goto ret;
 		}
+\t\tif(istype(n->left->type, TFUNC) && n->left->class == PFUNC) {
+\t\t\tif(!n->diag) {
+\t\t\t\tn->diag = 1;
+\t\t\t\tyyerror("cannot take address of function");
+\t\t\t}\n+\t\t}\n \t\twalktype(n->left, Elv);\n \t\taddrescapes(n->left);\n \t\tif(n->left == N)\n@@ -1721,6 +1725,8 @@ loop:
 		badtype(op, l->type, r->type);
 		return N;
 	}\n+\tif(l->op == ONAME && l->class == PFUNC)\n+\t\tyyerror("cannot assign to function");
 \n \ta = nod(OAS, l, r);\n \ta = convas(a);\n```
この `walk.c` の変更は、Go言語のセマンティクスにおいて非常に重要です。
*   `OADDR` (アドレス演算子 `&`) の処理において、左辺が関数型 (`TFUNC`) かつ `PFUNC` クラスである場合に「cannot take address of function」というエラーを発生させるようになりました。これは、Goにおいて関数はすでに値であり、C言語の関数ポインタのように明示的にアドレスを取る必要がないという設計思想を強制するものです。関数はそれ自体が呼び出し可能なオブジェクトとして扱われます。
*   `OAS` (代入演算子 `=`) の処理において、左辺が `ONAME` (名前) で `PFUNC` クラスである場合に「cannot assign to function」というエラーを発生させるようになりました。これは、Goの関数が不変な値であることを意味します。一度定義された関数は、その参照先を変更することはできません。

これらの変更は、Go言語における関数の「第一級の市民」としての地位を確立しつつ、C言語のような低レベルなポインタ操作とは一線を画す、Go独自の安全で一貫性のある関数モデルを構築するためのものです。

## 関連リンク

*   Go言語の公式ドキュメント: [https://go.dev/](https://go.dev/)
*   Go言語の初期の設計に関する議論(Goのメーリングリストやデザインドキュメントなど)

## 参考にした情報源リンク

*   コミットの差分情報: [https://github.com/golang/go/commit/4a431983906fb1bcb7d8b95b2e1cb497c799b76a](https://github.com/golang/go/commit/4a431983906fb1bcb7d8b95b2e1cb497c799b76a)
*   Go言語のコンパイラソースコード (特に `src/cmd/gc` と `src/cmd/6g` ディレクトリ)
*   Go言語の歴史に関する記事やドキュメント (例: "The Evolution of Go" など)
*   コンパイラ設計に関する一般的な知識 (字句解析、構文解析、意味解析、コード生成など)