[インデックス 11807] ファイルの概要
このコミットは、Goコンパイラの型チェックフェーズにおける循環参照エラーの診断メッセージを改善するものです。具体的には、型チェックのループが検出された際に、そのループに関与しているノードの詳細な情報を出力するように変更されています。これにより、開発者が型チェックの循環エラーの原因を特定しやすくなります。
コミット
commit 2aafad25b4fe6c8e0d7f75b71b4b6d6e238483d6
Author: Russ Cox <rsc@golang.org>
Date: Sat Feb 11 01:04:33 2012 -0500
gc: print detail for typechecking loop error
R=ken2
CC=golang-dev
https://golang.org/cl/5654060
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2aafad25b4fe6c8e0d7f75b71b4b6d6e238483d6
元コミット内容
gc: print detail for typechecking loop error
R=ken2
CC=golang-dev
https://golang.org/cl/5654060
変更の背景
Goコンパイラは、プログラムの型が正しく定義され、使用されているかを検証するために「型チェック」というプロセスを実行します。この型チェックの過程で、型定義が互いに参照し合うような循環(ループ)が発生することがあります。例えば、型Aが型Bを参照し、型Bが型Aを参照するようなケースです。このような循環は、コンパイラが型のサイズや構造を決定する上で問題を引き起こす可能性があります。
以前のコンパイラでは、このような型チェックの循環が検出された際に、単に「typechecking loop involving %N」という一般的なエラーメッセージが表示されるだけでした。このメッセージだけでは、どの型や式が循環の原因となっているのか、その詳細なパスが不明瞭であり、開発者が問題をデバッグする上で困難を伴いました。
このコミットの背景には、より詳細なエラー情報を提供することで、開発者のデバッグ体験を向上させ、コンパイラが報告するエラーメッセージの有用性を高めるという目的があります。特に、循環のパスを明示することで、問題の根本原因を迅速に特定できるようにすることが狙いです。
前提知識の解説
Goコンパイラ (gc)
gcは、Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担っています。Goコンパイラは、複数のフェーズを経てコンパイルを行います。主要なフェーズには、字句解析、構文解析、型チェック、最適化、コード生成などがあります。
型チェック (Type Checking)
型チェックは、コンパイラの重要なフェーズの一つで、プログラムが型システムの一貫性規則に従っていることを検証します。具体的には、以下のようなことを確認します。
- 型の互換性: 演算子のオペランドや関数の引数と戻り値の型が互換性があるか。
- 型推論: 変数宣言などで型が明示されていない場合に、初期値から型を推論する。
- 循環参照の検出: 型定義や式の評価において、無限ループに陥る可能性のある循環参照がないか。
Go言語の型システムは静的型付けであり、コンパイル時に厳密な型チェックが行われます。これにより、多くの型関連のエラーが実行時ではなくコンパイル時に検出され、プログラムの信頼性が向上します。
抽象構文木 (AST: Abstract Syntax Tree)
コンパイラは、ソースコードを解析して抽象構文木(AST)と呼ばれるツリー構造を構築します。ASTは、プログラムの構造を抽象的に表現したもので、型チェックやコード生成などの後続のフェーズで利用されます。Goコンパイラでは、Node構造体がASTの各ノードを表します。
Node構造体
Goコンパイラの内部では、Node構造体がASTの各要素(変数、定数、関数呼び出し、演算子など)を表します。各Nodeは、その種類(opフィールド)、型(typeフィールド)、シンボル(symフィールド)、そして型チェックの状態(typecheckフィールド)などの情報を持っています。
n->typecheck: このフィールドは、ノードの型チェックの状態を示します。0: 未チェック1: 型チェック済み2: 型チェック中(循環検出に使用)
NodeList構造体
NodeListは、Nodeのリンクリストを構成するための構造体です。型チェックの循環を検出する際に、現在型チェック中のノードのスタックを管理するために使用されます。
Fmt構造体とfmtstrinit/fmtprint/fmtstrflush
これらは、Goコンパイラ内部で使用されるフォーマット済み文字列を構築するためのユーティリティです。Fmt構造体は、文字列のバッファリングとフォーマットを管理し、fmtstrinitで初期化、fmtprintで文字列を追加、fmtstrflushで最終的な文字列を取得します。これは、C言語のsprintfに似た機能を提供しますが、コンパイラの内部で効率的に文字列を構築するために最適化されています。
yyerrorとfatal
yyerror: コンパイラがエラーを報告するために使用する関数です。通常、エラーメッセージを標準エラー出力に表示し、コンパイルプロセスを継続しようとします(ただし、エラーが多すぎると停止することもあります)。fatal: 回復不可能な致命的なエラーが発生した場合に呼び出される関数です。通常、エラーメッセージを表示した後にプログラムを終了します。
技術的詳細
このコミットの主要な変更点は、src/cmd/gc/typecheck.cファイル内のtypecheck関数と、新しく導入されたtypecheck1関数のロジックにあります。
型チェックの循環検出メカニズム
Goコンパイラの型チェックは、再帰的に行われることがあります。例えば、ある式の型をチェックする際に、その式が参照する別の式の型をチェックする必要が生じ、それがさらに別の式を参照するという具合です。この再帰的な処理中に、既に型チェック中のノードに再び遭遇した場合、それは循環参照を示します。
typecheck関数は、ノードのtypecheckフィールドを利用して循環を検出します。
- ノードの型チェックを開始する前に、
n->typecheckを2に設定します。これは「型チェック中」の状態を示します。 - もし
typecheck関数が、既にtypecheck == 2となっているノードに遭遇した場合、それは循環が検出されたことを意味します。
変更前のエラー報告
変更前は、循環が検出されると以下のシンプルなエラーメッセージが出力されていました。
yyerror("typechecking loop involving %N", n);
ここで%Nは、循環が検出されたノードを表します。しかし、このメッセージだけでは、循環に至るまでのパスが不明であり、デバッグが困難でした。
変更後のエラー報告の改善
このコミットでは、循環が検出された際に、その循環に関与するノードのスタックトレースのような情報を出力するように改善されました。
-
typecheck1関数の導入: 既存のtypecheck関数から実際の型チェックロジックがtypecheck1という新しい静的関数に分離されました。これにより、typecheck関数は主に循環検出とスタック管理のラッパーとして機能するようになりました。 -
型チェックスタックの管理:
tcstackというNodeListのリンクリストが導入され、現在型チェック中のノードがこのスタックにプッシュされます。typecheck関数が呼び出されるたびに、現在のノードnがtcstackの先頭に追加されます。typecheck1による型チェックが完了すると、対応するノードがtcstackからポップされます。tcfreeというリンクリストは、NodeListのメモリを再利用するためのフリーリストとして機能します。これにより、頻繁なメモリ割り当てと解放を避けることができます。
-
詳細なエラーメッセージの生成:
- 循環が検出され(
n->typecheck == 2)、かつまだエラーが報告されていない場合(nsavederrors+nerrors == 0)、tcstackを逆順に辿り、循環に関与するすべてのノードとその行番号をFmt構造体を使ってフォーマットします。 - このフォーマットされた詳細情報が、
yyerrorメッセージに追加されます。
if(nsavederrors+nerrors == 0) { fmtstrinit(&fmt); for(l=tcstack; l; l=l->next) fmtprint(&fmt, "\\n\\t%L %N", l->n->lineno, l->n); yyerror("typechecking loop involving %N%s", n, fmtstrflush(&fmt)); }ここで、
%Lは行番号、%Nはノードを表します。これにより、エラーメッセージは以下のような形式になります。typechecking loop involving <node_N> <line_X> <node_A> <line_Y> <node_B> <line_Z> <node_C>これは、
<node_C>が<node_B>を参照し、<node_B>が<node_A>を参照し、最終的に<node_A>が<node_N>(または<node_N>が<node_A>)を参照して循環を形成していることを示します。 - 循環が検出され(
-
アサーションの追加:
typecheck関数内で、tcstackが期待通りに管理されているかを確認するためのfatalアサーションが追加されました。if(tcstack != l) fatal("typecheck stack out of sync");このアサーションは、スタックの不整合が発生した場合にコンパイラをクラッシュさせ、開発中にバグを早期に発見するのに役立ちます。
これらの変更により、型チェックの循環エラーが発生した際に、開発者は循環のパスを明確に把握できるようになり、デバッグの効率が大幅に向上します。
コアとなるコードの変更箇所
src/cmd/gc/typecheck.c ファイルにおける変更の差分は以下の通りです。
--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -111,16 +111,15 @@ typekind(Type *t)
* replaces *np with a new pointer in some cases.
* returns the final value of *np as a convenience.
*/
+static void typecheck1(Node **, int);
Node*
typecheck(Node **np, int top)
{
-\tint et, aop, op, ptr;\n-\tNode *n, *l, *r;\n-\tNodeList *args;\n-\tint lno, ok, ntop;\n-\tType *t, *tp, *ft, *missing, *have, *badtype;\n-\tVal v;\n-\tchar *why;\n+\tNode *n;\n+\tint lno;\n+\tFmt fmt;\n+\tNodeList *l;\n+\tstatic NodeList *tcstack, *tcfree;\
\n \t// cannot type check until all the source has been parsed
\tif(!typecheckok)
@@ -157,11 +156,52 @@ typecheck(Node **np, int top)
\t}\
\n \tif(n->typecheck == 2) {\
-\t\tyyerror(\"typechecking loop involving %N\", n);\n+\t\tif(nsavederrors+nerrors == 0) {\
+\t\t\tfmtstrinit(&fmt);\
+\t\t\tfor(l=tcstack; l; l=l->next)\
+\t\t\t\tfmtprint(&fmt, \"\\n\\t%L %N\", l->n->lineno, l->n);\
+\t\t\tyyerror(\"typechecking loop involving %N%s\", n, fmtstrflush(&fmt));\
+\t\t}\
\t\tlineno = lno;\
\t\treturn n;\
\t}\
\tn->typecheck = 2;\
+\t\n+\tif(tcfree != nil) {\
+\t\tl = tcfree;\
+\t\ttcfree = l->next;\
+\t} else\
+\t\tl = mal(sizeof *l);\
+\tl->next = tcstack;\
+\tl->n = n;\
+\ttcstack = l;\
+\n+\ttypecheck1(&n, top);\
+\t*np = n;\
+\tn->typecheck = 1;\
+\n+\tif(tcstack != l)\
+\t\tfatal(\"typecheck stack out of sync\");\
+\ttcstack = l->next;\
+\tl->next = tcfree;\
+\ttcfree = l;\
+\n+\tlineno = lno;\
+\treturn n;\
+}\
+\n+static void\n+typecheck1(Node **np, int top)\
+{\
+\tint et, aop, op, ptr;\
+\tNode *n, *l, *r;\
+\tNodeList *args;\
+\tint ok, ntop;\
+\tType *t, *tp, *ft, *missing, *have, *badtype;\
+\tVal v;\
+\tchar *why;\
+\t\n+\tn = *np;\
\n \tif(n->sym) {\
\t\tif(n->op == ONAME && n->etype != 0 && !(top & Ecall)) {\
@@ -1484,10 +1524,7 @@ error:\
\tn->type = T;\
\n out:\
-\tlineno = lno;\n-\tn->typecheck = 1;\
\t*np = n;\
-\treturn n;\
}\
\n static void
コアとなるコードの解説
typecheck関数の変更
- 変数宣言の変更:
typecheck関数から、型チェックの具体的なロジックで使用される多くのローカル変数(et,aop,op,ptr,r,args,ok,ntop,t,tp,ft,missing,have,badtype,v,why)が削除されました。これらの変数は新しく導入されたtypecheck1関数に移動されました。 - 新しいローカル変数の追加:
Fmt fmt;、NodeList *l;、static NodeList *tcstack, *tcfree;が追加されました。fmt: エラーメッセージのフォーマットに使用されるFmt構造体。l:NodeListの要素を一時的に保持するためのポインタ。tcstack: 型チェック中のノードを追跡するためのスタック(リンクリスト)。staticキーワードにより、関数呼び出し間で状態が保持されます。tcfree:NodeListの要素を再利用するためのフリーリスト。staticキーワードにより、関数呼び出し間で状態が保持されます。
- 循環検出時のエラーメッセージの改善:
if(n->typecheck == 2)ブロック内で、以前はyyerror("typechecking loop involving %N", n);とだけ出力されていましたが、より詳細な情報が出力されるようになりました。if(nsavederrors+nerrors == 0)という条件が追加されています。これは、まだエラーが報告されていない場合にのみ詳細なループ情報を出力するためのものです。これにより、同じ循環エラーが複数回報告されるのを防ぎます。fmtstrinit(&fmt);でFmt構造体を初期化し、for(l=tcstack; l; l=l->next)ループでtcstackを逆順に辿ります。fmtprint(&fmt, "\\n\\t%L %N", l->n->lineno, l->n);で、スタック上の各ノードの行番号とノード自体をフォーマットしてfmtに追加します。\n\tは、新しい行とインデントを追加して、スタックトレースのような表示にするためのものです。- 最終的に
yyerror("typechecking loop involving %N%s", n, fmtstrflush(&fmt));で、循環が検出されたノードと、fmtstrflush(&fmt)で取得した詳細なスタックトレース情報を結合してエラーメッセージとして出力します。
- 型チェックスタックの管理ロジックの追加:
n->typecheck = 2;の後に、現在のノードnをtcstackにプッシュするロジックが追加されました。tcfreeリストからNodeList要素を再利用するか、新しくmal(メモリ割り当て)で確保します。l->next = tcstack;とtcstack = l;で、lをtcstackの先頭に挿入します。
typecheck1の呼び出し: 実際の型チェックロジックはtypecheck1(&n, top);として呼び出されます。- スタックのポップとクリーンアップ:
n->typecheck = 1;でノードの型チェックが完了したことをマークします。if(tcstack != l) fatal("typecheck stack out of sync");は、スタックの整合性をチェックするアサーションです。もしスタックが期待通りにポップされていない場合、致命的なエラーとしてコンパイラを終了させます。tcstack = l->next;でスタックから現在のノードをポップします。l->next = tcfree;とtcfree = l;で、ポップしたNodeList要素をtcfreeリストに戻し、メモリを再利用できるようにします。
linenoとreturn n;の移動:lineno = lno;とreturn n;は、typecheck関数の末尾に移動され、typecheck1の呼び出し後に行われるようになりました。
typecheck1関数の導入
static void typecheck1(Node **, int);として新しい静的関数が宣言されました。typecheck関数から移動された型チェックの具体的なロジックがこの関数内に配置されました。n = *np;で、引数として渡されたノードポインタの参照を解除してnに代入します。- 以前
typecheck関数の末尾にあったlineno = lno;、n->typecheck = 1;、return n;の行は、typecheck1からは削除されました。これらの処理はtypecheck関数がtypecheck1を呼び出した後に責任を持って行います。
これらの変更により、typecheck関数は型チェックの循環検出とスタック管理の役割に特化し、実際の型チェックロジックはtypecheck1に委譲されることで、コードの関心事が分離され、可読性と保守性が向上しています。また、詳細なエラーメッセージの生成により、デバッグの効率が大幅に改善されました。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- このコミットが属するGoの変更リスト (CL): https://golang.org/cl/5654060
参考にした情報源リンク
- Go言語のコンパイラに関するドキュメントやソースコード(
src/cmd/gc/ディレクトリ内のファイル) - Go言語の型システムに関する公式ドキュメント
- 抽象構文木 (AST) に関する一般的なコンパイラ理論の資料
- Goコンパイラの内部構造に関するブログ記事や解説(例: "Go's Type System" by Russ Cox, "A Tour of the Go Compiler")
yyerrorやfatalといったコンパイラ内部のユーティリティ関数の一般的な役割に関する情報Fmt構造体や関連するフォーマット関数のGoコンパイラ内での使用例- Go言語のコンパイラ開発に関するメーリングリスト(golang-dev)の議論
[インデックス 11807] ファイルの概要
このコミットは、Goコンパイラの型チェックフェーズにおける循環参照エラーの診断メッセージを改善するものです。具体的には、型チェックのループが検出された際に、そのループに関与しているノードの詳細な情報を出力するように変更されています。これにより、開発者が型チェックの循環エラーの原因を特定しやすくなります。
コミット
commit 2aafad25b4fe6c8e0d7f75b71b4b6d6e238483d6
Author: Russ Cox <rsc@golang.org>
Date: Sat Feb 11 01:04:33 2012 -0500
gc: print detail for typechecking loop error
R=ken2
CC=golang-dev
https://golang.org/cl/5654060
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2aafad25b4fe6c8e0d7f75b71b4b6d6e238483d6
元コミット内容
gc: print detail for typechecking loop error
R=ken2
CC=golang-dev
https://golang.org/cl/5654060
変更の背景
Goコンパイラは、プログラムの型が正しく定義され、使用されているかを検証するために「型チェック」というプロセスを実行します。この型チェックの過程で、型定義が互いに参照し合うような循環(ループ)が発生することがあります。例えば、型Aが型Bを参照し、型Bが型Aを参照するようなケースです。このような循環は、コンパイラが型のサイズや構造を決定する上で問題を引き起こす可能性があります。
以前のコンパイラでは、このような型チェックの循環が検出された際に、単に「typechecking loop involving %N」という一般的なエラーメッセージが表示されるだけでした。このメッセージだけでは、どの型や式が循環の原因となっているのか、その詳細なパスが不明瞭であり、開発者が問題をデバッグする上で困難を伴いました。
このコミットの背景には、より詳細なエラー情報を提供することで、開発者のデバッグ体験を向上させ、コンパイラが報告するエラーメッセージの有用性を高めるという目的があります。特に、循環のパスを明示することで、問題の根本原因を迅速に特定できるようにすることが狙いです。
前提知識の解説
Goコンパイラ (gc)
gcは、Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担っています。Goコンパイラは、複数のフェーズを経てコンパイルを行います。主要なフェーズには、字句解析、構文解析、型チェック、最適化、コード生成などがあります。
型チェック (Type Checking)
型チェックは、コンパイラの重要なフェーズの一つで、プログラムが型システムの一貫性規則に従っていることを検証します。具体的には、以下のようなことを確認します。
- 型の互換性: 演算子のオペランドや関数の引数と戻り値の型が互換性があるか。
- 型推論: 変数宣言などで型が明示されていない場合に、初期値から型を推論する。
- 循環参照の検出: 型定義や式の評価において、無限ループに陥る可能性のある循環参照がないか。
Go言語の型システムは静的型付けであり、コンパイル時に厳密な型チェックが行われます。これにより、多くの型関連のエラーが実行時ではなくコンパイル時に検出され、プログラムの信頼性が向上します。
抽象構文木 (AST: Abstract Syntax Tree)
コンパイラは、ソースコードを解析して抽象構文木(AST)と呼ばれるツリー構造を構築します。ASTは、プログラムの構造を抽象的に表現したもので、型チェックやコード生成などの後続のフェーズで利用されます。Goコンパイラでは、Node構造体がASTの各ノードを表します。
Node構造体
Goコンパイラの内部では、Node構造体がASTの各要素(変数、定数、関数呼び出し、演算子など)を表します。各Nodeは、その種類(opフィールド)、型(typeフィールド)、シンボル(symフィールド)、そして型チェックの状態(typecheckフィールド)などの情報を持っています。
n->typecheck: このフィールドは、ノードの型チェックの状態を示します。0: 未チェック1: 型チェック済み2: 型チェック中(循環検出に使用)
NodeList構造体
NodeListは、Nodeのリンクリストを構成するための構造体です。型チェックの循環を検出する際に、現在型チェック中のノードのスタックを管理するために使用されます。
Fmt構造体とfmtstrinit/fmtprint/fmtstrflush
これらは、Goコンパイラ内部で使用されるフォーマット済み文字列を構築するためのユーティリティです。Fmt構造体は、文字列のバッファリングとフォーマットを管理し、fmtstrinitで初期化、fmtprintで文字列を追加、fmtstrflushで最終的な文字列を取得します。これは、C言語のsprintfに似た機能を提供しますが、コンパイラの内部で効率的に文字列を構築するために最適化されています。
yyerrorとfatal
yyerror: コンパイラがエラーを報告するために使用する関数です。通常、エラーメッセージを標準エラー出力に表示し、コンパイルプロセスを継続しようとします(ただし、エラーが多すぎると停止することもあります)。fatal: 回復不可能な致命的なエラーが発生した場合に呼び出される関数です。通常、エラーメッセージを表示した後にプログラムを終了します。
技術的詳細
このコミットの主要な変更点は、src/cmd/gc/typecheck.cファイル内のtypecheck関数と、新しく導入されたtypecheck1関数のロジックにあります。
型チェックの循環検出メカニズム
Goコンパイラの型チェックは、再帰的に行われることがあります。例えば、ある式の型をチェックする際に、その式が参照する別の式の型をチェックする必要が生じ、それがさらに別の式を参照するという具合です。この再帰的な処理中に、既に型チェック中のノードに再び遭遇した場合、それは循環参照を示します。
typecheck関数は、ノードのtypecheckフィールドを利用して循環を検出します。
- ノードの型チェックを開始する前に、
n->typecheckを2に設定します。これは「型チェック中」の状態を示します。 - もし
typecheck関数が、既にtypecheck == 2となっているノードに遭遇した場合、それは循環が検出されたことを意味します。
変更前のエラー報告
変更前は、循環が検出されると以下のシンプルなエラーメッセージが出力されていました。
yyerror("typechecking loop involving %N", n);
ここで%Nは、循環が検出されたノードを表します。しかし、このメッセージだけでは、循環に至るまでのパスが不明であり、デバッグが困難でした。
変更後のエラー報告の改善
このコミットでは、循環が検出された際に、その循環に関与するノードのスタックトレースのような情報を出力するように改善されました。
-
typecheck1関数の導入: 既存のtypecheck関数から実際の型チェックロジックがtypecheck1という新しい静的関数に分離されました。これにより、typecheck関数は主に循環検出とスタック管理のラッパーとして機能するようになりました。 -
型チェックスタックの管理:
tcstackというNodeListのリンクリストが導入され、現在型チェック中のノードがこのスタックにプッシュされます。typecheck関数が呼び出されるたびに、現在のノードnがtcstackの先頭に追加されます。typecheck1による型チェックが完了すると、対応するノードがtcstackからポップされます。tcfreeというリンクリストは、NodeListのメモリを再利用するためのフリーリストとして機能します。これにより、頻繁なメモリ割り当てと解放を避けることができます。
-
詳細なエラーメッセージの生成:
- 循環が検出され(
n->typecheck == 2)、かつまだエラーが報告されていない場合(nsavederrors+nerrors == 0)、tcstackを逆順に辿り、循環に関与するすべてのノードとその行番号をFmt構造体を使ってフォーマットします。 - このフォーマットされた詳細情報が、
yyerrorメッセージに追加されます。
if(nsavederrors+nerrors == 0) { fmtstrinit(&fmt); for(l=tcstack; l; l=l->next) fmtprint(&fmt, "\\n\\t%L %N", l->n->lineno, l->n); yyerror("typechecking loop involving %N%s", n, fmtstrflush(&fmt)); }ここで、
%Lは行番号、%Nはノードを表します。これにより、エラーメッセージは以下のような形式になります。typechecking loop involving <node_N> <line_X> <node_A> <line_Y> <node_B> <line_Z> <node_C>これは、
<node_C>が<node_B>を参照し、<node_B>が<node_A>を参照し、最終的に<node_A>が<node_N>(または<node_N>が<node_A>)を参照して循環を形成していることを示します。 - 循環が検出され(
-
アサーションの追加:
typecheck関数内で、tcstackが期待通りに管理されているかを確認するためのfatalアサーションが追加されました。if(tcstack != l) fatal("typecheck stack out of sync");このアサーションは、スタックの不整合が発生した場合にコンパイラをクラッシュさせ、開発中にバグを早期に発見するのに役立ちます。
これらの変更により、型チェックの循環エラーが発生した際に、開発者は循環のパスを明確に把握できるようになり、デバッグの効率が大幅に向上します。
コアとなるコードの変更箇所
src/cmd/gc/typecheck.c ファイルにおける変更の差分は以下の通りです。
--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -111,16 +111,15 @@ typekind(Type *t)
* replaces *np with a new pointer in some cases.
* returns the final value of *np as a convenience.
*/
+static void typecheck1(Node **, int);
Node*
typecheck(Node **np, int top)
{
-\tint et, aop, op, ptr;\n-\tNode *n, *l, *r;\n-\tNodeList *args;\n-\tint lno, ok, ntop;\n-\tType *t, *tp, *ft, *missing, *have, *badtype;\n-\tVal v;\n-\tchar *why;\n+\tNode *n;\n+\tint lno;\n+\tFmt fmt;\n+\tNodeList *l;\n+\tstatic NodeList *tcstack, *tcfree;\
\n \t// cannot type check until all the source has been parsed
\tif(!typecheckok)
@@ -157,11 +156,52 @@ typecheck(Node **np, int top)
\t}\
\n \tif(n->typecheck == 2) {\
-\t\tyyerror(\"typechecking loop involving %N\", n);\n+\t\tif(nsavederrors+nerrors == 0) {\
+\t\t\tfmtstrinit(&fmt);\
+\t\t\tfor(l=tcstack; l; l=l->next)\
+\t\t\t\tfmtprint(&fmt, \"\\n\\t%L %N\", l->n->lineno, l->n);\
+\t\t\tyyerror(\"typechecking loop involving %N%s\", n, fmtstrflush(&fmt));
+\t\t}\
\t\tlineno = lno;\
\t\treturn n;\
\t}\
\tn->typecheck = 2;\
+\t\n+\tif(tcfree != nil) {\
+\t\tl = tcfree;\
+\t\ttcfree = l->next;\
+\t} else
+\t\tl = mal(sizeof *l);\
+\tl->next = tcstack;\
+\tl->n = n;\
+\ttcstack = l;\
+\n+\ttypecheck1(&n, top);\
+\t*np = n;\
+\tn->typecheck = 1;\
+\n+\tif(tcstack != l)\
+\t\tfatal(\"typecheck stack out of sync\");\
+\ttcstack = l->next;\
+\tl->next = tcfree;\
+\ttcfree = l;\
+\n+\tlineno = lno;\
+\treturn n;\
+}\
+\n+static void\n+typecheck1(Node **np, int top)\
+{\
+\tint et, aop, op, ptr;\
+\tNode *n, *l, *r;\
+\tNodeList *args;\
+\tint ok, ntop;\
+\tType *t, *tp, *ft, *missing, *have, *badtype;\
+\tVal v;\
+\tchar *why;\
+\t\n+\tn = *np;\
\n \tif(n->sym) {\
\t\tif(n->op == ONAME && n->etype != 0 && !(top & Ecall)) {\
@@ -1484,10 +1524,7 @@ error:\
\tn->type = T;\
\n out:\
-\tlineno = lno;\n-\tn->typecheck = 1;\
\t*np = n;\
-\treturn n;\
}\
\n static void
コアとなるコードの解説
typecheck関数の変更
- 変数宣言の変更:
typecheck関数から、型チェックの具体的なロジックで使用される多くのローカル変数(et,aop,op,ptr,r,args,ok,ntop,t,tp,ft,missing,have,badtype,v,why)が削除されました。これらの変数は新しく導入されたtypecheck1関数に移動されました。 - 新しいローカル変数の追加:
Fmt fmt;、NodeList *l;、static NodeList *tcstack, *tcfree;が追加されました。fmt: エラーメッセージのフォーマットに使用されるFmt構造体。l:NodeListの要素を一時的に保持するためのポインタ。tcstack: 型チェック中のノードを追跡するためのスタック(リンクリスト)。staticキーワードにより、関数呼び出し間で状態が保持されます。tcfree:NodeListの要素を再利用するためのフリーリスト。staticキーワードにより、関数呼び出し間で状態が保持されます。
- 循環検出時のエラーメッセージの改善:
if(n->typecheck == 2)ブロック内で、以前はyyerror("typechecking loop involving %N", n);とだけ出力されていましたが、より詳細な情報が出力されるようになりました。if(nsavederrors+nerrors == 0)という条件が追加されています。これは、まだエラーが報告されていない場合にのみ詳細なループ情報を出力するためのものです。これにより、同じ循環エラーが複数回報告されるのを防ぎます。fmtstrinit(&fmt);でFmt構造体を初期化し、for(l=tcstack; l; l=l->next)ループでtcstackを逆順に辿ります。fmtprint(&fmt, "\\n\\t%L %N", l->n->lineno, l->n);で、スタック上の各ノードの行番号とノード自体をフォーマットしてfmtに追加します。\n\tは、新しい行とインデントを追加して、スタックトレースのような表示にするためのものです。- 最終的に
yyerror("typechecking loop involving %N%s", n, fmtstrflush(&fmt));で、循環が検出されたノードと、fmtstrflush(&fmt)で取得した詳細なスタックトレース情報を結合してエラーメッセージとして出力します。
- 型チェックスタックの管理ロジックの追加:
n->typecheck = 2;の後に、現在のノードnをtcstackにプッシュするロジックが追加されました。tcfreeリストからNodeList要素を再利用するか、新しくmal(メモリ割り当て)で確保します。l->next = tcstack;とtcstack = l;で、lをtcstackの先頭に挿入します。
typecheck1の呼び出し: 実際の型チェックロジックはtypecheck1(&n, top);として呼び出されます。- スタックのポップとクリーンアップ:
n->typecheck = 1;でノードの型チェックが完了したことをマークします。if(tcstack != l) fatal("typecheck stack out of sync");は、スタックの整合性をチェックするアサーションです。もしスタックが期待通りにポップされていない場合、致命的なエラーとしてコンパイラを終了させます。tcstack = l->next;でスタックから現在のノードをポップします。l->next = tcfree;とtcfree = l;で、ポップしたNodeList要素をtcfreeリストに戻し、メモリを再利用できるようにします。
linenoとreturn n;の移動:lineno = lno;とreturn n;は、typecheck関数の末尾に移動され、typecheck1の呼び出し後に行われるようになりました。
typecheck1関数の導入
static void typecheck1(Node **, int);として新しい静的関数が宣言されました。typecheck関数から移動された型チェックの具体的なロジックがこの関数内に配置されました。n = *np;で、引数として渡されたノードポインタの参照を解除してnに代入します。- 以前
typecheck関数の末尾にあったlineno = lno;、n->typecheck = 1;、return n;の行は、typecheck1からは削除されました。これらの処理はtypecheck関数がtypecheck1を呼び出した後に責任を持って行います。
これらの変更により、typecheck関数は型チェックの循環検出とスタック管理の役割に特化し、実際の型チェックロジックはtypecheck1に委譲されることで、コードの関心事が分離され、可読性と保守性が向上しています。また、詳細なエラーメッセージの生成により、デバッグの効率が大幅に改善されました。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- このコミットが属するGoの変更リスト (CL): https://golang.org/cl/5654060
参考にした情報源リンク
- Go言語のコンパイラに関するドキュメントやソースコード(
src/cmd/gc/ディレクトリ内のファイル) - Go言語の型システムに関する公式ドキュメント
- 抽象構文木 (AST) に関する一般的なコンパイラ理論の資料
- Goコンパイラの内部構造に関するブログ記事や解説(例: "Go's Type System" by Russ Cox, "A Tour of the Go Compiler")
yyerrorやfatalといったコンパイラ内部のユーティリティ関数の一般的な役割に関する情報Fmt構造体や関連するフォーマット関数のGoコンパイラ内での使用例- Go言語のコンパイラ開発に関するメーリングリスト(golang-dev)の議論