[インデックス 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言語およびコンパイラ/パーサーの基本的な概念を理解しておく必要があります。
-
Go言語の
go/parserパッケージ:- Go言語のソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を生成するための標準ライブラリです。
- コンパイラやリンター、コード分析ツールなどがGoのコードを理解するために利用します。
parser構造体は、パース処理中の状態(現在のトークン、エラー、スコープ情報など)を保持します。
-
抽象構文木(AST: Abstract Syntax Tree):
- ソースコードの構造を木構造で表現したものです。
go/astパッケージで定義されており、ast.FileがGoソースファイル全体のASTのルートノードとなります。
-
スコープ(Scope):
- プログラム内で識別子(変数名、関数名、型名など)が参照可能な範囲を定義する概念です。
- Go言語では、ブロック、関数、パッケージなど、様々なレベルでスコープが存在します。
go/astパッケージのast.Scope構造体は、このスコープ情報を表現し、識別子の解決(どの識別子がどの宣言に対応するかを見つけること)に利用されます。parser内部では、openScope()で新しいスコープを作成し、現在のスコープスタックにプッシュし、closeScope()で現在のスコープをポップすることで、スコープの階層を管理します。
-
ラベルスコープ(Label Scope):
goto文や、ラベル付きbreak/continue文のターゲットとなるラベルの可視性を管理する特殊なスコープです。- 通常の識別子スコープとは別に管理されることが一般的です。
-
token.FileSet:go/tokenパッケージの一部で、ソースコード内の位置情報(行番号、列番号など)を管理するためのものです。- パーサーは、エラーメッセージの生成やデバッグのために、この
FileSetを利用します。
-
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
コアとなるコードの解説
-
parser.init()からのスコープ初期化の削除:parser構造体のinitメソッド(行69-78)から、p.openScope()、p.pkgScope = p.topScope、p.openLabelScope()の呼び出しが削除されました。- これは、
parseFileが唯一の主要なパースエントリーポイントであるという新しい設計思想を反映しています。これにより、パーサーの初期化はより軽量になり、スコープの管理は実際のパース処理の開始時に行われるようになります。
-
parseFile()内でのスコープ初期化の移動と対称的な開閉:parseFile()メソッド(行2302-2304)の冒頭に、p.openScope()とp.pkgScope = p.topScopeが移動されました。これにより、ファイル全体のパースが開始されると同時に、そのファイルに対応するトップレベルのパッケージスコープが作成されます。parseFile()の終了間際(行2315)に、p.closeScope()が追加されました。これは、parseFileの冒頭で開かれたスコープを明示的に閉じることで、スコープの開閉が対称的であることを保証します。
-
スコープバランスのアサーションの強化:
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ソースファイルパースの主要なエントリーポイントとして確立することで、スコープ管理の堅牢性と可読性を向上させています。
関連リンク
- Go言語の
go/parserパッケージのドキュメント: https://pkg.go.dev/go/parser - Go言語の
go/astパッケージのドキュメント: https://pkg.go.dev/go/ast - Go言語の
go/tokenパッケージのドキュメント: https://pkg.go.dev/go/token - Gerrit Code Review (Goプロジェクトのコードレビューシステム): https://go-review.googlesource.com/
参考にした情報源リンク
- コミットメッセージと差分情報:
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上での詳細なレビュー履歴は追跡が難しい場合があります。)
- https://golang.org/cl/6230047 (このリンクは古いGerritのURL形式であり、現在はリダイレクトされるか、直接アクセスできない場合があります。新しい形式は