[インデックス 19548] ファイルの概要
このコミットは、Go言語の標準ライブラリである text/scanner
パッケージに、カスタム識別子(identifier)を定義する機能を追加するものです。これにより、ユーザーはGo言語の標準的な識別子のルールに縛られず、独自の構文解析要件に合わせて識別子の構成文字を柔軟に指定できるようになります。具体的には、Scanner
構造体に IsIdentRune
という関数フィールドが追加され、識別子を構成する各ルーン(Unicodeコードポイント)が有効であるかを判定するためのカスタムロジックを注入できるようになりました。
コミット
commit 60c0b3b5cf89c8054328e27d7ee58da79a12999e
Author: Robert Griesemer <gri@golang.org>
Date: Mon Jun 16 16:32:47 2014 -0700
text/scanner: provide facility for custom identifiers
LGTM=r
R=golang-codereviews, r
CC=golang-codereviews
https://golang.org/cl/108030044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/60c0b3b5cf89c8054328e27d7ee58da79a12999e
元コミット内容
text/scanner: provide facility for custom identifiers
LGTM=r
R=golang-codereviews, r
CC=golang-codereviews
https://golang.org/cl/108030044
変更の背景
text/scanner
パッケージは、Go言語のソースコードや類似のテキスト形式を字句解析(スキャン)するための基本的な機能を提供します。しかし、Go言語の識別子の定義(英字、数字、アンダースコアで構成され、数字で始まらない)は、すべてのテキスト形式やカスタム言語の要件に合致するわけではありません。
この変更の背景には、以下のようなニーズがあったと考えられます。
- カスタム言語やDSL(Domain Specific Language)の解析: Go言語の構文とは異なる識別子ルールを持つカスタム言語やDSLをGoで実装する際に、
text/scanner
を再利用できるようにするため。例えば、特定の記号を識別子の一部として許可したい場合や、数字で始まる識別子を許可したい場合などです。 - 特定のファイル形式の解析: 設定ファイルやデータ形式など、Go言語の識別子ルールとは異なる命名規則を持つテキストファイルを解析する際に、柔軟に対応できるようにするため。
- 既存のツールの拡張性向上:
text/scanner
はGoコンパイラやその他のツールで利用される可能性があり、その汎用性を高めることで、より多様な字句解析タスクに対応できるようになります。
この機能が導入される前は、Go言語の識別子ルールに合わないテキストを解析する場合、text/scanner
を使うと不便であったり、カスタムの字句解析器をゼロから実装する必要がありました。今回の変更により、既存の text/scanner
の堅牢な基盤を活かしつつ、識別子の定義部分だけをカスタマイズできるようになったため、開発の効率と柔軟性が大幅に向上しました。
前提知識の解説
字句解析(Lexical Analysis / Scanning)
字句解析とは、コンパイラやインタプリタの最初の段階であり、入力された文字列(ソースコードなど)を意味のある最小単位(トークン)の並びに変換するプロセスです。このプロセスを担当するプログラムを「字句解析器(lexer)」または「スキャナー(scanner)」と呼びます。
例えば、x = 10 + y
というコードがあった場合、字句解析器はこれを以下のようなトークンに分解します。
x
(識別子)=
(代入演算子)10
(整数リテラル)+
(加算演算子)y
(識別子)
text/scanner
パッケージは、この字句解析の機能を提供します。
トークン(Token)
トークンは、字句解析によって識別される意味のある最小単位です。トークンには種類(例: 識別子、キーワード、演算子、リテラル)と、そのトークンが表す実際の値(字句、lexeme)があります。
識別子(Identifier)
識別子とは、プログラミング言語において変数名、関数名、型名などを区別するために使われる名前のことです。Go言語では、識別子はUnicodeの文字、数字、アンダースコア _
で構成され、数字で始まることはできません。また、予約語(if
, for
, func
など)は識別子として使用できません。
text/scanner
パッケージ
Go言語の text/scanner
パッケージは、Go言語の字句規則に準拠したテキストをスキャンするための汎用的なスキャナーを提供します。主な機能は以下の通りです。
- 入力ソース(
io.Reader
)からの文字の読み込み。 - 空白文字やコメントのスキップ。
- Go言語の識別子、キーワード、リテラル(整数、浮動小数点数、文字列、ルーン)の認識。
- トークンの種類と値、そしてソースコード上の位置(行番号、列番号)の提供。
このパッケージは、Go言語のパーサーを構築する際や、Go言語に似た構文を持つテキストファイルを処理する際に非常に便利です。
ルーン(Rune)
Go言語における「ルーン」は、Unicodeのコードポイントを表す int32
型のエイリアスです。Go言語の文字列はUTF-8でエンコードされており、1つのルーンが1バイト以上になることがあります。text/scanner
は文字単位で処理を行うため、ルーンの概念が重要になります。
技術的詳細
このコミットの技術的な核心は、text/scanner.Scanner
構造体に IsIdentRune
という新しいフィールドを追加し、識別子の構成文字を判定するロジックを外部から注入可能にした点にあります。
Scanner
構造体への IsIdentRune
フィールドの追加
type Scanner struct {
// ... 既存のフィールド ...
// IsIdentRune is a predicate controlling the characters accepted
// as the ith rune in an identifier. The set of valid characters
// must not intersect with the set of white space characters.
// If no IsIdentRune function is set, regular Go identifiers are
// accepted instead. The field may be changed at any time.
IsIdentRune func(ch rune, i int) bool
// ... 既存のフィールド ...
}
IsIdentRune
は func(ch rune, i int) bool
型の関数ポインタです。
ch
: 現在評価しているルーン。i
: 識別子内のルーンのインデックス(0から始まる)。最初の文字であれば0
、2番目の文字であれば1
となります。これにより、識別子の先頭文字とそれ以降の文字で異なるルールを適用することが可能になります(例: Go言語の識別子は数字で始まらないが、2文字目以降は数字を許容する)。 この関数がtrue
を返すと、そのルーンは識別子の一部として受け入れられます。nil
の場合、デフォルトのGo言語の識別子ルールが適用されます。
isIdentRune
ヘルパーメソッドの導入
Scanner
構造体には、IsIdentRune
フィールドの有無をチェックし、適切な識別子判定ロジックを呼び出すための内部ヘルパーメソッド isIdentRune
が追加されました。
func (s *Scanner) isIdentRune(ch rune, i int) bool {
if s.IsIdentRune != nil {
return s.IsIdentRune(ch, i)
}
// Default Go identifier rules:
// '_' or letter for the first character (i == 0),
// '_' or letter or digit for subsequent characters (i > 0).
return ch == '_' || unicode.IsLetter(ch) || unicode.IsDigit(ch) && i > 0
}
このメソッドは、s.IsIdentRune
が設定されていればそれを呼び出し、そうでなければGo言語の標準的な識別子ルール(アンダースコアまたは文字、そして2文字目以降は数字も許可)に従って判定を行います。この抽象化により、scanIdentifier
や Scan
メソッドは、カスタムルールが設定されているかどうかを意識することなく、常に s.isIdentRune
を呼び出すだけでよくなりました。
scanIdentifier
メソッドの変更
識別子をスキャンする scanIdentifier
メソッドは、s.isIdentRune
ヘルパーメソッドを使用するように変更されました。
func (s *Scanner) scanIdentifier() rune {
// we know the zero'th rune is OK; start with 2nd one
ch := s.next()
for i := 1; s.isIdentRune(ch, i); i++ { // ここが変更点
ch = s.next()
}
return ch
}
以前は for ch == '_' || unicode.IsLetter(ch) || unicode.IsDigit(ch)
のようにGo言語の識別子ルールがハードコードされていましたが、s.isIdentRune(ch, i)
を呼び出すことで、カスタムルールが適用されるようになりました。ループの i
は識別子内のルーンのインデックスを表し、isIdentRune
に渡されます。
Scan
メソッドの変更
Scan
メソッドは、次のトークンを決定する主要なメソッドです。このメソッドも、識別子の開始文字を判定する部分で s.isIdentRune
を使用するように変更されました。
func (s *Scanner) Scan() rune {
// ...
switch {
case s.isIdentRune(ch, 0): // ここが変更点
if s.Mode&ScanIdents != 0 {
tok = Ident
ch = s.scanIdentifier()
}
// ...
}
// ...
}
unicode.IsLetter(ch) || ch == '_'
というGo言語の識別子開始文字の判定ロジックが、s.isIdentRune(ch, 0)
に置き換えられました。これにより、カスタム IsIdentRune
関数が設定されている場合、その関数が識別子の最初の文字の有効性を判定する役割を担うことになります。
scanner_test.go
におけるテストケース
TestScanCustomIdent
という新しいテストケースが追加され、この新機能の動作が検証されています。このテストでは、IsIdentRune
を以下のように設定しています。
s.IsIdentRune = func(ch rune, i int) bool {
return i == 0 && (ch == 'a' || ch == 'b') || 0 < i && i < 4 && '0' <= ch && ch <= '3'
}
このカスタムルールは、「識別子の最初の文字は 'a' または 'b' でなければならず、2文字目から4文字目までは '0' から '3' までの数字でなければならない(最大長4)」という非常に具体的なものです。このテストケースは、text/scanner
がこのカスタムルールに従って正しくトークンを識別できることを示しており、新機能の柔軟性を実証しています。
コアとなるコードの変更箇所
src/pkg/text/scanner/scanner.go
-
Scanner
構造体へのIsIdentRune
フィールドの追加:--- a/src/pkg/text/scanner/scanner.go +++ b/src/pkg/text/scanner/scanner.go @@ -164,6 +162,13 @@ type Scanner struct { // for values ch > ' '). The field may be changed at any time. Whitespace uint64 + // IsIdentRune is a predicate controlling the characters accepted + // as the ith rune in an identifier. The set of valid characters + // must not intersect with the set of white space characters. + // If no IsIdentRune function is set, regular Go identifiers are + // accepted instead. The field may be changed at any time. + IsIdentRune func(ch rune, i int) bool + // Start position of most recently scanned token; set by Scan. // Calling Init or Next invalidates the position (Line == 0). // The Filename field is always left untouched by the Scanner.
-
isIdentRune
ヘルパーメソッドの追加:--- a/src/pkg/text/scanner/scanner.go +++ b/src/pkg/text/scanner/scanner.go @@ -334,9 +339,17 @@ func (s *Scanner) error(msg string) { fmt.Fprintf(os.Stderr, "%s: %s\n", pos, msg) } +func (s *Scanner) isIdentRune(ch rune, i int) bool { + if s.IsIdentRune != nil { + return s.IsIdentRune(ch, i) + } + return ch == '_' || unicode.IsLetter(ch) || unicode.IsDigit(ch) && i > 0 +} + func (s *Scanner) scanIdentifier() rune { - ch := s.next() // read character after first '_' or letter - for ch == '_' || unicode.IsLetter(ch) || unicode.IsDigit(ch) { + // we know the zero'th rune is OK; start with 2nd one + ch := s.next() + for i := 1; s.isIdentRune(ch, i); i++ { ch = s.next() } return ch
-
Scan
メソッド内の識別子判定ロジックの変更:--- a/src/pkg/text/scanner/scanner.go +++ b/src/pkg/text/scanner/scanner.go @@ -563,7 +576,7 @@ redo: // determine token value tok := ch switch { - case unicode.IsLetter(ch) || ch == '_': + case s.isIdentRune(ch, 0): if s.Mode&ScanIdents != 0 { tok = Ident ch = s.scanIdentifier()
src/pkg/text/scanner/scanner_test.go
TestScanCustomIdent
テスト関数の追加:--- a/src/pkg/text/scanner/scanner_test.go +++ b/src/pkg/text/scanner/scanner_test.go @@ -357,6 +357,28 @@ func TestScanSelectedMask(t *testing.T) { testScanSelectedMode(t, ScanComments, Comment) } +func TestScanCustomIdent(t *testing.T) { + const src = "faab12345 a12b123 a12 3b" + s := new(Scanner).Init(strings.NewReader(src)) + // ident = ( 'a' | 'b' ) { digit } . + // digit = '0' .. '3' . + // with a maximum length of 4 + s.IsIdentRune = func(ch rune, i int) bool { + return i == 0 && (ch == 'a' || ch == 'b') || 0 < i && i < 4 && '0' <= ch && ch <= '3' + } + checkTok(t, s, 1, s.Scan(), 'f', "f") + checkTok(t, s, 1, s.Scan(), Ident, "a") + checkTok(t, s, 1, s.Scan(), Ident, "a") + checkTok(t, s, 1, s.Scan(), Ident, "b123") + checkTok(t, s, 1, s.Scan(), Int, "45") + checkTok(t, s, 1, s.Scan(), Ident, "a12") + checkTok(t, s, 1, s.Scan(), Ident, "b123") + checkTok(t, s, 1, s.Scan(), Ident, "a12") + checkTok(t, s, 1, s.Scan(), Int, "3") + checkTok(t, s, 1, s.Scan(), Ident, "b") + checkTok(t, s, 1, s.Scan(), EOF, "") +} + func TestScanNext(t *testing.T) { const BOM = '\uFEFF' BOMs := string(BOM)
コアとなるコードの解説
Scanner
構造体への IsIdentRune
フィールドの追加
このフィールドは、text/scanner
のユーザーが識別子の構成ルールをカスタマイズするための「フック」を提供します。IsIdentRune
に関数を割り当てることで、Scanner
はその関数を使って各ルーンが識別子の一部として有効かどうかを判定するようになります。これにより、Go言語の標準的な識別子ルールに縛られずに、任意の字句規則を適用できるようになります。i
パラメータがあることで、識別子の先頭文字とそれ以降の文字で異なるルールを適用できる柔軟性も提供されます。
isIdentRune
ヘルパーメソッドの追加
このプライベートメソッドは、IsIdentRune
フィールドが設定されているかどうかに応じて、カスタムの判定ロジックまたはデフォルトのGo言語の識別子判定ロジックのいずれかを透過的に呼び出す役割を担います。これにより、scanIdentifier
や Scan
といった他のメソッドは、カスタムロジックの有無を意識することなく、常にこのヘルパーメソッドを呼び出すだけで済み、コードの簡潔性と保守性が向上します。
scanIdentifier
メソッドの変更
scanIdentifier
は、識別子の最初の文字が認識された後に、その識別子を構成する残りの文字を読み進めるためのメソッドです。このメソッドが s.isIdentRune(ch, i)
を使用するように変更されたことで、識別子の各文字がカスタムルールに合致するかどうかが動的に判定されるようになりました。これにより、text/scanner
は、Go言語の識別子だけでなく、ユーザーが定義した任意の識別子パターンを正確に抽出できるようになります。
Scan
メソッド内の識別子判定ロジックの変更
Scan
メソッドは、入力ストリームから次のトークンを読み取る主要なエントリポイントです。このメソッド内で、文字が識別子の開始文字であるかを判定する部分が s.isIdentRune(ch, 0)
に変更されました。これは、識別子の最初の文字がカスタムルールに合致するかどうかをチェックすることを意味します。この変更により、text/scanner
は、カスタム識別子ルールに基づいてトークンの種類を正しく識別し、その後の scanIdentifier
メソッドに処理を渡すことができるようになりました。
TestScanCustomIdent
テスト関数の追加
このテストは、新しく追加された IsIdentRune
機能が意図通りに動作することを確認するためのものです。具体的なカスタム識別子ルールを設定し、そのルールに従ってテキストが正しくスキャンされ、トークン化されることを検証しています。これにより、開発者はこの機能の正しい使い方を理解し、その動作を信頼することができます。
関連リンク
- Go CL 108030044: https://golang.org/cl/108030044
- Go
text/scanner
パッケージドキュメント: https://pkg.go.dev/text/scanner
参考にした情報源リンク
- Go
text/scanner
パッケージのソースコード (コミット時点): https://github.com/golang/go/tree/60c0b3b5cf89c8054328e27d7ee58da79a12999e/src/pkg/text/scanner - Go言語仕様 (識別子に関する記述): https://go.dev/ref/spec#Identifiers
- 字句解析に関する一般的な情報 (例: Wikipedia): https://ja.wikipedia.org/wiki/%E5%AD%97%E5%8F%A5%E8%A7%A3%E6%9E%90
- Unicode
unicode
パッケージドキュメント: https://pkg.go.dev/unicode