[インデックス 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コンパイラの型チェックフェーズにおいて、関数の戻り値要件をより厳密に検証するためのロジックが追加されたことです。
具体的には、以下の変更が行われています。
-
Node
構造体へのhasbreak
フィールドの追加:src/cmd/gc/go.h
において、ASTノードを表すNode
構造体にuchar hasbreak;
というフィールドが追加されました。これは、for
、switch
、select
、range
などのループや選択ステートメントが、その内部にbreak
ステートメントを含んでいるかどうかを示すフラグです。この情報は、コードパスが終端に到達するかどうかを判断する際に重要になります。break
が存在する場合、ループや選択ステートメントが途中で終了し、その後のコードパスに影響を与える可能性があるためです。 -
checkreturn
関数の導入とisterminating
関数の改善:src/cmd/gc/typecheck.c
にcheckreturn
関数が新しく追加されました。この関数は、関数のASTを受け取り、その関数が戻り値を必要とする場合に、すべての実行パスがreturn
ステートメントで終了しているかを検証します。checkreturn
関数は、新たに導入されたisterminating
関数を利用します。isterminating
関数は、与えられたステートメントリストが、その終端に到達する前に必ず実行を終了するか(つまり、return
、panic
、goto
、または無限ループによって終了するか)を再帰的に判断します。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
: これらのステートメントは、常に実行を終了すると見なされます。
-
markbreak
およびmarkbreaklist
関数の追加:src/cmd/gc/typecheck.c
にmarkbreak
とmarkbreaklist
というヘルパー関数が追加されました。これらの関数は、ASTを走査し、OBREAK
ノードを見つけると、そのbreak
が属する最も内側のループまたは選択ステートメントのhasbreak
フラグをセットします。これにより、isterminating
関数が正確な判断を下せるようになります。 -
コンパイルフローへの
checkreturn
の統合:src/cmd/gc/lex.c
のmain
関数内で、各関数の型チェックが完了した後(typechecklist
の呼び出し後)に、checkreturn(l->n);
が呼び出されるようになりました。これにより、すべての関数に対して戻り値の要件チェックが実行されるようになります。 -
古い戻り値チェックロジックの削除:
src/cmd/gc/walk.c
から、以前の不完全な戻り値チェックロジックであるwalkret
関数とその関連コードが削除されました。これは、新しいcheckreturn
関数とisterminating
関数がより正確で包括的なチェックを提供するようになったためです。 -
go.y
とy.tab.c
の変更:go.y
ファイルには、構文解析器がASTを構築する際のルールに関する微調整が含まれています。特に、compound_stmt
(複合ステートメント、つまりブロック)の処理において、空のブロックの場合にnod(OEMPTY, N, N)
を返すように変更されています。これは、ASTの構造をより一貫性のあるものにするための変更であり、戻り値チェックのロジックと間接的に関連しています。y.tab.c
はgo.y
から自動生成されるファイルであるため、go.y
の変更に伴って更新されています。 -
テストケースの追加と修正:
test/return.go
に、新しい戻り値チェックロジックを検証するための広範なテストケースが追加されました。これには、様々な制御フロー構造(for
,switch
,select
,if
)とreturn
ステートメントの組み合わせが含まれており、コンパイラが正しいエラーを報告するか、またはエラーを報告しないかを検証します。test/fixedbugs/bug086.go
とtest/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コンパイラが関数の戻り値要件を厳密にチェックするための新しいメカニズムを導入したことです。
-
Node
構造体へのhasbreak
フィールドの追加 (src/cmd/gc/go.h
): これは、ASTノードがbreak
ステートメントを含むかどうかを追跡するための単純なフラグです。for
、switch
、select
などの制御フロー構造は、break
によって途中で終了する可能性があります。このフラグは、そのような制御フローが「終端に到達する」かどうかを正確に判断するために不可欠です。 -
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
,OXFALL
(fallthrough
)のようなステートメントは、それ自体が実行を終了させるため、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
節が存在する場合)が実行を終了する場合にのみ、全体が実行を終了すると判断します。- その他のステートメントは、デフォルトで実行を終了しないと見なされます。
-
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
フラグが設定されます。
-
コンパイルフローへの
checkreturn
の統合 (src/cmd/gc/lex.c
):main
関数内で、各関数の型チェックが完了した直後にcheckreturn(l->n);
が呼び出されるようになりました。これにより、コンパイルの早い段階で戻り値の要件が検証され、エラーが早期に検出されます。 -
古い戻り値チェックロジックの削除 (
src/cmd/gc/walk.c
): 以前のwalkret
関数は、戻り値のチェックが不完全であり、特定の複雑な制御フローを正確に分析できませんでした。新しいcheckreturn
とisterminating
関数がより洗練された分析を提供するため、この古いコードは削除されました。
これらの変更により、Goコンパイラは、関数のすべての実行パスが戻り値要件を満たしていることを、より正確かつ網羅的に検証できるようになりました。これは、Go言語の型安全性と堅牢性を高める上で重要な改善です。
関連リンク
- Go Issue #65: https://github.com/golang/go/issues/65
- Go CL 7441049: https://golang.org/cl/7441049
参考にした情報源リンク
- 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コンパイラの型チェックフェーズにおいて、関数の戻り値要件をより厳密に検証するためのロジックが追加されたことです。
具体的には、以下の変更が行われています。
-
Node
構造体へのhasbreak
フィールドの追加:src/cmd/gc/go.h
において、ASTノードを表すNode
構造体にuchar hasbreak;
というフィールドが追加されました。これは、for
、switch
、select
、range
などのループや選択ステートメントが、その内部にbreak
ステートメントを含んでいるかどうかを示すフラグです。この情報は、コードパスが終端に到達するかどうかを判断する際に重要になります。break
が存在する場合、ループや選択ステートメントが途中で終了し、その後のコードパスに影響を与える可能性があるためです。 -
checkreturn
関数の導入とisterminating
関数の改善:src/cmd/gc/typecheck.c
にcheckreturn
関数が新しく追加されました。この関数は、関数のASTを受け取り、その関数が戻り値を必要とする場合に、すべての実行パスがreturn
ステートメントで終了しているかを検証します。checkreturn
関数は、新たに導入されたisterminating
関数を利用します。isterminating
関数は、与えられたステートメントリストが、その終端に到達する前に必ず実行を終了するか(つまり、return
、panic
、goto
、または無限ループによって終了するか)を再帰的に判断します。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
: これらのステートメントは、常に実行を終了すると見なされます。
-
markbreak
およびmarkbreaklist
関数の追加:src/cmd/gc/typecheck.c
にmarkbreak
とmarkbreaklist
というヘルパー関数が追加されました。これらの関数は、ASTを走査し、OBREAK
ノードを見つけると、そのbreak
が属する最も内側のループまたは選択ステートメントのhasbreak
フラグをセットします。これにより、isterminating
関数が正確な判断を下せるようになります。 -
コンパイルフローへの
checkreturn
の統合:src/cmd/gc/lex.c
のmain
関数内で、各関数の型チェックが完了した後(typechecklist
の呼び出し後)に、checkreturn(l->n);
が呼び出されるようになりました。これにより、すべての関数に対して戻り値の要件チェックが実行されるようになります。 -
古い戻り値チェックロジックの削除:
src/cmd/gc/walk.c
から、以前の不完全な戻り値チェックロジックであるwalkret
関数とその関連コードが削除されました。これは、新しいcheckreturn
関数とisterminating
関数がより正確で包括的なチェックを提供するようになったためです。 -
go.y
とy.tab.c
の変更:go.y
ファイルには、構文解析器がASTを構築する際のルールに関する微調整が含まれています。特に、compound_stmt
(複合ステートメント、つまりブロック)の処理において、空のブロックの場合にnod(OEMPTY, N, N)
を返すように変更されています。これは、ASTの構造をより一貫性のあるものにするための変更であり、戻り値チェックのロジックと間接的に関連しています。y.tab.c
はgo.y
から自動生成されるファイルであるため、go.y
の変更に伴って更新されています。 -
テストケースの追加と修正:
test/return.go
に、新しい戻り値チェックロジックを検証するための広範なテストケースが追加されました。これには、様々な制御フロー構造(for
,switch
,select
,if
)とreturn
ステートメントの組み合わせが含まれており、コンパイラが正しいエラーを報告するか、またはエラーを報告しないかを検証します。test/fixedbugs/bug086.go
とtest/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コンパイラが関数の戻り値要件を厳密にチェックするための新しいメカニズムを導入したことです。
-
Node
構造体へのhasbreak
フィールドの追加 (src/cmd/gc/go.h
): これは、ASTノードがbreak
ステートメントを含むかどうかを追跡するための単純なフラグです。for
、switch
、select
などの制御フロー構造は、break
によって途中で終了する可能性があります。このフラグは、そのような制御フローが「終端に到達する」かどうかを正確に判断するために不可欠です。 -
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
,OXFALL
(fallthrough
)のようなステートメントは、それ自体が実行を終了させるため、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
節が存在する場合)が実行を終了する場合にのみ、全体が実行を終了すると判断します。- その他のステートメントは、デフォルトで実行を終了しないと見なされます。
-
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
フラグが設定されます。
-
コンパイルフローへの
checkreturn
の統合 (src/cmd/gc/lex.c
):main
関数内で、各関数の型チェックが完了した直後にcheckreturn(l->n);
が呼び出されるようになりました。これにより、コンパイルの早い段階で戻り値の要件が検証され、エラーが早期に検出されます。 -
古い戻り値チェックロジックの削除 (
src/cmd/gc/walk.c
): 以前のwalkret
関数は、戻り値のチェックが不完全であり、特定の複雑な制御フローを正確に分析できませんでした。新しいcheckreturn
とisterminating
関数がより洗練された分析を提供するため、この古いコードは削除されました。
これらの変更により、Goコンパイラは、関数のすべての実行パスが戻り値要件を満たしていることを、より正確かつ網羅的に検証できるようになりました。これは、Go言語の型安全性と堅牢性を高める上で重要な改善です。
関連リンク
- Go Issue #65: https://github.com/golang/go/issues/65
- Go CL 7441049: https://golang.org/cl/7441049
参考にした情報源リンク
- Go言語の公式ドキュメント
- Goコンパイラのソースコード
- Yacc/Bisonのドキュメンテーション (構文解析器の理解のため)
- コンパイラ設計に関する一般的な書籍やオンラインリソース