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

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

このコミットは、Go言語のパーサー(go/parserパッケージ)における内部的なクリーンアップとスコープ管理の改善に関するものです。具体的には、パーサーの初期化ロジックを簡素化し、parseFile関数がGoソースファイルのパースにおける唯一のエントリーポイントであることを前提としたスコープの対称的な開閉を保証するように変更されています。

コミット

commit 7482822bba44be97fd0b08ff396fa92f777baa42
Author: Robert Griesemer <gri@golang.org>
Date:   Wed May 23 09:37:48 2012 -0700

    go/parser: minor cleanup
    
    - there is no label scope at package level
    - open/close all scopes symmetrically now
      that there is only one parse entry point
      (parseFile)
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/6230047

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

https://github.com/golang/go/commit/7482822bba44be97fd0b08ff396fa92f777baa42

元コミット内容

Goパーサーのマイナーなクリーンアップ。

  • パッケージレベルにはラベルスコープが存在しない。
  • parseFileが唯一のパースエントリーポイントとなったため、全てのスコープを対称的に開閉する。

変更の背景

このコミットの背景には、Go言語のパーサー(go/parser)の内部設計の進化があります。初期のパーサーは、完全なGoソースファイルだけでなく、式(ParseExpr)や宣言など、より小さなコード片をパースするための複数のエントリーポイントを持っていました。そのため、パーサーのinitメソッド内で、パッケージスコープ(pkgScope)やラベルスコープを事前に設定する必要がありました。これは、どのエントリーポイントからパースが開始されても、基本的なスコープが利用可能であることを保証するためです。

しかし、Goパーサーの設計が成熟するにつれて、完全なGoソースファイルをパースする主要な関数はparseFileに集約されていきました。この変更により、initメソッドで事前にスコープを設定する必要がなくなり、parseFile関数内で必要なスコープを明示的に開閉する方が、コードの意図が明確になり、スコープ管理がより対称的で堅牢になるという判断がなされました。

また、「パッケージレベルにはラベルスコープが存在しない」という点は、Go言語のセマンティクスに基づいています。Goにおいてラベル(goto文やbreak/continue文のターゲット)は、関数やブロックの内部でのみ意味を持ち、パッケージのトップレベルで宣言されることはありません。したがって、パッケージレベルでラベルスコープを管理する必要がないという認識が、このクリーンアップの一環として反映されています。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびコンパイラ/パーサーの基本的な概念を理解しておく必要があります。

  1. Go言語のgo/parserパッケージ:

    • Go言語のソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を生成するための標準ライブラリです。
    • コンパイラやリンター、コード分析ツールなどがGoのコードを理解するために利用します。
    • parser構造体は、パース処理中の状態(現在のトークン、エラー、スコープ情報など)を保持します。
  2. 抽象構文木(AST: Abstract Syntax Tree):

    • ソースコードの構造を木構造で表現したものです。
    • go/astパッケージで定義されており、ast.FileがGoソースファイル全体のASTのルートノードとなります。
  3. スコープ(Scope):

    • プログラム内で識別子(変数名、関数名、型名など)が参照可能な範囲を定義する概念です。
    • Go言語では、ブロック、関数、パッケージなど、様々なレベルでスコープが存在します。
    • go/astパッケージのast.Scope構造体は、このスコープ情報を表現し、識別子の解決(どの識別子がどの宣言に対応するかを見つけること)に利用されます。
    • parser内部では、openScope()で新しいスコープを作成し、現在のスコープスタックにプッシュし、closeScope()で現在のスコープをポップすることで、スコープの階層を管理します。
  4. ラベルスコープ(Label Scope):

    • goto文や、ラベル付きbreak/continue文のターゲットとなるラベルの可視性を管理する特殊なスコープです。
    • 通常の識別子スコープとは別に管理されることが一般的です。
  5. token.FileSet:

    • go/tokenパッケージの一部で、ソースコード内の位置情報(行番号、列番号など)を管理するためのものです。
    • パーサーは、エラーメッセージの生成やデバッグのために、このFileSetを利用します。
  6. assert関数:

    • Goの標準ライブラリや内部コードでよく見られる、開発時の不変条件(invariant)チェックのためのヘルパー関数です。
    • 条件が満たされない場合にパニックを引き起こし、プログラムの論理的な誤りを示します。

技術的詳細

このコミットの技術的詳細は、go/parserパッケージのparser構造体のinitメソッドとparseFileメソッドにおけるスコープ管理の変更に集約されます。

変更前:

  • parser.init()メソッド内で、p.openScope()を呼び出してトップレベルのスコープを開き、それをp.pkgScopeとして設定していました。これは、ParseExprのような他のパースエントリーポイントが存在するため、init時に基本的なパッケージスコープを準備しておく必要があったためです。
  • 同様に、p.openLabelScope()initメソッド内で呼び出され、パッケージレベルのラベルスコープが設定されていました。
  • parseFile()の最後では、assert(p.topScope == p.pkgScope, "imbalanced scopes")というアサーションがあり、parseFileの実行中に開かれたスコープが全て閉じられ、最終的にpkgScopeがトップスコープに戻っていることを確認していました。

変更後:

  • parser.init()メソッドから、p.openScope()p.pkgScope = p.topScope、およびp.openLabelScope()の呼び出しが削除されました。これにより、initメソッドはよりシンプルになり、パーサーの初期状態はスコープが閉じられた状態になります。
  • parseFile()メソッドの冒頭で、p.openScope()を呼び出して新しいスコープを開き、それをp.pkgScopeとして設定するようになりました。これは、parseFileがGoソースファイルのパースにおける唯一の「完全な」エントリーポイントであるという新しい前提に基づいています。これにより、parseFileの実行中に必要なトップレベルのスコープが明示的に作成されます。
  • parseFile()メソッドの最後で、p.closeScope()が明示的に呼び出されるようになりました。これにより、parseFile内で開かれたトップレベルのスコープが対称的に閉じられます。
  • アサーションも変更され、assert(p.topScope == nil, "unbalanced scopes")assert(p.labelScope == nil, "unbalanced label scopes")になりました。これは、parseFileの終了時には、全てのスコープ(通常のスコープとラベルスコープの両方)が適切に閉じられ、スコープスタックが空になっているべきであるという、より厳密な不変条件を強制します。

この変更により、スコープのライフサイクルがparseFile関数内に完全にカプセル化され、パーサーの他の部分や他のパースエントリーポイント(もし存在したとしても)からの影響を受けにくくなります。また、パッケージレベルでラベルスコープが存在しないというGoのセマンティクスがコードに反映され、不要なスコープ管理が削除されました。

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

変更はsrc/pkg/go/parser/parser.goファイルに集中しています。

--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -56,7 +56,7 @@ type parser struct {
 	unresolved []*ast.Ident      // unresolved identifiers
 	imports    []*ast.ImportSpec // list of imports
 
-	// Label scope
+	// Label scopes
 	// (maintained by open/close LabelScope)
 	labelScope  *ast.Scope     // label scope for current function
 	targetStack [][]*ast.Ident // stack of unresolved labels
@@ -75,14 +75,6 @@ func (p *parser) init(fset *token.FileSet, filename string, src []byte, mode Mod
 	p.trace = mode&Trace != 0 // for convenience (p.trace is used frequently)
 
 	p.next()
-
-	// set up the pkgScope here (as opposed to in parseFile) because
-	// there are other parser entry points (ParseExpr, etc.)
-	p.openScope()
-	p.pkgScope = p.topScope
-
-	// for the same reason, set up a label scope
-	p.openLabelScope()
 }
 
 // ----------------------------------------------------------------------------
@@ -2297,11 +2289,12 @@ func (p *parser) parseFile() *ast.File {
 	}
 	p.expectSemi()
 
-	var decls []ast.Decl
-
 	// Don't bother parsing the rest if we had errors already.
 	// Likely not a Go source file at all.
 
+	p.openScope()
+	p.pkgScope = p.topScope
+	var decls []ast.Decl
 	if p.errors.Len() == 0 && p.mode&PackageClauseOnly == 0 {
 		// import decls
 		for p.tok == token.IMPORT {
@@ -2315,8 +2308,9 @@ func (p *parser) parseFile() *ast.File {
 			}
 		}
 	}
-
-	assert(p.topScope == p.pkgScope, "imbalanced scopes")
+
+	p.closeScope()
+	assert(p.topScope == nil, "unbalanced scopes")
+	assert(p.labelScope == nil, "unbalanced label scopes")
 
 	// resolve global identifiers within the same file
 	i := 0

コアとなるコードの解説

  1. parser.init()からのスコープ初期化の削除:

    • parser構造体のinitメソッド(行69-78)から、p.openScope()p.pkgScope = p.topScopep.openLabelScope()の呼び出しが削除されました。
    • これは、parseFileが唯一の主要なパースエントリーポイントであるという新しい設計思想を反映しています。これにより、パーサーの初期化はより軽量になり、スコープの管理は実際のパース処理の開始時に行われるようになります。
  2. parseFile()内でのスコープ初期化の移動と対称的な開閉:

    • parseFile()メソッド(行2302-2304)の冒頭に、p.openScope()p.pkgScope = p.topScopeが移動されました。これにより、ファイル全体のパースが開始されると同時に、そのファイルに対応するトップレベルのパッケージスコープが作成されます。
    • parseFile()の終了間際(行2315)に、p.closeScope()が追加されました。これは、parseFileの冒頭で開かれたスコープを明示的に閉じることで、スコープの開閉が対称的であることを保証します。
  3. スコープバランスのアサーションの強化:

    • parseFile()の最後のアサーション(行2316-2317)が、assert(p.topScope == p.pkgScope, "imbalanced scopes")から、assert(p.topScope == nil, "unbalanced scopes")assert(p.labelScope == nil, "unbalanced label scopes")に変更されました。
    • これは、parseFileが完了した時点で、通常のスコープスタックもラベルスコープスタックも完全に空になっているべきであるという、より厳密なチェックを導入しています。これにより、パーサーがスコープを適切に管理し、メモリリークや論理的な不整合がないことを保証します。

これらの変更は、Goパーサーの内部ロジックを簡素化し、parseFileをGoソースファイルパースの主要なエントリーポイントとして確立することで、スコープ管理の堅牢性と可読性を向上させています。

関連リンク

参考にした情報源リンク

  • コミットメッセージと差分情報: commit_data/13141.txtの内容
  • Go言語の公式ドキュメント(go/parser, go/ast, go/tokenパッケージ)
  • Go言語のソースコード(src/pkg/go/parser/parser.goの変更前後のコード)
  • Gerrit Change-Id: 6230047 (コミットメッセージに記載されているGerritの変更ID)
    • https://golang.org/cl/6230047 (このリンクは古いGerritのURL形式であり、現在はリダイレクトされるか、直接アクセスできない場合があります。新しい形式はhttps://go-review.googlesource.com/c/go/+/6230047のようになりますが、このコミットは非常に古いため、Gerrit上での詳細なレビュー履歴は追跡が難しい場合があります。)