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

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

このコミットは、Go言語のコンパイラにおけるswitch文の実装を抜本的に変更するものです。特に、将来的に導入されるtype switch(型スイッチ)のサポートを念頭に置いた大規模なリファクタリングであり、既存のswitch文の機能的な変更は伴いませんが、その内部構造を大きく刷新しています。従来のスイッチ処理ロジックが複数のファイルに分散していたものを、新たに導入されたsrc/cmd/gc/swt.cに集約し、よりモジュール化された設計へと移行しています。

コミット

commit bf983477a22bcb09fdd4e9c81d44a36ee8a4cd49
Author: Ken Thompson <ken@golang.org>
Date:   Thu Mar 5 15:49:34 2009 -0800

    new switch implementation
    in preparation of type switch.
    no functional change (yet).

    R=r
    OCL=25784
    CL=25788

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

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

元コミット内容

このコミットは、Go言語のswitch文の新しい実装を導入するものです。コミットメッセージには「new switch implementation」「in preparation of type switch」「no functional change (yet)」と明記されており、これは既存のswitch文の動作を変更することなく、将来的に導入されるtype switch(型スイッチ)のための基盤を構築するリファクタリングであることを示しています。Ken Thompsonによるこの変更は、Goコンパイラの初期段階における重要な構造的改善の一つです。

変更の背景

Go言語のコンパイラは、その初期段階から継続的に進化を遂げてきました。このコミットが行われた2009年3月は、Go言語が一般に公開される前の開発初期段階にあたります。当時のコンパイラは、C言語で記述されており、コードベースの整理と将来の機能拡張への対応が重要な課題でした。

従来のswitch文の実装は、コンパイラの異なるフェーズ(例えば、ASTのウォーク処理を行うwalk.cと、コード生成を行うgen.c)にまたがってロジックが分散しており、複雑で保守が難しい状態でした。特に、Go言語の設計思想の一つである「型アサーション」や「型スイッチ」といった高度な型システム機能を導入するためには、switch文の処理をより柔軟かつ効率的に行うための基盤が必要でした。

このコミットの主な背景は以下の点に集約されます。

  1. type switchの導入準備: Go言語の強力な機能の一つであるswitch x.(type)構文(型スイッチ)を実装するためには、コンパイラが異なる型のケースを効率的に処理できるようなswitch文の内部構造が必要でした。このコミットは、そのための土台作りを目的としています。
  2. コードのモジュール化と保守性の向上: switch文の処理ロジックをswt.cという新しい専用ファイルに集約することで、コードの可読性、保守性、および将来的な拡張性を向上させる狙いがありました。これにより、関連するロジックが一箇所にまとまり、コンパイラの他の部分への影響を最小限に抑えつつ開発を進めることが可能になります。
  3. コンパイラの構造的改善: Goコンパイラは、ソースコードをASTに変換し、それを最適化し、最終的に機械語に変換するという複数のフェーズを経て動作します。このコミットは、ASTのウォークフェーズにおけるswitch文の処理をより体系的に行うための構造的改善の一環です。

前提知識の解説

このコミットの変更内容を理解するためには、Go言語のコンパイラの基本的な構造と、switch文がどのように処理されるかについての知識が必要です。

Goコンパイラの構造(cmd/gccmd/6g

Go言語の初期のコンパイラは、主にC言語で書かれており、cmd/gccmd/6g(または8g, 5gなど、ターゲットアーキテクチャに応じたもの)というディレクトリに分かれていました。

  • cmd/gc: これはGoコンパイラのフロントエンドとミドルエンドに相当する部分です。Goのソースコードを解析し、抽象構文木(AST)を構築し、型チェック、最適化、および一部のコード変換(ウォーク処理)を行います。この段階では、まだ特定のCPUアーキテクチャに依存しない中間表現が生成されます。
  • cmd/6g: これはGoコンパイラのバックエンドに相当する部分で、AMD64(x86-64)アーキテクチャ向けのコード生成を担当します。gcによって生成された中間表現を受け取り、具体的な機械語命令に変換します。

このコミットでは、gcsrc/cmd/gc/以下のファイル)と6gsrc/cmd/6g/以下のファイル)の両方に変更が加えられており、これはコンパイラの異なるフェーズにわたるswitch文処理の再構築を示しています。

switch文の基本的な動作とコンパイル時の処理

Go言語のswitch文は、他の多くの言語と同様に、与えられた式の値に基づいて異なるコードブロックを実行するための制御構造です。コンパイラは、switch文を効率的な分岐命令のシーケンスに変換する必要があります。

コンパイル時、switch文は通常、以下のようなステップで処理されます。

  1. AST構築: ソースコードのswitch文は、コンパイラ内部でNodeと呼ばれる抽象構文木のノードとして表現されます。
  2. ウォーク処理(walkフェーズ): ASTを走査し、型チェック、定数畳み込み、一部の最適化、および高レベルな構文の低レベルな中間表現への変換を行います。このフェーズで、switch文はより単純なif-else if-elseの連鎖や、ジャンプテーブル(値が連続している場合)のような構造に変換されることがあります。
  3. コード生成(genフェーズ): 中間表現をターゲットアーキテクチャの機械語命令に変換します。

type switchとは何か、その構文と目的

type switchは、Go言語の強力な機能の一つで、インターフェース型の変数が実行時に保持している具体的な型に基づいて異なる処理を行うための構文です。

構文例:

package main

import "fmt"

func describe(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Integer: %d\n", v)
	case string:
		fmt.Printf("String: %q\n", v)
	default:
		fmt.Printf("Unknown type: %T\n", v)
	}
}

func main() {
	describe(10)
	describe("hello")
	describe(true)
}

目的:

  • 動的な型チェック: インターフェースが持つ具体的な型を安全に判別し、その型に応じた処理を実行できます。
  • ポリモーフィズムの実現: 異なる型に対して共通のインターフェースを介して操作を行い、実行時に適切な振る舞いを選択できます。
  • コードの簡潔化: 複数のif-else if文でv, ok := i.(Type)のような型アサーションを繰り返すよりも、type switchを使用することでコードがより簡潔で読みやすくなります。

このコミットは、このようなtype switchの複雑な型判別ロジックを効率的にコンパイルするための基盤を整備しています。

AST (Abstract Syntax Tree) と walk フェーズ、gen フェーズの役割

  • AST (Abstract Syntax Tree): ソースコードの構文構造を木構造で表現したものです。コンパイラはソースコードを字句解析・構文解析してASTを構築し、その後の処理の基盤とします。各ノードは、変数宣言、関数呼び出し、演算子、制御構造(if, for, switchなど)といったプログラムの要素を表します。
  • walk フェーズ: ASTを深さ優先探索などで走査し、各ノードに対して意味解析、型チェック、一部の最適化、および高レベルな構文の低レベルな中間表現への変換を行います。例えば、rangeループが通常のforループに変換されたり、switch文がif-gotoのシーケンスに変換されたりします。このフェーズはsrc/cmd/gc/walk.cなどで実装されます。
  • gen フェーズ: walkフェーズで変換された中間表現を受け取り、最終的な機械語コードを生成します。このフェーズはsrc/cmd/6g/gen.cなどで実装されます。

このコミットは、特にwalkフェーズにおけるswitch文の処理ロジックを大きく変更し、その結果がgenフェーズに渡される形を再定義しています。

Node構造体とOp(操作コード)の概念

GoコンパイラのASTは、NodeというC言語の構造体で表現されます。各Nodeは、その種類を示すopフィールド(操作コード)を持ちます。例えば、OSWITCHswitch文を表し、OCASEcase節を表します。

  • Node構造体: ASTの各要素を表す汎用的な構造体。opleftrightnbodyntestなどのフィールドを持ち、それぞれが異なる種類のノードやその子ノード、関連する式やステートメントリストを指します。
  • Op(操作コード): Nodeopフィールドに格納される列挙型で、そのノードが表す構文要素の種類を識別します。例えば、OFORforループ、OEQは等価比較演算子を表します。

このコミットでは、OSWITCHノードの内部処理が大きく変更され、OCASEノードの解釈も新しいswt.c内のロジックによって行われるようになります。

Case構造体と従来のスイッチ処理の概要

このコミット以前のswitch文の処理では、src/cmd/6g/gg.hに定義されていたCase構造体が使用されていました。この構造体は、各case節の式(scase)と、そのcase節に対応するコードの開始位置(sprog)を保持していました。

従来のswgen関数(src/cmd/6g/gen.cに存在)は、このCase構造体のリストをソートし(csort)、定数ケースに対しては二分探索のような効率的な分岐ロジック(swconst)を生成していました。しかし、このアプローチは、特にtype switchのような複雑なケースを扱うには限界がありました。

このコミットでは、このCase構造体と関連するヘルパー関数が廃止され、より汎用的なASTノードの操作と、if-goto変換に基づく新しいアプローチが採用されています。

技術的詳細

このコミットの核心は、Goコンパイラにおけるswitch文の処理ロジックを、よりモジュール化され、将来のtype switchに対応可能な形に再構築した点にあります。

主要な変更点

  1. swt.cの新規導入と役割:

    • src/cmd/gc/swt.cという新しいファイルが追加されました。このファイルは、switch文のASTウォーク処理と、それを低レベルなif-gotoシーケンスに変換するロジックを専門に担当します。
    • これにより、switch文に関するすべての複雑な処理がこのファイルに集約され、コンパイラの他の部分(gen.c, walk.cなど)からは、より抽象化されたインターフェースを通じてswitch処理を呼び出す形になります。
  2. src/cmd/6g/gen.csrc/cmd/gc/walk.cからのスイッチ関連コードの削除と移動:

    • src/cmd/6g/gen.cから、swgen関数(従来のスイッチコード生成ロジック)と、csort, casecmp, swconstといったCase構造体に関連するヘルパー関数が完全に削除されました。
    • src/cmd/gc/walk.cからも、walkswitch関数(従来のスイッチウォーク処理)と、sw1, sw2, sw3といった型関連のヘルパー関数の宣言が削除されました。
    • これらの削除されたロジックの多くは、src/cmd/gc/swt.cに再実装または新しい形で統合されています。
  3. Case構造体と関連ヘルパー関数の廃止:

    • src/cmd/6g/gg.hから、Case構造体の定義と、それに関連するCマクロが削除されました。これは、従来のCase構造体に基づくスイッチ処理モデルが完全に置き換えられたことを意味します。
    • csort, casecmp, swconstといった、Case構造体のリストを操作していた関数も廃止されました。
  4. walkswitch関数の再設計と一元化:

    • walkswitch関数は、src/cmd/gc/walk.cからsrc/cmd/gc/swt.cに移動し、そのシグネチャも変更されました(Type* walkswitch(Node*, Type*(*)(Node*, Type*))からvoid walkswitch(Node*)へ)。
    • 新しいwalkswitchは、switch文全体の処理を統括するメインエントリポイントとなり、内部でcasebodywalkcasesprepswといった新しいヘルパー関数を呼び出すことで、より構造化された処理フローを実現しています。
  5. selectas関数の拡張(mapinterfaceのサポート):

    • src/cmd/gc/walk.cselectas関数が変更され、ORECV(チャネル受信)だけでなく、OINDEX(マップのインデックスアクセス)とODOTTYPE(インターフェースの型アサーション)も処理できるようになりました。
    • これは、select文のcase節や、将来のtype switchにおいて、マップの要素アクセスやインターフェースの型アサーションをcaseとして扱えるようにするための重要な変更です。特にtype switchでは、interface{}.(type)という構文がODOTTYPEとして表現されるため、この拡張は不可欠です。

swt.c内の新関数

src/cmd/gc/swt.cには、switch文の処理を効率的かつ柔軟に行うための新しい関数群が導入されています。

  • sw0, sw1, sw2, sw3: これらはwalkcases関数に渡されるコールバック関数で、switch文の型推論と型互換性チェックの異なるパスを表現します。

    • sw0: case節内の式(特にv := exprのような代入)のウォーク処理を行い、型チェックを行います。
    • sw1: switch式の型が不明な場合に、最初のcase節の型をswitch式の型として提案します。
    • sw2: switch式の型がまだ決定できない場合に、デフォルトとしてint型を提案します(これは「botch」とコメントされており、一時的な措置の可能性が高い)。
    • sw3: switch式の型が決定された後、各case節の式がその型と互換性があるかをチェックし、必要に応じて型変換を行います。
  • walkcases(Node *sw, Type*(*call)(Node*, Type*)): この関数は、switch文のすべてのcase節を走査し、引数として渡されたコールバック関数callを各case節の式に適用します。これにより、型推論や型チェックのロジックを抽象化し、複数のパスで再利用できるようになります。

  • newlabel(): 新しいユニークなラベル名を生成するためのヘルパー関数です。switch文がif-gotoのシーケンスに変換される際に、ジャンプ先として使用されるラベルを生成します。

  • casebody(Node *sw): switch文のボディ(case節とステートメントのリスト)を解析し、それをコンパイラが扱いやすい形式に再構築します。

    • OXCASE(一時的な内部表現)をOCASE(実際のcase節)に変換します。
    • fallthroughOFALL)の処理を適切に行います。
    • デフォルトケース(default)の処理を管理し、複数のデフォルトケースがないことを保証します。
    • case節に対応するジャンプ先ラベル(OGOTOノード)を生成し、ステートメントリストに挿入します。
    • 最終的に、switch文のボディを、case節のリストとステートメントのリストという2つの独立したリストに分離します。
  • prepsw(Node *sw): casebodyによって整理されたswitch文の構造を受け取り、それを実際のif-gotoのシーケンスに変換します。

    • switch式が定数ブール値(trueまたはfalse)である場合に、特別な最適化を行います。
    • case節をif文に変換し、switch式とcase式の比較(OEQ)を行います。
    • if文の条件が真の場合に、対応するcaseボディのラベルへジャンプするOGOTOノードを生成します。
    • これにより、高レベルなswitch文が、コンパイラのバックエンドが処理しやすい低レベルな分岐命令のシーケンスに変換されます。
  • walkswitch(Node *n): src/cmd/gc/swt.cにおけるswitch文処理のメインエントリポイントです。

    1. casebody(n)を呼び出し、switch文のボディを整理します。
    2. n->ntestswitch式のノード)がN(nil)の場合、booltrueswitch trueに相当)を設定します。
    3. n->ninitswitch文の初期化ステートメント)とn->ntestn->nbodywalkstateおよびwalktypeでウォークします。
    4. walkcases(n, sw0)を呼び出し、case節内の代入などの初期ウォーク処理を行います。
    5. switch式の型を決定するために、sw1sw2sw3を順にwalkcasesに渡して呼び出します。これにより、case節の型からswitch式の型を推論し、互換性をチェックします。
    6. prepsw(n)を呼び出し、switch文をif-gotoのシーケンスに変換します。

AST変換

このコミットの重要な側面は、コンパイラがswitch文をどのようにASTレベルで変換するかです。

従来のswitch文は、swgen関数によって直接コード生成フェーズで処理されていましたが、新しい実装では、walkフェーズのswt.c内で、switch文のASTがより低レベルなif-gotoのシーケンスに変換されます。

例えば、以下のようなGoのswitch文があったとします。

switch x {
case 1:
    // do something
case 2:
    // do something else
default:
    // default action
}

これがprepsw関数によって、概念的には以下のようなif-gotoのシーケンスに変換されます(実際のASTノードはより複雑ですが、ロジックはこれに近いです)。

// (conceptually)
temp_var = x // if x is not a constant boolean

if temp_var == 1 {
    goto label_case_1
}
if temp_var == 2 {
    goto label_case_2
}
goto label_default

label_case_1:
    // do something
    goto label_break

label_case_2:
    // do something else
    goto label_break

label_default:
    // default action

label_break:
    // end of switch

この変換により、コンパイラのバックエンドは、より単純なif文とgoto命令の組み合わせとしてswitch文を扱うことができ、コード生成が容易になります。また、type switchのような複雑な条件分岐も、この汎用的なif-goto変換の枠組みの中で処理できるようになります。

型スイッチへの影響

このリファクタリングは、type switchの実現に不可欠な基盤を構築しました。

  • selectasの拡張: selectas関数がODOTTYPE(インターフェースの型アサーション)を処理できるようになったことで、switch x.(type)構文のx.(type)部分がASTで適切に表現され、その後の処理に引き渡せるようになりました。
  • swt.cの型処理パス: swt.c内のsw0, sw1, sw2, sw3といった型関連のヘルパー関数とwalkcasesの導入により、switch式の型と各case節の型の互換性をチェックし、必要に応じて型変換を行うための柔軟なメカニズムが提供されました。これにより、type switchにおける複雑な型マッチングロジックをコンパイラが処理できるようになります。
  • 汎用的なif-goto変換: prepsw関数によるif-gotoへの変換は、type switchの各case節(例: case int:, case string:)が、実行時の型チェックとそれに続く分岐として表現されることを可能にします。

このコミット自体はtype switchの完全な機能実装ではありませんが、その後の開発でtype switchがGo言語に導入されるための、非常に重要な土台を築いたと言えます。

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

このコミットにおけるコアとなるコードの変更箇所は以下のファイルと関数に集中しています。

  1. src/cmd/6g/gen.c:

    • OSWITCHの処理が大幅に簡素化されました。従来のswgen(n)の呼び出しが削除され、代わりにgen(n->nbody, L)が呼び出されています。これは、switch文の具体的なコード生成ロジックがgen.cからswt.cに移動したことを示します。
    • csort, casecmp, swconst, swgenといった関数が完全に削除されています。
    --- a/src/cmd/6g/gen.c
    +++ b/src/cmd/6g/gen.c
    @@ -299,15 +299,15 @@ loop:
     		break;
    
     	case OSWITCH:
    -		p1 = gbranch(AJMP, T);			// 	goto test
     		sbreak = breakpc;
    +		p1 = gbranch(AJMP, T);			// 	goto test
     		breakpc = gbranch(AJMP, T);		// break:	goto done
     		patch(p1, pc);				// test:
     		if(labloop != L) {
     			labloop->op = OFOR;
     			labloop->breakpc = breakpc;
     		}
    -		swgen(n);				//	switch(test) body
    +		gen(n->nbody, L);			//	switch(test) body
     		patch(breakpc, pc);			// done:
     		breakpc = sbreak;
     		break;
    
  2. src/cmd/6g/gg.h:

    • Case構造体の定義と、swgen, selgen関数の宣言が削除されています。
    --- a/src/cmd/6g/gg.h
    +++ b/src/cmd/6g/gg.h
    @@ -64,15 +64,6 @@ struct Sig
      	Sig*	link;
      };
    
    -typedef	struct	Case Case;
    -struct	Case
    -{
    -	Prog*	sprog;
    -	Node*	scase;
    -	Case*	slink;
    -};
    -#define	C	((Case*)0)
    -
     typedef	struct	Pool Pool;
     struct	Pool
     {
    @@ -143,8 +134,6 @@ EXTERN	int	sizeof_Array;	// runtime sizeof(Array)
     void	compile(Node*);
     void	proglist(void);
     void	gen(Node*, Label*);
    -void	swgen(Node*);
    -void	selgen(Node*);
     Node*	lookdot(Node*, Node*, int);
     void	inarggen(void);
     void	cgen_as(Node*, Node*);
    
  3. src/cmd/gc/Makefile:

    • swt.oOFILESに追加され、新しいswt.cファイルがコンパイル対象になったことを示します。
    --- a/src/cmd/gc/Makefile
    +++ b/src/cmd/gc/Makefile
    @@ -21,6 +21,7 @@ OFILES=\
      	dcl.$O\
      	export.$O\
      	walk.$O\
    +	swt.$O\
      	const.$O\
      	mparith1.$O\
      	mparith2.$O\
    
  4. src/cmd/gc/go.h:

    • walkswitch関数のシグネチャが変更され、casebody関数の宣言が削除されています。
    --- a/src/cmd/gc/go.h
    +++ b/src/cmd/gc/go.h
    @@ -812,8 +812,7 @@ void	walktype(Node*, int);
     void	walkconv(Node*);
     void	walkas(Node*);
     void	walkbool(Node*);
    -Type*	walkswitch(Node*, Type*(*)(Node*, Type*));
    -int	casebody(Node*);
    +void	walkswitch(Node*);
     void	walkselect(Node*);
     int	whatis(Node*);
     void	walkdot(Node*);
    
  5. src/cmd/gc/swt.c:

    • 新規ファイルとして追加され、switch文の新しい実装の大部分が含まれています。
    • 特に、walkswitch, casebody, prepsw, walkcases関数がこのファイルの核となります。
  6. src/cmd/gc/walk.c:

    • OSWITCHの処理がwalkswitch(n)の呼び出しに置き換えられ、従来の複雑なロジックが削除されています。
    • sw1, sw2, sw3といった静的関数の宣言と実装が削除されています。
    • selectas関数が拡張され、OINDEXODOTTYPEのケースが追加されています。
    --- a/src/cmd/gc/walk.c
    +++ b/src/cmd/gc/walk.c
    @@ -318,26 +315,7 @@ loop:
      	\tif(top != Etop)\
      	\t\tgoto nottop;\
      \
    -\t\tcasebody(n->nbody);\
    -\t\tif(n->ntest == N)\
    -\t\t\tn->ntest = booltrue;\
    -\t\twalkstate(n->ninit);\
    -\t\twalktype(n->ntest, Erv);\
    -\t\twalkstate(n->nbody);\
    -\
    -\t\t// find common type\
    -\t\tif(n->ntest->type == T)\
    -\t\t\tn->ntest->type = walkswitch(n, sw1);\
    -\
    -\t\t// if that fails pick a type\
    -\t\tif(n->ntest->type == T)\
    -\t\t\tn->ntest->type = walkswitch(n, sw2);\
    -\
    -\t\t// set the type on all literals\
    -\t\tif(n->ntest->type != T)\
    -\t\t\twalkswitch(n, sw3);\
    -\t\twalktype(n->ntest, Erv);\t// BOTCH is this right
    -\t\twalktype(n->nincr, Erv);\
    +\t\twalkswitch(n);\
      \t\tgoto ret;\
      \
      \tcase OSELECT:\
    @@ -1477,21 +1341,58 @@ out:\
      \treturn r;\
      }\
      \
    +/*\
    + * enumerate the special cases\
    + * of the case statement:\
    + *\tcase v := <-chan\t\t// select and switch\
    + *\tcase v := map[]\t\t\t// switch\
    + *\tcase v := interface.(TYPE)\t// switch\
    + */\
     Node*\
     selectas(Node *name, Node *expr)\
     {\
      \tNode *a;\
      \tType *t;\
      \
    -\tif(expr == N || expr->op != ORECV)\
    -\t\tgoto bad;\
    -\twalktype(expr->left, Erv);\
    -\tt = expr->left->type;\
    -\tif(t == T)\
    +\tif(expr == N)\
      \t\tgoto bad;\
    -\tif(t->etype != TCHAN)\
    +\tswitch(expr->op) {\
    +\tdefault:\
    +//dump(\"case\", expr);\
      \t\tgoto bad;\
    -\ta = old2new(name, t->type);\
    +\
    +\tcase ORECV:\
    +\t\twalktype(expr->left, Erv);\
    +\t\tt = expr->left->type;\
    +\t\tif(t == T)\
    +\t\t\tgoto bad;\
    +\t\tif(t->etype != TCHAN)\
    +\t\t\tgoto bad;\
    +\t\tt = t->type;\
    +\t\tbreak;\
    +\
    +\tcase OINDEX:\
    +\t\twalktype(expr->left, Erv);\
    +\t\twalktype(expr->right, Erv);\
    +\t\tt = expr->left->type;\
    +\t\tif(t == T)\
    +\t\t\tgoto bad;\
    +\t\tif(t->etype != TMAP)\
    +\t\t\tgoto bad;\
    +\t\tt = t->type;\
    +\t\tbreak;\
    +\
    +\tcase ODOTTYPE:\
    +\t\twalktype(expr->left, Erv);\
    +\t\tt = expr->left->type;\
    +\t\tif(t == T)\
    +\t\t\tgoto bad;\
    +\t\tif(t->etype != TINTER)\
    +\t\t\tgoto bad;\
    +\t\tt = expr->type;\
    +\t\tbreak;\
    +\t}\
    +\ta = old2new(name, t);\
      \treturn a;\
      \
     bad:\
    

コアとなるコードの解説

src/cmd/6g/gen.c の変更

従来のsrc/cmd/6g/gen.cでは、OSWITCHswitch文を表すASTノード)の処理はswgen(n)という関数に委譲されていました。このswgen関数が、Case構造体を使って各case節を処理し、最終的な機械語コードを生成する役割を担っていました。

このコミットでは、swgen(n)の呼び出しが削除され、代わりにgen(n->nbody, L)が呼び出されています。これは、switch文のコード生成ロジックがgen.cから完全に切り離され、n->nbodyswitch文のボディを表すASTノード)が直接gen関数に渡されるようになったことを意味します。gen関数は、より低レベルなASTノード(この場合はswt.cによって変換されたif-gotoのシーケンス)を処理する汎用的なコード生成関数です。

この変更により、gen.cswitch文の内部構造を知る必要がなくなり、swt.cが生成した中間表現をそのままコードに変換する役割に徹するようになります。これは、コンパイラのモジュール性を高める上で非常に重要な変更です。

src/cmd/gc/swt.c の新規追加と主要関数

src/cmd/gc/swt.cは、このコミットの最も重要な部分であり、switch文の新しい処理ロジックのほとんどがここに集約されています。

walkswitch(Node *n)

この関数は、swt.cにおけるswitch文処理のメインエントリポイントです。src/cmd/gc/walk.cから呼び出されます。

void
walkswitch(Node *n)
{
	Type *t;

	casebody(n); // 1. switch文のボディを整理
	if(n->ntest == N)
		n->ntest = booltrue; // 2. switch式がない場合は `switch true` とみなす

	walkstate(n->ninit); // 3. 初期化ステートメントをウォーク
	walktype(n->ntest, Erv); // 4. switch式の型をウォーク
	walkstate(n->nbody); // 5. switchボディをウォーク

	// walktype
	walkcases(n, sw0); // 6. 各case節の式をウォークし、型チェックを行う

	// find common type
	t = n->ntest->type;
	if(t == T)
		t = walkcases(n, sw1); // 7. switch式の型が不明なら、sw1で推論

	// if that fails pick a type
	if(t == T)
		t = walkcases(n, sw2); // 8. それでも不明なら、sw2でデフォルト型を提案

	// set the type on all literals
	if(t != T) {
		walkcases(n, sw3); // 9. 型が決定したら、sw3で各case節の型を調整
		convlit(n->ntest, t); // 10. switch式の型を確定
		prepsw(n); // 11. switch文をif-gotoシーケンスに変換
	}
}

この関数は、switch文のASTノードnを受け取り、以下の主要なステップを実行します。

  1. casebody(n): switch文のボディを解析し、case節とステートメントのリストを整理します。このステップで、fallthroughの処理や、各case節へのジャンプ先ラベルの生成が行われます。
  2. n->ntest = booltrue: switch式が省略されている場合(例: switch { ... })、これはswitch true { ... }と等価であるため、n->ntestbooltrueノードを設定します。
  3. walkstate(n->ninit): switch文の初期化ステートメント(例: switch x := f(); x { ... }x := f()部分)をウォークします。
  4. walktype(n->ntest, Erv): switch式の型を決定するためにウォークします。
  5. walkstate(n->nbody): switchボディ全体をウォークします。
  6. walkcases(n, sw0): sw0コールバックを使って、各case節の式(特にv := exprのような代入)をウォークし、型チェックを行います。
  7. 型推論と型チェック (sw1, sw2, sw3):
    • sw1は、switch式の型がまだ決定されていない場合に、最初のcase節の型をswitch式の型として提案します。
    • sw2は、それでも型が決定できない場合に、デフォルトとしてint型を提案します(これは一時的な措置)。
    • sw3は、switch式の型が最終的に決定された後、各case節の式がその型と互換性があるかをチェックし、必要に応じて型変換を行います。
  8. prepsw(n): switch文のASTを、最終的なコード生成のために、より低レベルなif-gotoのシーケンスに変換します。

casebody(Node *sw)

この関数は、switch文のボディを構造化し、case節とステートメントのリストを分離します。

void
casebody(Node *sw)
{
	Iter save;
	Node *os, *oc, *t, *c;
	Node *cas, *stat, *def;
	Node *go, *br;
	int32 lno;

	lno = setlineno(sw);
	t = listfirst(&save, &sw->nbody);
	if(t == N || t->op == OEMPTY) {
		sw->nbody = nod(OLIST, N, N);
		return;
	}

	cas = N;	// cases
	stat = N;	// statements
	def = N;	// defaults
	os = N;		// last statement
	oc = N;		// last case
	br = nod(OBREAK, N, N); // break文を表すノード

loop:

	if(t == N) {
		if(oc == N && os != N)
			yyerror("first switch statement must be a case");

		stat = list(stat, br); // 最後にbreakを追加
		cas = list(cas, def); // デフォルトケースをケースリストに追加

		sw->nbody = nod(OLIST, rev(cas), rev(stat)); // ケースとステートメントを分離して設定
		lineno = lno;
		return;
	}

	lno = setlineno(t);

	switch(t->op) {
	case OXCASE: // 従来のcase節の内部表現
		t->op = OCASE; // OCASEに変換
		if(oc == N && os != N)
			yyerror("first switch statement must be a case");

		if(os != N && os->op == OXFALL)
			os->op = OFALL; // fallthroughの処理
		else
			stat = list(stat, br); // 各case節の終わりにbreakを追加

		go = nod(OGOTO, newlabel(), N); // 各case節のボディへのジャンプ先ラベルを生成

		c = t->left;
		if(c == N) { // defaultケースの場合
			if(def != N)
				yyerror("more than one default case");

			t->right = go;
			def = t;
		}

		// 複数の値を持つcase節を展開
		for(; c!=N; c=c->right) {
			if(c->op != OLIST) {
				t->left = c;
				t->right = go;
				cas = list(cas, t);
				break;
			}
			cas = list(cas, nod(OCASE, c->left, go));
		}
		stat = list(stat, nod(OLABEL, go->left, N)); // ラベルをステートメントリストに追加
		oc = t;
		os = N;
		break;

	default: // 通常のステートメント
		stat = list(stat, t);
		os = t;
		break;
	}
	t = listnext(&save);
	goto loop;
}

この関数は、switch文のボディを走査し、OXCASEノードをOCASEに変換したり、fallthroughを適切に処理したり、各case節の終わりに暗黙のbreakを追加したりします。また、各case節のボディへのジャンプ先となるOGOTOノードとOLABELノードを生成し、最終的にswitch文のボディをcase節のリストとステートメントのリストに分離します。これにより、prepsw関数がif-goto変換を行うための準備が整います。

prepsw(Node *sw)

この関数は、casebodyによって整理されたswitch文のASTを、実際のif-gotoのシーケンスに変換します。

void
prepsw(Node *sw)
{
	Iter save;
	Node *name, *cas;
	Node *t, *a;
	int bool;

	bool = 0;
	if(whatis(sw->ntest) == Wlitbool) { // switch式が定数ブール値の場合
		bool = 1;		// true
		if(sw->ntest->val.u.xval == 0)
			bool = 2;	// false
	}

	cas = N;
	name = N;
	if(bool == 0) { // switch式が定数ブール値でない場合
		name = nod(OXXX, N, N);
		tempname(name, sw->ntest->type); // switch式の値を一時変数に格納
		cas = nod(OAS, name, sw->ntest); // 一時変数への代入ノード
	}

	t = listfirst(&save, &sw->nbody->left); // case節のリストを取得

loop:
	if(t == N) {
		sw->nbody->left = rev(cas); // 変換されたif-gotoシーケンスをswitchボディに設定
		walkstate(sw->nbody->left);
		return;
	}

	if(t->left == N) { // defaultケースの場合
		cas = list(cas, t->right); // goto default
		t = listnext(&save);
		goto loop;
	}

	a = nod(OIF, N, N);
	a->nbody = t->right; // then goto l (caseボディへのジャンプ)

	switch(bool) {
	default: // switch式が定数ブール値でない場合
		a->ntest = nod(OEQ, name, t->left); // if name == val
		break;

	case 1: // switch trueの場合
		a->ntest = t->left; // if val
		break;

	case 2: // switch falseの場合
		a->ntest = nod(ONOT, t->left, N); // if !val
		break;
	}
	cas = list(cas, a); // ifノードをケースリストに追加

	t = listnext(&save);
	goto loop;
}

この関数は、casebodyによって生成されたcase節のリストを走査し、各case節を対応するif文とgoto命令の組み合わせに変換します。switch式が定数ブール値である場合は、より最適化されたif条件を生成します。この変換により、switch文はコンパイラのバックエンドが処理しやすい低レベルな分岐構造になります。

src/cmd/gc/walk.cselectas 関数の変更

src/cmd/gc/walk.cselectas関数は、select文のcase節や、switch文のcase節で、変数への代入を伴う特殊な形式(例: case v := <-ch:)を処理します。

このコミットでは、selectas関数が拡張され、ORECV(チャネル受信)だけでなく、OINDEX(マップのインデックスアクセス)とODOTTYPE(インターフェースの型アサーション)も処理できるようになりました。

Node*
selectas(Node *name, Node *expr)
{
	Node *a;
	Type *t;

	if(expr == N)
		goto bad;
	switch(expr->op) {
	default:
		goto bad;

	case ORECV: // チャネル受信
		walktype(expr->left, Erv);
		t = expr->left->type;
		if(t == T)
			goto bad;
		if(t->etype != TCHAN)
			goto bad;
		t = t->type;
		break;

	case OINDEX: // マップのインデックスアクセス (map[e])
		walktype(expr->left, Erv);
		walktype(expr->right, Erv);
		t = expr->left->type;
		if(t == T)
			goto bad;
		if(t->etype != TMAP)
			goto bad;
		t = t->type;
		break;

	case ODOTTYPE: // インターフェースの型アサーション (interface.(type))
		walktype(expr->left, Erv);
		t = expr->left->type;
		if(t == T)
			goto bad;
		if(t->etype != TINTER)
			goto bad;
		t = expr->type; // アサーション後の型
		break;
	}
	a = old2new(name, t); // 新しい変数ノードを生成
	return a;

bad:
	yyerror("inappropriate assignment in a case statement");
	return N;
}

この変更は、type switchの実現に不可欠です。switch x.(type)という構文は、内部的にはODOTTYPEノードとして表現されます。selectasODOTTYPEを処理できるようになることで、type switchcase節で型アサーションを伴う代入(例: case v := int:)を適切に扱うことが可能になります。

関連リンク

参考にした情報源リンク

  • Go言語のコンパイラに関する一般的な情報源(Goのコンパイラがどのように動作するかを理解するのに役立ちます):
  • Goコンパイラの内部構造に関する議論や資料(当時のコンパイラの設計思想を理解するのに役立ちます):
    • Goの初期のコミット履歴やメーリングリストのアーカイブ(一般には公開されていないが、当時の開発状況を推測するのに役立つ)
    • Goのコンパイラに関する学術論文や技術ブログ記事(後世に書かれたものも、当時の設計意図を解説している場合がある)

(注: 2009年当時のGoコンパイラの詳細な内部資料は一般に公開されているものが少ないため、主にコミット内容とGo言語の仕様から推測しています。)