[インデックス 1728] ファイルの概要
このコミットは、Go言語の初期のprettyパッケージ(現在のgo/scannerパッケージの前身にあたる可能性が高い)におけるスキャナーのクリーンアップとライブラリ化に向けた変更を目的としています。主な変更点は、スキャナーが処理するソースコードの表現をstring型から[]byte型へ移行したこと、不要なコードの削除、およびインターフェースの簡素化です。これにより、スキャナーの再利用性と効率性が向上しています。
コミット
commit d169268a1a9b0f6b28a02ffd6590ba7b288e7aab
Author: Robert Griesemer <gri@golang.org>
Date: Mon Mar 2 20:27:09 2009 -0800
scanner cleanup - getting it ready to as a library
- removed unneeded code that accumulated over time
- change src from string to []byte (perhaps should be io.Read
but that has some other disadvantages)
- simplified interface
R=r
OCL=25615
CL=25615
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d169268a1a9b0f6b28a02ffd6590ba7b288e7aab
元コミット内容
このコミットの元の内容は以下の通りです。
- スキャナーのクリーンアップ - ライブラリとして利用可能にするための準備
- 時間とともに蓄積された不要なコードの削除
- ソースコードの表現を
stringから[]byteに変更(io.Readも検討されたが、他の欠点があったため見送られた) - インターフェースの簡素化
変更の背景
Go言語の初期開発段階において、コンパイラやツールチェインの各コンポーネントは進化を続けていました。このコミットが行われた2009年3月は、Go言語が一般に公開される前の時期であり、言語仕様や標準ライブラリの設計が活発に行われていた時期です。
スキャナーは、ソースコードをトークンに分解する役割を担うコンパイラのフロントエンドの重要な部分です。初期の実装では、開発の過程で一時的な機能やデバッグ用のコードが追加され、インターフェースが複雑になることがよくあります。このコミットの背景には、以下のような目的があったと考えられます。
- ライブラリとしての再利用性向上: スキャナーをコンパイラの一部としてだけでなく、他のツール(例: フォーマッタ、リンタ、IDEなど)からも独立して利用できる汎用的なライブラリとして設計し直す必要がありました。そのためには、不要な依存関係を排除し、インターフェースを明確かつ簡潔にすることが求められます。
- パフォーマンスの最適化: ソースコードの読み込みと処理において、
string型はUTF-8文字列として扱われるため、文字単位のアクセスや部分文字列の抽出にオーバーヘッドが生じる可能性があります。一方、[]byte型はバイト列として直接メモリにアクセスできるため、特にASCII文字が大部分を占めるソースコードの処理において、より効率的な操作が可能です。また、io.Readインターフェースも検討されたようですが、ストリーム処理のオーバーヘッドや、ランダムアクセスが必要なスキャナーの特性との相性が悪かったため、[]byteが選択されたと推測されます。 - コードベースの健全性維持: 開発が進むにつれて、一時的なデバッグコードや実験的な機能がコードベースに残ることがあります。これらを定期的にクリーンアップすることで、コードの可読性、保守性、および将来的な拡張性を高めることができます。特に、
Testmodeのような内部的なテストメカニズムが削除されたことは、より標準的なテストフレームワークへの移行や、スキャナーの内部状態に依存しないテスト手法への変更を示唆しています。
これらの背景から、このコミットはGo言語のツールチェインの基盤を固め、将来の発展に備えるための重要なステップであったと言えます。
前提知識の解説
このコミットを理解するためには、以下の概念に関する基本的な知識が必要です。
- コンパイラのフロントエンド:
- スキャナー(字句解析器、Lexer): ソースコードの文字列を読み込み、意味のある最小単位である「トークン」(例: 識別子、キーワード、演算子、リテラルなど)のストリームに変換するコンポーネントです。このコミットの主要な対象です。
- パーサー(構文解析器、Parser): スキャナーが生成したトークンのストリームを読み込み、言語の文法規則に従って構文木(AST: Abstract Syntax Tree)を構築するコンポーネントです。
- Go言語のデータ型:
string: Go言語のstring型は、不変のバイトスライスであり、UTF-8エンコードされたUnicodeコードポイントのシーケンスを表します。文字列操作は通常、UTF-8のデコード/エンコードを伴うため、バイト列としての直接操作よりもオーバーヘッドが生じることがあります。[]byte: バイトのスライスです。メモリ上の連続したバイト列を直接操作する際に使用されます。ファイルI/Oやネットワーク通信など、バイナリデータを扱う際に効率的です。rune: Go言語におけるrune型は、Unicodeコードポイントを表すint32のエイリアスです。UTF-8エンコードされた文字列から個々のUnicode文字を扱う際に使用されます。
utf8パッケージ: Go言語の標準ライブラリの一部で、UTF-8エンコードされたバイトスライスを操作するための関数を提供します。utf8.DecodeRuneInString(s string, i int) (r rune, size int): 文字列sのインデックスiから始まるUTF-8エンコードされたルーンをデコードし、ルーンとそのバイトサイズを返します。utf8.DecodeRune(p []byte) (r rune, size int): バイトスライスpの先頭からUTF-8エンコードされたルーンをデコードし、ルーンとそのバイトサイズを返します。utf8.RuneCountInString(s string) int: 文字列sに含まれるルーンの数を返します。utf8.RuneCount(p []byte) int: バイトスライスpに含まれるルーンの数を返します。
- エラーハンドリング: Go言語では、エラーは通常、関数の戻り値として
errorインターフェースを実装する型で返されます。このコミットでは、ErrorHandlerインターフェースが導入され、エラー報告のメカニズムが抽象化されています。 vectorパッケージ: Go言語の初期のバージョンで使用されていた、動的配列(スライス)を扱うためのパッケージです。現在のGoでは、組み込みのスライス型が非常に強力であるため、このパッケージはほとんど使われていません。strconvパッケージ: Go言語の標準ライブラリの一部で、基本的なデータ型(整数、浮動小数点数、ブール値など)と文字列との間の変換を行うための関数を提供します。
技術的詳細
このコミットの技術的な核心は、スキャナーの内部でソースコードを表現する方法をstringから[]byteに変更した点にあります。この変更は、単なる型変換以上の意味を持ち、スキャナーの設計とパフォーマンスに大きな影響を与えます。
-
ソースコード表現の変更 (
string->[]byte):Scanner構造体:Scanner構造体のsrcフィールドがstringから[]byteに変更されました。これにより、スキャナーはソースコードをバイト列として直接保持し、操作できるようになります。next()メソッド:Scanner.next()メソッドは、現在の読み取り位置から次のUnicodeルーンを読み込む役割を担います。以前はutf8.DecodeRuneInStringを使用していましたが、srcが[]byteになったことで、utf8.DecodeRuneを使用するように変更されました。utf8.DecodeRuneはバイトスライスを直接操作するため、文字列からのデコードに伴う潜在的なオーバーヘッドが削減されます。- トークン値の返却:
scanComment(),scanIdentifier(),scanNumber(),scanChar(),scanString(),scanRawString(),Scan()といった、トークンの値を返すメソッドの戻り値の型がstringから[]byteに変更されました。これにより、スキャナーはトークンをバイト列として抽出し、そのまま返すことができるため、不要な文字列変換が削減されます。パーサー側では、必要に応じてstring(val)のように明示的に文字列に変換する処理が追加されています(例:parser.goのnext0())。 platform.goの変更: ファイル読み込み関数(readfile,ReadObjectFile,ReadSourceFile)も、読み込んだファイルの内容をstringではなく[]byteとして返すように変更されました。これは、スキャナーの変更と一貫性を持たせるための変更であり、ファイルI/Oからスキャナーへのデータフロー全体で[]byteが使用されることを意味します。
-
インターフェースの簡素化とクリーンアップ:
ErrorHandlerインターフェースの導入:parser.goでErrorHandlerインターフェースが定義され、Error(pos int, msg string)とWarning(pos int, msg string)メソッドを持つようになりました。しかし、scanner.goのErrorHandlerインターフェースからはWarningメソッドが削除され、Errorメソッドのみが残されました。これは、スキャナーが警告を直接報告するのではなく、エラーのみを報告するように役割を限定し、インターフェースを簡素化したことを示唆しています。Scanner.Initメソッドの変更:Scanner.Initメソッドのシグネチャが大幅に変更されました。特に、testmode引数が削除され、ErrorHandlerの引数の位置も変更されました。これは、スキャナーの内部的なテストモードのロジックが削除され、よりクリーンな初期化プロセスになったことを示しています。- 不要なコードの削除:
scanner.goからtestmodeに関連するフィールド(testmode,testpos)やメソッド(Error,expectNoErrors)が削除されました。また、lineposフィールドも削除されており、これはスキャナーが内部的に行の開始位置を追跡する必要がなくなったことを示唆しています。これらの削除は、スキャナーのコードベースをスリム化し、その主要な機能に集中させるためのものです。 - トークン定数の再編成:
scanner.goのトークン定数(ILLEGAL,EOF,IDENTなど)の順序が変更されました。これは機能的な変更ではありませんが、定数の論理的なグループ化や、将来的な拡張性を考慮した整理である可能性があります。 TokenStringの改善:TokenString関数で、トークン値を文字列に変換する際にUtils.IntToStringの代わりにstrconv.Itoaが使用されるようになりました。これは、標準ライブラリの関数を使用することで、依存関係を減らし、より効率的な変換を実現するための変更です。
-
pretty.goとtest.shの変更:pretty.goでは、flags.Testmodeに関連するコードが削除されました。これは、スキャナーからTestmodeの概念が取り除かれたことに伴う変更です。test.shでは、selftest1.goのテスト実行部分が削除されました。これもTestmodeの削除と関連しており、特定のテストモードに依存しない、より汎用的なテスト手法への移行を示しています。
これらの変更は、スキャナーがより効率的で、クリーンで、再利用可能なライブラリとして機能するようにするための包括的な取り組みの一環です。特に[]byteへの移行は、Go言語の設計思想である「シンプルさと効率性」を反映した重要な決定と言えます。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主にusr/gri/pretty/scanner.goと、それに伴うusr/gri/pretty/compilation.go、usr/gri/pretty/parser.go、usr/gri/pretty/platform.goの変更です。
usr/gri/pretty/scanner.go
- パッケージ名の変更:
package Scannerからpackage scannerへと小文字に変更されました。これはGoの慣習に従い、パッケージ名がディレクトリ名と一致するようにするためです。 Scanner構造体の変更:--- a/usr/gri/pretty/scanner.go +++ b/usr/gri/pretty/scanner.go @@ -273,25 +272,19 @@ func digit_val(ch int) int { type ErrorHandler interface { Error(pos int, msg string); - Warning(pos int, msg string); } type Scanner struct { // setup + src []byte; // source err ErrorHandler; - src string; // source scan_comments bool; // scanning pos int; // current reading position ch int; // one char look-ahead chpos int; // position of ch - linepos int; // position of beginning of line - - // testmode - testmode bool; - testpos int; }srcフィールドがstringから[]byteに変更され、linepos,testmode,testposフィールドが削除されました。next()メソッドの変更:--- a/usr/gri/pretty/scanner.go +++ b/usr/gri/pretty/scanner.go @@ -303,7 +296,7 @@ func (S *Scanner) next() { if S.pos < len(S.src) { r, w := int(S.src[S.pos]), 1; if r >= 0x80 { - // not ascii - r, w = utf8.DecodeRuneInString(S.src, S.pos); + // not ascii + r, w = utf8.DecodeRune(S.src[S.pos : len(S.src)]); } S.ch = r; S.chpos = S.pos;utf8.DecodeRuneInStringがutf8.DecodeRuneに変更されました。Init()メソッドの変更:--- a/usr/gri/pretty/scanner.go +++ b/usr/gri/pretty/scanner.go @@ -315,38 +308,16 @@ func (S *Scanner) next() { } -func (S *Scanner) Error(pos int, msg string) { - // check for expected errors (test mode) - if S.testpos < 0 || pos == S.testpos { - // test mode: - // S.testpos < 0: // follow-up errors are expected and ignored - // S.testpos == 0: // an error is expected at S.testpos and ignored - S.testpos = -1; - return; - } - - S.err.Error(pos, msg); -} - - -func (S *Scanner) expectNoErrors() { - // set the next expected error position to one after eof - // (the eof position is a legal error position!) - S.testpos = len(S.src) + 1; -} - - -func (S *Scanner) Init(err ErrorHandler, src string, scan_comments, testmode bool) { - S.err = err; +func (S *Scanner) error(pos int, msg string) { + S.err.Error(pos, msg); +} + + +func (S *Scanner) Init(src []byte, err ErrorHandler, scan_comments bool) { S.src = src; + S.err = err; S.scan_comments = scan_comments; - - S.pos = 0; - S.linepos = 0; - - S.testmode = testmode; - S.expectNoErrors(); // S.src must be set - S.next(); // S.expectNoErrrors() must be called before + S.next(); }Errorメソッドがプライベートなerrorメソッドに変更され、testmode関連のロジックが削除されました。Initの引数もsrc []byteを受け取るように変更されました。Scan()メソッドの戻り値の型変更:--- a/usr/gri/pretty/scanner.go +++ b/usr/gri/pretty/scanner.go @@ -676,7 +607,7 @@ func (S *Scanner) select4(tok0, tok1, ch2, tok2, tok3 int) int { } -func (S *Scanner) Scan() (pos, tok int, val string) { +func (S *Scanner) Scan() (pos, tok int, val []byte) { loop: S.skipWhitespace(); @@ -689,7 +620,7 @@ loop: S.next(); // always make progress switch ch { case -1: tok = EOF; - case '\n': tok, val = COMMENT, "\n"; + case '\n': tok, val = COMMENT, []byte('\n'); case '"': tok, val = STRING, S.scanString(); case '\'': tok, val = INT, S.scanChar(); case '`': tok, val = STRING, S.scanRawString();valの型がstringから[]byteに変更されました。他のscan*系のメソッドも同様に変更されています。
usr/gri/pretty/compilation.go
errorHandler構造体の変更:srcフィールドがstringから[]byteに変更されました。errorHandler.Initメソッドの変更:src引数がstringから[]byteに変更されました。errorHandler.LineColメソッドの変更:utf8.RuneCountInStringがutf8.RuneCountに変更されました。Compile関数の変更:scanner.Initとparser.Openの呼び出し引数が変更されました。
usr/gri/pretty/parser.go
ErrorHandlerインターフェースの導入:type ErrorHandler interface { Error(pos int, msg string); Warning(pos int, msg string); // Note: This Warning method was later removed in scanner.go's ErrorHandler }Parser構造体の変更:scannerフィールドの宣言位置が移動し、err ErrorHandlerフィールドが追加されました。next0()メソッドの変更:P.scanner.Scan()の戻り値valが[]byteで受け取られ、string(val)でP.valに代入されるようになりました。Open()メソッドの変更:ErrorHandlerを引数として受け取るように変更され、P.errに設定されるようになりました。error()メソッドの変更:P.scanner.Errorの呼び出しがP.err.Errorに変更されました。
usr/gri/pretty/platform.go
- ファイル読み込み関数の戻り値の型変更:
readfile,ReadObjectFile,ReadSourceFileがstringではなく[]byteを返すように変更されました。
usr/gri/pretty/pretty.go
flags.Testmode関連コードの削除:flag.BoolVarの呼び出しと、Compile後のエラー処理におけるflags.Testmodeの条件分岐が削除されました。
usr/gri/pretty/test.sh
selftest1.goテストの削除:Testmodeに関連するselftest1.goのテスト実行部分が削除されました。
コアとなるコードの解説
このコミットの核心は、Go言語のスキャナーがソースコードを処理する方法の根本的な変更にあります。
-
stringから[]byteへの移行:- 効率性: Goの
stringは不変のUTF-8バイトスライスですが、文字列操作(特に部分文字列の抽出や文字単位のアクセス)は、内部的にUTF-8デコード/エンコードのオーバーヘッドを伴うことがあります。[]byteにすることで、スキャナーはソースコードを生のバイト列として直接扱うことができ、メモリコピーや変換の回数を減らすことでパフォーマンスを向上させます。これは、コンパイラのフロントエンドのような、大量のテキストデータを高速に処理する必要があるコンポーネントにとって非常に重要です。 utf8パッケージの活用:utf8.DecodeRuneInStringからutf8.DecodeRuneへの変更は、この移行の直接的な結果です。どちらの関数もUTF-8バイト列からUnicodeルーンをデコードしますが、utf8.DecodeRuneは[]byteスライスを直接受け取るため、stringから[]byteへの暗黙的な変換が不要になります。- トークン値の表現: スキャナーが識別子、リテラル、コメントなどのトークンを抽出する際、それらを
[]byteとして返すようになりました。これにより、スキャナーの出力はより「生の」データとなり、後続のパーサーや他のツールが、必要に応じて最適な方法で文字列に変換したり、バイト列として直接処理したりする柔軟性が得られます。
- 効率性: Goの
-
インターフェースの簡素化と責務の分離:
ErrorHandlerの導入:ErrorHandlerインターフェースを導入し、エラー報告のメカニズムを抽象化することで、スキャナーは特定のエラー処理実装に依存しなくなりました。これにより、スキャナーはより汎用的なライブラリとして機能し、異なるコンテキスト(例: コンパイラ、リンタ、IDE)で異なるエラー報告戦略(例: コンソール出力、GUI表示、ログファイルへの書き込み)を使用できるようになります。Testmodeの削除:Testmodeは、おそらくスキャナーの内部的なデバッグやテストを容易にするための機能でした。しかし、このような内部的なメカニズムが外部に公開されたインターフェースに影響を与えることは、ライブラリとしての再利用性を損ないます。Testmodeの削除は、スキャナーのインターフェースをクリーンにし、その主要な責務(ソースコードをトークン化すること)に集中させるための重要なステップです。これにより、スキャナーのテストは、その公開されたインターフェースを介して行われるようになり、より堅牢なテスト戦略に移行したと考えられます。lineposの削除:lineposフィールドは、おそらくエラー報告のために行の開始位置を追跡するために使用されていました。このフィールドの削除は、エラーハンドリングのロジックが変更され、スキャナーが直接行情報を管理する必要がなくなったことを示唆しています。これは、エラーハンドラーがソースコード全体にアクセスできる場合、行/列情報の計算をエラーハンドラー自身に任せることで、スキャナーの責務をさらに限定できるためです。
これらの変更は、Go言語の設計哲学である「シンプルさ、効率性、そして明確な責務の分離」を反映しています。スキャナーをより堅牢で、高性能で、再利用可能なコンポーネントにすることで、Go言語のツールチェイン全体の品質と保守性が向上しました。
関連リンク
- Go言語の公式GitHubリポジトリ: https://github.com/golang/go
- Go言語の
go/scannerパッケージ(現在のスキャナーの実装): https://pkg.go.dev/go/scanner - Go言語の
go/tokenパッケージ(トークン定義): https://pkg.go.dev/go/token - Go言語の
utf8パッケージ: https://pkg.go.dev/unicode/utf8
参考にした情報源リンク
- Go言語のドキュメント: https://go.dev/doc/
- コンパイラの設計に関する一般的な情報源(例: Dragon Book)
- Go言語の初期のコミット履歴(GitHub)
- Go言語の
stringと[]byteに関する議論やベストプラクティス