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

[インデックス 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月)に行われたもので、言語の基本的な組み込み関数であるmakenewのセマンティクスを定義し、コンパイラにその機能を組み込むことを目的としています。

Go言語は、C++やJavaのような既存の言語が抱える大規模ソフトウェア開発における課題(コンパイル時間の遅さ、複雑な依存関係管理、冗長なエラー処理など)を解決するために設計されました。その設計思想の一つに、シンプルさと効率性、そして並行処理の強力なサポートがあります。

makenewは、Go言語においてメモリ割り当てと初期化を行うための重要な組み込み関数です。このコミット以前は、これらの関数の振る舞いやコンパイラでの扱いが完全に定義されていなかった可能性があります。特に、スライス、マップ、チャネルといったGo特有のデータ構造の初期化には、単なるメモリ割り当て以上のロジックが必要となるため、makeという専用の関数が導入されました。一方、newはより汎用的なメモリ割り当て(ゼロ値で初期化されたポインタを返す)に特化されることになりました。

この変更は、Go言語の型システムとメモリ管理の基盤を固める上で不可欠なステップであり、これらの組み込み関数が言語仕様の一部として確立される過程を示しています。

前提知識の解説

Go言語のmakenew

Go言語には、メモリを割り当てて初期化するための2つの組み込み関数makenewがあります。

  • new(T): 型Tのゼロ値が格納されるようにメモリを割り当て、その型*Tのポインタを返します。newは、任意の型に対して使用でき、常にポインタを返します。割り当てられたメモリは、その型のゼロ値で初期化されます(例: int0string""、ポインタは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言語のセマンティクスを確立する初期段階の変更です。

コンパイラの基本的な仕組み

このコミットの変更を理解するためには、コンパイラの基本的な動作を把握しておく必要があります。

  1. 字句解析 (Lexical Analysis): ソースコードをトークン(意味を持つ最小単位)のストリームに変換します。例えば、makeというキーワードはLMAKEというトークンになります。
  2. 構文解析 (Syntax Analysis): トークンのストリームを解析し、言語の文法規則に従って抽象構文木(AST: Abstract Syntax Tree)を構築します。ASTは、プログラムの構造を階層的に表現したものです。
  3. 意味解析 (Semantic Analysis): ASTを走査し、型チェック、名前解決、エラー検出などの意味的な検証を行います。この段階で、makenewのような組み込み関数の引数の型や数などが検証されます。
  4. 中間コード生成 (Intermediate Code Generation): ASTを、ターゲットマシンに依存しない中間表現に変換します。
  5. コード最適化 (Code Optimization): 中間コードを最適化し、実行効率を高めます。
  6. コード生成 (Code Generation): 最適化された中間コードを、ターゲットマシン(この場合はGoのランタイムとアセンブリ)の機械語に変換します。

このコミットでは、主に字句解析、構文解析、意味解析、そしてASTの走査と変換(walk.c)の段階に影響を与えています。

技術的詳細

このコミットは、Goコンパイラの内部でmake組み込み関数を認識し、処理するための基盤を構築しています。

  1. src/cmd/gc/go.hの変更:

    • enum定義にOMAKEという新しいノードタイプが追加されました。これは、コンパイラのAST内でmake関数呼び出しを表すために使用されます。
    • newcompat(Node*)関数の宣言が削除され、代わりにmakecompat(Node*)が追加されました。これは、newmakeの処理ロジックが分離されることを示唆しています。
  2. src/cmd/gc/go.yの変更:

    • 字句トークン定義からLNEWが削除され、LMAKEが追加されました。これは、makeがキーワードとして認識されるようになったことを意味します。
    • pexpr(プライマリ式)の文法規則に、LMAKEキーワードを用いた新しい規則が追加されました。
      • LMAKE '(' type ')': make(Type)形式の呼び出しをパースします。
      • LMAKE '(' type ',' expr_list ')': make(Type, args...)形式の呼び出しをパースします。
      • これらの規則は、OMAKEノードを生成し、その型情報を設定します。
    • sym3(シンボル)の定義にLMAKEが追加されました。
  3. src/cmd/gc/lex.cの変更:

    • 字句解析器のキーワードテーブルに"make"LMAKEの対応が追加されました。これにより、ソースコード中のmakeという文字列がLMAKEトークンとして認識されるようになります。
  4. src/cmd/gc/subr.cの変更:

    • opnames配列にOMAKEノードタイプに対応する文字列"MAKE"が追加されました。これは、デバッグやエラーメッセージの出力時にOMAKEノードを識別するために使用されます。
  5. 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);に変更されました。これは、特定のケースでONEWOMAKEに置き換えられることを示しています。
    • OMAKEケースがloop関数内のswitch文に追加されました。
      • top != Ervの場合にnottopにジャンプするチェックが追加されています。Ervは「式として評価される値」を意味し、makeが値を返す式であることを示しています。
      • indir(n, makecompat(n));が呼び出され、makeのセマンティックチェックと変換が行われます。
    • newcompat関数の名前がmakecompatに変更され、そのロジックがmakeのセマンティクスに合わせて調整されました。
      • isptr[t0->etype]のチェックが削除され、makeがポインタ型だけでなく、スライス、マップ、チャネルといった非ポインタ型にも適用されることを反映しています。
      • t == Tのチェックが追加され、型が不明な場合のbadラベルへのジャンプが追加されました。
      • TSTRINGTMAPTCHANのケースがswitch文から削除され、makeがこれらの型を直接処理するようになったことを示唆しています。
      • defaultケースでyyerror("cannot make(%T, expr)", t0);というエラーメッセージが追加され、makeの引数に関する型チェックが強化されました。
      • mal(メモリ割り当て)関数の呼び出しロジックが追加され、makeがメモリを割り当てる処理を行うことを示しています。
    • newcompat関数が新たに定義され、newのセマンティクスに合わせてロジックが調整されました。
      • TSTRINGTMAPTCHANのケースがbadラベルにジャンプするように変更され、newがこれらの型には適用されないことが明確化されました。
      • yyerror("cannot new(%T, expr)", t);というエラーメッセージが追加され、newの引数に関する型チェックが強化されました。
    • mapopchanoparrayop関数内のONEWケースがOMAKEに変更されました。これは、マップ、チャネル、配列(スライス)の初期化がmakeによって行われるようになったことを明確に示しています。
    • arraylitmaplit関数内のONEWノード生成がOMAKEに変更されました。これは、配列リテラルやマップリテラルの内部的な初期化もmakeのロジックを使用するようになったことを示しています。

これらの変更は、Goコンパイラがmakenewという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*)に変更されました。これは、newmakeのセマンティックチェックとコード生成ロジックが、それぞれ独立した関数で処理されるようになったことを示しています。

src/cmd/gc/go.y

  • %token定義でLMAKEが追加され、LNEWLMAKEの後に移動しました。これにより、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を含むように更新されました。これは、newmakeの両方によって生成される動的な型に対して、静的な型への変換処理が適用されることを示しています。
  • 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がポインタ型だけでなく、スライス、マップ、チャネルといった非ポインタ型にも適用されるようになったことが明確になります。
    • TSTRINGTMAPTCHANのケースがswitch文から削除され、makeがこれらの型を直接処理するようになったことを示唆しています。
    • defaultケースでは、yyerror("cannot make(%T, expr)", t0);というエラーメッセージが追加され、makeの引数に関する厳密な型チェックが行われるようになりました。
    • mal(メモリ割り当て)関数の呼び出しロジックが追加され、makeがメモリを割り当て、初期化を行う役割を担うことが示されています。
  • newcompat関数が新たに定義され、newのセマンティクスに合わせてロジックが調整されました。
    • TSTRINGTMAPTCHANのケースがbadラベルにジャンプするように変更され、newがこれらの型には適用されないことが明確化されました。
    • yyerror("cannot new(%T, expr)", t);というエラーメッセージが追加され、newの引数に関する型チェックが強化されました。
  • mapopchanoparrayop関数内のONEWケースがOMAKEに変更されました。これは、マップ、チャネル、配列(スライス)の初期化が、newではなくmakeによって行われるようになったことを明確に示しています。
  • arraylitmaplit関数内の配列リテラルやマップリテラルの内部的な初期化も、ONEWからOMAKEノードの生成に切り替わりました。これは、これらのリテラルの内部的なメモリ割り当てと初期化がmakeのロジックを使用するようになったことを示しています。

これらの変更は、Go言語のコンパイラがmakenewという異なる組み込み関数を、それぞれの役割に応じて適切に処理するための重要な基盤を構築したことを示しています。特に、スライス、マップ、チャネルの初期化にmakeが使用されるというGo言語の現在のセマンティクスが、このコミットによって確立され始めています。

関連リンク

参考にした情報源リンク