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

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

このコミットは、Go言語のパーサーがレシーバ型における括弧の利用を許可するように変更するものです。具体的には、レシーバ型が括弧で囲まれた複雑な型表現を含む場合に、パーサーがそれを正しく解釈できるように修正が加えられました。これにより、Go言語のメソッド定義におけるレシーバ型の柔軟性が向上し、より多様な型表現が許容されるようになります。

コミット

  • コミットハッシュ: ef639b0936161bfb2a024acc05ec7beffcb08d56
  • Author: Robert Griesemer gri@golang.org
  • Date: Thu Jun 26 09:45:11 2014 -0700

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

https://github.com/golang/go/commit/ef639b0936161bfb2a024acc05ec7beffcb08d56

元コミット内容

go/parser: permit parentheses in receiver types

Pending acceptance of CL 101500044
and adjustment of test/fixedbugs/bug299.go.

LGTM=adonovan
R=golang-codereviews, adonovan
CC=golang-codereviews
https://golang.org/cl/110160043

変更の背景

Go言語のパーサーは、メソッドのレシーバ型を解析する際に、特定の形式(例: *identifier または identifier)のみを期待していました。しかし、Go言語の型システムでは、括弧を使用して型表現をグループ化したり、複雑な型を定義したりすることが可能です(例: *(T) はポインタ型 *T と同じ意味を持つが、括弧で囲まれている)。

このコミット以前は、パーサーがレシーバ型に現れるこれらの括弧を適切に処理できず、構文エラーとして扱ってしまう可能性がありました。これは、Go言語の表現力を制限し、開発者がより柔軟な型定義をレシーバとして使用することを妨げていました。

この変更の背景には、Go言語のパーサーが、より複雑で、しかしGoの型システムとしては有効なレシーバ型表現を正しく認識し、抽象構文木(AST)を構築できるようにするという目的があります。コミットメッセージにある「Pending acceptance of CL 101500044」は、このパーサーの変更に関連する、より広範な言語設計の議論や、他の変更リスト(CL)の承認を指している可能性があります。これは、単なるバグ修正ではなく、Go言語の進化に伴うパーサーの堅牢性向上の一環と見なせます。

前提知識の解説

Go言語のメソッドとレシーバ

Go言語では、関数にレシーバを付与することで、その関数を「メソッド」として特定の型に関連付けることができます。レシーバは、メソッドが操作する値またはポインタを指定します。

例:

type MyType struct {
    Value int
}

// 値レシーバのメソッド
func (m MyType) GetValue() int {
    return m.Value
}

// ポインタレシーバのメソッド
func (m *MyType) SetValue(newValue int) {
    m.Value = newValue
}

レシーバは (変数名 型) の形式で定義され、この括弧はGo言語の構文上必須です。

Go言語の型表現と括弧

Go言語では、型を定義する際に括弧を使用することがあります。例えば、ポインタ型 *T は、*(T) と書くこともできます。これは、特に複雑な型(例: 関数型やインターフェース型)を定義する際に、可読性を高めたり、曖昧さを解消したりするために使用されます。

Go言語のパーサーとAST

Go言語のコンパイラツールチェーンにおいて、パーサー(go/parserパッケージ)は非常に重要な役割を担っています。パーサーは、Goのソースコードを読み込み、その構文構造を解析して、抽象構文木(AST: Abstract Syntax Tree)と呼ばれるツリー構造のデータ表現に変換します。このASTは、その後のコンパイルプロセス(型チェック、コード生成など)の入力となります。

パーサーは、Go言語の文法規則に従ってソースコードを解析し、各構文要素(変数宣言、関数定義、式など)をASTノードとして表現します。もしソースコードが文法規則に違反している場合、パーサーはエラーを報告します。

go/ast パッケージの Unparen 関数

go/ast パッケージには、ASTノードを操作するためのユーティリティ関数が多数含まれています。その一つに Unparen(e ast.Expr) ast.Expr があります。この関数は、与えられた式ノード e が括弧で囲まれている場合、その括弧を取り除いた内側の式ノードを返します。これは、パーサーが構文上の括弧を無視して、その内側の実際の式を評価するために使用されます。例えば、((x + y)) のような式があった場合、Unparen を繰り返し適用することで x + y という本質的な式を取り出すことができます。

技術的詳細

このコミットの技術的な核心は、go/parser パッケージ内の parseReceiver 関数が、レシーバ型を解析する際に、括弧で囲まれた型表現を適切に処理できるように変更された点にあります。

変更前は、parseReceiver 関数はレシーバ型を ["*"] identifier の形式、つまりオプションのポインタ記号の後に識別子が続く形式であると厳密に解釈していました。これは、*TT のような単純なレシーバ型には対応できますが、(*T) のように括弧で囲まれた型表現には対応できませんでした。

変更後のコードでは、parseReceiver 関数内でレシーバ型を処理する際に、unparen 関数が複数回適用されています。具体的には、以下の行が変更されました。

-	base := deref(recv.Type)
+	base := unparen(deref(unparen(recv.Type)))

この変更により、レシーバ型 recv.Type が以下のように処理されます。

  1. unparen(recv.Type): まず、レシーバ型全体を囲む可能性のある外側の括弧を取り除きます。例えば、レシーバ型が ((T)) のような場合、最初の unparen(T) になります。
  2. deref(...): 次に、ポインタ型の場合にそのポインタをデリファレンスします。これは、レシーバが *T(*T) のようなポインタ型である場合に、その基底型(T)を取得するために行われます。deref 関数は、ポインタ型 *Expr から Expr を取り出す役割を担っていると推測されます。
  3. unparen(...): 最後に、デリファレンスされた型がさらに括弧で囲まれている可能性を考慮し、もう一度 unparen を適用します。例えば、*(T) のような型が deref された後に (T) となった場合、この最後の unparenT を取り出します。

この多段階の unparenderef の適用により、パーサーは (T), (*T), *(T) のような、括弧を含むより複雑なレシーバ型表現を正しく解析し、その基底となる識別子(T)を抽出できるようになりました。これにより、パーサーはGo言語の型システムの柔軟性に対応し、より堅牢になりました。

また、short_test.go に追加されたテストケース func ((T),) m() {}, func ((*T),) m() {}, func (*(T),) m() {} は、この変更が意図した通りに、括弧で囲まれたレシーバ型が正しく解析されることを検証しています。これらのテストケースは、Go言語の通常のレシーバ構文とは異なる、より複雑な(あるいはエッジケース的な)括弧の利用パターンを示しており、パーサーがこれらのケースを適切に処理できるようになったことを示唆しています。

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

src/pkg/go/parser/parser.goparseReceiver 関数内の変更:

--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -2310,9 +2310,9 @@ func (p *parser) parseReceiver(scope *ast.Scope) *ast.FieldList {
 		return par
 	}
 
-	// recv type must be of the form ["*"] identifier
+	// recv type must be of the form ["*"] identifier, possibly using parentheses
 	recv := par.List[0]
-	base := deref(recv.Type)
+	base := unparen(deref(unparen(recv.Type)))
 	if _, isIdent := base.(*ast.Ident); !isIdent {
 		if _, isBad := base.(*ast.BadExpr); !isBad {
 			// only report error if it's a new one

src/pkg/go/parser/short_test.go へのテストケースの追加:

--- a/src/pkg/go/parser/short_test.go
+++ b/src/pkg/go/parser/short_test.go
@@ -35,6 +35,9 @@ var valids = []string{
 	`package p; func f() { for _ = range "foo" + "bar" {} };`,\n \t`package p; func f() { var s []int; g(s[:], s[i:], s[:j], s[i:j], s[i:j:k], s[:j:k]) };`,\n \t`package p; var ( _ = (struct {*T}).m; _ = (interface {T}).m )`,\n+\t`package p; func ((T),) m() {}`,\n+\t`package p; func ((*T),) m() {}`,\n+\t`package p; func (*(T),) m() {}`,\n }\n \n func TestValid(t *testing.T) {

コアとなるコードの解説

変更された行 base := unparen(deref(unparen(recv.Type))) は、レシーバ型 recv.Type からその基底となる型(通常は識別子)を抽出するための処理です。

  1. unparen(recv.Type):

    • これは、レシーバ型 recv.Type の最も外側の括弧を取り除く最初のステップです。
    • 例えば、recv.Type((T)) であれば、この呼び出しの結果は (T) になります。
    • もし recv.Type*TT のように括弧で囲まれていなければ、そのままの型が返されます。
  2. deref(...):

    • unparen の結果がポインタ型(例: *T(T)*T に変換された後)である場合、この関数はそのポインタをデリファレンスし、基底となる型(例: T)を返します。
    • GoのASTにおいて、ポインタ型は ast.StarExpr として表現されることが多いため、derefast.StarExpr からその X フィールド(ポインタが指す型)を取り出す処理を行うと考えられます。
  3. unparen(...):

    • deref の結果がさらに括弧で囲まれている可能性を考慮し、再度 unparen を適用します。
    • 例えば、元のレシーバ型が *(T) であった場合、最初の unparen は何もせず *(T) のまま、deref(T) になり、最後の unparenT が抽出されます。

この三段階の処理により、パーサーはレシーバ型がどのような形式(単純な識別子、ポインタ、括弧で囲まれた型、またはそれらの組み合わせ)であっても、最終的にそのレシーバの「名前」となる識別子(ast.Ident)を正確に特定できるようになります。これにより、if _, isIdent := base.(*ast.Ident); !isIdent のチェックが、より広範な有効なレシーバ型に対して機能するようになります。

関連リンク

参考にした情報源リンク