[インデックス 1414] ファイルの概要
このコミットは、Go言語のコンパイラ(gc
)におけるmake
組み込み関数の導入と、new
組み込み関数のポインタ型への適用範囲の明確化に関する変更を含んでいます。具体的には、以下のファイルが変更されています。
src/cmd/gc/go.h
: コンパイラの内部で使用されるノードタイプや関数の宣言が含まれるヘッダファイル。src/cmd/gc/go.y
: Go言語の文法定義(Yacc/Bison形式)が含まれるファイル。src/cmd/gc/lex.c
: 字句解析器(lexer)の実装が含まれるファイル。src/cmd/gc/subr.c
: コンパイラのサブルーチンやユーティリティ関数が含まれるファイル。src/cmd/gc/walk.c
: 抽象構文木(AST)の走査(walk)と変換を行うコードが含まれるファイル。
コミット
commit 88b5c5f0f872e176be8e87b04c612817eeed5844
Author: Ken Thompson <ken@golang.org>
Date: Tue Jan 6 14:52:26 2009 -0800
make for slice/map/chan
new for pointers
R=r
OCL=22158
CL=22158
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/88b5c5f0f872e176be8e87b04c612817eeed5844
元コミット内容
make for slice/map/chan
new for pointers
変更の背景
このコミットは、Go言語の初期開発段階(2009年1月)に行われたもので、言語の基本的な組み込み関数であるmake
とnew
のセマンティクスを定義し、コンパイラにその機能を組み込むことを目的としています。
Go言語は、C++やJavaのような既存の言語が抱える大規模ソフトウェア開発における課題(コンパイル時間の遅さ、複雑な依存関係管理、冗長なエラー処理など)を解決するために設計されました。その設計思想の一つに、シンプルさと効率性、そして並行処理の強力なサポートがあります。
make
とnew
は、Go言語においてメモリ割り当てと初期化を行うための重要な組み込み関数です。このコミット以前は、これらの関数の振る舞いやコンパイラでの扱いが完全に定義されていなかった可能性があります。特に、スライス、マップ、チャネルといったGo特有のデータ構造の初期化には、単なるメモリ割り当て以上のロジックが必要となるため、make
という専用の関数が導入されました。一方、new
はより汎用的なメモリ割り当て(ゼロ値で初期化されたポインタを返す)に特化されることになりました。
この変更は、Go言語の型システムとメモリ管理の基盤を固める上で不可欠なステップであり、これらの組み込み関数が言語仕様の一部として確立される過程を示しています。
前提知識の解説
Go言語のmake
とnew
Go言語には、メモリを割り当てて初期化するための2つの組み込み関数make
とnew
があります。
-
new(T)
: 型T
のゼロ値が格納されるようにメモリを割り当て、その型*T
のポインタを返します。new
は、任意の型に対して使用でき、常にポインタを返します。割り当てられたメモリは、その型のゼロ値で初期化されます(例:int
は0
、string
は""
、ポインタはnil
)。p := new(int) // pは*int型で、*pは0
-
make(T, args)
: スライス、マップ、チャネルといった特定の組み込み型のみに使用できます。これらの型は、単にメモリを割り当てるだけでなく、内部的なデータ構造の初期化が必要となるため、make
が提供されます。make
は、初期化された(ゼロ値ではない)型T
の値を返します(ポインタではありません)。- スライス:
make([]T, len, cap)
のように使用し、長さlen
と容量cap
を持つスライスを作成します。 - マップ:
make(map[K]V, cap)
のように使用し、初期容量cap
を持つマップを作成します。 - チャネル:
make(chan T, cap)
のように使用し、バッファ容量cap
を持つチャネルを作成します。
s := make([]int, 5, 10) // 長さ5、容量10のintスライス m := make(map[string]int) // 空のstring-intマップ c := make(chan int, 10) // 容量10のintチャネル
- スライス:
このコミットは、make
がスライス、マップ、チャネルに特化され、new
がポインタの割り当てに特化されるという、現在のGo言語のセマンティクスを確立する初期段階の変更です。
コンパイラの基本的な仕組み
このコミットの変更を理解するためには、コンパイラの基本的な動作を把握しておく必要があります。
- 字句解析 (Lexical Analysis): ソースコードをトークン(意味を持つ最小単位)のストリームに変換します。例えば、
make
というキーワードはLMAKE
というトークンになります。 - 構文解析 (Syntax Analysis): トークンのストリームを解析し、言語の文法規則に従って抽象構文木(AST: Abstract Syntax Tree)を構築します。ASTは、プログラムの構造を階層的に表現したものです。
- 意味解析 (Semantic Analysis): ASTを走査し、型チェック、名前解決、エラー検出などの意味的な検証を行います。この段階で、
make
やnew
のような組み込み関数の引数の型や数などが検証されます。 - 中間コード生成 (Intermediate Code Generation): ASTを、ターゲットマシンに依存しない中間表現に変換します。
- コード最適化 (Code Optimization): 中間コードを最適化し、実行効率を高めます。
- コード生成 (Code Generation): 最適化された中間コードを、ターゲットマシン(この場合はGoのランタイムとアセンブリ)の機械語に変換します。
このコミットでは、主に字句解析、構文解析、意味解析、そしてASTの走査と変換(walk.c
)の段階に影響を与えています。
技術的詳細
このコミットは、Goコンパイラの内部でmake
組み込み関数を認識し、処理するための基盤を構築しています。
-
src/cmd/gc/go.h
の変更:enum
定義にOMAKE
という新しいノードタイプが追加されました。これは、コンパイラのAST内でmake
関数呼び出しを表すために使用されます。newcompat(Node*)
関数の宣言が削除され、代わりにmakecompat(Node*)
が追加されました。これは、new
とmake
の処理ロジックが分離されることを示唆しています。
-
src/cmd/gc/go.y
の変更:- 字句トークン定義から
LNEW
が削除され、LMAKE
が追加されました。これは、make
がキーワードとして認識されるようになったことを意味します。 pexpr
(プライマリ式)の文法規則に、LMAKE
キーワードを用いた新しい規則が追加されました。LMAKE '(' type ')'
:make(Type)
形式の呼び出しをパースします。LMAKE '(' type ',' expr_list ')'
:make(Type, args...)
形式の呼び出しをパースします。- これらの規則は、
OMAKE
ノードを生成し、その型情報を設定します。
sym3
(シンボル)の定義にLMAKE
が追加されました。
- 字句トークン定義から
-
src/cmd/gc/lex.c
の変更:- 字句解析器のキーワードテーブルに
"make"
とLMAKE
の対応が追加されました。これにより、ソースコード中のmake
という文字列がLMAKE
トークンとして認識されるようになります。
- 字句解析器のキーワードテーブルに
-
src/cmd/gc/subr.c
の変更:opnames
配列にOMAKE
ノードタイプに対応する文字列"MAKE"
が追加されました。これは、デバッグやエラーメッセージの出力時にOMAKE
ノードを識別するために使用されます。
-
src/cmd/gc/walk.c
の変更:loop
関数内のコメントが// convert dynamic to static generated by ONEW
から// convert dynamic to static generated by ONEW/OMAKE
に変更されました。これは、ONEW
だけでなくOMAKE
によって生成された動的な型も静的な型に変換する処理が適用されることを示唆しています。ONEW
ノードを処理する箇所で、nnew = nod(ONEW, N, N);
がnnew = nod(OMAKE, N, N);
に変更されました。これは、特定のケースでONEW
がOMAKE
に置き換えられることを示しています。OMAKE
ケースがloop
関数内のswitch
文に追加されました。top != Erv
の場合にnottop
にジャンプするチェックが追加されています。Erv
は「式として評価される値」を意味し、make
が値を返す式であることを示しています。indir(n, makecompat(n));
が呼び出され、make
のセマンティックチェックと変換が行われます。
newcompat
関数の名前がmakecompat
に変更され、そのロジックがmake
のセマンティクスに合わせて調整されました。isptr[t0->etype]
のチェックが削除され、make
がポインタ型だけでなく、スライス、マップ、チャネルといった非ポインタ型にも適用されることを反映しています。t == T
のチェックが追加され、型が不明な場合のbad
ラベルへのジャンプが追加されました。TSTRING
、TMAP
、TCHAN
のケースがswitch
文から削除され、make
がこれらの型を直接処理するようになったことを示唆しています。default
ケースでyyerror("cannot make(%T, expr)", t0);
というエラーメッセージが追加され、make
の引数に関する型チェックが強化されました。mal
(メモリ割り当て)関数の呼び出しロジックが追加され、make
がメモリを割り当てる処理を行うことを示しています。
newcompat
関数が新たに定義され、new
のセマンティクスに合わせてロジックが調整されました。TSTRING
、TMAP
、TCHAN
のケースがbad
ラベルにジャンプするように変更され、new
がこれらの型には適用されないことが明確化されました。yyerror("cannot new(%T, expr)", t);
というエラーメッセージが追加され、new
の引数に関する型チェックが強化されました。
mapop
、chanop
、arrayop
関数内のONEW
ケースがOMAKE
に変更されました。これは、マップ、チャネル、配列(スライス)の初期化がmake
によって行われるようになったことを明確に示しています。arraylit
とmaplit
関数内のONEW
ノード生成がOMAKE
に変更されました。これは、配列リテラルやマップリテラルの内部的な初期化もmake
のロジックを使用するようになったことを示しています。
これらの変更は、Goコンパイラがmake
とnew
という2つの異なる組み込み関数を、それぞれの役割に応じて適切に処理するための重要なステップです。特に、make
がスライス、マップ、チャネルの初期化に特化され、new
がポインタのゼロ値割り当てに特化されるという、Go言語の現在のセマンティクスがこのコミットによって確立され始めています。
コアとなるコードの変更箇所
src/cmd/gc/go.h
--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -292,7 +292,7 @@ enum
OLIST, OCMP, OPTR, OARRAY, ORANGE,
ORETURN, OFOR, OIF, OSWITCH,
OAS, OASOP, OCASE, OXCASE, OFALL, OXFALL,
-\tOGOTO, OPROC, ONEW, OEMPTY, OSELECT,
+\tOGOTO, OPROC, OMAKE, ONEW, OEMPTY, OSELECT,
OLEN, OCAP, OPANIC, OPANICN, OPRINT, OPRINTN, OTYPEOF,
OOROR,
@@ -789,6 +789,7 @@ int
ascompat(Type*, Type*);
Node* prcompat(Node*, int);
Node* nodpanic(int32);
-Node* newcompat(Node*);
+Node* makecompat(Node*);
Node* stringop(Node*, int);
Type* fixmap(Type*);
Node* mapop(Node*, int);
src/cmd/gc/go.y
--- a/src/cmd/gc/go.y
+++ b/src/cmd/gc/go.y
@@ -18,8 +18,8 @@
%token <sym> LPACKAGE LIMPORT LEXPORT
%token <sym> LMAP LCHAN LINTERFACE LFUNC LSTRUCT
%token <sym> LCOLAS LFALL LRETURN LDDD
-%token <sym> LNEW LLEN LCAP LTYPEOF LPANIC LPANICN LPRINT LPRINTN
-%token <sym> LVAR LTYPE LCONST LCONVERT LSELECT
+%token <sym> LLEN LCAP LTYPEOF LPANIC LPANICN LPRINT LPRINTN
+%token <sym> LVAR LTYPE LCONST LCONVERT LSELECT LMAKE LNEW
%token <sym> LFOR LIF LELSE LSWITCH LCASE LDEFAULT
%token <sym> LBREAK LCONTINUE LGO LGOTO LRANGE
%token <sym> LNIL LTRUE LFALSE LIOTA
@@ -864,6 +864,16 @@ pexpr:
$$ = nod(ONEW, $5, N);
$$->type = $3;
}
+|\tLMAKE '(' type ')'
+\t{
+\t\t$$ = nod(OMAKE, N, N);
+\t\t$$->type = $3;
+\t}\n+|\tLMAKE '(' type ',' expr_list ')'
+\t{
+\t\t$$ = nod(OMAKE, $5, N);
+\t\t$$->type = $3;
+\t}\n |\tatype '(' expr ')'
{
$$ = nod(OCONV, $3, N);
}
@@ -980,6 +990,7 @@ sym3:
|\tLPRINT
|\tLPRINTN
|\tLNEW
+|\tLMAKE
|\tLBASETYPE
|\tLTYPEOF
src/cmd/gc/walk.c
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -979,7 +979,7 @@ loop:
nvar = nod(0, N, N);
tempname(nvar, t);
-\t\t\tnnew = nod(ONEW, N, N);
+\t\t\tnnew = nod(OMAKE, N, N);
\t\t\tnnew->type = t;
\t\t\tnnew = newcompat(nnew);
@@ -1017,6 +1017,12 @@ loop:
n->type = t->type;
goto ret;
+\tcase OMAKE:
+\t\tif(top != Erv)
+\t\t\tgoto nottop;
+\t\tindir(n, makecompat(n));
+\t\tgoto ret;
+\n \tcase ONEW:
\t\tif(top != Erv)
\t\t\tgoto nottop;
\n@@ -2014,7 +2020,7 @@ nodpanic(int32 lineno)
}
Node*
-newcompat(Node *n)
+makecompat(Node *n)
{
Node *r, *on;
Type *t, *t0;
@@ -2026,21 +2032,24 @@ newcompat(Node *n)
if(t0->etype == TARRAY)
return arrayop(n, Erv);
-\tif(!isptr[t0->etype] || t0->type == T)
+\tif(!isptr[t0->etype])
goto bad;
t = t0->type;
+\tif(t == T)
+\t\tgoto bad;
+\n \tswitch(t->etype) {
case TSTRING:
goto bad;
-\t// the call looks like new(map[int]int)
+\t// the call looks like new(MAP[int]int)
// but internally we see new(*MAP[int]int)
case TMAP:
r = mapop(n, Erv);
break;
-\t// the call looks like new(chan int)
+\t// the call looks like new(CHAN int)
// but internally we see new(*CHAN int)
case TCHAN:
r = chanop(n, Erv);
@@ -2048,7 +2057,42 @@ newcompat(Node *n)
default:
if(n->left != N)
-\t\t\tyyerror("cannot new(%T, expr)", t0);
+\t\t\tyyerror("cannot make(%T, expr)", t0);
+\t\tdowidth(t);
+\t\ton = syslook("mal", 1);
+\t\targtype(on, t);
+\t\tr = nodintconst(t->width);
+\t\tr = nod(OCALL, on, r);
+\t\twalktype(r, Erv);
+\t\tbreak;
+\t}
+\n+\treturn r;
+\n+bad:
+\tyyerror("cannot make(%T)", t0);
+\treturn n;
+}\n+\n+Node*\n+newcompat(Node *n)
+{\n+\tNode *r, *on;\n+\tType *t, *t0;\n+\n+\tt = n->type;\n+\tif(t == T)
+\t\tgoto bad;
+\n+\tswitch(t->etype) {
+\tcase TSTRING:
+\tcase TMAP:
+\tcase TCHAN:
+\t\tgoto bad;
+\n+\tdefault:
+\t\tif(n->left != N)
+\t\t\tyyerror("cannot new(%T, expr)", t);\n \t\tdowidth(t);
\t\ton = syslook("mal", 1);
\t\targtype(on, t);
\t\tr = nodintconst(t->width);
\t\tr = nod(OCALL, on, r);
\t\twalktype(r, Erv);
\t\tbreak;
@@ -2061,7 +2105,7 @@ newcompat(Node *n)
return r;
bad:
-\tyyerror("cannot new(%T)", t0);
+\tyyerror("cannot new(%T)", t);
return n;
}
@@ -2224,7 +2268,7 @@ mapop(Node *n, int top)
default:
fatal("mapop: unknown op %O", n->op);
-\tcase ONEW:
+\tcase OMAKE:
\tif(top != Erv)
goto nottop;
@@ -2436,7 +2480,7 @@ chanop(Node *n, int top)
default:
fatal("chanop: unknown op %O", n->op);
-\tcase ONEW:
+\tcase OMAKE:
// newchan(elemsize int, elemalg int,
// hint int) (hmap *chan[any-1]);
@@ -2642,7 +2686,7 @@ arrayop(Node *n, int top)
n->right = r;
return n;
-\tcase ONEW:
+\tcase OMAKE:
// newarray(nel int, max int, width int) (ary []any)
t = fixarray(n->type);
if(t == T)
@@ -3523,7 +3567,7 @@ arraylit(Node *n)
var = nod(OXXX, N, N);
tempname(var, t);
-\tnnew = nod(ONEW, N, N);
+\tnnew = nod(OMAKE, N, N);
nnew->type = t;
nas = nod(OAS, var, nnew);
@@ -3560,7 +3604,7 @@ maplit(Node *n)
var = nod(OXXX, N, N);
tempname(var, t);
-\ta = nod(ONEW, N, N);
+\ta = nod(OMAKE, N, N);
a->type = t;
a = nod(OAS, var, a);
addtop = list(addtop, a);
コアとなるコードの解説
src/cmd/gc/go.h
enum
内のONEW
の前にOMAKE
が追加されました。これは、コンパイラがmake
という新しい種類の操作を内部的に区別できるようにするためです。newcompat(Node*)
の宣言がmakecompat(Node*)
に変更されました。これは、new
とmake
のセマンティックチェックとコード生成ロジックが、それぞれ独立した関数で処理されるようになったことを示しています。
src/cmd/gc/go.y
%token
定義でLMAKE
が追加され、LNEW
がLMAKE
の後に移動しました。これにより、make
がGo言語のキーワードとして認識されるようになります。pexpr
(プライマリ式)の文法規則に、make
関数の呼び出しをパースするための新しい規則が追加されました。LMAKE '(' type ')'
:make(Type)
のような形式を処理します。これは、例えばmake(chan int)
のように、容量指定なしでチャネルを作成する場合などに使われます。LMAKE '(' type ',' expr_list ')'
:make(Type, args...)
のような形式を処理します。これは、例えばmake([]int, 10, 20)
のように、スライスやマップ、チャネルの初期容量などを指定する場合に使われます。- これらの規則は、
OMAKE
ノードを生成し、そのノードに引数と型情報を関連付けます。
src/cmd/gc/walk.c
loop
関数内のコメントがONEW/OMAKE
を含むように更新されました。これは、new
とmake
の両方によって生成される動的な型に対して、静的な型への変換処理が適用されることを示しています。loop
関数内の特定の箇所で、ONEW
ノードの生成がOMAKE
ノードの生成に置き換えられました。これは、Go言語の内部的な処理において、特定のメモリ割り当てと初期化のシナリオがmake
のセマンティクスに移行したことを示唆しています。loop
関数内のswitch
文にOMAKE
ケースが追加されました。if(top != Erv)
チェックは、make
が値を返す式として使用されることを保証します。indir(n, makecompat(n));
は、make
関数のセマンティックチェックと、それに対応する中間コードの生成を行うmakecompat
関数を呼び出します。
newcompat
関数の名前がmakecompat
に変更され、その内部ロジックがmake
のセマンティクスに合わせて大幅に修正されました。if(!isptr[t0->etype])
のチェックが削除されたことで、make
がポインタ型だけでなく、スライス、マップ、チャネルといった非ポインタ型にも適用されるようになったことが明確になります。TSTRING
、TMAP
、TCHAN
のケースがswitch
文から削除され、make
がこれらの型を直接処理するようになったことを示唆しています。default
ケースでは、yyerror("cannot make(%T, expr)", t0);
というエラーメッセージが追加され、make
の引数に関する厳密な型チェックが行われるようになりました。mal
(メモリ割り当て)関数の呼び出しロジックが追加され、make
がメモリを割り当て、初期化を行う役割を担うことが示されています。
newcompat
関数が新たに定義され、new
のセマンティクスに合わせてロジックが調整されました。TSTRING
、TMAP
、TCHAN
のケースがbad
ラベルにジャンプするように変更され、new
がこれらの型には適用されないことが明確化されました。yyerror("cannot new(%T, expr)", t);
というエラーメッセージが追加され、new
の引数に関する型チェックが強化されました。
mapop
、chanop
、arrayop
関数内のONEW
ケースがOMAKE
に変更されました。これは、マップ、チャネル、配列(スライス)の初期化が、new
ではなくmake
によって行われるようになったことを明確に示しています。arraylit
とmaplit
関数内の配列リテラルやマップリテラルの内部的な初期化も、ONEW
からOMAKE
ノードの生成に切り替わりました。これは、これらのリテラルの内部的なメモリ割り当てと初期化がmake
のロジックを使用するようになったことを示しています。
これらの変更は、Go言語のコンパイラがmake
とnew
という異なる組み込み関数を、それぞれの役割に応じて適切に処理するための重要な基盤を構築したことを示しています。特に、スライス、マップ、チャネルの初期化にmake
が使用されるというGo言語の現在のセマンティクスが、このコミットによって確立され始めています。
関連リンク
- Go言語の
make
とnew
に関する公式ドキュメント: https://go.dev/doc/effective_go#allocation_make
参考にした情報源リンク
- Go言語の歴史に関する情報:
- Go言語の初期の設計に関する批判(
make
vsnew
には直接言及なし): https://alexgaynor.net/