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

[インデックス 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言語のコンパイラにおける以下の主要な変更を含んでいます。

  1. エラーハンドリングの分離: Scanner構造体からエラー処理に関連するフィールド(filename, nerrors, errpos, columns)とメソッド(LineCol, ErrorMsg)を削除。
  2. ErrorHandlerインターフェースの導入: compilation.goに新しいErrorHandler型(構造体とメソッド群)を定義し、エラー報告の責務を移譲。
  3. Scannerの初期化変更: Scanner.OpenメソッドがScanner.Initに改名され、ErrorHandlerインターフェースを引数として受け取るように変更。これにより、Scannerは自身でエラーを処理するのではなく、提供されたErrorHandlerにエラーを委譲するようになる。
  4. 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)を構築します。
    • エラーハンドリング: コンパイル中に発生する構文エラーや意味エラーなどを検出し、ユーザーに報告する仕組みです。
  • インターフェース(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.goErrorHandlerという新しい構造体が導入され、エラー報告のすべてのロジックがこの構造体に移されました。

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
}

ScannerErrorメソッドは、自身でエラーを処理する代わりに、保持しているErrorHandlerErrorメソッドを呼び出すように変更されました。

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フィールドを通じてErrorHandlerErrorメソッドを呼び出すように変更されました。また、Scanner.Open()メソッドはScanner.Init()に改名され、ErrorHandlerインターフェースのインスタンスを引数として受け取るようになりました。これにより、Scannerは外部から提供されるエラーハンドラーを使用するようになります。

これらの変更により、Scannerは字句解析という単一の責務に集中できるようになり、エラー処理のロジックはErrorHandlerにカプセル化され、再利用性とテスト容易性が向上します。

関連リンク

参考にした情報源リンク

  • Go言語の初期のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
  • Go言語のコンパイラ設計に関する一般的な情報 (Goのコンパイラはオープンソースであり、その設計に関する多くの議論やドキュメントが存在します)
  • ソフトウェア設計原則(特に責務の分離、依存性逆転の原則)に関する一般的な情報。
  • UTF-8エンコーディングに関する一般的な情報。
  • Go言語のsysパッケージに関する情報(初期のGo言語には、より低レベルなシステム操作のためのsysパッケージが存在した可能性がありますが、現在はほとんどが標準ライブラリに統合されています)。
    • 注: sysパッケージはGo言語の非常に初期の段階に存在し、その後標準ライブラリの他のパッケージに機能が移管されたため、現在のGoのドキュメントでは直接見つけるのが難しい場合があります。