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

[インデックス 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言語の識別子の定義(英字、数字、アンダースコアで構成され、数字で始まらない)は、すべてのテキスト形式やカスタム言語の要件に合致するわけではありません。

この変更の背景には、以下のようなニーズがあったと考えられます。

  1. カスタム言語やDSL(Domain Specific Language)の解析: Go言語の構文とは異なる識別子ルールを持つカスタム言語やDSLをGoで実装する際に、text/scanner を再利用できるようにするため。例えば、特定の記号を識別子の一部として許可したい場合や、数字で始まる識別子を許可したい場合などです。
  2. 特定のファイル形式の解析: 設定ファイルやデータ形式など、Go言語の識別子ルールとは異なる命名規則を持つテキストファイルを解析する際に、柔軟に対応できるようにするため。
  3. 既存のツールの拡張性向上: 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

	// ... 既存のフィールド ...
}

IsIdentRunefunc(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文字目以降は数字も許可)に従って判定を行います。この抽象化により、scanIdentifierScan メソッドは、カスタムルールが設定されているかどうかを意識することなく、常に 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

  1. 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.
    
  2. 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
    
  3. 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

  1. 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言語の識別子判定ロジックのいずれかを透過的に呼び出す役割を担います。これにより、scanIdentifierScan といった他のメソッドは、カスタムロジックの有無を意識することなく、常にこのヘルパーメソッドを呼び出すだけで済み、コードの簡潔性と保守性が向上します。

scanIdentifier メソッドの変更

scanIdentifier は、識別子の最初の文字が認識された後に、その識別子を構成する残りの文字を読み進めるためのメソッドです。このメソッドが s.isIdentRune(ch, i) を使用するように変更されたことで、識別子の各文字がカスタムルールに合致するかどうかが動的に判定されるようになりました。これにより、text/scanner は、Go言語の識別子だけでなく、ユーザーが定義した任意の識別子パターンを正確に抽出できるようになります。

Scan メソッド内の識別子判定ロジックの変更

Scan メソッドは、入力ストリームから次のトークンを読み取る主要なエントリポイントです。このメソッド内で、文字が識別子の開始文字であるかを判定する部分が s.isIdentRune(ch, 0) に変更されました。これは、識別子の最初の文字がカスタムルールに合致するかどうかをチェックすることを意味します。この変更により、text/scanner は、カスタム識別子ルールに基づいてトークンの種類を正しく識別し、その後の scanIdentifier メソッドに処理を渡すことができるようになりました。

TestScanCustomIdent テスト関数の追加

このテストは、新しく追加された IsIdentRune 機能が意図通りに動作することを確認するためのものです。具体的なカスタム識別子ルールを設定し、そのルールに従ってテキストが正しくスキャンされ、トークン化されることを検証しています。これにより、開発者はこの機能の正しい使い方を理解し、その動作を信頼することができます。

関連リンク

参考にした情報源リンク