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

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

このコミットは、Go言語のパーサー(go/parser)におけるエラー同期メカニズムの改善を目的としています。具体的には、構文解析中にカンマ(,)が欠落している場合に、より適切にエラーを検出し、回復するためのロジックが修正されています。特に、改行の前にカンマが欠落しているケースに対するハンドリングが強化されています。

コミット

commit 9e8e4a9313f8bff5c40d82166818f98b4a8cc9ed
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Mar 7 10:19:32 2012 -0800

    go/parser: better error sync. if commas are missing
    
    This time for sure.
    Runs all tests.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5752060

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

https://github.com/golang/go/commit/9e8e4a9313f8bff5c40d82166818f98b4a8cc9ed

元コミット内容

go/parser: カンマが欠落している場合のエラー同期を改善。 今度こそは。 全てのテストを実行。

変更の背景

Go言語のコンパイラやツールチェーンにおいて、ソースコードの構文解析(パース)は非常に重要なフェーズです。パーサーは、与えられたGoのソースコードが言語仕様に準拠しているかを検証し、抽象構文木(AST)を構築します。この過程で構文エラーが発生した場合、パーサーはエラーを報告し、可能であれば残りのコードの解析を続行(エラー回復)して、できるだけ多くの診断情報を提供することが望ましいとされます。

このコミットの背景には、Goのパーサーがカンマの欠落、特に改行の直前でカンマが欠落している場合に、エラー回復が不十分であったという問題があったと考えられます。例えば、構造体のフィールドリストや関数の引数リストなどでカンマが欠落していると、パーサーがその後の構文を正しく解釈できなくなり、連鎖的なエラー(カスケードエラー)を引き起こす可能性がありました。

コミットメッセージの「This time for sure.」という表現から、以前にも同様の問題に対する修正が試みられたが、完全には解決していなかったことが示唆されます。今回の変更は、この特定のエラーシナリオにおけるパーサーの堅牢性を高め、より正確なエラー報告と回復を可能にすることを目的としています。

前提知識の解説

go/parser パッケージ

go/parser パッケージは、Go言語のソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を生成するためのパッケージです。Goのコンパイラ、go fmtgo vet などのツールで利用されています。パーサーは、字句解析器(lexer)からトークンストリームを受け取り、Go言語の文法規則に従って構文を検証し、ASTを構築します。

トークン (token.COMMA, token.SEMICOLON)

字句解析器は、ソースコードを意味のある最小単位である「トークン」に分割します。

  • token.COMMA: カンマ(,)を表すトークンです。Go言語では、変数リスト、引数リスト、構造体リテラルなどで要素の区切りとして使用されます。
  • token.SEMICOLON: セミコロン(;)を表すトークンです。Go言語では、文の区切りとして使用されますが、多くの場合、改行によって自動的に挿入(ASI: Automatic Semicolon Insertion)されます。

抽象構文木(AST)と ast.Expr

ASTは、ソースコードの構造を木構造で表現したものです。go/ast パッケージで定義されています。

  • ast.Expr: ASTノードの一種で、式(expression)を表すインターフェースです。変数、リテラル、関数呼び出しなどがこれに該当します。

parser 構造体とメソッド

go/parser パッケージの内部では、parser 構造体が構文解析の状態を管理します。

  • p.tok: 現在処理中のトークンの種類を示します。
  • p.lit: 現在処理中のトークンのリテラル値(文字列)を示します。例えば、token.IDENT であれば識別子の名前、token.STRING であれば文字列リテラルの内容など。
  • p.pos: 現在処理中のトークンのソースコード上の位置(行番号、列番号など)を示します。
  • p.error(pos, msg): 指定された位置 pos でエラーメッセージ msg を報告するためのメソッドです。
  • p.next(): 次のトークンを読み込み、p.tokp.lit を更新するメソッドです。
  • p.expectSemi(): セミコロンを期待するメソッドです。GoのASIルールを考慮して、改行や特定のトークンの後にセミコロンがなくても許容するロジックが含まれます。

エラー回復 (Error Recovery)

構文解析中にエラーが発生した場合、パーサーは通常、そのエラーを報告し、解析を停止するか、あるいはエラーを乗り越えて解析を続行しようとします。後者のプロセスがエラー回復です。エラー回復の目的は、単一の構文エラーによって後続の有効な構文が全てエラーとして報告される「カスケードエラー」を防ぎ、開発者にできるだけ多くの有用な診断情報を提供することです。

技術的詳細

このコミットの主要な変更点は、parser.go ファイル内の seesComma メソッドのロファクタリングと、それに伴うカンマ欠落時のエラー同期ロジックの改善です。

seesComma から atComma へのリネームとロジック変更

元の seesComma メソッドは、現在のトークンが token.COMMA であるか、または token.SEMICOLON であり、かつそのリテラルが改行(\n)である場合に true を返していました。後者のケースでは、改行の前にカンマが欠落していると判断し、エラーを報告しつつも true を返すことで、パーサーがカンマが存在するかのように振る舞い、解析を続行できるようにしていました。これは、エラー回復の一種です。

変更後、メソッド名は atComma に変更されました。そして、重要な変更として、seesComma メソッド内にコメントアウトされていたブロック(/* ... */)が削除され、そのロジックが直接 atComma メソッドの本体に組み込まれました。

// 変更前 (seesComma)
func (p *parser) seesComma(context string) bool {
	if p.tok == token.COMMA {
		return true
	}
	/*
	if p.tok == token.SEMICOLON && p.lit == "\n" {
		p.error(p.pos, "missing ',' before newline in "+context)
		return true // "insert" the comma and continue
	}
	*/
	return false
}

// 変更後 (atComma)
func (p *parser) atComma(context string) bool {
	if p.tok == token.COMMA {
		return true
	}
	if p.tok == token.SEMICOLON && p.lit == "\n" {
		p.error(p.pos, "missing ',' before newline in "+context)
		return true // "insert" the comma and continue
	}
	return false
}

この変更により、atComma メソッドは、現在のトークンがカンマであるか、または改行による自動セミコロン挿入の対象となるセミコロンである場合に、カンマが存在すると見なすようになりました。これにより、カンマが欠落しているが改行がある場合に、パーサーがより早くエラーを検出し、かつ解析を継続できるようになったと考えられます。

p.tok != token.COMMA への変更

parseVarList, parseParameterList, parseCallOrConversion, parseElementList といった、リスト構造を解析する複数のメソッドにおいて、ループの継続条件が !p.seesComma(...) から p.tok != token.COMMA に変更されました。

これは非常に重要な変更です。

  • 変更前 (!p.seesComma(...)): seesCommatrue を返す(つまり、実際のカンマがあるか、改行による擬似カンマがある)限りループが継続していました。
  • 変更後 (p.tok != token.COMMA): ループは実際の token.COMMA が存在しない場合にのみ終了します。

一見すると、エラー回復のロジックが弱くなったように見えますが、そうではありません。atComma メソッド自体が、改行によるカンマ欠落を検出してエラーを報告しつつ true を返すようになったため、p.tok != token.COMMA という条件と組み合わせることで、より正確なエラー同期が可能になります。

具体的には、atCommatrue を返しても、p.toktoken.COMMA でなければループは終了します。しかし、atComma の内部で p.error が呼び出されているため、エラーは適切に報告されます。これにより、パーサーはカンマが欠落していることを認識しつつ、その後のトークンに基づいて次の構文要素の解析を試みることができます。これは、エラー発生時にパーサーが「どこまで読み飛ばすべきか」を判断する上で非常に重要です。

この変更は、パーサーが構文エラーに遭遇した際に、不必要なトークンを読み飛ばしすぎることなく、かつ解析を継続できるような、より洗練されたエラー回復戦略を実装するためのものです。

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

--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -376,17 +376,15 @@ func (p *parser) expectSemi() {
 	}
 }
 
-func (p *parser) seesComma(context string) bool {
+func (p *parser) atComma(context string) bool {
 	if p.tok == token.COMMA {
 		return true
 	}
-	/*
-	if p.tok == token.SEMICOLON && p.lit == "\n" {
-		p.error(p.pos, "missing ',' before newline in "+context)
-		return true // "insert" the comma and continue
-
-	}
-	*/
+	if p.tok == token.SEMICOLON && p.lit == "\n" {
+		p.error(p.pos, "missing ',' before newline in "+context)
+		return true // "insert" the comma and continue
+
+	}
 	return false
 }
 
@@ -661,7 +659,7 @@ func (p *parser) parseVarList(isParam bool) (list []ast.Expr, typ ast.Expr) {
 	// accept them all for more robust parsing and complain later
 	for typ := p.parseVarType(isParam); typ != nil; {
 		list = append(list, typ)
-	if !p.seesComma("variable list") {
+	if p.tok != token.COMMA {
 			break
 		}
 		p.next()
@@ -702,7 +700,7 @@ func (p *parser) parseParameterList(scope *ast.Scope, ellipsisOk bool) (params [
 			// Go spec: The scope of an identifier denoting a function
 			// parameter or result variable is the function body.
 			p.declare(field, nil, scope, ast.Var, idents...)
-	if !p.seesComma("parameter list") {
+	if !p.atComma("parameter list") {
 				break
 			}
 			p.next()
@@ -1092,7 +1090,7 @@ func (p *parser) parseCallOrConversion(fun ast.Expr) *ast.CallExpr {
 		ellipsis = p.pos
 		p.next()
 	}
-	if !p.seesComma("argument list") {
+	if !p.atComma("argument list") {
 			break
 		}
 		p.next()
@@ -1132,7 +1130,7 @@ func (p *parser) parseElementList() (list []ast.Expr) {
 
 	for p.tok != token.RBRACE && p.tok != token.EOF {
 		list = append(list, p.parseElement(true))
-	if !p.seesComma("composite literal") {
+	if !p.atComma("composite literal") {
 			break
 		}
 		p.next()

コアとなるコードの解説

func (p *parser) seesComma(context string) bool から func (p *parser) atComma(context string) bool への変更

  • メソッド名の変更: seesComma から atComma へと変更されました。これは、メソッドの意図をより明確にするためのリファクタリングです。seesComma は「カンマが見えるか」というニュアンスでしたが、atComma は「現在カンマの位置にいるか(またはカンマと見なせる状況か)」という、より現在のパーサーの状態に焦点を当てた表現になっています。
  • コメントアウトされたコードの統合: 以前 seesComma 内でコメントアウトされていた、p.tok == token.SEMICOLON && p.lit == "\n" の条件でエラーを報告しつつ true を返すロジックが、atComma の本体に直接組み込まれました。これにより、改行の前にカンマが欠落している場合に、パーサーがその場でエラーを報告し、「カンマが挿入された」かのように振る舞って解析を続行できるようになります。これは、エラー回復の重要な部分です。

parseVarList メソッド内の変更

-	if !p.seesComma("variable list") {
+	if p.tok != token.COMMA {
 		break
 	}

parseVarList は変数宣言リストを解析するメソッドです。ループの継続条件が !p.seesComma("variable list") から p.tok != token.COMMA に変更されました。

  • 変更前: seesCommatrue を返す限り(つまり、実際のカンマがあるか、改行による擬似カンマがある限り)ループが継続していました。
  • 変更後: ループは実際の token.COMMA が存在しない場合にのみ終了します。これにより、atComma が改行によるカンマ欠落を検出してエラーを報告した場合でも、p.toktoken.COMMA でなければループは終了し、パーサーは次の構文要素の解析に進むことができます。これは、エラー発生時にパーサーが不必要なトークンを読み飛ばしすぎることなく、かつ解析を継続できるような、より洗練されたエラー回復戦略を実装するためのものです。

parseParameterList, parseCallOrConversion, parseElementList メソッド内の変更

これらのメソッドも同様に、リスト構造(パラメータリスト、引数リスト、複合リテラルの要素リスト)を解析する部分で、ループの継続条件が !p.seesComma(...) から !p.atComma(...) または p.tok != token.COMMA に変更されています。

  • parseParameterList:

    -	if !p.seesComma("parameter list") {
    +	if !p.atComma("parameter list") {
    			break
    		}
    

    ここでは !p.atComma に変更されています。atComma がエラーを報告しつつ true を返す場合、!p.atCommafalse となり、ループは継続します。これは、パラメータリストの解析において、カンマが欠落していても、その後のパラメータを解析し続けようとする意図があることを示唆しています。

  • parseCallOrConversion:

    -	if !p.seesComma("argument list") {
    +	if !p.atComma("argument list") {
    			break
    		}
    

    関数呼び出しや型変換の引数リストの解析です。ここも !p.atComma に変更されています。

  • parseElementList:

    -	if !p.seesComma("composite literal") {
    +	if !p.atComma("composite literal") {
    			break
    		}
    

    複合リテラル(配列、スライス、マップ、構造体リテラル)の要素リストの解析です。ここも !p.atComma に変更されています。

これらの変更は、Goのパーサーが、カンマの欠落という一般的な構文エラーに対して、より堅牢かつ柔軟に対応できるようにするためのものです。エラーを適切に報告しつつ、可能な限り解析を続行することで、開発者にとってより有用なエラーメッセージを提供し、コンパイルプロセス全体の効率を向上させます。

関連リンク

  • Go言語のパーサーに関する公式ドキュメントやチュートリアルは、Goの公式ウェブサイトやGoのソースコードリポジトリで確認できます。
  • Go言語の構文解析に関する詳細な情報は、go/parser および go/ast パッケージのドキュメントを参照してください。

参考にした情報源リンク

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

このコミットは、Go言語のパーサー(go/parser)におけるエラー同期メカニズムの改善を目的としています。具体的には、構文解析中にカンマ(,)が欠落している場合に、より適切にエラーを検出し、回復するためのロジックが修正されています。特に、改行の前にカンマが欠落しているケースに対するハンドリングが強化されています。

コミット

commit 9e8e4a9313f8bff5c40d82166818f98b4a8cc9ed
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Mar 7 10:19:32 2012 -0800

    go/parser: better error sync. if commas are missing
    
    This time for sure.
    Runs all tests.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5752060

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

https://github.com/golang/go/commit/9e8e4a9313f8bff5c40d82166818f98b4a8cc9ed

元コミット内容

go/parser: カンマが欠落している場合のエラー同期を改善。 今度こそは。 全てのテストを実行。

変更の背景

Go言語のコンパイラやツールチェーンにおいて、ソースコードの構文解析(パース)は非常に重要なフェーズです。パーサーは、与えられたGoのソースコードが言語仕様に準拠しているかを検証し、抽象構文木(AST)を構築します。この過程で構文エラーが発生した場合、パーサーはエラーを報告し、可能であれば残りのコードの解析を続行(エラー回復)して、できるだけ多くの診断情報を提供することが望ましいとされます。

このコミットの背景には、Goのパーサーがカンマの欠落、特に改行の直前でカンマが欠落している場合に、エラー回復が不十分であったという問題があったと考えられます。例えば、構造体のフィールドリストや関数の引数リストなどでカンマが欠落していると、パーサーがその後の構文を正しく解釈できなくなり、連鎖的なエラー(カスケードエラー)を引き起こす可能性がありました。

コミットメッセージの「This time for sure.」という表現から、以前にも同様の問題に対する修正が試みられたが、完全には解決していなかったことが示唆されています。今回の変更は、この特定のエラーシナリオにおけるパーサーの堅牢性を高め、より正確なエラー報告と回復を可能にすることを目的としています。

前提知識の解説

go/parser パッケージ

go/parser パッケージは、Go言語のソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を生成するためのパッケージです。Goのコンパイラ、go fmtgo vet などのツールで利用されています。パーサーは、字句解析器(lexer)からトークンストリームを受け取り、Go言語の文法規則に従って構文を検証し、ASTを構築します。

トークン (token.COMMA, token.SEMICOLON)

字句解析器は、ソースコードを意味のある最小単位である「トークン」に分割します。

  • token.COMMA: カンマ(,)を表すトークンです。Go言語では、変数リスト、引数リスト、構造体リテラルなどで要素の区切りとして使用されます。
  • token.SEMICOLON: セミコロン(;)を表すトークンです。Go言語では、文の区切りとして使用されますが、多くの場合、改行によって自動的に挿入(ASI: Automatic Semicolon Insertion)されます。

抽象構文木(AST)と ast.Expr

ASTは、ソースコードの構造を木構造で表現したものです。go/ast パッケージで定義されています。

  • ast.Expr: ASTノードの一種で、式(expression)を表すインターフェースです。変数、リテラル、関数呼び出しなどがこれに該当します。

parser 構造体とメソッド

go/parser パッケージの内部では、parser 構造体が構文解析の状態を管理します。

  • p.tok: 現在処理中のトークンの種類を示します。
  • p.lit: 現在処理中のトークンのリテラル値(文字列)を示します。例えば、token.IDENT であれば識別子の名前、token.STRING であれば文字列リテラルの内容など。
  • p.pos: 現在処理中のトークンのソースコード上の位置(行番号、列番号など)を示します。
  • p.error(pos, msg): 指定された位置 pos でエラーメッセージ msg を報告するためのメソッドです。
  • p.next(): 次のトークンを読み込み、p.tokp.lit を更新するメソッドです。
  • p.expectSemi(): セミコロンを期待するメソッドです。GoのASIルールを考慮して、改行や特定のトークンの後にセミコロンがなくても許容するロジックが含まれます。

エラー回復 (Error Recovery)

構文解析中にエラーが発生した場合、パーサーは通常、そのエラーを報告し、解析を停止するか、あるいはエラーを乗り越えて解析を続行しようとします。後者のプロセスがエラー回復です。エラー回復の目的は、単一の構文エラーによって後続の有効な構文が全てエラーとして報告される「カスケードエラー」を防ぎ、開発者にできるだけ多くの有用な診断情報を提供することです。

技術的詳細

このコミットの主要な変更点は、parser.go ファイル内の seesComma メソッドのロファクタリングと、それに伴うカンマ欠落時のエラー同期ロジックの改善です。

seesComma から atComma へのリネームとロジック変更

元の seesComma メソッドは、現在のトークンが token.COMMA であるか、または token.SEMICOLON であり、かつそのリテラルが改行(\n)である場合に true を返していました。後者のケースでは、改行の前にカンマが欠落していると判断し、エラーを報告しつつも true を返すことで、パーサーがカンマが存在するかのように振る舞い、解析を続行できるようにしていました。これは、エラー回復の一種です。

変更後、メソッド名は atComma に変更されました。そして、重要な変更として、seesComma メソッド内にコメントアウトされていたブロック(/* ... */)が削除され、そのロジックが直接 atComma メソッドの本体に組み込まれました。

// 変更前 (seesComma)
func (p *parser) seesComma(context string) bool {
	if p.tok == token.COMMA {
		return true
	}
	/*
	if p.tok == token.SEMICOLON && p.lit == "\n" {
		p.error(p.pos, "missing ',' before newline in "+context)
		return true // "insert" the comma and continue
	}
	*/
	return false
}

// 変更後 (atComma)
func (p *parser) atComma(context string) bool {
	if p.tok == token.COMMA {
		return true
	}
	if p.tok == token.SEMICOLON && p.lit == "\n" {
		p.error(p.pos, "missing ',' before newline in "+context)
		return true // "insert" the comma and continue
	}
	return false
}

この変更により、atComma メソッドは、現在のトークンがカンマであるか、または改行による自動セミコロン挿入の対象となるセミコロンである場合に、カンマが存在すると見なすようになりました。これにより、カンマが欠落しているが改行がある場合に、パーサーがより早くエラーを検出し、かつ解析を継続できるようになったと考えられます。

p.tok != token.COMMA への変更

parseVarList, parseParameterList, parseCallOrConversion, parseElementList といった、リスト構造を解析する複数のメソッドにおいて、ループの継続条件が !p.seesComma(...) から p.tok != token.COMMA に変更されました。

これは非常に重要な変更です。

  • 変更前 (!p.seesComma(...)): seesCommatrue を返す(つまり、実際のカンマがあるか、改行による擬似カンマがある)限りループが継続していました。
  • 変更後 (p.tok != token.COMMA): ループは実際の token.COMMA が存在しない場合にのみ終了します。

一見すると、エラー回復のロジックが弱くなったように見えますが、そうではありません。atComma メソッド自体が、改行によるカンマ欠落を検出してエラーを報告しつつ true を返すようになったため、p.tok != token.COMMA という条件と組み合わせることで、より正確なエラー同期が可能になります。

具体的には、atCommatrue を返しても、p.toktoken.COMMA でなければループは終了します。しかし、atComma の内部で p.error が呼び出されているため、エラーは適切に報告されます。これにより、パーサーはカンマが欠落していることを認識しつつ、その後のトークンに基づいて次の構文要素の解析を試みることができます。これは、エラー発生時にパーサーが「どこまで読み飛ばすべきか」を判断する上で非常に重要です。

この変更は、パーサーが構文エラーに遭遇した際に、不必要なトークンを読み飛ばしすぎることなく、かつ解析を継続できるような、より洗練されたエラー回復戦略を実装するためのものです。

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

--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -376,17 +376,15 @@ func (p *parser) expectSemi() {
 	}
 }
 
-func (p *parser) seesComma(context string) bool {
+func (p *parser) atComma(context string) bool {
 	if p.tok == token.COMMA {
 		return true
 	}
-	/*
-	if p.tok == token.SEMICOLON && p.lit == "\n" {
-		p.error(p.pos, "missing ',' before newline in "+context)
-		return true // "insert" the comma and continue
-
-	}
-	*/
+	if p.tok == token.SEMICOLON && p.lit == "\n" {
+		p.error(p.pos, "missing ',' before newline in "+context)
+		return true // "insert" the comma and continue
+
+	}
 	return false
 }
 
@@ -661,7 +659,7 @@ func (p *parser) parseVarList(isParam bool) (list []ast.Expr, typ ast.Expr) {
 	// accept them all for more robust parsing and complain later
 	for typ := p.parseVarType(isParam); typ != nil; {
 		list = append(list, typ)
-	if !p.seesComma("variable list") {
+	if p.tok != token.COMMA {
 			break
 		}
 		p.next()
@@ -702,7 +700,7 @@ func (p *parser) parseParameterList(scope *ast.Scope, ellipsisOk bool) (params [
 			// Go spec: The scope of an identifier denoting a function
 			// parameter or result variable is the function body.
 			p.declare(field, nil, scope, ast.Var, idents...)\n-	if !p.seesComma("parameter list") {
    +	if !p.atComma("parameter list") {
 				break
 			}
 			p.next()
@@ -1092,7 +1090,7 @@ func (p *parser) parseCallOrConversion(fun ast.Expr) *ast.CallExpr {\n 		ellipsis = p.pos
 		p.next()
 	}
-	if !p.seesComma("argument list") {
+	if !p.atComma("argument list") {
 			break
 		}
 		p.next()
@@ -1132,7 +1130,7 @@ func (p *parser) parseElementList() (list []ast.Expr) {\n 
 	for p.tok != token.RBRACE && p.tok != token.EOF {
 		list = append(list, p.parseElement(true))
-	if !p.seesComma("composite literal") {
+	if !p.atComma("composite literal") {
 			break
 		}
 		p.next()

コアとなるコードの解説

func (p *parser) seesComma(context string) bool から func (p *parser) atComma(context string) bool への変更

  • メソッド名の変更: seesComma から atComma へと変更されました。これは、メソッドの意図をより明確にするためのリファクタリングです。seesComma は「カンマが見えるか」というニュアンスでしたが、atComma は「現在カンマの位置にいるか(またはカンマと見なせる状況か)」という、より現在のパーサーの状態に焦点を当てた表現になっています。
  • コメントアウトされたコードの統合: 以前 seesComma 内でコメントアウトされていた、p.tok == token.SEMICOLON && p.lit == "\n" の条件でエラーを報告しつつ true を返すロジックが、atComma の本体に直接組み込まれました。これにより、改行の前にカンマが欠落している場合に、パーサーがその場でエラーを報告し、「カンマが挿入された」かのように振る舞って解析を続行できるようになります。これは、エラー回復の重要な部分です。

parseVarList メソッド内の変更

-	if !p.seesComma("variable list") {
+	if p.tok != token.COMMA {
 		break
 	}

parseVarList は変数宣言リストを解析するメソッドです。ループの継続条件が !p.seesComma("variable list") から p.tok != token.COMMA に変更されました。

  • 変更前: seesCommatrue を返す限り(つまり、実際のカンマがあるか、改行による擬似カンマがある限り)ループが継続していました。
  • 変更後: ループは実際の token.COMMA が存在しない場合にのみ終了します。これにより、atComma が改行によるカンマ欠落を検出してエラーを報告した場合でも、p.toktoken.COMMA でなければループは終了し、パーサーは次の構文要素の解析に進むことができます。これは、エラー発生時にパーサーが不必要なトークンを読み飛ばしすぎることなく、かつ解析を継続できるような、より洗練されたエラー回復戦略を実装するためのものです。

parseParameterList, parseCallOrConversion, parseElementList メソッド内の変更

これらのメソッドも同様に、リスト構造(パラメータリスト、引数リスト、複合リテラルの要素リスト)を解析する部分で、ループの継続条件が !p.seesComma(...) から !p.atComma(...) または p.tok != token.COMMA に変更されています。

  • parseParameterList:

    -	if !p.seesComma("parameter list") {
    +	if !p.atComma("parameter list") {
    			break
    		}
    

    ここでは !p.atComma に変更されています。atComma がエラーを報告しつつ true を返す場合、!p.atCommafalse となり、ループは継続します。これは、パラメータリストの解析において、カンマが欠落していても、その後のパラメータを解析し続けようとする意図があることを示唆しています。

  • parseCallOrConversion:

    -	if !p.seesComma("argument list") {
    +	if !p.atComma("argument list") {
    			break
    		}
    

    関数呼び出しや型変換の引数リストの解析です。ここも !p.atComma に変更されています。

  • parseElementList:

    -	if !p.seesComma("composite literal") {
    +	if !p.atComma("composite literal") {
    			break
    		}
    

    複合リテラル(配列、スライス、マップ、構造体リテラル)の要素リストの解析です。ここも !p.atComma に変更されています。

これらの変更は、Goのパーサーが、カンマの欠落という一般的な構文エラーに対して、より堅牢かつ柔軟に対応できるようにするためのものです。エラーを適切に報告しつつ、可能な限り解析を続行することで、開発者にとってより有用なエラーメッセージを提供し、コンパイルプロセス全体の効率を向上させます。

関連リンク

  • Go言語のパーサーに関する公式ドキュメントやチュートリアルは、Goの公式ウェブサイトやGoのソースコードリポジトリで確認できます。
  • Go言語の構文解析に関する詳細な情報は、go/parser および go/ast パッケージのドキュメントを参照してください。

参考にした情報源リンク

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

このコミットは、Go言語のパーサー(go/parser)におけるエラー同期メカニズムの改善を目的としています。具体的には、構文解析中にカンマ(,)が欠落している場合に、より適切にエラーを検出し、回復するためのロジックが修正されています。特に、改行の前にカンマが欠落しているケースに対するハンドリングが強化されています。

コミット

commit 9e8e4a9313f8bff5c40d82166818f98b4a8cc9ed
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Mar 7 10:19:32 2012 -0800

    go/parser: better error sync. if commas are missing
    
    This time for sure.
    Runs all tests.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5752060

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

https://github.com/golang/go/commit/9e8e4a9313f8bff5c40d82166818f98b4a8cc9ed

元コミット内容

go/parser: カンマが欠落している場合のエラー同期を改善。 今度こそは。 全てのテストを実行。

変更の背景

Go言語のコンパイラやツールチェーンにおいて、ソースコードの構文解析(パース)は非常に重要なフェーズです。パーサーは、与えられたGoのソースコードが言語仕様に準拠しているかを検証し、抽象構文木(AST)を構築します。この過程で構文エラーが発生した場合、パーサーはエラーを報告し、可能であれば残りのコードの解析を続行(エラー回復)して、できるだけ多くの診断情報を提供することが望ましいとされます。

このコミットの背景には、Goのパーサーがカンマの欠落、特に改行の直前でカンマが欠落している場合に、エラー回復が不十分であったという問題があったと考えられます。例えば、構造体のフィールドリストや関数の引数リストなどでカンマが欠落していると、パーサーがその後の構文を正しく解釈できなくなり、連鎖的なエラー(カスケードエラー)を引き起こす可能性がありました。

コミットメッセージの「This time for sure.」という表現から、以前にも同様の問題に対する修正が試みられたが、完全には解決していなかったことが示唆されています。今回の変更は、この特定のエラーシナリオにおけるパーサーの堅牢性を高め、より正確なエラー報告と回復を可能にすることを目的としています。

前提知識の解説

go/parser パッケージ

go/parser パッケージは、Go言語のソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を生成するためのパッケージです。Goのコンパイラ、go fmtgo vet などのツールで利用されています。パーサーは、字句解析器(lexer)からトークンストリームを受け取り、Go言語の文法規則に従って構文を検証し、ASTを構築します。

トークン (token.COMMA, token.SEMICOLON)

字句解析器は、ソースコードを意味のある最小単位である「トークン」に分割します。

  • token.COMMA: カンマ(,)を表すトークンです。Go言語では、変数リスト、引数リスト、構造体リテラルなどで要素の区切りとして使用されます。
  • token.SEMICOLON: セミコロン(;)を表すトークンです。Go言語では、文の区切りとして使用されますが、多くの場合、改行によって自動的に挿入(ASI: Automatic Semicolon Insertion)されます。

抽象構文木(AST)と ast.Expr

ASTは、ソースコードの構造を木構造で表現したものです。go/ast パッケージで定義されています。

  • ast.Expr: ASTノードの一種で、式(expression)を表すインターフェースです。変数、リテラル、関数呼び出しなどがこれに該当します。

parser 構造体とメソッド

go/parser パッケージの内部では、parser 構造体が構文解析の状態を管理します。

  • p.tok: 現在処理中のトークンの種類を示します。
  • p.lit: 現在処理中のトークンのリテラル値(文字列)を示します。例えば、token.IDENT であれば識別子の名前、token.STRING であれば文字列リテラルの内容など。
  • p.pos: 現在処理中のトークンのソースコード上の位置(行番号、列番号など)を示します。
  • p.error(pos, msg): 指定された位置 pos でエラーメッセージ msg を報告するためのメソッドです。
  • p.next(): 次のトークンを読み込み、p.tokp.lit を更新するメソッドです。
  • p.expectSemi(): セミコロンを期待するメソッドです。GoのASIルールを考慮して、改行や特定のトークンの後にセミコロンがなくても許容するロジックが含まれます。

エラー回復 (Error Recovery)

構文解析中にエラーが発生した場合、パーサーは通常、そのエラーを報告し、解析を停止するか、あるいはエラーを乗り越えて解析を続行しようとします。後者のプロセスがエラー回復です。エラー回復の目的は、単一の構文エラーによって後続の有効な構文が全てエラーとして報告される「カスケードエラー」を防ぎ、開発者にできるだけ多くの有用な診断情報を提供することです。

技術的詳細

このコミットの主要な変更点は、parser.go ファイル内の seesComma メソッドのロファクタリングと、それに伴うカンマ欠落時のエラー同期ロジックの改善です。

seesComma から atComma へのリネームとロジック変更

元の seesComma メソッドは、現在のトークンが token.COMMA であるか、または token.SEMICOLON であり、かつそのリテラルが改行(\n)である場合に true を返していました。後者のケースでは、改行の前にカンマが欠落していると判断し、エラーを報告しつつも true を返すことで、パーサーがカンマが存在するかのように振る舞い、解析を続行できるようにしていました。これは、エラー回復の一種です。

変更後、メソッド名は atComma に変更されました。そして、重要な変更として、seesComma メソッド内にコメントアウトされていたブロック(/* ... */)が削除され、そのロジックが直接 atComma メソッドの本体に組み込まれました。

// 変更前 (seesComma)
func (p *parser) seesComma(context string) bool {
	if p.tok == token.COMMA {
		return true
	}
	/*
	if p.tok == token.SEMICOLON && p.lit == "\n" {
		p.error(p.pos, "missing ',' before newline in "+context)
		return true // "insert" the comma and continue
	}
	*/
	return false
}

// 変更後 (atComma)
func (p *parser) atComma(context string) bool {
	if p.tok == token.COMMA {
		return true
	}
	if p.tok == token.SEMICOLON && p.lit == "\n" {
		p.error(p.pos, "missing ',' before newline in "+context)
		return true // "insert" the comma and continue
	}
	return false
}

この変更により、atComma メソッドは、現在のトークンがカンマであるか、または改行による自動セミコロン挿入の対象となるセミコロンである場合に、カンマが存在すると見なすようになりました。これにより、カンマが欠落しているが改行がある場合に、パーサーがより早くエラーを検出し、かつ解析を継続できるようになったと考えられます。

p.tok != token.COMMA への変更

parseVarList, parseParameterList, parseCallOrConversion, parseElementList といった、リスト構造を解析する複数のメソッドにおいて、ループの継続条件が !p.seesComma(...) から p.tok != token.COMMA に変更されました。

これは非常に重要な変更です。

  • 変更前 (!p.seesComma(...)): seesCommatrue を返す(つまり、実際のカンマがあるか、改行による擬似カンマがある)限りループが継続していました。
  • 変更後 (p.tok != token.COMMA): ループは実際の token.COMMA が存在しない場合にのみ終了します。

一見すると、エラー回復のロジックが弱くなったように見えますが、そうではありません。atComma メソッド自体が、改行によるカンマ欠落を検出してエラーを報告しつつ true を返すようになったため、p.tok != token.COMMA という条件と組み合わせることで、より正確なエラー同期が可能になります。

具体的には、atCommatrue を返しても、p.toktoken.COMMA でなければループは終了します。しかし、atComma の内部で p.error が呼び出されているため、エラーは適切に報告されます。これにより、パーサーはカンマが欠落していることを認識しつつ、その後のトークンに基づいて次の構文要素の解析を試みることができます。これは、エラー発生時にパーサーが「どこまで読み飛ばすべきか」を判断する上で非常に重要です。

この変更は、パーサーが構文エラーに遭遇した際に、不必要なトークンを読み飛ばしすぎることなく、かつ解析を継続できるような、より洗練されたエラー回復戦略を実装するためのものです。

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

--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -376,17 +376,15 @@ func (p *parser) expectSemi() {
 	}
 }
 
-func (p *parser) seesComma(context string) bool {
+func (p *parser) atComma(context string) bool {
 	if p.tok == token.COMMA {
 		return true
 	}
-	/*
-	if p.tok == token.SEMICOLON && p.lit == "\n" {
-		p.error(p.pos, "missing ',' before newline in "+context)
-		return true // "insert" the comma and continue
-
-	}
-	*/
+	if p.tok == token.SEMICOLON && p.lit == "\n" {
+		p.error(p.pos, "missing ',' before newline in "+context)
+		return true // "insert" the comma and continue
+
+	}
 	return false
 }
 
@@ -661,7 +659,7 @@ func (p *parser) parseVarList(isParam bool) (list []ast.Expr, typ ast.Expr) {
 	// accept them all for more robust parsing and complain later
 	for typ := p.parseVarType(isParam); typ != nil; {
 		list = append(list, typ)
-	if !p.seesComma("variable list") {
+	if p.tok != token.COMMA {
 			break
 		}
 		p.next()
@@ -702,7 +700,7 @@ func (p *parser) parseParameterList(scope *ast.Scope, ellipsisOk bool) (params [
 			// Go spec: The scope of an identifier denoting a function
 			// parameter or result variable is the function body.
 			p.declare(field, nil, scope, ast.Var, idents...)\n-	if !p.seesComma("parameter list") {
    +	if !p.atComma("parameter list") {
 				break
 			}
 			p.next()
@@ -1092,7 +1090,7 @@ func (p *parser) parseCallOrConversion(fun ast.Expr) *ast.CallExpr {\n 		ellipsis = p.pos
 		p.next()
 	}
-	if !p.seesComma("argument list") {
+	if !p.atComma("argument list") {
 			break
 		}
 		p.next()
@@ -1132,7 +1130,7 @@ func (p *parser) parseElementList() (list []ast.Expr) {\n 
 	for p.tok != token.RBRACE && p.tok != token.EOF {
 		list = append(list, p.parseElement(true))
-	if !p.seesComma("composite literal") {
+	if !p.atComma("composite literal") {
 			break
 		}
 		p.next()

コアとなるコードの解説

func (p *parser) seesComma(context string) bool から func (p *parser) atComma(context string) bool への変更

  • メソッド名の変更: seesComma から atComma へと変更されました。これは、メソッドの意図をより明確にするためのリファクタリングです。seesComma は「カンマが見えるか」というニュアンスでしたが、atComma は「現在カンマの位置にいるか(またはカンマと見なせる状況か)」という、より現在のパーサーの状態に焦点を当てた表現になっています。
  • コメントアウトされたコードの統合: 以前 seesComma 内でコメントアウトされていた、p.tok == token.SEMICOLON && p.lit == "\n" の条件でエラーを報告しつつ true を返すロジックが、atComma の本体に直接組み込まれました。これにより、改行の前にカンマが欠落している場合に、パーサーがその場でエラーを報告し、「カンマが挿入された」かのように振る舞って解析を続行できるようになります。これは、エラー回復の重要な部分です。

parseVarList メソッド内の変更

-	if !p.seesComma("variable list") {
+	if p.tok != token.COMMA {
 		break
 	}

parseVarList は変数宣言リストを解析するメソッドです。ループの継続条件が !p.seesComma("variable list") から p.tok != token.COMMA に変更されました。

  • 変更前: seesCommatrue を返す限り(つまり、実際のカンマがあるか、改行による擬似カンマがある限り)ループが継続していました。
  • 変更後: ループは実際の token.COMMA が存在しない場合にのみ終了します。これにより、atComma が改行によるカンマ欠落を検出してエラーを報告した場合でも、p.toktoken.COMMA でなければループは終了し、パーサーは次の構文要素の解析に進むことができます。これは、エラー発生時にパーサーが不必要なトークンを読み飛ばしすぎることなく、かつ解析を継続できるような、より洗練されたエラー回復戦略を実装するためのものです。

parseParameterList, parseCallOrConversion, parseElementList メソッド内の変更

これらのメソッドも同様に、リスト構造(パラメータリスト、引数リスト、複合リテラルの要素リスト)を解析する部分で、ループの継続条件が !p.seesComma(...) から !p.atComma(...) または p.tok != token.COMMA に変更されています。

  • parseParameterList:

    -	if !p.seesComma("parameter list") {
    +	if !p.atComma("parameter list") {
    			break
    		}
    

    ここでは !p.atComma に変更されています。atComma がエラーを報告しつつ true を返す場合、!p.atCommafalse となり、ループは継続します。これは、パラメータリストの解析において、カンマが欠落していても、その後のパラメータを解析し続けようとする意図があることを示唆しています。

  • parseCallOrConversion:

    -	if !p.seesComma("argument list") {
    +	if !p.atComma("argument list") {
    			break
    		}
    

    関数呼び出しや型変換の引数リストの解析です。ここも !p.atComma に変更されています。

  • parseElementList:

    -	if !p.seesComma("composite literal") {
    +	if !p.atComma("composite literal") {
    			break
    		}
    

    複合リテラル(配列、スライス、マップ、構造体リテラル)の要素リストの解析です。ここも !p.atComma に変更されています。

これらの変更は、Goのパーサーが、カンマの欠落という一般的な構文エラーに対して、より堅牢かつ柔軟に対応できるようにするためのものです。エラーを適切に報告しつつ、可能な限り解析を続行することで、開発者にとってより有用なエラーメッセージを提供し、コンパイルプロセス全体の効率を向上させます。

関連リンク

  • Go言語のパーサーに関する公式ドキュメントやチュートリアルは、Goの公式ウェブサイトやGoのソースコードリポジトリで確認できます。
  • Go言語の構文解析に関する詳細な情報は、go/parser および go/ast パッケージのドキュメントを参照してください。

参考にした情報源リンク