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

[インデックス 15207] ファイルの概要

このコミットは、Go言語の型システムにおける len および cap ビルトイン関数の挙動に関する修正です。具体的には、ポインタを通じてアクセスされる配列の長さや容量が、特定の条件下で定数として扱われるべきであるにもかかわらず、そうではないというバグ(Issue 4744)を修正しています。

コミット

commit ae8da3a28c4182acec1f74f22a615a68fc5c195d
Author: Robert Griesemer <gri@golang.org>
Date:   Mon Feb 11 22:39:55 2013 -0800

    go/types: len(((*T)(nil)).X) is const if X is an array
    
    Fixes #4744.
    
    R=adonovan
    CC=golang-dev
    https://golang.org/cl/7305080

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/ae8da3a28c4182acec1f74f22a615a68fc5c195d

元コミット内容

このコミットは、Go言語の型チェッカー(go/typesパッケージ)において、lencapビルトイン関数が、ポインタを介してアクセスされる配列のフィールドに対して適用された場合に、その結果が定数となるべきケースで定数として扱われない問題を修正します。具体的には、len(((*T)(nil)).X)のような式(ここでXは配列型のフィールド)が定数として評価されるように変更されました。これはGo言語の仕様に準拠するための修正です。

変更の背景

Go言語の仕様では、len(s)cap(s)の式は、sの型が配列または配列へのポインタであり、かつsの式がチャネル受信や関数呼び出しを含まない場合、定数として扱われると定められています。

しかし、Goの型チェッカーの実装において、(*T)(nil)のようなnilポインタのデリファレンスを通じて構造体の配列フィールドにアクセスするケース(例: len(((*T)(nil)).a))が、この定数評価の対象から漏れていました。これは、lencapの引数がポインタのデリファレンスを含む場合に、型チェッカーがその引数を「関数呼び出しやチャネル受信を含む」と誤って判断し、結果として定数ではなく実行時に評価される値として扱ってしまっていたためです。

この問題はGo Issue #4744として報告され、runtime/debugパッケージのコンパイルエラーを引き起こしていました。runtime/debugパッケージには、このような形式のlen呼び出しが含まれており、それが型チェッカーによって不正な定数式と判断され、コンパイルが通らないという状況でした。

前提知識の解説

  • Go言語の型システム: Go言語は静的型付け言語であり、コンパイル時に厳密な型チェックが行われます。go/typesパッケージは、Goコンパイラの一部として、プログラムの型チェックを担当します。
  • lencapビルトイン関数:
    • len(v): vの長さ(要素数)を返します。配列、スライス、文字列、マップ、チャネルに適用可能です。
    • cap(v): vの容量を返します。配列、スライス、チャネルに適用可能です。
    • 定数評価: Go言語では、コンパイル時に値が確定する式は「定数」として扱われます。lencapは、引数が配列または配列へのポインタであり、かつ副作用(関数呼び出しやチャネル受信)がない場合に定数として評価されるという特別なルールがあります。これにより、コンパイル時に最適化が行われ、実行時のオーバーヘッドが削減されます。
  • 抽象構文木 (AST): Goのソースコードは、コンパイラによって抽象構文木(AST)にパースされます。ASTはプログラムの構造を木構造で表現したもので、型チェックやコード生成の基盤となります。
  • ポインタのデリファレンス: Goにおけるポインタは、メモリ上のアドレスを指します。*Pのようにポインタをデリファレンスすることで、そのアドレスに格納されている値にアクセスできます。
  • ast.CallExprと型変換: Goでは、型変換(例: int(x))もAST上ではast.CallExprとして表現されます。これは通常の関数呼び出しと同じASTノードであるため、型チェッカーはこれらを区別する必要があります。

技術的詳細

このコミットの主要な変更点は以下の通りです。

  1. implicitDerefからimplicitArrayDerefへの名称変更と役割の明確化:

    • 以前のimplicitDeref関数は、型がポインタ*Aであり、かつAが配列である場合にAを返す関数でした。この関数は、lencapの引数に対して、暗黙的なポインタのデリファレンスを考慮して配列型を取得するために使用されていました。
    • 新しいimplicitArrayDerefという名称は、この関数の目的が「配列へのポインタの暗黙的なデリファレンス」に特化していることをより明確に示しています。機能的な変更はありませんが、コードの可読性と意図が向上しました。
  2. containsCallsOrReceives関数の修正とcheckerへのメソッド化:

    • 以前のcontainsCallsOrReceives関数は、与えられたAST式が関数呼び出しやチャネル受信を含むかどうかを判定していました。しかし、この関数は型変換(ast.CallExprとして表現される)と実際の関数呼び出しを区別していませんでした。そのため、len(((*T)(nil)).a)のような式で、(*T)(nil)の部分が型変換と誤認され、定数評価の妨げになっていました。
    • このコミットでは、containsCallsOrReceives関数がchecker構造体のメソッドとなり、check.conversionsマップを参照するようになりました。
    • check.conversionsマップは、型チェッカーが型変換として処理したast.CallExprノードを記録するために新しく導入されました。
    • containsCallsOrReceivesは、ast.CallExprに遭遇した場合、それがcheck.conversionsマップに存在しない(つまり、型変換ではない真の関数呼び出しである)場合にのみfound = trueを設定するように変更されました。これにより、型変換が誤って関数呼び出しと判断されることがなくなりました。
    • また、ast.Inspectのコールバック内でfoundtrueになったらすぐに探索を停止するように最適化されています(return !found)。
  3. checker構造体へのconversionsマップの追加:

    • go/types/check.go内のchecker構造体に、conversions map[*ast.CallExpr]boolという新しいフィールドが追加されました。このマップは、型チェッカーが処理した型変換のast.CallExprノードを追跡するために使用されます。
  4. conversionメソッドでのconversionsマップへの登録:

    • go/types/conversions.go内のconversionメソッド(型変換を処理する部分)で、型変換が成功した場合に、そのast.CallExprノードをcheck.conversionsマップに登録するようになりました。これにより、containsCallsOrReceives関数が型変換と関数呼び出しを正確に区別できるようになります。
  5. テストケースの追加:

    • src/pkg/go/types/testdata/builtins.srcに、Issue 4744を再現する新しいテストケースが追加されました。具体的には、type T struct{ a [10]int }という構造体を定義し、const _ = cap(((*T)(nil)).a)const _ = len(((*T)(nil)).a)という式が定数として正しく評価されることを確認するものです。
    • src/pkg/exp/gotype/gotype_test.goでは、runtime/debugパッケージのテストがコメントアウトされていた部分が解除され、この修正によってコンパイルが通るようになったことが示されています。

これらの変更により、lencapの引数がポインタのデリファレンスを含む配列フィールドであっても、Goの仕様通りに定数として正しく評価されるようになりました。

コアとなるコードの変更箇所

  • src/pkg/exp/gotype/gotype_test.go:
    • "runtime/debug"のコメントアウトが解除されました。これは、この修正によってruntime/debugパッケージが正しくコンパイルされるようになったことを示しています。
  • src/pkg/go/types/builtins.go:
    • builtinメソッド内のlenおよびcapの処理で、implicitDerefimplicitArrayDerefに名称変更されました。
    • containsCallsOrReceivesの呼び出しがcheck.containsCallsOrReceivesとなり、レシーバを持つメソッドとして呼び出されるようになりました。
    • implicitDeref関数がimplicitArrayDerefに名称変更されました。
    • containsCallsOrReceives関数がcheckerのメソッドとなり、型変換を区別するためのロジックが追加されました。
  • src/pkg/go/types/check.go:
    • checker構造体にconversions map[*ast.CallExpr]boolフィールドが追加されました。
    • checkerの初期化時にconversionsマップがmakeで初期化されるようになりました。
  • src/pkg/go/types/conversions.go:
    • conversionメソッド内で、型変換が処理されたast.CallExprノードがcheck.conversionsマップに登録されるようになりました。
  • src/pkg/go/types/testdata/builtins.src:
    • Issue 4744のテストケースとして、cap(((*T)(nil)).a)len(((*T)(nil)).a)が定数として評価されることを確認するコードが追加されました。

コアとなるコードの解説

src/pkg/go/types/builtins.go の変更点

// 旧: switch typ := implicitDeref(underlying(x.typ)).(type) {
// 新: switch typ := implicitArrayDeref(underlying(x.typ)).(type) {
// implicitDerefからimplicitArrayDerefへの名称変更。機能は同じだが、意図が明確に。

// 旧: if !containsCallsOrReceives(arg0) {
// 新: if !check.containsCallsOrReceives(arg0) {
// containsCallsOrReceivesがcheckerのメソッドになったため、レシーバ(check)を付けて呼び出す。

// implicitDeref関数の名称変更
// 旧: func implicitDeref(typ Type) Type {
// 新: func implicitArrayDeref(typ Type) Type {

// containsCallsOrReceives関数の変更
// 旧: func containsCallsOrReceives(x ast.Expr) bool {
// 旧: 	res := false
// 旧: 	ast.Inspect(x, func(x ast.Node) bool {
// 旧: 		switch x := x.(type) {
// 旧: 		case *ast.CallExpr:
// 旧: 			res = true
// 旧: 			return false
// 旧: 		case *ast.UnaryExpr:
// 旧: 			if x.Op == token.ARROW {
// 旧: 				res = true
// 旧: 				return false
// 旧: 			}
// 旧: 		}
// 旧: 		return true
// 旧: 	})
// 旧: 	return res
// 新: func (check *checker) containsCallsOrReceives(x ast.Expr) (found bool) {
// 新: 	ast.Inspect(x, func(x ast.Node) bool {
// 新: 		switch x := x.(type) {
// 新: 		case *ast.CallExpr:
// 新: 			// calls and conversions look the same
// 新: 			if !check.conversions[x] { // ここが重要: 型変換でなければ関数呼び出しと判断
// 新: 				found = true
// 新: 			}
// 新: 		case *ast.UnaryExpr:
// 新: 			if x.Op == token.ARROW {
// 新: 				found = true
// 新: 			}
// 新: 		}
// 新: 		return !found // no need to continue if found
// 新: 	})
// 新: 	return
// 新しいcontainsCallsOrReceivesは、checkerのメソッドとなり、conversionsマップを使って型変換と関数呼び出しを区別します。
// foundがtrueになったら、それ以上ASTを探索する必要がないため、`return !found`で早期終了します。

src/pkg/go/types/check.go の変更点

type checker struct {
	// ... 既存のフィールド ...
	// lazily initialized
	// 旧: pkg       *Package
	// 旧: firsterr  error
	// 旧: idents    map[*ast.Ident]Object
	// 旧: objects   map[*ast.Object]Object
	// 旧: initspecs map[*ast.ValueSpec]*ast.ValueSpec
	// 旧: methods   map[*TypeName]*Scope
	// 旧: funclist  []function
	// 旧: funcsig   *Signature
	// 旧: pos       []token.Pos
	// 新: pkg         *Package
	// 新: firsterr    error
	// 新: idents      map[*ast.Ident]Object
	// 新: objects     map[*ast.Object]Object
	// 新: initspecs   map[*ast.ValueSpec]*ast.ValueSpec
	// 新: methods     map[*TypeName]*Scope
	// 新: conversions map[*ast.CallExpr]bool            // set of type-checked conversions (to distinguish from calls)
	// 新: funclist    []function
	// 新: funcsig     *Signature
	// 新: pos         []token.Pos
}

func check(ctxt *Context, fset *token.FileSet, files []*ast.File) (pkg *Package, err error) {
	// ... 既存の初期化 ...
	check := checker{
		// ... 既存の初期化 ...
		// 新: conversions: make(map[*ast.CallExpr]bool), // 新しく追加されたマップの初期化
	}
	// ...
}

checker構造体にconversionsマップが追加され、型チェッカーの初期化時にこのマップが作成されます。

src/pkg/go/types/conversions.go の変更点

func (check *checker) conversion(x *operand, conv *ast.CallExpr, typ Type, iota int) {
	// ... 既存の処理 ...
	// 新: check.conversions[conv] = true // for cap/len checking
	// 新しく追加: 型変換が処理されたast.CallExprをconversionsマップに登録
	x.expr = conv
	x.typ = typ
	return
}

型変換を処理するconversionメソッド内で、変換に使用されたast.CallExprノードがcheck.conversionsマップに登録されます。これにより、containsCallsOrReceivesがこの情報を利用して、型変換を関数呼び出しと誤認しないようになります。

関連リンク

参考にした情報源リンク