[インデックス 1242] ファイルの概要
このコミットは、Go言語の初期開発段階における、コンパイラの字句解析(スキャナー)とエラーハンドリングの構造に関する重要なリファクタリングを示しています。具体的には、エラー処理ロジックをScanner
(字句解析器)から分離し、より汎用的なErrorHandler
インターフェースを導入することで、既存のライブラリコードの利用を促進し、コードの再利用性と保守性を向上させています。
コミット
commit b1297aa04f72992186f75441ad2c34eddd829100
Author: Robert Griesemer <gri@golang.org>
Date: Mon Nov 24 18:24:21 2008 -0800
- move error handling out of scanner
- use more of the existing library code
R=r
OCL=19957
CL=19959
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b1297aa04f72992186f75441ad2c34eddd829100
元コミット内容
このコミットは、Go言語のコンパイラにおける以下の主要な変更を含んでいます。
- エラーハンドリングの分離:
Scanner
構造体からエラー処理に関連するフィールド(filename
,nerrors
,errpos
,columns
)とメソッド(LineCol
,ErrorMsg
)を削除。 ErrorHandler
インターフェースの導入:compilation.go
に新しいErrorHandler
型(構造体とメソッド群)を定義し、エラー報告の責務を移譲。Scanner
の初期化変更:Scanner.Open
メソッドがScanner.Init
に改名され、ErrorHandler
インターフェースを引数として受け取るように変更。これにより、Scanner
は自身でエラーを処理するのではなく、提供されたErrorHandler
にエラーを委譲するようになる。- UTF-8処理の改善:
sys.stringtorune
の呼び出しがutf8.DecodeRuneInString
に置き換えられ、より標準的で堅牢なUTF-8文字のデコード処理が導入された。
変更の背景
この変更の背景には、Go言語のコンパイラ設計におけるモジュール性と再利用性の向上が挙げられます。
初期のコンパイラ設計では、字句解析器(Scanner
)が直接エラーの記録と報告のロジックを持っていました。これは、単一のコンポーネントが複数の責務を持つ「密結合」の状態であり、以下のような問題を引き起こす可能性がありました。
- 責務の混在:
Scanner
が字句解析とエラー処理の両方の責務を持つため、コードが複雑になり、理解や変更が困難になる。 - 再利用性の低さ: エラー処理ロジックが
Scanner
に密接に結合しているため、他のコンポーネントや異なるコンテキストで同じエラー処理ロジックを再利用することが難しい。 - テストの複雑化:
Scanner
のテストを行う際に、エラー処理の側面も考慮する必要があり、テストが複雑になる。
このコミットは、これらの問題を解決するために、エラー処理の責務をScanner
から切り離し、独立したErrorHandler
インターフェースとして抽象化することを目的としています。これにより、Scanner
は純粋に字句解析に専念できるようになり、エラー処理はErrorHandler
の実装に委ねられるため、より柔軟で再利用可能な設計が実現されます。また、「既存のライブラリコードの利用」という言及は、Go言語の標準ライブラリや共通のユーティリティ関数を活用することで、コードの品質と一貫性を高める意図があったことを示唆しています。
sys.stringtorune
からutf8.DecodeRuneInString
への変更は、Go言語が多言語対応を重視し、UTF-8をネイティブにサポートする設計思想を反映しています。初期段階から文字エンコーディングの扱いを標準ライブラリに集約することで、将来的な国際化対応や文字処理の正確性を確保しようとしたと考えられます。
前提知識の解説
このコミットを理解するためには、以下の概念が重要です。
-
コンパイラの構造:
- 字句解析器(Lexer/Scanner): ソースコードを読み込み、意味のある最小単位(トークン)に分割する役割を担います。例えば、
int x = 10;
というコードは、int
(キーワード)、x
(識別子)、=
(演算子)、10
(リテラル)、;
(区切り文字)といったトークンに分割されます。 - 構文解析器(Parser): 字句解析器が生成したトークンの並びが、言語の文法規則に合致しているかを検証し、抽象構文木(AST)を構築します。
- エラーハンドリング: コンパイル中に発生する構文エラーや意味エラーなどを検出し、ユーザーに報告する仕組みです。
- 字句解析器(Lexer/Scanner): ソースコードを読み込み、意味のある最小単位(トークン)に分割する役割を担います。例えば、
-
インターフェース(Go言語): Go言語におけるインターフェースは、メソッドのシグネチャの集合を定義する型です。特定のインターフェースを実装する型は、そのインターフェースが定義するすべてのメソッドを持つ必要があります。インターフェースは、具体的な実装から抽象化された振る舞いを定義するために使用され、ポリモーフィズムを実現し、コードの柔軟性と再利用性を高めます。このコミットでは、
ErrorHandler
インターフェースを導入することで、エラー報告の具体的な方法をScanner
から分離し、様々なエラー処理の実装を差し替え可能にしています。 -
UTF-8エンコーディング: UTF-8は、Unicode文字を可変長でエンコードする方式です。ASCII文字は1バイトで表現され、他の多くの文字は2バイト以上で表現されます。Go言語は文字列をUTF-8で扱うことを前提としており、
unicode/utf8
パッケージはUTF-8文字列の操作(ルーンのデコード、エンコードなど)のための機能を提供します。sys.stringtorune
のような低レベルなシステムコールに依存するのではなく、標準ライブラリのutf8.DecodeRuneInString
を使用することは、よりポータブルで安全なUTF-8処理を実現するための重要なステップです。 -
責務の分離(Separation of Concerns): ソフトウェア設計の原則の一つで、プログラムを異なる機能や関心事に基づいて独立したモジュールに分割することを指します。これにより、各モジュールは単一の明確な責務を持ち、変更が他のモジュールに与える影響を最小限に抑えることができます。このコミットでは、字句解析とエラー処理という異なる責務を
Scanner
からErrorHandler
に分離しています。
技術的詳細
このコミットの技術的詳細は、Go言語のコンパイラにおけるエラー処理のアーキテクチャの進化と、UTF-8文字処理の標準化に焦点を当てています。
エラーハンドリングの委譲モデル
以前のScanner
は、エラーメッセージのフォーマット、行と列の計算、エラー数のカウント、そしてエラーが多すぎる場合のプログラム終了といった、エラー報告の具体的なロジックを直接持っていました。これは、Scanner
が字句解析という主要な責務に加えて、エラー報告という二次的な責務も負っていたことを意味します。
このコミットでは、compilation.go
にErrorHandler
という新しい構造体が導入され、エラー報告のすべてのロジックがこの構造体に移されました。
type ErrorHandler struct {
filename string;
src string;
nerrors int;
nwarnings int;
errpos int;
columns bool;
}
func (h *ErrorHandler) Init(filename, src string, columns bool) { ... }
func (h *ErrorHandler) LineCol(pos int) (line, col int) { ... }
func (h *ErrorHandler) ErrorMsg(pos int, msg string) { ... }
func (h *ErrorHandler) Error(pos int, msg string) { ... }
func (h *ErrorHandler) Warning(pos int, msg string) { ... }
そして、scanner.go
では、Scanner
構造体からエラー関連のフィールドが削除され、代わりにErrorHandler
インターフェースのインスタンスを保持するerr ErrorHandler
フィールドが追加されました。
export type ErrorHandler interface {
Error(pos int, msg string);
Warning(pos int, msg string);
}
export type Scanner struct {
err ErrorHandler; // New field
// ... other scanning fields
}
Scanner
のError
メソッドは、自身でエラーを処理する代わりに、保持しているErrorHandler
のError
メソッドを呼び出すように変更されました。
func (S *Scanner) Error(pos int, msg string) {
S.err.Error(pos, msg); // Delegate error handling
}
この変更により、Scanner
はエラーが発生したことをErrorHandler
に通知するだけでよくなり、エラーの具体的な処理方法(どこにログを出すか、どのようにフォーマットするかなど)はErrorHandler
の実装に委ねられます。これは、**依存性逆転の原則(Dependency Inversion Principle)**の一例と見なすことができます。高レベルモジュール(Scanner
)が低レベルモジュール(具体的なエラーハンドリングの実装)に直接依存するのではなく、両方が抽象化(ErrorHandler
インターフェース)に依存する形になります。
また、ErrorHandler
には、連続するエラーが近すぎる場合に報告を抑制するロジック(errdist
定数とdelta
計算)が含まれており、これはコンパイル時のエラーメッセージの洪水(error cascade)を防ぐための一般的な手法です。
UTF-8文字処理の標準化
もう一つの重要な変更は、Scanner.Next()
メソッドにおける文字デコードのロジックです。
変更前:
r, w := int(S.src[S.pos]), 1;
if r >= 0x80 {
// not ascii
r, w = sys.stringtorune(S.src, S.pos);
}
変更後:
r, w := int(S.src[S.pos]), 1;
if r >= 0x80 {
// not ascii
r, w = utf8.DecodeRuneInString(S.src, S.pos);
}
sys.stringtorune
は、Go言語の初期のランタイムシステムに依存する可能性のある、より低レベルな関数であったと考えられます。これをimport "utf8"
してutf8.DecodeRuneInString
に置き換えることで、以下の利点が得られます。
- 標準化とポータビリティ:
unicode/utf8
パッケージはGo言語の標準ライブラリの一部であり、プラットフォームに依存しないUTF-8処理を提供します。これにより、コンパイラが異なるオペレーティングシステムやアーキテクチャでより容易に動作するようになります。 - 堅牢性:
utf8.DecodeRuneInString
は、不正なUTF-8シーケンスを適切に処理し、unicode.ReplacementChar
(U+FFFD)を返すなど、より堅牢なエラーハンドリングを提供します。 - 可読性と保守性: 標準ライブラリの関数を使用することで、コードの意図がより明確になり、他のGo開発者にとっても理解しやすくなります。
この変更は、Go言語が設計の初期段階からUTF-8を第一級の市民として扱い、その処理を標準ライブラリに集約するという強いコミットメントを示しています。
コアとなるコードの変更箇所
usr/gri/pretty/compilation.go
- 新しい型
ErrorHandler
の定義と、そのメソッド群 (Init
,LineCol
,ErrorMsg
,Error
,Warning
) の追加。 Compile
関数内でErrorHandler
のインスタンスを生成し、scanner.Init
に渡すように変更。Compile
関数の戻り値で、scanner.nerrors
の代わりにerr.nerrors
を返すように変更。
usr/gri/pretty/scanner.go
import "utf8"
の追加。ErrorHandler
インターフェースの定義。Scanner
構造体からエラー処理関連のフィールド (filename
,nerrors
,errpos
,columns
) を削除し、err ErrorHandler
フィールドを追加。Scanner.Next()
メソッド内で、sys.stringtorune
の呼び出しをutf8.DecodeRuneInString
に変更。Scanner.LineCol()
およびScanner.ErrorMsg()
メソッドを削除。Scanner.Error()
メソッドの内部実装を、S.err.Error(pos, msg)
を呼び出すように変更。Scanner.Open()
メソッドをScanner.Init()
に改名し、引数を(err ErrorHandler, src string, testmode bool)
に変更。
コアとなるコードの解説
compilation.go
における ErrorHandler
の導入
// 新しいErrorHandler構造体
type ErrorHandler struct {
filename string;
src string;
nerrors int;
nwarnings int;
errpos int;
columns bool;
}
// ErrorHandlerの初期化メソッド
func (h *ErrorHandler) Init(filename, src string, columns bool) {
h.filename = filename;
h.src = src;
h.nerrors = 0;
h.nwarnings = 0;
h.errpos = 0;
h.columns = columns;
}
// ソースコード中の位置から行と列を計算するヘルパーメソッド
func (h *ErrorHandler) LineCol(pos int) (line, col int) {
line = 1;
lpos := 0;
src := h.src;
if pos > len(src) {
pos = len(src);
}
for i := 0; i < pos; i++ {
if src[i] == '\n' {
line++;
lpos = i;
}
}
return line, pos - lpos;
}
// 実際のエラーメッセージを出力し、エラー数をカウントするメソッド
func (h *ErrorHandler) ErrorMsg(pos int, msg string) {
print(h.filename, ":");
if pos >= 0 {
line, col := h.LineCol(pos);
print(line, ":");
if h.columns {
print(col, ":");
}
}
print(" ", msg, "\n");
h.nerrors++;
h.errpos = pos;
if h.nerrors >= 10 { // エラーが多すぎる場合に終了
sys.exit(1);
}
}
// エラーを報告する主要なメソッド。連続するエラーの報告を抑制するロジックを含む。
func (h *ErrorHandler) Error(pos int, msg string) {
const errdist = 20; // 前のエラーからの最小距離
delta := pos - h.errpos;
if delta < 0 {
delta = -delta;
}
if delta > errdist || h.nerrors == 0 /* 最初のエラーは常に報告 */ {
h.ErrorMsg(pos, msg);
}
}
// Compile関数がErrorHandlerを使用するように変更
export func Compile(src_file string, flags *Flags) (*AST.Program, int) {
// ... (ソースファイルの読み込み) ...
var err ErrorHandler; // ErrorHandlerのインスタンスを宣言
err.Init(src_file, src, flags.columns); // 初期化
var scanner Scanner.Scanner;
// Scannerの初期化時にErrorHandlerのポインタを渡す
scanner.Init(&err, src, flags.testmode);
// ... (パーサーのオープン) ...
prog := parser.ParseProgram();
return prog, err.nerrors; // エラー数をErrorHandlerから取得
}
compilation.go
では、ErrorHandler
という新しい型が定義され、エラーのファイル名、ソースコード、エラー数、警告数、最後のエラー位置、列表示の有無といった状態を管理します。また、エラーメッセージのフォーマット、行と列の計算、そしてエラーが多すぎる場合にプログラムを終了させるロジックもこのErrorHandler
のメソッドとして実装されています。Compile
関数は、Scanner
を初期化する際に、この新しく作成したErrorHandler
のインスタンスを渡すようになります。これにより、Scanner
はエラー処理の具体的な実装を知る必要がなくなり、ErrorHandler
にその責務を委譲できるようになります。
scanner.go
における変更
package Scanner
import "utf8" // utf8パッケージをインポート
import Utils "utils"
// ErrorHandlerインターフェースの定義
export type ErrorHandler interface {
Error(pos int, msg string);
Warning(pos int, msg string);
}
export type Scanner struct {
err ErrorHandler; // ErrorHandlerインターフェースのインスタンスを保持
// ... (他のフィールド) ...
}
// Next() メソッドにおける文字デコードの変更
func (S *Scanner) Next() {
// ...
if S.pos < len(S.src) {
r, w := int(S.src[S.pos]), 1;
if r >= 0x80 {
// ASCII以外の文字の場合、utf8.DecodeRuneInStringを使用
r, w = utf8.DecodeRuneInString(S.src, S.pos);
}
S.ch = r;
S.chpos = S.pos;
S.pos += w;
} else {
S.ch = -1; // eof
S.chpos = len(S.src);
}
}
// Scannerのエラー報告メソッドがErrorHandlerに委譲するように変更
func (S *Scanner) Error(pos int, msg string) {
S.err.Error(pos, msg); // 保持しているErrorHandlerのErrorメソッドを呼び出す
}
// Scannerの初期化メソッドの変更
func (S *Scanner) Init(err ErrorHandler, src string, testmode bool) {
S.err = err; // 渡されたErrorHandlerを保持
S.src = src;
S.pos = 0;
S.ch = ' '; // dummy value
S.chpos = -1; // dummy value
S.testmode = testmode;
S.Next(); // initialize S.ch and S.chpos
}
scanner.go
では、まずutf8
パッケージがインポートされ、Scanner.Next()
メソッド内の文字デコード処理がsys.stringtorune
からutf8.DecodeRuneInString
に置き換えられています。これにより、UTF-8文字の処理がより標準的で堅牢になります。
最も重要な変更は、Scanner
構造体からエラー処理に関連するフィールドが削除され、代わりにErrorHandler
インターフェース型のerr
フィールドが追加されたことです。これにより、Scanner
はエラー処理の具体的な実装から切り離されます。Scanner.Error()
メソッドは、自身でエラーを処理する代わりに、このerr
フィールドを通じてErrorHandler
のError
メソッドを呼び出すように変更されました。また、Scanner.Open()
メソッドはScanner.Init()
に改名され、ErrorHandler
インターフェースのインスタンスを引数として受け取るようになりました。これにより、Scanner
は外部から提供されるエラーハンドラーを使用するようになります。
これらの変更により、Scanner
は字句解析という単一の責務に集中できるようになり、エラー処理のロジックはErrorHandler
にカプセル化され、再利用性とテスト容易性が向上します。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語のインターフェースに関する公式ブログ記事: https://go.dev/blog/interfaces
unicode/utf8
パッケージのドキュメント: https://pkg.go.dev/unicode/utf8
参考にした情報源リンク
- Go言語の初期のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語のコンパイラ設計に関する一般的な情報 (Goのコンパイラはオープンソースであり、その設計に関する多くの議論やドキュメントが存在します)
- ソフトウェア設計原則(特に責務の分離、依存性逆転の原則)に関する一般的な情報。
- UTF-8エンコーディングに関する一般的な情報。
- Go言語の
sys
パッケージに関する情報(初期のGo言語には、より低レベルなシステム操作のためのsys
パッケージが存在した可能性がありますが、現在はほとんどが標準ライブラリに統合されています)。- 注:
sys
パッケージはGo言語の非常に初期の段階に存在し、その後標準ライブラリの他のパッケージに機能が移管されたため、現在のGoのドキュメントでは直接見つけるのが難しい場合があります。
- 注: