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

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

このコミットは、Goコンパイラ(cmd/gc)における、関数スコープ内で定義された型が持つメソッドの名前解決に関するバグを修正するものです。具体的には、異なる関数スコープ内で同じ名前の型が定義され、それらが埋め込みによってメソッドを持つ場合に、メソッド名が衝突する問題に対処しています。この修正により、コンパイラは関数スコープ内の型に属するメソッド名を一意に生成するようになり、予期せぬ名前衝突を防ぎます。

コミット

commit 280c8b90e2785a7de2216cb129752bbeca09210a
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date:   Thu Aug 29 16:48:44 2013 +0200

    cmd/gc: make method names for function scoped types unique
    
    Types in function scope can have methods on them if they embed another type, but we didn't make the name unique, meaning that 2 identically named types in different functions would conflict with eachother.
    
    Fixes #6269.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/13326045

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

https://github.com/golang/go/commit/280c8b90e2785a7de2216cb129752bbeca09210a

元コミット内容

このコミットは、Goコンパイラ(cmd/gc)が、関数スコープ内で定義された型(function-scoped types)のメソッド名を生成する際の不具合を修正します。Go言語では、関数内で構造体などの型を定義し、その型が別の型を埋め込むことでメソッドを持つことができます。しかし、この修正以前は、異なる関数内で同じ名前の型が定義された場合、コンパイラが生成する内部的なメソッド名が一意でなかったため、名前衝突が発生し、コンパイルエラーや予期せぬ動作を引き起こす可能性がありました。このコミットは、このような名前衝突を回避するために、関数スコープ内の型に属するメソッド名に一意性を保証する識別子を追加する変更を導入しています。

変更の背景

Go言語では、関数内でローカルな型を定義することが可能です。例えば、以下のようなコードが考えられます。

package main

import "fmt"

type commonInterface interface {
	Method() string
}

func funcA() {
	type LocalType struct {
		// Some embedded type that provides Method()
		commonInterface
	}
	// ... use LocalType
}

func funcB() {
	type LocalType struct {
		// Another embedded type that provides Method()
		commonInterface
	}
	// ... use LocalType
}

この例では、funcAfuncBの両方でLocalTypeという名前の型が定義されています。Goの型システムでは、構造体がインターフェースを埋め込むことで、そのインターフェースのメソッドを「昇格(promoted)」させ、自身のメソッドとして持つことができます。

問題は、コンパイラがこれらの関数スコープ内の型に対して内部的な名前を生成する際に発生しました。以前のコンパイラは、異なる関数スコープに属するLocalTypeであっても、そのメソッド名(例: LocalType.Method)を区別するための十分な情報を内部名に含めていませんでした。これにより、コンパイラがリンケージのためにシンボルを生成する際に、異なるLocalTypeMethodメソッドが同じ内部名を持ってしまい、名前衝突(name collision)が発生していました。

この衝突は、特にreflectパッケージを使用したり、コンパイラが内部的に型情報を処理する際に問題となり、Fixes #6269で報告されたようなコンパイルエラーや、より深刻なランタイムエラーにつながる可能性がありました。このコミットは、この根本的な名前衝突の問題を解決することを目的としています。

前提知識の解説

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

  1. Goの型システム:

    • 構造体(Structs): 複数のフィールドをまとめた複合型。
    • インターフェース(Interfaces): メソッドのシグネチャの集合を定義する型。
    • 型の埋め込み(Embedding): 構造体内に匿名フィールドとして別の型を埋め込むことで、埋め込まれた型のフィールドやメソッドを外側の型が直接利用できるようにするGoのユニークな機能。これにより、コードの再利用性が高まります。埋め込まれた型のメソッドは、外側の型のメソッドセットに「昇格」します。
    • メソッドセット(Method Sets): 特定の型が持つメソッドの集合。インターフェース型は、そのメソッドセットが別のインターフェースのメソッドセットのスーパーセットである場合、そのインターフェースを実装していると見なされます。
  2. 関数スコープ内の型(Function-scoped Types): Goでは、関数内部で新しい型を定義することができます。これらの型は、その関数内でのみ可視であり、外部からはアクセスできません。これは、特定の関数内でしか必要とされない一時的なデータ構造を定義する際に便利です。

  3. Goコンパイラ(cmd/gc)の内部動作:

    • シンボル(Symbols): コンパイラは、プログラム内の変数、関数、型、メソッドなど、すべての名前付きエンティティを内部的に「シンボル」として表現します。各シンボルには、そのエンティティを一意に識別するための内部名が割り当てられます。
    • リンケージ(Linkage): コンパイルされたコードが最終的に実行可能ファイルになる過程で、異なるオブジェクトファイルやライブラリ間でシンボルが解決され、結合されるプロセス。シンボル名の一意性は、このリンケージプロセスにおいて非常に重要です。
    • 内部名生成: コンパイラは、Goソースコードのエンティティ(例: main.MyFuncMyType.MyMethod)を、リンカが理解できるような内部的なシンボル名に変換します。この内部名は、パッケージパス、型名、メソッド名などを組み合わせたものになることが多いです。

この問題は、関数スコープ内の型が埋め込みによってメソッドを持つ場合に、コンパイラが生成する内部的なメソッド名が、異なる関数スコープ間で一意性を保てなかったことに起因します。

技術的詳細

このコミットの技術的詳細の中心は、Goコンパイラ(cmd/gc)が型やメソッドの内部名を生成するロジック、特にfmt.cファイル内のtypefmt関数にあります。

Goコンパイラは、プログラム内の各型やシンボルに対して、内部的に一意な識別子を割り当てます。これは、コンパイルの様々な段階(型チェック、コード生成、リンケージなど)でこれらのエンティティを参照するために不可欠です。特に、メソッドは特定の型に関連付けられているため、そのメソッドの内部名には、関連する型の情報が含まれる必要があります。

問題となっていたのは、関数スコープ内で定義された型の場合です。例えば、func A内で定義されたtype T struct{}と、func B内で定義されたtype T struct{}は、ソースコード上は同じTという名前ですが、セマンティクス的には全く異なる型です。しかし、コンパイラがこれらの型に属するメソッドの内部名を生成する際に、関数スコープの情報を十分に考慮していなかったため、同じメソッド名(例: T.Method)を持つ異なる型が、結果的に同じ内部シンボル名を持ってしまうことがありました。

この修正では、Type構造体のvargenフィールドが利用されています。vargenは「variable generation」の略で、コンパイラが内部的に型や変数を生成する際に割り当てる一意の識別子(世代番号やバージョン番号のようなもの)です。この値は、特に匿名型や関数スコープ内の型など、ソースコード上では明示的なパスを持たない型に対して、その一意性を保証するために使用されます。

fmt.ctypefmt関数は、型のフォーマット(文字列化)を行うための関数であり、コンパイラが内部的な型名を生成する際に使用されます。この関数は、FmtShortフラグが設定されている場合に、型のシンボル名(t->sym)を短縮形式で出力します。

修正前は、FmtShortフラグが設定されている場合、単にt->sym(型のシンボル)を出力していました。しかし、修正後は、t->vargenが設定されている(つまり、関数スコープ内の型など、一意性が必要な型である)場合に、%hS·%dというフォーマット文字列を使用して、シンボル名に加えてvargenの値を付加するようになりました。

%hSはシンボル名を短縮形式で出力するためのフォーマット指定子であり、%dvargenの整数値を出力します。·(中点)は、Goの内部的なシンボル名で、パッケージパスと型名、メソッド名を区切るために使われる特殊な文字です。例えば、main.MyType.MyMethodのように使われます。ここでは、型名とvargenを区切るために使われています。

この変更により、異なる関数スコープで定義された同じ名前の型であっても、それぞれに異なるvargen値が割り当てられるため、それらの型に属するメソッドの内部名も一意になります。例えば、funcA内のLocalTypeのメソッドはLocalType·1.Methodのような内部名になり、funcB内のLocalTypeのメソッドはLocalType·2.Methodのような内部名になることで、名前衝突が回避されます。

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

変更はsrc/cmd/gc/fmt.cファイルに集中しています。

--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -604,8 +604,11 @@ typefmt(Fmt *fp, Type *t)
  	if(!(fp->flags&FmtLong) && t->sym && t->etype != TFIELD && t != types[t->etype]) {
  		switch(fmtmode) {
  		case FTypeId:
- 			if(fp->flags&FmtShort)
- 				return fmtprint(fp, "%hS", t->sym);
+ 			if(fp->flags&FmtShort) {
+ 				if(t->vargen)
+ 					return fmtprint(fp, "%hS·%d", t->sym, t->vargen);
+ 				return fmtprint(fp, "%hS", t->sym);
+ 			}
  			if(fp->flags&FmtUnsigned)
  				return fmtprint(fp, "%uS", t->sym);
  			// fallthrough

また、この修正の動作を検証するためのテストケースがtest/fixedbugs/issue6269.goとして追加されています。

// run

// Copyright 2013 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 6269: name collision on method names for function local types.

package main

type foo struct{}

func (foo) Error() string {
	return "ok"
}

type bar struct{}

func (bar) Error() string {
	return "fail"
}

func unused() {
	type collision struct {
		bar
	}
	_ = collision{}
}

func main() {
	type collision struct {
		foo
	}
	s := error(collision{})
	if str := s.Error(); str != "ok" {
		println("s.Error() ==", str)
		panic(`s.Error() != "ok"`)
	}
}

このテストケースは、main関数内でcollisionという名前の型を定義し、その中にfoo型を埋め込んでいます。また、unused関数内でも同じくcollisionという名前の型を定義し、bar型を埋め込んでいます。修正前は、これらのcollision型が持つError()メソッドの内部名が衝突し、コンパイルエラーや予期せぬ動作を引き起こす可能性がありました。修正後は、main関数内のcollision型が正しくfooError()メソッドを呼び出し、"ok"を返すことを確認しています。

コアとなるコードの解説

src/cmd/gc/fmt.cの変更は、typefmt関数内のFTypeIdケース、特にFmtShortフラグが設定されている部分にあります。

  • if(fp->flags&FmtShort): これは、コンパイラが型の内部名を短縮形式で生成しようとしていることを示します。これは、リンカシンボル名など、コンパクトで一意な識別子が必要な場合に発生します。
  • if(t->vargen): ここが変更の核心です。tは現在処理中のType構造体へのポインタです。t->vargenは、その型が「可変生成(variable generation)」されたものであるか、つまり、コンパイラによって動的に生成され、一意性を保証する必要がある型であるかを示すフラグまたは識別子です。関数スコープ内の型は、このvargenが設定される典型的な例です。
  • return fmtprint(fp, "%hS·%d", t->sym, t->vargen);: t->vargenがゼロでない(つまり、一意性が必要な型である)場合、コンパイラは型のシンボル名(t->sym)に加えて、vargenの値を·で区切って付加した内部名を生成します。例えば、LocalTypeというシンボル名を持つ型でvargen1であれば、内部名はLocalType·1のようになります。
  • return fmtprint(fp, "%hS", t->sym);: t->vargenがゼロの場合(通常のトップレベルの型など)、以前と同様にシンボル名のみを内部名として使用します。

この変更により、Goコンパイラは、関数スコープ内で定義された同じ名前の型であっても、それぞれに異なるvargen値が割り当てられるため、それらの型に属するメソッドの内部名も一意に生成されるようになります。これにより、リンケージ時の名前衝突が回避され、Goプログラムの堅牢性が向上しました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goコンパイラのソースコード(src/cmd/gc/ディレクトリ)