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

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

このコミットは、Go言語のパーサー(go/parserパッケージ)において、const(定数)とvar(変数)の宣言を解析するコードを統合し、共通化することを目的としています。これにより、コードの重複を削減し、パーサーのコードサイズを縮小するとともに、抽象構文木(AST)の表現における一貫性を高め、後続の型チェッカーでのコード共有を可能にしています。また、Go言語の「型関数」(メソッド)の利用を促進するリファクタリングも含まれています。

コミット

commit 6c740e769f11ab4a7e4300a2011fe86109bd32ae
Author: Robert Griesemer <gri@golang.org>
Date:   Thu Oct 4 20:53:43 2012 -0700

    go/parser: unify parsing of const and var declarations
    
    The AST representation is already identical. Making the
    code (nearly) identical in the parser reduces code size
    and ensures that the ast.ValueSpec nodes have the same
    values (specifically, iota). This in turn permits the
    sharing of much of the respective code in the typechecker.
    
    While at it: type functions work now, so use them.
    
    R=r
    CC=golang-dev
    https://golang.org/cl/6624047

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

https://github.com/golang/go/commit/6c740e769f11ab4a7e4300a2011fe86109bd32ae

元コミット内容

このコミットの目的は、Go言語のパーサーであるgo/parserパッケージにおいて、const(定数)とvar(変数)の宣言を解析するロジックを統合することです。コミットメッセージによると、これらの宣言は既に抽象構文木(AST)上では同一の表現(ast.ValueSpecノード)を持っています。パーサー内のコードをほぼ同一にすることで、コードの重複を減らし、コードサイズを削減します。さらに、これによりast.ValueSpecノードがiota(定数宣言で用いられる特殊な識別子)のような値を含めて一貫した値を保持することが保証され、結果として型チェッカー(typechecker)で多くの関連コードを共有できるようになります。

また、このコミットでは「type functions work now, so use them.」という記述があり、これはGo言語の機能改善(おそらくメソッド式のサポート強化など)に伴い、関数をレシーバを持つメソッドとして定義するリファクタリングも同時に行われたことを示唆しています。

変更の背景

この変更の背景には、主に以下の点が挙げられます。

  1. コードの重複と保守性の問題: Go言語のパーサーにおいて、constvarの宣言は構文的には類似していますが、それぞれ異なる解析関数(parseConstSpecparseVarSpec)で処理されていました。これにより、類似したロジックが二重に存在し、コードの重複と保守性の低下を招いていました。
  2. ASTの一貫性: constvar宣言は、最終的に同じast.ValueSpecというASTノードに変換されます。パーサーの段階でこの共通性を反映させることで、ASTの生成ロジックをより一貫性のあるものにできます。
  3. 型チェッカーの効率化: パーサーがconstvarの宣言を統一的に処理することで、後続の型チェッカーは、これらの宣言を区別することなく、共通のロジックで処理できるようになります。これにより、型チェッカーのコードも簡素化され、効率が向上します。特にiotaのような定数特有の概念も、統一されたast.ValueSpecノード内で適切に扱われることで、型チェッカーでの処理が容易になります。
  4. Go言語機能の活用: 「type functions work now」という記述は、Go言語のコンパイラやツールチェインの進化により、メソッド(レシーバを持つ関数)の利用がより柔軟になったことを示唆しています。これにより、パーサーの内部関数をparser構造体のメソッドとして定義し、コードの構造を改善する機会が生まれました。これは、オブジェクト指向的なアプローチでコードを整理し、可読性と保守性を高めるためのリファクタリングです。

これらの背景から、パーサーのコードベースをより効率的で保守しやすいものにし、Go言語全体のコンパイラパイプラインの最適化を図る目的でこの変更が行われました。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびコンパイラの基本的な概念を理解しておく必要があります。

  1. Go言語のパーサー (go/parser):

    • Go言語のコンパイラツールチェインの一部であり、Goのソースコードを読み込み、その構文構造を解析して抽象構文木(AST)を生成する役割を担います。
    • 字句解析器(lexer)から受け取ったトークン列を基に、Go言語の文法規則に従って構文解析を行います。
    • go/parserパッケージは、Goのソースコードをプログラム的に解析するためのAPIを提供します。
  2. 抽象構文木 (AST - Abstract Syntax Tree):

    • ソースコードの抽象的な構文構造を木構造で表現したものです。具体的な構文要素(括弧、セミコロンなど)は省略され、プログラムの意味的な構造が強調されます。
    • Go言語では、go/astパッケージがASTのノード型を定義しています。例えば、変数や定数の宣言はast.ValueSpecノードとして表現されます。
    • パーサーはソースコードをASTに変換し、その後のコンパイラフェーズ(型チェック、コード生成など)がASTを操作して処理を進めます。
  3. constvar 宣言:

    • const (定数宣言): 変更不可能な値を宣言します。コンパイル時に値が確定します。
      const Pi = 3.14
      const (
          A = 1
          B
          C = "hello"
      )
      
    • var (変数宣言): 変更可能な値を格納する変数を宣言します。
      var x int
      var y = 10
      var (
          Name string
          Age  int
      )
      
    • 両者は、複数の識別子をまとめて宣言したり、型を省略して初期値から型を推論させたり、括弧で囲んでグループ化したりするなど、構文的に多くの共通点を持っています。
  4. iota:

    • Go言語のconst宣言でのみ使用される特殊な識別子です。
    • constブロック内で使用され、連続する定数に自動的にインクリメントされる整数値を割り当てます。最初のiota0で、以降のconst宣言ごとに1ずつ増加します。
    • このコミットでは、iotaの値がast.ValueSpecノードで一貫して扱われることが重要視されています。
  5. token.Token:

    • go/tokenパッケージで定義されている型で、Go言語の字句解析器(lexer)がソースコードから切り出した「トークン」(キーワード、識別子、演算子、リテラルなど)を表します。
    • 例えば、token.CONSTconstキーワード、token.VARvarキーワードを表します。パーサーはこれらのトークンを基に構文解析を進めます。
  6. Go言語のメソッド (レシーバを持つ関数):

    • Go言語では、関数は特定の型に関連付けることができます。これをメソッドと呼びます。
    • メソッドは、レシーバ引数(func (r ReceiverType) MethodName(...))を持ち、その型の値またはポインタに対して呼び出されます。
    • このコミットで言及されている「type functions work now」は、おそらくGoのコンパイラがメソッド式(T.Methodのような形式でメソッドを関数値として扱う機能)をより適切にサポートするようになったことを指している可能性があります。これにより、パーサーの内部関数をparser構造体のメソッドとして定義し、コードの構造をより整理することが可能になりました。

技術的詳細

このコミットの技術的な核心は、constvar宣言の解析ロジックをparseValueSpecという単一の関数に統合し、その関数がtoken.Token型のkeyword引数を受け取るように変更した点にあります。

  1. parseSpecFunction シグネチャの変更:

    • 変更前: type parseSpecFunction func(p *parser, doc *ast.CommentGroup, iota int) ast.Spec
    • 変更後: type parseSpecFunction func(p *parser, doc *ast.CommentGroup, keyword token.Token, iota int) ast.Spec
    • この変更により、parseSpecFunction型の関数は、解析対象の宣言がconstなのかvarなのかを示すkeywordトークンを受け取れるようになりました。これにより、単一の関数内で異なるキーワードに応じた処理を分岐させることが可能になります。
  2. parseConstSpecparseVarSpec の統合:

    • 既存のparseConstSpec関数がparseValueSpecにリネームされ、parseVarSpec関数は完全に削除されました。
    • parseValueSpec関数は、constvarの両方の宣言を処理する責任を負います。
    • trace関数の呼び出しがkeyword.String() + "Spec"に変更され、デバッグトレース時にConstSpecまたはVarSpecと動的に表示されるようになりました。
  3. parseValueSpec 内での constvar の分岐ロジック:

    • 最も重要な変更は、values(初期値のリスト)を解析するかどうかの条件式です。
    • 変更前(parseConstSpec): if typ != nil || p.tok == token.ASSIGN || iota == 0
    • 変更前(parseVarSpec): if typ == nil || p.tok == token.ASSIGN
    • 変更後(parseValueSpec): if p.tok == token.ASSIGN || keyword == token.CONST && (typ != nil || iota == 0) || keyword == token.VAR && typ == nil
    • この新しい条件式は、以下の論理を統合しています。
      • p.tok == token.ASSIGN: =(代入演算子)がある場合は、必ず初期値のリストを解析します。これはconstvarに共通です。
      • keyword == token.CONST && (typ != nil || iota == 0): const宣言の場合、型が指定されているか、またはiota0(最初の定数)の場合は、初期値のリストを解析します。iota0の場合は、明示的な値がなくても暗黙的に0が割り当てられるため、初期値の解析が必要です。
      • keyword == token.VAR && typ == nil: var宣言の場合、型が省略されている(初期値から型を推論する)場合は、初期値のリストを解析します。型が指定されている場合は、初期値は必須ではありません。
    • この複雑な条件式により、constvarの異なる初期値の規則が単一の関数内で正確に処理されます。
  4. 関数からメソッドへの変更:

    • parseImportSpecparseTypeSpecは、グローバル関数から*parser型のメソッドに変換されました。
    • 変更前: func parseImportSpec(p *parser, ...)
    • 変更後: func (p *parser) parseImportSpec(...)
    • これにより、これらの関数はparser構造体の状態にアクセスしやすくなり、コードのモジュール性が向上します。また、parseSpecFunctionのシグネチャに合わせるために、未使用のtoken.Token引数も追加されています。
  5. parseGenDecl および parseDecl での呼び出し箇所の変更:

    • parseGenDecl関数は、parseSpecFunction型の引数fを呼び出す際に、新しいkeyword引数(token.IMPORT, token.CONST, token.VARなど)を渡すように変更されました。
    • parseDecl関数内のswitch文では、token.CONSTtoken.VARの両方が(*parser).parseValueSpecを指すように変更され、token.TYPE(*parser).parseTypeSpecを指すようになりました。
    • parseFile関数内のtoken.IMPORT(*parser).parseImportSpecを指すように変更されました。
    • これらの変更により、パーサー全体で統一された解析ロジックが適用されるようになりました。

これらの変更は、Go言語のパーサーの内部構造をより洗練させ、コードの重複を排除し、将来的な拡張性や保守性を高めることに貢献しています。

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

src/pkg/go/parser/parser.go ファイルにおける主要な変更箇所は以下の通りです。

--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -2040,7 +2040,7 @@ func (p *parser) parseStmt() (s ast.Stmt) {
 // ----------------------------------------------------------------------------
 // Declarations
 
-type parseSpecFunction func(p *parser, doc *ast.CommentGroup, iota int) ast.Spec
+type parseSpecFunction func(p *parser, doc *ast.CommentGroup, keyword token.Token, iota int) ast.Spec
 
 func isValidImport(lit string) bool {
 	const illegalChars = `!"#$%&'()*,:;<=>?[\\]^{|}` + "`\uFFFD"
@@ -2053,7 +2053,7 @@ func isValidImport(lit string) bool {
 	return s != ""
 }
 
-func parseImportSpec(p *parser, doc *ast.CommentGroup, _ int) ast.Spec {
+func (p *parser) parseImportSpec(doc *ast.CommentGroup, _ token.Token, _ int) ast.Spec {
 	if p.trace {
 		defer un(trace(p, "ImportSpec"))
 	}
@@ -2091,15 +2091,15 @@ func parseImportSpec(p *parser, doc *ast.CommentGroup, _ int) ast.Spec {
 	return spec
 }
 
-func parseConstSpec(p *parser, doc *ast.CommentGroup, iota int) ast.Spec {
+func (p *parser) parseValueSpec(doc *ast.CommentGroup, keyword token.Token, iota int) ast.Spec {
 	if p.trace {
-\t\tdefer un(trace(p, "ConstSpec"))
+\t\tdefer un(trace(p, keyword.String()+"Spec"))
 	}
 
 	idents := p.parseIdentList()
 	typ := p.tryType()
 	var values []ast.Expr
-\tif typ != nil || p.tok == token.ASSIGN || iota == 0 {
+\tif p.tok == token.ASSIGN || keyword == token.CONST && (typ != nil || iota == 0) || keyword == token.VAR && typ == nil {
 		p.expect(token.ASSIGN)
 		values = p.parseRhsList()
 	}
@@ -2121,7 +2121,7 @@ func parseConstSpec(p *parser, doc *ast.CommentGroup, iota int) ast.Spec {
 	return spec
 }
 
-func parseTypeSpec(p *parser, doc *ast.CommentGroup, _ int) ast.Spec {
+func (p *parser) parseTypeSpec(doc *ast.CommentGroup, _ token.Token, _ int) ast.Spec {
 	if p.trace {
 		defer un(trace(p, "TypeSpec"))
 	}
@@ -2142,36 +2142,6 @@ func parseTypeSpec(p *parser, doc *ast.CommentGroup, _ int) ast.Spec {
 	return spec
 }
 
-func parseVarSpec(p *parser, doc *ast.CommentGroup, _ int) ast.Spec {
-\tif p.trace {\n-\t\tdefer un(trace(p, "VarSpec"))\n-\t}\n-\n-\tidents := p.parseIdentList()\n-\ttyp := p.tryType()\n-\tvar values []ast.Expr\n-\tif typ == nil || p.tok == token.ASSIGN {\n-\t\tp.expect(token.ASSIGN)\n-\t\tvalues = p.parseRhsList()\n-\t}\n-\tp.expectSemi() // call before accessing p.linecomment\n-\n-\t// Go spec: The scope of a constant or variable identifier declared inside\n-\t// a function begins at the end of the ConstSpec or VarSpec and ends at\n-\t// the end of the innermost containing block.\n-\t// (Global identifiers are resolved in a separate phase after parsing.)\n-\tspec := &ast.ValueSpec{\n-\t\tDoc:     doc,\n-\t\tNames:   idents,\n-\t\tType:    typ,\n-\t\tValues:  values,\n-\t\tComment: p.lineComment,\n-\t}\n-\tp.declare(spec, nil, p.topScope, ast.Var, idents...)\n-\n-\treturn spec\n-}\n-\n func (p *parser) parseGenDecl(keyword token.Token, f parseSpecFunction) *ast.GenDecl {\
 	if p.trace {
 		defer un(trace(p, "GenDecl("+keyword.String()+")"))
 	}
@@ -2185,12 +2155,12 @@ func (p *parser) parseGenDecl(keyword token.Token, f parseSpecFunction) *ast.Gen
 		tlparen = p.pos
 		p.next()
 		for iota := 0; p.tok != token.RPAREN && p.tok != token.EOF; iota++ {
-\t\t\tlist = append(list, f(p, p.leadComment, iota))\
+\t\t\tlist = append(list, f(p, p.leadComment, keyword, iota))\
 		}
 		trparen = p.expect(token.RPAREN)
 		p.expectSemi()
 	} else {
-\t\tlist = append(list, f(p, nil, 0))\
+\t\tlist = append(list, f(p, nil, keyword, 0))\
 	}
 
 	return &ast.GenDecl{
@@ -2290,14 +2260,11 @@ func (p *parser) parseDecl(sync func(*parser)) ast.Decl {
 
 	var f parseSpecFunction
 	switch p.tok {
-\tcase token.CONST:\
-\t\tf = parseConstSpec
+\tcase token.CONST, token.VAR:\
+\t\tf = (*parser).parseValueSpec
 
 	case token.TYPE:\
-\t\tf = parseTypeSpec
-\n-\tcase token.VAR:\
-\t\tf = parseVarSpec
+\t\tf = (*parser).parseTypeSpec
 
 	case token.FUNC:
 		return p.parseFuncDecl()
@@ -2349,7 +2316,7 @@ func (p *parser) parseFile() *ast.File {
 	if p.mode&PackageClauseOnly == 0 {
 		// import decls
 		for p.tok == token.IMPORT {
-\t\t\tdecls = append(decls, p.parseGenDecl(token.IMPORT, parseImportSpec))\
+\t\t\tdecls = append(decls, p.parseGenDecl(token.IMPORT, (*parser).parseImportSpec))\
 		}
 
 		if p.mode&ImportsOnly == 0 {

コアとなるコードの解説

上記の差分における主要な変更点とそれぞれの解説は以下の通りです。

  1. parseSpecFunction 型の変更:

    -type parseSpecFunction func(p *parser, doc *ast.CommentGroup, iota int) ast.Spec
    +type parseSpecFunction func(p *parser, doc *ast.CommentGroup, keyword token.Token, iota int) ast.Spec
    
    • parseSpecFunctionは、一般的な宣言(import, const, type, var)の仕様を解析する関数のシグネチャを定義しています。
    • 変更前はiota intのみを受け取っていましたが、変更後はkeyword token.Tokenが追加されました。これにより、この型の関数は、現在解析している宣言がconstvarimporttypeのいずれであるかを示すキーワードトークンを受け取れるようになり、単一の関数で複数の種類の宣言を処理するための柔軟性が増しました。
  2. parseImportSpec のメソッド化:

    -func parseImportSpec(p *parser, doc *ast.CommentGroup, _ int) ast.Spec {
    +func (p *parser) parseImportSpec(doc *ast.CommentGroup, _ token.Token, _ int) ast.Spec {
    
    • parseImportSpec関数が、parser構造体のメソッド(レシーバp *parserを持つ関数)に変更されました。
    • これにより、parserインスタンスの内部状態に直接アクセスできるようになり、コードの構造がよりオブジェクト指向的になりました。
    • また、parseSpecFunctionの新しいシグネチャに合わせるため、未使用のtoken.Token引数(_ token.Token)が追加されています。
  3. parseConstSpecparseValueSpec へのリネームと統合:

    -func parseConstSpec(p *parser, doc *ast.CommentGroup, iota int) ast.Spec {
    +func (p *parser) parseValueSpec(doc *ast.CommentGroup, keyword token.Token, iota int) ast.Spec {
    
    • parseConstSpec関数がparseValueSpecにリネームされ、*parserのメソッドになりました。
    • この関数は、constvarの両方の宣言を解析する共通のロジックを担うことになります。
  4. トレースメッセージの動的化:

    -		defer un(trace(p, "ConstSpec"))
    +		defer un(trace(p, keyword.String()+"Spec"))
    
    • パーサーのトレース出力(デバッグ情報)が、keyword.String()+"Spec"という形式に変更されました。
    • これにより、const宣言の場合はConstSpecvar宣言の場合はVarSpecといったように、動的に適切なトレースメッセージが表示されるようになります。
  5. 初期値解析の条件ロジックの統合:

    -	if typ != nil || p.tok == token.ASSIGN || iota == 0 {
    +	if p.tok == token.ASSIGN || keyword == token.CONST && (typ != nil || iota == 0) || keyword == token.VAR && typ == nil {
    
    • これがconstvarの解析ロジックを統合する最も重要な部分です。
    • p.tok == token.ASSIGN: 現在のトークンが=(代入演算子)である場合、初期値が明示的に指定されているため、values(初期値のリスト)を解析します。これはconstvarに共通です。
    • keyword == token.CONST && (typ != nil || iota == 0): 現在の宣言がconstである場合、以下のいずれかの条件を満たせば初期値を解析します。
      • typ != nil: 型が明示的に指定されている場合。
      • iota == 0: iota0の場合(constブロックの最初の定数)。iotaは暗黙的に0から始まるため、値がなくても初期値として0が割り当てられる可能性があります。
    • keyword == token.VAR && typ == nil: 現在の宣言がvarである場合、型が省略されている(typ == nil)場合は初期値を解析します。Goでは、var x = 10のように型を省略すると初期値から型が推論されます。
    • この統合された条件式により、constvarの異なる初期値の規則が単一のparseValueSpec関数内で正確に処理されます。
  6. parseTypeSpec のメソッド化:

    -func parseTypeSpec(p *parser, doc *ast.CommentGroup, _ int) ast.Spec {
    +func (p *parser) parseTypeSpec(doc *ast.CommentGroup, _ token.Token, _ int) ast.Spec {
    
    • parseTypeSpec*parserのメソッドに変更され、parseImportSpecと同様にtoken.Token引数が追加されました。
  7. parseVarSpec の削除:

    -func parseVarSpec(p *parser, doc *ast.CommentGroup, _ int) ast.Spec {
    -... (約30行のコードが削除) ...
    
    • parseVarSpec関数は、そのロジックがparseValueSpecに統合されたため、完全に削除されました。これにより、コードの重複が解消され、コードベースが簡素化されました。
  8. parseGenDecl での keyword 引数の追加:

    -			list = append(list, f(p, p.leadComment, iota))
    +			list = append(list, f(p, p.leadComment, keyword, iota))
    ...
    -		list = append(list, f(p, nil, 0))
    +		list = append(list, f(p, nil, keyword, 0))
    
    • parseGenDecl関数は、parseSpecFunction型の引数fを呼び出す際に、新しく追加されたkeyword引数(token.CONST, token.VAR, token.IMPORT, token.TYPEなど)を渡すように変更されました。これにより、parseValueSpecなどの統合された関数が、どの種類の宣言を解析しているかを識別できるようになります。
  9. parseDecl および parseFile での関数ポインタの更新:

    -	case token.CONST:
    -		f = parseConstSpec
    +	case token.CONST, token.VAR:
    +		f = (*parser).parseValueSpec
    
     	case token.TYPE:
    -		f = parseTypeSpec
    -
    -	case token.VAR:
    -		f = parseVarSpec
    +		f = (*parser).parseTypeSpec
    ...
    -		decls = append(decls, p.parseGenDecl(token.IMPORT, parseImportSpec))
    +		decls = append(decls, p.parseGenDecl(token.IMPORT, (*parser).parseImportSpec))
    
    • parseDecl関数内のswitch文で、token.CONSTtoken.VARの両方が新しい(*parser).parseValueSpecメソッドを指すように変更されました。
    • token.TYPE(*parser).parseTypeSpecを指すようになりました。
    • parseFile関数内のtoken.IMPORT(*parser).parseImportSpecを指すようになりました。
    • これらの変更により、パーサー全体で新しい統合された解析ロジックが適切に呼び出されるようになります。

これらの変更は、Go言語のパーサーの内部実装を大幅に改善し、コードの重複を排除し、よりクリーンで保守性の高いコードベースを実現しています。

関連リンク

参考にした情報源リンク