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

[インデックス 11296] ファイルの概要

このコミットは、Go言語のコンパイラ(gc)におけるバグ修正に関するものです。具体的には、マップリテラルにおいて、誤った型のキーが暗黙的に正しい型に変換される場合に、コンパイラがキーの重複チェックを適切に行わない問題を修正しています。これにより、本来であればコンパイルエラーとなるべきコードが誤ってコンパイルされてしまう可能性がありました。

コミット

commit 46e7cb57c951724630a722c55cda684889a7123b
Author: Jeff R. Allen <jra@nella.org>
Date:   Fri Jan 20 13:34:38 2012 -0500

    gc: do not try to add a key with incorrect type to a hash
    
    Fixes #2623.
    
    R=rsc, bradfitz
    CC=golang-dev
    https://golang.org/cl/5533043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/46e7cb57c951724630a722c55cda684889a7123b

元コミット内容

このコミットは、Go言語のIssue 2623で報告されたバグを修正するものです。元の問題は、マップリテラルを初期化する際に、キーの型がマップの宣言されたキーの型と一致しない場合でも、コンパイラが暗黙的な型変換(例えば、intからstringへの変換)を試み、その結果、キーの重複チェックが正しく機能しないというものでした。

具体的には、map[string]intのようなマップに対して、"abc": 1, 1: 2のように、1というint型の値をstring型のキーとして指定した場合、Goコンパイラは通常であれば型不一致のエラーを報告します。しかし、このバグが存在するバージョンでは、1string型に変換可能であると誤って判断され、"1"という文字列として扱われてしまい、その後のキー重複チェックがスキップされるか、あるいは誤ったコンテキストで実行されていました。これにより、本来コンパイル時に検出されるべき型エラーが報告されず、予期せぬ動作を引き起こす可能性がありました。

変更の背景

Go言語のコンパイラは、コードの型安全性を保証するために厳密な型チェックを行います。マップリテラルも例外ではなく、キーと値の型はマップの宣言と一致している必要があります。しかし、Go言語には暗黙的な型変換(例えば、数値リテラルが適切な型に変換されるなど)のルールも存在します。

Issue 2623は、この型変換のロジックとマップリテラルのキー重複チェックのロジックが相互に作用する際に発生する問題でした。コンパイラがマップリテラルのキーを処理する際、まずキーの式を評価し、必要に応じてマップのキー型に変換します。この変換処理(assignconv)が成功すると、その結果のノードに対してkeydup関数が呼び出され、マップリテラル内でキーが重複していないかどうかがチェックされます。

問題は、assignconvが型変換を行った結果、元のノードのオペレーションがOCONV(型変換オペレーション)になる場合、keydupがそのOCONVノードを適切に処理できない、またはOCONVノードの背後にある元の値の型を正しく認識できないことにありました。これにより、型変換が行われたキーに対しては、重複チェックがスキップされるか、誤った比較が行われ、結果として型エラーが報告されないという状況が生じていました。

このバグは、コンパイラが開発者の意図しない型変換を許容し、かつその後の重要なチェックを怠るという、型安全性を損なう深刻な問題であったため、早急な修正が必要とされました。

前提知識の解説

Go言語の型システムと型変換 (Type Conversion)

Go言語は静的型付け言語であり、変数は特定の型を持ちます。異なる型の値を直接代入することは、通常は許されません。しかし、Goには特定の条件下で暗黙的な型変換が行われる場合があります。例えば、数値リテラルは、その値が収まる範囲であれば、intfloat64などの異なる数値型に自動的に変換されることがあります。また、明示的な型変換(例: string(1))も可能です。

このコミットの文脈では、マップリテラルのキーがマップの宣言されたキー型と異なる場合に、コンパイラが暗黙的な変換を試みる挙動が問題となります。

Go言語のマップ (map) の動作とキーの型

Goのマップは、キーと値のペアを格納するハッシュテーブルです。マップのキーは、比較可能(comparable)な型である必要があります(例: 整数、文字列、ポインタ、構造体、配列など)。マップリテラルを初期化する際には、map[KeyType]ValueType{key1: value1, key2: value2, ...}のように記述します。この際、各keyNの型はKeyTypeと互換性がある必要があります。

Goコンパイラのgc (Go Compiler) の役割、特にtypecheck.cが担当する型チェックフェーズ

gcはGo言語の公式コンパイラです。コンパイルプロセスには、字句解析、構文解析、型チェック、最適化、コード生成など、複数のフェーズがあります。 src/cmd/gc/typecheck.cは、コンパイラの型チェックフェーズを担当するC言語のソースファイルです。このファイルには、GoプログラムのAST(抽象構文木)を走査し、各ノード(変数、関数呼び出し、リテラルなど)の型がGo言語の仕様に準拠しているかを確認するロジックが含まれています。型チェックは、プログラムの正しさを保証し、実行時エラーを未然に防ぐ上で非常に重要なステップです。

OCONVオペレーションの意味

OCONVは、Goコンパイラの内部表現(ASTノードのオペレーションコード)の一つで、「型変換(Conversion)」を表します。コンパイラが、ある型の値を別の型に変換する必要があると判断した場合、その変換操作を表すOCONVノードがASTに挿入されます。例えば、int型の1string型に変換するような場合、内部的にはOCONVノードが生成されます。

keydup関数の役割

keydup関数は、マップリテラル内でキーが重複していないかをチェックするために使用されるコンパイラ内部の関数です。Go言語のマップリテラルでは、同じキーを複数回指定することはできません。もし重複するキーが指定された場合、コンパイルエラーとなるべきです。keydupは、ハッシュテーブル(hashnhash)を使用して、既に処理されたキーを追跡し、新しいキーが既存のキーと重複していないかを確認します。

技術的詳細

このバグは、src/cmd/gc/typecheck.c内のtypecheckcomplit関数で発生していました。この関数は、複合リテラル(マップリテラルや構造体リテラルなど)の型チェックを行います。

マップリテラルのキーを処理する際、以下のステップが実行されます。

  1. typecheck(&l->left, Erv);:キーの式自体の型チェックを行います。
  2. defaultlit(&l->left, t->down);:デフォルトリテラル変換を適用します(例: 1int型に変換されるなど)。
  3. l->left = assignconv(l->left, t->down, "map key");:キーの式をマップの宣言されたキー型(t->down)に変換します。この関数は、必要に応じて暗黙的な型変換を行い、変換後のASTノードを返します。もし変換が行われた場合、返されるノードのオペレーションはOCONVとなる可能性があります。
  4. keydup(l->left, hash, nhash);:変換後のキーノード(l->left)を使用して、マップリテラル内でキーが重複していないかをチェックします。

問題は、ステップ3でassignconvによって型変換が行われ、l->leftOCONVノードになった場合、ステップ4のkeydupがそのOCONVノードを正しく扱えず、キーの重複チェックがスキップされてしまうことにありました。例えば、map[string]int{"abc":1, 1:2}というコードでは、1というintリテラルがstring型に変換される際にOCONVノードが生成されます。このOCONVノードがkeydupに渡されると、keydup"1"という文字列としてのキーを正しく認識できず、重複チェックが機能しなかったのです。

修正は、keydupを呼び出す前に、l->leftOCONVノードであるかどうかをチェックする条件を追加することでした。もしl->leftOCONVノードであれば、それは既に型変換が行われたことを意味し、keydupを呼び出す必要がない、あるいはkeydupが正しく処理できないケースであると判断されます。この修正により、OCONVノードがkeydupに渡されることを防ぎ、コンパイラが型不一致のキーを正しくエラーとして報告できるようになりました。

コアとなるコードの変更箇所

src/cmd/gc/typecheck.c

--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -2130,7 +2130,8 @@ typecheckcomplit(Node **np)
 			typecheck(&l->left, Erv);
 			defaultlit(&l->left, t->down);
 			l->left = assignconv(l->left, t->down, "map key");
-			keydup(l->left, hash, nhash);
+			if (l->left->op != OCONV)
+				keydup(l->left, hash, nhash);

 			r = l->right;
 			pushtype(r, t->type);

test/fixedbugs/bug397.go (新規追加)

// errchk $G -e $D/$F.go

// Copyright 2011 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.

package main

// Issue 2623
var m = map[string]int {
	"abc":1,
	1:2, // ERROR "cannot use 1.*as type string in map key"
}

コアとなるコードの解説

src/cmd/gc/typecheck.cの変更

変更はtypecheckcomplit関数内で行われています。この関数は、マップリテラルなどの複合リテラルの要素を処理するループの一部です。

元のコードでは、マップキーの型チェックと変換を行った後、無条件にkeydup関数を呼び出してキーの重複をチェックしていました。

l->left = assignconv(l->left, t->down, "map key");
keydup(l->left, hash, nhash);

修正後のコードでは、keydupの呼び出しに条件が追加されています。

l->left = assignconv(l->left, t->down, "map key");
if (l->left->op != OCONV)
    keydup(l->left, hash, nhash);

このif (l->left->op != OCONV)という条件が重要です。

  • l->leftは、マップリテラルの現在のキーを表すASTノードです。
  • l->left->opは、そのノードのオペレーションコードです。
  • OCONVは、型変換が行われたことを示すオペレーションコードです。

この条件は、「もし現在のキーノードが型変換(OCONV)の結果ではない場合のみ、keydupを呼び出す」という意味になります。

なぜこの条件が必要なのか?

assignconv関数は、キーの型がマップの宣言されたキー型と異なる場合に、そのキーを適切な型に変換しようとします。例えば、map[string]intに対して1:2というエントリがあった場合、assignconv1int型リテラル)をstring型に変換しようとします。この変換が成功すると、l->leftOCONVオペレーションを持つノードに変わります。

元のバグは、OCONVノードがkeydupに渡されたときに、keydupがそのノードを正しく処理できず、結果としてキーの重複チェックが機能しなかったことにありました。OCONVノードは、その内部に変換元の値と変換先の型情報を持つ複雑な構造であるため、keydupが直接そのノードから「キーの値」を抽出してハッシュ計算を行うのが困難だったと考えられます。

この修正により、assignconvによって型変換が行われた(つまりl->left->opOCONVである)キーに対しては、keydupが呼び出されなくなります。これにより、コンパイラは、型変換によって曖昧になったキーの重複チェックを試みる代わりに、より早い段階で型不一致のエラーを報告するようになります。test/fixedbugs/bug397.goの例では、1:2というエントリはmap[string]intに対して型不一致であるため、assignconvの段階でエラーが検出され、keydupが呼び出される前にコンパイルエラーとなることが期待されます。

test/fixedbugs/bug397.goの解説

このファイルは、修正が正しく機能することを確認するための新しいテストケースです。

// errchk $G -e $D/$F.go

この行は、Goコンパイラ($G)を実行し、エラー(-e)をチェックすることを指示しています。期待される動作は、指定されたファイル($D/$F.go)のコンパイルがエラーになることです。

var m = map[string]int {
	"abc":1,
	1:2, // ERROR "cannot use 1.*as type string in map key"
}

このコードスニペットがテストの核心です。

  • map[string]intというマップが宣言されています。キーはstring型、値はint型です。
  • 最初のエントリ"abc":1は正しい型です。
  • 二番目のエントリ1:2が問題の箇所です。ここではint型の1がキーとして指定されていますが、マップのキーはstring型であるべきです。

このテストの目的は、この1:2という行が「cannot use 1.*as type string in map key」というエラーメッセージを伴ってコンパイルエラーになることを確認することです。修正前は、このエラーが適切に報告されず、1が暗黙的に"1"に変換されてしまう可能性がありました。修正後は、assignconvの段階で型不一致が検出され、keydupが呼び出される前にコンパイルエラーが報告されるようになります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (マップ、型変換に関するセクション)
  • Goコンパイラのソースコード (src/cmd/gc/typecheck.cの周辺コード)
  • Go言語のIssueトラッカー (Issue 2623の議論)
  • Go言語のGerritコードレビューシステム (CL 5533043のレビューコメント)
  • Go言語のコンパイラ設計に関する一般的な情報源