[インデックス 10127] ファイルの概要
このコミットは、Go言語のパーサー(go/parser
パッケージ)における、:=
(短い変数宣言)のスコープに関するバグ修正と、それに関連するテストの追加を扱っています。具体的には、:=
演算子を用いた変数宣言において、変数がスコープに入るタイミングが不適切であった問題を修正し、その挙動を検証するためのテストケースが追加されています。
コミット
commit fd31d9fd7beaac218899c5bdb74004152b076d82
Author: Russ Cox <rsc@golang.org>
Date: Thu Oct 27 12:22:06 2011 -0700
go/parser: test and fix := scoping bug
R=iant
CC=golang-dev, gri
https://golang.org/cl/5327048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fd31d9fd7beaac218899c5bdb74004152b076d82
元コミット内容
go/parser: test and fix := scoping bug
このコミットは、Go言語のパーサーにおいて、:=
演算子(短い変数宣言)のスコープに関するバグをテストし、修正するものです。
変更の背景
Go言語の:=
演算子は、変数の宣言と初期化を同時に行うための便利な構文です。しかし、この演算子には、変数が実際にスコープに入るタイミングに関する微妙なルールが存在します。特に、同じ行で宣言される変数と、その初期化に使われる式の中に、まだ宣言されていない変数が含まれる場合に問題が発生することがあります。
このコミットが行われた2011年10月時点のGo言語は、まだ開発の初期段階にあり、言語仕様やコンパイラ、ツールチェインの細部が固まりつつある時期でした。パーサーは、ソースコードを抽象構文木(AST)に変換する役割を担っており、言語のセマンティクス(意味)を正しく解釈することが極めて重要です。
このバグは、:=
宣言の左辺(LHS: Left Hand Side)で宣言される変数が、右辺(RHS: Right Hand Side)の式が評価される前にスコープに入ってしまうという、パーサーの誤った挙動に起因していたと考えられます。これにより、RHSの式が、本来はまだ存在しないはずのLHSの変数を参照してしまう可能性があり、予期せぬコンパイルエラーや、より深刻な場合は誤ったプログラムの解釈につながる恐れがありました。
このコミットは、このような潜在的な問題を特定し、Go言語のパーサーが:=
宣言のスコープルールを正確に処理するように修正することを目的としています。また、このようなバグが将来的に再発しないよう、厳密なテストケースを追加することで、パーサーの堅牢性を高めることも意図されています。
前提知識の解説
Go言語の:=
(短い変数宣言)
Go言語では、変数を宣言し、初期化する方法として、var
キーワードを使用する方法と、:=
演算子を使用する「短い変数宣言」の2つが主に使用されます。
var
宣言:var name type = expression
の形式で、明示的に型を指定して変数を宣言します。var i int = 10 var s string = "hello"
:=
短い変数宣言:name := expression
の形式で、型推論を利用して変数を宣言し、初期化します。関数内でのみ使用できます。
このi := 10 // i は int 型と推論される s := "hello" // s は string 型と推論される
:=
演算子の重要な特性は、左辺の変数の少なくとも1つが新しい変数である場合にのみ使用できるという点です。
Go言語のスコープ
スコープとは、プログラム内で変数が参照可能な範囲を指します。Go言語には、以下の主要なスコープがあります。
- ブロック・スコープ:
{}
で囲まれたブロック内で宣言された変数は、そのブロック内でのみ有効です。 - パッケージ・スコープ: パッケージレベルで宣言された変数は、そのパッケージ内のすべてのファイルから参照可能です。
- ファイル・スコープ: Go言語には厳密なファイルスコープはありませんが、パッケージスコープの一部として、ファイル内で宣言されたトップレベルのエンティティはそのファイル内で参照可能です。
- ユニバース・スコープ:
true
,false
,int
,string
などの組み込み型や定数は、プログラム全体で参照可能です。
:=
宣言におけるスコープのルールは、特に注意が必要です。Go言語の仕様では、:=
宣言の左辺で宣言される新しい変数は、右辺の式が評価された後にスコープに入るとされています。これは、右辺の式が、同じ:=
宣言で新しく宣言される変数を参照できないようにするためです。
例:
x := x // これはコンパイルエラーになる。右辺のxはまだ宣言されていないため。
しかし、以下のようなケースでは、既存の変数と新しい変数が混在する場合があります。
x := 10
y, x := 20, x // yは新しい変数、xは既存の変数。右辺のxは既存のxを参照する。
go/parser
パッケージ
go/parser
パッケージは、Go言語のソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を生成するための標準ライブラリです。ASTは、プログラムの構造を木構造で表現したもので、コンパイラやリンター、コード分析ツールなどがソースコードを理解するために利用します。
go/parser
は、字句解析(トークン化)と構文解析(パース)の2つの主要なフェーズを実行します。
- 字句解析: ソースコードを意味のある最小単位(トークン)に分割します。例えば、キーワード、識別子、演算子、リテラルなどです。
- 構文解析: トークンのストリームを文法規則に従って解析し、ASTを構築します。この過程で、変数の宣言、式の評価順序、スコープなどが正しく解釈される必要があります。
このコミットは、go/parser
が:=
宣言のスコープルールを正しく解釈していなかったという、構文解析のロジックにおけるバグを修正するものです。
技術的詳細
このコミットの技術的詳細は、Go言語のパーサーが:=
演算子(短い変数宣言)のスコープをどのように処理するか、という点に集約されます。
Go言語の仕様では、短い変数宣言 LHS := RHS
において、LHS
で宣言される新しい変数は、RHS
の式が評価された後にスコープに入ると規定されています。これは、RHS
の式が、同じ宣言で新しく導入される変数を参照することを防ぐためです。もし、LHS
の変数がRHS
の評価前にスコープに入ってしまうと、RHS
が未初期化の、あるいは意図しない値を持つLHS
の変数を参照してしまう「シャドーイング」のような問題が発生する可能性があります。
コミット前のgo/parser
の実装では、このスコープのタイミングが正しく処理されていなかったようです。特に、parseLhsList
関数がtoken.DEFINE
(:=
演算子)を検出した際に、p.shortVarDecl
を呼び出すタイミングが問題でした。コメントアウトされた行が示唆するように、以前はparseLhsList
内でp.shortVarDecl
が直接呼び出されていた可能性があります。しかし、この関数は「後で呼び出し元がp.shortVarDecl
を呼び出す必要がある」とコメントされており、変数がスコープに入るタイミングを遅らせる意図があったことが伺えます。
修正は、parseSimpleStmt
関数とparseCommClause
関数に集中しています。これらの関数は、それぞれ通常の代入文/短い変数宣言と、select
文の通信句(case <-ch:
や case ch <- val:
など)をパースする役割を担っています。
-
parseSimpleStmt
における修正:parseSimpleStmt
は、x := y
のような短い変数宣言を処理します。修正前は、:=
演算子を検出しても、p.shortVarDecl
の呼び出しがありませんでした。修正後は、tok == token.DEFINE
(つまり:=
)の場合に、p.shortVarDecl(p.makeIdentList(x))
が呼び出されるようになりました。ここでx
は左辺の識別子のリストです。この変更により、左辺の変数が、右辺の式がパースされた後にスコープに入るように、shortVarDecl
の呼び出しが適切なタイミングで行われるようになりました。 -
parseCommClause
における修正:parseCommClause
は、select
文のcase
句内の通信操作を処理します。ここでも、lhs := rhs
のような短い変数宣言が通信操作と組み合わされる場合があります(例:case v := <-ch:
)。修正前は、このコンテキストで:=
が使用された場合に、左辺の変数が適切にスコープに入らない可能性がありました。修正後は、tok == token.DEFINE && lhs != nil
の場合に、p.shortVarDecl(p.makeIdentList(lhs))
が呼び出されるようになりました。これにより、通信句内の短い変数宣言も、正しいスコープルールに従って処理されるようになります。
これらの修正は、go/parser
がGo言語の仕様に厳密に従い、:=
宣言における変数のスコープを正確に管理することを保証します。特に、右辺の式が評価される前に左辺の変数がスコープに入ってしまうという、潜在的なセマンティックエラーを防ぐ上で重要です。
また、コミットには新しいテストケースが追加されています。TestColonEqualsScope
とTestVarScope
は、それぞれ:=
宣言とvar
宣言における変数のスコープ挙動を検証します。これらのテストは、左辺の変数が右辺の式が評価される前にスコープに入っていないこと(つまり、右辺の式が左辺の変数を参照できないこと)、そして左辺の変数が正しくスコープに入っていること(つまり、ast.Ident
のObj
フィールドが適切に設定されていること)を確認します。これにより、修正が正しく機能していること、および将来の回帰を防ぐことが保証されます。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/pkg/go/parser/parser.go
ファイルと src/pkg/go/parser/parser_test.go
ファイルにあります。
src/pkg/go/parser/parser.go
--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -434,7 +434,9 @@ func (p *parser) parseLhsList() []ast.Expr {
switch p.tok {
case token.DEFINE:
// lhs of a short variable declaration
- //p.shortVarDecl(p.makeIdentList(list))
+ // but doesn't enter scope until later:
+ // caller must call p.shortVarDecl(p.makeIdentList(list))
+ // at appropriate time.
case token.COLON:
// lhs of a label declaration or a communication clause of a select
// statement (parseLhsList is not called when parsing the case clause
@@ -1398,6 +1400,9 @@ func (p *parser) parseSimpleStmt(mode int) (ast.Stmt, bool) {
} else {
y = p.parseRhsList()
}
+ if tok == token.DEFINE {
+ p.shortVarDecl(p.makeIdentList(x))
+ }
return &ast.AssignStmt{x, pos, tok, y}, isRange
}
@@ -1722,6 +1727,9 @@ func (p *parser) parseCommClause() *ast.CommClause {
}
p.next()
rhs = p.parseRhs()
+ if tok == token.DEFINE && lhs != nil {
+ p.shortVarDecl(p.makeIdentList(lhs))
+ }
} else {
// rhs must be single receive operation
if len(lhs) > 1 {
src/pkg/go/parser/parser_test.go
--- a/src/pkg/go/parser/parser_test.go
+++ b/src/pkg/go/parser/parser_test.go
@@ -5,6 +5,7 @@
package parser
import (
+ "go/ast"
"go/token"
"os"
"testing"
@@ -134,3 +135,46 @@ func TestParse4(t *testing.T) {
}
}\n}\n+\n+func TestColonEqualsScope(t *testing.T) {\n+\tf, err := ParseFile(fset, \"\", `package p; func f() { x, y, z := x, y, z }`, 0)\n+\tif err != nil {\n+\t\tt.Errorf(\"parse: %s\", err)\n+\t}\n+\n+\t// RHS refers to undefined globals; LHS does not.\n+\tas := f.Decls[0].(*ast.FuncDecl).Body.List[0].(*ast.AssignStmt)\n+\tfor _, v := range as.Rhs {\n+\t\tid := v.(*ast.Ident)\n+\t\tif id.Obj != nil {\n+\t\t\tt.Errorf(\"rhs %s has Obj, should not\", id.Name)\n+\t\t}\n+\t}\n+\tfor _, v := range as.Lhs {\n+\t\tid := v.(*ast.Ident)\n+\t\tif id.Obj == nil {\n+\t\t\tt.Errorf(\"lhs %s does not have Obj, should\", id.Name)\n+\t\t}\n+\t}\n+}\n+\n+func TestVarScope(t *testing.T) {\n+\tf, err := ParseFile(fset, \"\", `package p; func f() { var x, y, z = x, y, z }`, 0)\n+\tif err != nil {\n+\t\tt.Errorf(\"parse: %s\", err)\n+\t}\n+\n+\t// RHS refers to undefined globals; LHS does not.\n+\tas := f.Decls[0].(*ast.FuncDecl).Body.List[0].(*ast.DeclStmt).Decl.(*ast.GenDecl).Specs[0].(*ast.ValueSpec)\n+\tfor _, v := range as.Values {\n+\t\tid := v.(*ast.Ident)\n+\t\tif id.Obj != nil {\n+\t\t\tt.Errorf(\"rhs %s has Obj, should not\", id.Name)\n+\t\t}\n+\t}\n+\tfor _, id := range as.Names {\n+\t\tif id.Obj == nil {\n+\t\t\tt.Errorf(\"lhs %s does not have Obj, should\", id.Name)\n+\t\t}\n+\t}\n+}\n```
## コアとなるコードの解説
### `src/pkg/go/parser/parser.go` の変更点
1. **`parseLhsList` 関数**:
* `token.DEFINE`(`:=`演算子)のケースで、以前はコメントアウトされていた `p.shortVarDecl(p.makeIdentList(list))` の行が、より詳細なコメントに置き換えられました。
* 新しいコメントは、「スコープに入るのは後で、呼び出し元が適切なタイミングで`p.shortVarDecl`を呼び出す必要がある」と明記しています。これは、`:=`宣言の左辺の変数が、右辺の式が評価される前にスコープに入らないようにするための設計意図を明確にしています。
2. **`parseSimpleStmt` 関数**:
* この関数は、`x := y` のような単純な代入文や短い変数宣言をパースします。
* 追加されたコードブロック:
```go
if tok == token.DEFINE {
p.shortVarDecl(p.makeIdentList(x))
}
```
この変更により、`:=`演算子(`token.DEFINE`)が検出された場合、左辺の識別子リスト`x`に対して`p.shortVarDecl`が呼び出されるようになりました。この呼び出しは、右辺の式`y`がパースされた後に行われるため、Go言語の仕様に従って、左辺の変数が右辺の評価後にスコープに入ることを保証します。
3. **`parseCommClause` 関数**:
* この関数は、`select`文の`case`句内の通信操作(例: `case v := <-ch:`)をパースします。
* 追加されたコードブロック:
```go
if tok == token.DEFINE && lhs != nil {
p.shortVarDecl(p.makeIdentList(lhs))
}
```
`parseSimpleStmt`と同様に、通信句内で`:=`演算子が使用された場合、左辺の識別子リスト`lhs`に対して`p.shortVarDecl`が呼び出されるようになりました。これにより、`select`文内の短い変数宣言も、正しいスコープルールに従って処理されるようになります。
これらの変更は、`p.shortVarDecl`の呼び出しを、変数が実際にスコープに入るべきタイミング(つまり、右辺の式が完全にパースされた後)に移動させることで、`:=`宣言のスコープバグを修正しています。
### `src/pkg/go/parser/parser_test.go` の変更点
1. **`go/ast` パッケージのインポート**:
* 新しいテストケースでASTの構造を検証するために、`go/ast`パッケージがインポートされました。
2. **`TestColonEqualsScope` 関数**:
* このテストは、`x, y, z := x, y, z` のような短い変数宣言のスコープ挙動を検証します。
* **目的**: 右辺の`x, y, z`が、宣言される前のグローバルな(未定義の)変数として扱われ、左辺の`x, y, z`が新しい変数として正しくスコープに入っていることを確認します。
* **検証内容**:
* `as.Rhs`(右辺の式)の各識別子について、`id.Obj`が`nil`であることを確認します。これは、右辺の識別子が、同じ宣言で新しく導入される変数ではなく、未定義のグローバル変数(または既存の変数)を参照していることを意味します。もし`id.Obj`が`nil`でなければ、右辺がまだスコープに入っていない左辺の変数を参照してしまっていることになり、バグを示します。
* `as.Lhs`(左辺の変数)の各識別子について、`id.Obj`が`nil`でないことを確認します。これは、左辺の識別子が新しい変数として正しく宣言され、AST内でそのオブジェクトが関連付けられていることを意味します。
3. **`TestVarScope` 関数**:
* このテストは、`var x, y, z = x, y, z` のような通常の`var`宣言のスコープ挙動を検証します。
* **目的**: `var`宣言の場合も、短い変数宣言と同様に、右辺の式が評価される前に左辺の変数がスコープに入らないことを確認します。
* **検証内容**:
* `as.Values`(右辺の式)の各識別子について、`id.Obj`が`nil`であることを確認します。これは、右辺の識別子が、宣言される前のグローバルな(未定義の)変数として扱われていることを意味します。
* `as.Names`(左辺の変数)の各識別子について、`id.Obj`が`nil`でないことを確認します。これは、左辺の識別子が新しい変数として正しく宣言され、AST内でそのオブジェクトが関連付けられていることを意味します。
これらのテストケースは、`:=`宣言と`var`宣言の両方において、Go言語のスコープルールがパーサーによって正しく適用されていることを厳密に検証し、将来的な回帰を防ぐための重要な安全網となります。
## 関連リンク
* Go言語の公式ドキュメント: [https://go.dev/](https://go.dev/)
* Go言語の仕様: [https://go.dev/ref/spec](https://go.dev/ref/spec)
* `go/parser`パッケージのドキュメント: [https://pkg.go.dev/go/parser](https://pkg.go.dev/go/parser)
* `go/ast`パッケージのドキュメント: [https://pkg.go.dev/go/ast](https://pkg.go.dev/go/ast)
* このコミットのChange List (CL): [https://golang.org/cl/5327048](https://golang.org/cl/5327048)
## 参考にした情報源リンク
* Go言語の公式ドキュメントおよび仕様書
* Go言語のソースコード(特に`go/parser`パッケージ)
* Go言語に関する技術ブログやフォーラム(一般的なGo言語のスコープルールや`:=`演算子に関する情報)
* Go言語のASTに関する解説記事
* GitHubのコミット履歴と関連する議論
# [インデックス 10127] ファイルの概要
このコミットは、Go言語のパーサー(`go/parser`パッケージ)における、`:=`(短い変数宣言)のスコープに関するバグ修正と、それに関連するテストの追加を扱っています。具体的には、`:=` 演算子を用いた変数宣言において、変数がスコープに入るタイミングが不適切であった問題を修正し、その挙動を検証するためのテストケースが追加されています。
## コミット
commit fd31d9fd7beaac218899c5bdb74004152b076d82 Author: Russ Cox rsc@golang.org Date: Thu Oct 27 12:22:06 2011 -0700
go/parser: test and fix := scoping bug
R=iant
CC=golang-dev, gri
https://golang.org/cl/5327048
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/fd31d9fd7beaac218899c5bdb74004152b076d82](https://github.com/golang/go/commit/fd31d9fd7beaac218899c5bdb74004152b076d82)
## 元コミット内容
`go/parser: test and fix := scoping bug`
このコミットは、Go言語のパーサーにおいて、`:=` 演算子(短い変数宣言)のスコープに関するバグをテストし、修正するものです。
## 変更の背景
Go言語の`:=`演算子は、変数の宣言と初期化を同時に行うための便利な構文です。しかし、この演算子には、変数が実際にスコープに入るタイミングに関する微妙なルールが存在します。特に、同じ行で宣言される変数と、その初期化に使われる式の中に、まだ宣言されていない変数が含まれる場合に問題が発生することがあります。
このコミットが行われた2011年10月時点のGo言語は、まだ開発の初期段階にあり、言語仕様やコンパイラ、ツールチェインの細部が固まりつつある時期でした。パーサーは、ソースコードを抽象構文木(AST)に変換する役割を担っており、言語のセマンティクス(意味)を正しく解釈することが極めて重要です。
このバグは、`:=` 宣言の左辺(LHS: Left Hand Side)で宣言される変数が、右辺(RHS: Right Hand Side)の式が評価される前にスコープに入ってしまうという、パーサーの誤った挙動に起因していたと考えられます。これにより、RHSの式が、本来はまだ存在しないはずのLHSの変数を参照してしまう可能性があり、予期せぬコンパイルエラーや、より深刻な場合は誤ったプログラムの解釈につながる恐れがありました。
このコミットは、このような潜在的な問題を特定し、Go言語のパーサーが`:=`宣言のスコープルールを正確に処理するように修正することを目的としています。また、このようなバグが将来的に再発しないよう、厳密なテストケースを追加することで、パーサーの堅牢性を高めることも意図されています。
## 前提知識の解説
### Go言語の`:=`(短い変数宣言)
Go言語では、変数を宣言し、初期化する方法として、`var`キーワードを使用する方法と、`:=`演算子を使用する「短い変数宣言」の2つが主に使用されます。
* **`var`宣言**: `var name type = expression` の形式で、明示的に型を指定して変数を宣言します。
```go
var i int = 10
var s string = "hello"
```
* **`:=`短い変数宣言**: `name := expression` の形式で、型推論を利用して変数を宣言し、初期化します。関数内でのみ使用できます。
```go
i := 10 // i は int 型と推論される
s := "hello" // s は string 型と推論される
```
この`:=`演算子の重要な特性は、左辺の変数の少なくとも1つが新しい変数である場合にのみ使用できるという点です。
### Go言語のスコープ
スコープとは、プログラム内で変数が参照可能な範囲を指します。Go言語には、以下の主要なスコープがあります。
* **ブロック・スコープ**: `{}`で囲まれたブロック内で宣言された変数は、そのブロック内でのみ有効です。これは`:=`で宣言された変数にも適用され、変数は宣言された最も内側のコードブロックとそのネストされたブロック内でのみアクセス可能です。
* **ローカル変数のみ**: `:=`演算子は、関数内のローカル変数を宣言および初期化するためにのみ使用されます。パッケージレベル(グローバル)変数を宣言するためには使用できず、その場合は`var`キーワードを使用する必要があります。
* **宣言と初期化**: `:=`演算子は、変数の宣言と初期化を単一のステートメントに結合します。Goは、割り当てられた値に基づいて変数の型を自動的に推論します。
* **再宣言とシャドーイング**:
* ネストされたスコープで、同じ名前の変数が外側のスコープに既に存在する場合でも、`:=`を使用して変数を「再宣言」することは許容されます。この場合、内側のスコープに新しい変数が作成され、外側の変数を効果的に「シャドーイング」します。外側の変数の値は変更されません。
* 同じ字句ブロック内では、`:=`を使用して変数を再宣言することはできません。これを行おうとすると、コンパイル時エラーが発生します。
* ただし、代入の左辺の変数の少なくとも1つが現在のスコープで**新しく宣言されている**場合は、`:=`を使用できます。左辺の他の変数が同じ字句ブロックで既に宣言されている場合、`:=`はそれらの既存の変数に対して通常の代入(`=`)として機能します。この動作は、`value, err := someFunction()`のようなGoの多値戻りパターンで頻繁に見られます。
`:=`宣言におけるスコープのルールは、特に注意が必要です。Go言語の仕様では、`:=`宣言の左辺で宣言される新しい変数は、右辺の式が評価された後にスコープに入るとされています。これは、右辺の式が、同じ`:=`宣言で新しく宣言される変数を参照できないようにするためです。
例:
```go
x := x // これはコンパイルエラーになる。右辺のxはまだ宣言されていないため。
しかし、以下のようなケースでは、既存の変数と新しい変数が混在する場合があります。
x := 10
y, x := 20, x // yは新しい変数、xは既存の変数。右辺のxは既存のxを参照する。
go/parser
パッケージ
go/parser
パッケージは、Go言語のソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を生成するための標準ライブラリです。ASTは、プログラムの構造を木構造で表現したもので、コンパイラやリンター、コード分析ツールなどがソースコードを理解するために利用します。
go/parser
は、字句解析(トークン化)と構文解析(パース)の2つの主要なフェーズを実行します。
- 字句解析: ソースコードを意味のある最小単位(トークン)に分割します。例えば、キーワード、識別子、演算子、リテラルなどです。
- 構文解析: トークンのストリームを文法規則に従って解析し、ASTを構築します。この過程で、変数の宣言、式の評価順序、スコープなどが正しく解釈される必要があります。
このコミットは、go/parser
が:=
宣言のスコープルールを正しく解釈していなかったという、構文解析のロジックにおけるバグを修正するものです。
技術的詳細
このコミットの技術的詳細は、Go言語のパーサーが:=
演算子(短い変数宣言)のスコープをどのように処理するか、という点に集約されます。
Go言語の仕様では、短い変数宣言 LHS := RHS
において、LHS
で宣言される新しい変数は、RHS
の式が評価された後にスコープに入ると規定されています。これは、RHS
の式が、同じ宣言で新しく導入される変数を参照することを防ぐためです。もし、LHS
の変数がRHS
の評価前にスコープに入ってしまうと、RHS
が未初期化の、あるいは意図しない値を持つLHS
の変数を参照してしまう「シャドーイング」のような問題が発生する可能性があります。
コミット前のgo/parser
の実装では、このスコープのタイミングが正しく処理されていなかったようです。特に、parseLhsList
関数がtoken.DEFINE
(:=
演算子)を検出した際に、p.shortVarDecl
を呼び出すタイミングが問題でした。コメントアウトされた行が示唆するように、以前はparseLhsList
内でp.shortVarDecl
が直接呼び出されていた可能性があります。しかし、この関数は「後で呼び出し元がp.shortVarDecl
を呼び出す必要がある」とコメントされており、変数がスコープに入る意図があったことが伺えます。
修正は、parseSimpleStmt
関数とparseCommClause
関数に集中しています。これらの関数は、それぞれ通常の代入文/短い変数宣言と、select
文の通信句(case <-ch:
や case ch <- val:
など)をパースする役割を担っています。
-
parseSimpleStmt
における修正:parseSimpleStmt
は、x := y
のような短い変数宣言を処理します。修正前は、:=
演算子を検出しても、p.shortVarDecl
の呼び出しがありませんでした。修正後は、tok == token.DEFINE
(つまり:=
)の場合に、p.shortVarDecl(p.makeIdentList(x))
が呼び出されるようになりました。ここでx
は左辺の識別子のリストです。この変更により、左辺の変数が、右辺の式がパースされた後にスコープに入るように、shortVarDecl
の呼び出しが適切なタイミングで行われるようになりました。 -
parseCommClause
における修正:parseCommClause
は、select
文のcase
句内の通信操作を処理します。ここでも、lhs := rhs
のような短い変数宣言が通信操作と組み合わされる場合があります(例:case v := <-ch:
)。修正前は、このコンテキストで:=
が使用された場合に、左辺の変数が適切にスコープに入らない可能性がありました。修正後は、tok == token.DEFINE && lhs != nil
の場合に、p.shortVarDecl(p.makeIdentList(lhs))
が呼び出されるようになりました。これにより、通信句内の短い変数宣言も、正しいスコープルールに従って処理されるようになります。
これらの修正は、go/parser
がGo言語の仕様に厳密に従い、:=
宣言における変数のスコープを正確に管理することを保証します。特に、右辺の式が評価される前に左辺の変数がスコープに入ってしまうという、潜在的なセマンティックエラーを防ぐ上で重要です。
また、コミットには新しいテストケースが追加されています。TestColonEqualsScope
とTestVarScope
は、それぞれ:=
宣言とvar
宣言における変数のスコープ挙動を検証します。これらのテストは、左辺の変数が右辺の式が評価される前にスコープに入っていないこと(つまり、右辺の式が左辺の変数を参照できないこと)、そして左辺の変数が正しくスコープに入っていること(つまり、ast.Ident
のObj
フィールドが適切に設定されていること)を確認します。これにより、修正が正しく機能していること、および将来の回帰を防ぐことが保証されます。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/pkg/go/parser/parser.go
ファイルと src/pkg/go/parser/parser_test.go
ファイルにあります。
src/pkg/go/parser/parser.go
--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -434,7 +434,9 @@ func (p *parser) parseLhsList() []ast.Expr {
switch p.tok {
case token.DEFINE:
// lhs of a short variable declaration
- //p.shortVarDecl(p.makeIdentList(list))
+ // but doesn't enter scope until later:
+ // caller must call p.shortVarDecl(p.makeIdentList(list))
+ // at appropriate time.
case token.COLON:
// lhs of a label declaration or a communication clause of a select
// statement (parseLhsList is not called when parsing the case clause
@@ -1398,6 +1400,9 @@ func (p *parser) parseSimpleStmt(mode int) (ast.Stmt, bool) {\n } else {\n y = p.parseRhsList()\n }\n+ if tok == token.DEFINE {\n+ p.shortVarDecl(p.makeIdentList(x))\n+ }\n return &ast.AssignStmt{x, pos, tok, y}, isRange\n }\n \n@@ -1722,6 +1727,9 @@ func (p *parser) parseCommClause() *ast.CommClause {\n }
p.next()
rhs = p.parseRhs()
+ if tok == token.DEFINE && lhs != nil {\n+ p.shortVarDecl(p.makeIdentList(lhs))\n+ }\n } else {\n // rhs must be single receive operation
if len(lhs) > 1 {
src/pkg/go/parser/parser_test.go
--- a/src/pkg/go/parser/parser_test.go
+++ b/src/pkg/go/parser/parser_test.go
@@ -5,6 +5,7 @@
package parser
import (
+ "go/ast"
"go/token"
"os"
"testing"
@@ -134,3 +135,46 @@ func TestParse4(t *testing.T) {
}
}\n}\n+\n+func TestColonEqualsScope(t *testing.T) {\n+\tf, err := ParseFile(fset, \"\", `package p; func f() { x, y, z := x, y, z }`, 0)\n+\tif err != nil {\n+\t\tt.Errorf(\"parse: %s\", err)\n+\t}\n+\n+\t// RHS refers to undefined globals; LHS does not.\n+\tas := f.Decls[0].(*ast.FuncDecl).Body.List[0].(*ast.AssignStmt)\n+\tfor _, v := range as.Rhs {\n+\t\tid := v.(*ast.Ident)\n+\t\tif id.Obj != nil {\n+\t\t\tt.Errorf(\"rhs %s has Obj, should not\", id.Name)\n+\t\t}\n+\t}\n+\tfor _, v := range as.Lhs {\n+\t\tid := v.(*ast.Ident)\n+\t\tif id.Obj == nil {\n+\t\t\tt.Errorf(\"lhs %s does not have Obj, should\", id.Name)\n+\t\t}\n+\t}\n+}\n+\n+func TestVarScope(t *testing.T) {\n+\tf, err := ParseFile(fset, \"\", `package p; func f() { var x, y, z = x, y, z }`, 0)\n+\tif err != nil {\n+\t\tt.Errorf(\"parse: %s\", err)\n+\t}\n+\n+\t// RHS refers to undefined globals; LHS does not.\n+\tas := f.Decls[0].(*ast.FuncDecl).Body.List[0].(*ast.DeclStmt).Decl.(*ast.GenDecl).Specs[0].(*ast.ValueSpec)\n+\tfor _, v := range as.Values {\n+\t\tid := v.(*ast.Ident)\n+\t\tif id.Obj != nil {\n+\t\t\tt.Errorf(\"rhs %s has Obj, should not\", id.Name)\n+\t\t}\n+\t}\n+\tfor _, id := range as.Names {\n+\t\tif id.Obj == nil {\n+\t\t\tt.Errorf(\"lhs %s does not have Obj, should\", id.Name)\n+\t\t}\n+\t}\n+}\n```
## コアとなるコードの解説
### `src/pkg/go/parser/parser.go` の変更点
1. **`parseLhsList` 関数**:
* `token.DEFINE`(`:=`演算子)のケースで、以前はコメントアウトされていた `p.shortVarDecl(p.makeIdentList(list))` の行が、より詳細なコメントに置き換えられました。
* 新しいコメントは、「スコープに入るのは後で、呼び出し元が適切なタイミングで`p.shortVarDecl`を呼び出す必要がある」と明記しています。これは、`:=`宣言の左辺の変数が、右辺の式が評価される前にスコープに入らないようにするための設計意図を明確にしています。
2. **`parseSimpleStmt` 関数**:
* この関数は、`x := y` のような単純な代入文や短い変数宣言をパースします。
* 追加されたコードブロック:
```go
if tok == token.DEFINE {
p.shortVarDecl(p.makeIdentList(x))
}
```
この変更により、`:=`演算子(`token.DEFINE`)が検出された場合、左辺の識別子リスト`x`に対して`p.shortVarDecl`が呼び出されるようになりました。この呼び出しは、右辺の式`y`がパースされた後に行われるため、Go言語の仕様に従って、左辺の変数が右辺の評価後にスコープに入ることを保証します。
3. **`parseCommClause` 関数**:
* この関数は、`select`文の`case`句内の通信操作(例: `case v := <-ch:`)をパースします。
* 追加されたコードブロック:
```go
if tok == token.DEFINE && lhs != nil {
p.shortVarDecl(p.makeIdentList(lhs))
}
```
`parseSimpleStmt`と同様に、通信句内で`:=`演算子が使用された場合、左辺の識別子リスト`lhs`に対して`p.shortVarDecl`が呼び出されるようになりました。これにより、`select`文内の短い変数宣言も、正しいスコープルールに従って処理されるようになります。
これらの変更は、`p.shortVarDecl`の呼び出しを、変数が実際にスコープに入るべきタイミング(つまり、右辺の式が完全にパースされた後)に移動させることで、`:=`宣言のスコープバグを修正しています。
### `src/pkg/go/parser/parser_test.go` の変更点
1. **`go/ast` パッケージのインポート**:
* 新しいテストケースでASTの構造を検証するために、`go/ast`パッケージがインポートされました。
2. **`TestColonEqualsScope` 関数**:
* このテストは、`x, y, z := x, y, z` のような短い変数宣言のスコープ挙動を検証します。
* **目的**: 右辺の`x, y, z`が、宣言される前のグローバルな(未定義の)変数として扱われ、左辺の`x, y, z`が新しい変数として正しくスコープに入っていることを確認します。
* **検証内容**:
* `as.Rhs`(右辺の式)の各識別子について、`id.Obj`が`nil`であることを確認します。これは、右辺の識別子が、同じ宣言で新しく導入される変数ではなく、未定義のグローバル変数(または既存の変数)を参照していることを意味します。もし`id.Obj`が`nil`でなければ、右辺がまだスコープに入っていない左辺の変数を参照してしまっていることになり、バグを示します。
* `as.Lhs`(左辺の変数)の各識別子について、`id.Obj`が`nil`でないことを確認します。これは、左辺の識別子が新しい変数として正しく宣言され、AST内でそのオブジェクトが関連付けられていることを意味します。
3. **`TestVarScope` 関数**:
* このテストは、`var x, y, z = x, y, z` のような通常の`var`宣言のスコープ挙動を検証します。
* **目的**: `var`宣言の場合も、短い変数宣言と同様に、右辺の式が評価される前に左辺の変数がスコープに入らないことを確認します。
* **検証内容**:
* `as.Values`(右辺の式)の各識別子について、`id.Obj`が`nil`であることを確認します。これは、右辺の識別子が、宣言される前のグローバルな(未定義の)変数として扱われていることを意味します。
* `as.Names`(左辺の変数)の各識別子について、`id.Obj`が`nil`でないことを確認します。これは、左辺の識別子が新しい変数として正しく宣言され、AST内でそのオブジェクトが関連付けられていることを意味します。
これらのテストケースは、`:=`宣言と`var`宣言の両方において、Go言語のスコープルールがパーサーによって正しく適用されていることを厳密に検証し、将来的な回帰を防ぐための重要な安全網となります。
## 関連リンク
* Go言語の公式ドキュメント: [https://go.dev/](https://go.dev/)
* Go言語の仕様: [https://go.dev/ref/spec](https://go.dev/ref/spec)
* `go/parser`パッケージのドキュメント: [https://pkg.go.dev/go/parser](https://pkg.go.dev/go/parser)
* `go/ast`パッケージのドキュメント: [https://pkg.go.dev/go/ast](https://pkg.go.dev/go/ast)
* このコミットのChange List (CL): [https://golang.org/cl/5327048](https://golang.org/cl/5327048)
## 参考にした情報源リンク
* Go言語の公式ドキュメントおよび仕様書
* Go言語のソースコード(特に`go/parser`パッケージ)
* Go言語に関する技術ブログやフォーラム(一般的なGo言語のスコープルールや`:=`演算子に関する情報)
* Go言語のASTに関する解説記事
* GitHubのコミット履歴と関連する議論
* Web検索結果: "Go language := operator scoping rules" (sparkcodehub.com, stackoverflow.com, glinteco.com, geeksforgeeks.org, tutorialspoint.com, secondspass.org, dev.to, go.dev)