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

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

このコミットは、Goコンパイラ(gc)における型宣言の処理に関するバグ修正です。具体的には、type n t; のような型宣言において、t の型情報が n に不適切にコピーされたり、t 自体が意図せず変更されたりする問題を解決しています。これにより、型のシグネチャ印刷キューの状態や、基底型にメソッドが誤って関連付けられるといった不整合が修正されました。

コミット

commit 07687705a4c2db718a3601d3558807833938dfbf
Author: Russ Cox <rsc@golang.org>
Date:   Thu Apr 2 21:38:11 2009 -0700

    type n t;
    was copying a bit too much about t into n,
    like whether the signature was queued to be printed.
    (bug reported by anton)
    
    was also editing t, meaning you could do
            type T int;
            func (p int) Meth() { }
    
    both fixed.
    
    R=ken
    OCL=27052
    CL=27052

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

https://github.com/golang/go/commit/07687705a4c2db718a3601d3558807833938dfbf

元コミット内容

 src/cmd/gc/dcl.c  | 14 ++++++++++++--
 src/cmd/gc/obj.c  |  1 +\
 src/cmd/gc/subr.c |  2 +-\
 3 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/src/cmd/gc/dcl.c b/src/cmd/gc/dcl.c
index 51c76be752..2d83d9f9fd 100644
--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -91,6 +91,7 @@ void
 updatetype(Type *n, Type *t)
 {
 	Sym *s;
+	int local;
 
 	s = n->sym;
 	if(s == S || s->otype != n)
@@ -118,10 +119,19 @@ updatetype(Type *n, Type *t)
 		fatal("updatetype %T / %T", n, t);
 	}
 
-	if(n->local)
-		t->local = 1;
+	// decl was
+	//	type n t;
+	// copy t, but then zero out state associated with t
+	// that is no longer associated with n.
+	local = n->local;
 	*n = *t;
 	n->sym = s;
+	n->local = local;
+	n->siggen = 0;
+	n->methptr = 0;
+	n->printed = 0;
+	n->method = nil;
+	n->vargen = 0;
 
 	// catch declaration of incomplete type
 	switch(n->etype) {
diff --git a/src/cmd/gc/obj.c b/src/cmd/gc/obj.c
index 7c80ee22a8..9c0b6edb66 100644
--- a/src/cmd/gc/obj.c
+++ b/src/cmd/gc/obj.c
@@ -425,6 +425,7 @@ dumpsignatures(void)
 		t = d->dtype;
 		et = t->etype;
 		s = signame(t);
+//print("signame %S for %T\n", s, t);
 		if(s == S)
 			continue;
 
diff --git a/src/cmd/gc/subr.c b/src/cmd/gc/subr.c
index fe5d33084e..fb8a1744a4 100644
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -1637,8 +1637,8 @@ signame(Type *t)
 		ss->oname->class = PEXTERN;
 	}
 
+//print("siggen %T %d\n", t, t->siggen);
 	if(!t->siggen) {
-//print("siggen %T\n", t);\n 	\t// special case: don\'t generate the empty interface
 	\t\tif(strcmp(buf, \"empty\") == 0)\
 	\t\t\tgoto out;\

変更の背景

このコミットは、Goコンパイラ(gc)における型システム処理の初期段階で発見された2つの関連するバグを修正するために行われました。

  1. 不適切な型情報コピー: type n t; のような型宣言(n が新しい型、t が既存の型)において、t の内部状態(例えば、そのシグネチャが印刷キューに入っているかどうかを示すフラグ siggen)が n に誤ってコピーされていました。これは、nt とは独立した新しい型であるにもかかわらず、t の一時的なコンパイラ内部状態まで引き継いでしまうという問題を引き起こしました。
  2. 基底型の意図しない変更: さらに深刻な問題として、updatetype 関数が t の内容を n にコピーする際に、t 自体の local フラグを n->local の値で上書きしていました。これにより、type T int; func (p int) Meth() { } のようなコードが書かれた場合、int 型(t に相当)に Meth メソッドが誤って関連付けられてしまう可能性がありました。これは、Goの型システムにおいて、基底型に直接メソッドを追加することはできないという重要な原則に反する動作です。

これらのバグは、コンパイラの健全性を損ない、予期せぬ動作やコンパイルエラーを引き起こす可能性がありました。特に、anton によって報告されたバグは、シグネチャの印刷に関する問題を示唆しています。

前提知識の解説

このコミットを理解するためには、以下のGoコンパイラの概念とGo言語の基本的な型システムに関する知識が必要です。

  • Goコンパイラ (gc): Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担います。src/cmd/gc ディレクトリにそのソースコードがあります。
  • 型システム: Go言語は静的型付け言語であり、すべての変数と式には型があります。型は、値がどのような種類のデータであるか、どのような操作が可能であるかを定義します。
  • 型宣言 (type): Goでは type NewType OldType のように既存の型から新しい型を宣言できます。この新しい型は、基底型とは異なる独自のメソッドセットを持つことができます。
  • メソッド: Goでは、型にメソッドを関連付けることができます。func (receiver Type) MethodName(args) { ... } の形式で定義されます。
  • Type 構造体: Goコンパイラの内部では、Go言語の各型は Type というC言語の構造体で表現されます。この構造体には、型の種類(整数、文字列、構造体など)、サイズ、アラインメント、そしてメソッドやシグネチャに関する情報など、型に関するあらゆるメタデータが含まれています。
  • Sym 構造体: シンボルテーブルのエントリを表す構造体で、変数名、関数名、型名などの識別子とそれに関連する情報(型、スコープなど)を管理します。
  • updatetype 関数: src/cmd/gc/dcl.c にあるこの関数は、型宣言 type n t; の際に、新しい型 nType 構造体を既存の型 t の情報で更新する役割を担います。
  • siggen (Signature Generation Flag): Type 構造体内のフラグの一つで、その型のシグネチャ(例えば、関数の引数と戻り値の型リスト)が既に生成され、コンパイラの内部で処理されたかどうかを示すために使用されます。これは、重複する処理を避けるための最適化や、特定の型のシグネチャを一度だけ印刷するために利用されます。
  • methptr (Method Pointer): Type 構造体内のフィールドで、その型に関連付けられたメソッドのリストへのポインタを保持します。
  • printed: Type 構造体内のフラグで、その型が既にコンパイラの出力(例えば、デバッグ情報や型定義の出力)で印刷されたかどうかを示します。
  • method: Type 構造体内のフィールドで、その型に直接関連付けられたメソッドのリストを保持します。
  • vargen: Type 構造体内のフラグで、変数の生成に関連する状態を示します。

技術的詳細

このコミットの核心は、src/cmd/gc/dcl.c 内の updatetype 関数の修正にあります。この関数は、type n t; という形式の型宣言を処理する際に呼び出されます。

元の実装では、*n = *t; という行で tType 構造体の内容が n にビット単位でコピーされていました。これは、t のすべてのフィールドが n にそのままコピーされることを意味します。このコピーには、siggenmethptrprintedmethodvargen といった、コンパイラの内部状態や一時的なフラグも含まれていました。

問題は、nt とは独立した新しい型であるにもかかわらず、t の一時的な状態(例えば、t のシグネチャが既に印刷キューに入っているという情報)まで n が引き継いでしまうことでした。これは、n のシグネチャが適切に処理されない、あるいは重複して処理されるといった不整合を引き起こす可能性がありました。

さらに、if(n->local) t->local = 1; という行は、n がローカル型である場合に tlocal フラグを 1 に設定していました。これは、t がグローバルな型であるにもかかわらず、n のローカル性によって t がローカル型として扱われるという、基底型への意図しない副作用を引き起こしました。この副作用が、type T int; func (p int) Meth() { } のようなコードで int 型にメソッドが誤って関連付けられるというバグの原因でした。Goでは、基底型に直接メソッドを追加することはできません。メソッドは、その型を基底型とする新しい名前付き型にのみ関連付けられます。

修正では、以下の変更が加えられました。

  1. local フラグの適切な処理: *n = *t; の前に n->local の値を local 変数に一時的に保存し、*n = *t; でコピーした後、n->local = local; で元の nlocal 状態を復元するようにしました。これにより、tlocal フラグが意図せず変更されることを防ぎます。
  2. コンパイラ内部状態のクリア: *n = *t;t の内容を n にコピーした後、n->siggen = 0; n->methptr = 0; n->printed = 0; n->method = nil; n->vargen = 0; といった行が追加されました。これにより、nt からコピーされた一時的なコンパイラ内部状態(シグネチャ生成フラグ、メソッドポインタ、印刷済みフラグなど)をリセットし、n が新しい独立した型として適切に初期化されるようにします。特に n->method = nil; は、基底型 t に関連付けられたメソッドが n に誤って引き継がれることを防ぎます。

src/cmd/gc/obj.csrc/cmd/gc/subr.c の変更は、デバッグ用の print ステートメントのコメントアウトであり、直接的なバグ修正というよりは、デバッグ中に使用されたコードのクリーンアップと考えられます。特に signame 関数は型のシグネチャ名を生成する役割を担っており、siggen フラグと密接に関連しています。

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

src/cmd/gc/dcl.c

--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -91,6 +91,7 @@ void
 updatetype(Type *n, Type *t)
 {
 	Sym *s;
+	int local; // 追加
 
 	s = n->sym;
 	if(s == S || s->otype != n)
@@ -118,10 +119,19 @@ updatetype(Type *n, Type *t)
 		fatal("updatetype %T / %T", n, t);
 	}
 
-	if(n->local)
-		t->local = 1;
+	// decl was
+	//	type n t;
+	// copy t, but then zero out state associated with t
+	// that is no longer associated with n.
+	local = n->local; // nのlocalフラグを一時保存
 	*n = *t;
 	n->sym = s;
+	n->local = local; // nのlocalフラグを復元
+	n->siggen = 0;    // siggenフラグをリセット
+	n->methptr = 0;   // methptrをリセット
+	n->printed = 0;   // printedフラグをリセット
+	n->method = nil;  // methodリストをnilに設定
+	n->vargen = 0;    // vargenフラグをリセット
 
 	// catch declaration of incomplete type
 	switch(n->etype) {

src/cmd/gc/obj.c

--- a/src/cmd/gc/obj.c
+++ b/src/cmd/gc/obj.c
@@ -425,6 +425,7 @@ dumpsignatures(void)
 		t = d->dtype;
 		et = t->etype;
 		s = signame(t);
+//print("signame %S for %T\n", s, t); // コメントアウト
 		if(s == S)
 			continue;
 

src/cmd/gc/subr.c

--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -1637,8 +1637,8 @@ signame(Type *t)
 		ss->oname->class = PEXTERN;
 	}
 
+//print("siggen %T %d\n", t, t->siggen); // コメントアウト
 	if(!t->siggen) {
-//print("siggen %T\n", t); // コメントアウト
 		// special case: don't generate the empty interface
 		if(strcmp(buf, "empty") == 0)
 			goto out;

コアとなるコードの解説

src/cmd/gc/dcl.cupdatetype 関数がこのコミットの主要な変更点です。

  1. local 変数の導入と復元:

    • 変更前: if(n->local) t->local = 1; は、新しい型 n がローカルスコープで宣言された場合、基底型 tlocal フラグまで変更していました。これは、int のような組み込み型が意図せずローカル型として扱われる可能性を生み出し、その結果、int にメソッドが関連付けられるという誤った動作につながりました。
    • 変更後: int local; を導入し、local = n->local;n の元の local 状態を保存します。*n = *t;t の内容が n にコピーされた後、n->local = local;nlocal フラグを元の状態に戻します。これにより、基底型 tlocal フラグが変更されることを防ぎ、n が自身のローカル性を持つことを保証します。
  2. コンパイラ内部状態のリセット:

    • 変更前: *n = *t;t のすべてのフィールドを n にコピーしていました。これには、siggenmethptrprintedmethodvargen といった、コンパイラが型の処理中に一時的に使用する内部状態やフラグも含まれていました。これらのフラグは、型のシグネチャが既に生成されたか、メソッドが既に処理されたか、型が既に印刷されたかなどを示します。新しい型 nt とは独立してこれらの処理を受けるべきであるため、これらの状態がコピーされるのは不適切でした。
    • 変更後: n->siggen = 0; n->methptr = 0; n->printed = 0; n->method = nil; n->vargen = 0; の行が追加されました。これは、*n = *t; によるコピーの後、n のこれらの内部状態フラグを明示的にゼロ(または nil)にリセットします。これにより、n は「未処理」の新しい型として扱われ、コンパイラは n のシグネチャ生成やメソッド処理を最初から適切に行うことができます。特に n->method = nil; は、基底型 t に関連付けられたメソッドが新しい型 n に誤って引き継がれることを確実に防ぎます。

src/cmd/gc/obj.csrc/cmd/gc/subr.c の変更は、デバッグ中に一時的に追加された print ステートメントをコメントアウトしたものです。これらはバグ修正の直接的な原因ではありませんが、修正作業中にデバッグのために使用され、その後クリーンアップされたものと考えられます。

このコミットは、Goコンパイラの型システムが、型宣言のセマンティクス(特に新しい名前付き型が基底型から独立した存在であること)を正確に反映するようにするための重要なステップでした。

関連リンク

  • Go言語の型システムに関する公式ドキュメント(当時のものを見つけるのは難しいですが、現在のドキュメントも参考になります)
  • Goコンパイラのソースコード(src/cmd/gc

参考にした情報源リンク