[インデックス 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つの主要な課題があったと考えられます。
- 型定義の柔軟性向上: 複雑な型を表現する際に、括弧を使ってグループ化する機能は、他の多くのプログラミング言語で一般的です。Go言語においても、より複雑な型表現を可能にし、コードの可読性を向上させるために、型定義における括弧のサポートが必要とされていました。例えば、関数型やポインタ型が組み合わさる場合に、括弧がないと解釈が曖昧になる可能性があります。
- インターフェースの表現力強化: 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
)の以下の主要な部分に影響を与えています。
-
型定義における括弧のサポート:
tryType()
関数に新しいcase Scanner.LPAREN
が追加されました。これは、パーサーが左括弧(
を検出した場合に、それが型定義の一部であると認識し、括弧内の型を再帰的に解析するようにします。- 具体的には、
P.next()
で左括弧を読み飛ばし、P.parseType()
で括弧内の型を解析し、最後にP.expect(Scanner.RPAREN)
で右括弧)
が存在することを検証します。これにより、(T)
のような形式で型を記述できるようになります。これは、例えば* (func())
のように、ポインタ型と関数型が組み合わさる場合に、結合順序を明確にするために重要です。
-
インターフェース内での埋め込みインターフェースのサポート:
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)
でリストに追加します。これにより、インターフェース定義内で他のインターフェース名を記述できるようになります。
-
parseIdentList
関数の変更:parseIdentList()
関数がparseIdentList(x AST.Expr)
となり、引数x
を受け取るようになりました。- これにより、識別子リストの解析を開始する際に、既に解析済みの識別子(例えば、
parseMethodOrInterfaceSpec
で最初に解析された識別子)を初期値として渡すことができるようになりました。これは、メソッド宣言のように、最初の識別子が既に読み取られている場合に、その識別子からリストの解析を継続するために役立ちます。 if x == nil { x = P.parseIdent(nil); }
の行は、引数x
がnil
の場合(つまり、リストの先頭から解析を開始する場合)に、最初の識別子を解析するようにします。
これらの変更は、Go言語の構文解析器が、より複雑で表現豊かな型およびインターフェースの定義を正しく処理できるようにするための重要なステップでした。
コアとなるコードの変更箇所
usr/gri/pretty/parser.go
ファイルが変更されています。
-
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
を受け取るようになり、x
がnil
の場合にのみparseIdent
を呼び出すようになりました。 -
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}\
parseMethodSpec
がparseMethodOrInterfaceSpec
に変更され、メソッドと埋め込みインターフェースのどちらかを解析するロジックが追加されました。parseInterfaceType
からの呼び出しも更新されています。 -
型定義における括弧のサポート追加:
--- 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
(左括弧) のケースが追加され、括弧で囲まれた型を解析できるようになりました。 -
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();
parseConstSpec
とparseVarSpec
からの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); }
の行は、もしx
がnil
であれば、通常通り最初の識別子を解析します。これは、リストの先頭から解析を開始する場合に該当します。- もし
x
がnil
でなければ、そのx
をリストの最初の要素として扱い、その後のカンマ区切りリストを解析し続けます。これは、例えばメソッド宣言のように、最初の識別子(メソッド名)が既に別の場所で解析されている場合に便利です。
parseMethodOrInterfaceSpec
の導入
この関数は、インターフェース定義内でメソッドのシグネチャを解析するか、埋め込みインターフェースを解析するかを決定する中心的なロジックを含んでいます。
x := P.parseQualifiedIdent()
:- まず、識別子または修飾識別子(例:
pkg.MyInterface
)を解析します。これは、メソッド名である可能性も、埋め込みインターフェース名である可能性もあります。
- まず、識別子または修飾識別子(例:
if tmp, is_ident := x.(*AST.Ident); is_ident && (P.tok == Scanner.COMMA || P.tok == Scanner.LPAREN)
:- 解析された
x
がAST.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言語のパーサーが、より複雑な型定義とインターフェースの構成を正確に理解し、抽象構文木に変換できるようにするための基盤を築きました。
関連リンク
- Go言語の仕様 (The Go Programming Language Specification):
- Go言語のインターフェースに関する公式ドキュメント:
参考にした情報源リンク
- Go言語のソースコード (GitHub):
- Go言語の初期のコミット履歴 (GitHub):
- Go言語のASTパッケージに関するドキュメント:
- Go言語のScannerパッケージに関するドキュメント:
- Go言語のインターフェース埋め込みに関する解説記事 (例):
- https://www.ardanlabs.com/blog/2013/07/interface-embedding-in-go.html (Ardan Labsのブログ記事、Goのインターフェース埋め込みについて)
- https://www.geeksforgeeks.org/embedding-interfaces-in-go/ (GeeksforGeeksのGoインターフェース埋め込みに関する記事)
- Go言語の型定義における括弧の使用に関する議論 (Stack Overflowなど):
- https://stackoverflow.com/questions/tagged/go+types+parentheses (Stack OverflowのGo言語の型と括弧に関する質問)
- https://stackoverflow.com/questions/tagged/go+function+types (Stack OverflowのGo言語の関数型に関する質問)
- コンパイラの構文解析に関する一般的な情報:
- https://ja.wikipedia.org/wiki/%E6%A7%8B%E6%96%87%E8%A7%A3%E6%9E%90 (Wikipediaの構文解析に関する記事)
- https://ja.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E6%A7%8B%E6%96%87%E6%9C%A8 (Wikipediaの抽象構文木に関する記事)