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

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

このコミットは、Go言語のyaccツール(cmd/yacc)が生成するパーサーコードにおいて、fmtパッケージが常に安全にインポートされるようにするための変更です。これにより、生成されたパーサーがfmtパッケージの関数に依存しているにもかかわらず、ユーザーのコードがfmtをインポートしていない場合に発生するコンパイルエラーが解消されます。

コミット

commit a225eaf9b771cdb42defcc89015dc12cd04c4438
Author: Rob Pike <r@golang.org>
Date:   Thu Sep 6 14:58:37 2012 -0700

    cmd/yacc: always import fmt, safely
    The parser depends on it but the client might not import it, so make sure it's there.
    Fixes #4038.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6497094

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

https://github.com/golang/go/commit/a225eaf9b771cdb42defcc89015dc12cd04c4438

元コミット内容

cmd/yacc: always import fmt, safely パーサーはfmtに依存しているが、クライアントがそれをインポートしない可能性があるため、確実に存在するようにする。 Fixes #4038.

変更の背景

このコミットは、Go言語のyaccツール(cmd/yacc)が生成するパーサーコードが抱えていた特定の問題を解決するために行われました。具体的には、Goのyaccによって生成されるパーサーのコード(通常y.goというファイル名で出力される)は、デバッグ出力やエラーメッセージの整形のためにfmtパッケージの関数(例: fmt.Sprintf, fmt.Printf)を内部的に使用していました。

しかし、もしユーザーが作成した.yファイル(Yaccの入力ファイル)が、生成されるGoコード内で明示的にfmtパッケージをインポートしていなかった場合、生成されたy.goファイルはfmtパッケージが未定義であるというコンパイルエラーを引き起こしていました。これは、yaccが生成するコードがfmtに暗黙的に依存しているにもかかわらず、その依存関係が常に満たされる保証がなかったためです。

この問題はGoのIssue #4038として報告されており、このコミットはその問題を修正することを目的としています。修正の目標は、fmtパッケージを常にインポートし、かつユーザーのコードとの名前衝突を避ける「安全な」方法でインポートすることでした。

前提知識の解説

Yacc (Yet Another Compiler Compiler)

Yaccは、文法定義から構文解析器(パーサー)を自動生成するためのツールです。Go言語のcmd/yaccは、C言語のYaccと同様の機能を提供し、Go言語のソースコードとしてパーサーを生成します。パーサーは、入力されたテキストが特定の文法規則に合致するかどうかを検証し、通常は抽象構文木(AST)などの構造を構築します。

Go言語のパッケージとインポート

Go言語では、コードは「パッケージ」という単位で管理されます。他のパッケージの関数や変数を使用するには、importキーワードを使ってそのパッケージをインポートする必要があります。 例: import "fmt"

パッケージをインポートする際に、エイリアス(別名)を付けることも可能です。 例: import myfmt "fmt" この場合、fmt.Printlnmyfmt.Printlnとして呼び出されます。これは、名前の衝突を避けるためや、長いパッケージ名を短縮するために使用されます。

fmtパッケージ

fmtパッケージは、Go言語の標準ライブラリの一部であり、フォーマットされたI/O(入出力)機能を提供します。これには、文字列の整形(Sprintf)、標準出力への出力(Printf)、エラーメッセージの生成などが含まれます。デバッグやログ出力で頻繁に使用されます。

%{ ... %} ブロック (Yacc)

Yaccの入力ファイルでは、%{%}で囲まれたブロック内にGo言語のコードを記述することができます。このブロック内のコードは、生成されるパーサーファイル(y.go)の先頭部分にそのままコピーされます。通常、パッケージ宣言、インポート、グローバル変数、ヘルパー関数などがここに記述されます。

技術的詳細

このコミットの技術的な核心は、cmd/yaccが生成するGoコードにfmtパッケージを「安全に」インポートするメカニズムを導入した点にあります。

  1. unicodeパッケージの追加インポート: 変更されたsrc/cmd/yacc/yacc.goファイル自体に"unicode"パッケージがインポートされています。これは、新しく追加されたisPackageClause関数内で、Goの識別子(パッケージ名など)が文字や数字で構成されているかをチェックするためにunicode.IsLetterunicode.IsDigitを使用するためです。

  2. fmtImportedフラグの導入: var fmtImported boolというグローバル変数が追加されました。このフラグは、生成されるGoファイルにfmtパッケージのインポート文が既に出力されたかどうかを追跡するために使用されます。これにより、fmtパッケージが複数回インポートされるのを防ぎます。

  3. cpycode関数の変更: cpycode関数は、Yaccの入力ファイル内の%{ ... %}ブロックに記述されたユーザーコードを読み込み、生成されるy.goファイルにコピーする役割を担っています。このコミットでは、cpycodeがコードを直接ftable(出力ファイルへのライター)に書き込むのではなく、まずcodeという[]runeスライスに蓄積するように変更されました。そして、%}に到達した時点で、蓄積されたコードを新しいemitcode関数に渡して処理するように変更されています。

  4. emitcode関数の追加: この新しい関数が、%{ ... %}ブロック内のユーザーコードを処理し、必要に応じてfmtパッケージのインポートを挿入する主要なロジックを含んでいます。

    • emitcodeは、入力されたコードを行ごとに処理します。
    • 各行をwritecode関数を使ってftableに書き込みます。
    • 最も重要なのは、fmtImportedfalseであり、かつ現在の行がGoのpackage宣言であるとisPackageClause関数が判断した場合です。この条件が満たされると、ftableimport __yyfmt__ "fmt"という行が書き込まれます。
    • インポートが書き込まれた後、fmtImportedtrueに設定され、以降の行では重複してインポートが挿入されることはありません。
    • //lineディレクティブも適切に調整され、生成されたコードの行番号が元のソースコードと一致するように保たれます。
  5. isPackageClause関数の追加: このヘルパー関数は、与えられた[]runeスライス(コードの行)がGoのpackage宣言であるかどうかを判定します。

    • 行頭の空白をスキップします。
    • "package"というキーワードが存在するかをチェックします。
    • "package"キーワードの後に有効なGoの識別子(パッケージ名)が続くかをチェックします。
    • 識別子の後に改行、行末、またはコメントが続くかをチェックします。
    • この関数は、コメントなどによって誤認識される可能性も考慮されていますが、一般的なケースでは正しく機能します。
  6. skipspace, lines, writecode関数の追加: これらは、emitcode関数がコードを処理するために使用するユーティリティ関数です。

    • skipspace: 行頭の空白文字をスキップします。
    • lines: []runeスライスを改行文字で区切られた行の[][]runeスライスに分割します。
    • writecode: []runeスライスをbufio.Writerに書き込みます。
  7. fmt関数のエイリアス化: 生成されるパーサーコード内で、fmt.Sprintffmt.Printfといったfmtパッケージの関数呼び出しが、__yyfmt__.Sprintf__yyfmt__.Printfといったエイリアス付きの呼び出しに置き換えられました。 これは、yaccが生成するコードが内部的に使用するfmtパッケージと、ユーザーが自身のコードでインポートするfmtパッケージとの間で名前の衝突が発生するのを防ぐための重要な変更です。__yyfmt__というユニークなエイリアスを使用することで、ユーザーがfmtという名前の変数や関数を定義していたとしても、生成されたコードがそれに影響されることなく、意図したfmtパッケージの関数を呼び出すことができます。

これらの変更により、cmd/yaccは、生成されるパーサーコードが常にfmtパッケージにアクセスできるようにし、かつユーザーの既存のコードベースとの互換性を最大限に保つことができるようになりました。

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

変更は主にsrc/cmd/yacc/yacc.goファイルに集中しています。

  1. インポートの追加:

    --- a/src/cmd/yacc/yacc.go
    +++ b/src/cmd/yacc/yacc.go
    @@ -51,6 +51,7 @@ import (
     	"fmt"
     	"os"
     	"strings"
    +	"unicode" // 追加
     )
    
  2. fmtImported変数の追加:

    --- a/src/cmd/yacc/yacc.go
    +++ b/src/cmd/yacc/yacc.go
    @@ -153,6 +154,8 @@ var ftable *bufio.Writer    // y.go file
     var fcode = &bytes.Buffer{} // saved code
     var foutput *bufio.Writer   // y.output file
     
    +var fmtImported bool // output file has recorded an import of "fmt" // 追加
    +
     var oflag string  // -o [y.go]		- y.go file
     var vflag string  // -v [y.output]	- y.output file
     var lflag bool    // -l			- disable line directives
    
  3. cpycode関数の変更: %{ ... %}ブロック内のコードを直接書き出すのではなく、codeスライスに蓄積し、emitcodeを呼び出すように変更。

    --- a/src/cmd/yacc/yacc.go
    +++ b/src/cmd/yacc/yacc.go
    @@ -1085,15 +1089,18 @@ func cpycode() {
      	if !lflag {
      		fmt.Fprintf(ftable, "\n//line %v:%v\n", infile, lineno)
      	}
    +	// accumulate until %}
    +	code := make([]rune, 0, 1024) // 変更
      	for c != EOF {
      		if c == '%' {
      			c = getrune(finput)
      			if c == '}' {
    +				emitcode(code, lno+1) // 変更
      				return
      			}
    -			ftable.WriteRune('%')
    +			code = append(code, '%') // 変更
      		}
    -		ftable.WriteRune(c)
    +		code = append(code, c) // 変更
      		if c == '\n' {
      			lineno++
      		}
    
  4. 新しいヘルパー関数の追加: emitcode, isPackageClause, skipspace, lines, writecodeが追加されました。これらはcpycodeから呼び出され、fmtインポートの挿入とコードの整形を担当します。

  5. 生成コード内のfmt呼び出しのエイリアス化: 生成されるパーサーコード内のfmt.Sprintffmt.Printfの呼び出しが、__yyfmt__.Sprintf__yyfmt__.Printfに置き換えられました。

    --- a/src/cmd/yacc/yacc.go
    +++ b/src/cmd/yacc/yacc.go
    @@ -3115,7 +3223,7 @@ func $$Tokname(c int) string {
      			return $$Toknames[c-1]
      		}
      	}
    -	return fmt.Sprintf("tok-%v", c)
    +	return __yyfmt__.Sprintf("tok-%v", c) // 変更
     }
     
     func $$Statname(s int) string {
    @@ -3124,7 +3232,7 @@ func $$Statname(s int) string {
      			return $$Statenames[s]
      		}
      	}
    -	return fmt.Sprintf("state-%v", s)
    +	return __yyfmt__.Sprintf("state-%v", s) // 変更
     }
     
     func $$lex1(lex $$Lexer, lval *$$SymType) int {
    @@ -3157,7 +3265,7 @@ out:
      		c = $$Tok2[1] /* unknown char */
      	}
      	if $$Debug >= 3 {
    -		fmt.Printf("lex %U %s\n", uint(char), $$Tokname(c))
    +		__yyfmt__.Printf("lex %U %s\n", uint(char), $$Tokname(c)) // 変更
      	}
      	return c
     }
    @@ -3184,7 +3292,7 @@ ret1:
     $$stack:
      	/* put a state and value onto the stack */
      	if $$Debug >= 4 {
    -		fmt.Printf("char %v in %v\n", $$Tokname($$char), $$Statname($$state))
    +		__yyfmt__.Printf("char %v in %v\n", $$Tokname($$char), $$Statname($$state)) // 変更
      	}
      
      	$$p++
    @@ -3253,8 +3361,8 @@ $$default:
      		if $$Debug >= 1 {
    -			fmt.Printf("%s", $$Statname($$state))
    -			fmt.Printf("saw %s\n", $$Tokname($$char))
    +			__yyfmt__.Printf("%s", $$Statname($$state)) // 変更
    +			__yyfmt__.Printf("saw %s\n", $$Tokname($$char)) // 変更
      		}
      		fallthrough
      
    @@ -3273,7 +3381,7 @@ $$default:
      
      			/* the current p has no shift on "error", pop stack */
      			if $$Debug >= 2 {
    -				fmt.Printf("error recovery pops state %d\n", $$S[$$p].yys)
    +				__yyfmt__.Printf("error recovery pops state %d\n", $$S[$$p].yys) // 変更
      			}
      			$$p--
      		}
    @@ -3282,7 +3390,7 @@ $$default:
      
      		case 3: /* no shift yet; clobber input char */
      		if $$Debug >= 2 {
    -			fmt.Printf("error recovery discards %s\n", $$Tokname($$char))
    +			__yyfmt__.Printf("error recovery discards %s\n", $$Tokname($$char)) // 変更
      		}
      		if $$char == $$EofCode {
      			goto ret1
    @@ -3294,7 +3402,7 @@ $$default:
      
      	/* reduction by production $$n */
      	if $$Debug >= 2 {
    -		fmt.Printf("reduce %v in:\n\t%v\n", $$n, $$Statname($$state))
    +		__yyfmt__.Printf("reduce %v in:\n\t%v\n", $$n, $$Statname($$state)) // 変更
      	}
      
      	$$nt := $$n
    

コアとなるコードの解説

このコミットの主要な変更は、src/cmd/yacc/yacc.goファイルに実装された、生成されるGoパーサーコードへのfmtパッケージの自動的かつ安全なインポート機構です。

fmtImported (グローバル変数)

var fmtImported bool // output file has recorded an import of "fmt"

このブール型変数は、生成中のGoファイルにfmtパッケージのインポート文が既に追加されたかどうかを追跡します。これにより、import __yyfmt__ "fmt"という行が複数回出力されるのを防ぎます。初期値はfalseです。

cpycode (変更された関数)

func cpycode() {
	lno := lineno
	// ... (既存のコード) ...
	if !lflag {
		fmt.Fprintf(ftable, "\n//line %v:%v\n", infile, lineno)
	}
	// accumulate until %}
	code := make([]rune, 0, 1024) // ユーザーコードを蓄積するスライス
	for c != EOF {
		if c == '%' {
			c = getrune(finput)
			if c == '}' {
				emitcode(code, lno+1) // 蓄積したコードをemitcodeに渡す
				return
			}
			code = append(code, '%') // '%'も蓄積
		}
		code = append(code, c) // 文字を蓄積
		if c == '\n' {
			lineno++
		}
		c = getrune(finput)
	}
	errorf("eof before %%}")
}

cpycode関数は、Yaccの入力ファイル内の%{ ... %}ブロックからGoコードを読み取ります。変更前は読み取ったコードを直接出力ファイルに書き込んでいましたが、変更後はcodeという[]runeスライスに一時的に蓄積するようになりました。%}の終了デリミタに到達すると、蓄積されたcodeスライスと開始行番号をemitcode関数に渡します。これにより、コードの出力前にfmtインポートの挿入などの追加処理が可能になります。

emitcode (新しく追加された関数)

func emitcode(code []rune, lineno int) {
	for i, line := range lines(code) { // コードを行ごとに処理
		writecode(line) // 行を出力ファイルに書き込む
		if !fmtImported && isPackageClause(line) { // fmtが未インポートで、かつパッケージ宣言行の場合
			fmt.Fprintln(ftable, `import __yyfmt__ "fmt"`) // エイリアス付きでfmtをインポート
			fmt.Fprintf(ftable, "//line %v:%v\n\t\t", infile, lineno+i) // 行ディレクティブを調整
			fmtImported = true // fmtがインポートされたことを記録
		}
	}
}

emitcode関数は、cpycodeから渡されたユーザーコードの[]runeスライスを受け取り、それを処理して出力ファイルに書き込みます。この関数がfmtパッケージのインポートを挿入する主要なロジックを含んでいます。

  • lines(code): codeスライスを行ごとに分割します。
  • writecode(line): 各行をftable(出力ファイル)に書き込みます。
  • 条件付きインポート: !fmtImported(まだfmtがインポートされていない)かつisPackageClause(line)(現在の行がpackage宣言である)という条件が両方満たされた場合、import __yyfmt__ "fmt"という行が出力ファイルに書き込まれます。このエイリアス(__yyfmt__)は、ユーザーが自身のコードでfmtという名前を使用している場合に発生する可能性のある名前衝突を避けるために重要です。
  • インポートが挿入された後、fmtImportedtrueに設定され、二重インポートを防ぎます。
  • //lineディレクティブも更新され、生成されたコードの行番号が元のソースコードの行番号と一致するように保たれます。

isPackageClause (新しく追加された関数)

func isPackageClause(line []rune) bool {
	line = skipspace(line) // 行頭の空白をスキップ

	// ... (長さチェック、"package"キーワードのチェック) ...

	line = skipspace(line[len("package"):]) // "package"キーワードの後の空白をスキップ

	// ... (識別子のチェック) ...

	// eol, newline, or comment must follow
	if len(line) == 0 {
		return true
	}
	if line[0] == '\r' || line[0] == '\n' {
		return true
	}
	if len(line) >= 2 {
		return line[0] == '/' && (line[1] == '/' || line[1] == '*') // コメントチェック
	}
	return false
}

この関数は、与えられた[]runeスライス(コードの行)がGoのpackage宣言であるかどうかを判定します。これは、emitcode関数がfmtインポートを挿入する正しい位置(package宣言の直後)を見つけるために使用されます。packageキーワード、それに続く識別子、そしてその後に改行またはコメントが続くパターンをチェックします。

skipspace, lines, writecode (新しく追加されたユーティリティ関数)

これらは、emitcode関数がコードを処理するために使用される補助関数です。

  • skipspace(line []rune) []rune: 行頭の空白文字(スペース、タブ)をスキップした後の[]runeスライスを返します。
  • lines(code []rune) [][]rune: []runeスライスを改行文字で区切られた行の[][]runeスライスに分割します。
  • writecode(code []rune): []runeスライスをftable(出力ファイルへのライター)に書き込みます。

fmt関数呼び出しのエイリアス化

生成されるパーサーコード内のfmt.Sprintffmt.Printfといったfmtパッケージの関数呼び出しは、すべて__yyfmt__.Sprintf__yyfmt__.Printfに置き換えられました。これにより、yaccが生成するコードが内部的に使用するfmtパッケージと、ユーザーが自身のコードでインポートするfmtパッケージとの間で名前の衝突が発生するのを防ぎます。

これらの変更の組み合わせにより、cmd/yaccは、生成されるGoパーサーコードが常にfmtパッケージにアクセスできることを保証し、同時にユーザーの既存のコードベースとの互換性を維持します。

関連リンク

  • Go言語のcmd/yaccツールに関する公式ドキュメント(もしあれば、Goのツールチェーンの一部として提供されることが多い)
  • Yacc (Wikipedia): https://ja.wikipedia.org/wiki/Yacc
  • Go言語のfmtパッケージに関する公式ドキュメント: https://pkg.go.dev/fmt

参考にした情報源リンク

  • GitHub Issue #4038: cmd/yacc: should import "fmt" for itself (Go言語リポジトリ)
  • Go CL 6497094 (Gerrit Code Review): cmd/yacc: always import fmt, safely
    • https://golang.org/cl/6497094 (これはコミットメッセージに記載されているリンクであり、コミットの詳細な変更内容を確認できます)