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

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

コミット

このコミットは、Goコンパイラ(cmd/gc)における「package ステートメントが最初にない」というエラーの扱いを変更し、このエラーを致命的なものとすることで、コンパイルが続行されることを防ぐものです。これにより、コンパイラが package main を仮定して処理を続行する挙動が廃止され、より厳格なエラーハンドリングが導入されました。

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

https://github.com/golang/go/commit/66e8471391faa33aced2752f4c06a9fab444542b

元コミット内容

cmd/gc: make missing package error fatal

No longer continue assuming package main.

Fixes #4776.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/12677043

変更の背景

Go言語のソースファイルは、必ずファイルの先頭に package 宣言を持つ必要があります。これは、そのファイルがどのパッケージに属するかをコンパイラに伝えるための重要な情報です。

このコミット以前のGoコンパイラ(cmd/gc)では、ソースファイルの先頭に package 宣言がない、または誤った位置にある場合、「package statement must be first」というエラーを報告していました。しかし、このエラーが発生しても、コンパイラは処理を中断せず、エラーメッセージを出力した後に、暗黙的にそのファイルを package main に属するものとしてコンパイルを続行していました。

この「エラーを報告しつつもコンパイルを続行する」という挙動は、以下のような問題を引き起こす可能性がありました。

  1. 不明瞭なエラー連鎖: package 宣言がない状態で package main と仮定してコンパイルが続行されると、その後のコード解析で、本来の原因とは異なる、より複雑で理解しにくいエラーが多数発生する可能性がありました。これにより、開発者は問題の根本原因を特定するのに時間を要していました。
  2. 意図しない挙動: 開発者が package 宣言を意図的に省略したり、誤った位置に配置したりした場合でも、コンパイラが勝手に package main と解釈してコンパイルを完了させてしまうため、開発者の意図しないバイナリが生成されるリスクがありました。
  3. コンパイラの堅牢性: 重要な構文エラーに対してコンパイラが回復を試みることは、場合によっては有用ですが、package 宣言のような基本的な構造の欠如に対しては、早期にコンパイルを中断し、明確なエラーを提示する方が、開発体験を向上させ、バグの早期発見に繋がります。

このコミットは、Fixes #4776 とあるように、この既存の挙動が引き起こす具体的な問題(おそらくは、上記のような不明瞭なエラーや意図しないコンパイルの成功)を解決するために導入されました。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびコンパイラに関する基本的な知識が必要です。

  1. Go言語のパッケージシステム:

    • Go言語のソースファイルは、必ず package <パッケージ名> という宣言で始まります。
    • main パッケージは特別なパッケージで、実行可能なプログラムのエントリポイント(main 関数)を含みます。
    • package 宣言は、そのファイルがどのパッケージに属し、他のパッケージからどのように参照されるかを定義します。
  2. Goコンパイラ(cmd/gc:

    • gc はGo言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担います。
    • コンパイラは、ソースコードを字句解析、構文解析、意味解析などの段階を経て処理します。
    • 字句解析 (Lexical Analysis): ソースコードをトークン(キーワード、識別子、演算子など)のストリームに変換します。
    • 構文解析 (Syntax Analysis): トークンのストリームがGo言語の文法規則に準拠しているかをチェックし、抽象構文木(AST)を構築します。この段階で、package 宣言の位置や有無などの構文エラーが検出されます。
    • Yacc/Bison: go.y ファイルは、Yacc(Yet Another Compiler Compiler)またはそのGNU版であるBisonの入力ファイルです。Yaccは、文法規則(プロダクションルール)に基づいて構文解析器(パーサー)を生成するツールです。go.y はGo言語の文法を定義しており、これをYacc/Bisonで処理することで、C言語のソースファイル(y.tab.c など)が生成されます。この生成されたCファイルが、実際の構文解析ロジックを含んでいます。
  3. エラーハンドリングとコンパイラの挙動:

    • コンパイラは、ソースコードにエラーを発見した場合、通常はエラーメッセージを出力します。
    • エラーの深刻度によっては、コンパイルを中断するもの(致命的エラー)と、エラーを報告しつつもコンパイルを続行するもの(非致命的エラー、警告など)があります。
    • yyerror(): Yacc/Bisonによって生成されたパーサー内で使用される関数で、構文エラーが発生した際にエラーメッセージを出力するために呼び出されます。
    • flusherrors(): エラーバッファをフラッシュする(蓄積されたエラーメッセージを出力する)関数であると推測されます。
    • mkpackage("main"): main パッケージを作成または設定する関数であると推測されます。これは、エラー発生時にコンパイラが回復を試みるためのメカニズムでした。
    • errorexit(): エラーが発生した際に、コンパイラプロセスを終了させる関数であると推測されます。

技術的詳細

このコミットの技術的な核心は、Goコンパイラの構文解析フェーズにおけるエラー回復戦略の変更にあります。具体的には、package 宣言がファイルの先頭にないという構文エラーが発生した場合の挙動が変更されています。

変更前は、src/cmd/gc/go.y(Go言語の文法定義ファイル)およびそれから生成される src/cmd/gc/y.tab.c(構文解析器の実装ファイル)において、package 宣言の誤りを検出した際に、以下の処理が行われていました。

yyerror("package statement must be first");
flusherrors();
mkpackage("main");
  • yyerror("package statement must be first"): エラーメッセージを標準エラー出力に出力します。
  • flusherrors(): 蓄積されたエラーメッセージをフラッシュします。
  • mkpackage("main"): ここが重要なポイントで、コンパイラはエラーを検出したにもかかわらず、強制的に現在のコンパイル対象ファイルを main パッケージに属するものとして扱い、コンパイル処理を続行していました。これは、コンパイラがエラーから回復し、可能な限り多くのコードを解析しようとする「エラー回復」の試みの一種です。

このコミットでは、上記の flusherrors(); mkpackage("main"); の行が削除され、代わりに errorexit(); が追加されました。

yyerror("package statement must be first");
errorexit();
  • errorexit(): この関数は、コンパイラプロセスを即座に終了させる役割を担います。つまり、package 宣言の誤りという構文エラーを検出した場合、コンパイラはそれ以上コンパイルを続行せず、致命的なエラーとして処理を中断するようになりました。

この変更により、コンパイラは package 宣言の欠如や誤った配置を、回復不可能なエラーとして扱うようになります。これにより、開発者はこの基本的な構文エラーを早期に、かつ明確に認識できるようになり、その後の不明瞭なエラー連鎖を防ぐことができます。

また、test/fixedbugs/issue4776.go という新しいテストファイルが追加されています。このテストは、package 宣言がないGoソースファイルがコンパイルされた際に、期待通りに「package statement must be first」というエラーが発生し、コンパイルが中断されることを検証するためのものです。// errorcheck コメントは、このファイルがコンパイラのエラーチェックテストであることを示しています。

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

このコミットにおけるコアとなるコードの変更は、Goコンパイラの構文解析器の定義ファイルと、それから生成されるC言語のソースファイルに集中しています。

  1. src/cmd/gc/go.y: Go言語の文法を定義するYacc(またはBison)の入力ファイルです。このファイル内で、package 宣言の構文規則に関連するエラーハンドリング部分が変更されています。

    --- a/src/cmd/gc/go.y
    +++ b/src/cmd/gc/go.y
    @@ -136,8 +136,7 @@ package:
     	{
     		prevlineno = lineno;
     		yyerror("package statement must be first");
    -		flusherrors();
    -		mkpackage("main");
    +		errorexit();
     	}
     |	LPACKAGE sym ';'
     	{
    
  2. src/cmd/gc/y.tab.c: go.y からYacc/Bisonによって自動生成されるC言語のソースファイルです。go.y の変更が反映され、同様のエラーハンドリングロジックが変更されています。

    --- a/src/cmd/gc/y.tab.c
    +++ b/src/cmd/gc/y.tab.c
    @@ -2428,8 +2428,7 @@ yyreduce:
         {
     		prevlineno = lineno;
     		yyerror("package statement must be first");
    -		flusherrors();
    -		mkpackage("main");
    +		errorexit();
     	}
         break;
    
  3. test/fixedbugs/issue4776.go: このコミットによって追加された新しいテストファイルです。package 宣言がないGoソースファイルが、期待通りにエラーを発生させることを検証します。

    --- /dev/null
    +++ b/test/fixedbugs/issue4776.go
    @@ -0,0 +1,10 @@
    +// errorcheck
    +
    +// Copyright 2013 The Go Authors.  All rights reserved.
    +// Use of this source code is governed by a BSD-style
    +// license that can be found in the LICENSE file.
    +
    +// Issue 4776: missing package declaration error should be fatal.
    +
    +type MyInt int32 // ERROR "package statement must be first"
    +
    

コアとなるコードの解説

変更の核心は、go.y および y.tab.c 内の、package 宣言がファイルの先頭にない場合に実行されるコードブロックにあります。

変更前は、このエラーパスでは以下の3つの関数が呼び出されていました。

  • yyerror("package statement must be first"): これは、構文解析器がエラーを検出した際に、指定されたエラーメッセージを報告するための標準的な関数です。
  • flusherrors(): この関数は、コンパイラが内部的に保持しているエラーメッセージのバッファを、実際の出力ストリーム(通常は標準エラー出力)に書き出す役割を担っていたと考えられます。これにより、エラーメッセージがユーザーに表示されます。
  • mkpackage("main"): この関数は、コンパイラがエラーを検出したにもかかわらず、コンパイルを続行するために、現在のコンパイル対象ファイルを強制的に main パッケージに属するものとして設定していました。これは、コンパイラがエラーから回復し、可能な限り多くのコードを解析しようとする試みでした。

変更後、flusherrors();mkpackage("main"); が削除され、代わりに errorexit(); が追加されました。

  • errorexit(): この関数は、コンパイラプロセスを即座に終了させるためのものです。この変更により、package 宣言の誤りというエラーが発生した場合、コンパイラはそれ以上処理を続行せず、直ちに終了するようになりました。

この変更の意図は、package 宣言の欠如や誤った配置が、Go言語のソースファイルの基本的な構造を損なう重大なエラーであるという認識に基づいています。以前の挙動では、コンパイラが package main と仮定して続行することで、その後のコンパイルプロセスでさらに多くの、そしてより混乱を招くようなエラーが発生する可能性がありました。errorexit() を導入することで、コンパイラはこの基本的なエラーに対して厳格な姿勢を取り、開発者に問題の根本原因を早期に、かつ明確に伝えることを目的としています。

test/fixedbugs/issue4776.go は、この新しい挙動を検証するためのテストケースです。このファイルには package 宣言がなく、type MyInt int32 という行が含まれています。// ERROR "package statement must be first" というコメントは、この行で「package statement must be first」というエラーが発生することを期待していることを示しています。このテストが成功すれば、コミットによる変更が正しく機能し、package 宣言がない場合にコンパイラが致命的なエラーを発生させるようになったことが確認できます。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(特に src/cmd/gc ディレクトリ)
  • Yacc/Bisonのドキュメンテーション(構文解析器の生成に関する一般的な情報)
  • Go言語のコンパイラ設計に関する一般的な知識