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

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

コミット

commit ecab408c4223be3f49d9df52f2900a35bc68f444
Author: Russ Cox <rsc@golang.org>
Date:   Mon Mar 4 17:02:04 2013 -0500

    cmd/gc: implement new return requirements
    
    Fixes #65.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/7441049

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

https://github.com/golang/go/commit/ecab408c4223be3f49d9df52f2900a35bc68f444

元コミット内容

cmd/gc: implement new return requirements

このコミットは、Goコンパイラ(cmd/gc)において、関数の戻り値に関する新しい要件を実装するものです。具体的には、関数が値を返す必要がある場合に、コードパスの終端でreturnステートメントが不足していることを検出する機能を追加しています。これはGo言語のIssue #65を修正するものです。

変更の背景

Go言語では、戻り値を持つ関数は、すべての実行パスにおいて値を返すことが保証されなければなりません。しかし、Goコンパイラの初期の実装では、この要件が十分に厳密にチェックされていませんでした。特に、forループ、switchステートメント、selectステートメントなどの制御フロー構造内で、すべての可能なパスがreturnステートメントで終了しているかどうかの検証が不十分でした。

Issue #65は、この問題点を指摘しており、特定のコードパターンにおいて、コンパイラが「missing return at end of function」(関数の終端に戻り値がありません)というエラーを適切に報告しないケースがあることを示していました。このコミットは、このコンパイラの不備を修正し、Go言語の仕様に準拠した厳密な戻り値チェックを導入することを目的としています。これにより、開発者が意図しないコードパスで値が返されないというバグを未然に防ぐことができます。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびコンパイラの基本的な概念を理解しておく必要があります。

  • Go言語の関数と戻り値: Go言語では、関数は0個以上の戻り値を宣言できます。戻り値が宣言されている場合、関数はすべての可能な実行パスでそれらの値を返す必要があります。
  • 制御フロー(Control Flow): if, for, switch, select, goto, break, continueなどのステートメントは、プログラムの実行順序を制御します。コンパイラは、これらの制御フローを分析し、すべてのコードパスが適切に処理されているかを確認します。
  • Goコンパイラ(cmd/gc): Go言語の公式コンパイラであり、Goソースコードを機械語に変換します。コンパイルプロセスには、字句解析(lexing)、構文解析(parsing)、型チェック(type checking)、中間コード生成(intermediate code generation)、最適化(optimization)、コード生成(code generation)などが含まれます。
  • 抽象構文木(AST: Abstract Syntax Tree): ソースコードの構文構造を木構造で表現したものです。コンパイラは、ソースコードをASTに変換し、そのASTを走査して型チェックやコード生成などの処理を行います。
  • go.h: Goコンパイラの内部で使用されるヘッダーファイルで、ASTノードの構造体定義や、コンパイラ内部のユーティリティ関数のプロトタイプ宣言などが含まれます。
  • go.y: Go言語の文法を定義するYacc(またはBison)の入力ファイルです。構文解析器がソースコードをASTに変換する際のルールが記述されています。
  • lex.c: 字句解析器(lexer)の実装ファイルです。ソースコードをトークンに分割する役割を担います。
  • typecheck.c: 型チェッカーの実装ファイルです。ASTを走査し、Go言語の型システムに従って型の整合性を検証します。
  • walk.c: ASTを走査し、最適化やコード生成の前処理を行うファイルです。
  • y.tab.c: go.yから生成される構文解析器のC言語ソースファイルです。

技術的詳細

このコミットの主要な変更点は、Goコンパイラの型チェックフェーズにおいて、関数の戻り値要件をより厳密に検証するためのロジックが追加されたことです。

具体的には、以下の変更が行われています。

  1. Node構造体へのhasbreakフィールドの追加: src/cmd/gc/go.hにおいて、ASTノードを表すNode構造体にuchar hasbreak;というフィールドが追加されました。これは、forswitchselectrangeなどのループや選択ステートメントが、その内部にbreakステートメントを含んでいるかどうかを示すフラグです。この情報は、コードパスが終端に到達するかどうかを判断する際に重要になります。breakが存在する場合、ループや選択ステートメントが途中で終了し、その後のコードパスに影響を与える可能性があるためです。

  2. checkreturn関数の導入とisterminating関数の改善: src/cmd/gc/typecheck.ccheckreturn関数が新しく追加されました。この関数は、関数のASTを受け取り、その関数が戻り値を必要とする場合に、すべての実行パスがreturnステートメントで終了しているかを検証します。 checkreturn関数は、新たに導入されたisterminating関数を利用します。isterminating関数は、与えられたステートメントリストが、その終端に到達する前に必ず実行を終了するか(つまり、returnpanicgoto、または無限ループによって終了するか)を再帰的に判断します。 isterminating関数は、特に以下の制御フロー構造を考慮して改善されています。

    • OFOR (forループ): 条件式がない無限ループ(for {})や、breakステートメントを含まないループは、常に実行を終了すると見なされます。hasbreakフラグがここで利用され、ループがbreakによって途中で終了する可能性がある場合は、終端に到達しないと判断されます。
    • OIF (ifステートメント): ifブロックとelseブロックの両方が実行を終了する場合にのみ、ifステートメント全体が実行を終了すると判断されます。
    • OSWITCH, OTYPESW, OSELECT (switch/selectステートメント): すべてのcase節が実行を終了し、かつdefault節が存在する場合にのみ、switch/selectステートメント全体が実行を終了すると判断されます。ここでもhasbreakフラグが利用され、breakによってswitch/selectが途中で終了する可能性がある場合は、終端に到達しないと判断されます。
    • OGOTO, ORETURN, OPANIC, OXFALL: これらのステートメントは、常に実行を終了すると見なされます。
  3. markbreakおよびmarkbreaklist関数の追加: src/cmd/gc/typecheck.cmarkbreakmarkbreaklistというヘルパー関数が追加されました。これらの関数は、ASTを走査し、OBREAKノードを見つけると、そのbreakが属する最も内側のループまたは選択ステートメントのhasbreakフラグをセットします。これにより、isterminating関数が正確な判断を下せるようになります。

  4. コンパイルフローへのcheckreturnの統合: src/cmd/gc/lex.cmain関数内で、各関数の型チェックが完了した後(typechecklistの呼び出し後)に、checkreturn(l->n);が呼び出されるようになりました。これにより、すべての関数に対して戻り値の要件チェックが実行されるようになります。

  5. 古い戻り値チェックロジックの削除: src/cmd/gc/walk.cから、以前の不完全な戻り値チェックロジックであるwalkret関数とその関連コードが削除されました。これは、新しいcheckreturn関数とisterminating関数がより正確で包括的なチェックを提供するようになったためです。

  6. go.yy.tab.cの変更: go.yファイルには、構文解析器がASTを構築する際のルールに関する微調整が含まれています。特に、compound_stmt(複合ステートメント、つまりブロック)の処理において、空のブロックの場合にnod(OEMPTY, N, N)を返すように変更されています。これは、ASTの構造をより一貫性のあるものにするための変更であり、戻り値チェックのロジックと間接的に関連しています。y.tab.cgo.yから自動生成されるファイルであるため、go.yの変更に伴って更新されています。

  7. テストケースの追加と修正: test/return.goに、新しい戻り値チェックロジックを検証するための広範なテストケースが追加されました。これには、様々な制御フロー構造(for, switch, select, if)とreturnステートメントの組み合わせが含まれており、コンパイラが正しいエラーを報告するか、またはエラーを報告しないかを検証します。 test/fixedbugs/bug086.gotest/fixedbugs/issue4663.goも、この変更に関連して修正されています。

これらの変更により、Goコンパイラは、関数の戻り値要件に関してより堅牢なチェックを行うようになり、Goプログラムの信頼性と安全性が向上しました。

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

src/cmd/gc/go.h

--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -268,6 +268,7 @@ struct	Node
 	uchar	addrtaken;	// address taken, even if not moved to heap
 	uchar	dupok;	// duplicate definitions ok (for func)
 	schar	likely; // likeliness of if statement
+	uchar	hasbreak;	// has break statement
 
 	// most nodes
 	Type*	type;
@@ -1363,6 +1364,7 @@ Node*	typecheck(Node **np, int top);
 void	typechecklist(NodeList *l, int top);
 Node*	typecheckdef(Node *n);
 void	copytype(Node *n, Type *t);
+void	checkreturn(Node*);
 void	queuemethod(Node *n);

src/cmd/gc/lex.c

--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -376,6 +376,7 @@ main(int argc, char *argv[])
 			curfn = l->n;
 			saveerrors();
 			typechecklist(l->n->nbody, Etop);
+			checkreturn(l->n);
 			if(nerrors != 0)
 				l->n->nbody = nil;  // type errors; do not compile
 		}

src/cmd/gc/typecheck.c

--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -3144,3 +3144,148 @@ checkmake(Type *t, char *arg, Node *n)
 	}
 	return 0;
 }
+
+static void	markbreaklist(NodeList*, Node*);
+
+static void
+markbreak(Node *n, Node *implicit)
+{
+	Label *lab;
+
+	if(n == N)
+		return;
+
+	switch(n->op) {
+	case OBREAK:
+		if(n->left == N) {
+			if(implicit)
+				implicit->hasbreak = 1;
+		} else {
+			lab = n->left->sym->label;
+			if(lab != L)
+				lab->def->hasbreak = 1;
+		}
+		break;
+	
+	case OFOR:
+	case OSWITCH:
+	case OTYPESW:
+	case OSELECT:
+	case ORANGE:
+		implicit = n;
+		// fall through
+	
+	default:
+		markbreak(n->left, implicit);
+		markbreak(n->right, implicit);
+		markbreak(n->ntest, implicit);
+		markbreak(n->nincr, implicit);
+		markbreaklist(n->ninit, implicit);
+		markbreaklist(n->nbody, implicit);
+		markbreaklist(n->nelse, implicit);
+		markbreaklist(n->list, implicit);
+		markbreaklist(n->rlist, implicit);
+		break;
+	}
+}
+
+static void
+markbreaklist(NodeList *l, Node *implicit)
+{
+	Node *n;
+	Label *lab;
+
+	for(; l; l=l->next) {
+		n = l->n;
+		if(n->op == OLABEL && l->next && n->defn == l->next->n) {
+			switch(n->defn->op) {
+			case OFOR:
+			case OSWITCH:
+			case OTYPESW:
+			case OSELECT:
+			case ORANGE:
+				lab = mal(sizeof *lab);
+				lab->def = n->defn;
+				n->left->sym->label = lab;
+				markbreak(n->defn, n->defn);
+				n->left->sym->label = L;
+				l = l->next;
+				continue;
+			}
+		}
+		markbreak(n, implicit);
+	}
+}
+
+static int
+isterminating(NodeList *l, int top)
+{
+	int def;
+	Node *n;
+
+	if(l == nil)
+		return 0;
+	if(top) {
+		while(l->next && l->n->op != OLABEL)
+			l = l->next;
+		markbreaklist(l, nil);
+	}
+	while(l->next)
+		l = l->next;
+	n = l->n;
+
+	if(n == N)
+		return 0;
+
+	switch(n->op) {
+	// NOTE: OLABEL is treated as a separate statement,
+	// not a separate prefix, so skipping to the last statement
+	// in the block handles the labeled statement case by
+	// skipping over the label. No case OLABEL here.
+
+	case OBLOCK:
+		return isterminating(n->list, 0);
+
+	case OGOTO:
+	case ORETURN:
+	case OPANIC:
+	case OXFALL:
+		return 1;
+
+	case OFOR:
+		if(n->ntest != N)
+			return 0;
+		if(n->hasbreak)
+			return 0;
+		return 1;
+
+	case OIF:
+		return isterminating(n->nbody, 0) && isterminating(n->nelse, 0);
+
+	case OSWITCH:
+	case OTYPESW:
+	case OSELECT:
+		if(n->hasbreak)
+			return 0;
+		def = 0;
+		for(l=n->list; l; l=l->next) {
+			if(!isterminating(l->n->nbody, 0))
+				return 0;
+			if(l->n->list == nil) // default
+				def = 1;
+		}
+		if(n->op != OSELECT && !def)
+			return 0;
+		return 1;
+	}
+	
+	return 0;
+}
+
+void
+checkreturn(Node *fn)
+{
+	if(fn->type->outtuple && fn->nbody != nil)
+		if(!isterminating(fn->nbody, 1))
+			yyerrorl(fn->endlineno, "missing return at end of function");
+}

src/cmd/gc/walk.c

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -29,40 +29,6 @@ static	void	walkdiv(Node**, NodeList**);
 static	int	bounded(Node*, int64);
 static	Mpint	mpzero;
 
-// can this code branch reach the end
-// without an unconditional RETURN
-// this is hard, so it is conservative
-static int
-walkret(NodeList *l)
-{
-	Node *n;
-
-loop:
-	while(l && l->next)
-		l = l->next;
-	if(l == nil)
-		return 1;
-
-	// at this point, we have the last
-	// statement of the function
-	n = l->n;
-	switch(n->op) {
-	case OBLOCK:
-		l = n->list;
-		goto loop;
-
-	case OGOTO:
-	case ORETURN:
-	case OPANIC:
-		return 0;
-		break;
-	}
-
-	// all other statements
-	// will flow to the end
-	return 1;
-}
-
 void
 walk(Node *fn)
 {
@@ -76,9 +42,6 @@ walk(Node *fn)
 		snprint(s, sizeof(s), "\nbefore %S", curfn->nname->sym);
 		dumplist(s, curfn->nbody);
 	}
-	if(curfn->type->outtuple)
-		if(walkret(curfn->nbody))
-			yyerror("function ends without a return statement");
 
 	lno = lineno;

コアとなるコードの解説

このコミットの核心は、Goコンパイラが関数の戻り値要件を厳密にチェックするための新しいメカニズムを導入したことです。

  1. Node構造体へのhasbreakフィールドの追加 (src/cmd/gc/go.h): これは、ASTノードがbreakステートメントを含むかどうかを追跡するための単純なフラグです。forswitchselectなどの制御フロー構造は、breakによって途中で終了する可能性があります。このフラグは、そのような制御フローが「終端に到達する」かどうかを正確に判断するために不可欠です。

  2. checkreturn関数の導入とisterminating関数の改善 (src/cmd/gc/typecheck.c):

    • checkreturn(Node *fn): この関数は、Goコンパイラの型チェックフェーズで呼び出されます。引数として関数のASTノードを受け取ります。もし関数が戻り値を宣言しており(fn->type->outtupleが真)、かつ関数本体が存在する場合(fn->nbody != nil)、isterminating関数を呼び出して、関数本体のすべての実行パスがreturnステートメントで終了しているかをチェックします。もし終了していない場合、yyerrorlを使って「missing return at end of function」というエラーを報告します。
    • isterminating(NodeList *l, int top): この関数は、与えられたステートメントリスト(NodeList)が、その終端に到達する前に必ず実行を終了するかどうかを判断する再帰的な関数です。
      • top引数は、関数本体のトップレベルのステートメントリストを処理しているかどうかを示します。トップレベルの場合、ラベル付きステートメントを適切に処理するために、リストの最後のステートメントに移動する前にmarkbreaklistを呼び出します。
      • 関数は、ステートメントリストの最後のステートメントを検査します。
      • OGOTO, ORETURN, OPANIC, OXFALLfallthrough)のようなステートメントは、それ自体が実行を終了させるため、1(終了する)を返します。
      • OBLOCK(ブロック)の場合、そのブロック内のステートメントリストに対して再帰的にisterminatingを呼び出します。
      • OFOR(forループ)の場合、条件式がない無限ループ(n->ntest == N)で、かつbreakステートメントを含まない(n->hasbreak == 0)場合にのみ、ループが実行を終了すると判断します。
      • OIF(ifステートメント)の場合、ifブロックとelseブロックの両方が実行を終了する場合にのみ、ifステートメント全体が実行を終了すると判断します。
      • OSWITCH, OTYPESW, OSELECT(switch/selectステートメント)の場合、breakステートメントを含まず(n->hasbreak == 0)、かつすべてのcase節(およびdefault節が存在する場合)が実行を終了する場合にのみ、全体が実行を終了すると判断します。
      • その他のステートメントは、デフォルトで実行を終了しないと見なされます。
  3. markbreakおよびmarkbreaklist関数の追加 (src/cmd/gc/typecheck.c):

    • markbreak(Node *n, Node *implicit): ASTノードnを走査し、OBREAKノードを見つけると、そのbreakが属する最も内側のループまたは選択ステートメント(implicit引数で渡される)のhasbreakフラグをセットします。ラベル付きbreakの場合も適切に処理します。
    • markbreaklist(NodeList *l, Node *implicit): ステートメントリストlを走査し、各ステートメントに対してmarkbreakを呼び出します。これにより、ネストされた制御フロー構造内のbreakステートメントが正しく検出され、対応する親ノードのhasbreakフラグが設定されます。
  4. コンパイルフローへのcheckreturnの統合 (src/cmd/gc/lex.c): main関数内で、各関数の型チェックが完了した直後にcheckreturn(l->n);が呼び出されるようになりました。これにより、コンパイルの早い段階で戻り値の要件が検証され、エラーが早期に検出されます。

  5. 古い戻り値チェックロジックの削除 (src/cmd/gc/walk.c): 以前のwalkret関数は、戻り値のチェックが不完全であり、特定の複雑な制御フローを正確に分析できませんでした。新しいcheckreturnisterminating関数がより洗練された分析を提供するため、この古いコードは削除されました。

これらの変更により、Goコンパイラは、関数のすべての実行パスが戻り値要件を満たしていることを、より正確かつ網羅的に検証できるようになりました。これは、Go言語の型安全性と堅牢性を高める上で重要な改善です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goコンパイラのソースコード
  • Yacc/Bisonのドキュメンテーション (構文解析器の理解のため)
  • コンパイラ設計に関する一般的な書籍やオンラインリソース# [インデックス 15584] ファイルの概要

コミット

commit ecab408c4223be3f49d9df52f2900a35bc68f444
Author: Russ Cox <rsc@golang.org>
Date:   Mon Mar 4 17:02:04 2013 -0500

    cmd/gc: implement new return requirements
    
    Fixes #65.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/7441049

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

https://github.com/golang/go/commit/ecab408c4223be3f49d9df52f2900a35bc68f444

元コミット内容

cmd/gc: implement new return requirements

このコミットは、Goコンパイラ(cmd/gc)において、関数の戻り値に関する新しい要件を実装するものです。具体的には、関数が値を返す必要がある場合に、コードパスの終端でreturnステートメントが不足していることを検出する機能を追加しています。これはGo言語のIssue #65を修正するものです。

変更の背景

Go言語では、戻り値を持つ関数は、すべての実行パスにおいて値を返すことが保証されなければなりません。しかし、Goコンパイラの初期の実装では、この要件が十分に厳密にチェックされていませんでした。特に、forループ、switchステートメント、selectステートメントなどの制御フロー構造内で、すべての可能なパスがreturnステートメントで終了しているかどうかの検証が不十分でした。

Issue #65は、この問題点を指摘しており、特定のコードパターンにおいて、コンパイラが「missing return at end of function」(関数の終端に戻り値がありません)というエラーを適切に報告しないケースがあることを示していました。このコミットは、このコンパイラの不備を修正し、Go言語の仕様に準拠した厳密な戻り値チェックを導入することを目的としています。これにより、開発者が意図しないコードパスで値が返されないというバグを未然に防ぐことができます。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびコンパイラの基本的な概念を理解しておく必要があります。

  • Go言語の関数と戻り値: Go言語では、関数は0個以上の戻り値を宣言できます。戻り値が宣言されている場合、関数はすべての可能な実行パスでそれらの値を返す必要があります。
  • 制御フロー(Control Flow): if, for, switch, select, goto, break, continueなどのステートメントは、プログラムの実行順序を制御します。コンパイラは、これらの制御フローを分析し、すべてのコードパスが適切に処理されているかを確認します。
  • Goコンパイラ(cmd/gc): Go言語の公式コンパイラであり、Goソースコードを機械語に変換します。コンパイルプロセスには、字句解析(lexing)、構文解析(parsing)、型チェック(type checking)、中間コード生成(intermediate code generation)、最適化(optimization)、コード生成(code generation)などが含まれます。
  • 抽象構文木(AST: Abstract Syntax Tree): ソースコードの構文構造を木構造で表現したものです。コンパイラは、ソースコードをASTに変換し、そのASTを走査して型チェックやコード生成などの処理を行います。
  • go.h: Goコンパイラの内部で使用されるヘッダーファイルで、ASTノードの構造体定義や、コンパイラ内部のユーティリティ関数のプロトタイプ宣言などが含まれます。
  • go.y: Go言語の文法を定義するYacc(またはBison)の入力ファイルです。構文解析器がソースコードをASTに変換する際のルールが記述されています。
  • lex.c: 字句解析器(lexer)の実装ファイルです。ソースコードをトークンに分割する役割を担います。
  • typecheck.c: 型チェッカーの実装ファイルです。ASTを走査し、Go言語の型システムに従って型の整合性を検証します。
  • walk.c: ASTを走査し、最適化やコード生成の前処理を行うファイルです。
  • y.tab.c: go.yから自動生成される構文解析器のC言語ソースファイルです。

技術的詳細

このコミットの主要な変更点は、Goコンパイラの型チェックフェーズにおいて、関数の戻り値要件をより厳密に検証するためのロジックが追加されたことです。

具体的には、以下の変更が行われています。

  1. Node構造体へのhasbreakフィールドの追加: src/cmd/gc/go.hにおいて、ASTノードを表すNode構造体にuchar hasbreak;というフィールドが追加されました。これは、forswitchselectrangeなどのループや選択ステートメントが、その内部にbreakステートメントを含んでいるかどうかを示すフラグです。この情報は、コードパスが終端に到達するかどうかを判断する際に重要になります。breakが存在する場合、ループや選択ステートメントが途中で終了し、その後のコードパスに影響を与える可能性があるためです。

  2. checkreturn関数の導入とisterminating関数の改善: src/cmd/gc/typecheck.ccheckreturn関数が新しく追加されました。この関数は、関数のASTを受け取り、その関数が戻り値を必要とする場合に、すべての実行パスがreturnステートメントで終了しているかを検証します。 checkreturn関数は、新たに導入されたisterminating関数を利用します。isterminating関数は、与えられたステートメントリストが、その終端に到達する前に必ず実行を終了するか(つまり、returnpanicgoto、または無限ループによって終了するか)を再帰的に判断します。 isterminating関数は、特に以下の制御フロー構造を考慮して改善されています。

    • OFOR (forループ): 条件式がない無限ループ(for {})や、breakステートメントを含まないループは、常に実行を終了すると見なされます。hasbreakフラグがここで利用され、ループがbreakによって途中で終了する可能性がある場合は、終端に到達しないと判断されます。
    • OIF (ifステートメント): ifブロックとelseブロックの両方が実行を終了する場合にのみ、ifステートメント全体が実行を終了すると判断されます。
    • OSWITCH, OTYPESW, OSELECT (switch/selectステートメント): すべてのcase節が実行を終了し、かつdefault節が存在する場合にのみ、switch/selectステートメント全体が実行を終了すると判断されます。ここでもhasbreakフラグが利用され、breakによってswitch/selectが途中で終了する可能性がある場合は、終端に到達しないと判断されます。
    • OGOTO, ORETURN, OPANIC, OXFALL: これらのステートメントは、常に実行を終了すると見なされます。
  3. markbreakおよびmarkbreaklist関数の追加: src/cmd/gc/typecheck.cmarkbreakmarkbreaklistというヘルパー関数が追加されました。これらの関数は、ASTを走査し、OBREAKノードを見つけると、そのbreakが属する最も内側のループまたは選択ステートメントのhasbreakフラグをセットします。これにより、isterminating関数が正確な判断を下せるようになります。

  4. コンパイルフローへのcheckreturnの統合: src/cmd/gc/lex.cmain関数内で、各関数の型チェックが完了した後(typechecklistの呼び出し後)に、checkreturn(l->n);が呼び出されるようになりました。これにより、すべての関数に対して戻り値の要件チェックが実行されるようになります。

  5. 古い戻り値チェックロジックの削除: src/cmd/gc/walk.cから、以前の不完全な戻り値チェックロジックであるwalkret関数とその関連コードが削除されました。これは、新しいcheckreturn関数とisterminating関数がより正確で包括的なチェックを提供するようになったためです。

  6. go.yy.tab.cの変更: go.yファイルには、構文解析器がASTを構築する際のルールに関する微調整が含まれています。特に、compound_stmt(複合ステートメント、つまりブロック)の処理において、空のブロックの場合にnod(OEMPTY, N, N)を返すように変更されています。これは、ASTの構造をより一貫性のあるものにするための変更であり、戻り値チェックのロジックと間接的に関連しています。y.tab.cgo.yから自動生成されるファイルであるため、go.yの変更に伴って更新されています。

  7. テストケースの追加と修正: test/return.goに、新しい戻り値チェックロジックを検証するための広範なテストケースが追加されました。これには、様々な制御フロー構造(for, switch, select, if)とreturnステートメントの組み合わせが含まれており、コンパイラが正しいエラーを報告するか、またはエラーを報告しないかを検証します。 test/fixedbugs/bug086.gotest/fixedbugs/issue4663.goも、この変更に関連して修正されています。

これらの変更により、Goコンパイラは、関数の戻り値要件に関してより堅牢なチェックを行うようになり、Goプログラムの信頼性と安全性が向上しました。

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

src/cmd/gc/go.h

--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -268,6 +268,7 @@ struct	Node
 	uchar	addrtaken;	// address taken, even if not moved to heap
 	uchar	dupok;	// duplicate definitions ok (for func)
 	schar	likely; // likeliness of if statement
+	uchar	hasbreak;	// has break statement
 
 	// most nodes
 	Type*	type;
@@ -1363,6 +1364,7 @@ Node*	typecheck(Node **np, int top);
 void	typechecklist(NodeList *l, int top);
 Node*	typecheckdef(Node *n);
 void	copytype(Node *n, Type *t);
+void	checkreturn(Node*);
 void	queuemethod(Node *n);

src/cmd/gc/lex.c

--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -376,6 +376,7 @@ main(int argc, char *argv[])
 			curfn = l->n;
 			saveerrors();
 			typechecklist(l->n->nbody, Etop);
+			checkreturn(l->n);
 			if(nerrors != 0)
 				l->n->nbody = nil;  // type errors; do not compile
 		}

src/cmd/gc/typecheck.c

--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -3144,3 +3144,148 @@ checkmake(Type *t, char *arg, Node *n)
 	}
 	return 0;
 }
+
+static void	markbreaklist(NodeList*, Node*);
+
+static void
+markbreak(Node *n, Node *implicit)
+{
+	Label *lab;
+
+	if(n == N)
+		return;
+
+	switch(n->op) {
+	case OBREAK:
+		if(n->left == N) {
+			if(implicit)
+				implicit->hasbreak = 1;
+		} else {
+			lab = n->left->sym->label;
+			if(lab != L)
+				lab->def->hasbreak = 1;
+		}
+		break;
+	
+	case OFOR:
+	case OSWITCH:
+	case OTYPESW:
+	case OSELECT:
+	case ORANGE:
+		implicit = n;
+		// fall through
+	
+	default:
+		markbreak(n->left, implicit);
+		markbreak(n->right, implicit);
+		markbreak(n->ntest, implicit);
+		markbreak(n->nincr, implicit);
+		markbreaklist(n->ninit, implicit);
+		markbreaklist(n->nbody, implicit);
+		markbreaklist(n->nelse, implicit);
+		markbreaklist(n->list, implicit);
+		markbreaklist(n->rlist, implicit);
+		break;
+	}
+}
+
+static void
+markbreaklist(NodeList *l, Node *implicit)
+{
+	Node *n;
+	Label *lab;
+
+	for(; l; l=l->next) {
+		n = l->n;
+		if(n->op == OLABEL && l->next && n->defn == l->next->n) {
+			switch(n->defn->op) {
+			case OFOR:
+			case OSWITCH:
+			case OTYPESW:
+			case OSELECT:
+			case ORANGE:
+				lab = mal(sizeof *lab);
+				lab->def = n->defn;
+				n->left->sym->label = lab;
+				markbreak(n->defn, n->defn);
+				n->left->sym->label = L;
+				l = l->next;
+				continue;
+			}
+		}
+		markbreak(n, implicit);
+	}
+}
+
+static int
+isterminating(NodeList *l, int top)
+{
+	int def;
+	Node *n;
+
+	if(l == nil)
+		return 0;
+	if(top) {
+		while(l->next && l->n->op != OLABEL)
+			l = l->next;
+		markbreaklist(l, nil);
+	}
+	while(l->next)
+		l = l->next;
+	n = l->n;
+
+	if(n == N)
+		return 0;
+
+	switch(n->op) {
+	// NOTE: OLABEL is treated as a separate statement,
+	// not a separate prefix, so skipping to the last statement
+	// in the block handles the labeled statement case by
+	// skipping over the label. No case OLABEL here.
+
+	case OBLOCK:
+		return isterminating(n->list, 0);
+
+	case OGOTO:
+	case ORETURN:
+	case OPANIC:
+	case OXFALL:
+		return 1;
+
+	case OFOR:
+		if(n->ntest != N)
+			return 0;
+		if(n->hasbreak)
+			return 0;
+		return 1;
+
+	case OIF:
+		return isterminating(n->nbody, 0) && isterminating(n->nelse, 0);
+
+	case OSWITCH:
+	case OTYPESW:
+	case OSELECT:
+		if(n->hasbreak)
+			return 0;
+		def = 0;
+		for(l=n->list; l; l=l->next) {
+			if(!isterminating(l->n->nbody, 0))
+				return 0;
+			if(l->n->list == nil) // default
+				def = 1;
+		}
+		if(n->op != OSELECT && !def)
+			return 0;
+		return 1;
+	}
+	
+	return 0;
+}
+
+void
+checkreturn(Node *fn)
+{
+	if(fn->type->outtuple && fn->nbody != nil)
+		if(!isterminating(fn->nbody, 1))
+			yyerrorl(fn->endlineno, "missing return at end of function");
+}

src/cmd/gc/walk.c

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -29,40 +29,6 @@ static	void	walkdiv(Node**, NodeList**);
 static	int	bounded(Node*, int64);
 static	Mpint	mpzero;
 
-// can this code branch reach the end
-// without an unconditional RETURN
-// this is hard, so it is conservative
-static int
-walkret(NodeList *l)
-{
-	Node *n;
-
-loop:
-	while(l && l->next)
-		l = l->next;
-	if(l == nil)
-		return 1;
-
-	// at this point, we have the last
-	// statement of the function
-	n = l->n;
-	switch(n->op) {
-	case OBLOCK:
-		l = n->list;
-		goto loop;
-
-	case OGOTO:
-	case ORETURN:
-	case OPANIC:
-		return 0;
-		break;
-	}
-
-	// all other statements
-	// will flow to the end
-	return 1;
-}
-
 void
 walk(Node *fn)
 {
@@ -76,9 +42,6 @@ walk(Node *fn)
 		snprint(s, sizeof(s), "\nbefore %S", curfn->nname->sym);
 		dumplist(s, curfn->nbody);
 	}
-	if(curfn->type->outtuple)
-		if(walkret(curfn->nbody))
-			yyerror("function ends without a return statement");
 
 	lno = lineno;

コアとなるコードの解説

このコミットの核心は、Goコンパイラが関数の戻り値要件を厳密にチェックするための新しいメカニズムを導入したことです。

  1. Node構造体へのhasbreakフィールドの追加 (src/cmd/gc/go.h): これは、ASTノードがbreakステートメントを含むかどうかを追跡するための単純なフラグです。forswitchselectなどの制御フロー構造は、breakによって途中で終了する可能性があります。このフラグは、そのような制御フローが「終端に到達する」かどうかを正確に判断するために不可欠です。

  2. checkreturn関数の導入とisterminating関数の改善 (src/cmd/gc/typecheck.c):

    • checkreturn(Node *fn): この関数は、Goコンパイラの型チェックフェーズで呼び出されます。引数として関数のASTノードを受け取ります。もし関数が戻り値を宣言しており(fn->type->outtupleが真)、かつ関数本体が存在する場合(fn->nbody != nil)、isterminating関数を呼び出して、関数本体のすべての実行パスがreturnステートメントで終了しているかをチェックします。もし終了していない場合、yyerrorlを使って「missing return at end of function」というエラーを報告します。
    • isterminating(NodeList *l, int top): この関数は、与えられたステートメントリスト(NodeList)が、その終端に到達する前に必ず実行を終了するかどうかを判断する再帰的な関数です。
      • top引数は、関数本体のトップレベルのステートメントリストを処理しているかどうかを示します。トップレベルの場合、ラベル付きステートメントを適切に処理するために、リストの最後のステートメントに移動する前にmarkbreaklistを呼び出します。
      • 関数は、ステートメントリストの最後のステートメントを検査します。
      • OGOTO, ORETURN, OPANIC, OXFALLfallthrough)のようなステートメントは、それ自体が実行を終了させるため、1(終了する)を返します。
      • OBLOCK(ブロック)の場合、そのブロック内のステートメントリストに対して再帰的にisterminatingを呼び出します。
      • OFOR(forループ)の場合、条件式がない無限ループ(n->ntest == N)で、かつbreakステートメントを含まない(n->hasbreak == 0)場合にのみ、ループが実行を終了すると判断します。
      • OIF(ifステートメント)の場合、ifブロックとelseブロックの両方が実行を終了する場合にのみ、ifステートメント全体が実行を終了すると判断します。
      • OSWITCH, OTYPESW, OSELECT(switch/selectステートメント)の場合、breakステートメントを含まず(n->hasbreak == 0)、かつすべてのcase節(およびdefault節が存在する場合)が実行を終了する場合にのみ、全体が実行を終了すると判断します。
      • その他のステートメントは、デフォルトで実行を終了しないと見なされます。
  3. markbreakおよびmarkbreaklist関数の追加 (src/cmd/gc/typecheck.c):

    • markbreak(Node *n, Node *implicit): ASTノードnを走査し、OBREAKノードを見つけると、そのbreakが属する最も内側のループまたは選択ステートメント(implicit引数で渡される)のhasbreakフラグをセットします。ラベル付きbreakの場合も適切に処理します。
    • markbreaklist(NodeList *l, Node *implicit): ステートメントリストlを走査し、各ステートメントに対してmarkbreakを呼び出します。これにより、ネストされた制御フロー構造内のbreakステートメントが正しく検出され、対応する親ノードのhasbreakフラグが設定されます。
  4. コンパイルフローへのcheckreturnの統合 (src/cmd/gc/lex.c): main関数内で、各関数の型チェックが完了した直後にcheckreturn(l->n);が呼び出されるようになりました。これにより、コンパイルの早い段階で戻り値の要件が検証され、エラーが早期に検出されます。

  5. 古い戻り値チェックロジックの削除 (src/cmd/gc/walk.c): 以前のwalkret関数は、戻り値のチェックが不完全であり、特定の複雑な制御フローを正確に分析できませんでした。新しいcheckreturnisterminating関数がより洗練された分析を提供するため、この古いコードは削除されました。

これらの変更により、Goコンパイラは、関数のすべての実行パスが戻り値要件を満たしていることを、より正確かつ網羅的に検証できるようになりました。これは、Go言語の型安全性と堅牢性を高める上で重要な改善です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goコンパイラのソースコード
  • Yacc/Bisonのドキュメンテーション (構文解析器の理解のため)
  • コンパイラ設計に関する一般的な書籍やオンラインリソース