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

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

このコミットは、Goコンパイラ(cmd/gc)における型の等価性判定ロジックのバグ修正に関するものです。具体的には、構造体のフィールドが1つしかない場合の等価性判定、およびパディングを持つフィールドが型の比較可能性に与える影響に関する誤りを修正しています。これにより、本来比較可能ではない型が誤って比較可能と判断されたり、その逆のケースが発生する問題を解決しました。

コミット

commit 428ea6865c7eff6d8632faa18335c64d4ae9f422
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Tue Jul 2 09:08:43 2013 +0200

    cmd/gc: fix computation of equality class of types.
    
    A struct with a single field was considered as equivalent to the
    field type, which is incorrect is the field is blank.
    
    Fields with padding could make the compiler think some
    types are comparable when they are not.
    
    Fixes #5698.
    
    R=rsc, golang-dev, daniel.morsing, bradfitz, gri, r
    CC=golang-dev
    https://golang.org/cl/10271046

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

https://github.com/golang/go/commit/428ea6865c7eff6d8632faa18335c64d4ae9f422

元コミット内容

cmd/gc: fix computation of equality class of types.

A struct with a single field was considered as equivalent to the
field type, which is incorrect is the field is blank.

Fields with padding could make the compiler think some
types are comparable when they are not.

Fixes #5698.

R=rsc, golang-dev, daniel.morsing, bradfitz, gri, r
CC=golang-dev
https://golang.org/cl/10271046

変更の背景

このコミットは、Goコンパイラが型の比較可能性(equality class)を誤って計算していたバグを修正するために導入されました。具体的には、以下の2つの主要な問題に対処しています。

  1. 単一フィールド構造体の誤った等価性判定: 構造体が単一のフィールドを持つ場合、コンパイラはその構造体全体をその単一フィールドの型と等価であると誤って判断していました。この問題は、特にその単一フィールドがブランク識別子(_)である場合に顕著でした。ブランクフィールドは値を持たないため、それを含む構造体がそのフィールドの型と等価であると見なされるのは不適切です。Go言語では、ブランクフィールドはメモリレイアウトには影響しますが、値としてはアクセスできず、比較の対象にもなりません。
  2. パディングを持つフィールドによる比較可能性の誤認: 構造体内のフィールドがアライメントのためにパディング(詰め物)を持つ場合、コンパイラが一部の型を比較可能であると誤って判断することがありました。Go言語において、構造体の比較可能性は、そのすべてのフィールドが比較可能である場合にのみ成立します。パディングは実際のデータではないため、比較の対象外ですが、コンパイラのロジックがこれを適切に扱えていなかったため、比較不可能なフィールド(例えばスライスやマップなど)が含まれていても、パディングの存在によって比較可能と誤認される可能性がありました。

これらの問題は、Goプログラムの型安全性と正確な動作に直接影響を与え、特にマップのキーとして使用される型や、==演算子による比較において予期せぬコンパイルエラーやランタイムエラーを引き起こす可能性がありました。この修正は、Go Issue 5698として報告されたバグを解決するものです。

前提知識の解説

このコミットの理解には、以下のGo言語およびコンパイラの概念に関する知識が役立ちます。

  • Go言語の型システム: Goは静的型付け言語であり、すべての変数には型があります。型は、その変数が保持できる値の種類と、その値に対して実行できる操作を定義します。
  • 型の比較可能性 (Comparability): Go言語では、すべての型が比較可能であるわけではありません。
    • 比較可能な型: 数値型、文字列型、ブール型、ポインタ型、チャネル型、インターフェース型(動的な型と値が両方とも比較可能な場合)、配列型(要素の型が比較可能な場合)、構造体型(すべてのフィールドが比較可能な場合)は比較可能です。
    • 比較不可能な型: スライス、マップ、関数は比較不可能です。これらの型は、==演算子で直接比較することはできません(ただし、スライスはnilとの比較、マップはnilとの比較、関数はnilとの比較は可能です)。
  • 構造体 (Structs): 構造体は、異なる型のフィールドをまとめた複合データ型です。構造体の比較可能性は、そのすべてのフィールドが比較可能であるかどうかに依存します。
  • ブランク識別子 (_): Go言語では、ブランク識別子 _ は、値が使用されないことを明示するために使用されます。例えば、関数の戻り値の一部を無視したり、インポートされたパッケージの副作用のみを利用したりする場合に用いられます。構造体のフィールド名として _ を使用することも可能で、これはそのフィールドがメモリレイアウトの一部を占めるが、プログラムからはアクセスできないことを意味します。ブランクフィールドは比較の対象にはなりません。
  • 構造体のパディング (Struct Padding): コンピュータのメモリは、効率的なアクセスやアライメントの要件のために、特定のバイト境界にデータを配置することがあります。構造体のフィールドがメモリに配置される際、プロセッサの効率的なアクセスを保証するために、フィールド間に未使用のバイト(パディング)が挿入されることがあります。このパディングは、構造体のサイズを増加させますが、実際のデータとしては扱われません。
  • cmd/gc: Go言語の公式コンパイラの一部であり、Goソースコードをコンパイルして実行可能バイナリを生成する役割を担います。型の解析、最適化、コード生成など、コンパイルプロセスの中心的な部分を担っています。
  • algtype1 関数: src/cmd/gc/subr.c に存在するこの関数は、Goコンパイラ内部で型の「アルゴリズムタイプ」(equality class)を決定するために使用されます。これは、その型がどのように比較されるべきか(例えば、メモリ比較、特殊な比較、比較不可能など)を示す分類です。この関数は、型の比較可能性を判断する上で非常に重要な役割を果たします。

技術的詳細

このコミットの核心は、src/cmd/gc/subr.c ファイル内の algtype1 関数の修正にあります。この関数は、与えられたGoの型 t の比較可能性(equality class)を決定します。戻り値は、AMEM(メモリ比較可能)、ANOEQ(比較不可能)、または -1(特殊な比較が必要)などの値を取ります。

修正前の algtype1 関数には、TSTRUCT(構造体型)を処理する際に以下の2つの論理的な欠陥がありました。

  1. 単一フィールド構造体の誤った扱い:

    case TSTRUCT:
    	if(t->type != T && t->type->down == T) {
    		// One-field struct is same as that one field alone.
    		return algtype1(t->type->type, bad);
    	}
    

    このコードは、構造体 t が単一のフィールドを持つ場合(t->type != T && t->type->down == T)、その構造体全体の比較可能性を、その単一フィールドの型(t->type->type)の比較可能性と同一視していました。これは、そのフィールドがブランク識別子 _ でない限りは問題ないように見えます。しかし、もしそのフィールドがブランク識別子であった場合、そのフィールドは値を持たず、比較の対象外であるにもかかわらず、そのフィールドの型(例えば int)の比較可能性が構造体全体の比較可能性として返されてしまうという問題がありました。これにより、ブランクフィールドを含む構造体が誤って比較可能と判断される可能性がありました。

  2. パディングフィールドとブランクフィールドの不適切な無視:

    		for(t1=t->type; t1!=T; t1=t1->down) {
    			// Blank fields and padding must be ignored,
    			// so need special compare.
    			if(isblanksym(t1->sym) || ispaddedfield(t1, t->width)) {
    				ret = -1;
    				continue;
    			}
    			a = algtype1(t1->type, bad);
    			if(a == ANOEQ)
    				return ANOEQ;  // not comparable
    			if(a != AMEM)
    				ret = -1;  // needs special compare
    		}
    

    このループは構造体の各フィールドを走査し、その比較可能性を判断します。問題は、isblanksym(t1->sym)(ブランクフィールド)または ispaddedfield(t1, t->width)(パディングフィールド)の場合に、ret = -1 を設定して continue していた点です。これは「特殊な比較が必要」というフラグを立てるだけで、そのフィールド自体の比較可能性(algtype1(t1->type, bad))を評価していませんでした。 Goの構造体が比較可能であるためには、すべてのフィールドが比較可能である必要があります。もしブランクフィールドやパディングフィールドの後に、実際には比較不可能なフィールド(例: スライス)が存在した場合でも、このロジックではその比較不可能性が適切に伝播されず、構造体全体が誤って比較可能と判断される可能性がありました。例えば、struct { _ int; S []int } のような構造体があった場合、_ int がスキップされ、S []int の比較不可能性が適切にチェックされない可能性がありました。

これらの欠陥により、Goコンパイラは、本来比較できないはずの構造体(例えば、スライスを含む構造体や、ブランクフィールドのみの構造体)を比較可能と誤認し、==演算子を使用した場合にコンパイルエラーではなくランタイムパニックを引き起こす可能性がありました。

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

src/cmd/gc/subr.calgtype1 関数内の TSTRUCT ケースが変更されています。

--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -615,23 +615,23 @@ algtype1(Type *t, Type **bad)
 		return -1;  // needs special compare
 
 	case TSTRUCT:
-		if(t->type != T && t->type->down == T) {
+		if(t->type != T && t->type->down == T && !isblanksym(t->type->sym)) {
 			// One-field struct is same as that one field alone.
 			return algtype1(t->type->type, bad);
 		}
 		ret = AMEM;
 		for(t1=t->type; t1!=T; t1=t1->down) {
-			// Blank fields and padding must be ignored,
-			// so need special compare.
-			if(isblanksym(t1->sym) || ispaddedfield(t1, t->width)) {
+			// All fields must be comparable.
+			a = algtype1(t1->type, bad);
+			if(a == ANOEQ)
+				return ANOEQ;
+
+			// Blank fields, padded fields, fields with non-memory
+			// equality need special compare.
+			if(a != AMEM || isblanksym(t1->sym) || ispaddedfield(t1, t->width)) {
 				ret = -1;
 				continue;
 			}
-			a = algtype1(t1->type, bad);
-			if(a == ANOEQ)
-				return ANOEQ;  // not comparable
-			if(a != AMEM)
-				ret = -1;  // needs special compare
 		}
 		return ret;
 	}

また、この修正を検証するためのテストケースが追加・変更されています。

  • test/blank.go
  • test/blank1.go
  • test/cmp.go
  • test/cmp6.go
  • test/fixedbugs/issue5698.go (新規追加)

コアとなるコードの解説

修正された algtype1 関数の TSTRUCT ケースの変更点を詳しく見ていきます。

  1. 単一フィールド構造体の修正:

    -		if(t->type != T && t->type->down == T) {
    +		if(t->type != T && t->type->down == T && !isblanksym(t->type->sym)) {
    			// One-field struct is same as that one field alone.
    			return algtype1(t->type->type, bad);
    		}
    

    変更前は、単一フィールドの構造体の場合、無条件にそのフィールドの型に等価性判定を委ねていました。修正後は、!isblanksym(t->type->sym) という条件が追加されました。これは、「その単一フィールドがブランク識別子 _ でない場合にのみ、そのフィールドの型に等価性判定を委ねる」という意味になります。もしフィールドがブランク識別子であれば、この if ブロックはスキップされ、通常のフィールド走査ロジックに進むことで、ブランクフィールドを含む構造体が適切に処理されるようになります。これにより、struct { _ int } のような型が、int と同じ比較可能性を持つと誤認されることがなくなります。

  2. フィールド走査ロジックの修正:

    		for(t1=t->type; t1!=T; t1=t1->down) {
    -			// Blank fields and padding must be ignored,
    -			// so need special compare.
    -			if(isblanksym(t1->sym) || ispaddedfield(t1, t->width)) {
    +			// All fields must be comparable.
    +			a = algtype1(t1->type, bad);
    +			if(a == ANOEQ)
    +				return ANOEQ;
    +
    +			// Blank fields, padded fields, fields with non-memory
    +			// equality need special compare.
    +			if(a != AMEM || isblanksym(t1->sym) || ispaddedfield(t1, t->width)) {
     				ret = -1;
     				continue;
     			}
    -			a = algtype1(t1->type, bad);
    -			if(a == ANOEQ)
    -				return ANOEQ;  // not comparable
    -			if(a != AMEM)
    -				ret = -1;  // needs special compare
     		}
    

    この変更は、構造体の各フィールドの比較可能性をより厳密にチェックするように改善しています。

    • // All fields must be comparable.: まず、各フィールド t1 の型 t1->type の比較可能性を algtype1(t1->type, bad) で取得し、変数 a に格納します。
    • if(a == ANOEQ) return ANOEQ;: ここが重要な変更点です。もし現在のフィールドの型が ANOEQ(比較不可能)であると判明した場合、その時点で構造体全体も比較不可能であると判断し、直ちに ANOEQ を返します。これにより、構造体内に一つでも比較不可能なフィールドがあれば、その構造体全体が比較不可能であるというGoのルールが厳密に適用されるようになりました。以前のコードでは、ブランクフィールドやパディングフィールドが先に検出されると、その後の比較不可能なフィールドのチェックがスキップされる可能性がありました。
    • if(a != AMEM || isblanksym(t1->sym) || ispaddedfield(t1, t->width)) { ret = -1; continue; }: この条件は、フィールドがメモリ比較可能でない場合(a != AMEM)、またはブランクフィールドである場合(isblanksym(t1->sym))、またはパディングフィールドである場合(ispaddedfield(t1, t->width))に、構造体全体の比較が「特殊な比較が必要」(ret = -1)であるとマークし、次のフィールドの処理に進みます。このロジックは、比較不可能なフィールドが検出された後にのみ実行されるため、比較可能性の判断がより正確になります。

これらの修正により、Goコンパイラは構造体の比較可能性をより正確に判断できるようになり、Issue 5698で報告されたような、スライスを含む構造体が誤ってマップのキーとして使用可能と判断されるなどのバグが解消されました。

関連リンク

参考にした情報源リンク