[インデックス 1153] ファイルの概要
このコミットは、Goコンパイラ(gc
)のソースコードにおける、以下の3つのファイルを変更しています。
src/cmd/gc/go.h
src/cmd/gc/lex.c
src/cmd/gc/subr.c
コミット
yacc
が先読みを行った場合でも、nod
関数で正しい行番号を使用するように修正しました。これにより、セミコロンのないステートメントでも行番号が正しくなります。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4656686cf510469d6c6d6be77a123e5dbf7ec9ab
元コミット内容
use correct lineno in nod even if yacc has looked ahead.
makes lineno correct for statements without semicolons.
R=ken
OCL=19454
CL=19454
変更の背景
このコミットは、Go言語の初期のコンパイラ(gc
)における、ソースコードの行番号の追跡に関するバグを修正することを目的としています。具体的には、yacc
(Yet Another Compiler Compiler)によって生成されたパーサーが、構文解析のためにトークンを「先読み」する際に、現在の行番号(lineno
)が実際のコードの位置とずれてしまう問題がありました。
特に、Go言語ではセミコロンが省略可能な場合が多く、このような構文要素の終わりで行番号が更新されるべきタイミングで、yacc
の先読みによってlineno
が既に次の行を指してしまっていることがありました。その結果、コンパイルエラーメッセージやデバッグ情報において、誤った行番号が報告される可能性がありました。
この修正は、nod
関数(抽象構文木(AST)のノードを作成する関数)が、ノードに正しい行番号を割り当てることを保証するために導入されました。これにより、ユーザーがより正確なエラーメッセージを受け取ることができ、デバッグが容易になります。
前提知識の解説
Goコンパイラ (gc
)
gc
は、Go言語の公式コンパイラであり、Go言語の初期から開発されてきました。Goコンパイラは、ソースコードを解析し、抽象構文木(AST)を構築し、中間表現に変換し、最終的に実行可能なバイナリコードを生成します。このコミットが対象としているのは、そのコンパイルプロセスの初期段階、特に字句解析(lexing)と構文解析(parsing)に関連する部分です。
yacc
(Yet Another Compiler Compiler)
yacc
は、BNF(Backus-Naur Form)のような形式で記述された文法定義から、C言語のソースコードを生成するパーサー生成ツールです。生成されたCコードは、入力ストリームを解析し、文法規則に従って構文木を構築します。yacc
ベースのパーサーは、通常、字句解析器(lexer)と連携して動作します。字句解析器がトークンストリームを提供し、パーサーがそのトークンストリームを文法規則に照らして解析します。
yacc
の重要な特徴の一つに「先読み(lookahead)」があります。これは、パーサーが現在の位置でどの文法規則を適用すべきかを決定するために、入力ストリームの次のトークン(または複数のトークン)を一時的に読み込む機能です。この先読みは、曖昧な文法を解決したり、より効率的な解析パスを選択したりするために不可欠ですが、同時に現在の「論理的な」行番号と、字句解析器が実際に読み進んだ「物理的な」行番号との間にずれを生じさせる可能性があります。
lineno
lineno
は、コンパイラが現在処理しているソースコードの行番号を追跡するために使用される変数です。字句解析器が新しい行の開始を検出するたびに、この変数はインクリメントされます。正確なlineno
は、コンパイルエラーや警告メッセージを生成する際に不可欠であり、デバッグ情報にも使用されます。
nod
関数
nod
関数は、Goコンパイラの内部で抽象構文木(AST)のノードを作成するために使用されるユーティリティ関数です。ASTは、ソースコードの構造を木構造で表現したもので、コンパイラの後の段階(型チェック、最適化、コード生成など)で利用されます。各ASTノードには、それがソースコードのどの部分に対応するかを示す情報(例えば、行番号)が関連付けられています。
技術的詳細
このコミットの核心は、yacc
の先読み動作と、それによって引き起こされるlineno
の不正確さに対処することです。
-
yacc
の先読みとlineno
のずれ:yacc
パーサーは、文法規則を適用する際に、現在のトークンだけでなく、その後のトークンも一時的に参照することがあります。例えば、if
文の後に続く(
や{
などのトークンを先読みすることで、if
文の終わりを正確に判断します。この先読みの過程で、字句解析器は実際にソースコードの次の行に進んでしまうことがあります。しかし、論理的には、現在のステートメントはまだ前の行で終わっていると見なされるべきです。このずれが、nod
関数がASTノードに割り当てる行番号の不正確さにつながっていました。 -
prevlineno
の導入: この問題を解決するために、prevlineno
という新しい変数が導入されました。これは、yylex
関数(字句解析器のメインループ)が新しいトークンを読み込む直前のlineno
の値を保持します。これにより、yacc
が先読みを行ってlineno
が更新されたとしても、prevlineno
には先読み前の、つまり現在のステートメントの「正しい」行番号が保持されることになります。 -
nod
関数での行番号の選択ロジック:nod
関数は、ASTノードを作成する際に、yychar
というyacc
の内部変数を利用して、先読みが行われたかどうかを判断します。yychar <= 0
の場合:これは、yacc
が先読みを行っていない、またはトークンが消費された状態を示します。この場合、現在のlineno
が正確であるため、n->lineno = lineno;
が実行されます。yychar > 0
の場合:これは、yacc
がトークンを先読みしており、まだ消費していない状態を示します。この場合、lineno
は既に次のトークンの行を指している可能性があるため、prevlineno
に保存されていた、先読み前の正しい行番号が使用されます。n->lineno = prevlineno;
が実行されます。
このロジックにより、nod
関数は、yacc
の先読みの有無にかかわらず、常にASTノードにそのノードが表すソースコードの正確な行番号を割り当てることが可能になります。これは、特にセミコロンが省略可能なGo言語の構文において、エラー報告の精度を向上させる上で非常に重要です。
コアとなるコードの変更箇所
src/cmd/gc/go.h
--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -419,6 +419,7 @@ EXTERN Dlist dotlist[10]; // size is max depth of embeddeds
EXTERN Io curio;
EXTERN Io pushedio;
EXTERN int32 lineno;
+EXTERN int32 prevlineno;
EXTERN char* pathname;
EXTERN Hist* hist;
EXTERN Hist* ehist;
prevlineno
という新しいグローバル変数が宣言されました。これは、字句解析器がトークンを読み込む直前の行番号を保持するために使用されます。
src/cmd/gc/lex.c
--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -300,6 +300,8 @@ yylex(void)
int escflag;
Sym *s;
+ prevlineno = lineno;
+
l0:
c = getc();
if(isspace(c))
yylex
関数(字句解析器のメイン関数)の冒頭で、prevlineno = lineno;
という行が追加されました。これにより、新しいトークンを読み込む直前のlineno
の値がprevlineno
に保存されます。
src/cmd/gc/subr.c
--- a/src/cmd/gc/subr.c
+++ b/cmd/gc/subr.c
@@ -269,6 +269,7 @@ dcl(void)
return d;
}
+extern int yychar;
Node*
nod(int op, Node *nleft, Node *nright)
{
@@ -278,7 +279,10 @@ nod(int op, Node *nleft, Node *nright)
n->op = op;
n->left = nleft;
n->right = nright;
- n->lineno = lineno;
+ if(yychar <= 0) // no lookahead
+ n->lineno = lineno;
+ else
+ n->lineno = prevlineno;
return n;
}
nod
関数が変更されました。
extern int yychar;
が追加され、yacc
の内部変数yychar
が利用可能になりました。n->lineno = lineno;
という既存の行が、条件分岐に置き換えられました。yychar <= 0
(先読みがない場合)は、現在のlineno
をそのまま使用します。yychar > 0
(先読みがある場合)は、prevlineno
に保存されていた行番号を使用します。
コアとなるコードの解説
このコミットの主要な変更は、lineno
の正確性を保証するために、字句解析器と構文解析器の連携を改善した点にあります。
-
go.h
でのprevlineno
の宣言:prevlineno
は、lineno
が更新される前の値を一時的に保持するための「退避用」変数として機能します。これにより、yacc
が先読みを行ってlineno
が先行してしまっても、直前の正しい行番号にアクセスできるようになります。 -
lex.c
でのprevlineno
の更新:yylex
関数は、Goコンパイラの字句解析器の心臓部です。この関数が新しいトークンを読み込む直前に、現在のlineno
の値をprevlineno
にコピーします。これは、yylex
が次のトークンを処理し、その結果lineno
がインクリメントされる可能性があるため、その「直前」の行番号を保存しておくことが重要だからです。 -
subr.c
のnod
関数での行番号の決定ロジック:nod
関数は、ASTノードを作成する際に、そのノードがソースコードのどの行に由来するかを示すlineno
フィールドを設定します。yychar
はyacc
が使用する内部変数で、次に処理されるトークンの種類を示します。yychar > 0
の場合、yacc
は既に次のトークンを先読みしており、lineno
がそのトークンの行を指している可能性があります。- このため、
yychar > 0
の場合は、yylex
によって保存されたprevlineno
(先読み前の正しい行番号)を使用します。 yychar <= 0
の場合は、先読みが行われていないか、先読みされたトークンが既に消費されている状態なので、現在のlineno
が正確であると判断し、そのまま使用します。
この一連の変更により、Goコンパイラは、yacc
の先読みというパーサーの内部的な動作に起因する行番号の不正確さを解消し、より堅牢で正確なエラー報告とデバッグ情報を提供できるようになりました。特に、Go言語のセミコロン省略規則のような構文的特徴を持つ言語において、このような行番号の正確性は非常に重要です。
関連リンク
- Go言語の公式ウェブサイト: https://golang.org/
- Goコンパイラのソースコード(GitHub): https://github.com/golang/go
yacc
に関する一般的な情報(Wikipediaなど): https://ja.wikipedia.org/wiki/Yacc
参考にした情報源リンク
- Go言語の初期のコンパイラ設計に関する議論やドキュメント(もし公開されていれば)
yacc
の動作原理、特に先読みに関する技術文書- Go言語の構文解析に関する一般的な情報
- GitHubのコミット履歴と関連するIssue/Pull Request(もしあれば)
- Go言語のコンパイラに関する書籍やオンラインリソース
yychar
の役割に関するyacc
のドキュメント# [インデックス 1153] ファイルの概要
このコミットは、Goコンパイラ(gc
)のソースコードにおける、以下の3つのファイルを変更しています。
src/cmd/gc/go.h
src/cmd/gc/lex.c
src/cmd/gc/subr.c
コミット
yacc
が先読みを行った場合でも、nod
関数で正しい行番号を使用するように修正しました。これにより、セミコロンのないステートメントでも行番号が正しくなります。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4656686cf510469d6c6d6be77a123e5dbf7ec9ab
元コミット内容
use correct lineno in nod even if yacc has looked ahead.
makes lineno correct for statements without semicolons.
R=ken
OCL=19454
CL=19454
変更の背景
このコミットは、Go言語の初期のコンパイラ(gc
)における、ソースコードの行番号の追跡に関するバグを修正することを目的としています。具体的には、yacc
(Yet Another Compiler Compiler)によって生成されたパーサーが、構文解析のためにトークンを「先読み」する際に、現在の行番号(lineno
)が実際のコードの位置とずれてしまう問題がありました。
特に、Go言語ではセミコロンが省略可能な場合が多く、このような構文要素の終わりで行番号が更新されるべきタイミングで、yacc
の先読みによってlineno
が既に次の行を指してしまっていることがありました。その結果、コンパイルエラーメッセージやデバッグ情報において、誤った行番号が報告される可能性がありました。
この修正は、nod
関数(抽象構文木(AST)のノードを作成する関数)が、ノードに正しい行番号を割り当てることを保証するために導入されました。これにより、ユーザーがより正確なエラーメッセージを受け取ることができ、デバッグが容易になります。
前提知識の解説
Goコンパイラ (gc
)
gc
は、Go言語の公式コンパイラであり、Go言語の初期から開発されてきました。Goコンパイラは、ソースコードを解析し、抽象構文木(AST)を構築し、中間表現に変換し、最終的に実行可能なバイナリコードを生成します。このコミットが対象としているのは、そのコンパイルプロセスの初期段階、特に字句解析(lexing)と構文解析(parsing)に関連する部分です。
yacc
(Yet Another Compiler Compiler)
yacc
は、BNF(Backus-Naur Form)のような形式で記述された文法定義から、C言語のソースコードを生成するパーサー生成ツールです。生成されたCコードは、入力ストリームを解析し、文法規則に従って構文木を構築します。yacc
ベースのパーサーは、通常、字句解析器(lexer)と連携して動作します。字句解析器がトークンストリームを提供し、パーサーがそのトークンストリームを文法規則に照らして解析します。
yacc
の重要な特徴の一つに「先読み(lookahead)」があります。これは、パーサーが現在の位置でどの文法規則を適用すべきかを決定するために、入力ストリームの次のトークン(または複数のトークン)を一時的に読み込む機能です。この先読みは、曖昧な文法を解決したり、より効率的な解析パスを選択したりするために不可欠ですが、同時に現在の「論理的な」行番号と、字句解析器が実際に読み進んだ「物理的な」行番号との間にずれを生じさせる可能性があります。
lineno
lineno
は、コンパイラが現在処理しているソースコードの行番号を追跡するために使用される変数です。字句解析器が新しい行の開始を検出するたびに、この変数はインクリメントされます。正確なlineno
は、コンパイルエラーや警告メッセージを生成する際に不可欠であり、デバッグ情報にも使用されます。
nod
関数
nod
関数は、Goコンパイラの内部で抽象構文木(AST)のノードを作成するために使用されるユーティリティ関数です。ASTは、ソースコードの構造を木構造で表現したもので、コンパイラの後の段階(型チェック、最適化、コード生成など)で利用されます。各ASTノードには、それがソースコードのどの部分に対応するかを示す情報(例えば、行番号)が関連付けられています。
技術的詳細
このコミットの核心は、yacc
の先読み動作と、それによって引き起こされるlineno
の不正確さに対処することです。
-
yacc
の先読みとlineno
のずれ:yacc
パーサーは、文法規則を適用する際に、現在のトークンだけでなく、その後のトークンも一時的に参照することがあります。例えば、if
文の後に続く(
や{
などのトークンを先読みすることで、if
文の終わりを正確に判断します。この先読みの過程で、字句解析器は実際にソースコードの次の行に進んでしまうことがあります。しかし、論理的には、現在のステートメントはまだ前の行で終わっていると見なされるべきです。このずれが、nod
関数がASTノードに割り当てる行番号の不正確さにつながっていました。 -
prevlineno
の導入: この問題を解決するために、prevlineno
という新しい変数が導入されました。これは、yylex
関数(字句解析器のメインループ)が新しいトークンを読み込む直前のlineno
の値を保持します。これにより、yacc
が先読みを行ってlineno
が更新されたとしても、prevlineno
には先読み前の、つまり現在のステートメントの「正しい」行番号が保持されることになります。 -
nod
関数での行番号の選択ロジック:nod
関数は、ASTノードを作成する際に、yychar
というyacc
の内部変数を利用して、先読みが行われたかどうかを判断します。yychar <= 0
の場合:これは、yacc
が先読みを行っていない、またはトークンが消費された状態を示します。この場合、現在のlineno
が正確であるため、n->lineno = lineno;
が実行されます。yychar > 0
の場合:これは、yacc
がトークンを先読みしており、まだ消費していない状態を示します。この場合、lineno
は既に次のトークンの行を指している可能性があるため、prevlineno
に保存されていた、先読み前の正しい行番号が使用されます。n->lineno = prevlineno;
が実行されます。
このロジックにより、nod
関数は、yacc
の先読みの有無にかかわらず、常にASTノードにそのノードが表すソースコードの正確な行番号を割り当てることが可能になります。これは、特にセミコロンが省略可能なGo言語の構文において、エラー報告の精度を向上させる上で非常に重要です。
コアとなるコードの変更箇所
src/cmd/gc/go.h
--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -419,6 +419,7 @@ EXTERN Dlist dotlist[10]; // size is max depth of embeddeds
EXTERN Io curio;
EXTERN Io pushedio;
EXTERN int32 lineno;
+EXTERN int32 prevlineno;
EXTERN char* pathname;
EXTERN Hist* hist;
EXTERN Hist* ehist;
prevlineno
という新しいグローバル変数が宣言されました。これは、字句解析器がトークンを読み込む直前の行番号を保持するために使用されます。
src/cmd/gc/lex.c
--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -300,6 +300,8 @@ yylex(void)
int escflag;
Sym *s;
+ prevlineno = lineno;
+
l0:
c = getc();
if(isspace(c))
yylex
関数(字句解析器のメイン関数)の冒頭で、prevlineno = lineno;
という行が追加されました。これにより、新しいトークンを読み込む直前のlineno
の値がprevlineno
に保存されます。
src/cmd/gc/subr.c
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -269,6 +269,7 @@ dcl(void)
return d;
}
+extern int yychar;
Node*
nod(int op, Node *nleft, Node *nright)
{
@@ -278,7 +279,10 @@ nod(int op, Node *nleft, Node *nright)
n->op = op;
n->left = nleft;
n->right = nright;
- n->lineno = lineno;
+ if(yychar <= 0) // no lookahead
+ n->lineno = lineno;
+ else
+ n->lineno = prevlineno;
return n;
}
nod
関数が変更されました。
extern int yychar;
が追加され、yacc
の内部変数yychar
が利用可能になりました。n->lineno = lineno;
という既存の行が、条件分岐に置き換えられました。yychar <= 0
(先読みがない場合)は、現在のlineno
をそのまま使用します。yychar > 0
(先読みがある場合)は、prevlineno
に保存されていた行番号を使用します。
コアとなるコードの解説
このコミットの主要な変更は、lineno
の正確性を保証するために、字句解析器と構文解析器の連携を改善した点にあります。
-
go.h
でのprevlineno
の宣言:prevlineno
は、lineno
が更新される前の値を一時的に保持するための「退避用」変数として機能します。これにより、yacc
が先読みを行ってlineno
が先行してしまっても、直前の正しい行番号にアクセスできるようになります。 -
lex.c
でのprevlineno
の更新:yylex
関数は、Goコンパイラの字句解析器の心臓部です。この関数が新しいトークンを読み込む直前に、現在のlineno
の値をprevlineno
にコピーします。これは、yylex
が次のトークンを処理し、その結果lineno
がインクリメントされる可能性があるため、その「直前」の行番号を保存しておくことが重要だからです。 -
subr.c
のnod
関数での行番号の決定ロジック:nod
関数は、ASTノードを作成する際に、そのノードがソースコードのどの行に由来するかを示すlineno
フィールドを設定します。yychar
はyacc
が使用する内部変数で、次に処理されるトークンの種類を示します。yychar > 0
の場合、yacc
は既に次のトークンを先読みしており、lineno
がそのトークンの行を指している可能性があります。- このため、
yychar > 0
の場合は、yylex
によって保存されたprevlineno
(先読み前の正しい行番号)を使用します。 yychar <= 0
の場合は、先読みが行われていないか、先読みされたトークンが既に消費されている状態なので、現在のlineno
が正確であると判断し、そのまま使用します。
この一連の変更により、Goコンパイラは、yacc
の先読みというパーサーの内部的な動作に起因する行番号の不正確さを解消し、より堅牢で正確なエラー報告とデバッグ情報を提供できるようになりました。特に、Go言語のセミコロン省略規則のような構文的特徴を持つ言語において、このような行番号の正確性は非常に重要です。
関連リンク
- Go言語の公式ウェブサイト: https://golang.org/
- Goコンパイラのソースコード(GitHub): https://github.com/golang/go
yacc
に関する一般的な情報(Wikipediaなど): https://ja.wikipedia.org/wiki/Yacc
参考にした情報源リンク
- Go言語の初期のコンパイラ設計に関する議論やドキュメント(もし公開されていれば)
yacc
の動作原理、特に先読みに関する技術文書- Go言語の構文解析に関する一般的な情報
- GitHubのコミット履歴と関連するIssue/Pull Request(もしあれば)
- Go言語のコンパイラに関する書籍やオンラインリソース
yychar
の役割に関するyacc
のドキュメント