[インデックス 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.Println
はmyfmt.Println
として呼び出されます。これは、名前の衝突を避けるためや、長いパッケージ名を短縮するために使用されます。
fmt
パッケージ
fmt
パッケージは、Go言語の標準ライブラリの一部であり、フォーマットされたI/O(入出力)機能を提供します。これには、文字列の整形(Sprintf
)、標準出力への出力(Printf
)、エラーメッセージの生成などが含まれます。デバッグやログ出力で頻繁に使用されます。
%{ ... %}
ブロック (Yacc)
Yaccの入力ファイルでは、%{
と%}
で囲まれたブロック内にGo言語のコードを記述することができます。このブロック内のコードは、生成されるパーサーファイル(y.go
)の先頭部分にそのままコピーされます。通常、パッケージ宣言、インポート、グローバル変数、ヘルパー関数などがここに記述されます。
技術的詳細
このコミットの技術的な核心は、cmd/yacc
が生成するGoコードにfmt
パッケージを「安全に」インポートするメカニズムを導入した点にあります。
-
unicode
パッケージの追加インポート: 変更されたsrc/cmd/yacc/yacc.go
ファイル自体に"unicode"
パッケージがインポートされています。これは、新しく追加されたisPackageClause
関数内で、Goの識別子(パッケージ名など)が文字や数字で構成されているかをチェックするためにunicode.IsLetter
やunicode.IsDigit
を使用するためです。 -
fmtImported
フラグの導入:var fmtImported bool
というグローバル変数が追加されました。このフラグは、生成されるGoファイルにfmt
パッケージのインポート文が既に出力されたかどうかを追跡するために使用されます。これにより、fmt
パッケージが複数回インポートされるのを防ぎます。 -
cpycode
関数の変更:cpycode
関数は、Yaccの入力ファイル内の%{ ... %}
ブロックに記述されたユーザーコードを読み込み、生成されるy.go
ファイルにコピーする役割を担っています。このコミットでは、cpycode
がコードを直接ftable
(出力ファイルへのライター)に書き込むのではなく、まずcode
という[]rune
スライスに蓄積するように変更されました。そして、%}
に到達した時点で、蓄積されたコードを新しいemitcode
関数に渡して処理するように変更されています。 -
emitcode
関数の追加: この新しい関数が、%{ ... %}
ブロック内のユーザーコードを処理し、必要に応じてfmt
パッケージのインポートを挿入する主要なロジックを含んでいます。emitcode
は、入力されたコードを行ごとに処理します。- 各行を
writecode
関数を使ってftable
に書き込みます。 - 最も重要なのは、
fmtImported
がfalse
であり、かつ現在の行がGoのpackage
宣言であるとisPackageClause
関数が判断した場合です。この条件が満たされると、ftable
にimport __yyfmt__ "fmt"
という行が書き込まれます。 - インポートが書き込まれた後、
fmtImported
はtrue
に設定され、以降の行では重複してインポートが挿入されることはありません。 //line
ディレクティブも適切に調整され、生成されたコードの行番号が元のソースコードと一致するように保たれます。
-
isPackageClause
関数の追加: このヘルパー関数は、与えられた[]rune
スライス(コードの行)がGoのpackage
宣言であるかどうかを判定します。- 行頭の空白をスキップします。
- "package"というキーワードが存在するかをチェックします。
- "package"キーワードの後に有効なGoの識別子(パッケージ名)が続くかをチェックします。
- 識別子の後に改行、行末、またはコメントが続くかをチェックします。
- この関数は、コメントなどによって誤認識される可能性も考慮されていますが、一般的なケースでは正しく機能します。
-
skipspace
,lines
,writecode
関数の追加: これらは、emitcode
関数がコードを処理するために使用するユーティリティ関数です。skipspace
: 行頭の空白文字をスキップします。lines
:[]rune
スライスを改行文字で区切られた行の[][]rune
スライスに分割します。writecode
:[]rune
スライスをbufio.Writer
に書き込みます。
-
fmt
関数のエイリアス化: 生成されるパーサーコード内で、fmt.Sprintf
やfmt.Printf
といったfmt
パッケージの関数呼び出しが、__yyfmt__.Sprintf
や__yyfmt__.Printf
といったエイリアス付きの呼び出しに置き換えられました。 これは、yacc
が生成するコードが内部的に使用するfmt
パッケージと、ユーザーが自身のコードでインポートするfmt
パッケージとの間で名前の衝突が発生するのを防ぐための重要な変更です。__yyfmt__
というユニークなエイリアスを使用することで、ユーザーがfmt
という名前の変数や関数を定義していたとしても、生成されたコードがそれに影響されることなく、意図したfmt
パッケージの関数を呼び出すことができます。
これらの変更により、cmd/yacc
は、生成されるパーサーコードが常にfmt
パッケージにアクセスできるようにし、かつユーザーの既存のコードベースとの互換性を最大限に保つことができるようになりました。
コアとなるコードの変更箇所
変更は主にsrc/cmd/yacc/yacc.go
ファイルに集中しています。
-
インポートの追加:
--- a/src/cmd/yacc/yacc.go +++ b/src/cmd/yacc/yacc.go @@ -51,6 +51,7 @@ import ( "fmt" "os" "strings" + "unicode" // 追加 )
-
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
-
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++ }
-
新しいヘルパー関数の追加:
emitcode
,isPackageClause
,skipspace
,lines
,writecode
が追加されました。これらはcpycode
から呼び出され、fmt
インポートの挿入とコードの整形を担当します。 -
生成コード内の
fmt
呼び出しのエイリアス化: 生成されるパーサーコード内のfmt.Sprintf
やfmt.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
という名前を使用している場合に発生する可能性のある名前衝突を避けるために重要です。 - インポートが挿入された後、
fmtImported
はtrue
に設定され、二重インポートを防ぎます。 //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.Sprintf
やfmt.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 (これはコミットメッセージに記載されているリンクであり、コミットの詳細な変更内容を確認できます)