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

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

このコミットは、Go言語のコンパイラツールチェインにおける、ソースコード上の位置情報(Position)の管理方法に関する重要なリファクタリングです。具体的には、scannerパッケージに存在していたLocation構造体をtokenパッケージに移動し、Positionと改名することで、位置情報の表現を一元化し、依存関係を整理しています。これにより、Go言語のパーサーや抽象構文木(AST)における位置情報の扱いがより明確かつ効率的になりました。

コミット

commit 5a72ca45fba1aad8ab08d66cd73fd40560407dbd
Author: Robert Griesemer <gri@golang.org>
Date:   Thu Mar 26 16:08:44 2009 -0700

    - renamed scanner.Location to token.Position
    - by moving Position into token, scanner dependencies
      are removed from several files
    - clearer field names in token.Position, now possible to
      have a Pos() accessor w/o naming conflicts
    - added Pos() accessor
    - use anonymous token.Position field in AST nodes
    
    R=r
    DELTA=244  (28 added, 55 deleted, 161 changed)
    OCL=26786
    CL=26793

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

https://github.com/golang/go/commit/5a72ca45fba1aad8ab08d66cd73fd40560407dbd

元コミット内容

このコミットの元の内容は以下の通りです。

  • scanner.Locationtoken.Position にリネームしました。
  • Positiontoken パッケージに移動することで、いくつかのファイルから scanner への依存関係が削除されました。
  • token.Position のフィールド名がより明確になり、名前の衝突なしに Pos() アクセサを持つことが可能になりました。
  • Pos() アクセサを追加しました。
  • ASTノードで匿名(埋め込み)の token.Position フィールドを使用しました。

変更の背景

Go言語のコンパイラは、ソースコードを解析する際に、各トークンや構文要素がソースコードのどの位置にあるかを正確に記録する必要があります。これは、エラーメッセージの表示、デバッグ情報の生成、コードフォーマットツール(go fmtなど)の動作に不可欠です。

このコミット以前は、ソースコード上の位置情報を表すLocation構造体がscannerパッケージ内に定義されていました。しかし、位置情報はスキャナーだけでなく、パーサーが構築する抽象構文木(AST)の各ノードにも必要とされます。Locationscannerパッケージに存在すると、ASTノードがscannerパッケージに依存することになり、モジュール間の結合度が高まっていました。

この状況は、Goコンパイラの設計原則である「疎結合」に反し、将来的な変更や拡張の際に不必要な複雑さをもたらす可能性がありました。また、Location構造体のフィールド名(Pos, Line, Col)も、より汎用的な「位置」を表すには曖昧な部分がありました。

このコミットは、これらの問題を解決し、位置情報の管理をより体系的かつ効率的にするためのリファクタリングとして行われました。

前提知識の解説

このコミットを理解するためには、Go言語のコンパイラがソースコードを処理する際の基本的な段階と、関連するパッケージの役割について理解しておく必要があります。

  1. スキャナー (Lexer/Tokenizer):

    • ソースコードの文字列を読み込み、意味のある最小単位である「トークン」(キーワード、識別子、演算子、リテラルなど)に分割する役割を担います。
    • Go言語では、go/scannerパッケージがこの機能を提供します。
    • 各トークンがソースコードのどこから始まったか、という位置情報(行番号、列番号、バイトオフセット)を生成します。
  2. トークン (Token):

    • スキャナーによって識別された各トークンの種類(例: IDENTADDFOR)を定義します。
    • Go言語では、go/tokenパッケージがトークンの種類と、このコミットで導入されるPosition構造体を定義します。
  3. パーサー (Parser):

    • スキャナーが生成したトークンのストリームを読み込み、Go言語の文法規則に従って、それらを構造化されたデータ表現である「抽象構文木(AST)」に変換します。
    • Go言語では、go/parserパッケージがこの機能を提供します。
    • ASTの各ノードは、対応するソースコードの断片の位置情報を持つ必要があります。
  4. 抽象構文木 (AST - Abstract Syntax Tree):

    • ソースコードの構造を木構造で表現したものです。各ノードは、変数宣言、関数呼び出し、式などのGo言語の構文要素に対応します。
    • Go言語では、go/astパッケージがASTのノード構造を定義します。
    • ASTは、コンパイラのセマンティック解析、コード生成、および各種ツール(go fmt, go vetなど)の入力として使用されます。
  5. Go言語の埋め込み(Anonymous Field Embedding):

    • Go言語のユニークな機能の一つで、構造体の中にフィールド名なしで別の構造体を埋め込むことができます。
    • 埋め込まれた構造体のフィールドやメソッドは、外側の構造体のフィールドやメソッドであるかのように直接アクセスできます。これは、継承に似た振る舞いを実現しますが、より柔軟な「コンポジション(合成)」のメカニズムです。
    • このコミットでは、ASTノードにtoken.Positionを匿名で埋め込むことで、各ASTノードが自動的に位置情報とそのアクセサメソッドを持つようにしています。

技術的詳細

このコミットの技術的な核心は、ソースコード上の位置情報を表すデータ構造の定義と、その利用方法の変更にあります。

  1. scanner.Location から token.Position への移動と改名:

    • 変更前: scannerパッケージ内にLocation構造体がありました。
      // src/lib/go/scanner.go (変更前)
      type Location struct {
          Pos int;  // byte position in source
          Line int;  // line count, starting at 1
          Col int;  // column, starting at 1 (character count)
      }
      
    • 変更後: tokenパッケージ内にPosition構造体が定義されました。
      // src/lib/go/token.go (変更後)
      type Position struct {
          Offset int;  // byte offset, starting at 0
          Line int;  // line number, starting at 1
          Column int;  // column number, starting at 1 (character count)
      }
      
    • この変更により、位置情報がスキャナー固有のものではなく、Go言語のトークンと構文解析全体で共有される基本的な概念として昇格しました。tokenパッケージは、Go言語の構文解析における基本的な構成要素(トークン、位置)を定義する役割を担うため、Positionの配置場所としてより適切です。
  2. フィールド名の明確化:

    • Pos -> Offset: ソースコードの先頭からのバイトオフセットを明確に示します。
    • Col -> Column: 列番号であることを明確にします。
    • これにより、各フィールドが表す意味がより直感的になりました。
  3. Pos() アクセサメソッドの追加:

    • token.PositionPos() Positionというメソッドが追加されました。
      // src/lib/go/token.go (変更後)
      func (pos *Position) Pos() Position {
          return *pos;
      }
      
    • このメソッドは、Position構造体自体を返すシンプルなアクセサです。これは、後述する匿名フィールド埋め込みと組み合わせて、ASTノードから位置情報に統一的にアクセスするための重要な役割を果たします。
  4. ASTノードでの匿名フィールド埋め込みの利用:

    • 変更前は、ASTノードはPos_ Positionのような明示的なフィールドを持っていました。そして、各ASTノード型でPos() Positionメソッドを個別に実装し、そのPos_フィールドを返していました。
      // usr/gri/pretty/ast.go (変更前 - 例)
      type BadExpr struct {
          Pos_ Position;  // beginning position of bad expression
      };
      func (x *BadExpr) Pos() Position  { return x.Pos_; }
      
    • 変更後、多くのASTノードでtoken.Positionが匿名フィールドとして埋め込まれるようになりました。
      // usr/gri/pretty/ast.go (変更後 - 例)
      type BadExpr struct {
          token.Position;  // beginning position of bad expression
      };
      // BadExprは自動的にtoken.Positionのフィールドとメソッド(Pos()を含む)を持つ
      
    • これにより、各ASTノード型でPos()メソッドを個別に実装する必要がなくなりました。Goの埋め込み機能により、BadExpr型のインスタンスは直接badExprInstance.OffsetbadExprInstance.LinebadExprInstance.Column、そしてbadExprInstance.Pos()にアクセスできるようになります。これはコードの重複を大幅に削減し、ASTの定義を簡潔に保ちます。

これらの変更は、Goコンパイラの内部構造をよりクリーンで保守しやすいものにし、位置情報の扱いに関する設計を改善しました。

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

このコミットにおける主要なコード変更は、以下のファイルに集中しています。

  1. src/lib/go/scanner.go:

    • Location構造体の定義が削除され、token.Positionがインポートされるようになりました。
    • Scanner構造体内の位置情報フィールドがloc Locationからpos token.Positionに、pos intからoffset intに変更されました。
    • ErrorHandlerインターフェースのErrorメソッドのシグネチャがError(loc Location, msg string)からError(pos token.Position, msg string)に変更されました。
    • next()メソッド内で、S.loc.Pos, S.loc.Col, S.loc.LineへのアクセスがS.pos.Offset, S.pos.Column, S.pos.Lineに更新されました。
    • scanComment, scanString, scanRawStringなどのメソッドの引数と内部での位置情報の参照がLocationからtoken.Positionに更新されました。
    • Scan()メソッドの戻り値の型がloc Locationからpos token.Positionに変更されました。
    • Tokenize関数のシグネチャも同様にLocationからtoken.Positionに変更されました。
  2. src/lib/go/scanner_test.go:

    • テストコード内でscanner.Locationを使用していた箇所がtoken.Positionに更新されました。
    • eloc.Coleloc.Columnに、eloc.Poseloc.Offsetに更新されました。
  3. src/lib/go/token.go:

    • Position構造体が新しく定義されました。
    • Position構造体にPos() Positionアクセサメソッドが追加されました。
  4. usr/gri/pretty/ast.go:

    • import "scanner"が削除され、import "token"のみになりました。
    • type Position scanner.Locationというエイリアスが削除されました。
    • ASTノードの定義において、Pos_ Positionのような明示的な位置情報フィールドが、token.Positionという匿名フィールドに置き換えられました。
      • 例: BadExpr struct { Pos_ Position; } -> BadExpr struct { token.Position; }
    • これにより、多くのASTノード型で個別に実装されていたPos()メソッドが削除されました。匿名フィールドの埋め込みにより、これらのメソッドは自動的に提供されるためです。

コアとなるコードの解説

src/lib/go/token.go における Position 構造体と Pos() メソッドの定義

// src/lib/go/token.go
// Token source positions are represented by a Position value.
type Position struct {
	Offset int;  // byte offset, starting at 0
	Line int;  // line number, starting at 1
	Column int;  // column number, starting at 1 (character count)
}

// Pos is an accessor method for anonymous Position fields.
// It returns its receiver.
func (pos *Position) Pos() Position {
	return *pos;
}

このコードは、ソースコード上の特定の位置をバイトオフセット、行番号、列番号で表現するPosition構造体を定義しています。Pos()メソッドは、このPosition構造体へのポインタを受け取り、その値(Position構造体自体)を返します。このシンプルなアクセサは、Goの匿名フィールド埋め込み機能と組み合わされることで、ASTノードから位置情報に統一的にアクセスするための重要な役割を担います。

usr/gri/pretty/ast.go における匿名フィールド埋め込みの利用

// usr/gri/pretty/ast.go (変更後 - 例: BadExpr)
type BadExpr struct {
	token.Position;  // beginning position of bad expression
};

この変更は、ASTノードの定義において最も影響の大きい部分です。以前はPos_ Position;のように明示的なフィールド名で位置情報を保持し、各ノード型でPos()メソッドを実装してそのフィールドを返していました。しかし、token.Position;とフィールド名なしで埋め込むことで、BadExpr構造体は自動的にtoken.Positionのすべてのフィールド(Offset, Line, Column)とメソッド(Pos())を「継承」したかのように振る舞います。

例えば、badExprInstance := &BadExpr{...}というインスタンスがあった場合、badExprInstance.OffsetbadExprInstance.LinebadExprInstance.Column、そしてbadExprInstance.Pos()のように直接アクセスできるようになります。これにより、ASTノードの定義が簡潔になり、位置情報へのアクセスが統一され、コードの重複が大幅に削減されました。

src/lib/go/scanner.go における変更

// src/lib/go/scanner.go (変更後 - 抜粋)
type ErrorHandler interface {
	Error(pos token.Position, msg string);
}

type Scanner struct {
	// ...
	pos token.Position;  // previous reading position (position before ch)
	offset int;  // current reading offset (position after ch)
	// ...
}

func (S *Scanner) next() {
	if S.offset < len(S.src) {
		S.pos.Offset = S.offset;
		S.pos.Column++;
		// ...
		case r == '\n':
			S.pos.Line++;
			S.pos.Column = 0;
		// ...
		S.offset += w;
		S.ch = r;
	} else {
		S.pos.Offset = len(S.src);
		S.ch = -1;  // eof
	}
}

func (S *Scanner) Init(src []byte, err ErrorHandler, scan_comments bool) {
	// ...
	S.pos.Line = 1;
	S.next();
}

func (S *Scanner) error(pos token.Position, msg string) {
	S.err.Error(pos, msg);
}

func (S *Scanner) Scan() (pos token.Position, tok token.Token, lit []byte) {
	// ...
	pos, tok = S.pos, token.ILLEGAL;
	// ...
	return pos, tok, S.src[pos.Offset : S.pos.Offset];
}

func Tokenize(src []byte, err ErrorHandler, scan_comments bool, f func (pos token.Position, tok token.Token, lit []byte) bool) {
	// ...
}

scanner.goでは、Location構造体の削除に伴い、token.Positionをインポートし、すべての位置情報参照を新しいtoken.Position型に切り替えています。Scanner構造体の内部状態も更新され、next()メソッドやInit()メソッドでの位置情報の更新ロジックがS.pos.OffsetS.pos.LineS.pos.Columnを使用するように変更されました。Scan()メソッドやTokenize関数のシグネチャも、Locationの代わりにtoken.Positionを返すように変更されています。これにより、スキャナーが生成する位置情報が、tokenパッケージで定義された標準的な形式に統一されました。

これらの変更は、Goコンパイラのフロントエンドにおける位置情報の管理を大幅に改善し、コードの可読性、保守性、および拡張性を向上させました。

関連リンク

参考にした情報源リンク