[インデックス 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
パッケージ)において、len
やcap
ビルトイン関数が、ポインタを介してアクセスされる配列のフィールドに対して適用された場合に、その結果が定数となるべきケースで定数として扱われない問題を修正します。具体的には、len(((*T)(nil)).X)
のような式(ここでX
は配列型のフィールド)が定数として評価されるように変更されました。これはGo言語の仕様に準拠するための修正です。
変更の背景
Go言語の仕様では、len(s)
とcap(s)
の式は、s
の型が配列または配列へのポインタであり、かつs
の式がチャネル受信や関数呼び出しを含まない場合、定数として扱われると定められています。
しかし、Goの型チェッカーの実装において、(*T)(nil)
のようなnilポインタのデリファレンスを通じて構造体の配列フィールドにアクセスするケース(例: len(((*T)(nil)).a)
)が、この定数評価の対象から漏れていました。これは、len
やcap
の引数がポインタのデリファレンスを含む場合に、型チェッカーがその引数を「関数呼び出しやチャネル受信を含む」と誤って判断し、結果として定数ではなく実行時に評価される値として扱ってしまっていたためです。
この問題はGo Issue #4744として報告され、runtime/debug
パッケージのコンパイルエラーを引き起こしていました。runtime/debug
パッケージには、このような形式のlen
呼び出しが含まれており、それが型チェッカーによって不正な定数式と判断され、コンパイルが通らないという状況でした。
前提知識の解説
- Go言語の型システム: Go言語は静的型付け言語であり、コンパイル時に厳密な型チェックが行われます。
go/types
パッケージは、Goコンパイラの一部として、プログラムの型チェックを担当します。 len
とcap
ビルトイン関数:len(v)
:v
の長さ(要素数)を返します。配列、スライス、文字列、マップ、チャネルに適用可能です。cap(v)
:v
の容量を返します。配列、スライス、チャネルに適用可能です。- 定数評価: Go言語では、コンパイル時に値が確定する式は「定数」として扱われます。
len
やcap
は、引数が配列または配列へのポインタであり、かつ副作用(関数呼び出しやチャネル受信)がない場合に定数として評価されるという特別なルールがあります。これにより、コンパイル時に最適化が行われ、実行時のオーバーヘッドが削減されます。
- 抽象構文木 (AST): Goのソースコードは、コンパイラによって抽象構文木(AST)にパースされます。ASTはプログラムの構造を木構造で表現したもので、型チェックやコード生成の基盤となります。
- ポインタのデリファレンス: Goにおけるポインタは、メモリ上のアドレスを指します。
*P
のようにポインタをデリファレンスすることで、そのアドレスに格納されている値にアクセスできます。 ast.CallExpr
と型変換: Goでは、型変換(例:int(x)
)もAST上ではast.CallExpr
として表現されます。これは通常の関数呼び出しと同じASTノードであるため、型チェッカーはこれらを区別する必要があります。
技術的詳細
このコミットの主要な変更点は以下の通りです。
-
implicitDeref
からimplicitArrayDeref
への名称変更と役割の明確化:- 以前の
implicitDeref
関数は、型がポインタ*A
であり、かつA
が配列である場合にA
を返す関数でした。この関数は、len
やcap
の引数に対して、暗黙的なポインタのデリファレンスを考慮して配列型を取得するために使用されていました。 - 新しい
implicitArrayDeref
という名称は、この関数の目的が「配列へのポインタの暗黙的なデリファレンス」に特化していることをより明確に示しています。機能的な変更はありませんが、コードの可読性と意図が向上しました。
- 以前の
-
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
のコールバック内でfound
がtrue
になったらすぐに探索を停止するように最適化されています(return !found
)。
- 以前の
-
checker
構造体へのconversions
マップの追加:go/types/check.go
内のchecker
構造体に、conversions map[*ast.CallExpr]bool
という新しいフィールドが追加されました。このマップは、型チェッカーが処理した型変換のast.CallExpr
ノードを追跡するために使用されます。
-
conversion
メソッドでのconversions
マップへの登録:go/types/conversions.go
内のconversion
メソッド(型変換を処理する部分)で、型変換が成功した場合に、そのast.CallExpr
ノードをcheck.conversions
マップに登録するようになりました。これにより、containsCallsOrReceives
関数が型変換と関数呼び出しを正確に区別できるようになります。
-
テストケースの追加:
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
パッケージのテストがコメントアウトされていた部分が解除され、この修正によってコンパイルが通るようになったことが示されています。
これらの変更により、len
やcap
の引数がポインタのデリファレンスを含む配列フィールドであっても、Goの仕様通りに定数として正しく評価されるようになりました。
コアとなるコードの変更箇所
src/pkg/exp/gotype/gotype_test.go
:"runtime/debug"
のコメントアウトが解除されました。これは、この修正によってruntime/debug
パッケージが正しくコンパイルされるようになったことを示しています。
src/pkg/go/types/builtins.go
:builtin
メソッド内のlen
およびcap
の処理で、implicitDeref
がimplicitArrayDeref
に名称変更されました。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)
が定数として評価されることを確認するコードが追加されました。
- Issue 4744のテストケースとして、
コアとなるコードの解説
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
がこの情報を利用して、型変換を関数呼び出しと誤認しないようになります。
関連リンク
- Go Issue #4744: https://github.com/golang/go/issues/4744
- Go Code Review: https://golang.org/cl/7305080
参考にした情報源リンク
- Go言語の公式ドキュメント (len/cap): https://go.dev/ref/spec#Length_and_capacity
- Go言語のASTパッケージ: https://pkg.go.dev/go/ast
- Go言語の型パッケージ: https://pkg.go.dev/go/types
- Go言語のトークンパッケージ: https://pkg.go.dev/go/token
- Go言語のコンパイラに関する一般的な情報源 (例: "Go compiler internals" で検索)
- Go言語のIssueトラッカー (Issue 4744の詳細)
- Go言語のコードレビューシステム (CL 7305080の詳細)