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

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

このコミットは、Goコンパイラ(gc)がソースファイルの先頭に存在するUTF-8のバイトオーダーマーク(BOM)を正しく処理できるようにするための変更です。以前は、ソースファイルの先頭にBOMが存在するとコンパイルエラーが発生する可能性がありましたが、この変更により、先頭のBOMは無視されるようになります。ただし、ファイルの途中にBOMが出現した場合は、引き続きエラーとして扱われます。

コミット

gc: initial BOM is legal.
Fixes #4040.

R=rsc
CC=golang-dev
https://golang.org/cl/6497098

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

https://github.com/golang/go/commit/6ce49303652420b55c7c878a924bbcd5e2e8e624

元コミット内容

commit 6ce49303652420b55c7c878a924bbcd5e2e8e624
Author: Rob Pike <r@golang.org>
Date:   Mon Sep 10 13:03:07 2012 -0700

    gc: initial BOM is legal.
    Fixes #4040.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/6497098
---
 src/cmd/gc/lex.c | 12 +++++++++++-\n test/bom.go      | 26 ++++++++++++++++++++++++++\n test/bombad.go   | 18 ++++++++++++++++++\n 3 files changed, 55 insertions(+), 1 deletion(-)\n\ndiff --git a/src/cmd/gc/lex.c b/src/cmd/gc/lex.c\nindex f07a23c150..0788a61514 100644\n--- a/src/cmd/gc/lex.c\n+++ b/src/cmd/gc/lex.c\n@@ -30,6 +30,8 @@ static void\taddidir(char*);\n static int\tgetlinepragma(void);\n static char *goos, *goarch, *goroot;\n \n+#define\tBOM\t0xFEFF\n+\n // Compiler experiments.\n // These are controlled by the GOEXPERIMENT environment\n // variable recorded when the compiler is built.\n@@ -319,6 +321,10 @@ main(int argc, char *argv[])\n \t\tcurio.peekc1 = 0;\n \t\tcurio.nlsemi = 0;\n \n+\t\t// Skip initial BOM if present.\n+\t\tif(Bgetrune(curio.bin) != BOM)\n+\t\t\tBungetrune(curio.bin);\n+\n \t\tblock = 1;\n \t\tiota = -1000000;\n \n@@ -1200,7 +1206,7 @@ talph:\n \t\t\trune = getr();\n \t\t\t// 0xb7 · is used for internal names\n \t\t\tif(!isalpharune(rune) && !isdigitrune(rune) && (importpkg == nil || rune != 0xb7))\n-\t\t\t\tyyerror(\"invalid identifier character 0x%ux\", rune);\n+\t\t\t\tyyerror(\"invalid identifier character U+%04x\", rune);\n \t\t\tcp += runetochar(cp, &rune);\n \t\t} else if(!yy_isalnum(c) && c != \'_\')\n \t\t\tbreak;\n@@ -1583,6 +1589,10 @@ loop:\n \tif(!fullrune(str, i))\n \t\tgoto loop;\n \tc = chartorune(&rune, str);\n+\tif(rune == BOM) {\n+\t\tlineno = lexlineno;\n+\t\tyyerror(\"Unicode (UTF-8) BOM in middle of file\");\n+\t}\n \tif(rune == Runeerror && c == 1) {\n \t\tlineno = lexlineno;\n \t\tyyerror(\"illegal UTF-8 sequence\");\ndiff --git a/test/bom.go b/test/bom.go\nnew file mode 100644\nindex 0000000000..37f73bc5d2\n--- /dev/null\n+++ b/test/bom.go\n@@ -0,0 +1,26 @@\n+// runoutput\n+\n+// Copyright 2011 The Go Authors. All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+// Test source file beginning with a byte order mark.\n+\n+package main\n+\n+import (\n+\t\"fmt\"\n+\t\"strings\"\n+)\n+\n+func main() {\n+\tprog = strings.Replace(prog, \"BOM\", \"\\uFEFF\", -1)\n+\tfmt.Print(prog)\n+}\n+\n+var prog = `BOM\n+package main\n+\n+func main() {\n+}\n+`\ndiff --git a/test/bombad.go b/test/bombad.go\nnew file mode 100644\nindex 0000000000..b894d9ba9f\n--- /dev/null\n+++ b/test/bombad.go\n@@ -0,0 +1,18 @@\n+// errorcheck\n+\n+// Copyright 2012 The Go Authors.  All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+// Here for reference, but hard to test automatically\n+// because the BOM muddles the\n+// processing done by ../run.\n+\n+package main\n+\n+func main() {\n+\t// There\'s a bom here.\t// ERROR \"BOM\"\n+\t// And here.\t// ERROR \"BOM\"\n+\t/* And here.*/\t// ERROR \"BOM\"\n+\tprintln(\"hi there\") // and here\t// ERROR \"BOM\"\n+}\n```

## 変更の背景

このコミットは、Goコンパイラがソースファイルの先頭に存在するバイトオーダーマーク(BOM)を適切に処理できるようにするために行われました。BOMは、特にWindows環境でUTF-8エンコードされたテキストファイルに付加されることがありますが、多くのUnix系システムやプログラミング言語では、UTF-8のBOMは不要であり、むしろ問題を引き起こす可能性があります。

Goコンパイラは、以前はソースファイルの先頭にBOMが存在すると、それを不正な文字として扱い、コンパイルエラーを発生させていました。しかし、一部のユーザーは、エディタの設定や既存のワークフローの都合上、BOM付きのUTF-8ファイルを使用する必要がありました。この問題はGoのIssue #4040として報告され、Go言語の設計思想として、可能な限り多くの環境でスムーズに動作することが重視されるため、この互換性の問題に対処する必要がありました。

この変更の目的は、ソースファイルの先頭にBOMが存在してもコンパイルが成功するようにすることです。ただし、BOMはファイルのエンコーディングを示すためのものであり、ファイルの途中に現れることは通常ありません。そのため、ファイルの途中にBOMが出現した場合は、それが意図しないものである可能性が高いため、引き続きエラーとして扱うことで、不正なファイル形式を検出できるようにしています。

## 前提知識の解説

### BOM (Byte Order Mark)

BOM(バイトオーダーマーク)は、Unicodeテキストファイルの先頭に挿入される特殊なUnicode文字(U+FEFF)です。その主な目的は以下の2つです。

1.  **バイト順序の指定**: UTF-16やUTF-32のようなマルチバイトエンコーディングでは、バイトの並び順(エンディアン)が異なるシステム間でファイルを交換する際に問題が生じる可能性があります。BOMは、そのファイルがビッグエンディアン(BE)でエンコードされているか、リトルエンディアン(LE)でエンコードされているかを示すマーカーとして機能します。
    *   UTF-16 BE: `FE FF`
    *   UTF-16 LE: `FF FE`
    *   UTF-32 BE: `00 00 FE FF`
    *   UTF-32 LE: `FF FE 00 00`

2.  **エンコーディングの識別**: UTF-8の場合、BOMはバイト順序を示す役割は持ちません(UTF-8はバイト順序が固定されているため)。しかし、ファイルがUTF-8でエンコードされていることを示す「署名」として使用されることがあります。UTF-8のBOMは、バイト列としては`EF BB BF`となります。
    *   UTF-8: `EF BB BF`

UTF-8におけるBOMの使用は任意であり、推奨されないことも多いです。特にUnix系システムでは、BOMは通常のデータの一部として扱われ、スクリプトの実行やプログラムのパースに問題を引き起こすことがあります。しかし、Windowsのメモ帳など、一部のテキストエディタはUTF-8ファイルを保存する際にデフォルトでBOMを付加することがあります。

### Go コンパイラ (gc)

`gc`は、Go言語の公式かつ主要なコンパイラです。Goのソースコード(`.go`ファイル)を機械語に変換する役割を担っています。`gc`は、字句解析、構文解析、意味解析、最適化、コード生成といったコンパイルの各段階を実行します。このコミットで変更されている`src/cmd/gc/lex.c`は、`gc`コンパイラの字句解析器(lexer)の一部です。

### 字句解析 (Lexical Analysis / Lexer)

字句解析(またはスキャニング)は、コンパイラの最初の段階です。字句解析器(lexerまたはscanner)は、ソースコードの文字列を読み込み、それを意味のある最小単位である「トークン」(token)のストリームに変換します。例えば、`var x = 10;`というコードは、`var`(キーワード)、`x`(識別子)、`=`(演算子)、`10`(リテラル)、`;`(区切り文字)といったトークンに分解されます。

字句解析器は、空白文字やコメントを無視し、文字列リテラルや数値リテラル、識別子などを認識します。このコミットでは、字句解析器がファイルの先頭にあるBOMを正しく認識し、それを無視して処理を進めるように変更されています。

## 技術的詳細

このコミットは、Goコンパイラの字句解析器(`src/cmd/gc/lex.c`)に以下の主要な変更を加えています。

1.  **BOM定数の定義**:
    `#define BOM 0xFEFF` という行が追加され、UnicodeのBOM文字のコードポイント `U+FEFF` が `BOM` というシンボルとして定義されました。これにより、コード内でBOMを明確に参照できるようになります。

2.  **ファイルの先頭でのBOMのスキップ**:
    `main` 関数内で、ファイルの読み込み開始直後にBOMが存在するかどうかのチェックが追加されました。
    `Bgetrune(curio.bin)` を呼び出して最初のルーン(Unicodeコードポイント)を読み込みます。もしそれが `BOM` と等しくない場合、`Bungetrune(curio.bin)` を呼び出して読み込んだルーンをストリームに戻します。これは、BOMが存在しない場合や、BOM以外の文字がファイルの先頭にある場合に、その文字が後続の字句解析で正しく処理されるようにするためです。BOMが存在した場合は、そのまま読み飛ばされることになります。

3.  **不正な識別子文字のエラーメッセージの改善**:
    `talph` 関数内で、不正な識別子文字に対するエラーメッセージのフォーマットが変更されました。
    `yyerror("invalid identifier character 0x%ux", rune);` から `yyerror("invalid identifier character U+%04x", rune);` へと変更されています。これは機能的な変更ではなく、エラーメッセージの表示形式をより標準的なUnicode表記(`U+XXXX`)に合わせるための改善です。

4.  **ファイルの途中でのBOMの検出とエラー報告**:
    `loop` というラベルが付いたコードブロック内で、ファイルの途中にBOMが出現した場合にエラーを報告するロジックが追加されました。
    `if(rune == BOM)` の条件が追加され、もし読み込んだルーンがBOMであった場合、`yyerror("Unicode (UTF-8) BOM in middle of file");` というエラーメッセージを出力します。これは、BOMがファイルの先頭以外に現れることは通常なく、もし現れた場合は意図しないファイル破損やエンコーディングの問題を示唆するため、開発者に警告を与えるためのものです。

これらの変更により、Goコンパイラは、ソースファイルの先頭にBOMが存在するUTF-8ファイルを問題なく処理できるようになり、同時にファイルの途中にBOMが誤って挿入された場合にはそれを検出して報告するようになりました。

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

### `src/cmd/gc/lex.c`

```c
@@ -30,6 +30,8 @@ static void	addidir(char*);\n static int	getlinepragma(void);\n static char *goos, *goarch, *goroot;\n \n+#define	BOM	0xFEFF\n+\n // Compiler experiments.\n // These are controlled by the GOEXPERIMENT environment\n // variable recorded when the compiler is built.\n@@ -319,6 +321,10 @@ main(int argc, char *argv[])\n \t\tcurio.peekc1 = 0;\n \t\tcurio.nlsemi = 0;\n \n+\t\t// Skip initial BOM if present.\n+\t\tif(Bgetrune(curio.bin) != BOM)\n+\t\t\tBungetrune(curio.bin);\n+\n \t\tblock = 1;\n \t\tiota = -1000000;\n \n@@ -1200,7 +1206,7 @@ talph:\n \t\t\trune = getr();\n \t\t\t// 0xb7 · is used for internal names\n \t\t\tif(!isalpharune(rune) && !isdigitrune(rune) && (importpkg == nil || rune != 0xb7))\n-\t\t\t\tyyerror(\"invalid identifier character 0x%ux\", rune);\n+\t\t\t\tyyerror(\"invalid identifier character U+%04x\", rune);\n \t\t\tcp += runetochar(cp, &rune);\n \t\t} else if(!yy_isalnum(c) && c != \'_\')\n \t\t\tbreak;\n@@ -1583,6 +1589,10 @@ loop:\n \tif(!fullrune(str, i))\n \t\tgoto loop;\n \tc = chartorune(&rune, str);\n+\tif(rune == BOM) {\n+\t\tlineno = lexlineno;\n+\t\tyyerror(\"Unicode (UTF-8) BOM in middle of file\");\n+\t}\n \tif(rune == Runeerror && c == 1) {\n \t\tlineno = lexlineno;\n \t\tyyerror(\"illegal UTF-8 sequence\");\n```

### `test/bom.go` (新規追加)

```go
// runoutput

// Copyright 2011 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.

// Test source file beginning with a byte order mark.

package main

import (
	"fmt"
	"strings"
)

func main() {
	prog = strings.Replace(prog, "BOM", "\uFEFF", -1)
	fmt.Print(prog)
}

var prog = `BOM
package main

func main() {
}
`

test/bombad.go (新規追加)

// errorcheck

// Copyright 2012 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.

// Here for reference, but hard to test automatically
// because the BOM muddles the
// processing done by ../run.

package main

func main() {
	// There's a bom here.	// ERROR "BOM"
	// And here.	// ERROR "BOM"
	/* And here.*/	// ERROR "BOM"
	println("hi there") // and here	// ERROR "BOM"
}

コアとなるコードの解説

src/cmd/gc/lex.c の変更点

  1. #define BOM 0xFEFF: この行は、UnicodeのBOM文字のコードポイント U+FEFFBOM という名前で定義しています。これにより、コード内でBOMを数値リテラルで直接記述する代わりに、より読みやすいシンボリックな名前を使用できるようになります。

  2. main 関数内のBOMスキップロジック:

    // Skip initial BOM if present.
    if(Bgetrune(curio.bin) != BOM)
    	Bungetrune(curio.bin);
    

    これは、ファイルの字句解析を開始する直前、main関数の初期化部分に追加されたコードです。

    • Bgetrune(curio.bin): 入力ストリーム curio.bin から最初のルーン(Unicodeコードポイント)を読み込みます。
    • if( ... != BOM): 読み込んだルーンがBOM(0xFEFF)ではない場合、条件が真となります。
    • Bungetrune(curio.bin): 読み込んだルーンをストリームに戻します。これにより、BOMが存在しない場合や、BOM以外の文字がファイルの先頭にある場合に、その文字が後続の字句解析で正しく処理されるようになります。 もし読み込んだルーンがBOMであった場合、if文の条件は偽となり、Bungetruneは呼び出されません。結果として、BOMは読み飛ばされ、字句解析はBOMの次の文字から開始されます。
  3. talph 関数内のエラーメッセージフォーマット変更:

    -			yyerror("invalid identifier character 0x%ux", rune);
    +			yyerror("invalid identifier character U+%04x", rune);
    

    この変更は、不正な識別子文字が検出された際のエラーメッセージのフォーマットを改善するものです。以前は0x%uxという形式でUnicodeコードポイントを表示していましたが、新しいU+%04xという形式は、Unicodeの標準的な表記(例: U+00A9)に準拠しており、より分かりやすくなっています。これは機能的な変更ではなく、ユーザーエクスペリエンスの向上を目的としています。

  4. loop ブロック内のBOM検出とエラー報告:

    if(rune == BOM) {
    	lineno = lexlineno;
    	yyerror("Unicode (UTF-8) BOM in middle of file");
    }
    

    このコードは、字句解析中に各ルーンを処理するループ内で追加されました。

    • if(rune == BOM): 現在処理しているルーンがBOMであるかどうかをチェックします。
    • lineno = lexlineno;: エラーが発生した行番号を現在の字句解析器の行番号に設定します。
    • yyerror("Unicode (UTF-8) BOM in middle of file");: 「ファイルの途中にUnicode (UTF-8) BOMがあります」というエラーメッセージを出力します。 このロジックにより、ファイルの先頭以外にBOMが出現した場合、それは不正な形式と見なされ、コンパイルエラーが報告されます。これは、BOMがファイルのエンコーディングを示すマーカーとしてのみ機能し、ファイルの途中に現れることは通常ないため、意図しないBOMの挿入を開発者に警告するためのものです。

test/bom.go の解説

このファイルは、ソースファイルの先頭にBOMが存在する場合のGoコンパイラの動作をテストするために新規追加されたものです。 prog 変数にBOMを含むGoのソースコードの文字列を定義し、strings.Replace を使ってプレースホルダーの "BOM" を実際のUnicode BOM文字 \uFEFF に置き換えています。そして、その文字列を標準出力に出力します。このテストは、BOM付きのGoソースファイルがコンパイル可能であることを検証するために使用されます。// runoutput コメントは、このテストが実行結果を検証するタイプであることを示しています。

test/bombad.go の解説

このファイルは、ソースファイルの途中にBOMが存在する場合のGoコンパイラの動作をテストするために新規追加されたものです。 コメントや文字列リテラルの中に意図的にBOM文字を挿入し、それぞれに // ERROR "BOM" というコメントを付けています。これは、GoコンパイラがこれらのBOMを検出し、"BOM" という文字列を含むエラーメッセージを出力することを期待していることを示しています。// errorcheck コメントは、このテストがコンパイルエラーの発生を検証するタイプであることを示しています。このテストは、ファイルの途中にBOMが存在するとコンパイルエラーになるという新しい動作を検証します。

関連リンク

参考にした情報源リンク