[インデックス 1894] ファイルの概要
このコミットは、Go言語の初期のscanner
パッケージにおける初期化の不具合を修正し、その修正を検証するためのテストを追加するものです。具体的には、scanner.go
ファイル内のScanner.Init
メソッドの挙動が改善され、scanner_test.go
に新しいテストケースが追加されています。
コミット
commit e4db08d26dd1e6fe47e8c7e6b2547b81683b2a15
Author: Robert Griesemer <gri@golang.org>
Date: Thu Mar 26 17:40:51 2009 -0700
fix scanner initialization, add test
R=r
DELTA=27 (25 added, 0 deleted, 2 changed)
OCL=26798
CL=26798
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e4db08d26dd1e6fe47e8c7e6b2547b81683b2a15
元コミット内容
fix scanner initialization, add test
R=r
DELTA=27 (25 added, 0 deleted, 2 changed)
OCL=26798
CL=26798
変更の背景
このコミットが行われた2009年3月は、Go言語がまだ一般に公開される前の開発初期段階にあたります。Go言語のコンパイラやツールチェインの基盤となる部分が活発に開発されており、その中核をなすのがソースコードを解析するための字句解析器(lexerまたはscanner)です。
scanner
パッケージは、Goのソースコードをトークン(識別子、キーワード、演算子、リテラルなど)のストリームに分解する役割を担っています。この字句解析器は、コンパイラのフロントエンドにおいて非常に重要なコンポーネントであり、その正確性と堅牢性は言語処理系全体の信頼性に直結します。
元の実装では、Scanner
オブジェクトが一度使用された後に再利用される際に、内部状態が適切にリセットされないという問題があったと考えられます。特に、Init
メソッドが呼び出された際に、pos
(現在の位置情報)やoffset
(現在の読み取りオフセット)といった重要なフィールドが完全に初期化されない可能性がありました。これにより、同じScanner
インスタンスを異なるソースコードに対して再利用しようとすると、以前の解析状態が残ってしまい、誤ったトークン列が生成されるなどの予期せぬ挙動を引き起こす可能性がありました。
このコミットは、このような潜在的なバグを修正し、Scanner
が何度でも安全に再利用できるようにすることで、Go言語のツールチェインの安定性と信頼性を向上させることを目的としています。また、このような初期化に関する問題は、テストケースがなければ発見しにくいため、再初期化の挙動を明示的に検証するテストを追加することも重要な背景となっています。
前提知識の解説
字句解析器(Lexer/Scanner)
字句解析器は、コンパイラの最初のフェーズであり、ソースコードの文字列を入力として受け取り、それを意味のある最小単位である「トークン」のシーケンスに変換します。例えば、if x == 10 { ... }
というコードは、if
(キーワード), x
(識別子), ==
(演算子), 10
(整数リテラル), {
(区切り文字) といったトークンに分解されます。
Go言語のscanner
パッケージは、この字句解析の機能を提供します。Scanner
構造体は、入力ソースコード、エラーハンドラ、コメントをスキャンするかどうかの設定などを保持し、Scan
メソッドを呼び出すことで次のトークンを読み取ります。
token
パッケージ
Go言語のtoken
パッケージは、字句解析器が生成するトークンの種類(token.IDENT
, token.INT
, token.ADD
など)や、ソースコード内でのトークンの位置情報(ファイル名、行番号、列番号、オフセット)を定義する型を提供します。
token.Token
: トークンの種類を表す型です。token.Position
: ソースコード内の位置を表す構造体で、Offset
(ファイル先頭からのバイトオフセット),Line
(行番号),Column
(列番号) などのフィールドを持ちます。
Scanner.Init
メソッド
Scanner
構造体のInit
メソッドは、スキャナーを特定のソースコードに対して初期化するために使用されます。このメソッドは、スキャン対象のソースコード、エラーハンドラ、コメントをスキャンするかどうかのフラグを受け取ります。スキャナーを再利用する際には、このInit
メソッドを再度呼び出すことで、新しいソースコードに対してスキャンを開始できるように内部状態をリセットする必要があります。
io.StringBytes
Go言語の初期のバージョンでは、文字列をバイトスライスに変換するためのヘルパー関数としてio.StringBytes
のようなものが存在した可能性があります。現在のGoの標準ライブラリでは、文字列をバイトスライスに変換するには[]byte(str)
のように型変換を行うのが一般的です。このコミットが書かれた時点でのGoのAPIの進化の過程を反映していると考えられます。
技術的詳細
このコミットの技術的ポイントは、Scanner
構造体の内部状態の完全なリセットにあります。
Scanner
構造体は、ソースコードを効率的にスキャンするために、以下のような内部状態を保持しています。
src
: スキャン対象のソースコード(バイトスライス)。err
: エラーハンドラ。scan_comments
: コメントをスキャンするかどうかのフラグ。pos
: 現在スキャンしている文字のソースコード上の位置情報(token.Position
型)。offset
:src
バイトスライス内での現在の読み取りオフセット。ch
: 現在読み取っている文字。
Scanner.Init
メソッドは、これらのフィールドを新しいスキャンセッションのために設定し直す役割を担います。コミット前のコードでは、S.pos.Line = 1;
という行がありましたが、これはtoken.Position
構造体の一部のフィールド(Line
)のみをリセットしていました。token.Position
にはOffset
やColumn
といった他のフィールドも含まれており、これらが適切にリセットされないと、以前のスキャンセッションの残骸が残ってしまう可能性がありました。
今回の修正では、S.pos = token.Position{0, 1, 0};
という行に変更されています。これは、token.Position
構造体全体をゼロ値(Offset: 0
, Line: 1
, Column: 0
)で初期化することを意味します。これにより、位置情報が完全にリセットされ、常にソースコードの先頭(オフセット0、1行目、0列目)からスキャンが開始されるようになります。
さらに、S.offset = 0;
という行が明示的に追加されました。これは、src
バイトスライス内での読み取りオフセットをゼロにリセットすることを保証します。これにより、next()
メソッドが呼び出された際に、常にソースコードの最初から文字を読み始めることが保証されます。
これらの変更により、Scanner
インスタンスが一度使用された後でも、Init
メソッドを再度呼び出すことで、完全にクリーンな状態から新しいソースコードのスキャンを開始できるようになります。これは、コンパイラやその他のツールが複数のファイルを処理する際など、Scanner
オブジェクトを再利用するシナリオにおいて非常に重要です。
追加されたテストケースTestInit
は、この再初期化の挙動を具体的に検証します。同じScanner
インスタンスを2つの異なるソースコードに対して連続して初期化し、それぞれのスキャンが正しく行われることを確認しています。これにより、将来の変更によってこの初期化ロジックが誤って壊されることを防ぐための回帰テストとしての役割も果たします。
コアとなるコードの変更箇所
src/lib/go/scanner.go
--- a/src/lib/go/scanner.go
+++ b/src/lib/go/scanner.go
@@ -76,10 +76,12 @@ func (S *Scanner) next() {
// white space and ignored.
//
func (S *Scanner) Init(src []byte, err ErrorHandler, scan_comments bool) {
+ // Explicitly initialize all fields since a scanner may be reused.
S.src = src;
S.err = err;
S.scan_comments = scan_comments;
- S.pos.Line = 1;
+ S.pos = token.Position{0, 1, 0};
+ S.offset = 0;
S.next();
}
src/lib/go/scanner_test.go
--- a/src/lib/go/scanner_test.go
+++ b/src/lib/go/scanner_test.go
@@ -176,7 +176,8 @@ func NewlineCount(s string) int {
}
-func Test(t *testing.T) {
+// Verify that calling Scan() provides the correct results.
+func TestScan(t *testing.T) {
// make source
var src string;
for i, e := range tokens {
@@ -223,3 +224,25 @@ func Test(t *testing.T) {
}\n\t);\n}\n+\n+\n+// Verify that initializing the same scanner more then once works correctly.
+func TestInit(t *testing.T) {
+\tvar s scanner.Scanner;
+\n+\t// 1st init
+\ts.Init(io.StringBytes(\"if true { }\"), &TestErrorHandler{t}, false);\n+\ts.Scan(); // if
+\ts.Scan(); // true
+\tpos, tok, lit := s.Scan(); // {\n+\tif tok != token.LBRACE {\n+\t\tt.Errorf(\"bad token: got %s, expected %s\", tok.String(), token.LBRACE);\n+\t}\n+\n+\t// 2nd init
+\ts.Init(io.StringBytes(\"go true { ]\"), &TestErrorHandler{t}, false);\n+\tpos, tok, lit = s.Scan(); // go\n+\tif tok != token.GO {\n+\t\tt.Errorf(\"bad token: got %s, expected %s\", tok.String(), token.GO);\n+\t}\n+}\n```
## コアとなるコードの解説
### `src/lib/go/scanner.go` の変更点
- **`S.pos.Line = 1;` から `S.pos = token.Position{0, 1, 0};` への変更**:
- 以前のコードでは、`S.pos`(`token.Position`型)の`Line`フィールドのみを`1`に設定していました。しかし、`token.Position`構造体は`Offset`、`Line`、`Column`の3つのフィールドを持ちます。`Line`だけをリセットしても、`Offset`や`Column`が以前の値のまま残ってしまう可能性がありました。
- 新しいコードでは、`token.Position{0, 1, 0}`という複合リテラルを使用して、`S.pos`全体を明示的に初期化しています。これは、`Offset`を`0`(ファイルの先頭)、`Line`を`1`(1行目)、`Column`を`0`(0列目)に設定することを意味します。これにより、スキャナーの位置情報が完全にリセットされ、常にソースコードの冒頭からスキャンが開始されることが保証されます。
- **`S.offset = 0;` の追加**:
- `S.offset`は、スキャン対象のバイトスライス`S.src`内での現在の読み取り位置(バイトオフセット)を追跡するフィールドです。このフィールドが適切にリセットされないと、`next()`メソッドが以前のオフセットから読み取りを再開してしまい、誤ったトークンを生成する原因となります。
- この行の追加により、`offset`が明示的に`0`に設定され、常にソースコードの先頭からバイトの読み取りが開始されることが保証されます。
- **コメントの追加**: `// Explicitly initialize all fields since a scanner may be reused.`
- このコメントは、なぜこれらのフィールドを明示的に初期化する必要があるのかという理由を明確にしています。`Scanner`インスタンスが再利用される可能性があるため、以前の状態が新しいスキャンに影響を与えないように、すべての関連フィールドをリセットすることが重要であるという設計意図を示しています。
### `src/lib/go/scanner_test.go` の変更点
- **`func Test(t *testing.T)` から `func TestScan(t *testing.T)` へのリネーム**:
- 既存のテスト関数の名前が`TestScan`に変更されました。これにより、このテストがスキャナーの基本的なスキャン機能(トークンを正しく読み取る能力)を検証するものであることが、より明確になりました。
- **`func TestInit(t *testing.T)` の追加**:
- この新しいテスト関数は、`Scanner`の再初期化の挙動を具体的に検証するために追加されました。
- **1回目の初期化とスキャン**:
- `var s scanner.Scanner;` で`Scanner`インスタンスを宣言します。
- `s.Init(io.StringBytes("if true { }"), &TestErrorHandler{t}, false);` で最初のソースコード(`"if true { }"`)に対してスキャナーを初期化します。
- `s.Scan();` を数回呼び出し、`if`、`true`、`{`といったトークンが正しく読み取られることを確認します。特に、`{`トークンが期待通りに読み取られるかを検証しています。
- **2回目の初期化とスキャン**:
- 同じ`s`インスタンスに対して、`s.Init(io.StringBytes("go true { ]"), &TestErrorHandler{t}, false);` を呼び出し、異なるソースコード(`"go true { ]"`)で再初期化します。
- 再初期化後、`s.Scan();` を呼び出し、最初のトークンが`go`であることを確認します。
- このテストの目的は、1回目のスキャンセッションの内部状態が2回目のスキャンセッションに影響を与えず、`Init`メソッドがスキャナーを完全にリセットして新しいソースコードを正しく処理できることを保証することです。もし初期化が不完全であれば、2回目のスキャンで誤ったトークンが返されるなどの問題が発生するはずです。
これらの変更は、Go言語の字句解析器の堅牢性と再利用性を高める上で重要なステップであり、言語処理系の安定した動作に貢献しています。
## 関連リンク
- Go言語の`go/scanner`パッケージ (現在のドキュメント): [https://pkg.go.dev/go/scanner](https://pkg.go.dev/go/scanner)
- Go言語の`go/token`パッケージ (現在のドキュメント): [https://pkg.go.dev/go/token](https://pkg.go.dev/go/token)
## 参考にした情報源リンク
- Go言語のGitHubリポジトリ: [https://github.com/golang/go](https://github.com/golang/go)
- Go言語の初期のコミット履歴 (GitHub): [https://github.com/golang/go/commits/master?after=e4db08d26dd1e6fe47e8c7e6b2547b81683b2a15+34&path=src%2Flib%2Fgo%2Fscanner.go](https://github.com/golang/go/commits/master?after=e4db08d26dd1e6fe47e8c7e6b2547b81683b2a15+34&path=src%2Flib%2Fgo%2Fscanner.go)
- コンパイラの設計に関する一般的な情報源 (字句解析器について):
- "Compilers: Principles, Techniques, and Tools" (通称 Dragon Book)
- オンラインのコンパイラ理論に関する講義資料や記事