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

[インデックス 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 = 10x := 10)。宣言の場合、左辺の識別子の型は右辺の式から推論されることがあります。
    • iota は、Go言語の定数宣言で使用される特殊な識別子で、連続する定数に自動的に値を割り当てるために使われます。このコミットでは、iota >= 0 が宣言の一部であることを示すフラグとして使われていましたが、変更後は decl フラグに統一されています。

技術的詳細

このコミットは、go/types パッケージ内の checker 構造体のメソッドである assign1to1assignNtoM に焦点を当てています。これらのメソッドは、Goプログラム内の代入文の型チェックを担当します。

assign1to1 関数の変更点

assign1to1 関数は、lhs = rhs のような単一の代入を型チェックします。

  1. コメントの明確化:

    • 変更前: 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 という具体的な用語を削除し、より抽象的な表現にすることで、コードの意図をより普遍的にしています。
  2. 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 のような複数の代入を型チェックします。

  1. コメントの追加と明確化:

    • 複数代入の2つの主要なケース(左辺と右辺の数が一致する場合、右辺が単一の式である場合)について、新しいコメントが追加され、コードの意図がより明確になりました。
    • 特に、右辺が単一の式である場合のコメントは、それが関数呼び出しによる複数戻り値や comma-ok 式である可能性を示唆しています。
  2. 型アサーションの変更:

    • 変更前: 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 変数(型アサーションの成功を示すブーリアン)の代わりに、アサートされた型 tnil でないことを直接チェックしています。これは機能的には同等ですが、一部のGoのコーディングスタイルでは、ok 変数を使用せずに直接 nil チェックを行うことが好まれる場合があります。これは、コードのわずかな簡潔化またはスタイルの一貫性のためと考えられます。
  3. x.mode = value の削除:

    • check.assign1to1(lhs[1], nil, &x, decl, iota) の直前にあった x.mode = value の行が削除されました。これは、その時点ですでに x.mode が正しく value に設定されているか、またはその設定が不要になったことを示唆しています。冗長なコードの削除によるクリーンアップです。
  4. 宣言時のエラーハンドリングの構造化 (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] の処理: 以前のコードでは、typTyp[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言語のソースコード (特に src/go/types ディレクトリ): https://github.com/golang/go
  • Go言語のコンパイラに関する一般的な情報源やブログ記事 (例: Go compiler internals, Go type system)
  • Go言語の nil の挙動に関する解説記事
  • Go言語の複数代入に関する解説記事
  • Go言語における goto の使用に関する議論