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

[インデックス 10013] スキャナー位置の無効化による安全なエラー報告の実装

コミット

コミットハッシュ: df219d5197cb1e6fe3be7383466dfcf5d755b24f
作成者: Robert Griesemer gri@golang.org
日付: 2011年10月17日 16:35:12 -0700

コミットメッセージ:

scanner: invalidate scanner.Position when no token is present

scanner.Position is the position of the most recently
scanned token. Make sure it is invalid if there is no
token scanned and update corresponding comment. This
is particularly important when reporting errors.

Fixes #2371.

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/df219d5197cb1e6fe3be7383466dfcf5d755b24f

元コミット内容

このコミットは、Go言語のtext/scannerパッケージにおいて、スキャナーの位置情報(Position)の取り扱いを改善する重要な修正を行いました。具体的には、まだトークンがスキャンされていない状態でスキャナーのPosition(位置)を無効化することにより、より正確なエラー報告を可能にしました。

主な変更点:

  • スキャナーのInit()関数で位置を無効化(Line = 0に設定)
  • Next()関数でも位置を無効化
  • Scan()関数の開始時に位置を無効化
  • エラー報告時の位置情報を適切に処理するロジックを追加
  • コメントの更新により仕様を明確化

変更ファイル:

  • src/pkg/scanner/scanner.go (18行追加, 4行削除)
  • src/pkg/scanner/scanner_test.go (15行追加, 9行削除)

変更の背景

この修正は、Goの字句解析器(lexical analyzer)における位置情報の扱いに関する問題を解決するために行われました。従来の実装では、まだトークンがスキャンされていない状態でも、以前にスキャンされたトークンの位置情報がそのまま残っていたため、エラー報告時に誤った位置情報が表示される可能性がありました。

この問題は特に以下の場面で重要でした:

  1. エラー報告時の精度向上: まだトークンがスキャンされていない状態でエラーが発生した場合、適切な位置情報を提供する必要があった
  2. スキャナー状態の明確化: Position構造体がその時点でのスキャナーの状態を正確に反映することが重要だった
  3. デバッグの容易性: 開発者がエラーの発生位置を正確に特定できるようにする必要があった

前提知識の解説

字句解析(Lexical Analysis)とは

字句解析は、コンパイラの最初の段階で行われる処理です。ソースコードを文字単位で読み込み、それらを意味のある単位(トークン)に分割する作業です。この処理において、スキャナーは入力テキストを走査し、識別子、キーワード、演算子、リテラルなどのトークンを識別します。

Goのtext/scannerパッケージ

Go言語のtext/scannerパッケージは、UTF-8エンコードされたテキストに対してスキャナーとトークナイザーを提供します。このパッケージは以下の機能を提供します:

  • 文字単位でのスキャン: Next()関数で文字を一つずつ読み込む
  • トークン単位でのスキャン: Scan()関数でトークンを単位として読み込む
  • 位置情報の追跡: Position構造体でファイル名、行番号、列番号、バイトオフセットを記録
  • エラー処理: スキャン中に発生したエラーの報告と位置情報の提供

Position構造体の仕様

type Position struct {
    Filename string // ファイル名(存在する場合)
    Offset   int    // バイトオフセット(0から開始)
    Line     int    // 行番号(1から開始)
    Column   int    // 列番号(1から開始、1行あたりの文字数)
}

位置情報はLine > 0の場合に有効とされ、Line == 0の場合は無効な位置を示します。これにより、トークンがまだスキャンされていない状態を明確に表現できます。

Scanner構造体の内部状態

type Scanner struct {
    // 内部状態フィールド
    src        io.Reader
    srcBuf     [bufLen + 1]byte
    srcPos     int
    srcEnd     int
    ch         int
    tokPos     int
    
    // 位置情報
    Position   // 埋め込みフィールド
    
    // その他のフィールド
    ErrorCount int
    Mode       uint
    Whitespace uint64
    Error      func(s *Scanner, msg string)
}

2011年のGo言語の状況

2011年は、Go言語がまだ比較的新しく、多くの基本的な機能が開発・改善されていた時期です。この時期のGoは:

  • Go 1.0のリリース前(2012年3月)
  • 標準ライブラリの多くの部分が活発に開発されていた
  • コンパイラとランタイムの基本的な機能が固まりつつある状態
  • Robert Griesemer氏がGo言語の設計者の一人として活動していた

技術的詳細

修正された関数の詳細

1. Init()関数の修正

func (s *Scanner) Init(src io.Reader) *Scanner {
    s.src = src
    s.srcBuf[0] = utf8.RuneSelf
    s.srcPos = 0
    s.srcEnd = 0
    s.ch = -1
    s.Position.Filename = ""
    s.Position.Offset = 0
    s.Position.Line = 1
    s.Position.Column = 0
    s.ErrorCount = 0
    s.Mode = GoTokens
    s.Whitespace = GoWhitespace
+   s.Line = 0 // invalidate token position
    
    return s
}

この修正により、スキャナーの初期化時にs.Line = 0を設定することで、まだトークンがスキャンされていない状態を明示的に表現しています。

2. Next()関数の修正

func (s *Scanner) Next() int {
    s.tokPos = -1 // don't collect token text
+   s.Line = 0    // invalidate token position
    ch := s.Peek()
    s.ch = s.next()
    return ch
}

Next()関数は文字単位でスキャンを行うため、トークンレベルの位置情報は無効化されます。この関数は個々の文字を読み取るためのものであり、トークンの境界を越えて動作するため、トークンの位置情報は意味を持ちません。

3. Scan()関数の修正

func (s *Scanner) Scan() int {
    // reset token text position
    s.tokPos = -1
+   s.Line = 0

redo:
    // skip white space
    // ... (以下のコード)
}

Scan()関数の開始時に位置を無効化することで、新しいトークンの位置情報が適切に設定されることを保証します。トークンのスキャンが完了すると、その時点で正しい位置情報が設定されます。

4. エラー処理の改善

func (s *Scanner) error(msg string) {
    if s.Error != nil {
        s.Error(s, msg)
        return
    }
-   fmt.Fprintf(os.Stderr, "%s: %s\n", s.Position, msg)
+   pos := s.Position
+   if !pos.IsValid() {
+       pos = s.Pos()
+   }
+   fmt.Fprintf(os.Stderr, "%s: %s\n", pos, msg)
}

この修正により、位置情報が無効な場合はs.Pos()を呼び出して現在の位置を取得し、適切なエラー位置を報告できるようになりました。

位置情報の有効性判定

func (pos Position) IsValid() bool {
    return pos.Line > 0
}

この関数により、位置情報が有効かどうかを判定できます。Lineフィールドが0の場合は無効な位置を示し、1以上の場合は有効な位置を示します。

同期処理の考慮

スキャナーは基本的に単一のgoroutineで動作するため、複雑な同期処理は必要ありません。ただし、位置情報の整合性を保つために、状態変更のタイミングが重要になります。

コアとなるコードの変更箇所

1. スキャナー初期化時の位置無効化

ファイル: src/pkg/scanner/scanner.go:46

func (s *Scanner) Init(src io.Reader) *Scanner {
    // ... 他の初期化処理
    s.ErrorCount = 0
    s.Mode = GoTokens
    s.Whitespace = GoWhitespace
+   s.Line = 0 // invalidate token position
    return s
}

2. 文字単位スキャン時の位置無効化

ファイル: src/pkg/scanner/scanner.go:54

func (s *Scanner) Next() int {
    s.tokPos = -1 // don't collect token text
+   s.Line = 0    // invalidate token position
    ch := s.Peek()
    s.ch = s.next()
    return ch
}

3. トークンスキャン開始時の位置無効化

ファイル: src/pkg/scanner/scanner.go:75

func (s *Scanner) Scan() int {
    // reset token text position
    s.tokPos = -1
+   s.Line = 0
    
redo:
    // skip white space
    // ... (以下のコード)
}

4. エラー報告時の位置情報改善

ファイル: src/pkg/scanner/scanner.go:63-67

func (s *Scanner) error(msg string) {
    if s.Error != nil {
        s.Error(s, msg)
        return
    }
+   pos := s.Position
+   if !pos.IsValid() {
+       pos = s.Pos()
+   }
+   fmt.Fprintf(os.Stderr, "%s: %s\n", pos, msg)
}

コアとなるコードの解説

位置無効化の仕組み

Goのtext/scannerパッケージでは、Position構造体のLineフィールドが0の場合を「無効な位置」として定義しています。この設計により:

  1. 明確な状態表現: トークンがスキャンされていない状態を明確に示す
  2. エラー処理の改善: 無効な位置の場合は現在位置を計算して使用
  3. デバッグの容易性: 開発者が位置情報の有効性を簡単に判定できる

エラー処理の改善点

従来の実装では、位置情報が無効な場合でもs.Positionをそのまま使用していました。修正後は以下の流れで処理されます:

  1. s.Positionpos変数にコピー
  2. pos.IsValid()で位置の有効性を確認
  3. 無効な場合はs.Pos()を呼び出して現在の位置を取得
  4. 有効な位置情報を使用してエラーメッセージを出力

この変更により、エラーが発生した際の位置情報がより正確になり、デバッグが容易になりました。

テストケースの調整

テストファイルでは、Next()関数を使用した際の期待値が変更されています:

// 修正前
checkTok(t, s, 1, s.Next(), '=', "")
checkTok(t, s, 1, s.Next(), ' ', "")
checkTok(t, s, 1, s.Next(), 'b', "")

// 修正後
checkTok(t, s, 0, s.Next(), '=', "")
checkTok(t, s, 0, s.Next(), ' ', "")
checkTok(t, s, 0, s.Next(), 'b', "")

これはNext()関数が位置情報を無効化(Line = 0)するため、テストの期待値も0に変更されています。

Unicode文字の処理

テストファイルでは、Unicode文字の処理に関するコメントアウトされていたテストケースが有効化されています:

// 修正前(コメントアウト)
// TODO for unknown reasons these fail when checking the literals
/*
    token{Ident, "äöü"},
    token{Ident, "本"},
*/

// 修正後(有効化)
{Ident, "äöü"},
{Ident, "本"},

これにより、Unicode文字を含む識別子のテストが正常に実行されるようになりました。

関連リンク

参考にした情報源リンク