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

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

このコミットは、Go言語のパーサー(usr/gri/pretty/parser.go)に対する変更であり、主に型定義における括弧の使用と、インターフェース内での埋め込みインターフェースの許容に関するものです。これにより、Go言語の文法がより柔軟になり、表現力が向上しました。

コミット

commit 4137f02bb7ffa637fb0f34275e61f80bb95e49da
Author: Robert Griesemer <gri@golang.org>
Date:   Thu Feb 19 16:47:58 2009 -0800

    - permit ()'s in types (TODO: update spec)
    - accept embedded interfaces in interfaces
    - missing: output incorrect, but at least all source code is accepted again
    
    R=r
    OCL=25223
    CL=25223

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

https://github.com/golang/go/commit/4137f02bb7ffa637fb0f34275e61f80bb95e49da

元コミット内容

  • 型定義における括弧 () の使用を許可する(TODO: 仕様を更新する必要がある)
  • インターフェース内での埋め込みインターフェースを受け入れる
  • 不足している点: 出力はまだ正しくないが、少なくともすべてのソースコードが再び受け入れられるようになった

変更の背景

Go言語は開発初期段階にあり、言語仕様とパーサーの実装が並行して進化していました。このコミットが行われた2009年2月は、Go言語が一般に公開される前の時期であり、言語の基本的な構文やセマンティクスが活発に議論され、変更されていました。

このコミットの背景には、以下の2つの主要な課題があったと考えられます。

  1. 型定義の柔軟性向上: 複雑な型を表現する際に、括弧を使ってグループ化する機能は、他の多くのプログラミング言語で一般的です。Go言語においても、より複雑な型表現を可能にし、コードの可読性を向上させるために、型定義における括弧のサポートが必要とされていました。例えば、関数型やポインタ型が組み合わさる場合に、括弧がないと解釈が曖昧になる可能性があります。
  2. インターフェースの表現力強化: Goのインターフェースは、型の振る舞いを定義するための強力なメカニズムです。初期の設計段階で、インターフェースが他のインターフェースを「埋め込む」ことで、より大きなインターフェースを構成できる機能が検討されていたと考えられます。これにより、既存のインターフェースを再利用し、よりモジュール化されたインターフェース設計が可能になります。これは、オブジェクト指向プログラミングにおける継承に似た概念ですが、Goのインターフェースは構造的な型付けに基づいているため、より柔軟な形で振る舞いを組み合わせることができます。

コミットメッセージの「missing: output incorrect, but at least all source code is accepted again」という記述は、当時のパーサーがまだ完全に安定しておらず、一部の有効なGoコードを正しく解析できていなかったことを示唆しています。このコミットは、パーサーの堅牢性を高め、より広範なGoコードを受け入れられるようにするための修正の一環であったと推測されます。

前提知識の解説

Go言語のパーサー

Go言語のコンパイラは、ソースコードを解析して実行可能なバイナリに変換する過程で、字句解析(Lexical Analysis)、構文解析(Syntactic Analysis)、意味解析(Semantic Analysis)などの段階を経ます。このコミットが修正しているのは、主に構文解析を担当する「パーサー」の部分です。

  • 字句解析(Lexical Analysis): ソースコードをトークン(識別子、キーワード、演算子、リテラルなど)のストリームに変換します。Go言語ではScannerパッケージがこの役割を担います。
  • 構文解析(Syntactic Analysis): トークンのストリームを解析し、言語の文法規則に従って抽象構文木(AST: Abstract Syntax Tree)を構築します。ASTは、プログラムの構造を階層的に表現したものです。このコミットで変更されているparser.goがこの役割を担います。
  • 抽象構文木(AST: Abstract Syntax Tree): ソースコードの構造を木構造で表現したものです。コンパイラの後の段階(意味解析、コード生成など)で利用されます。Go言語ではASTパッケージがASTのノードを定義しています。

Go言語の型システム

Go言語は静的型付け言語であり、変数は使用される前に型が宣言されます。型は、値が保持できるデータの種類と、その値に対して実行できる操作を決定します。

  • 基本型: int, float64, bool, stringなど。
  • 複合型:
    • 配列: [N]T (N個のT型の要素を持つ配列)
    • スライス: []T (T型の要素を持つ動的配列)
    • 構造体: struct { ... } (フィールドの集まり)
    • ポインタ: *T (T型へのポインタ)
    • 関数型: func(args) (results) (関数のシグネチャ)
    • インターフェース型: interface { ... } (メソッドのセット)
    • マップ: map[K]V (キーKと値Vのペア)
    • チャネル: chan T (T型の値を送受信するためのチャネル)

Go言語のインターフェース

Go言語のインターフェースは、メソッドのシグネチャの集まりです。型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たします。これは「構造的型付け(Structural Typing)」または「ダックタイピング(Duck Typing)」と呼ばれます。

  • 埋め込みインターフェース: Goのインターフェースは、他のインターフェースを埋め込むことができます。これにより、埋め込まれたインターフェースのすべてのメソッドが、埋め込む側のインターフェースのメソッドセットに含まれることになります。これは、インターフェースの再利用と構成を可能にする強力な機能です。

    type Reader interface {
        Read(p []byte) (n int, err error)
    }
    
    type Writer interface {
        Write(p []byte) (n int, err error)
    }
    
    type ReadWriter interface {
        Reader // Readerインターフェースを埋め込む
        Writer // Writerインターフェースを埋め込む
    }
    

    上記の例では、ReadWriterインターフェースはReadメソッドとWriteメソッドの両方を持つことになります。

技術的詳細

このコミットは、Go言語のパーサー(usr/gri/pretty/parser.go)の以下の主要な部分に影響を与えています。

  1. 型定義における括弧のサポート:

    • tryType() 関数に新しいcase Scanner.LPARENが追加されました。これは、パーサーが左括弧 ( を検出した場合に、それが型定義の一部であると認識し、括弧内の型を再帰的に解析するようにします。
    • 具体的には、P.next() で左括弧を読み飛ばし、P.parseType() で括弧内の型を解析し、最後に P.expect(Scanner.RPAREN) で右括弧 ) が存在することを検証します。これにより、(T) のような形式で型を記述できるようになります。これは、例えば * (func()) のように、ポインタ型と関数型が組み合わさる場合に、結合順序を明確にするために重要です。
  2. インターフェース内での埋め込みインターフェースのサポート:

    • parseMethodSpec 関数が parseMethodOrInterfaceSpec にリネームされ、そのロジックが拡張されました。
    • 以前は、インターフェースの定義内でメソッドのシグネチャのみを解析していましたが、この変更により、識別子(AST.Ident)が検出された場合に、それがメソッド名であるか、あるいは埋め込みインターフェースであるかを区別するロジックが追加されました。
    • x := P.parseQualifiedIdent() で識別子または修飾識別子を解析します。
    • if tmp, is_ident := x.(*AST.Ident); is_ident && (P.tok == Scanner.COMMA || P.tok == Scanner.LPAREN) の条件で、解析されたものが識別子であり、かつその後にカンマ , または左括弧 ( が続く場合(これはメソッドの引数リストの開始を示唆する)、それはメソッドの宣言であると判断し、P.parseIdentList(x) で識別子リスト(メソッド名)を解析し、P.parseSignature() でメソッドのシグネチャを解析します。
    • それ以外の場合(elseブロック)、解析された x は埋め込みインターフェースであると判断し、そのまま list.Push(x) でリストに追加します。これにより、インターフェース定義内で他のインターフェース名を記述できるようになります。
  3. parseIdentList 関数の変更:

    • parseIdentList() 関数が parseIdentList(x AST.Expr) となり、引数 x を受け取るようになりました。
    • これにより、識別子リストの解析を開始する際に、既に解析済みの識別子(例えば、parseMethodOrInterfaceSpec で最初に解析された識別子)を初期値として渡すことができるようになりました。これは、メソッド宣言のように、最初の識別子が既に読み取られている場合に、その識別子からリストの解析を継続するために役立ちます。
    • if x == nil { x = P.parseIdent(nil); } の行は、引数 xnilの場合(つまり、リストの先頭から解析を開始する場合)に、最初の識別子を解析するようにします。

これらの変更は、Go言語の構文解析器が、より複雑で表現豊かな型およびインターフェースの定義を正しく処理できるようにするための重要なステップでした。

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

usr/gri/pretty/parser.go ファイルが変更されています。

  1. parseIdentList 関数のシグネチャ変更と初期化ロジックの追加:

    --- a/usr/gri/pretty/parser.go
    +++ b/usr/gri/pretty/parser.go
    @@ -244,13 +244,15 @@ func (P *Parser) parseIdent(scope *SymbolTable.Scope) *AST.Ident {
     }
    
    
    -func (P *Parser) parseIdentList() AST.Expr {
    +func (P *Parser) parseIdentList(x AST.Expr) AST.Expr {
     	if P.trace {
     		defer un(trace(P, "IdentList"));
     	}
    
     	var last *AST.BinaryExpr;
    -	var x AST.Expr = P.parseIdent(nil);
    +	if x == nil {
    +		x = P.parseIdent(nil);
    +	}
     	for P.tok == Scanner.COMMA {
     		pos := P.pos;
     		P.next();
    

    parseIdentList が引数 x を受け取るようになり、xnil の場合にのみ parseIdent を呼び出すようになりました。

  2. parseMethodSpec のリネームとインターフェース埋め込みロジックの追加:

    --- a/usr/gri/pretty/parser.go
    +++ b/usr/gri/pretty/parser.go
    @@ -551,14 +553,20 @@ func (P *Parser) parseFunctionType() *AST.Type {
     }
    
    
    -func (P *Parser) parseMethodSpec(list *vector.Vector) {
    +func (P *Parser) parseMethodOrInterfaceSpec(list *vector.Vector) {
     	if P.trace {
    -		defer un(trace(P, "MethodDecl"));
    +		defer un(trace(P, "MethodOrInterfaceSpec"));
     	}
    
    -	list.Push(P.parseIdentList());
    -	t := P.parseSignature();
    -	list.Push(&AST.TypeLit(t));
    +	x := P.parseQualifiedIdent();
    +	if tmp, is_ident := x.(*AST.Ident); is_ident && (P.tok == Scanner.COMMA || P.tok == Scanner.LPAREN) {
    +		// method(s)
    +		list.Push(P.parseIdentList(x));
    +		list.Push(&AST.TypeLit(P.parseSignature()));
    +	} else {
    +		// embedded interface
    +		list.Push(x);
    +	}
     }
    
    
    @@ -576,7 +584,7 @@ func (P *Parser) parseInterfaceType() *AST.Type {
    
     	\tt.List = vector.New(0);\
     	\tfor P.tok == Scanner.IDENT {\
    -\t\t\tP.parseMethodSpec(t.List);\
    +\t\t\tP.parseMethodOrInterfaceSpec(t.List);\
     \t\t\tif P.tok != Scanner.RBRACE {\
     \t\t\t\tP.expect(Scanner.SEMICOLON);\
     \t\t\t}\
    

    parseMethodSpecparseMethodOrInterfaceSpec に変更され、メソッドと埋め込みインターフェースのどちらかを解析するロジックが追加されました。parseInterfaceType からの呼び出しも更新されています。

  3. 型定義における括弧のサポート追加:

    --- a/usr/gri/pretty/parser.go
    +++ b/usr/gri/pretty/parser.go
    @@ -678,6 +686,11 @@ func (P *Parser) tryType() *AST.Type {
     	case Scanner.MAP: return P.parseMapType();
     	case Scanner.STRUCT: return P.parseStructType();
     	case Scanner.MUL: return P.parsePointerType();
    +\tcase Scanner.LPAREN:
    +\t\tP.next();
    +\t\tt := P.parseType();
    +\t\tP.expect(Scanner.RPAREN);\
    +\t\treturn t;\
     	}\
     	\n\
     	// no type found
    

    tryType 関数に Scanner.LPAREN (左括弧) のケースが追加され、括弧で囲まれた型を解析できるようになりました。

  4. parseIdentList の呼び出し箇所の更新:

    --- a/usr/gri/pretty/parser.go
    +++ b/usr/gri/pretty/parser.go
    @@ -1374,7 +1387,7 @@ func (P *Parser) parseConstSpec(d *AST.Decl) {
     	\t\tdefer un(trace(P, "ConstSpec"));
     	}
    
    -\td.Ident = P.parseIdentList();
    +\td.Ident = P.parseIdentList(nil);
     \td.Typ = P.tryType();
     \tif P.tok == Scanner.ASSIGN {\
     \t\tP.next();
    @@ -1399,7 +1412,7 @@ func (P *Parser) parseVarSpec(d *AST.Decl) {
     	\t\tdefer un(trace(P, "VarSpec"));
     	}
    
    -\td.Ident = P.parseIdentList();
    +\td.Ident = P.parseIdentList(nil);
     \tif P.tok == Scanner.ASSIGN {\
     \t\tP.next();
     \t\td.Val = P.parseExpressionList();
    

    parseConstSpecparseVarSpec からの parseIdentList の呼び出しが、引数 nil を渡すように更新されました。

コアとなるコードの解説

parseIdentList の変更

parseIdentList は、識別子のカンマ区切りリスト(例: a, b, c)を解析するための関数です。この変更により、関数がより汎用的に使えるようになりました。

  • func (P *Parser) parseIdentList(x AST.Expr) AST.Expr:
    • 以前は引数なしで、常に新しい識別子の解析から開始していました。
    • 新しいシグネチャでは x AST.Expr を受け取ります。これは、既に解析された最初の識別子を渡すことができるようにするためです。
    • if x == nil { x = P.parseIdent(nil); } の行は、もし xnil であれば、通常通り最初の識別子を解析します。これは、リストの先頭から解析を開始する場合に該当します。
    • もし xnil でなければ、その x をリストの最初の要素として扱い、その後のカンマ区切りリストを解析し続けます。これは、例えばメソッド宣言のように、最初の識別子(メソッド名)が既に別の場所で解析されている場合に便利です。

parseMethodOrInterfaceSpec の導入

この関数は、インターフェース定義内でメソッドのシグネチャを解析するか、埋め込みインターフェースを解析するかを決定する中心的なロジックを含んでいます。

  • x := P.parseQualifiedIdent():
    • まず、識別子または修飾識別子(例: pkg.MyInterface)を解析します。これは、メソッド名である可能性も、埋め込みインターフェース名である可能性もあります。
  • if tmp, is_ident := x.(*AST.Ident); is_ident && (P.tok == Scanner.COMMA || P.tok == Scanner.LPAREN):
    • 解析された xAST.Ident 型(つまり単なる識別子)であり、かつ次のトークンが Scanner.COMMA (カンマ) または Scanner.LPAREN (左括弧) であるかをチェックします。
    • Scanner.COMMA は、複数のメソッド名がカンマで区切られている場合(例: Method1, Method2()) を示唆します。
    • Scanner.LPAREN は、メソッドの引数リストの開始を示唆します。
    • この条件が真の場合、x はメソッド名であると判断されます。
    • list.Push(P.parseIdentList(x)): parseIdentList を呼び出し、既に解析済みのメソッド名 x を渡して、残りのメソッド名を解析します。
    • list.Push(&AST.TypeLit(P.parseSignature())): メソッドのシグネチャ(引数と戻り値の型)を解析し、それをASTノードとしてリストに追加します。
  • else { list.Push(x); }:
    • 上記の条件が偽の場合、解析された x は埋め込みインターフェースであると判断されます。
    • この場合、x はそのままリストに追加されます。これにより、インターフェース定義内で他のインターフェース名を記述できるようになります。

tryType における括弧のサポート

tryType 関数は、現在のトークンに基づいて様々な型の解析関数を呼び出す役割を担っています。

  • case Scanner.LPAREN::
    • 左括弧 ( が検出された場合、パーサーはそれが型定義の一部であると認識します。
    • P.next(): 左括弧を読み飛ばします。
    • t := P.parseType(): 括弧内の型を再帰的に解析します。これにより、(*int)(func() string) のような複雑な型も正しく解析できるようになります。
    • P.expect(Scanner.RPAREN): 括弧の閉じ ) が存在することを検証します。
    • return t;: 括弧内の解析された型を返します。

これらの変更は、Go言語のパーサーが、より複雑な型定義とインターフェースの構成を正確に理解し、抽象構文木に変換できるようにするための基盤を築きました。

関連リンク

参考にした情報源リンク