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

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

このコミットは、Goコンパイラ(cmd/gc)における入力ファイルの終端処理に関するバグ修正です。具体的には、すべての入力ファイルの末尾に改行文字(\n)が確実に挿入されるように変更されました。これにより、特に複数の入力ファイルを処理する際に発生していた構文解析エラーが解消されました。

コミット

commit 27d17255dbe5c620dba1a427c2fd5e2d46cb03f7
Author: Russ Cox <rsc@golang.org>
Date:   Tue Jul 30 10:27:08 2013 -0400

    cmd/gc: insert \n at end of every input file
    
    Not just the first one.
    
    Fixes #5433.
    Fixes #5913.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/12028049

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

https://github.com/golang/go/commit/27d17255dbe5c620dba1a427c2fd5e2d46cb03f7

元コミット内容

Goコンパイラ(cmd/gc)が、入力ファイルの末尾に改行文字を挿入する際の挙動を修正しました。これまでは、最初の入力ファイルにのみ改行が挿入されるか、あるいは複数ファイルの場合に正しく処理されない問題がありました。このコミットは、すべての入力ファイルの末尾に改行が確実に挿入されるようにすることで、この問題を解決します。

この変更は、以下のバグを修正します。

  • Issue 5433: cmd/gc: missing newline at end of file causes parse error
  • Issue 5913: cmd/gc: multiple files without trailing newline cause parse error

変更の背景

Go言語のソースコードは、通常、ファイルの末尾に改行文字を持つことが期待されます。これは、Unix系のシステムにおけるテキストファイルの慣習であり、多くのツール(コンパイラ、リンカ、テキストエディタなど)がこの慣習に依存しています。Goコンパイラも例外ではなく、ファイルの終端が改行で終わることを前提とした構文解析ロジックを持っていました。

しかし、Goコンパイラ(cmd/gc)の以前のバージョンでは、この「ファイルの末尾に改行を挿入する」という処理が、特に複数のソースファイルをまとめてコンパイルする際に、一貫して行われていませんでした。具体的には、最初の入力ファイルには改行が挿入されるものの、それ以降のファイルには挿入されない、あるいは、ファイルが既に改行で終わっている場合の二重挿入の回避が不十分である、といった問題がありました。

この不整合は、以下のような問題を引き起こしました。

  1. 構文解析エラー: ファイルの末尾に改行がない場合、コンパイラの字句解析器(lexer)や構文解析器(parser)が予期しないトークンシーケンスに遭遇し、unexpectedmissing といったエラーを報告することがありました。これは、特にセミコロン自動挿入のルールと関連して問題となることがありました。
  2. 複数ファイルコンパイル時の問題: 複数のGoソースファイルを連結してコンパイルするような内部的な処理において、各ファイルの境界が明確に区切られないため、後続のファイルの先頭が前のファイルの末尾と結合されてしまい、不正な構文として扱われる可能性がありました。

これらの問題は、Issue 5433とIssue 5913として報告されており、このコミットはこれらの問題を解決するために導入されました。

前提知識の解説

このコミットを理解するためには、以下の概念が役立ちます。

  1. Goコンパイラ (cmd/gc): Go言語の公式コンパイラの一つで、Goソースコードを機械語に変換する役割を担います。gcは、字句解析、構文解析、型チェック、最適化、コード生成といったコンパイルの各段階を実行します。
  2. 字句解析器 (Lexer/Scanner): ソースコードを読み込み、意味のある最小単位(トークン)に分割するコンパイラの最初の段階です。例えば、func main() {というコードは、func(キーワード)、main(識別子)、((記号)、)(記号)、{(記号)といったトークンに分割されます。
  3. 構文解析器 (Parser): 字句解析器が生成したトークンのストリームを受け取り、言語の文法規則に従って構文木(AST: Abstract Syntax Tree)を構築します。この段階で、文法的な誤り(構文エラー)が検出されます。
  4. EOF (End Of File): ファイルの終端を示す特殊なマーカーです。多くのプログラミング言語やシステムでは、ファイルの読み込みがEOFに達すると、それ以上のデータがないことを示します。
  5. 改行文字 (\n): テキストファイルにおいて、行の終わりを示す特殊な文字です。Unix系システムではLF(Line Feed)が使われます。
  6. セミコロン自動挿入 (Automatic Semicolon Insertion): Go言語の構文規則の一つで、特定の条件下で改行の後に自動的にセミコロンが挿入されるというものです。これにより、C言語のように明示的にセミコロンを記述する必要が少なくなります。しかし、このルールは改行の位置に厳密に依存するため、ファイルの末尾に改行がない場合に予期せぬ挙動を引き起こすことがあります。
  7. struct Io: Goコンパイラの内部で、入力ストリーム(ファイルなど)を管理するための構造体です。ファイルの読み込み位置、現在の行番号、peekされた文字などの情報が含まれます。

技術的詳細

このコミットの技術的な核心は、Goコンパイラの字句解析器(src/cmd/gc/lex.c)におけるEOF処理の改善にあります。

変更点の詳細:

  1. src/cmd/gc/go.h の変更:

    • struct Io に新しいフィールド int last; が追加されました。
    • last フィールドは、字句解析器が最後に読み込んだ文字を保持するために使用されます。これにより、ファイルの終端に到達した際に、直前の文字が改行であったかどうかを判断できるようになります。
  2. src/cmd/gc/lex.c の変更:

    • main 関数内(コンパイラの初期化部分)で、新しい curio.eofnlcurio.last フィールドが 0 に初期化されます。curio は現在の入力ストリームを表す Io 構造体のインスタンスです。
      • curio.eofnl = 0;
      • curio.last = 0;
      • eofnl は、EOFで改行が既に挿入されたことを示すフラグです。
    • check 関数(字句解析器の主要な読み込みループの一部)の case EOF: ブロックが修正されました。このブロックは、入力ストリームがEOFに達したときに呼び出されます。
      • 変更前: if(curio.eofnl)
        • これは、既にEOFで改行が挿入されている場合に、それ以上処理せずにEOFを返すというロジックでした。
      • 変更後: if(curio.eofnl || curio.last == '\n')
        • この変更により、curio.eofnl が真であるか、または最後に読み込んだ文字が既に改行であった場合にも、EOFを返すようになりました。これにより、ファイルが既に改行で終わっている場合に、余分な改行が挿入されるのを防ぎます。
      • curio.eofnl = 1;
        • EOFで改行を挿入する際に、このフラグを立てます。
      • c = '\n';
        • 実際に改行文字を挿入します。
      • lexlineno++;
        • 行番号をインクリメントします。
    • check 関数の最後に curio.last = c; が追加されました。
      • これは、check 関数が文字を返す直前に、その文字を curio.last に保存することを保証します。これにより、次に check 関数が呼び出されたときに、直前の文字が何であったかを正確に参照できるようになります。
  3. test/fixedbugs/bug435.go の変更:

    • このテストファイルは、意図的にファイルの末尾に改行がない状態で作成されています。コミットでは、ファイルの最後に \ No newline at end of file というコメントが追加されていますが、これはGitがファイルの末尾に改行がないことを示すための特別な表記であり、実際のコードの変更ではありません。このテストは、改行がない場合のコンパイラの挙動を検証するために使用されます。

これらの変更により、Goコンパイラは、入力ファイルの末尾に改行がない場合に自動的に改行を挿入し、かつ、既に改行で終わっている場合には二重に挿入しないという、より堅牢なEOF処理を実現しました。特に、curio.last の導入は、直前の文字のコンテキストを保持することで、このロジックを正確に実装するために不可欠でした。

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

src/cmd/gc/go.h

--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -751,6 +751,7 @@ struct	Io
 	int32	ilineno;
 	int	nlsemi;
 	int	eofnl;
+	int	last;
 	int	peekc;
 	int	peekc1;	// second peekc for ...
 	char*	cp;	// used for content when bin==nil

src/cmd/gc/lex.c

--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -329,6 +329,8 @@ main(int argc, char *argv[])
 		curio.peekc = 0;
 		curio.peekc1 = 0;
 		curio.nlsemi = 0;
+		curio.eofnl = 0;
+		curio.last = 0;
 
 		// Skip initial BOM if present.
 		if(Bgetrune(curio.bin) != BOM)
@@ -1602,7 +1604,7 @@ check:
 		}
 	case EOF:
 		// insert \n at EOF
-		if(curio.eofnl)
+		if(curio.eofnl || curio.last == '\n')
 			return EOF;
 		curio.eofnl = 1;
 		c = '\n';
@@ -1611,6 +1613,7 @@ check:
 			lexlineno++;
 		break;
 	}
+	curio.last = c;
 	return c;
 }

test/fixedbugs/bug435.go

--- a/test/fixedbugs/bug435.go
+++ b/test/fixedbugs/bug435.go
@@ -12,4 +12,4 @@
 package main
 
 func foo() {
-	bar(1, // ERROR "unexpected|missing|undefined"
+	bar(1, // ERROR "unexpected|missing|undefined"
\ No newline at end of file

コアとなるコードの解説

このコミットの主要な変更は、src/cmd/gc/lex.c 内の check 関数に集中しています。

  1. struct Io への last フィールド追加: src/cmd/gc/go.hstruct Ioint last; が追加されました。これは、字句解析器が最後に処理した文字を記憶するためのものです。これにより、EOFに到達した際に、その直前の文字が改行であったかどうかを正確に判断できるようになります。

  2. main 関数での初期化: src/cmd/gc/lex.cmain 関数内で、curio.eofnlcurio.last0 に初期化されます。これは、新しい入力ファイルを処理するたびに、これらの状態がリセットされることを保証します。

  3. check 関数内のEOF処理の修正: check 関数は、Goコンパイラの字句解析器の中核をなす部分で、次の文字を読み込む役割を担っています。case EOF: ブロックは、入力ストリームがファイルの終端に達したときに実行されます。

    • if(curio.eofnl || curio.last == '\n'): この条件文が変更の核心です。
      • curio.eofnl: これは、既にこのファイルに対してEOFでの改行挿入処理が行われたことを示すフラグです。一度挿入されたら、それ以上挿入しないようにします。
      • curio.last == '\n': これが新しく追加された条件です。もし、EOFに到達する直前の文字が既に改行文字であった場合、それはファイルが既に改行で終わっていることを意味します。この場合も、余分な改行を挿入する必要がないため、すぐにEOFを返します。 この論理和 (||) により、二重の改行挿入を防ぎつつ、必要な場合にのみ改行を挿入するという正確な挙動を実現しています。
    • curio.eofnl = 1;: 条件が満たされず、改行を挿入する必要がある場合、このフラグを 1 に設定し、改行が挿入されたことを記録します。
    • c = '\n';: 実際に改行文字を c に設定し、これが次の文字として返されるようにします。
    • lexlineno++;: 改行が挿入されたため、字句解析器の行番号をインクリメントします。
  4. curio.last = c; の追加: check 関数の最後にこの行が追加されました。これにより、check 関数が文字 c を返す直前に、その文字が curio.last に保存されます。この last の値は、次に check 関数が呼び出されたときに、上記のEOF処理ロジックで利用されます。

これらの変更により、Goコンパイラは、単一または複数の入力ファイルに対して、ファイルの末尾に改行文字が適切に存在することを保証し、Go言語のセミコロン自動挿入ルールやその他の構文解析ロジックが正しく機能するようにしています。

関連リンク

参考にした情報源リンク