[インデックス 14156] ファイルの概要
このコミットは、Go言語の実験的な型チェッカー (exp/types/staging
) において、配列、スライス、文字列のインデックス操作とスライス操作に関する型チェックの改善、およびブランク識別子 (_
) への代入のハンドリングを追加するものです。
コミット
commit 71588bc2bceddb95795bc5a306c835e5d8f58fdc
Author: Robert Griesemer <gri@golang.org>
Date: Tue Oct 16 10:20:03 2012 -0700
exp/types/staging: index and slice type checks
Also: handle assignments to the blank identifier.
R=rsc
CC=golang-dev
https://golang.org/cl/6658050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/71588bc2bceddb95795bc5a306c835e5d8f58fdc
元コミット内容
exp/types/staging: index and slice type checks
Also: handle assignments to the blank identifier.
R=rsc
CC=golang-dev
https://golang.org/cl/6658050
変更の背景
このコミットは、Go言語の型システムとコンパイラの進化の初期段階におけるものです。exp/types/staging
パッケージは、Go言語の公式な型チェッカーが go/types
パッケージとして標準ライブラリに統合される前の実験的な開発ステージでした。当時のGo言語のコンパイラやツールチェーンはまだ成熟しておらず、型チェックの厳密性や正確性を向上させるための継続的な作業が行われていました。
特に、配列、スライス、文字列に対するインデックス操作(a[i]
)やスライス操作(s[low:high]
)は、Go言語の基本的なデータ構造操作であり、これらの操作が型安全かつ正確に行われることを保証することは非常に重要です。以前の実装では、これらの操作に対する型チェックが不十分であったり、エッジケース(例えば、負のインデックス、範囲外のインデックス、逆転したスライス範囲など)が適切に扱われていなかった可能性があります。
また、Go言語のブランク識別子 (_
) は、値を破棄するために使用される特殊な識別子です。これへの代入は、通常の変数への代入とは異なるセマンティクスを持つため、型チェッカーがこれを正しく認識し、適切な処理を行う必要がありました。このコミットは、これらの領域における型チェックの堅牢性を高めることを目的としています。
前提知識の解説
Go言語の型システム
Go言語は静的型付け言語であり、コンパイル時に厳密な型チェックが行われます。これにより、多くのプログラミングエラーを早期に発見し、実行時の安全性を高めます。
- 基本型 (Basic Types):
int
,float64
,string
,bool
など。 - 複合型 (Composite Types):
- 配列 (Array): 固定長で同じ型の要素のシーケンス。
[N]T
の形式で宣言され、N
は配列の長さ、T
は要素の型です。 - スライス (Slice): 可変長で同じ型の要素のシーケンス。配列の上に構築され、動的にサイズを変更できます。
[]T
の形式で宣言されます。 - 文字列 (String): 不変のバイトシーケンス。UTF-8エンコードされたテキストを表すことが一般的です。
- ポインタ (Pointer): 変数のメモリアドレスを保持する型。
- 構造体 (Struct): 異なる型のフィールドをまとめた複合型。
- インターフェース (Interface): メソッドのシグネチャの集合を定義する型。
- 配列 (Array): 固定長で同じ型の要素のシーケンス。
インデックス操作とスライス操作
- インデックス操作 (Indexing):
a[i]
の形式で、配列、スライス、または文字列の特定の要素にアクセスします。i
はインデックスであり、整数型である必要があります。文字列の場合、結果はバイト値になります。 - スライス操作 (Slicing):
s[low:high]
の形式で、配列、スライス、または文字列の一部を新しいスライスとして抽出します。low
は開始インデックス(含む)、high
は終了インデックス(含まない)です。low
とhigh
は省略可能で、それぞれ0と元の長さがデフォルト値となります。
ブランク識別子 (_
)
Go言語のブランク識別子 (_
) は、特定の値を意図的に破棄するために使用されます。例えば、関数の複数の戻り値のうち一部だけが必要な場合や、インポートしたパッケージを直接使用しないが副作用のためにインポートする必要がある場合などに使われます。ブランク識別子に代入された値は使用されず、コンパイラはそれに関するエラーを報告しません。
exp/types/staging
パッケージ
これは、Go言語の型チェッカーのプロトタイプまたは実験的なバージョンでした。最終的には、この機能は go/types
パッケージとしてGo標準ライブラリに統合され、Goコンパイラや他のツール(例えば gofmt
や go vet
)によって利用されるようになりました。このパッケージの目的は、Go言語のセマンティクスを正確にモデル化し、コンパイル時に型関連のエラーを検出することでした。
技術的詳細
このコミットは、主に src/pkg/exp/types/staging/const.go
、src/pkg/exp/types/staging/expr.go
、src/pkg/exp/types/staging/stmt.go
の3つのファイルに影響を与えています。
-
const.go
の変更:isNegConst
という新しいヘルパー関数が追加されました。この関数は、与えられた定数値が負であるかどうかを判定します。これは、インデックスが負でないことをチェックするために使用されます。int64
,*big.Int
,*big.Rat
の各型に対応しています。
-
expr.go
の変更:index
関数が大幅に改修されました。以前はoperand
型の引数を受け取っていましたが、新しいバージョンではast.Expr
型のindex
とint64
型のlength
(インデックスの上限) を受け取るようになりました。- インデックスが整数型でない場合のチェック (
!x.isInteger()
) が追加されました。 - インデックスが定数である場合 (
x.mode == constant
)、その値が負でないこと (isNegConst(x.val)
) をチェックするようになりました。 - インデックスが
int64
に収まらない場合のハンドリングが追加されました。 length
が指定されている場合(配列の長さなど)、インデックスが範囲外 (i >= length
) でないことをチェックするようになりました。
- インデックスが整数型でない場合のチェック (
exprOrType
関数内の*ast.IndexExpr
(インデックス式) の処理が強化されました。- 文字列 (
*Basic
でisString(typ)
) のインデックス操作が明示的にサポートされ、結果の型がByte
になるように設定されました。文字列が定数の場合、その長さがlength
として考慮されます。 - 配列 (
*Array
) のインデックス操作が改善され、length
が配列の長さとして設定されるようになりました。 - スライス (
*Slice
) のインデックス操作も同様に改善されました。 - インデックス操作が許可されていない型 (
!valid
) に対してエラーを報告するようになりました。 - インデックス式にインデックスが欠けている場合 (
e.Index == nil
) のASTエラーチェックが追加されました。 - 最終的に、新しい
check.index
関数を呼び出してインデックスの妥当性を検証します。
- 文字列 (
*ast.SliceExpr
(スライス式) の処理が大幅に改善されました。- 文字列、配列、スライスに対するスライス操作が明示的にサポートされました。
- 文字列のスライス操作では、結果の型が元の文字列と同じ型になるように設定されました。文字列が定数の場合、その長さが
length
として考慮されます(スライス操作のため+1
されます)。 - 配列のスライス操作では、スライスされる配列がアドレス可能 (
x.mode != variable
) でない場合にエラーを報告し、結果の型が対応するスライス型 (&Slice{Elt: typ.Elt}
) になるように設定されました。 - スライス操作が許可されていない型に対してエラーを報告するようになりました。
- スライス範囲の
low
とhigh
インデックスのチェックに、新しいcheck.index
関数が使用されるようになりました。 high
インデックスがlow
インデックスよりも小さい場合 (lo > hi
)、つまりスライス範囲が逆転している場合にエラーを報告するようになりました。
-
stmt.go
の変更:assign1to1
関数において、ブランク識別子 (_
) への代入のハンドリングが追加されました。lhs
(左辺) がブランク識別子である場合 (ident != nil && ident.Name == "_"
)、右辺 (rhs
) の式のみをチェックし、型の一致は不要とすることで、ブランク識別子への代入が常に許可されるようになりました。
-
テストデータの変更:
testdata/decls1.src
とtestdata/expr3.src
のテストファイルが更新され、新しい型チェックルールに対応するエラーメッセージの期待値が修正されたり、インデックス/スライス操作に関する新しいテストケースが追加されたりしています。特にexpr3.src
には、負のインデックス、範囲外のインデックス、逆転したスライス範囲など、様々なエッジケースのテストが含まれています。
これらの変更により、Go言語の型チェッカーは、インデックスおよびスライス操作のセマンティクスをより正確に強制し、コンパイル時に不正な操作を検出できるようになりました。
コアとなるコードの変更箇所
src/pkg/exp/types/staging/const.go
// isNegConst reports whether the value of constant x is < 0.
// x must be a non-complex numeric value.
//
func isNegConst(x interface{}) bool {
switch x := x.(type) {
case int64:
return x < 0
case *big.Int:
return x.Sign() < 0
case *big.Rat:
return x.Sign() < 0
}
unreachable()
return false
}
src/pkg/exp/types/staging/expr.go
// index checks an index expression for validity. If length >= 0, it is the upper
// bound for the index. The result is a valid constant index >= 0, or a negative
// value.
//
func (check *checker) index(index ast.Expr, length int64, iota int) int64 {
var x operand
var i int64 // index value, valid if >= 0
check.expr(&x, index, nil, iota)
if !x.isInteger() {
check.errorf(x.pos(), "index %s must be integer", &x)
return -1
}
if x.mode != constant {
return -1 // we cannot check more
}
// x.mode == constant and the index value must be >= 0
if isNegConst(x.val) {
check.errorf(x.pos(), "index %s must not be negative", &x)
return -1
}
var ok bool
if i, ok = x.val.(int64); !ok {
// index value doesn't fit into an int64
i = length // trigger out of bounds check below if we know length (>= 0)
}
if length >= 0 && i >= length {
check.errorf(x.pos(), "index %s is out of bounds (>= %d)", &x, length)
return -1
}
return i
}
// ... (exprOrType function内の変更) ...
case *ast.IndexExpr:
check.expr(x, e.X, hint, iota)
valid := false
length := int64(-1) // valid if >= 0
switch typ := underlying(x.typ).(type) {
case *Basic:
if isString(typ) {
valid = true
if x.mode == constant {
length = int64(len(x.val.(string)))
}
// an indexed string always yields a byte value
// (not a constant) even if the string and the
// index are constant
x.mode = value
x.typ = Typ[Byte]
}
case *Array:
valid = true
length = typ.Len
if x.mode != variable {
x.mode = value
}
x.typ = typ.Elt
case *Slice:
valid = true
x.mode = variable
x.typ = typ.Elt
case *Pointer:
// TODO(gri) check index type
x.mode = variable
x.typ = typ.Elt
return
}
if !valid {
check.invalidOp(x.pos(), "cannot index %s", x)
goto Error
}
if e.Index == nil {
check.invalidAST(e.Pos(), "missing index expression for %s", x)
return
}
check.index(e.Index, length, iota)
// ok to continue
case *ast.SliceExpr:
check.expr(x, e.X, hint, iota)
valid := false
length := int64(-1) // valid if >= 0
switch typ := underlying(x.typ).(type) {
case *Basic:
if isString(typ) {
valid = true
if x.mode == constant {
length = int64(len(x.val.(string))) + 1 // +1 for slice
}
// a sliced string always yields a string value
// of the same type as the original string (not
// a constant) even if the string and the indexes
// are constant
x.mode = value
// x.typ doesn't change
}
case *Array:
valid = true
length = typ.Len + 1 // +1 for slice
if x.mode != variable {
check.invalidOp(x.pos(), "cannot slice %s (value not addressable)", x)
goto Error
}
x.typ = &Slice{Elt: typ.Elt}
case *Slice:
valid = true
x.mode = variable
// x.typ doesn't change
}
if !valid {
check.invalidOp(x.pos(), "cannot slice %s", x)
goto Error
}
var lo int64
if e.Low != nil {
lo = check.index(e.Low, length, iota)
}
var hi int64 = length
if e.High != nil {
hi = check.index(e.High, length, iota)
}
if hi >= 0 && lo > hi {
check.errorf(e.Low.Pos(), "inverted slice range: %d > %d", lo, hi)
// ok to continue
}
src/pkg/exp/types/staging/stmt.go
func (check *checker) assign1to1(lhs, rhs ast.Expr, decl bool, iota int) {
ident, _ := lhs.(*ast.Ident)
if ident != nil && ident.Name == "_" {
// anything can be assigned to a blank identifier - check rhs only
var x operand
check.expr(&x, rhs, nil, iota)
return
}
// ... (既存のコード) ...
}
コアとなるコードの解説
const.go
の isNegConst
関数
この関数は、定数式が負の値を持つかどうかを効率的にチェックするために導入されました。Go言語では、配列やスライスのインデックスは非負の整数でなければならないため、コンパイル時にこの制約を強制するために使用されます。int64
、*big.Int
、*big.Rat
といったGoの数値型をサポートしており、それぞれの型に応じた負値の判定ロジックが実装されています。
expr.go
の index
関数
この関数は、インデックス操作の妥当性を検証する中心的なロジックをカプセル化しています。
- 型チェック: まず、インデックス式が整数型であるかを
x.isInteger()
で確認します。そうでなければエラーを報告します。 - 定数チェック: インデックスが定数でない場合 (
x.mode != constant
) は、コンパイル時にそれ以上のチェックができないため、処理を終了します。 - 負値チェック: インデックスが定数であり、かつ負の値である場合 (
isNegConst(x.val)
) はエラーを報告します。 int64
への適合性: インデックス値がint64
に収まらない場合、範囲外チェックをトリガーするためにlength
の値がi
に代入されます。- 範囲外チェック:
length
(配列や文字列の長さなど) が指定されている場合、インデックスi
がlength
以上であるかを確認し、範囲外であればエラーを報告します。
この関数は、インデックス操作の様々なエラーケースを早期に検出することで、実行時エラーを防ぎ、より堅牢なコードを保証します。
expr.go
の exprOrType
関数内の *ast.IndexExpr
および *ast.SliceExpr
処理
exprOrType
関数は、GoのAST (Abstract Syntax Tree) を走査し、各式の型を決定する主要な場所です。
-
*ast.IndexExpr
(インデックス式):- インデックスされる対象 (
e.X
) の型 (x.typ
) に基づいて、インデックス操作が有効かどうかを判断します。 - 文字列: 文字列のインデックス操作は有効であり、結果の型は
byte
になります。文字列が定数の場合、その長さがインデックスの範囲チェックに使用されます。 - 配列: 配列のインデックス操作は有効であり、結果の型は配列の要素型になります。配列の長さがインデックスの範囲チェックに使用されます。
- スライス: スライスのインデックス操作は有効であり、結果の型はスライスの要素型になります。
- 上記以外の型に対してインデックス操作が行われた場合、
check.invalidOp
でエラーが報告されます。 - インデックス式にインデックス自体が欠けている場合 (
e.Index == nil
) は、ASTの構造が不正であるとしてcheck.invalidAST
でエラーが報告されます。 - 最終的に、新しい
check.index
関数を呼び出して、インデックスの具体的な値と範囲の妥当性を検証します。
- インデックスされる対象 (
-
*ast.SliceExpr
(スライス式):- スライスされる対象 (
e.X
) の型 (x.typ
) に基づいて、スライス操作が有効かどうかを判断します。 - 文字列: 文字列のスライス操作は有効であり、結果の型は元の文字列と同じ
string
型になります。文字列が定数の場合、その長さがスライスの範囲チェックに使用されます。 - 配列: 配列のスライス操作は有効であり、結果の型は対応するスライス型 (
[]T
) になります。配列がアドレス可能でない場合(例: 定数配列を直接スライスしようとする場合)はエラーが報告されます。配列の長さがスライスの範囲チェックに使用されます。 - スライス: スライスのスライス操作は有効であり、結果の型は元のスライスと同じ型になります。
- 上記以外の型に対してスライス操作が行われた場合、
check.invalidOp
でエラーが報告されます。 - スライス範囲の
low
とhigh
インデックスは、それぞれcheck.index
関数を使って妥当性が検証されます。 high
インデックスがlow
インデックスよりも小さい場合(例:s[5:2]
のように範囲が逆転している場合)は、check.errorf
でエラーが報告されます。
- スライスされる対象 (
これらの変更により、Goの型チェッカーは、インデックスおよびスライス操作に関するGo言語の仕様をより厳密に適用し、コンパイル時に多くの潜在的な実行時エラーを捕捉できるようになりました。
stmt.go
の assign1to1
関数内のブランク識別子への代入ハンドリング
assign1to1
関数は、単一の左辺に単一の右辺を代入する際の型チェックを行います。
このコミットでは、左辺がブランク識別子 (_
) である場合の特殊なケースが追加されました。
ident != nil && ident.Name == "_"
の条件で、左辺がブランク識別子であるかを判定します。- ブランク識別子への代入の場合、右辺の式 (
rhs
) の型チェックのみを行い、その結果を破棄します。これにより、ブランク識別子には任意の型の値を代入できるというGo言語のセマンティクスが正しく実装されます。通常の変数への代入のように、左辺と右辺の型の一致を強制する必要がなくなります。
この変更は、Go言語のブランク識別子の振る舞いを型チェッカーが正確に反映するために不可欠です。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Go言語の仕様: https://golang.org/ref/spec
go/types
パッケージのドキュメント (現在の型チェッカー): https://pkg.go.dev/go/types
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/
- Go言語のIssue Tracker: https://github.com/golang/go/issues
- Go言語のメーリングリスト (golang-dev): https://groups.google.com/g/golang-dev
- Go言語のブログ (特に型システムやコンパイラに関する記事): https://go.dev/blog/
- Go言語のASTパッケージ: https://pkg.go.dev/go/ast
- Go言語のトークンパッケージ: https://pkg.go.dev/go/token
- Go言語の
math/big
パッケージ: https://pkg.go.dev/math/big