[インデックス 14005] ファイルの概要
このコミットは、Go言語のパーサーにおける特定の構文解析の誤りを修正するものです。具体的には、<-chan T(x)
のような表現が、正しく <- (chan T)(x)
として解釈されるように変更されました。これは、チャネル型と単項演算子(受信演算子 <-
)の組み合わせに関するパーサーの挙動を改善するものです。
コミット
commit 05dc3bf572931723b8af89386af4488c5e775851
Author: Robert Griesemer <gri@golang.org>
Date: Tue Oct 2 16:48:30 2012 -0700
go/parser: correctly parse <-chan T(x) as <-(chan T)(x)
Fixes #4110.
R=iant
CC=golang-dev
https://golang.org/cl/6597069
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/05dc3bf572931723b8af89386af4488c5e775851
元コミット内容
go/parser: correctly parse <-chan T(x) as <-(chan T)(x)
Fixes #4110.
R=iant
CC=golang-dev
https://golang.org/cl/6597069
変更の背景
この変更は、Go言語のパーサーが特定のチャネル型と単項演算子の組み合わせを誤って解釈していたバグ(Issue #4110)を修正するために行われました。
Go言語では、<-
演算子はチャネルからの受信(receive operation)を示す場合と、チャネルの方向(direction)を示す場合(例: <-chan int
は受信専用チャネル)があります。この二重の意味合いが、パーサーにとって曖昧さを生じさせることがありました。
具体的には、<-chan T(x)
のような構文において、パーサーはこれを (<-chan T)(x)
と解釈すべきか、それとも <- (chan T)(x)
と解釈すべきかで混乱していました。正しい解釈は後者であり、これは (chan T)
という型を持つ x
から値を受信する操作を意味します。しかし、以前のパーサーはこれを <-chan T
という型を x
に適用しようとする、不正な構文として扱っていた可能性があります。
この問題は、Go言語の構文解析の正確性に直接影響し、開発者が意図したコードが正しくコンパイルされない、あるいは予期せぬエラーが発生する原因となっていました。そのため、パーサーのロバスト性を向上させ、言語仕様に厳密に従った解析を行うためにこの修正が必要とされました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とパーサーの基本的な動作に関する知識が必要です。
-
Go言語のチャネル (Channels):
- チャネルはGoルーチン間で値を送受信するための通信メカニズムです。
chan T
は双方向チャネルを表します。<-chan T
は受信専用チャネル(receive-only channel)を表し、チャネルから値を受信することのみが可能です。chan<- T
は送信専用チャネル(send-only channel)を表し、チャネルに値を送信することのみが可能です。<-
演算子は、チャネルの型定義において方向を示す場合と、チャネルからの値の受信操作を示す場合があります。- 例:
c <- v
はv
をチャネルc
に送信する操作。 - 例:
v := <-c
はチャネルc
から値を受信し、v
に代入する操作。 - 例:
func f(c <-chan int)
は受信専用のint
型チャネルを引数にとる関数定義。
- 例:
-
Go言語の型アサーションと型変換:
- Go言語では、
T(x)
の形式で型変換(type conversion)や型アサーション(type assertion)を行うことができます。 T(x)
は、x
を型T
に変換しようと試みます。この文脈では、T
はチャネル型chan int
のような型を表します。
- Go言語では、
-
抽象構文木 (AST: Abstract Syntax Tree):
- コンパイラやインタープリタは、ソースコードを直接処理するのではなく、まずソースコードを抽象構文木というツリー構造に変換します。
- ASTは、プログラムの構造を抽象的に表現したもので、各ノードはプログラムの構成要素(変数、関数、演算子など)に対応します。
- Go言語の
go/ast
パッケージは、このASTを表現するためのデータ構造を提供します。 ast.ChanType
はチャネル型を表すASTノードです。ast.UnaryExpr
は単項演算子(例:!
、+
、-
、<-
)を含む式を表すASTノードです。
-
パーサー (Parser):
- パーサーは、字句解析器(lexer/scanner)によって生成されたトークン列を受け取り、それらを文法規則に従って解析し、ASTを構築する役割を担います。
- パーサーは、曖昧な構文を解決し、正しいASTを生成する必要があります。このコミットのケースでは、
<-
演算子の二重の意味合いが曖昧さを生んでいました。
-
単項演算子の優先順位と結合性:
- プログラミング言語では、演算子には優先順位と結合性があります。これにより、複雑な式がどのように評価されるかが決まります。
<-
演算子は、チャネルからの受信操作の場合、比較的高い優先順位を持ちます。しかし、型定義の一部として使用される場合は、その文脈で解釈されます。
これらの概念を理解することで、<-chan T(x)
という構文がなぜ問題となり、どのように修正されたのかを深く把握することができます。
技術的詳細
このコミットの核心は、Go言語のパーサーが <-
演算子を処理するロジックの改善にあります。特に、parseUnaryExpr
関数内で、<-
がチャネル型の一部なのか、それとも受信操作なのかを正確に区別するメカニズムが導入されました。
以前のパーサーのロジックでは、<-
の直後に chan
キーワードが続く場合、すぐにチャネル型として解釈していました。
// 変更前 (簡略化)
if p.tok == token.CHAN {
p.next()
value := p.parseType()
return &ast.ChanType{Begin: pos, Dir: ast.RECV, Value: value}
}
このアプローチでは、<-chan T(x)
のようなケースで問題が生じます。パーサーは <-chan T
を一つのチャネル型として認識し、その後に続く (x)
をこの型に対する不正な適用と見なしてしまう可能性がありました。しかし、正しい解釈は <- (chan T)(x)
であり、これは (chan T)(x)
という式の結果から値を受信する操作です。
新しいロジックでは、<-
を検出した後、すぐにチャネル型と断定するのではなく、まず p.parseUnaryExpr(false)
を呼び出して、<-
の右側の式(x
)を解析します。この x
が解析された後、その結果が *ast.ChanType
であるかどうかをチェックします。
// 変更後 (簡略化)
x := p.parseUnaryExpr(false) // まず <- の右側の式を解析する
if typ, ok := x.(*ast.ChanType); ok {
// ここに到達した場合、x はチャネル型 (例: chan int, <-chan int, chan<- int) である
// これは、<- (chan type) の形式である可能性が高い
// この場合、<- はチャネル型の一部として再関連付けされる必要がある
// 例: <- (chan int) は (<-chan int) となる
// 例: <- (chan<- int) は (<-chan (<-chan int)) となる
// 複数の <- が連なる場合 (例: <- <-chan int) もここで処理される
// 既存のチャネル型の方向 (Dir) と開始位置 (Begin) を調整し、
// 最終的に正しいチャネル型ASTノードを構築する
// エラーチェックもここで行われる (例: <- (<-chan T) は不正)
return x // 調整されたチャネル型ASTノードを返す
}
// ここに到達した場合、x はチャネル型ではない
// これは、<- (expr) の形式である
// この場合、<- は受信演算子として機能する
return &ast.UnaryExpr{OpPos: pos, Op: token.ARROW, X: p.checkExpr(x)}
この変更により、パーサーは以下の2つのケースを正確に区別できるようになります。
<- (chan type)
: この場合、<-
はチャネル型の一部として解釈され、ast.ChanType
ノードのDir
(方向) とBegin
(開始位置) が適切に調整されます。例えば、<- (chan int)
は(<-chan int)
という受信専用チャネル型としてASTに表現されます。複数の<-
が連なる場合(例:<- <-chan int
)も、このロジックで再帰的に処理されます。<- (expr)
: この場合、<-
はチャネルからの受信演算子として解釈され、ast.UnaryExpr
ノードが生成されます。expr
はチャネル型の値を持つ任意の式です。
特に注目すべきは、for ok && arrow
ループ内で、既存の ast.ChanType
の Dir
を ast.RECV
に設定し、Begin
を最初の <-
の位置に再関連付けしている点です。これにより、<-chan T
のような型が正しく構築されます。また、typ.Dir == ast.RECV
の場合にエラーを報告することで、<- (<-chan T)
のような不正な二重受信チャネル型を検出しています。
この修正は、Go言語のパーサーがより複雑な型表現と演算子の組み合わせを、言語仕様に厳密に従って解析するための重要なステップです。
コアとなるコードの変更箇所
変更は主に src/pkg/go/parser/parser.go
の parseUnaryExpr
関数と、テストファイル src/pkg/go/parser/short_test.go
にあります。
src/pkg/go/parser/parser.go
--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -1399,13 +1399,50 @@ func (p *parser) parseUnaryExpr(lhs bool) ast.Expr {
// channel type or receive expression
pos := p.pos
p.next()
- if p.tok == token.CHAN {
- p.next()
- value := p.parseType()
- return &ast.ChanType{Begin: pos, Dir: ast.RECV, Value: value}
- }
+
+ // If the next token is token.CHAN we still don't know if it
+ // is a channel type or a receive operation - we only know
+ // once we have found the end of the unary expression. There
+ // are two cases:
+ //
+ // <- type => (<-type) must be channel type
+ // <- expr => <-(expr) is a receive from an expression
+ //
+ // In the first case, the arrow must be re-associated with
+ // the channel type parsed already:
+ //
+ // <- (chan type) => (<-chan type)
+ // <- (chan<- type) => (<-chan (<-type))
+
+ x := p.parseUnaryExpr(false)
+
+ // determine which case we have
+ if typ, ok := x.(*ast.ChanType); ok {
+ // (<-type)
+
+ // re-associate position info and <-
+ arrow := true
+ for ok && arrow {
+ begin := typ.Begin
+ if typ.Dir == ast.RECV {
+ // error: (<-type) is (<-(<-chan T))
+ p.errorExpected(begin, "'chan'")
+ }
+ arrow = typ.Dir == ast.SEND
+ typ.Begin = pos
+ typ.Dir = ast.RECV
+ typ, ok = typ.Value.(*ast.ChanType)
+ // TODO(gri) ast.ChanType should store exact <- position
+ pos = begin // estimate (we don't have the exact position of <- for send channels)
+ }
+ if arrow {
+ p.errorExpected(pos, "'chan'")
+ }
+
+ return x
+ }
+
+ // <-(expr)
+ return &ast.UnaryExpr{OpPos: pos, Op: token.ARROW, X: p.checkExpr(x)}
src/pkg/go/parser/short_test.go
テストケースが追加・修正されています。
--- a/src/pkg/go/parser/short_test.go
+++ b/src/pkg/go/parser/short_test.go
@@ -13,8 +13,10 @@ var valids = []string{
`package p;`,\
`package p; import \"fmt\"; func f() { fmt.Println(\"Hello, World!\") };`,\
`package p; func f() { if f(T{}) {} };`,\
- `package p; func f() { _ = (<-chan int)(x) };`,\
- `package p; func f() { _ = (<-chan <-chan int)(x) };`,\
+ `package p; func f() { _ = <-chan int(nil) };`,\
+ `package p; func f() { _ = (<-chan int)(nil) };`,\
+ `package p; func f() { _ = (<-chan <-chan int)(nil) };`,\
+ `package p; func f() { _ = <-chan <-chan <-chan <-chan <-int(nil) };`,\
`package p; func f(func() func() func());`,\
`package p; func f(...T);`,\
`package p; func f(float, ...int);`,\
@@ -64,8 +66,10 @@ var invalids = []string{
`package p; var a = []int{[ /* ERROR \"expected expression\" */ ]int};`,\
`package p; var a = ( /* ERROR \"expected expression\" */ []int);`,\
`package p; var a = a[[ /* ERROR \"expected expression\" */ ]int:[]int];`,\
- `package p; var a = <- /* ERROR \"expected expression\" */ chan int;`,\
- `package p; func f() { select { case _ <- chan /* ERROR \"expected expression\" */ int: } };`,\
+ `package p; var a = <- /* ERROR \"expected expression\" */ chan int;`,\
+ `package p; func f() { select { case _ <- chan /* ERROR \"expected expression\" */ int: } };`,\
+ `package p; func f() { _ = (<-<- /* ERROR \"expected 'chan'\" */ chan int)(nil) };`,\
+ `package p; func f() { _ = (<-chan<-chan<-chan<-chan<-chan /* ERROR \"expected 'chan'\" */ <-int)(nil) };`,\
}\
\n func TestInvalid(t *testing.T) {\
コアとなるコードの解説
src/pkg/go/parser/parser.go
の parseUnaryExpr
関数は、単項演算子を解析する役割を担っています。この関数内で <-
トークンが検出された際の処理が大幅に変更されました。
変更前:
<-
の直後に chan
が続く場合、すぐに受信専用チャネル型 ast.ChanType{Dir: ast.RECV}
を構築していました。これは単純なケースでは機能しますが、<-chan T(x)
のような、<-
の後に型変換が続く場合に問題を引き起こしました。パーサーは <-chan T
を型として認識し、その後の (x)
を不正な構文として扱ってしまう可能性がありました。
変更後:
x := p.parseUnaryExpr(false)
:<-
を読み込んだ後、まず<-
の右側の式を再帰的にparseUnaryExpr
で解析します。これにより、chan T(x)
の部分が先に解析され、その結果がx
に格納されます。if typ, ok := x.(*ast.ChanType); ok
: 解析されたx
がast.ChanType
であるかどうかをチェックします。x
がast.ChanType
の場合: これは<- (chan type)
の形式であることを意味します。例えば、x
が(chan int)
や(chan<- int)
のようなチャネル型として解析された場合です。for ok && arrow
ループ: このループは、<- <-chan int
のように複数の<-
が連なるケースを処理するために導入されました。typ.Begin = pos
: 現在の<-
の位置をチャネル型の開始位置として設定し直します。typ.Dir = ast.RECV
: チャネルの方向を受信専用 (ast.RECV
) に設定します。これは、外側の<-
が受信方向を示すためです。if typ.Dir == ast.RECV { p.errorExpected(begin, "'chan'") }
:<- (<-chan T)
のように、既に受信専用であるチャネル型に対してさらに受信演算子を適用しようとする不正な構文を検出します。arrow = typ.Dir == ast.SEND
: 内側のチャネルが送信専用 (chan<-
) であれば、さらに<-
を適用できる可能性があるため、ループを継続します。typ, ok = typ.Value.(*ast.ChanType)
: ネストされたチャネル型を処理するために、Value
フィールドを次のチャネル型として取得します。
- このループとロジックにより、
<- (chan int)
は(<-chan int)
に、<- (chan<- int)
は(<-chan (<-chan int))
に、<- <-chan <-chan <-chan <-int
のような複雑なケースも正しく解析されるようになります。
x
がast.ChanType
でない場合: これは<- (expr)
の形式であることを意味します。expr
はチャネル型ではない通常の式です。この場合、<-
はチャネルからの受信演算子として機能します。return &ast.UnaryExpr{OpPos: pos, Op: token.ARROW, X: p.checkExpr(x)}
:<-
を単項演算子 (token.ARROW
) として持つast.UnaryExpr
ノードを生成して返します。
この修正により、パーサーは <-
演算子の文脈をより正確に判断し、Go言語のチャネル構文の曖昧さを解消できるようになりました。
src/pkg/go/parser/short_test.go
では、この修正が正しく機能することを確認するための新しいテストケースが追加されています。特に、<-chan int(nil)
や (<-chan <-chan int)(nil)
のような、チャネル型と型変換、そして複数の <-
演算子が絡む複雑なケースが正しく解析されることを検証しています。また、<-<-chan int
のような不正な構文がエラーとして検出されることも確認しています。
関連リンク
- Go Issue #4110: https://github.com/golang/go/issues/4110
- このコミットが修正した具体的なバグ報告です。詳細な議論や再現コードが記載されている可能性があります。
- Go Code Review (CL) 6597069: https://golang.org/cl/6597069
- このコミットに対応するGoのコードレビューシステム(Gerrit)上の変更リストです。レビューコメントや変更の経緯がより詳細に記録されています。
参考にした情報源リンク
- Go言語の公式ドキュメント(チャネル、型、演算子に関するセクション)
- Go言語のASTに関するドキュメント(
go/ast
パッケージ) - コンパイラの理論に関する一般的な情報(字句解析、構文解析、AST構築)
- Go言語のパーサーのソースコード(
src/go/parser
ディレクトリ) - Go言語のIssueトラッカー(Issue #4110の議論)
- Go言語のコードレビューシステム(CL 6597069のレビュー)
- Go言語の仕様書(特にチャネル型と演算子の優先順位に関する記述)
- Go言語のブログや技術記事(パーサーの動作やチャネルの構文に関する解説)
- Go言語のコミュニティフォーラムやメーリングリスト(golang-devなど)