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

[インデックス 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の不正確さに対処することです。

  1. yaccの先読みとlinenoのずれ: yaccパーサーは、文法規則を適用する際に、現在のトークンだけでなく、その後のトークンも一時的に参照することがあります。例えば、if文の後に続く({などのトークンを先読みすることで、if文の終わりを正確に判断します。この先読みの過程で、字句解析器は実際にソースコードの次の行に進んでしまうことがあります。しかし、論理的には、現在のステートメントはまだ前の行で終わっていると見なされるべきです。このずれが、nod関数がASTノードに割り当てる行番号の不正確さにつながっていました。

  2. prevlinenoの導入: この問題を解決するために、prevlinenoという新しい変数が導入されました。これは、yylex関数(字句解析器のメインループ)が新しいトークンを読み込む直前のlinenoの値を保持します。これにより、yaccが先読みを行ってlinenoが更新されたとしても、prevlinenoには先読み前の、つまり現在のステートメントの「正しい」行番号が保持されることになります。

  3. 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の正確性を保証するために、字句解析器と構文解析器の連携を改善した点にあります。

  1. go.hでのprevlinenoの宣言: prevlinenoは、linenoが更新される前の値を一時的に保持するための「退避用」変数として機能します。これにより、yaccが先読みを行ってlinenoが先行してしまっても、直前の正しい行番号にアクセスできるようになります。

  2. lex.cでのprevlinenoの更新: yylex関数は、Goコンパイラの字句解析器の心臓部です。この関数が新しいトークンを読み込む直前に、現在のlinenoの値をprevlinenoにコピーします。これは、yylexが次のトークンを処理し、その結果linenoがインクリメントされる可能性があるため、その「直前」の行番号を保存しておくことが重要だからです。

  3. subr.cnod関数での行番号の決定ロジック: nod関数は、ASTノードを作成する際に、そのノードがソースコードのどの行に由来するかを示すlinenoフィールドを設定します。

    • yycharyaccが使用する内部変数で、次に処理されるトークンの種類を示します。yychar > 0の場合、yaccは既に次のトークンを先読みしており、linenoがそのトークンの行を指している可能性があります。
    • このため、yychar > 0の場合は、yylexによって保存されたprevlineno(先読み前の正しい行番号)を使用します。
    • yychar <= 0の場合は、先読みが行われていないか、先読みされたトークンが既に消費されている状態なので、現在のlinenoが正確であると判断し、そのまま使用します。

この一連の変更により、Goコンパイラは、yaccの先読みというパーサーの内部的な動作に起因する行番号の不正確さを解消し、より堅牢で正確なエラー報告とデバッグ情報を提供できるようになりました。特に、Go言語のセミコロン省略規則のような構文的特徴を持つ言語において、このような行番号の正確性は非常に重要です。

関連リンク

参考にした情報源リンク

  • 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の不正確さに対処することです。

  1. yaccの先読みとlinenoのずれ: yaccパーサーは、文法規則を適用する際に、現在のトークンだけでなく、その後のトークンも一時的に参照することがあります。例えば、if文の後に続く({などのトークンを先読みすることで、if文の終わりを正確に判断します。この先読みの過程で、字句解析器は実際にソースコードの次の行に進んでしまうことがあります。しかし、論理的には、現在のステートメントはまだ前の行で終わっていると見なされるべきです。このずれが、nod関数がASTノードに割り当てる行番号の不正確さにつながっていました。

  2. prevlinenoの導入: この問題を解決するために、prevlinenoという新しい変数が導入されました。これは、yylex関数(字句解析器のメインループ)が新しいトークンを読み込む直前のlinenoの値を保持します。これにより、yaccが先読みを行ってlinenoが更新されたとしても、prevlinenoには先読み前の、つまり現在のステートメントの「正しい」行番号が保持されることになります。

  3. 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の正確性を保証するために、字句解析器と構文解析器の連携を改善した点にあります。

  1. go.hでのprevlinenoの宣言: prevlinenoは、linenoが更新される前の値を一時的に保持するための「退避用」変数として機能します。これにより、yaccが先読みを行ってlinenoが先行してしまっても、直前の正しい行番号にアクセスできるようになります。

  2. lex.cでのprevlinenoの更新: yylex関数は、Goコンパイラの字句解析器の心臓部です。この関数が新しいトークンを読み込む直前に、現在のlinenoの値をprevlinenoにコピーします。これは、yylexが次のトークンを処理し、その結果linenoがインクリメントされる可能性があるため、その「直前」の行番号を保存しておくことが重要だからです。

  3. subr.cnod関数での行番号の決定ロジック: nod関数は、ASTノードを作成する際に、そのノードがソースコードのどの行に由来するかを示すlinenoフィールドを設定します。

    • yycharyaccが使用する内部変数で、次に処理されるトークンの種類を示します。yychar > 0の場合、yaccは既に次のトークンを先読みしており、linenoがそのトークンの行を指している可能性があります。
    • このため、yychar > 0の場合は、yylexによって保存されたprevlineno(先読み前の正しい行番号)を使用します。
    • yychar <= 0の場合は、先読みが行われていないか、先読みされたトークンが既に消費されている状態なので、現在のlinenoが正確であると判断し、そのまま使用します。

この一連の変更により、Goコンパイラは、yaccの先読みというパーサーの内部的な動作に起因する行番号の不正確さを解消し、より堅牢で正確なエラー報告とデバッグ情報を提供できるようになりました。特に、Go言語のセミコロン省略規則のような構文的特徴を持つ言語において、このような行番号の正確性は非常に重要です。

関連リンク

参考にした情報源リンク

  • Go言語の初期のコンパイラ設計に関する議論やドキュメント(もし公開されていれば)
  • yaccの動作原理、特に先読みに関する技術文書
  • Go言語の構文解析に関する一般的な情報
  • GitHubのコミット履歴と関連するIssue/Pull Request(もしあれば)
  • Go言語のコンパイラに関する書籍やオンラインリソース
  • yycharの役割に関するyaccのドキュメント