[インデックス 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
文の処理をより柔軟かつ効率的に行うための基盤が必要でした。
このコミットの主な背景は以下の点に集約されます。
type switch
の導入準備: Go言語の強力な機能の一つであるswitch x.(type)
構文(型スイッチ)を実装するためには、コンパイラが異なる型のケースを効率的に処理できるようなswitch
文の内部構造が必要でした。このコミットは、そのための土台作りを目的としています。- コードのモジュール化と保守性の向上:
switch
文の処理ロジックをswt.c
という新しい専用ファイルに集約することで、コードの可読性、保守性、および将来的な拡張性を向上させる狙いがありました。これにより、関連するロジックが一箇所にまとまり、コンパイラの他の部分への影響を最小限に抑えつつ開発を進めることが可能になります。 - コンパイラの構造的改善: Goコンパイラは、ソースコードをASTに変換し、それを最適化し、最終的に機械語に変換するという複数のフェーズを経て動作します。このコミットは、ASTのウォークフェーズにおける
switch
文の処理をより体系的に行うための構造的改善の一環です。
前提知識の解説
このコミットの変更内容を理解するためには、Go言語のコンパイラの基本的な構造と、switch
文がどのように処理されるかについての知識が必要です。
Goコンパイラの構造(cmd/gc
とcmd/6g
)
Go言語の初期のコンパイラは、主にC言語で書かれており、cmd/gc
とcmd/6g
(または8g
, 5g
など、ターゲットアーキテクチャに応じたもの)というディレクトリに分かれていました。
cmd/gc
: これはGoコンパイラのフロントエンドとミドルエンドに相当する部分です。Goのソースコードを解析し、抽象構文木(AST)を構築し、型チェック、最適化、および一部のコード変換(ウォーク処理)を行います。この段階では、まだ特定のCPUアーキテクチャに依存しない中間表現が生成されます。cmd/6g
: これはGoコンパイラのバックエンドに相当する部分で、AMD64(x86-64)アーキテクチャ向けのコード生成を担当します。gc
によって生成された中間表現を受け取り、具体的な機械語命令に変換します。
このコミットでは、gc
(src/cmd/gc/
以下のファイル)と6g
(src/cmd/6g/
以下のファイル)の両方に変更が加えられており、これはコンパイラの異なるフェーズにわたるswitch
文処理の再構築を示しています。
switch
文の基本的な動作とコンパイル時の処理
Go言語のswitch
文は、他の多くの言語と同様に、与えられた式の値に基づいて異なるコードブロックを実行するための制御構造です。コンパイラは、switch
文を効率的な分岐命令のシーケンスに変換する必要があります。
コンパイル時、switch
文は通常、以下のようなステップで処理されます。
- AST構築: ソースコードの
switch
文は、コンパイラ内部でNode
と呼ばれる抽象構文木のノードとして表現されます。 - ウォーク処理(
walk
フェーズ): ASTを走査し、型チェック、定数畳み込み、一部の最適化、および高レベルな構文の低レベルな中間表現への変換を行います。このフェーズで、switch
文はより単純なif-else if-else
の連鎖や、ジャンプテーブル(値が連続している場合)のような構造に変換されることがあります。 - コード生成(
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
フィールド(操作コード)を持ちます。例えば、OSWITCH
はswitch
文を表し、OCASE
はcase
節を表します。
Node
構造体: ASTの各要素を表す汎用的な構造体。op
、left
、right
、nbody
、ntest
などのフィールドを持ち、それぞれが異なる種類のノードやその子ノード、関連する式やステートメントリストを指します。Op
(操作コード):Node
のop
フィールドに格納される列挙型で、そのノードが表す構文要素の種類を識別します。例えば、OFOR
はfor
ループ、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
に対応可能な形に再構築した点にあります。
主要な変更点
-
swt.c
の新規導入と役割:src/cmd/gc/swt.c
という新しいファイルが追加されました。このファイルは、switch
文のASTウォーク処理と、それを低レベルなif-goto
シーケンスに変換するロジックを専門に担当します。- これにより、
switch
文に関するすべての複雑な処理がこのファイルに集約され、コンパイラの他の部分(gen.c
,walk.c
など)からは、より抽象化されたインターフェースを通じてswitch
処理を呼び出す形になります。
-
src/cmd/6g/gen.c
とsrc/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
に再実装または新しい形で統合されています。
-
Case
構造体と関連ヘルパー関数の廃止:src/cmd/6g/gg.h
から、Case
構造体の定義と、それに関連するC
マクロが削除されました。これは、従来のCase
構造体に基づくスイッチ処理モデルが完全に置き換えられたことを意味します。csort
,casecmp
,swconst
といった、Case
構造体のリストを操作していた関数も廃止されました。
-
walkswitch
関数の再設計と一元化:walkswitch
関数は、src/cmd/gc/walk.c
からsrc/cmd/gc/swt.c
に移動し、そのシグネチャも変更されました(Type* walkswitch(Node*, Type*(*)(Node*, Type*))
からvoid walkswitch(Node*)
へ)。- 新しい
walkswitch
は、switch
文全体の処理を統括するメインエントリポイントとなり、内部でcasebody
、walkcases
、prepsw
といった新しいヘルパー関数を呼び出すことで、より構造化された処理フローを実現しています。
-
selectas
関数の拡張(map
とinterface
のサポート):src/cmd/gc/walk.c
のselectas
関数が変更され、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
節)に変換します。fallthrough
(OFALL
)の処理を適切に行います。- デフォルトケース(
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
文処理のメインエントリポイントです。casebody(n)
を呼び出し、switch
文のボディを整理します。n->ntest
(switch
式のノード)がN
(nil)の場合、booltrue
(switch true
に相当)を設定します。n->ninit
(switch
文の初期化ステートメント)とn->ntest
、n->nbody
をwalkstate
およびwalktype
でウォークします。walkcases(n, sw0)
を呼び出し、case
節内の代入などの初期ウォーク処理を行います。switch
式の型を決定するために、sw1
、sw2
、sw3
を順にwalkcases
に渡して呼び出します。これにより、case
節の型からswitch
式の型を推論し、互換性をチェックします。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言語に導入されるための、非常に重要な土台を築いたと言えます。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルと関数に集中しています。
-
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;
-
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*);
-
src/cmd/gc/Makefile
:swt.o
がOFILES
に追加され、新しい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\
-
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*);
-
src/cmd/gc/swt.c
:- 新規ファイルとして追加され、
switch
文の新しい実装の大部分が含まれています。 - 特に、
walkswitch
,casebody
,prepsw
,walkcases
関数がこのファイルの核となります。
- 新規ファイルとして追加され、
-
src/cmd/gc/walk.c
:OSWITCH
の処理がwalkswitch(n)
の呼び出しに置き換えられ、従来の複雑なロジックが削除されています。sw1
,sw2
,sw3
といった静的関数の宣言と実装が削除されています。selectas
関数が拡張され、OINDEX
とODOTTYPE
のケースが追加されています。
--- 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
では、OSWITCH
(switch
文を表すASTノード)の処理はswgen(n)
という関数に委譲されていました。このswgen
関数が、Case
構造体を使って各case
節を処理し、最終的な機械語コードを生成する役割を担っていました。
このコミットでは、swgen(n)
の呼び出しが削除され、代わりにgen(n->nbody, L)
が呼び出されています。これは、switch
文のコード生成ロジックがgen.c
から完全に切り離され、n->nbody
(switch
文のボディを表すASTノード)が直接gen
関数に渡されるようになったことを意味します。gen
関数は、より低レベルなASTノード(この場合はswt.c
によって変換されたif-goto
のシーケンス)を処理する汎用的なコード生成関数です。
この変更により、gen.c
はswitch
文の内部構造を知る必要がなくなり、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
を受け取り、以下の主要なステップを実行します。
casebody(n)
:switch
文のボディを解析し、case
節とステートメントのリストを整理します。このステップで、fallthrough
の処理や、各case
節へのジャンプ先ラベルの生成が行われます。n->ntest = booltrue
:switch
式が省略されている場合(例:switch { ... }
)、これはswitch true { ... }
と等価であるため、n->ntest
にbooltrue
ノードを設定します。walkstate(n->ninit)
:switch
文の初期化ステートメント(例:switch x := f(); x { ... }
のx := f()
部分)をウォークします。walktype(n->ntest, Erv)
:switch
式の型を決定するためにウォークします。walkstate(n->nbody)
:switch
ボディ全体をウォークします。walkcases(n, sw0)
:sw0
コールバックを使って、各case
節の式(特にv := expr
のような代入)をウォークし、型チェックを行います。- 型推論と型チェック (
sw1
,sw2
,sw3
):sw1
は、switch
式の型がまだ決定されていない場合に、最初のcase
節の型をswitch
式の型として提案します。sw2
は、それでも型が決定できない場合に、デフォルトとしてint
型を提案します(これは一時的な措置)。sw3
は、switch
式の型が最終的に決定された後、各case
節の式がその型と互換性があるかをチェックし、必要に応じて型変換を行います。
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.c
の selectas
関数の変更
src/cmd/gc/walk.c
のselectas
関数は、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
ノードとして表現されます。selectas
がODOTTYPE
を処理できるようになることで、type switch
のcase
節で型アサーションを伴う代入(例: case v := int:
)を適切に扱うことが可能になります。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語の
switch
文に関するドキュメント: https://go.dev/ref/spec#Switch_statements - Go言語の
type switch
に関するドキュメント: https://go.dev/ref/spec#Type_switches - Goコンパイラのソースコード(GitHub): https://github.com/golang/go
参考にした情報源リンク
- Go言語のコンパイラに関する一般的な情報源(Goのコンパイラがどのように動作するかを理解するのに役立ちます):
- "Go's Declaration Syntax" by Rob Pike: https://go.dev/blog/declaration-syntax (Goの構文解析に関する基本的な理解に役立つ)
- "The Go Programming Language Specification": https://go.dev/ref/spec (言語仕様はコンパイラの動作の基礎となる)
- Goコンパイラの内部構造に関する議論や資料(当時のコンパイラの設計思想を理解するのに役立ちます):
- Goの初期のコミット履歴やメーリングリストのアーカイブ(一般には公開されていないが、当時の開発状況を推測するのに役立つ)
- Goのコンパイラに関する学術論文や技術ブログ記事(後世に書かれたものも、当時の設計意図を解説している場合がある)
(注: 2009年当時のGoコンパイラの詳細な内部資料は一般に公開されているものが少ないため、主にコミット内容とGo言語の仕様から推測しています。)