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

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

このコミットは、Go言語のパーサー(go/parserパッケージ)における配列型の解析に関する変更です。具体的には、配列型が許可される場所で[...]T形式(要素の型Tを持つ任意の長さの配列)の構文をより寛容に受け入れるように修正されています。これにより、パーサーのエラー回復能力が向上し、型チェックフェーズでより柔軟にエラーを処理できるようになります。

コミット

commit 305d7ada2b94f4d61ed41727c82660795ac85698
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Nov 28 16:03:34 2012 -0800

    go/parser: permit [...]T where array types are permitted
    
    More lenient parsing with better error recovery.
    It's easier for the type check to pick up the slack.
    
    R=iant
    CC=golang-dev
    https://golang.org/cl/6856108

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

https://github.com/golang/go/commit/305d7ada2b94f4d61ed41727c8266079ac85698

元コミット内容

go/parser: permit [...]T where array types are permitted

More lenient parsing with better error recovery.
It's easier for the type check to pick up the slack.

変更の背景

Go言語のコンパイラは、ソースコードを解析する際に複数のフェーズを経ます。初期のフェーズの一つが字句解析(lexing)と構文解析(parsing)です。go/parserパッケージは、Goのソースコードを抽象構文木(AST: Abstract Syntax Tree)に変換する役割を担っています。

このコミットの背景には、パーサーの堅牢性とエラー回復能力の向上が挙げられます。以前のパーサーでは、配列型を解析する際に、特定のコンテキストでのみ...(エリプシス)の使用を許可していました。例えば、可変長引数(variadic arguments)や複合リテラル(composite literals)の文脈では...が許容されていましたが、配列型の定義において[...]Tのような形式が常に適切に扱われるわけではありませんでした。

しかし、プログラマーが意図的に、あるいは誤って[...]Tのような構文を記述した場合、パーサーが厳密すぎると、その時点で構文エラーとして処理を中断してしまう可能性がありました。このコミットの目的は、パーサーをより「寛容」にし、構文的に多少不完全または曖昧な場合でも、可能な限りASTを構築し、その後の型チェックフェーズでより詳細なセマンティックな検証を行えるようにすることです。これにより、コンパイラ全体としてより良いエラーメッセージを提供し、開発者のデバッグ体験を向上させることが期待されます。

「It's easier for the type check to pick up the slack.」(型チェックがその不足分を補う方が簡単だ)というコメントは、この設計思想を明確に示しています。つまり、パーサーは構文的に可能な限り多くの情報を抽出し、セマンティックな妥当性の検証は型チェッカーに任せる、という役割分担の変更です。

前提知識の解説

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

    • Go言語の標準ライブラリの一部であり、Goのソースコードを解析して抽象構文木(AST)を生成します。
    • ASTは、プログラムの構造を木構造で表現したもので、コンパイラの後のフェーズ(型チェック、コード生成など)で利用されます。
    • パーサーは、字句解析器(lexer)からトークンを受け取り、文法規則に基づいてそれらを構造化します。
  2. 抽象構文木 (AST: Abstract Syntax Tree):

    • ソースコードの抽象的な構文構造を表現する木構造のデータ構造です。
    • go/astパッケージで定義されるノード(ast.Expr, ast.Stmt, ast.Declなど)で構成されます。
    • 例えば、ast.ArrayTypeは配列型を表すASTノードです。
  3. 配列型とエリプシス (...):

    • Go言語では、配列は[N]Tのように定義され、Nは配列の長さ、Tは要素の型です。
    • [...]Tという構文は、配列リテラル(例: [...]int{1, 2, 3})や可変長引数(例: func foo(args ...int))の文脈で、コンパイラが自動的に長さを推論する場合に使用されます。
    • このコミット以前は、配列型の定義自体で[...]Tが常に許可されるわけではありませんでした。
  4. エラー回復 (Error Recovery):

    • コンパイラがソースコード中のエラーを検出した際に、処理を中断せずに、可能な限り解析を続行しようとする機能です。
    • これにより、単一のエラーでコンパイルが停止するのを防ぎ、複数のエラーを一度に報告できるようになります。
    • パーサーがより寛容になることで、構文エラーがあってもASTの一部を構築し、その後のフェーズでより意味のあるエラーメッセージを生成できる場合があります。
  5. 型チェック (Type Checking):

    • コンパイラのフェーズの一つで、プログラムが型システム(Goの型規則)に準拠しているかを検証します。
    • 例えば、[...]Tが配列型の定義として構文的に受け入れられたとしても、そのコンテキストでそれがセマンティックに有効であるか(例えば、配列の長さが不明なまま型として使用できるか)は型チェッカーが判断します。

技術的詳細

このコミットの主要な変更点は、go/parserパッケージ内の複数の関数において、配列型を解析する際の...(エリプシス)の扱いをより柔軟にしたことです。

具体的には、以下の変更が行われました。

  1. parseArrayType関数の変更:

    • 以前はellipsisOkというブール型の引数を受け取り、この引数がtrueの場合にのみ...を許可していました。
    • 変更後、ellipsisOk引数は削除され、parseArrayType関数は常にp.tok == token.ELLIPSISの場合にエリプシスを許可するように変更されました。
    • これにより、配列型を解析する際に、どのようなコンテキストであっても[...]という形式が構文的に受け入れられるようになります。パーサーは、[...]ast.EllipsisノードとしてASTに組み込みます。
  2. tryVarType関数の変更:

    • この関数は、変数宣言やパラメータリストにおける型を解析します。
    • 以前はp.tryIdentOrType(isParam)のようにisParam引数を渡していましたが、tryIdentOrTypeのシグネチャ変更に伴い、引数なしでp.tryIdentOrType()を呼び出すように変更されました。
    • これにより、可変長引数(...T)の解析ロジックが、より一般的な型解析ロジックと整合性が取れるようになります。
  3. tryIdentOrType関数の変更:

    • この関数は、識別子または型を解析します。
    • 以前はellipsisOkというブール型の引数を受け取り、parseArrayTypeにその引数を渡していました。
    • 変更後、ellipsisOk引数は削除され、parseArrayTypeを引数なしで呼び出すように変更されました。
    • これにより、tryIdentOrTypeが呼び出されるあらゆる場所で、配列型内のエリプシスが常に許可されるようになります。
  4. tryType関数の変更:

    • この関数は、型を解析します。
    • 以前はp.tryIdentOrType(false)のようにfalse引数を渡していましたが、tryIdentOrTypeのシグネチャ変更に伴い、引数なしでp.tryIdentOrType()を呼び出すように変更されました。
  5. parseOperand関数の変更:

    • この関数は、オペランド(式の一部)を解析します。
    • 以前はp.tryIdentOrType(true)のようにtrue引数を渡していましたが、tryIdentOrTypeのシグネチャ変更に伴い、引数なしでp.tryIdentOrType()を呼び出すように変更されました。

これらの変更により、パーサーは[...]Tという構文を、それが配列型が期待される場所であればどこでも、構文的に有効なものとして扱います。パーサーは、この構文をast.ArrayTypeノードとしてASTに組み込み、そのLenフィールドにast.Ellipsisノードを設定します。この時点では、[...]がセマンティックに有効かどうか(例えば、配列の長さが不明なまま型として使用できるか)は判断されず、その後の型チェックフェーズに委ねられます。

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

src/pkg/go/parser/parser.go ファイルにおいて、以下の変更が行われました。

--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -578,14 +578,15 @@ func (p *parser) parseTypeName() ast.Expr {
 	return ident
 }
 
-func (p *parser) parseArrayType(ellipsisOk bool) ast.Expr {
+func (p *parser) parseArrayType() ast.Expr {
 	if p.trace {
 		defer un(trace(p, "ArrayType"))
 	}
 
 	lbrack := p.expect(token.LBRACK)
 	var len ast.Expr
-	if ellipsisOk && p.tok == token.ELLIPSIS {
+	// always permit ellipsis for more fault-tolerant parsing
+	if p.tok == token.ELLIPSIS {
 		len = &ast.Ellipsis{Ellipsis: p.pos}
 		p.next()
 	} else if p.tok != token.RBRACK {
@@ -697,7 +698,7 @@ func (p *parser) tryVarType(isParam bool) ast.Expr {
 	if isParam && p.tok == token.ELLIPSIS {
 		pos := p.pos
 		p.next()
-\t\ttyp := p.tryIdentOrType(isParam) // don't use parseType so we can provide better error message
+\t\ttyp := p.tryIdentOrType() // don't use parseType so we can provide better error message
 		if typ != nil {
 			p.resolve(typ)
 		} else {
@@ -706,7 +707,7 @@ func (p *parser) tryVarType(isParam bool) ast.Expr {
 		}
 		return &ast.Ellipsis{Ellipsis: pos, Elt: typ}
 	}
-\treturn p.tryIdentOrType(false)
+\treturn p.tryIdentOrType()
 }
 
 // If the result is an identifier, it is not resolved.
@@ -943,12 +944,12 @@ func (p *parser) parseChanType() *ast.ChanType {
 }
 
 // If the result is an identifier, it is not resolved.
-func (p *parser) tryIdentOrType(ellipsisOk bool) ast.Expr {
+func (p *parser) tryIdentOrType() ast.Expr {
 	switch p.tok {
 	case token.IDENT:
 		return p.parseTypeName()
 	case token.LBRACK:
-\t\treturn p.parseArrayType(ellipsisOk)
+\t\treturn p.parseArrayType()
 	case token.STRUCT:
 		return p.parseStructType()
 	case token.MUL:
@@ -975,7 +976,7 @@ func (p *parser) tryIdentOrType(ellipsisOk bool) ast.Expr {
 }
 
 func (p *parser) tryType() ast.Expr {
-\ttyp := p.tryIdentOrType(false)
+\ttyp := p.tryIdentOrType()
 	if typ != nil {
 		p.resolve(typ)
 	}
@@ -1083,7 +1084,7 @@ func (p *parser) parseOperand(lhs bool) ast.Expr {\n 		return p.parseFuncTypeOrLit()\n 	}\n \n-\tif typ := p.tryIdentOrType(true); typ != nil {\n+\tif typ := p.tryIdentOrType(); typ != nil {\n \t\t// could be type for composite literal or conversion\n \t\t_, isIdent := typ.(*ast.Ident)\n \t\tassert(!isIdent, "type cannot be identifier")

コアとなるコードの解説

このコミットの核心は、parseArrayType関数のシグネチャ変更と、それに伴う関連関数の呼び出し方の変更です。

  1. func (p *parser) parseArrayType():

    • 変更前: func (p *parser) parseArrayType(ellipsisOk bool) ast.Expr
    • 変更後: func (p *parser) parseArrayType() ast.Expr
    • 最も重要な変更は、ellipsisOkというブール型の引数が削除されたことです。この引数は、配列の長さ部分に...(エリプシス)を許可するかどうかを制御していました。
    • 引数削除に伴い、関数内のif ellipsisOk && p.tok == token.ELLIPSIS {という条件がif p.tok == token.ELLIPSIS {に変更されました。
    • この変更により、parseArrayTypeが呼び出されるあらゆるコンテキストで、[の直後に...が来た場合、それが常にast.Ellipsisとして解析されるようになりました。コメント// always permit ellipsis for more fault-tolerant parsingがこの意図を明確に示しています。これにより、パーサーは[...]Tのような構文を常に受け入れ、ASTを構築します。
  2. func (p *parser) tryVarType(isParam bool) ast.Expr:

    • この関数は、変数宣言や関数のパラメータの型を解析する際に使用されます。
    • 内部でp.tryIdentOrType(isParam)を呼び出していた箇所が、p.tryIdentOrType()に変更されました。これは、tryIdentOrTypeのシグネチャ変更に合わせたものです。
  3. func (p *parser) tryIdentOrType() ast.Expr:

    • 変更前: func (p *parser) tryIdentOrType(ellipsisOk bool) ast.Expr
    • 変更後: func (p *parser) tryIdentOrType() ast.Expr
    • この関数もellipsisOk引数が削除されました。
    • 内部でp.parseArrayType(ellipsisOk)を呼び出していた箇所が、p.parseArrayType()に変更されました。これにより、tryIdentOrTypeが型を解析する際に、配列型内のエリプシスが常に許可されるようになりました。
  4. func (p *parser) tryType() ast.Expr:

    • この関数は、一般的な型を解析する際に使用されます。
    • 内部でp.tryIdentOrType(false)を呼び出していた箇所が、p.tryIdentOrType()に変更されました。
  5. func (p *parser) parseOperand(lhs bool) ast.Expr:

    • この関数は、式のオペランドを解析する際に使用されます。
    • 内部でp.tryIdentOrType(true)を呼び出していた箇所が、p.tryIdentOrType()に変更されました。

これらの変更は、Goパーサーの設計哲学における重要なシフトを示しています。パーサーは、構文的に可能な限り多くのコードを受け入れ、ASTを構築することに焦点を当てます。そして、その構文がGoのセマンティックな規則に適合しているかどうかの厳密なチェックは、より後のフェーズである型チェッカーに委ねられます。これにより、パーサーはより堅牢になり、エラー回復能力が向上し、開発者に対してより有用なエラーメッセージを提供できるようになります。例えば、var x [...]intのようなコードは、パーサーによってASTに変換されますが、型チェッカーが「配列の長さが型定義で不明なのは不正である」と判断し、適切なエラーを報告する、といった流れになります。

関連リンク

参考にした情報源リンク

  • Go言語のコミット履歴: https://github.com/golang/go/commits/master
  • Go Code Review Comments (CL 6856108): https://golang.org/cl/6856108 (コミットメッセージに記載されているCLリンク)
  • Go言語のコンパイラ設計に関する一般的な情報源(例: Goコンパイラの内部構造に関するブログ記事やプレゼンテーション)