[インデックス 15633] ファイルの概要
このコミットは、Go言語のコンパイラにおける型チェック機構の中核を担う go/types
パッケージ内の stmt.go
ファイルに対する内部的なクリーンアップと改善を目的としています。具体的には、代入文の型チェックロジック、特に単一代入 (assign1to1
) および複数代入 (assignNtoM
) の処理におけるエラーハンドリングと型推論の挙動が洗練されています。
コミット
commit 6ee75663c987dca914a34cf298e65484088250a8
Author: Robert Griesemer <gri@golang.org>
Date: Thu Mar 7 11:17:30 2013 -0800
go/types: more internal cleanups
R=adonovan, bradfitz
CC=golang-dev
https://golang.org/cl/7492045
---
src/pkg/go/types/stmt.go | 66 +++++++++++++++++++++++++-----------------------
1 file changed, 34 insertions(+), 32 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6ee75663c987dca914a34cf298e65484088250a8
元コミット内容
このコミットは、src/pkg/go/types/stmt.go
ファイルに対して行われた変更です。主な変更点は以下の通りです。
assign1to1
関数のコメント修正と、Typ[UntypedNil]
の型チェックロジックの改善。assignNtoM
関数のコメント修正、型アサーションの変更、そして特に宣言時のエラーハンドリングの構造化(goto Error
ラベルの導入)。
これらの変更は、型チェッカーの堅牢性と正確性を向上させ、コードの可読性を高めることを目的としています。
変更の背景
Go言語のコンパイラは、コードの正確性を保証するために厳密な型チェックを行います。go/types
パッケージは、この型チェックの中核を担う部分であり、Goプログラムの抽象構文木(AST)を走査し、各式の型を決定し、型規則に違反がないかを確認します。
このコミットが行われた2013年3月は、Go言語がまだ比較的新しく、コンパイラや標準ライブラリが活発に開発・改善されていた時期です。特に go/types
パッケージは、Go 1.0のリリース後も継続的に改良が加えられており、より正確で効率的な型チェックを実現するための内部的なクリーンアップやバグ修正が頻繁に行われていました。
この「内部的なクリーンアップ」というコミットメッセージは、特定のバグ修正というよりも、既存のコードベースの品質向上、ロジックの明確化、そして将来的な拡張性や保守性の向上を意図していることを示唆しています。特に、型チェックにおけるエラーパスの統一や、nil
のような特殊な型の扱いをより厳密にすることは、コンパイラの安定性にとって重要です。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラの基本的な概念を理解しておく必要があります。
- Go言語の型システム: Goは静的型付け言語であり、すべての変数と式には型があります。型チェックは、プログラムが実行される前にこれらの型が正しく使用されていることを保証します。
go/types
パッケージ: Go標準ライブラリの一部であり、Goプログラムの型チェックを行うためのAPIを提供します。これは、Goコンパイラのフロントエンドの一部として機能し、ソースコードのASTを受け取り、型情報を付与し、型エラーを報告します。- 抽象構文木 (AST): ソースコードを解析して得られる、プログラムの構造を木構造で表現したものです。
go/ast
パッケージで定義されるast.Expr
は式を表し、ast.Ident
は識別子(変数名、関数名など)を表します。 operand
構造体:go/types
パッケージ内で使用される内部的な構造体で、式の結果(値、型、モードなど)を表現します。mode
フィールドは、式が値、型、パッケージなどを表すかを示します。Typ[UntypedNil]
とTyp[Invalid]
:Typ[UntypedNil]
: Go言語におけるnil
は、特定の型を持たない「untyped nil」として扱われます。これは、ポインタ、スライス、マップ、チャネル、インターフェースなど、複数の型に代入可能です。型チェッカーは、nil
が具体的な型に代入される際にその型を推論します。Typ[Invalid]
: 型チェック中にエラーが発生した場合や、型が決定できない場合に割り当てられる特殊な型です。この型が割り当てられた式は、それ以降の型チェックでエラーとして扱われます。
defaultType
関数: Go言語では、型が明示されていない数値リテラルやブーリアンリテラル(例:100
,true
)は「untyped constant」として扱われます。これらは、代入や演算の際に文脈に応じて適切なデフォルト型(例:int
,bool
)に変換されます。defaultType
関数は、この変換を行います。- 代入文の型チェック: Go言語には、単一代入 (
x = y
) と複数代入 (x, y = a, b
またはx, y = funcReturningTwoValues()
) があります。型チェッカーは、左辺と右辺の数と型が一致するかを検証します。特に、関数が複数の戻り値を返す場合や、comma-ok
イディオム(例:v, ok := m[key]
)の場合の複数代入は複雑なロジックを必要とします。 - 宣言 (
decl
) とiota
:decl
パラメータは、現在の代入が変数宣言の一部であるかどうかを示します(例:var x = 10
やx := 10
)。宣言の場合、左辺の識別子の型は右辺の式から推論されることがあります。iota
は、Go言語の定数宣言で使用される特殊な識別子で、連続する定数に自動的に値を割り当てるために使われます。このコミットでは、iota >= 0
が宣言の一部であることを示すフラグとして使われていましたが、変更後はdecl
フラグに統一されています。
技術的詳細
このコミットは、go/types
パッケージ内の checker
構造体のメソッドである assign1to1
と assignNtoM
に焦点を当てています。これらのメソッドは、Goプログラム内の代入文の型チェックを担当します。
assign1to1
関数の変更点
assign1to1
関数は、lhs = rhs
のような単一の代入を型チェックします。
-
コメントの明確化:
- 変更前:
lhs = x (if rhs == nil). If decl is set, the lhs operand must be an identifier;
- 変更後:
lhs = x (if rhs == nil). If decl is set, the lhs expression must be an identifier;
operand
からexpression
への変更は、より一般的な用語を使用し、左辺が単なるオペランドではなく、式全体として扱われることを示唆しています。// don't exit for declarations - we need the lhs obj first
から// don't exit for declarations - we need the lhs first
への変更は、obj
という具体的な用語を削除し、より抽象的な表現にすることで、コードの意図をより普遍的にしています。
- 変更前:
-
Typ[UntypedNil]
の処理の改善: この変更は、nil
の型チェックロジックをより堅牢にします。- 変更前は、
typ == Typ[UntypedNil]
の場合、エラーを報告し、左辺のオブジェクトの型をTyp[Invalid]
に設定してすぐにreturn
していました。その後、defaultType(typ)
が無条件に呼び出されていました。 - 変更後は、
Typ[UntypedNil]
の場合、エラーを報告し、typ
自体をTyp[Invalid]
に設定しますが、return
はしません。代わりに、else
ブロックでdefaultType(typ)
が呼び出されるようになりました。 - この変更の意図は、
Typ[UntypedNil]
がエラーとして扱われた場合でも、その後の処理フローを継続させ、defaultType
が不適切に呼び出されることを防ぐことにあります。これにより、型チェックのロジックがより一貫性を持ち、エラー発生時の挙動が予測可能になります。
- 変更前は、
assignNtoM
関数の変更点
assignNtoM
関数は、lhs1, ..., lhsN = rhs1, ..., rhsM
のような複数の代入を型チェックします。
-
コメントの追加と明確化:
- 複数代入の2つの主要なケース(左辺と右辺の数が一致する場合、右辺が単一の式である場合)について、新しいコメントが追加され、コードの意図がより明確になりました。
- 特に、右辺が単一の式である場合のコメントは、それが関数呼び出しによる複数戻り値や
comma-ok
式である可能性を示唆しています。
-
型アサーションの変更:
- 変更前:
if t, ok := x.typ.(*Result); ok && len(lhs) == len(t.Values) {
- 変更後:
if t, _ := x.typ.(*Result); t != nil && len(lhs) == len(t.Values) {
ok
変数(型アサーションの成功を示すブーリアン)の代わりに、アサートされた型t
がnil
でないことを直接チェックしています。これは機能的には同等ですが、一部のGoのコーディングスタイルでは、ok
変数を使用せずに直接nil
チェックを行うことが好まれる場合があります。これは、コードのわずかな簡潔化またはスタイルの一貫性のためと考えられます。
- 変更前:
-
x.mode = value
の削除:check.assign1to1(lhs[1], nil, &x, decl, iota)
の直前にあったx.mode = value
の行が削除されました。これは、その時点ですでにx.mode
が正しくvalue
に設定されているか、またはその設定が不要になったことを示唆しています。冗長なコードの削除によるクリーンアップです。
-
宣言時のエラーハンドリングの構造化 (
goto Error
): この変更は、assignNtoM
関数における宣言時のエラー処理を大幅に改善します。- 変更前は、
x.mode == invalid
の場合にreturn
していましたが、これにより左辺の識別子が型付けされないままになる可能性がありました。そして、iota >= 0
(宣言を示す)の場合に、各左辺の識別子をループしてTyp[Invalid]
を設定していました。このロジックは分散しており、エラーパスが複数存在していました。 - 変更後は、
x.mode == invalid
の場合にgoto Error
を使用して、関数の末尾にある共通のエラーハンドリングブロックにジャンプします。 Error:
ラベルのブロックでは、if decl
(宣言の場合)という条件で、すべての左辺の識別子をループし、その型をTyp[Invalid]
に設定します。- さらに、左辺の式が
ast.Ident
でない場合(例:_ = 10
のような不正な宣言)のチェックが追加され、より具体的なエラーメッセージが報告されるようになりました。 - この
goto
の使用は、Go言語では一般的に推奨されませんが、エラー処理のような特定の状況では、コードの重複を避け、エラーパスを明確にするために使用されることがあります。ここでは、複数のエラー発生箇所から単一のエラー処理ロジックに集約するために採用されています。これにより、宣言時の型エラー処理が一元化され、堅牢性が向上しています。
- 変更前は、
コアとなるコードの変更箇所
src/pkg/go/types/stmt.go
ファイル内の assign1to1
関数と assignNtoM
関数が変更されています。
assign1to1
の変更点
--- a/src/pkg/go/types/stmt.go
+++ b/src/pkg/go/types/stmt.go
@@ -34,8 +34,8 @@ func (check *checker) assignment(x *operand, to Type) bool {
return x.mode != invalid && x.isAssignable(check.ctxt, to)
}
-// assign1to1 typechecks a single assignment of the form lhs = rhs (if rhs != nil),
-// or lhs = x (if rhs == nil). If decl is set, the lhs operand must be an identifier;
+// assign1to1 typechecks a single assignment of the form lhs = rhs (if rhs != nil), or
+// lhs = x (if rhs == nil). If decl is set, the lhs expression must be an identifier;
// if its type is not set, it is deduced from the type of x or set to Typ[Invalid] in
// case of an error.
//
@@ -45,7 +45,7 @@ func (check *checker) assign1to1(lhs, rhs ast.Expr, x *operand, decl bool, iota
if x == nil {
x = new(operand)
check.expr(x, rhs, nil, iota)
- // don't exit for declarations - we need the lhs obj first
+ // don't exit for declarations - we need the lhs first
if x.mode == invalid && !decl {
return
}
@@ -117,10 +117,10 @@ func (check *checker) assign1to1(lhs, rhs ast.Expr, x *operand, decl bool, iota
// convert untyped types to default types
if typ == Typ[UntypedNil] {
check.errorf(x.pos(), "use of untyped nil")
- obj.Type = Typ[Invalid]
- return
+ typ = Typ[Invalid]
+ } else {
+ typ = defaultType(typ)
}
- typ = defaultType(typ)
}
}
obj.Type = typ
assignNtoM
の変更点
--- a/src/pkg/go/types/stmt.go
+++ b/src/pkg/go/types/stmt.go
@@ -156,15 +156,16 @@ func (check *checker) assign1to1(lhs, rhs ast.Expr, x *operand, decl bool, iota
}
}
-// assignNtoM typechecks a general assignment. If decl is set, the lhs operands
-// must be identifiers. If their types are not set, they are deduced from the
-// types of the corresponding rhs expressions. iota >= 0 indicates that the
-// "assignment" is part of a constant/variable declaration.
+// assignNtoM typechecks a general assignment. If decl is set, the lhs expressions
+// must be identifiers; if their types are not set, they are deduced from the types
+// of the corresponding rhs expressions, or set to Typ[Invalid] in case of an error.
// Precondition: len(lhs) > 0 .
//
func (check *checker) assignNtoM(lhs, rhs []ast.Expr, decl bool, iota int) {
assert(len(lhs) > 0)
+ // If the lhs and rhs have corresponding expressions, treat each
+ // matching pair as an individual pair.
if len(lhs) == len(rhs) {
for i, e := range rhs {
check.assign1to1(lhs[i], e, nil, decl, iota)
@@ -172,20 +173,20 @@ func (check *checker) assignNto1(lhs, rhs ast.Expr, x *operand, decl bool, iota
return
}
+ // Otherwise, the rhs must be a single expression (possibly
+ // a function call returning multiple values, or a comma-ok
+ // expression).
if len(rhs) == 1 {
- // len(lhs) > 1, therefore a correct rhs expression
- // cannot be a shift and we don't need a type hint;
- // ok to evaluate rhs first
+ // len(lhs) > 1
+ // Start with rhs so we have expression types
+ // for declarations with implicit types.
var x operand
check.expr(&x, rhs[0], nil, iota)
if x.mode == invalid {
- // If decl is set, this leaves the lhs identifiers
- // untyped. We catch this when looking up the respective
- // object.
- return
+ goto Error
}
- if t, ok := x.typ.(*Result); ok && len(lhs) == len(t.Values) {
+ if t, _ := x.typ.(*Result); t != nil && len(lhs) == len(t.Values) {
// function result
x.mode = value
for i, obj := range t.Values {
@@ -201,7 +202,6 @@ func (check *checker) assignNto1(lhs, rhs ast.Expr, x *operand, decl bool, iota
x.mode = value
check.assign1to1(lhs[0], nil, &x, decl, iota)
- x.mode = value
x.typ = Typ[UntypedBool]
check.assign1to1(lhs[1], nil, &x, decl, iota)
return
@@ -210,20 +210,22 @@ func (check *checker) assignNto1(lhs, rhs ast.Expr, x *operand, decl bool, iota
check.errorf(lhs[0].Pos(), "assignment count mismatch: %d = %d", len(lhs), len(rhs))
- // avoid checking the same declaration over and over
- // again for each lhs identifier that has no type yet
- if iota >= 0 {
- // declaration
+ Error:
+ // In case of a declaration, set all lhs types to Typ[Invalid].
+ if decl {
for _, e := range lhs {
- if name, ok := e.(*ast.Ident); ok {
- switch obj := check.lookup(name).(type) {
- case *Const:
- obj.Type = Typ[Invalid]
- case *Var:
- obj.Type = Typ[Invalid]
- default:
- unreachable()
- }
+ ident, _ := e.(*ast.Ident)
+ if ident == nil {
+ check.errorf(e.Pos(), "cannot declare %s", e)
+ continue
+ }
+ switch obj := check.lookup(ident).(type) {
+ case *Const:
+ obj.Type = Typ[Invalid]
+ case *Var:
+ obj.Type = Typ[Invalid]
+ default:
+ unreachable()
}
}
}
コアとなるコードの解説
assign1to1
の変更解説
assign1to1
関数は、単一の代入 lhs = rhs
を処理します。
- コメントの修正:
lhs operand
からlhs expression
への変更は、より正確な用語の使用を反映しています。GoのASTでは、左辺はast.Expr
として表現され、それがast.Ident
(識別子)である場合に特別な処理が行われます。 Typ[UntypedNil]
の処理: 以前のコードでは、typ
がTyp[UntypedNil]
の場合、エラーを報告し、左辺のオブジェクトの型をTyp[Invalid]
に設定してすぐにreturn
していました。しかし、その後のtyp = defaultType(typ)
は無条件に実行されるため、return
された後では意味がありませんでした。新しいコードでは、Typ[UntypedNil]
の場合でもreturn
せずに、typ
自体をTyp[Invalid]
に設定し、defaultType(typ)
の呼び出しをelse
ブロック内に移動しました。これにより、Typ[UntypedNil]
がエラーとして処理された場合、defaultType
が呼び出されることなく、型チェックのフローが継続されます。これは、エラー発生時の型伝播をより正確に制御し、不必要な型変換を防ぐための改善です。
assignNtoM
の変更解説
assignNtoM
関数は、複数の代入 lhs1, ..., lhsN = rhs1, ..., rhsM
を処理します。
- コメントの追加: 左辺と右辺の数が一致する場合と、右辺が単一の式である場合のロジックフローを説明するコメントが追加され、コードの意図が明確になりました。
- 型アサーションの変更:
if t, ok := x.typ.(*Result); ok
からif t, _ := x.typ.(*Result); t != nil
への変更は、ok
変数を使用せずに直接t != nil
をチェックするスタイルへの移行です。これは機能的な違いはありませんが、コードの簡潔性を追求したものです。 - 冗長な
x.mode = value
の削除:check.assign1to1(lhs[1], nil, &x, decl, iota)
の直前にあったx.mode = value
の行が削除されました。これは、その時点ですでにx.mode
が正しく設定されているため、この行が冗長であったことを示しています。 - エラーハンドリングの構造化: 最も重要な変更は、宣言時のエラーハンドリングの改善です。
- 以前は、
x.mode == invalid
の場合にreturn
していましたが、これにより宣言された変数が型付けされないままになる可能性がありました。また、宣言(iota >= 0
)の場合に、各左辺の識別子をループしてTyp[Invalid]
を設定するロジックが分散していました。 - 新しいコードでは、
x.mode == invalid
の場合にgoto Error
を使用して、関数の末尾に新しく追加されたError:
ラベルのブロックにジャンプします。 - この
Error:
ブロックでは、if decl
(代入が宣言の一部である場合)という条件で、すべての左辺の式をループします。 - 各左辺の式が
ast.Ident
(識別子)でない場合(例:_ = 10
のような不正な宣言)、check.errorf
でエラーを報告し、次の左辺の処理に進みます。 - 識別子である場合は、
check.lookup
で対応するオブジェクト(定数または変数)を取得し、その型をTyp[Invalid]
に設定します。 - この
goto
を用いたエラー処理の構造化により、複数のエラー発生箇所から単一の共通エラー処理ロジックに集約され、コードの重複が減り、宣言時の型エラー処理がより堅牢で一貫性のあるものになりました。
- 以前は、
これらの変更は、Goコンパイラの型チェッカーの内部的な品質と正確性を向上させ、特にエラー発生時の挙動をより予測可能にするための重要なステップです。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
go/types
パッケージのドキュメント: https://pkg.go.dev/go/typesgo/ast
パッケージのドキュメント: https://pkg.go.dev/go/ast- Gerrit Change-Id:
7492045
(このコミットの元のGerritレビューページ)
参考にした情報源リンク
- Go言語のソースコード (特に
src/go/types
ディレクトリ): https://github.com/golang/go - Go言語のコンパイラに関する一般的な情報源やブログ記事 (例: Go compiler internals, Go type system)
- Go言語の
nil
の挙動に関する解説記事 - Go言語の複数代入に関する解説記事
- Go言語における
goto
の使用に関する議論