[インデックス 1905] ファイルの概要
このコミットは、Go言語の初期のパーサー実装の一部である usr/gri/pretty/parser.go
ファイルに対する変更です。このファイルは、Goソースコードを解析し、抽象構文木(AST)を構築する役割を担っていました。特に、関数やメソッドの宣言、複合リテラルなどの構文要素の解析ロジックが含まれています。pretty
というディレクトリ名から、このパーサーがコードの整形(pretty-printing)にも関連していた可能性が示唆されます。
コミット
このコミットは、Go言語のパーサーにおけるレシーバ構文の検証を強化し、既存のバグを修正するとともに、不要なパニック(panic)呼び出しを削除することを目的としています。これにより、パーサーの堅牢性と正確性が向上し、より厳密な構文チェックが可能になります。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/de9cf52835c134a8e5a0df9a0880caa79b9b9b88
元コミット内容
- receiver syntax verification
- removed left-over panic() call
- fixed a couple of bugs
R=r
OCL=26856
CL=26856
変更の背景
Go言語は、その設計初期からシンプルで明確な構文を目指していました。このコミットが行われた2009年は、Go言語がまだ活発に開発され、その仕様が固まりつつあった時期です。このような時期には、言語の構文規則を正確に解釈し、不正なコードを早期に検出するためのパーサーの改善が頻繁に行われます。
このコミットの主な背景は以下の点にあります。
- レシーバ構文の厳密な検証: Go言語のメソッドは、レシーバと呼ばれる特別な引数を持ちます。このレシーバの構文は厳密に定義されており、例えば「レシーバは常に1つであること」や「レシーバの型は型名またはそのポインタであること」といったルールがあります。初期のパーサーでは、これらのルールに対する検証が不十分であった可能性があり、本コミットでその検証を強化しています。これにより、コンパイル時に不正なメソッド宣言をより確実に捕捉できるようになります。
- パーサーの堅牢性向上: コード解析中に予期せぬエラーが発生した場合、パーサーは適切なエラーメッセージを生成し、可能であれば解析を続行するか、少なくともクリーンに終了する必要があります。
panic()
は通常、回復不能なエラーやプログラマーの論理的誤りを示すために使用されますが、パーサーのようなツールでは、構文エラーに対してはより穏やかなエラー報告メカニズム(例: エラーを記録して解析を続行する)が望ましい場合があります。このコミットでは、不要なpanic()
呼び出しを削除し、より適切なエラーハンドリングに移行することで、パーサーの堅牢性を高めています。 - 既存のバグ修正: 開発初期のソフトウェアには、当然ながら多くのバグが存在します。このコミットは、パーサー内のいくつかの既存のバグを修正し、特定の構文パターンに対する解析の正確性を向上させています。特に、複合リテラルの型解析や、括弧で囲まれた式の処理に関する修正が含まれています。
これらの変更は、Go言語のコンパイラがより正確にコードを解釈し、開発者に対してより明確なエラーフィードバックを提供するための重要なステップでした。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語およびコンパイラに関する基本的な知識が必要です。
-
Go言語の構文解析(Parsing):
- 字句解析(Lexical Analysis/Tokenizing): ソースコードを意味のある最小単位(トークン)に分解するプロセスです。例えば、
func (p *Parser) Parse() {}
というコードは、func
、(
、p
、*
、Parser
、)
、Parse
、(
、)
、{
、}
といったトークンに分解されます。 - 構文解析(Syntactic Analysis/Parsing): 字句解析で得られたトークンのストリームが、言語の文法規則に従っているかを検証し、同時に抽象構文木(AST: Abstract Syntax Tree)を構築するプロセスです。ASTは、ソースコードの構造を木構造で表現したもので、コンパイラの次の段階(意味解析、コード生成など)で利用されます。
- 再帰下降パーサー(Recursive Descent Parser): Go言語のパーサーは、主に再帰下降方式で実装されています。これは、文法の各規則に対応する関数を作成し、それらの関数が再帰的に呼び出し合うことで構文を解析する手法です。
- 字句解析(Lexical Analysis/Tokenizing): ソースコードを意味のある最小単位(トークン)に分解するプロセスです。例えば、
-
Go言語のレシーバ(Receiver):
- Go言語にはクラスベースの継承はありませんが、型にメソッドを関連付けることができます。このメソッドが関連付けられる型を「レシーバ」と呼びます。
- メソッド宣言の構文は
func (レシーバ名 レシーバ型) メソッド名(引数リスト) (戻り値リスト) { ... }
となります。 - レシーバは、メソッドがどの型の値に対して操作を行うかを示します。レシーバは値型でもポインタ型でも宣言できます。
- Goの仕様では、レシーバは常に1つであり、その型は型名(
MyStruct
)またはそのポインタ(*MyStruct
)である必要があります。
-
Go言語の型システム:
- 型名(Type Name):
int
,string
,MyStruct
のように、特定の型を識別するための名前です。 - 複合リテラル(Composite Literal): 構造体、配列、スライス、マップなどの複合型を初期化するための構文です。例えば、
MyStruct{Field: value}
や[]int{1, 2, 3}
などがあります。複合リテラルは、その型が明確に指定されるか、文脈から推論できる必要があります。
- 型名(Type Name):
-
go/ast
パッケージ:- Go言語の標準ライブラリに含まれる
go/ast
パッケージは、Goソースコードの抽象構文木(AST)を表現するためのデータ構造を提供します。 ast.Expr
は、式を表すASTノードのインターフェースです。ast.Ident
は識別子(変数名、型名など)を表します。ast.ParenExpr
は括弧で囲まれた式を表します。ast.SelectorExpr
はセレクタ式(例:obj.Field
)を表します。ast.StarExpr
はポインタ型(例:*Type
)を表します。ast.BadExpr
は、構文エラーによって正しく解析できなかった式を表すために使用される特別なASTノードです。
- Go言語の標準ライブラリに含まれる
-
Go言語のエラーハンドリングと
panic()
:- Go言語では、通常、エラーは多値戻り値(
value, err := function()
)によって明示的に処理されます。 panic()
は、プログラムが回復不能な状態に陥った場合や、プログラマーの論理的誤りを示すために使用されます。panic()
が呼び出されると、現在のゴルーチンは実行を停止し、遅延関数(defer
)が実行された後、コールスタックを遡り、recover()
によって捕捉されない限りプログラム全体が終了します。- パーサーのようなツールでは、構文エラーは回復可能なエラーと見なされることが多く、
panic()
ではなく、エラーを報告して解析を続行する方が望ましいとされます。
- Go言語では、通常、エラーは多値戻り値(
技術的詳細
このコミットは、usr/gri/pretty/parser.go
ファイル内の複数の関数にわたる変更を含んでおり、Go言語の構文解析の正確性と堅牢性を向上させています。
-
makeExpr
関数の変更とpanic()
の削除:makeExpr
関数は、与えられたASTノードが有効な式であるかを検証する役割を担っています。- 変更前は、有効な式でない場合に
p.error_expected(...)
を呼び出した後、panic()
を呼び出してプログラムを異常終了させていました。 - 変更後は、
panic()
の呼び出しが削除され、代わりに&ast.BadExpr{x.Pos()}
というBadExpr
ノードを返しています。これは、構文エラーが発生した場合でもパーサーがクラッシュすることなく、エラー箇所をBadExpr
としてASTにマークし、解析を続行できるようにするための重要な変更です。これにより、複数の構文エラーを一度に報告できるようになり、ユーザーエクスペリエンスが向上します。 *ast.ParenExpr
のケースでは、変更前はreturn p.makeExpr(t.X)
としていましたが、変更後はp.makeExpr(t.X); return x;
となっています。これは、括弧内の式が有効な式であることを確認しつつ、元のParenExpr
ノード自体を返すように修正されたことを意味します。
-
makeType
からmakeTypeName
への変更と型検証の細分化:- 変更前は
makeType
という関数があり、与えられたASTノードが一般的な「型」であるかを検証していました。 - 変更後は、この関数が
makeTypeName
にリネームされ、その役割が「型名」であるかを検証することに特化されました。makeTypeName
は、ast.Ident
(識別子)や、括弧で囲まれた型名(ast.ParenExpr
)、セレクタ式(ast.SelectorExpr
、例:pkg.Type
)を有効な型名として扱います。 - この変更は、Go言語の型システムにおける「型名」と「型リテラル」(例:
[]int
,struct{}
)の区別をより明確にし、パーサーがより厳密な文脈依存の型検証を行えるようにするためのものです。
- 変更前は
-
makeCompositeLitType
の導入:makeTypeName
とは別に、新たにmakeCompositeLitType
関数が導入されました。この関数は、複合リテラル({}
で初期化される構造体、配列、スライス、マップなど)の型が有効であるかを検証します。- 複合リテラルの型としては、型名(
ast.Ident
)、括弧で囲まれた型(ast.ParenExpr
)、セレクタ式(ast.SelectorExpr
)、配列型(ast.ArrayType
)、スライス型(ast.SliceType
)、構造体型(ast.StructType
)、マップ型(ast.MapType
)などが許可されます。 - この分離により、パーサーは複合リテラルの文脈において、より正確な型検証を行うことができるようになりました。例えば、
makeTypeName
では許可されないArrayType
やSliceType
が、makeCompositeLitType
では許可されるなど、文脈に応じた柔軟かつ厳密なチェックが可能になります。
-
parseCompositeLit
関数の修正:parseCompositeLit
関数は、複合リテラルを解析する役割を担っています。- 変更前は、複合リテラルの型を
p.parseCompositeLit(x)
のように直接x
として受け取っていましたが、変更後はp.parseCompositeLit(p.makeCompositeLitType(x))
と、makeCompositeLitType
関数を介して型を検証するようになりました。これにより、複合リテラルの型がGoの仕様に準拠しているかどうかが、解析の早い段階で厳密にチェックされるようになります。
-
parseReceiver
関数の導入とレシーバ構文の検証:- このコミットの最も重要な変更点の一つは、
parseReceiver
という新しい関数の導入です。この関数は、メソッド宣言におけるレシーバ部分(例:(p *Parser)
)を解析し、その構文がGoの仕様に準拠しているかを検証します。 parseReceiver
は、まずp.parseParameters(false)
を呼び出して、レシーバ部分を通常のパラメータリストとして解析します。- その後、以下の検証を行います。
- レシーバの数:
len(par) != 1
またはlen(par) == 1 && len(par[0].Names) > 1
の場合、つまりレシーバが1つでない場合(例:(p1 *P, p2 *P)
や(p *P, q *Q)
)、"exactly one receiver"
というエラーを報告します。Goの仕様では、メソッドのレシーバは常に1つでなければなりません。 - レシーバの型: レシーバの基本型(ポインタ型の場合はその参照先の型)が
makeTypeName
によって有効な型名であるかを検証します。これにより、レシーバの型がMyStruct
や*MyStruct
のように、型名またはそのポインタであるというGoのルールが強制されます。
- レシーバの数:
- この関数により、レシーバの構文エラーがより具体的かつ早期に検出されるようになります。
- このコミットの最も重要な変更点の一つは、
-
parseFunctionDecl
関数の修正:parseFunctionDecl
関数は、関数またはメソッドの宣言を解析する役割を担っています。- 変更前は、レシーバの解析ロジックがこの関数内に直接記述されており、レシーバの数に関する簡単なチェックしか行われていませんでした。
- 変更後は、レシーバの解析と検証の責任が新しく導入された
parseReceiver
関数に完全に委譲されました。これにより、parseFunctionDecl
のコードが簡潔になり、レシーバの検証ロジックが一箇所に集約され、保守性が向上しました。
これらの変更は、Go言語のパーサーがより正確で、堅牢で、そしてGoの言語仕様に厳密に準拠するように進化していく過程を示しています。特に、エラーハンドリングの改善と、構文要素ごとの検証ロジックの細分化は、コンパイラの品質向上に大きく貢献しています。
コアとなるコードの変更箇所
usr/gri/pretty/parser.go
ファイルにおいて、以下の関数が変更または新規追加されています。
func (p *parser) makeExpr(x ast.Expr) ast.Expr
:panic()
呼び出しが削除され、&ast.BadExpr{x.Pos()}
を返すように変更。*ast.ParenExpr
の処理がp.makeExpr(t.X); return x;
に変更。
func (p *parser) makeTypeName(x ast.Expr) ast.Expr
:- 旧
makeType
関数がリネームされ、型名の検証に特化。 *ast.ParenExpr
と*ast.SelectorExpr
のケースが追加。
- 旧
func (p *parser) makeCompositeLitType(x ast.Expr) ast.Expr
:- 新規追加された関数。複合リテラルの型検証に特化。
func (p *parser) parseCompositeLit(typ ast.Expr) ast.Expr
:p.parseCompositeLit(p.makeCompositeLitType(x))
のように、makeCompositeLitType
を介して型を検証するように変更。
func (p *parser) parseReceiver() *ast.Field
:- 新規追加された関数。メソッドのレシーバ構文の解析と検証を担当。
func (p *parser) parseFunctionDecl() *ast.FuncDecl
:- レシーバの解析ロジックが
parseReceiver()
の呼び出しに置き換えられ、簡潔化。
- レシーバの解析ロジックが
コアとなるコードの解説
func (p *parser) makeExpr(x ast.Expr) ast.Expr
この関数は、与えられたASTノード x
が有効な式であるかを検証します。
変更前は、有効な式でない場合にpanic()
を呼び出していましたが、これはパーサーがクラッシュする原因となり、複数のエラーを一度に報告できないという問題がありました。
変更後は、panic()
を削除し、代わりに&ast.BadExpr{x.Pos()}
を返します。ast.BadExpr
は、構文エラーによって正しく解析できなかった部分を表すASTノードであり、これによりパーサーはエラーを記録しつつ解析を続行できるようになります。
また、*ast.ParenExpr
(括弧で囲まれた式)のケースでは、括弧内の式 t.X
が有効な式であることを確認しつつ、元のParenExpr
ノード x
を返すように修正されました。これにより、括弧の構造がASTに正しく保持されます。
// makeExpr makes sure x is an expression and not a type.
func (p *parser) makeExpr(x ast.Expr) ast.Expr {
switch t := x.(type) {
// ... (既存のケース) ...
case *ast.ParenExpr: p.makeExpr(t.X); return x; // 変更点: panic()削除, return x
// ... (既存のケース) ...
}
// all other nodes are not proper expressions
p.error_expected(x.Pos(), "expression");
// panic(); // 削除された行
return &ast.BadExpr{x.Pos()}; // 追加された行
}
func (p *parser) makeTypeName(x ast.Expr) ast.Expr
この関数は、旧makeType
関数がリネームされたもので、その役割は「型名」であるかを厳密に検証することに特化されました。Go言語では、int
やstring
、ユーザー定義の構造体名などが型名にあたります。
この関数は、ast.Ident
(識別子)、ast.ParenExpr
(括弧で囲まれた型名、例: (MyType)
)、ast.SelectorExpr
(セレクタ式、例: pkg.MyType
)を有効な型名として認識します。
これにより、パーサーは型名の文脈において、より正確な構文チェックを行うことができます。
// makeTypeName makes sure that x is type name.
func (p *parser) makeTypeName(x ast.Expr) ast.Expr {
// TODO should provide predicate in AST nodes
switch t := x.(type) {
case *ast.BadExpr: return x;
case *ast.Ident: return x;
case *ast.ParenExpr: p.makeTypeName(t.X); return x; // TODO should (TypeName) be illegal?
case *ast.SelectorExpr: p.makeTypeName(t.X); return x;
}
// all other nodes are not type names
p.error_expected(x.Pos(), "type name");
return &ast.BadExpr{x.Pos()};
}
func (p *parser) makeCompositeLitType(x ast.Expr) ast.Expr
この関数は新規に追加されたもので、複合リテラル({}
で初期化される構造体、配列、スライス、マップなど)の型が有効であるかを検証します。
複合リテラルの型は、型名だけでなく、配列型([]int
)、スライス型([]T
)、構造体型(struct{}
)、マップ型(map[K]V
)など、様々な型リテラルも取り得ます。
この関数は、これらの複合リテラルとして有効な型を識別し、それ以外の不正な型が指定された場合にはエラーを報告します。これにより、複合リテラルの構文解析がより厳密になります。
// makeCompositeLitType makes sure x is a legal composite literal type.
func (p *parser) makeCompositeLitType(x ast.Expr) ast.Expr {
// TODO should provide predicate in AST nodes
switch t := x.(type) {
case *ast.BadExpr: return x;
case *ast.Ident: return x;
case *ast.ParenExpr: p.makeCompositeLitType(t.X); return x;
case *ast.SelectorExpr: p.makeTypeName(t.X); return x; // ここでは型名として検証
case *ast.ArrayType: return x;
case *ast.SliceType: return x;
case *ast.StructType: return x;
case *ast.MapType: return x;
}
// all other nodes are not legal composite literal types
p.error_expected(x.Pos(), "composite literal type");
return &ast.BadExpr{x.Pos()};
}
func (p *parser) parseReceiver() *ast.Field
この関数は新規に追加されたもので、Go言語のメソッド宣言におけるレシーバ部分(例: (p *MyType)
)を解析し、その構文がGoの仕様に準拠しているかを検証する役割を担います。
Goの仕様では、メソッドのレシーバは常に1つであり、その型は型名またはそのポインタでなければなりません。
この関数は、まずレシーバ部分を通常のパラメータリストとして解析し、その後、以下の厳密なチェックを行います。
- レシーバの数の検証:
len(par) != 1 || len(par) == 1 && len(par[0].Names) > 1
par
は解析されたパラメータのリストです。len(par) != 1
は、レシーバが1つでない場合(0個または2個以上)をチェックします。len(par) == 1 && len(par[0].Names) > 1
は、レシーバが1つだが、そのレシーバが複数の名前を持っている場合(例:(p, q *MyType)
)をチェックします。Goのレシーバは単一の名前を持つ必要があります。- これらの条件のいずれかが真の場合、
"exactly one receiver"
というエラーを報告します。
- レシーバの型の検証:
p.makeTypeName(base)
- レシーバの型がポインタ型(
*ast.StarExpr
)である場合、その参照先の型(ptr.X
)を取得します。 - 最終的に得られた基本型
base
が、makeTypeName
関数によって有効な型名であるかを検証します。これにより、レシーバの型がMyStruct
や*MyStruct
のように、型名またはそのポインタであるというGoのルールが強制されます。
- レシーバの型がポインタ型(
この関数により、レシーバの構文エラーがより具体的かつ早期に検出されるようになり、パーサーの正確性が大幅に向上します。
func (p *parser) parseReceiver() *ast.Field {
if p.trace {
defer un(trace(p, "Receiver"));
}
pos := p.pos;
par := p.parseParameters(false);
// must have exactly one receiver
if len(par) != 1 || len(par) == 1 && len(par[0].Names) > 1 {
p.error_expected(pos, "exactly one receiver");
return &ast.Field{nil, nil, &ast.BadExpr{noPos}, nil};
}
recv := par[0];
// recv type must be TypeName or *TypeName
base := recv.Type;
if ptr, is_ptr := base.(*ast.StarExpr); is_ptr {
base = ptr.X;
}
p.makeTypeName(base); // レシーバの基本型が型名であることを検証
return recv;
}
func (p *parser) parseFunctionDecl() *ast.FuncDecl
この関数は、関数またはメソッドの宣言を解析します。
変更前は、レシーバの解析ロジックがこの関数内に直接記述されており、レシーバの数に関する簡単なチェックしか行われていませんでした。
変更後は、レシーバの解析と検証の責任が新しく導入されたparseReceiver
関数に完全に委譲されました。これにより、parseFunctionDecl
のコードが簡潔になり、レシーバの検証ロジックが一箇所に集約され、保守性が向上しました。
func (p *parser) parseFunctionDecl() *ast.FuncDecl {
if p.trace {
defer un(trace(p, "FunctionDecl"));
}
pos := p.pos;
p.expect(token.FUNC);
var recv *ast.Field;
if p.tok == token.LPAREN {
recv = p.parseReceiver(); // 変更点: parseReceiver()を呼び出す
}
ident := p.parseIdent();
typ := p.parseFuncType(false); // no receiver
return &ast.FuncDecl{pos, recv, ident, typ, nil};
}
関連リンク
- Go言語仕様: https://go.dev/ref/spec
- 特に「Method declarations」と「Composite literals」のセクションが関連します。
go/ast
パッケージドキュメント: https://pkg.go.dev/go/astgo/parser
パッケージドキュメント: https://pkg.go.dev/go/parser
参考にした情報源リンク
- Go language receiver syntax early developmentに関するWeb検索結果
- Go composite literal syntax early developmentに関するWeb検索結果
- Go ast package early developmentに関するWeb検索結果
- Go parser design principles earlyに関するWeb検索結果
usr/gri/pretty/parser.go golang
に関するWeb検索結果- Go言語の公式ドキュメントおよびブログ記事 (Go Blogなど)
- Go言語のソースコードリポジトリ (GitHub)
- Go言語のコンパイラ設計に関する一般的な情報源