[インデックス 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
に関する議論やベストプラクティス