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

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

このコミットは、Goコンパイラの型チェックおよびコード生成フェーズにおけるインターフェース変換の取り扱いに関する修正を含んでいます。具体的には、src/cmd/gc/go.hsrc/cmd/gc/walk.c の2つのファイルが変更されています。

  • src/cmd/gc/go.h: Goコンパイラのグローバルヘッダーファイルで、型定義や関数プロトタイプが宣言されています。このコミットでは、新しいインターフェース変換関連関数のプロトタイプが追加されています。
  • src/cmd/gc/walk.c: Goコンパイラのバックエンドの一部であり、抽象構文木 (AST) を走査し、型チェック、最適化、および中間コード生成を行うファイルです。このコミットでは、インターフェース変換ロジックが修正されています。

コミット

bug 135

R=r
OCL=23646
CL=23646

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

https://github.com/golang/go/commit/4b8e030762b7c4eb66d505530b873ba62494c26a

元コミット内容

このコミットの元の内容は、簡潔に「bug 135」とだけ記されています。これは、Goプロジェクトの初期段階におけるバグトラッキングシステムに関連するもので、特定のバグ報告に対応する修正であることを示唆しています。具体的なバグの内容はコミットメッセージからは直接読み取れませんが、コードの変更内容からインターフェースの型アサーションや変換に関する問題であったと推測されます。

変更の背景

このコミットは「bug 135」という簡潔なメッセージで示されていますが、コードの変更内容から、Goコンパイラがインターフェースからインターフェースへの変換(特にソースとターゲットのインターフェース型が同一である場合)をどのように扱うかに関するバグまたは非効率性に対処していることがわかります。

Go言語では、インターフェースは動的な型情報を持つため、インターフェース間の変換や型アサーションはコンパイル時だけでなく、実行時にも特定のチェックや操作を必要とすることがあります。以前の実装では、ソースとターゲットのインターフェース型が完全に同一である場合、コンパイラは変換が不要であると判断し、「no operation (Inone)」として扱っていました。しかし、このアプローチが特定のシナリオで問題を引き起こす可能性がありました。例えば、同一のインターフェース型であっても、内部的な表現や最適化の観点から、何らかの処理が必要となるケースが存在したのかもしれません。

このコミットは、同一のインターフェース型間の変換をより詳細に区別するための新しい内部状態 (I2Isame) を導入し、コンパイラがこれらのケースをより正確に処理できるようにすることで、この問題を解決しようとしています。これにより、コンパイラは必要に応じて適切なランタイムチェックや最適化を適用できるようになります。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびGoコンパイラに関する前提知識が必要です。

Go言語のインターフェース

Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。Goのインターフェースは、他の言語のインターフェースとは異なり、明示的な実装宣言を必要としません。ある型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たしているとみなされます(構造的型付け)。

インターフェース型の変数は、任意の具象型の値を保持できます。この値は、その具象型と、その具象型が満たすインターフェースの型情報の両方を含んでいます。

インターフェースの型アサーションと変換

Goでは、インターフェース型の値を別のインターフェース型や具象型に変換する「型アサーション」が可能です。

  • value.(Type): valueType型であるとアサートします。成功すればType型の値が返され、失敗すればパニックが発生します。
  • value.(Type), ok := value.(Type): valueType型であるとアサートし、成功したかどうかを示すブール値okも返します。失敗してもパニックは発生しません。

コンパイラは、これらの変換が安全に行われるか、または実行時にチェックが必要かを判断します。

Goコンパイラ (gc / cmd/compile)

Goコンパイラは、Go言語のソースコードを機械語に変換するツールチェーンの中核をなす部分です。初期のGoコンパイラは gc (Go Compiler) と呼ばれ、C言語で書かれていました。現在のGoコンパイラは cmd/compile と呼ばれ、Go言語自体で書かれています。

コンパイラの主要なフェーズには以下が含まれます。

  1. 字句解析と構文解析: ソースコードをトークンに分割し、抽象構文木 (AST) を構築します。
  2. 型チェック: ASTを走査し、型の整合性を検証します。
  3. 中間コード生成: ASTを中間表現 (IR) に変換します。
  4. 最適化: 中間コードを最適化します。
  5. コード生成: 中間コードをターゲットアーキテクチャの機械語に変換します。

このコミットで変更されている src/cmd/gc/walk.c は、主に型チェックと中間コード生成のフェーズに関わっています。walk 関数はASTを走査し、各ノード(式、ステートメントなど)に対して適切な処理(型チェック、変換、最適化など)を行います。

ifaceas 関数

ifaceas (interface as) 関数は、Goコンパイラ内部でインターフェースの型変換の可能性を判断し、その変換がどのような種類であるかを示す値を返すユーティリティ関数です。例えば、インターフェースから具象型への変換 (I2T)、インターフェースから別のインターフェースへの変換 (I2I) などの種類を識別します。この関数が返す値に基づいて、コンパイラは適切なランタイムヘルパー関数を呼び出すコードを生成します。

技術的詳細

このコミットの技術的な核心は、インターフェース変換の内部的な分類をより細分化し、特にソースとターゲットのインターフェース型が同一であるケースを明示的に区別できるようにした点にあります。

go.h の変更

src/cmd/gc/go.h には、新しい関数プロトタイプ int ifaceas1(Type*, Type*); が追加されました。これは、既存の ifaceas 関数と似た目的を持つ新しい関数が導入されることを示しています。

walk.c の変更

  1. 新しい変換タイプ I2Isame の導入: enum 定義に I2Isame という新しい値が追加されました。これは I2I (interface to interface) とは別に、ソースとターゲットのインターフェース型が同一である場合のインターフェース変換を示すために使用されます。

    enum
    {
        // ...
        I2I,
        I2I2,
        T2I,
        I2Isame, // 新しく追加された
    };
    
  2. ifaceas 関数のリファクタリングと ifaceas1 の導入: 元の ifaceas 関数は ifaceas1 に名前が変更されました。この ifaceas1 関数は、ソースとターゲットのインターフェース型が同一である場合に、以前は Inone (no operation) を返していた箇所で、新しく導入された I2Isame を返すように変更されました。

    変更前 (ifaceas):

    int
    ifaceas(Type *dst, Type *src)
    {
        // ...
        if(isinter(dst)) {
            if(isinter(src)) {
                if(eqtype(dst, src, 0))
                    return Inone; // 同一型の場合はInoneを返す
                return I2I;
            }
            // ...
        }
        // ...
    }
    

    変更後 (ifaceas1):

    int
    ifaceas1(Type *dst, Type *src) // ifaceasからifaceas1にリネーム
    {
        // ...
        if(isinter(dst)) {
            if(isinter(src)) {
                if(eqtype(dst, src, 0))
                    return I2Isame; // 同一型の場合はI2Isameを返す
                return I2I;
            }
            // ...
        }
        // ...
    }
    
  3. 新しい ifaceas ラッパー関数の導入: ifaceas1 の導入に伴い、元の ifaceas という名前で新しいラッパー関数が定義されました。この新しい ifaceas 関数は ifaceas1 を呼び出し、その結果が I2Isame であった場合は Inone に変換して返します。これにより、ifaceas を呼び出す既存のコードは、同一インターフェース型間の変換を以前と同様に Inone として扱うことができます。これは、コンパイラの他の部分への影響を最小限に抑えつつ、内部的に I2Isame を区別できるようにするための設計です。

    /*
     * treat convert T to T as noop
     */
    int
    ifaceas(Type *dst, Type *src)
    {
        int et;
    
        et = ifaceas1(dst, src); // ifaceas1を呼び出す
        if(et == I2Isame)
            et = Inone; // I2Isameの場合はInoneに変換
        return et;
    }
    
  4. walk 関数での ifaceas1 の使用: walk 関数内のインターフェース変換処理において、ifaceas の代わりに ifaceas1 が直接呼び出されるようになりました。これにより、walk 関数は I2Isame という新しい変換タイプを直接受け取ることができるようになります。

    // 変更前
    // et = ifaceas(r->type, r->left->type);
    // 変更後
    et = ifaceas1(r->type, r->left->type);
    
  5. walk 関数での I2Isame のハンドリング: walk 関数内の switch ステートメントに case I2Isame: が追加されました。このケースは case I2I: と同様に et = I2I2; を実行します。I2I2 はインターフェースからインターフェースへの変換で、ランタイムチェックが必要な場合に使用されるコード生成タイプです。これは、たとえインターフェース型が同一であっても、ランタイムでの何らかの処理(例えば、基底の具象型の抽出や検証)が必要になる可能性があることを示唆しています。

    switch(et) {
    case I2T:
        et = I2T2;
        break;
    case I2Isame: // 新しく追加されたケース
    case I2I:
        et = I2I2;
        break;
    // ...
    }
    
  6. ifacename 配列の更新: デバッグやログ出力のために使用される ifacename 配列に "ifaceI2Isame" が追加されました。

    static  char*
    ifacename[] =
    {
        // ...
        [I2I]       = "ifaceI2I",
        [I2I2]      = "ifaceI2I2",
        [I2Isame]   = "ifaceI2Isame", // 新しく追加された
    };
    

これらの変更により、Goコンパイラはインターフェース変換のセマンティクスをより正確に表現できるようになり、特に同一インターフェース型間の変換における潜在的なバグや非効率性に対処しています。

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

src/cmd/gc/go.h

--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -801,6 +801,7 @@ Node*	chanop(Node*, int);
 Node*	arrayop(Node*, int);
 Node*	ifaceop(Type*, Node*, int);
 int	ifaceas(Type*, Type*);
+int	ifaceas1(Type*, Type*); // 追加
 void	ifacecheck(Type*, Type*, int);
 void	runifacechecks(void);
 Node*	convas(Node*);

src/cmd/gc/walk.c

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -17,6 +17,7 @@ enum
 	I2I,
 	I2I2,
 	T2I,
+	I2Isame, // 追加
 };
 
 // can this code branch reach the end
@@ -500,11 +501,12 @@ loop:
 			walktype(r->left, Erv);
 			if(r->left == N)
 				break;
-			et = ifaceas(r->type, r->left->type); // ifaceas1に変更
+			et = ifaceas1(r->type, r->left->type); // 変更
 			switch(et) {
 			case I2T:
 				et = I2T2;
 				break;
+			case I2Isame: // 追加
 			case I2I:
 				et = I2I2;
 				break;
@@ -2772,7 +2774,7 @@ arrayop(Node *n, int top)\n  * return op to use.\n  */\n int\n-ifaceas(Type *dst, Type *src)\n+ifaceas1(Type *dst, Type *src)\n {\n 	if(src == T || dst == T)\n 		return Inone;\n@@ -2780,7 +2782,7 @@ ifaceas(Type *dst, Type *src)\n 	if(isinter(dst)) {\n 		if(isinter(src)) {\n 			if(eqtype(dst, src, 0))\n-				return Inone; // I2Isameに変更
+				return I2Isame; // 変更
 			return I2I;\n 		}\n 	if(isnilinter(dst))\n@@ -2797,13 +2799,28 @@ ifaceas(Type *dst, Type *src)\n 	return Inone;\n }\n \n+/*\n+ * treat convert T to T as noop\n+ */\n+int\n+ifaceas(Type *dst, Type *src)\n+{\n+\tint et;\n+\n+\tet = ifaceas1(dst, src);\n+\tif(et == I2Isame)\n+\t\tet = Inone;\n+\treturn et;\n+}\n+\n static	char*\n ifacename[] =\n {\n-\t[I2T]	= "ifaceI2T",\n-\t[I2T2]	= "ifaceI2T2",\n-\t[I2I]	= "ifaceI2I",\n-\t[I2I2]	= "ifaceI2I2",\n+\t[I2T]		= "ifaceI2T",\n+\t[I2T2]		= "ifaceI2T2",\n+\t[I2I]		= "ifaceI2I",\n+\t[I2I2]		= "ifaceI2I2",\n+\t[I2Isame]	= "ifaceI2Isame", // 追加
 };
 
 Node*

コアとなるコードの解説

このコミットの主要な変更点は、インターフェース変換の内部的な分類をより詳細にしたことです。

  1. I2Isame 列挙値の追加: enumI2Isame が追加されたことで、コンパイラはインターフェースからインターフェースへの変換において、ソースとターゲットのインターフェース型が同一であるという特定のケースを明示的に識別できるようになりました。これは、以前は単に「変換不要 (Inone)」として扱われていたケースです。

  2. ifaceas から ifaceas1 へのリネームと動作変更: 元の ifaceas 関数は ifaceas1 に名前が変更されました。この新しい ifaceas1 関数は、ソースとターゲットのインターフェース型が同一である場合に、以前の Inone ではなく I2Isame を返すようになりました。これにより、ifaceas1 はインターフェース変換の種類をより詳細に報告するようになりました。

  3. 新しい ifaceas ラッパー関数の導入: ifaceas1 の導入後、元の ifaceas という名前で新しい関数が定義されました。この新しい ifaceasifaceas1 を呼び出し、もし ifaceas1I2Isame を返した場合、それを Inone に変換して返します。このラッパーの目的は、コンパイラの他の部分が ifaceas を呼び出す際に、以前と同じ Inone のセマンティクス(同一型の場合は変換不要)を維持できるようにすることです。これにより、既存のコードベースへの影響を最小限に抑えつつ、内部的には I2Isame の区別を可能にしています。

  4. walk 関数での ifaceas1 の直接利用と I2Isame の処理: walk 関数内で、インターフェース変換の種類を判断するために ifaceas の代わりに ifaceas1 が直接呼び出されるようになりました。これにより、walk 関数は I2Isame という新しい情報を受け取ることができます。 さらに、walk 関数内の switch ステートメントに case I2Isame: が追加され、このケースは I2I と同様に I2I2 (インターフェースからインターフェースへのランタイム変換) を生成するように処理されます。これは、たとえインターフェース型が同一であっても、実行時に何らかのチェックや操作が必要となる可能性があることを示しています。例えば、インターフェースが保持する具象型の値がnilであるかどうかのチェックや、メソッドテーブルの参照などが考えられます。

これらの変更は、Goコンパイラがインターフェースの型アサーションや変換を処理する際の正確性と堅牢性を向上させることを目的としています。特に、同一のインターフェース型間の変換であっても、コンパイル時に最適化できるケースと、実行時に特定の処理が必要なケースをより細かく区別できるようになり、潜在的なバグの修正や、より効率的なコード生成に繋がる可能性があります。

関連リンク

参考にした情報源リンク

  • Go言語のインターフェースの動作に関する一般的な知識
  • Goコンパイラの内部構造に関する一般的な知識
  • 提示されたコミットの差分情報