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

[インデックス 1806] ファイルの概要

このコミットは、Go言語の初期のコミットの一つであり、主に字句解析器(スキャナー)の機能強化と、tabwriterパッケージの軽微な修正を含んでいます。特に、スキャナーがバイト位置だけでなく、行番号と列番号も追跡するように変更された点が重要です。これにより、コンパイラやツールがより正確なエラーメッセージやデバッグ情報を提供できるようになります。

コミット

commit 68c69fac9efb9f5e65d2ad0bc9cadd92b0e7a398
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Mar 11 12:48:45 2009 -0700

    - scanner to track line/col number instead of byte position only
    - fixed a parameter name in tabwriter

    R=rsc
    DELTA=110  (21 added, 17 deleted, 72 changed)
    OCL=26123
    CL=26127

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

https://github.com/golang/go/commit/68c69fac9efb9f5e65d2ad0bc9cadd92b0e7a398

元コミット内容

  • スキャナーがバイト位置のみではなく、行番号と列番号も追跡するように変更。
  • tabwriterパッケージ内のパラメータ名を修正。

変更の背景

Go言語のコンパイラや開発ツールにおいて、ソースコード内のエラーや警告を報告する際、単なるバイトオフセットだけではユーザーにとって理解しにくい場合があります。例えば、「ファイル内の1234バイト目でエラーが発生しました」というメッセージよりも、「ファイルmain.goの10行目、25列目でエラーが発生しました」というメッセージの方が、開発者は問題の箇所を特定しやすくなります。

このコミットは、このようなより人間が読みやすいエラー報告を可能にするための基盤を構築することを目的としています。字句解析の段階で正確な行と列の情報を取得することで、後続の構文解析や意味解析、さらにはデバッグツールがこの情報を活用できるようになります。これは、Go言語のユーザーエクスペリエンスを向上させる上で不可欠な変更でした。

また、tabwriterパッケージのパラメータ名修正は、コードの可読性と一貫性を向上させるための一般的なリファクタリングの一環と考えられます。

前提知識の解説

字句解析(Lexical Analysis / Scanning)

字句解析は、コンパイラの最初のフェーズであり、ソースコードをトークン(token)と呼ばれる意味のある最小単位の並びに変換するプロセスです。例えば、if x > 0 { ... }というコードは、if(キーワード)、x(識別子)、>(演算子)、0(整数リテラル)、{(区切り文字)といったトークンに分解されます。この処理を行うプログラムを「字句解析器(lexer)」または「スキャナー(scanner)」と呼びます。

字句解析器は通常、ソースコードを先頭から1文字ずつ読み込み、定義されたパターン(正規表現など)に基づいてトークンを識別します。この際、各トークンの種類だけでなく、そのトークンがソースコードのどこに位置していたか(位置情報)も記録することが重要です。

位置情報(Source Position)

ソースコード内の位置情報は、通常、以下のいずれかまたは複数の形式で表現されます。

  1. バイトオフセット(Byte Offset): ソースファイルの先頭からのバイト数で位置を示します。これはプログラムにとって最も直接的な表現ですが、人間にとっては直感的ではありません。
  2. 行番号(Line Number): ソースコードの行数で位置を示します。通常1から始まります。
  3. 列番号(Column Number): 特定の行内での文字位置を示します。通常1から始まります。タブ文字の扱いなど、文字幅の計算には注意が必要です。

コンパイラやIDEは、これらの位置情報を組み合わせて、エラーメッセージの表示、デバッガでのブレークポイント設定、コードエディタでのハイライト表示などに利用します。

tabwriterパッケージ

Go言語の標準ライブラリにあるtext/tabwriterパッケージは、テキストを整形して、列がタブで揃うように調整するためのユーティリティを提供します。これは、コマンドラインツールの出力や、整形されたレポートなどを生成する際に非常に便利です。内部的には、タブ文字(\t)を検出して、その後のテキストが指定された幅に揃うようにスペースを挿入します。

技術的詳細

このコミットの主要な変更は、src/lib/go/scanner.goにおけるScanner構造体の内部状態と、それに関連するメソッドのシグネチャの変更です。

  1. Location構造体の導入:

    type Location struct {
        Pos int;  // byte position in source
        Line int;  // line count, starting at 1
        Col int;  // column, starting at 1 (character count)
    }
    

    この新しい構造体は、ソースコード内の位置をバイトオフセット、行番号、列番号の3つの情報で表現します。これにより、単一のint型でバイトオフセットのみを保持していた以前の設計よりも、はるかにリッチな位置情報を提供できるようになりました。

  2. ErrorHandlerインターフェースの変更:

    // 変更前: Error(pos int, msg string)
    // 変更後: Error(loc Location, msg string)
    type ErrorHandler interface {
        Error(loc Location, msg string);
    }
    

    エラーハンドリングのインターフェースが、単なるバイト位置ではなく、Location構造体を受け取るように変更されました。これにより、スキャナーがエラーを報告する際に、より詳細な位置情報を提供できるようになり、エラーメッセージの質が向上します。

  3. Scanner構造体の内部状態の変更:

    type Scanner struct {
        // ...
        loc Location;  // location of ch
        pos int;  // current reading position (position after ch)
        ch int;  // one char look-ahead
        // chpos int;  // 削除されたフィールド
    }
    

    Scanner構造体からchpos intフィールドが削除され、代わりにloc Locationフィールドが導入されました。locは現在処理中の文字chの開始位置を示し、poschの次の文字の開始位置(つまり、chが消費された後の位置)を示します。これにより、スキャナーは常に現在の文字の正確な行と列の情報を保持できるようになります。

  4. next()メソッドのロジック変更: next()メソッドは、スキャナーが次の文字を読み込む際に呼び出されます。このコミットでは、next()の内部ロジックが大幅に強化され、文字を読み込むたびに行番号と列番号を適切に更新するようになりました。

    • S.loc.Pos = S.pos;:現在の文字のバイト位置をloc.Posに設定。
    • S.loc.Col++;:列番号をインクリメント。
    • case r == '\n': S.loc.Line++; S.loc.Col = 1;:改行文字を検出した場合、行番号をインクリメントし、列番号を1にリセット。
    • UTF-8文字のデコードも考慮され、マルチバイト文字の場合でもS.posが正しく進むようにutf8.DecodeRuneが使用されています。
  5. Scan()メソッドのシグネチャ変更と内部ロジックの調整:

    // 変更前: func (S *Scanner) Scan() (pos, tok int, lit []byte)
    // 変更後: func (S *Scanner) Scan() (loc Location, tok int, lit []byte)
    

    Scan()メソッドは、次のトークンをスキャンしてその情報を返します。このメソッドの戻り値にLocationが追加され、トークンの開始位置がより詳細に提供されるようになりました。 また、scanComment, scanIdentifier, scanNumber, scanChar, scanString, scanRawStringといった個別のスキャンメソッドが、以前はリテラルテキスト([]byte)を直接返していましたが、この変更により、それらのメソッドはリテラルテキストを返さなくなり、代わりにScan()メソッドがS.src[loc.Pos : S.loc.Pos]を使って最終的にリテラルを構築して返すようになりました。これは、リテラルテキストの生成を一元化し、コードの重複を減らすためのリファクタリングです。

  6. tabwriter.goの変更: NewWriter関数の引数名がwriterからoutputに変更されました。これは機能的な変更ではなく、コードの意図をより明確にするための命名規則の改善です。

これらの変更は、Go言語のコンパイラがより堅牢でユーザーフレンドリーなエラー報告システムを構築するための重要なステップでした。

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

src/lib/go/scanner.go

  • Location構造体の追加。
  • ErrorHandlerインターフェースのErrorメソッドのシグネチャ変更。
  • Scanner構造体からchposフィールドを削除し、loc Locationフィールドを追加。
  • next()メソッドのロジックを修正し、行番号と列番号の追跡を追加。
  • error()およびexpect()メソッドがLocationを受け取るように変更。
  • scanComment, scanIdentifier, scanNumber, scanChar, scanString, scanRawStringメソッドのシグネチャと内部ロジックを変更し、リテラルテキストの返却をScan()メソッドに一元化。
  • Scan()メソッドの戻り値にLocationを追加。

src/lib/go/scanner_test.go

  • TestErrorHandlerErrorメソッドのシグネチャをscanner.Locationを受け取るように変更。
  • テスト内の位置比較ロジックをloc.Posを使用するように変更。

src/lib/tabwriter/tabwriter.go

  • NewWriter関数の引数名writeroutputにリネーム。

コアとなるコードの解説

src/lib/go/scanner.goにおけるnext()メソッドの変更

func (S *Scanner) next() {
    if S.pos < len(S.src) {
        S.loc.Pos = S.pos; // 現在の文字のバイト位置を記録
        S.loc.Col++;       // 列番号をインクリメント

        r, w := int(S.src[S.pos]), 1;
        switch {
        case r == '\n': // 改行文字の場合
            S.loc.Line++; // 行番号をインクリメント
            S.loc.Col = 1; // 列番号を1にリセット
        case r >= 0x80: // ASCII範囲外の文字(UTF-8マルチバイト文字)の場合
            r, w = utf8.DecodeRune(S.src[S.pos : len(S.src)]); // ルーンとバイト数をデコード
        }
        S.pos += w; // 次の文字の開始バイト位置に進む
        S.ch = r;   // 次の文字をchに設定
    } else {
        S.loc.Pos = len(S.src); // ファイル終端の場合、バイト位置をファイルの長さに設定
        S.ch = -1;  // EOFを示す
    }
}

このnext()メソッドは、スキャナーの心臓部であり、ソースコードを1文字ずつ読み進めるたびに行と列の情報を正確に更新します。特に、改行文字(\n)を検出した際に行番号をインクリメントし、列番号をリセットするロジックは、正確な位置情報追跡の鍵となります。また、UTF-8のマルチバイト文字も適切に処理することで、国際化されたソースコードにも対応しています。

src/lib/go/scanner.goにおけるScan()メソッドの変更

func (S *Scanner) Scan() (loc Location, tok int, lit []byte) {
scan_again:
    S.skipWhitespace(); // 空白文字をスキップ

    loc, tok = S.loc, token.ILLEGAL; // 現在のロケーションと初期トークンを設定

    switch ch := S.ch; {
    case isLetter(ch):
        tok = S.scanIdentifier(); // 識別子をスキャン
    case digitVal(ch) < 10:
        tok = S.scanNumber(false); // 数値をスキャン
    default:
        S.next(); // 常に進行
        switch ch {
        // ... 各種トークンのスキャンロジック ...
        case '"' : tok = token.STRING; S.scanString(loc); // 文字列をスキャンし、locを渡す
        // ...
        case '/':
            if S.ch == '/' || S.ch == '*' {
                S.scanComment(loc); // コメントをスキャンし、locを渡す
                tok = token.COMMENT;
                if !S.scan_comments {
                    goto scan_again; // コメントをトークンとして扱わない場合、再度スキャン
                }
            } else {
                // ...
            }
        // ...
        default: S.error(loc, "illegal character " + charString(ch)); // エラー報告もlocを使用
        }
    }

    return loc, tok, S.src[loc.Pos : S.loc.Pos]; // トークンのロケーション、種類、リテラルを返す
}

Scan()メソッドは、次のトークンを識別し、その種類(tok)と、ソースコード上の正確な位置(loc)、そしてそのトークンに対応する元のテキスト(lit)を返します。以前は各スキャンメソッドがリテラルを返していましたが、この変更により、Scan()メソッドの最後にS.src[loc.Pos : S.loc.Pos]という形で、トークンの開始位置から現在のスキャナーの位置までのバイトスライスを切り出すことで、リテラルテキストを一元的に生成するようになりました。これにより、コードの重複が削減され、保守性が向上しています。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(GitHub): https://github.com/golang/go
  • コンパイラの設計に関する一般的な情報源(例: Dragon Book)
  • Go言語の初期の設計に関する議論(Go言語のメーリングリストや初期のブログ記事など)