[インデックス 1628] ファイルの概要
このコミットは、Go言語の初期のコンパイラ/ツールチェインの一部であるpretty
ツールにおける、シンボル管理と型システムに関する重要なリファクタリングとクリーンアップを含んでいます。特に、抽象構文木(AST)からObject
、Type
、Scope
といった概念を分離し、symboltable
パッケージに集約することで、コンパイラの構造をより明確にし、役割分担を整理しています。また、スキャナーとパーサー間のトークンチャネル接続の削除や、依存関係計算の修正も行われています。
コミット
commit 187cf78a7c2fff4c1f606dacc412d5dda84f45b2
Author: Robert Griesemer <gri@golang.org>
Date: Fri Feb 6 11:10:25 2009 -0800
- preparation for setting up types
- moved Object, Type, Scope out of AST into symboltable
- moved universe into symboltable
- removed dead code
- fixed dependency computation (pretty -d filename.go)
- lots of cleanups
- removed tocken channel connection between parser and scanner
(was cute, but not really needed)
R=r
OCL=24545
CL=24545
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/187cf78a7c2fff4c1f606dacc412d5dda84f45b2
元コミット内容
- 型設定のための準備
Object
、Type
、Scope
をASTからsymboltable
へ移動universe
をsymboltable
へ移動- 不要なコードの削除
- 依存関係計算の修正 (
pretty -d filename.go
) - 多数のクリーンアップ
- パーサーとスキャナー間のトークンチャネル接続の削除(可愛かったが、実際には不要だった)
変更の背景
このコミットは、Go言語のコンパイラ(特にpretty
ツール、おそらく初期のGoコード整形ツールや解析ツール)の初期開発段階における重要な構造変更を示しています。当時のGoコンパイラはまだ発展途上にあり、コンポーネント間の責任分担や設計原則が固まっていく過程でした。
主な背景としては、以下の点が挙げられます。
- コンパイラ設計の洗練: コンパイラは通常、複数のフェーズ(字句解析、構文解析、意味解析、コード生成など)に分かれています。それぞれのフェーズが明確な役割を持つことで、コードの保守性、拡張性、理解しやすさが向上します。このコミットは、意味解析の基盤となるシンボル管理と型システムを、構文解析の結果であるASTから分離しようとする意図が見られます。
- ASTとシンボルテーブルの役割分担の明確化:
- AST (Abstract Syntax Tree): ソースコードの構文構造を木構造で表現したものです。ASTはプログラムの「形」を表現しますが、識別子が何を指すのか(変数、関数、型など)や、その型が何であるかといった「意味」の情報は直接持ちません。
- シンボルテーブル (Symbol Table): プログラム内で宣言された識別子(シンボル)に関する情報を格納するデータ構造です。これには、識別子の名前、種類(変数、関数、型など)、スコープ、型情報などが含まれます。シンボルテーブルは、意味解析フェーズで識別子の解決や型チェックを行うために不可欠です。
このコミット以前は、
Object
(識別子の情報)、Type
(型情報)、Scope
(スコープ情報)がASTパッケージ内に混在していたと考えられます。これは、ASTが構文情報だけでなく意味情報も持ちすぎていたことを意味し、単一責任の原則に反していました。これらをsymboltable
パッケージに移動することで、ASTは純粋な構文表現に徹し、シンボルテーブルが意味情報の管理を一手に担うという、よりクリーンな設計に移行しています。
universe
の分離:universe
(ユニバーススコープ)は、Go言語における組み込みの識別子(int
,string
,true
,false
,len
などの組み込み関数)が定義されている最上位のスコープです。これもASTパッケージ内にあったものをsymboltable
に移動することで、組み込みシンボルの管理もシンボルテーブルの責任範囲として明確化されています。- スキャナーとパーサー間の通信改善: 初期Goでは、スキャナー(字句解析器)とパーサー(構文解析器)がGoのチャネルを使って通信する設計が試みられていたようです。これはGoの並行処理機能の活用例として「可愛かった」と表現されていますが、コンパイラのフロントエンドにおいては、スキャナーがトークンを生成し、パーサーがそれを消費するという、より直接的で同期的なモデルの方がシンプルで効率的であると判断された可能性があります。チャネルによる通信はオーバーヘッドがあり、この文脈では不要と判断されたのでしょう。
- 依存関係計算の修正:
pretty -d filename.go
は、Goソースファイルの依存関係を計算する機能を示唆しています。コンパイラやビルドシステムにとって、ファイルの依存関係を正確に把握することは、効率的なコンパイルや再ビルドのために非常に重要です。この修正は、その依存関係計算ロジックの改善を目的としています。
これらの変更は、Goコンパイラの基盤をより堅牢でスケーラブルなものにするための、初期段階における重要なアーキテクチャ決定の一部と言えます。
前提知識の解説
このコミットを理解するためには、以下のコンピュータサイエンスおよびプログラミング言語の概念に関する知識が役立ちます。
-
コンパイラの基本構造:
- 字句解析 (Lexical Analysis / Scanning): ソースコードをトークン(意味を持つ最小単位、例: 識別子、キーワード、演算子、リテラル)のストリームに変換するフェーズ。担当は「スキャナー」または「字句解析器」。
- 構文解析 (Syntax Analysis / Parsing): トークンのストリームが言語の文法規則に従っているかを検証し、抽象構文木(AST)を構築するフェーズ。担当は「パーサー」または「構文解析器」。
- 意味解析 (Semantic Analysis): ASTを走査し、プログラムの意味的な正当性を検証するフェーズ。これには、型チェック、識別子の解決(どの宣言に対応するか)、スコープ規則の適用などが含まれます。このフェーズでシンボルテーブルが活用されます。
- 中間コード生成 (Intermediate Code Generation): 意味解析の結果を基に、機械独立な中間表現を生成するフェーズ。
- コード最適化 (Code Optimization): 中間コードをより効率的な形に変換するフェーズ。
- コード生成 (Code Generation): ターゲットマシンコードを生成するフェーズ。
-
抽象構文木 (Abstract Syntax Tree - AST): ソースコードの構文構造を抽象的に表現した木構造のデータ構造です。各ノードはソースコードの構成要素(式、文、宣言など)を表し、子ノードはそれらの構成要素の内部構造を表します。ASTは、コンパイラの構文解析フェーズで生成され、意味解析、最適化、コード生成などの後続フェーズで利用されます。
-
シンボルテーブル (Symbol Table): コンパイラがプログラム内の識別子(変数名、関数名、型名など)に関する情報を格納するために使用するデータ構造です。シンボルテーブルには、識別子の名前、その識別子の種類(変数、関数、型など)、データ型、スコープ(識別子が有効な範囲)、メモリ上のアドレスなどの情報が記録されます。意味解析フェーズで、識別子の参照がどの宣言に対応するかを解決したり、型チェックを行ったりするために不可欠です。
-
スコープ (Scope): プログラム内で識別子が有効な範囲を指します。Go言語には、ファイルスコープ、パッケージスコープ、ブロックスコープなどがあります。シンボルテーブルは通常、これらのスコープ階層を反映した構造(例: ネストされたハッシュマップやリンクリスト)を持ち、識別子の解決(名前解決)は現在のスコープから親スコープへと順に探索することで行われます。
-
型システム (Type System): プログラミング言語における、値や式の種類(型)を定義し、それらの型がどのように相互作用できるかを規定する規則の集合です。型システムは、プログラムの安全性(例: 整数と文字列の加算を防ぐ)と正確性(例: 関数が期待する型の引数を受け取ることを保証する)を保証する上で重要な役割を果たします。コンパイラは型チェックを通じて、プログラムが型システムの規則に違反していないかを確認します。
-
Go言語のチャネル (Channels): Go言語におけるゴルーチン間の通信メカニズムです。チャネルを通じて値を送受信することで、ゴルーチンは安全にデータを共有し、同期を取ることができます。このコミットでは、スキャナーとパーサー間の通信にチャネルが使用されていたが、後に削除されたことが示されており、初期のGoコンパイラ設計における並行処理の試みと、その後の設計変更の背景を理解する上で重要です。
技術的詳細
このコミットの技術的詳細は、主にGoコンパイラのフロントエンドにおけるデータ構造とコンポーネント間の相互作用の再構築に焦点を当てています。
1. Object
, Type
, Scope
の AST
から symboltable
への移動
- 変更前:
usr/gri/pretty/ast.go
にObject
、Type
、Scope
の定義が含まれていました。これは、構文解析の結果であるASTが、識別子の意味情報(種類、型、スコープ)まで直接管理していたことを意味します。 - 変更後:
usr/gri/pretty/symboltable.go
という新しいファイルが作成され、Object
、Type
、Scope
の定義がそこへ移動されました。Object
構造体は、識別子の種類 (Kind
)、名前 (Ident
)、ソースコード上の位置 (Pos
)、関連する型 (Typ
)、スコープレベル (Pnolev
) などの情報を持つようになりました。Type
構造体は、型の形式 (Form
)、サイズ (Size
)、関連するシンボルテーブルのスコープ (Scope
) などの情報を持つようになりました。Scope
構造体は、親スコープ (Parent
) と、識別子名からObject
へのマップ (entries
) を持つようになりました。LookupLocal
、Lookup
、Insert
などのメソッドを通じて、スコープ内での識別子の検索と挿入を管理します。
- 影響:
ast.go
は、純粋に構文構造を表現するASTノードの定義に特化しました。これにより、ASTの役割が明確になり、コードの理解と保守が容易になります。parser.go
やprinter.go
など、以前AST
パッケージのObject
やScope
を直接参照していた箇所は、新しく導入されたSymbolTable
パッケージを参照するように変更されました。例えば、parser.go
のtop_scope
フィールドの型が*AST.Scope
から*SymbolTable.Scope
に変更されています。AST.NewObject
やAST.NewScope
の呼び出しは、それぞれSymbolTable.NewObject
やSymbolTable.NewScope
に置き換えられました。
2. universe
の symboltable
への移動と universe.go
の削除
- 変更前:
usr/gri/pretty/universe.go
というファイルが存在し、Go言語の組み込み型や組み込み関数、定数(true
,false
,nil
など)が定義されている「ユニバーススコープ」を管理していました。このファイルはAST
パッケージのObject
やType
を利用していました。 - 変更後:
universe.go
ファイルが削除され、その内容はsymboltable.go
に統合されました。symboltable.go
内にUniverse
というグローバルなScope
変数が定義され、init()
関数内でGoの組み込み型、定数、組み込み関数がこのUniverse
スコープに登録されるようになりました。AST.Universe_void_typ
のようなASTパッケージ内のユニバース関連の参照も、SymbolTable
パッケージ内の対応する型に更新されました。
- 影響: ユニバーススコープの管理もシンボルテーブルの責任範囲として明確化され、関連するコードが一箇所に集約されました。
3. スキャナーとパーサー間のトークンチャネル接続の削除
- 変更前:
usr/gri/pretty/scanner.go
にToken
構造体とTokenStream()
メソッドが存在し、usr/gri/pretty/parser.go
はこのチャネル (tokchan <-chan *Scanner.Token
) を介してトークンを受け取っていました。これは、スキャナーがトークンを生成し、パーサーがそれをチャネルから読み取るという、並行処理的なアプローチを試みていたことを示唆しています。 - 変更後:
scanner.go
からToken
構造体とTokenStream()
メソッドが削除されました。parser.go
からtokchan
フィールドが削除され、Open
メソッドの引数からもtokchan
が取り除かれました。parser.go
のnext0()
メソッド内で、チャネルからの読み取り (<-P.tokchan
) のロジックが削除され、直接P.scanner.Scan()
を呼び出すようになりました。
- 影響: スキャナーとパーサー間の通信が、チャネルを介した並行処理モデルから、より直接的で同期的な関数呼び出しモデルに切り替わりました。これは、コンパイラのフロントエンドにおいては、シンプルさと効率性を優先した結果と考えられます。チャネルのオーバーヘッドや、この特定の文脈での並行処理のメリットが小さいと判断されたのでしょう。
4. 依存関係計算の修正 (pretty -d filename.go
)
usr/gri/pretty/compilation.go
内の依存関係計算ロジック (addDeps
関数など) が修正されました。- 特に、
fmt
パッケージのインポートが追加され、print
関数がfmt.Printf
に置き換えられています。これは、出力形式の改善や、より標準的なI/Oライブラリへの移行を示唆しています。 src_file
から.go
拡張子を削除するロジックが追加され、依存関係のファイル名処理がより堅牢になりました。flags.Deps
フラグが正しく機能するように、pretty.go
のコメントアウトされていた部分が有効化されました。これにより、pretty -d
コマンドが実際に依存関係情報を出力するようになりました。
5. その他のクリーンアップとデッドコードの削除
ast.go
からutf8
およびunicode
パッケージのインポートが削除されました。これは、Object.IsExported()
メソッドがsymboltable.go
に移動したため、ASTパッケージではこれらのパッケージが不要になったためです。scanner.go
からEXPRSTAT
というトークン定数が削除されました。これは、おそらくASTの表現方法の変更に伴い、不要になったデッドコードの一部です。Makefile
の依存関係が更新され、symboltable.6
が新しい依存関係として追加され、universe.6
が削除されました。これは、パッケージ構造の変更をビルドシステムに反映したものです。
これらの変更は、Goコンパイラの初期段階における設計の進化と成熟を示しており、コンポーネント間の責任分担を明確にし、より効率的で保守しやすいコードベースを構築するための重要なステップでした。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下のファイルに集中しています。
-
usr/gri/pretty/ast.go
:Object
,Type
,Scope
構造体とその関連メソッド(KindStr
,IsExported
,String
,NewObject
,NewScope
,LookupLocal
,Lookup
,add
,Insert
,InsertImport
,Print
)が完全に削除されました。Type
構造体の定義からRef
フィールドとObj
フィールドが削除され、Scope
フィールドの型が*SymbolTable.Scope
に変更されました。Ident
構造体のObj
フィールドの型が*AST.Object
から*SymbolTable.Object
に変更されました。import
文からutf8
とunicode
が削除されました。
-
usr/gri/pretty/symboltable.go
(新規ファイル):Object
,Type
,Scope
構造体とその関連メソッドが、ast.go
から移動されてここに定義されました。universe.go
の内容(組み込み型、定数、組み込み関数の定義と初期化ロジック)がこのファイルに統合されました。init()
関数内でUniverse
スコープが初期化され、各種組み込みシンボルが登録されます。
-
usr/gri/pretty/universe.go
:- ファイル全体が削除されました。その内容は
symboltable.go
に移動されました。
- ファイル全体が削除されました。その内容は
-
usr/gri/pretty/parser.go
:tokchan <-chan *Scanner.Token
フィールドが削除されました。Open
メソッドのシグネチャからtokchan
引数が削除されました。next0()
メソッド内のチャネルからのトークン読み取りロジックが削除され、直接P.scanner.Scan()
を呼び出すようになりました。top_scope
フィールドの型が*AST.Scope
から*SymbolTable.Scope
に変更されました。openScope()
メソッド内でAST.NewScope
の代わりにSymbolTable.NewScope
が呼び出されるようになりました。declareInScope
およびparseIdent
メソッド内で、AST.Object
やAST.Scope
の代わりにSymbolTable.Object
やSymbolTable.Scope
が使用されるようになりました。また、AST.NONE
,AST.TYPE
,AST.FUNC
,AST.FIELD
,AST.VAR
,AST.CONST
,AST.PACKAGE
といった定数がSymbolTable
パッケージの対応する定数に置き換えられました。
-
usr/gri/pretty/scanner.go
:Token
構造体とTokenStream()
メソッドが削除されました。EXPRSTAT
トークン定数が削除されました。
-
usr/gri/pretty/compilation.go
:flags.Tokenchan
フィールドが削除されました。Compile
関数内でflags.Tokenchan
に基づくトークンチャネルの初期化ロジックが削除されました。addDeps
関数内の依存関係計算ロジックが修正され、fmt
パッケージがインポートされ、print
がfmt.Printf
に置き換えられました。
-
usr/gri/pretty/pretty.go
:flags.Tokenchan
のFlag.BoolVar
の呼び出しが削除されました。flags.Deps
のコメントアウトが解除され、依存関係計算機能が有効になりました。
-
usr/gri/pretty/printer.go
:AST.Object
の代わりにSymbolTable.Object
を参照するように変更されました。
-
usr/gri/pretty/Makefile
:ast.6
の依存関係からscanner.6
が削除され、symboltable.6
が追加されました。typechecker.6
の依存関係からuniverse.6
が削除されました。parser.6
の依存関係にsymboltable.6
が追加されました。printer.6
の依存関係にsymboltable.6
が追加されました。universe.6
のビルドルールが削除されました。
これらの変更は、Goコンパイラの内部構造におけるシンボル管理と型システムの責任をASTから分離し、symboltable
パッケージに集約するという、根本的なアーキテクチャ変更を反映しています。
コアとなるコードの解説
このコミットの核心は、コンパイラの意味解析フェーズで中心的な役割を果たすObject
、Type
、Scope
といった概念を、構文解析の結果であるASTから独立したsymboltable
パッケージに分離した点にあります。
symboltable
パッケージの導入
-
Object
構造体:type Object struct { Id int; // unique id Pos int; // source position (< 0 if unknown position) Kind int; // object kind (CONST, TYPE, VAR, FUNC, etc.) Ident string; Typ *Type; // nil for packages Pnolev int; // >= 0: package no., <= 0: function nesting level, 0: global level }
これは、Goプログラム内の各識別子(変数、関数、型、定数など)に関するメタデータを保持します。
Kind
フィールドは識別子の種類を示し、Typ
フィールドはそれが持つ型へのポインタです。Pnolev
はスコープのネストレベルを示し、識別子の可視性を判断するのに使われます。 -
Scope
構造体:type Scope struct { Parent *Scope; entries map[string] *Object; }
Scope
は、識別子の有効範囲を管理します。Parent
フィールドは親スコープへのポインタであり、これによりスコープの階層構造が形成されます。entries
マップは、そのスコープ内で宣言された識別子名と、それに対応するObject
のマッピングを保持します。LookupLocal
は現在のスコープ内のみを検索し、Lookup
は親スコープを辿って検索します。Insert
はスコープに新しいObject
を追加します。 -
Type
構造体:type Type struct { Id int; // unique id Ref int; // for exporting only: >= 0 means already exported Form int; // type form (ARRAY, STRUCT, INTERFACE, FUNCTION, etc.) Size int; // size in bytes Obj *Object; // primary type object or nil Scope *Scope; // locals, fields & methods // ... その他の型に関する詳細情報 }
Type
は、Go言語の型に関する情報を保持します。Form
フィールドは型の種類(配列、構造体、インターフェース、関数など)を示します。Scope
フィールドは、構造体のフィールドやインターフェースのメソッドなど、型に関連するスコープを管理するために使用されます。
universe
スコープの統合
symboltable.go
の init()
関数内で、Go言語の組み込み型(int
, string
, bool
など)、組み込み定数(true
, false
, nil
, iota
)、組み込み関数(len
, new
, panic
, print
など)が Universe
というグローバルスコープに登録されます。これにより、これらの組み込みシンボルがコンパイラの意味解析フェーズで正しく解決されるようになります。
スキャナーとパーサー間の通信モデルの変更
以前は、スキャナーがトークンをGoのチャネルに送信し、パーサーがそのチャネルからトークンを受信するという並行処理モデルが採用されていました。このコミットでは、このチャネル接続が削除され、パーサーがスキャナーの Scan()
メソッドを直接呼び出してトークンを取得する、より伝統的な同期的なモデルに移行しました。
// parser.go の変更前 (抜粋)
func (P *Parser) next0() {
if P.tokchan == nil {
P.pos, P.tok, P.val = P.scanner.Scan();
} else {
t := <-P.tokchan; // チャネルから読み込み
P.tok, P.pos, P.val = t.Tok, t.Pos, t.Val;
}
// ...
}
// parser.go の変更後 (抜粋)
func (P *Parser) next0() {
P.pos, P.tok, P.val = P.scanner.Scan(); // 直接 Scan() を呼び出し
// ...
}
この変更は、コンパイラのフロントエンドにおけるトークン生成と消費のプロセスが、並行処理のメリットよりも同期的なシンプルさを優先した結果と考えられます。
依存関係計算の改善
compilation.go
内の addDeps
関数は、Goソースファイルの依存関係を再帰的に解決し、ビルドシステムが利用できる形式で出力します。このコミットでは、fmt
パッケージの使用や、ファイルパス処理の改善により、この依存関係計算の堅牢性と出力形式が向上しました。
これらの変更は、Goコンパイラの設計において、意味解析の基盤となるデータ構造とロジックをASTから分離し、よりモジュール化されたクリーンなアーキテクチャへと進化させるための重要なステップでした。これにより、コンパイラの各フェーズの責任が明確になり、将来的な機能追加や改善が容易になる基盤が築かれました。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Go言語の初期の設計に関する議論やドキュメント(当時のメーリングリストやデザインドキュメントを探すと、より深い背景情報が見つかる可能性があります)
参考にした情報源リンク
- Go言語のコンパイラに関する一般的な知識(AST, シンボルテーブル, スコープなど)
- Go言語のチャネルに関するドキュメント
- Go言語の歴史に関する情報(特に初期の設計決定について)
- GitHubのコミット履歴とファイル変更差分
- Go言語のコンパイラ設計に関する書籍やオンラインリソース(例: "Compilers: Principles, Techniques, and Tools" by Aho, Lam, Sethi, Ullman)
- Go言語の
go/ast
,go/token
,go/types
パッケージの現在のドキュメント(当時の設計との比較のため) - Robert Griesemer氏のGo言語に関する講演や記事(もしあれば)
(注:2009年当時のGo言語の内部実装に関する詳細な公開ドキュメントは限られているため、上記の解説は一般的なコンパイラ設計の原則と、コミットメッセージおよびコード変更から推測される内容に基づいています。)