[インデックス 19041] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるバグ修正です。具体的には、interface{}
型をキーとするマップリテラルにおいて、重複するキーが正しく検出されない問題を解決します。これにより、コンパイル時に重複キーのエラーが適切に報告されるようになります。
コミット
commit 3072df5c1d04e44dbe388e966801a3913c53a720
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date: Fri Apr 4 16:46:23 2014 +0200
cmd/gc: check duplicate keys in maps with interface{} key type
Fixes #7214
LGTM=rsc
R=golang-codereviews, bradfitz, rsc
CC=golang-codereviews, minux.ma
https://golang.org/cl/82080044
---
src/cmd/gc/typecheck.c | 27 +++++++++++++++++++++------
test/fixedbugs/issue7214.go | 30 ++++++++++++++++++++++++++++++
2 files changed, 51 insertions(+), 6 deletions(-)
diff --git a/src/cmd/gc/typecheck.c b/src/cmd/gc/typecheck.c
index b6e43b7594..2b44cd8202 100644
--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -2304,10 +2304,13 @@ keydup(Node *n, Node *hash[], ulong nhash)\n ulong b;\n double d;\n int i;\n-\tNode *a;\n+\tNode *a, *orign;\n Node cmp;\n char *s;\n \n+\torign = n;\n+\tif(n->op == OCONVIFACE)\n+\t\tn = n->left;\n \tevconst(n);\n \tif(n->op != OLITERAL)\n \t\treturn;\t// we dont check variables\n@@ -2340,17 +2343,29 @@ keydup(Node *n, Node *hash[], ulong nhash)\n for(a=hash[h]; a!=N; a=a->ntest) {\n cmp.op = OEQ;\n cmp.left = n;\n-\t\tcmp.right = a;\n-\t\tevconst(&cmp);\n-\t\tb = cmp.val.u.bval;\n+\t\tif(a->op == OCONVIFACE && orign->op == OCONVIFACE) {\n+\t\t\tif(a->left->type == n->type) {\n+\t\t\t\tcmp.right = a->left;\n+\t\t\t\tevconst(&cmp);\n+\t\t\t\tb = cmp.val.u.bval;\n+\t\t\t}\n+\t\t\telse {\n+\t\t\t\tb = 0;\n+\t\t\t}\n+\t\t}\n+\t\telse {\n+\t\t\tcmp.right = a;\n+\t\t\tevconst(&cmp);\n+\t\t\tb = cmp.val.u.bval;\n+\t\t}\n \t\tif(b) {\n \t\t\t// too lazy to print the literal\n \t\t\tyyerror(\"duplicate key %N in map literal\", n);\n \t\t\treturn;\n \t\t}\n \t}\n-\tn->ntest = hash[h];\n-\thash[h] = n;\n+\torign->ntest = hash[h];\n+\thash[h] = orign;\n }\n \n static void\ndiff --git a/test/fixedbugs/issue7214.go b/test/fixedbugs/issue7214.go\nnew file mode 100644\nindex 0000000000..82ddf74c31\n--- /dev/null\n+++ b/test/fixedbugs/issue7214.go\n@@ -0,0 +1,30 @@\n+// errorcheck\n+\n+// Copyright 2014 The Go Authors. All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+// Issue 7214: No duplicate key error for maps with interface{} key type\n+\n+package p\n+\n+var _ = map[interface{}]int{2: 1, 2: 1} // ERROR \"duplicate key\"\n+var _ = map[interface{}]int{int(2): 1, int16(2): 1}\n+var _ = map[interface{}]int{int16(2): 1, int16(2): 1} // ERROR \"duplicate key\"\n+\n+type S string\n+\n+var _ = map[interface{}]int{\"a\": 1, \"a\": 1} // ERROR \"duplicate key\"\n+var _ = map[interface{}]int{\"a\": 1, S(\"a\"): 1}\n+var _ = map[interface{}]int{S(\"a\"): 1, S(\"a\"): 1} // ERROR \"duplicate key\"\n+\n+type I interface {\n+\tf()\n+}\n+\n+type N int\n+\n+func (N) f() {}\n+\n+var _ = map[I]int{N(0): 1, N(2): 1}\n+var _ = map[I]int{N(2): 1, N(2): 1} // ERROR \"duplicate key\"\n```
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/3072df5c1d04e44dbe388e966801a3913c53a720](https://github.com/golang/go/commit/3072df5c1d04e44dbe388e966801a3913c53a720)
## 元コミット内容
このコミットは、Goコンパイラ(`cmd/gc`)において、`interface{}`型をキーとするマップリテラル内の重複キーをチェックする機能を追加します。これは、Issue 7214で報告された問題を修正するものです。
## 変更の背景
Go言語のマップ(`map`)は、キーと値のペアを格納するためのデータ構造です。マップリテラルを定義する際、同じキーを複数回指定することは通常エラーとなります。しかし、このコミットが導入される前は、マップのキーの型が`interface{}`である場合に、コンパイラが重複するキーを正しく検出できないというバグが存在しました。
例えば、`map[interface{}]int{2: 1, 2: 1}`のようなマップリテラルは、`interface{}`型がキーであるため、コンパイル時に重複キーのエラーが報告されず、予期せぬ動作を引き起こす可能性がありました。この問題はIssue 7214として報告され、このコミットはその問題を解決するために導入されました。
## 前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語およびGoコンパイラに関する前提知識が必要です。
* **Goの`map`リテラル**: Goでは、`map[KeyType]ValueType{key1: value1, key2: value2, ...}`のように記述することで、マップを宣言し、初期値を設定できます。コンパイル時、コンパイラはマップリテラル内のキーに重複がないかチェックします。
* **`interface{}`型(空インターフェース)**: Goの`interface{}`型は、任意の型の値を保持できる特別なインターフェースです。これは、Goにおけるポリモーフィズムを実現するための強力な機能ですが、その内部では値と型の両方を保持しています。
* **Goコンパイラ (`cmd/gc`)**: `cmd/gc`は、Go言語の公式コンパイラです。Goのソースコードを解析し、抽象構文木(AST)を構築し、型チェック、最適化、コード生成などの様々な段階を経て実行可能なバイナリを生成します。
* **型チェック**: コンパイラの重要な段階の一つで、プログラムがGoの型システム規則に準拠しているかを確認します。
* **定数評価**: コンパイル時に値が確定する式(定数式)を評価し、その結果をコードに埋め込みます。
* **`OCONVIFACE`**: これはGoコンパイラの内部表現(ASTノードの操作コード)の一つで、具体的な型(concrete type)の値が`interface{}`型に変換される操作を表します。例えば、`var i interface{} = 10`のような代入が行われると、内部的には`10`という`int`型の値が`interface{}`型に変換される`OCONVIFACE`ノードが生成されます。
* **マップのキーの等価性**: Goにおいて、マップのキーの等価性は、キーの型と値の両方が一致するかどうかで判断されます。特に`interface{}`型の場合、`interface{}`が保持する**動的な型**と**動的な値**の両方が等しい場合にのみ、キーは等しいとみなされます。例えば、`int(2)`と`int16(2)`は異なる型であるため、`interface{}`として比較された場合、たとえ値が同じ`2`であっても、通常は等しいとはみなされません。
## 技術的詳細
このコミットの主要な変更は、Goコンパイラの`src/cmd/gc/typecheck.c`ファイル内の`keydup`関数にあります。`keydup`関数は、マップリテラル内のキーが重複していないかをチェックする役割を担っています。
以前の`keydup`関数は、`interface{}`型をキーとするマップリテラルにおいて、キーが`interface{}`型に変換される(`OCONVIFACE`ノードとして表現される)ケースを適切に処理できていませんでした。これにより、例えば`map[interface{}]int{2: 1, 2: 1}`のようなコードで、両方の`2`が`interface{}`型に変換された結果、コンパイラがそれらを異なるものとして扱い、重複を検出できないという問題が発生していました。
このコミットでは、以下の点が改善されました。
1. **元のノードの保持**: `keydup`関数内で、現在のキーを表す`Node *n`とは別に、元のノードを保持するための`Node *orign`が導入されました。これは、`n`が`OCONVIFACE`ノードの内部の具体的な値に「剥がされる」可能性があるため、元の`interface{}`変換のコンテキストを失わないようにするためです。
2. **`OCONVIFACE`の特別扱い**: `n`が`OCONVIFACE`ノードである場合、その内部の具体的な値(`n->left`)を`n`として処理するように変更されました。これにより、定数評価(`evconst(n)`)が、`interface{}`に変換される前の具体的な値に対して行われるようになります。
3. **`interface{}`キーの比較ロジックの強化**: 最も重要な変更は、既存のハッシュテーブル内のキー`a`と現在のキー`orign`の両方が`OCONVIFACE`ノードである場合の比較ロジックです。
* この場合、まず`a`が保持する具体的な型(`a->left->type`)と`orign`が保持する具体的な型(`n->type`、これは`orign`が`OCONVIFACE`であれば`orign->left->type`と同じ)が一致するかどうかをチェックします。
* 型が一致する場合にのみ、`cmp.right = a->left`として、`interface{}`の内部にある具体的な値を比較対象とします。これにより、`int(2)`と`int(2)`のように、同じ具体的な型と値を持つ`interface{}`キーが重複として正しく検出されます。
* 型が一致しない場合(例: `int(2)`と`int16(2)`)、たとえ値が同じであっても、Goの`interface{}`の等価性ルールに従い、これらは異なるキーとみなされ、`b = 0`(非重複)と設定されます。
4. **ハッシュテーブルへの登録**: 重複が検出されなかった場合、ハッシュテーブルには`n`ではなく`orign`(元のノード)が登録されるようになりました。これにより、`interface{}`変換のコンテキストが保持され、後続のキーとの比較で正しく利用されます。
これらの変更により、Goコンパイラは`interface{}`型をキーとするマップリテラルにおいても、Goのマップキーの等価性ルールに則って重複キーを正確に検出できるようになりました。
## コアとなるコードの変更箇所
`src/cmd/gc/typecheck.c`の`keydup`関数における変更点です。
```diff
--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -2304,10 +2304,13 @@ keydup(Node *n, Node *hash[], ulong nhash)\n ulong b;\n double d;\n int i;\n-\tNode *a;\n+\tNode *a, *orign;\n Node cmp;\n char *s;\n \n+\torign = n;\n+\tif(n->op == OCONVIFACE)\n+\t\tn = n->left;\n \tevconst(n);\n \tif(n->op != OLITERAL)\n \t\treturn;\t// we dont check variables\n@@ -2340,17 +2343,29 @@ keydup(Node *n, Node *hash[], ulong nhash)\n for(a=hash[h]; a!=N; a=a->ntest) {\n cmp.op = OEQ;\n cmp.left = n;\n-\t\tcmp.right = a;\n-\t\tevconst(&cmp);\n-\t\tb = cmp.val.u.bval;\n+\t\tif(a->op == OCONVIFACE && orign->op == OCONVIFACE) {\n+\t\t\tif(a->left->type == n->type) {\n+\t\t\t\tcmp.right = a->left;\n+\t\t\t\tevconst(&cmp);\n+\t\t\t\tb = cmp.val.u.bval;\n+\t\t\t}\n+\t\t\telse {\n+\t\t\t\tb = 0;\n+\t\t\t}\n+\t\t}\n+\t\telse {\n+\t\t\tcmp.right = a;\n+\t\t\tevconst(&cmp);\n+\t\t\tb = cmp.val.u.bval;\n+\t\t}\n \t\tif(b) {\n \t\t\t// too lazy to print the literal\n \t\t\tyyerror(\"duplicate key %N in map literal\", n);\n \t\t\treturn;\n \t\t}\n \t}\n-\tn->ntest = hash[h];\n-\thash[h] = n;\n+\torign->ntest = hash[h];\n+\thash[h] = orign;\n }\n \n static void
test/fixedbugs/issue7214.go
が新規追加されています。
// errorcheck
// 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.
// Issue 7214: No duplicate key error for maps with interface{} key type
package p
var _ = map[interface{}]int{2: 1, 2: 1} // ERROR "duplicate key"
var _ = map[interface{}]int{int(2): 1, int16(2): 1}
var _ = map[interface{}]int{int16(2): 1, int16(2): 1} // ERROR "duplicate key"
type S string
var _ = map[interface{}]int{"a": 1, "a": 1} // ERROR "duplicate key"
var _ = map[interface{}]int{"a": 1, S("a"): 1}
var _ = map[interface{}]int{S("a"): 1, S("a"): 1} // ERROR "duplicate key"
type I interface {
f()
}
type N int
func (N) f() {}
var _ = map[I]int{N(0): 1, N(2): 1}
var _ = map[I]int{N(2): 1, N(2): 1} // ERROR "duplicate key"
コアとなるコードの解説
src/cmd/gc/typecheck.c
のkeydup
関数内の変更について解説します。
-
Node *a, *orign;
:orign
という新しいNode
ポインタが導入されました。これは、keydup
関数に渡された元のキーノードn
を保持するために使用されます。n
は、OCONVIFACE
ノードの場合にその内部の具体的な値に「剥がされる」可能性があるため、元のinterface{}
変換のコンテキストをorign
で保持することが重要になります。
-
orign = n;
:- 関数の冒頭で、
orign
に元のキーノードn
が代入されます。
- 関数の冒頭で、
-
if(n->op == OCONVIFACE) n = n->left;
:- もし現在のキーノード
n
がOCONVIFACE
(interface{}
への変換)操作を表す場合、n
をその左の子ノード(n->left
)、つまりinterface{}
に変換される前の具体的な値のノードに置き換えます。 - これにより、続く
evconst(n)
(定数評価)が、interface{}
に変換される前の実際の値に対して行われるようになります。
- もし現在のキーノード
-
if(a->op == OCONVIFACE && orign->op == OCONVIFACE)
ブロック:- このブロックは、ハッシュテーブル内の既存のキー
a
と、現在チェックしているキーorign
の両方がOCONVIFACE
ノードである場合に実行されます。これは、両方のキーがinterface{}
型として扱われていることを意味します。 if(a->left->type == n->type)
: ここで、a
が保持する具体的な型(a->left->type
)と、orign
が保持する具体的な型(n->type
、これはorign
がOCONVIFACE
であればorign->left->type
と同じ)が一致するかどうかをチェックします。- Goの
interface{}
の等価性ルールでは、動的な型と動的な値の両方が一致する必要があります。このチェックは、動的な型の一致を確認しています。 - 型が一致する場合、
cmp.right = a->left;
として、interface{}
の内部にある具体的な値を比較対象とします。そしてevconst(&cmp);
でその値の等価性を評価し、結果をb
に格納します。
- Goの
else { b = 0; }
: 型が一致しない場合(例:int(2)
とint16(2)
がinterface{}
として比較される場合)、たとえ値が同じであっても、これらは異なるキーとみなされるため、b
を0
(非重複)に設定します。
- このブロックは、ハッシュテーブル内の既存のキー
-
else { cmp.right = a; evconst(&cmp); b = cmp.val.u.bval; }
:- 上記の
OCONVIFACE
の特別なケースに該当しない場合(つまり、少なくともどちらか一方がOCONVIFACE
ではない場合)、従来の比較ロジックが適用されます。cmp.right = a;
として、直接ノードa
を比較対象とします。
- 上記の
-
orign->ntest = hash[h]; hash[h] = orign;
:- 重複が検出されなかった場合、ハッシュテーブルには
n
ではなくorign
(元のノード)が登録されるようになりました。これにより、interface{}
変換のコンテキストが保持され、後続のキーとの比較で正しく利用されます。
- 重複が検出されなかった場合、ハッシュテーブルには
これらの変更により、keydup
関数はinterface{}
型をキーとするマップリテラルにおいて、Goのマップキーの等価性ルール(動的な型と値の両方の一致)に厳密に従って重複キーを正確に検出できるようになりました。
関連リンク
- GitHub上のコミットページ: https://github.com/golang/go/commit/3072df5c1d04e44dbe388e966801a3913c53a720
- Go Code Review (CL 82080044): https://golang.org/cl/82080044
- このコードレビューページで、Issue 7214に関する詳細な議論とテストケースが確認できます。
参考にした情報源リンク
- Go Code Review (CL 82080044): https://golang.org/cl/82080044
- Go言語仕様 (Maps): https://go.dev/ref/spec#Map_types
- Go言語仕様 (Interface types): https://go.dev/ref/spec#Interface_types
- Go言語仕様 (Comparison operators): https://go.dev/ref/spec#Comparison_operators
- 特に、インターフェース値の比較に関するセクションが関連します。