[インデックス 19364] ファイルの概要
本コミットは、Goコンパイラ(cmd/gc
)におけるマップリテラルの重複キーチェックのバグ修正に関するものです。具体的には、interface{}
型のマップキーにおいてnil
とtrue
のような異なる型の定数を比較しようとした際に発生していた誤った重複キー検出とコンパイルエラーを修正します。
コミット
commit ec38c6f5e324bd550a5d034d36d48f479a8b9b47
Author: Russ Cox <rsc@golang.org>
Date: Thu May 15 15:34:37 2014 -0400
cmd/gc: fix duplicate map key check
Do not compare nil and true.
Fixes #7996.
LGTM=r
R=golang-codereviews, r
CC=golang-codereviews
https://golang.org/cl/91470043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ec38c6f5e324bd550a5d034d36d48f479a8b9b47
元コミット内容
このコミットは、Goコンパイラ(cmd/gc
)がマップリテラル内の重複キーをチェックする際のロジックを修正します。特に、nil
とtrue
のような異なる型の定数を誤って比較しようとする問題を解決します。これにより、Fixes #7996
で報告されたバグが修正されます。
変更の背景
Go言語では、マップリテラルを定義する際に、同じキーを複数回指定することはできません。コンパイラは、この重複キーを検出してコンパイルエラーを発生させる必要があります。しかし、特定の条件下、特にマップのキー型がinterface{}
である場合に、コンパイラがnil
とtrue
のような本質的に異なる型の定数を、あたかも比較可能であるかのように扱おうとするバグが存在しました。
このバグは、issue #7996
として報告されました。報告された問題は、以下のようなコードがコンパイルエラーになるというものでした。
package p
var m = map[interface{}]struct{}{
nil: {},
true: {},
}
このコードは、nil
とtrue
が異なる型であるため、本来であれば重複キーとは見なされず、正しくコンパイルされるべきです。しかし、当時のコンパイラは「illegal constant expression: bool == interface {}
」のようなエラーを出力し、コンパイルに失敗していました。これは、コンパイラが重複キーチェックの過程で、bool
型のtrue
とinterface{}
型のnil
を直接比較しようとしたために発生したものです。
このコミットは、このような誤った比較を防ぎ、コンパイラがマップリテラルの重複キーを正しく識別できるようにすることを目的としています。
前提知識の解説
本コミットの理解には、以下のGoコンパイラの内部構造とGo言語の概念に関する知識が役立ちます。
- Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラです。ソースコードを解析し、中間表現を生成し、最終的に実行可能なバイナリを生成します。型チェックはコンパイルプロセスの重要なフェーズの一つです。 - マップリテラル: Go言語でマップ(ハッシュマップ、連想配列)を初期化するための構文です。
map[KeyType]ValueType{key1: value1, key2: value2, ...}
のように記述します。 - 重複キーチェック: マップリテラル内で同じキーが複数回出現していないかをコンパイラが検証するプロセスです。
interface{}
型: Go言語における空のインターフェース型です。任意の型の値を保持できるため、Goにおけるポリモーフィズムの基本的なメカニズムとして機能します。nil
はインターフェース型のゼロ値であり、true
はbool
型の値です。interface{}
型の変数には、nil
やtrue
のような異なる型の値を代入できます。- 定数式評価 (
evconst
): コンパイル時に評価可能な定数式を評価するコンパイラの機能です。例えば、1 + 2
のような式はコンパイル時に3
として評価されます。 - 型比較 (
eqtype
): コンパイラが2つの型が等しいかどうかを判断するために使用する関数です。Goの型システムは厳格であり、型が異なる値は通常、直接比較できません。 OCONVIFACE
: コンパイラの内部ノードタイプの一つで、値をインターフェース型に変換する操作を表します。Node
構造体: コンパイラの抽象構文木(AST)における各要素を表す内部構造体です。n->type
はノードの型情報を、n->op
はノードの操作タイプ(例:OCONVIFACE
)を表します。cmp.op = OEQ
: 比較操作が等価性(==
)であることを示します。cmp.val.u.bval
: 定数式評価の結果がブーリアン値である場合に、その値(true
またはfalse
)を保持するフィールドです。
技術的詳細
この修正は、src/cmd/gc/typecheck.c
ファイル内のkeydup
関数に焦点を当てています。keydup
関数は、マップリテラル内のキーが重複しているかどうかをチェックする役割を担っています。
問題の核心は、interface{}
型のキーを持つマップにおいて、コンパイラが異なる型の定数(例: nil
とtrue
)を比較しようとした際に、その比較が常にfalse
になるべきであるにもかかわらず、誤って「重複」と判断したり、無効な比較を試みたりしていた点にあります。
修正前のコードでは、OCONVIFACE
(インターフェースへの変換)ノードを処理する際に、a->left->type == n->type
という直接的な型比較を行っていました。これは、インターフェース型に変換された後の基底型が完全に一致する場合にのみtrue
を返します。しかし、interface{}
型の場合、nil
は特定の基底型を持たず、true
はbool
型を持つため、この比較は常にfalse
となり、結果としてelse
ブロックにフォールバックしていました。
else
ブロックでは、cmp.right = a; evconst(&cmp); b = cmp.val.u.bval;
という処理が行われていました。ここで、evconst
は定数式を評価しようとしますが、nil
とtrue
のような互換性のない型を比較しようとすると、コンパイル時に「illegal constant expression
」エラーが発生していました。これは、Goの型システムでは異なる型の値を直接比較できないためです。
このコミットによる修正は、このロジックを改善し、より堅牢な型比較を行うことで、誤った重複キー検出を防ぎます。
コアとなるコードの変更箇所
src/cmd/gc/typecheck.c
ファイルのkeydup
関数において、以下の変更が行われました。
--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -2415,23 +2415,19 @@ keydup(Node *n, Node *hash[], ulong nhash)
for(a=hash[h]; a!=N; a=a->ntest) {
cmp.op = OEQ;
cmp.left = n;
+ b = 0; // <-- 追加
if(a->op == OCONVIFACE && orign->op == OCONVIFACE) {
- if(a->left->type == n->type) { // <-- 変更前
+ if(eqtype(a->left->type, n->type)) { // <-- 変更後
cmp.right = a->left;
evconst(&cmp);
b = cmp.val.u.bval;
}
- else { // <-- 削除
- b = 0; // <-- 削除
- } // <-- 削除
- }
- else { // <-- 変更前
+ } else if(eqtype(a->type, n->type)) { // <-- 変更後
cmp.right = a;
evconst(&cmp);
b = cmp.val.u.bval;
}
if(b) {
- // too lazy to print the literal // <-- 削除
yyerror("duplicate key %N in map literal", n);
return;
}
また、この修正を検証するための新しいテストケースが追加されました。
test/fixedbugs/issue7996.go
// compile
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// /tmp/x.go:5: illegal constant expression: bool == interface {}
package p
var m = map[interface{}]struct{}{
nil: {},
true: {},
}
コアとなるコードの解説
修正のポイントは以下の通りです。
-
b = 0;
の初期化:for
ループの冒頭でb
(比較結果のブーリアン値)が0
(false)に初期化されるようになりました。これにより、以降の条件分岐でb
が明示的にtrue
に設定されない限り、重複キーとは見なされないようになります。これは、以前のコードでelse
ブロックにフォールバックした際にb
が未定義または意図しない値を持つ可能性があった問題を解消します。 -
OCONVIFACE
ブロック内の型比較の改善: 変更前:if(a->left->type == n->type)
変更後:if(eqtype(a->left->type, n->type))
==
演算子による直接的な型ポインタの比較から、eqtype
関数によるより厳密な型比較に変更されました。eqtype
は、Goの型システムにおける型の等価性を正確に判断するための関数です。interface{}
型の場合、その基底型が異なる場合でも、eqtype
は適切にfalse
を返すため、nil
とtrue
のような異なる型の値が誤って同じ型として扱われることを防ぎます。 -
else if
への変更とロジックの整理: 変更前はOCONVIFACE
のif
ブロックの後に独立したelse
ブロックがありました。 変更後:} else if(eqtype(a->type, n->type)) {
これにより、OCONVIFACE
の条件が満たされない場合でも、a
とn
の直接の型がeqtype
によって比較されるようになりました。この構造により、コンパイラはまずインターフェース変換のケースを考慮し、次に一般的な型比較を行うという、より論理的なフローで重複キーをチェックできるようになります。
これらの変更により、コンパイラはnil
とtrue
のような異なる型の定数をinterface{}
型のマップキーとして使用した場合に、それらが本質的に異なる型であることを正しく認識し、無効な比較を試みることなく、重複キーではないと判断できるようになりました。結果として、issue #7996
で報告されたコンパイルエラーが解消され、Go言語のマップリテラルの型チェックがより堅牢になりました。
関連リンク
- Go言語のマップに関する公式ドキュメント: https://go.dev/blog/maps
- Go言語のインターフェースに関する公式ドキュメント: https://go.dev/tour/methods/10
- Go言語のコンパイラソースコード (
cmd/gc
): https://github.com/golang/go/tree/master/src/cmd/compile
参考にした情報源リンク
- Go言語のIssueトラッカー (旧):
code.google.com/p/go
(現在はgo.dev/issue
に移行していますが、古いIssueはアーカイブされている可能性があります。) - Go言語のコードレビューシステム (旧):
codereview.appspot.com
(現在はgo.googlesource.com/go
とgo.dev/cl
に移行しています。) - Go言語のマップキーの比較可能性に関する情報:
- Go言語の
nil
に関する情報:- https://go.dev/blog/nil
- https://your-favorite-go-blog.com/nil-in-go (架空のURL、一般的な情報源の例)
- Go言語のコンパイラ開発に関する一般的な情報源 (例: Go internals blogs, mailing lists)