[インデックス 10711] ファイルの概要
このコミットは、Goコンパイラ(gc
)における//line
ディレクティブのファイル名解析に関するバグ修正です。具体的には、ファイル名にコロン(:
)が含まれる場合に、正しく行番号を認識できない問題を解決します。
コミット
commit ecda0fa5d4a448806bdea15e506b11bb71798d8a
Author: Russ Cox <rsc@golang.org>
Date: Mon Dec 12 15:41:54 2011 -0500
gc: allow colon in //line file name
Assume last colon introduces line number.
Fixes #2543.
R=ken2
CC=golang-dev
https://golang.org/cl/5485047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ecda0fa5d4a448806bdea15e506b11bb71798d8a
元コミット内容
gc: allow colon in //line file name
Assume last colon introduces line number.
Fixes #2543.
変更の背景
この変更は、Go言語のIssue 2543「gc: //line directive doesn't handle colons in filenames
」を修正するために行われました。
Go言語のコンパイラには、//line
ディレクティブという特殊なコメント行があります。これは、コンパイラに対して、その後のコードが指定されたファイルと行番号から来ているかのように扱うよう指示するものです。主に、コード生成ツール(例: yacc
やlex
のようなツール、あるいはGoのgo generate
で利用されるツール)が生成したコードのデバッグ情報を元のソースコードにマッピングするために使用されます。これにより、生成されたコードでエラーが発生した場合でも、ユーザーは元のソースコードのどの部分が原因であるかを特定できます。
しかし、従来のgc
コンパイラの実装では、//line
ディレクティブのファイル名部分にコロン(:
)が含まれていると、そのコロンをファイル名と行番号の区切りと誤認識してしまう問題がありました。特にWindows環境では、パスにドライブレター(例: C:\path\to\file.go
)が含まれるため、この問題が顕著でした。C:
のコロンが行番号の区切りと解釈され、ファイル名が途中で切れてしまい、正しいデバッグ情報が生成されない、あるいはコンパイルエラーになる可能性がありました。
このコミットは、この問題を解決し、ファイル名にコロンが含まれていても//line
ディレクティブが正しく機能するようにすることを目的としています。
前提知識の解説
//line
ディレクティブ
//line
ディレクティブは、Go言語のソースコード内で使用される特殊なコメント形式です。その構文は通常、//line filename:line_number
のようになります。
例: //line generated.go:100
このディレクティブは、コンパイラに対して、そのディレクティブ以降のコードが、指定されたfilename
のline_number
から来ているものとして処理するよう指示します。これにより、以下のような利点があります。
- デバッグ情報の改善: コード生成ツールが元のソースコードからGoコードを生成する際、生成されたコードのエラーメッセージやスタックトレースを、元のソースコードのファイル名と行番号にマッピングできます。
- エラー報告の正確性: コンパイルエラーが発生した場合、ユーザーは生成されたコードではなく、元の意図したソースコードの場所を指摘されるため、問題の特定が容易になります。
Goコンパイラ (gc
) の構造と lex.c
Goコンパイラ(gc
)は、Go言語のソースコードを機械語に変換する主要なツールです。その処理は複数のフェーズに分かれています。
- 字句解析 (Lexical Analysis): ソースコードをトークン(キーワード、識別子、演算子など)のストリームに変換します。
- 構文解析 (Syntactic Analysis): トークンのストリームを解析し、抽象構文木(AST)を構築します。
- 型チェック (Type Checking): ASTに対して型の一貫性を検証します。
- 中間コード生成 (Intermediate Code Generation): ASTを中間表現に変換します。
- 最適化 (Optimization): 中間コードを最適化します。
- コード生成 (Code Generation): 最適化された中間コードをターゲットアーキテクチャの機械語に変換します。
このコミットで変更されたsrc/cmd/gc/lex.c
ファイルは、Goコンパイラの字句解析フェーズの一部を担っています。特に、//line
ディレクティブのような特殊なコメントを処理し、その中のファイル名や行番号を抽出するロジックが含まれています。getlinepragma
関数は、この//line
ディレクティブの解析を担当する関数です。
技術的詳細
変更の核心は、getlinepragma
関数が//line
ディレクティブのファイル名と行番号をどのように区別するか、という点にあります。
変更前:
getlinepragma
関数は、//line
ディレクティブのファイル名部分を読み込む際に、最初に現れるコロン(:
)を行番号の区切りと見なしていました。
例えば、//line C:\path\to\file.go:100
というディレクティブがあった場合、コンパイラはC
の後のコロンを区切りと判断し、ファイル名をC
、行番号を\path\to\file.go:100
として解釈しようとしていました。これは明らかに誤りです。
変更後: このコミットでは、この解析ロジックを修正し、最後のコロンが行番号の区切りであると仮定するように変更しました。
linep
という新しいポインタ変数を導入しました。これは、ファイル名部分を読み進める中でコロンが見つかるたびに、そのコロンの位置を記録します。- ファイル名部分の読み込みループは、コロンが見つかっても中断せず、改行文字(
\n
)またはファイルの終端(EOF
)に達するまで続行します。 - ループが終了した後、
linep
が指す位置(つまり、最後に発見されたコロンの位置)にヌル終端文字(\0
)を挿入します。これにより、ファイル名が正しく区切られます。 - 行番号の解析は、
linep
の次の文字から開始されます。行番号は数字のみで構成されるため、その部分を数値としてパースします。
この変更により、C:\path\to\file.go:100
のようなパスでも、最後のコロン(:100
の前のコロン)が正しく行番号の区切りとして認識され、ファイル名がC:\path\to\file.go
、行番号が100
と正確に解析されるようになります。
コアとなるコードの変更箇所
src/cmd/gc/lex.c
ファイルのgetlinepragma
関数における変更点です。
--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -1336,7 +1336,7 @@ static int
getlinepragma(void)
{
int i, c, n;
- char *cp, *ep;
+ char *cp, *ep, *linep;
Hist *h;
for(i=0; i<5; i++) {
@@ -1347,32 +1347,36 @@ getlinepragma(void)
cp = lexbuf;
ep = lexbuf+sizeof(lexbuf)-5;
+ linep = nil;
for(;;) {
c = getr();
- if(c == '\n' || c == EOF)
+ if(c == EOF)
goto out;
+ if(c == '\n')
+ break;
if(c == ' ')
continue;
if(c == ':')
- break;
+ linep = cp;
if(cp < ep)
*cp++ = c;
}
*cp = 0;
+ if(linep == nil || linep >= ep)
+ goto out;
+ *linep++ = '\0';
n = 0;
- for(;;) {
- c = getr();
- if(!yy_isdigit(c))
- break;
- n = n*10 + (c-'0');
+ for(cp=linep; *cp; cp++) {
+ if(*cp < '0' || *cp > '9')
+ goto out;
+ n = n*10 + *cp - '0';
if(n > 1e8) {
yyerror("line number out of range");
errorexit();
}
}
-
- if(c != '\n' || n <= 0)
+ if(n <= 0)
goto out;
// try to avoid allocating file name over and over
コアとなるコードの解説
-
char *cp, *ep, *linep;
:linep
という新しいポインタが追加されました。これは、ファイル名部分を読み込む際に最後に検出されたコロンの位置を記憶するために使用されます。
-
linep = nil;
:linep
が初期化され、コロンがまだ見つかっていない状態を示します。
-
ファイル名解析ループの変更:
if(c == '\n' || c == EOF)
がif(c == EOF)
とif(c == '\n') break;
に分割されました。これにより、改行文字が見つかった場合でも、すぐにループを抜けるのではなく、ファイル名部分の読み込みを完了させ、その後に改行文字を処理するようになりました。if(c == ':') break;
がif(c == ':') linep = cp;
に変更されました。これは最も重要な変更点です。以前はコロンが見つかるとすぐにファイル名の読み込みを中断していましたが、変更後はコロンが見つかっても読み込みを続行し、そのコロンの位置をlinep
に記録するだけになりました。これにより、ファイル名に複数のコロンが含まれていても、最後のコロンが区切りとして扱われるようになります。
-
ファイル名と行番号の区切り処理:
if(linep == nil || linep >= ep) goto out;
が追加されました。これは、コロンが全く見つからなかった場合、またはコロンがバッファの終端近くで見つかり、行番号をパースするスペースがない場合にエラーとして処理するためのガードです。*linep++ = '\0';
が追加されました。これは、linep
が指す最後のコロンの位置にヌル終端文字を挿入することで、ファイル名文字列をそこで区切ります。linep
はその後、行番号の開始位置を指すようにインクリメントされます。
-
行番号解析ループの変更:
- 行番号の解析ロジックが
for(cp=linep; *cp; cp++)
に変更されました。以前はgetr()
で一文字ずつ読み込んでいましたが、変更後はlinep
が指す位置からヌル終端までを文字列として扱い、その中の文字が数字であるかをチェックしながら数値に変換します。これにより、ファイル名と行番号の区切りが明確になったため、行番号部分の解析がより堅牢になりました。
- 行番号の解析ロジックが
-
終了条件の変更:
if(c != '\n' || n <= 0)
がif(n <= 0)
に変更されました。ファイル名解析ループで改行文字の処理が改善されたため、行番号が0以下であるかどうかのチェックのみで十分になりました。
これらの変更により、//line
ディレクティブのファイル名にコロンが含まれていても、最後のコロンが行番号の区切りとして正しく認識され、コンパイラが正確なデバッグ情報を生成できるようになりました。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/ecda0fa5d4a448806bdea15e506b11bb71798d8a
- Go Issue 2543: https://go.dev/issue/2543
- Gerrit Change-ID: https://golang.org/cl/5485047
参考にした情報源リンク
- Go Issue 2543の議論内容
- Go言語の
//line
ディレクティブに関する一般的なドキュメント(Go言語の公式ドキュメントや関連ブログ記事) - Goコンパイラのソースコード(
src/cmd/gc/lex.c
) - 字句解析、構文解析に関する一般的なコンパイラ理論の知識
[インデックス 10711] ファイルの概要
このコミットは、Goコンパイラ(gc
)における//line
ディレクティブのファイル名解析に関するバグ修正です。具体的には、ファイル名にコロン(:
)が含まれる場合に、正しく行番号を認識できない問題を解決します。
コミット
commit ecda0fa5d4a448806bdea15e506b11bb71798d8a
Author: Russ Cox <rsc@golang.org>
Date: Mon Dec 12 15:41:54 2011 -0500
gc: allow colon in //line file name
Assume last colon introduces line number.
Fixes #2543.
R=ken2
CC=golang-dev
https://golang.org/cl/5485047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ecda0fa5d4a448806bdea15e506b11bb71798d8a
元コミット内容
gc: allow colon in //line file name
Assume last colon introduces line number.
Fixes #2543.
変更の背景
この変更は、Go言語のIssue 2543「gc: //line directive doesn't handle colons in filenames
」を修正するために行われました。
Go言語のコンパイラには、//line
ディレクティブという特殊なコメント行があります。これは、コンパイラに対して、その後のコードが指定されたファイルと行番号から来ているかのように扱うよう指示するものです。主に、コード生成ツール(例: yacc
やlex
のようなツール、あるいはGoのgo generate
で利用されるツール)が生成したコードのデバッグ情報を元のソースコードにマッピングするために使用されます。これにより、生成されたコードでエラーが発生した場合でも、ユーザーは元のソースコードのどの部分が原因であるかを特定できます。
しかし、従来のgc
コンパイラの実装では、//line
ディレクティブのファイル名部分にコロン(:
)が含まれていると、そのコロンをファイル名と行番号の区切りと誤認識してしまう問題がありました。特にWindows環境では、パスにドライブレター(例: C:\path\to\file.go
)が含まれるため、この問題が顕著でした。C:
のコロンが行番号の区切りと解釈され、ファイル名が途中で切れてしまい、正しいデバッグ情報が生成されない、あるいはコンパイルエラーになる可能性がありました。
このコミットは、この問題を解決し、ファイル名にコロンが含まれていても//line
ディレクティブが正しく機能するようにすることを目的としています。
前提知識の解説
//line
ディレクティブ
//line
ディレクティブは、Go言語のソースコード内で使用される特殊なコメント形式です。その構文は通常、//line filename:line_number
のようになります。
例: //line generated.go:100
このディレクティブは、コンパイラに対して、そのディレクティブ以降のコードが、指定されたfilename
のline_number
から来ているものとして処理するよう指示します。これにより、以下のような利点があります。
- デバッグ情報の改善: コード生成ツールが元のソースコードからGoコードを生成する際、生成されたコードのエラーメッセージやスタックトレースを、元のソースコードのファイル名と行番号にマッピングできます。
- エラー報告の正確性: コンパイルエラーが発生した場合、ユーザーは生成されたコードではなく、元の意図したソースコードの場所を指摘されるため、問題の特定が容易になります。
Goコンパイラ (gc
) の構造と lex.c
Goコンパイラ(gc
)は、Go言語のソースコードを機械語に変換する主要なツールです。その処理は複数のフェーズに分かれています。
- 字句解析 (Lexical Analysis): ソースコードをトークン(キーワード、識別子、演算子など)のストリームに変換します。
- 構文解析 (Syntactic Analysis): トークンのストリームを解析し、抽象構文木(AST)を構築します。
- 型チェック (Type Checking): ASTに対して型の一貫性を検証します。
- 中間コード生成 (Intermediate Code Generation): ASTを中間表現に変換します。
- 最適化 (Optimization): 中間コードを最適化します。
- コード生成 (Code Generation): 最適化された中間コードをターゲットアーキテクチャの機械語に変換します。
このコミットで変更されたsrc/cmd/gc/lex.c
ファイルは、Goコンパイラの字句解析フェーズの一部を担っています。特に、//line
ディレクティブのような特殊なコメントを処理し、その中のファイル名や行番号を抽出するロジックが含まれています。getlinepragma
関数は、この//line
ディレクティブの解析を担当する関数です。
技術的詳細
変更の核心は、getlinepragma
関数が//line
ディレクティブのファイル名と行番号をどのように区別するか、という点にあります。
変更前:
getlinepragma
関数は、//line
ディレクティブのファイル名部分を読み込む際に、最初に現れるコロン(:
)を行番号の区切りと見なしていました。
例えば、//line C:\path\to\file.go:100
というディレクティブがあった場合、コンパイラはC
の後のコロンを区切りと判断し、ファイル名をC
、行番号を\path\to\file.go:100
として解釈しようとしていました。これは明らかに誤りです。
変更後: このコミットでは、この解析ロジックを修正し、最後のコロンが行番号の区切りであると仮定するように変更しました。
linep
という新しいポインタ変数を導入しました。これは、ファイル名部分を読み進める中でコロンが見つかるたびに、そのコロンの位置を記録します。- ファイル名部分の読み込みループは、コロンが見つかっても中断せず、改行文字(
\n
)またはファイルの終端(EOF
)に達するまで続行します。 - ループが終了した後、
linep
が指す位置(つまり、最後に発見されたコロンの位置)にヌル終端文字(\0
)を挿入します。これにより、ファイル名が正しく区切られます。 - 行番号の解析は、
linep
の次の文字から開始されます。行番号は数字のみで構成されるため、その部分を数値としてパースします。
この変更により、C:\path\to\file.go:100
のようなパスでも、最後のコロン(:100
の前のコロン)が正しく行番号の区切りとして認識され、ファイル名がC:\path\to\file.go
、行番号が100
と正確に解析されるようになります。
コアとなるコードの変更箇所
src/cmd/gc/lex.c
ファイルのgetlinepragma
関数における変更点です。
--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -1336,7 +1336,7 @@ static int
getlinepragma(void)
{
int i, c, n;
- char *cp, *ep;
+ char *cp, *ep, *linep;
Hist *h;
for(i=0; i<5; i++) {
@@ -1347,32 +1347,36 @@ getlinepragma(void)
cp = lexbuf;
ep = lexbuf+sizeof(lexbuf)-5;
+ linep = nil;
for(;;) {
c = getr();
- if(c == '\n' || c == EOF)
+ if(c == EOF)
goto out;
+ if(c == '\n')
+ break;
if(c == ' ')
continue;
if(c == ':')
- break;
+ linep = cp;
if(cp < ep)
*cp++ = c;
}
*cp = 0;
+ if(linep == nil || linep >= ep)
+ goto out;
+ *linep++ = '\0';
n = 0;
- for(;;) {
- c = getr();
- if(!yy_isdigit(c))
- break;
- n = n*10 + (c-'0');
+ for(cp=linep; *cp; cp++) {
+ if(*cp < '0' || *cp > '9')
+ goto out;
+ n = n*10 + *cp - '0';
if(n > 1e8) {
yyerror("line number out of range");
errorexit();
}
}
-
- if(c != '\n' || n <= 0)
+ if(n <= 0)
goto out;
// try to avoid allocating file name over and over
コアとなるコードの解説
-
char *cp, *ep, *linep;
:linep
という新しいポインタが追加されました。これは、ファイル名部分を読み込む際に最後に検出されたコロンの位置を記憶するために使用されます。
-
linep = nil;
:linep
が初期化され、コロンがまだ見つかっていない状態を示します。
-
ファイル名解析ループの変更:
if(c == '\n' || c == EOF)
がif(c == EOF)
とif(c == '\n') break;
に分割されました。これにより、改行文字が見つかった場合でも、すぐにループを抜けるのではなく、ファイル名部分の読み込みを完了させ、その後に改行文字を処理するようになりました。if(c == ':') break;
がif(c == ':') linep = cp;
に変更されました。これは最も重要な変更点です。以前はコロンが見つかるとすぐにファイル名の読み込みを中断していましたが、変更後はコロンが見つかっても読み込みを続行し、そのコロンの位置をlinep
に記録するだけになりました。これにより、ファイル名に複数のコロンが含まれていても、最後のコロンが区切りとして扱われるようになります。
-
ファイル名と行番号の区切り処理:
if(linep == nil || linep >= ep) goto out;
が追加されました。これは、コロンが全く見つからなかった場合、またはコロンがバッファの終端近くで見つかり、行番号をパースするスペースがない場合にエラーとして処理するためのガードです。*linep++ = '\0';
が追加されました。これは、linep
が指す最後のコロンの位置にヌル終端文字を挿入することで、ファイル名文字列をそこで区切ります。linep
はその後、行番号の開始位置を指すようにインクリメントされます。
-
行番号解析ループの変更:
- 行番号の解析ロジックが
for(cp=linep; *cp; cp++)
に変更されました。以前はgetr()
で一文字ずつ読み込んでいましたが、変更後はlinep
が指す位置からヌル終端までを文字列として扱い、その中の文字が数字であるかをチェックしながら数値に変換します。これにより、ファイル名と行番号の区切りが明確になったため、行番号部分の解析がより堅牢になりました。
- 行番号の解析ロジックが
-
終了条件の変更:
if(c != '\n' || n <= 0)
がif(n <= 0)
に変更されました。ファイル名解析ループで改行文字の処理が改善されたため、行番号が0以下であるかどうかのチェックのみで十分になりました。
これらの変更により、//line
ディレクティブのファイル名にコロンが含まれていても、最後のコロンが行番号の区切りとして正しく認識され、コンパイラが正確なデバッグ情報を生成できるようになりました。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/ecda0fa5d4a448806bdea15e506b11bb71798d8a
- Go Issue 2543: https://go.dev/issue/2543
- Gerrit Change-ID: https://golang.org/cl/5485047
参考にした情報源リンク
- Go Issue 2543の議論内容
- Go言語の
//line
ディレクティブに関する一般的なドキュメント(Go言語の公式ドキュメントや関連ブログ記事) - Goコンパイラのソースコード(
src/cmd/gc/lex.c
) - 字句解析、構文解析に関する一般的なコンパイラ理論の知識