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

[インデックス 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.ckeydup関数内の変更について解説します。

  1. Node *a, *orign;:

    • orignという新しいNodeポインタが導入されました。これは、keydup関数に渡された元のキーノードnを保持するために使用されます。
    • nは、OCONVIFACEノードの場合にその内部の具体的な値に「剥がされる」可能性があるため、元のinterface{}変換のコンテキストをorignで保持することが重要になります。
  2. orign = n;:

    • 関数の冒頭で、orignに元のキーノードnが代入されます。
  3. if(n->op == OCONVIFACE) n = n->left;:

    • もし現在のキーノードnOCONVIFACEinterface{}への変換)操作を表す場合、nをその左の子ノード(n->left)、つまりinterface{}に変換される前の具体的な値のノードに置き換えます。
    • これにより、続くevconst(n)(定数評価)が、interface{}に変換される前の実際の値に対して行われるようになります。
  4. if(a->op == OCONVIFACE && orign->op == OCONVIFACE)ブロック:

    • このブロックは、ハッシュテーブル内の既存のキーaと、現在チェックしているキーorignの両方がOCONVIFACEノードである場合に実行されます。これは、両方のキーがinterface{}型として扱われていることを意味します。
    • if(a->left->type == n->type): ここで、aが保持する具体的な型(a->left->type)と、orignが保持する具体的な型(n->type、これはorignOCONVIFACEであればorign->left->typeと同じ)が一致するかどうかをチェックします。
      • Goのinterface{}の等価性ルールでは、動的な型と動的な値の両方が一致する必要があります。このチェックは、動的な型の一致を確認しています。
      • 型が一致する場合、cmp.right = a->left;として、interface{}の内部にある具体的な値を比較対象とします。そしてevconst(&cmp);でその値の等価性を評価し、結果をbに格納します。
    • else { b = 0; }: 型が一致しない場合(例: int(2)int16(2)interface{}として比較される場合)、たとえ値が同じであっても、これらは異なるキーとみなされるため、b0(非重複)に設定します。
  5. else { cmp.right = a; evconst(&cmp); b = cmp.val.u.bval; }:

    • 上記のOCONVIFACEの特別なケースに該当しない場合(つまり、少なくともどちらか一方がOCONVIFACEではない場合)、従来の比較ロジックが適用されます。cmp.right = a;として、直接ノードaを比較対象とします。
  6. orign->ntest = hash[h]; hash[h] = orign;:

    • 重複が検出されなかった場合、ハッシュテーブルにはnではなくorign(元のノード)が登録されるようになりました。これにより、interface{}変換のコンテキストが保持され、後続のキーとの比較で正しく利用されます。

これらの変更により、keydup関数はinterface{}型をキーとするマップリテラルにおいて、Goのマップキーの等価性ルール(動的な型と値の両方の一致)に厳密に従って重複キーを正確に検出できるようになりました。

関連リンク

参考にした情報源リンク