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

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

このコミットは、Goコンパイラのパーサー定義ファイルである src/cmd/gc/go.y と、字句解析に関連する src/cmd/gc/lex.c に加え、テストファイル test/bugs/bug085.go および test/bugs/bug129.go の移動と、test/golden.out の更新を含んでいます。主な目的は、Go言語の構文解析におけるバグ修正と、パーサーの曖昧性解消です。

コミット

commit 63985b489b2ad5307de221120df39fbeb66532eb
Author: Russ Cox <rsc@golang.org>
Date:   Thu Mar 5 15:57:03 2009 -0800

    bug085 bug129
    
    R=ken
    OCL=25787
    CL=25791

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

https://github.com/golang/go/commit/63985b489b2ad5307de221120df39fbeb66532eb

元コミット内容

bug085 bug129

R=ken
OCL=25787
CL=25791

変更の背景

このコミットは、Goコンパイラにおける2つの特定のバグ、bug085bug129 の修正を目的としています。

  • bug085: このバグは、Goのパッケージ内での変数スコープと階層に関する「未定義」エラーに関連していました。具体的には、P パッケージ内で宣言された変数 x が、同じパッケージ内の関数 foo からアクセスされた際に「未定義」と誤って報告され、コンパイル時または実行時に「Bus error」を引き起こす可能性がありました。この修正は、Goコンパイラがパッケージスコープ内の変数の可視性を正しく解決することを保証します。
  • bug129: このバグは、ローカル変数名がインポートされたパッケージ名をシャドウイングする際に発生する問題に関連していました。例えば、fmt パッケージをインポートしているにもかかわらず、関数内で fmt という名前のローカル変数を宣言すると、コンパイラがこれを正しく処理できないケースがありました。この修正は、このような変数シャドウイングのケースが正しく識別され、コンパイルエラーや予期せぬ動作を引き起こさないようにすることを目的としています。

これらのバグは、Goコンパイラのパーサーが特定の構文構造を正しく解釈できないことに起因しており、特にYaccによって生成されるパーサーにおける「reduce/reduce conflict」のような曖昧性の問題が関与していた可能性があります。

前提知識の解説

このコミットを理解するためには、以下の概念が重要です。

  • コンパイラのフロントエンド: コンパイラは通常、ソースコードを機械語に変換する複数のフェーズに分かれています。フロントエンドは、ソースコードを解析し、中間表現(抽象構文木など)を生成する部分です。これには、字句解析(Lexical Analysis)と構文解析(Syntax Analysis)が含まれます。
  • 字句解析 (Lexical Analysis): ソースコードを読み込み、意味のある最小単位である「トークン」のストリームに変換するプロセスです。この処理を行うプログラムを「レキサー (Lexer)」または「スキャナー (Scanner)」と呼びます。Goコンパイラでは、src/cmd/gc/lex.c がこの役割の一部を担っていた可能性がありますが、現代のGoコンパイラの字句解析は主にGo言語で書かれた src/cmd/compile/internal/syntax/lex.go で行われています。
  • 構文解析 (Syntax Analysis): トークンのストリームを文法規則に従って解析し、プログラムの構造を表現する「抽象構文木 (Abstract Syntax Tree, AST)」を構築するプロセスです。この処理を行うプログラムを「パーサー (Parser)」と呼びます。
  • Yacc (Yet Another Compiler Compiler): Yaccは、文法定義ファイル(通常 .y 拡張子を持つ)からC言語のパーサーコードを生成するツールです。Goコンパイラの初期段階では、src/cmd/gc/go.y がGo言語の文法を定義するために使用されていました。YaccはLALR(1)パーサーを生成します。
  • reduce/reduce conflict (還元/還元衝突): Yaccのようなパーサー生成器において発生する文法上の曖昧性の一種です。これは、パーサーが入力ストリームの特定の時点で、スタック上のシンボルのシーケンスが複数の異なる文法規則の右辺に一致し、どの規則を使って「還元 (reduce)」すればよいかを決定できない場合に発生します。Yaccはデフォルトで、文法ファイル内で先に定義されている規則を優先して衝突を解決しますが、これは必ずしも意図した動作とは限りません。
  • %prec ディレクティブ: Yaccにおいて、演算子の優先順位や結合規則を明示的に指定するために使用されるディレクティブです。特に、シフト/還元衝突 (shift/reduce conflict) や還元/還元衝突を解決するために、特定の規則に優先順位を割り当てることができます。これにより、文法が曖昧な場合でも、パーサーの動作を制御できます。

技術的詳細

このコミットの技術的詳細の核心は、Goコンパイラのパーサーにおける文法規則の調整と、それに伴う字句解析器の挙動の微調整にあります。

src/cmd/gc/go.y はGo言語の文法をYacc形式で記述したファイルです。このファイルへの変更は、主に以下の問題に対処しています。

  1. 還元/還元衝突の解消: コミットメッセージの差分に明示的に「this rule introduces 1 reduce/reduce conflict with the rule lpack: LPACK above. the reduce/reduce conflict is only with lookahead '.', in which case the correct resolution is the lpack rule. (and it wins because it is above.)」と記述されているように、name 規則と lpack 規則の間で還元/還元衝突が発生していました。これは、LPACK(パッケージ名を表すトークン)が、通常の識別子(name)としても解釈されうる状況で、パーサーがどちらの規則を適用すべきか判断に迷うために起こります。
    • この衝突を解決するために、%left ディレクティブが導入され、NotPackageLPACKAGENotDot.NotParen( の間に優先順位が設定されました。これにより、特定のトークン(例: LPACKAGE.()が出現した場合に、パーサーがどの規則を優先して適用すべきかが明確になります。
    • 特に、name 規則に LPACK %prec NotDot が追加されたことで、LPACK. の前に来た場合に name として還元されることを防ぎ、lpack 規則が優先されるように制御しています。
  2. ラベル名の導入: labelname という新しい型が導入され、name または keyword がラベルとして使用できることを示しています。これにより、goto 文などで使用されるラベルの構文がより柔軟になります。
  3. 文字列リテラルの型推論: pexpr 規則において、文字列リテラル (CTSTR) の場合にその型を types[TSTRING] (文字列型) に明示的に設定する処理が追加されました。これにより、文字列リテラルの型が正しく推論されるようになります。
  4. package 宣言の制約強化: package 規則に %prec NotPackage が追加され、「package statement must be first」というエラーメッセージが出力されるように、package 宣言がファイルの先頭になければならないという制約が強化されました。
  5. laconst 規則の変更: laconst 規則から LPACK のケースが削除されました。これは、LPACK がパッケージ名として扱われるべきであり、定数としては不適切であるという文法的な修正です。

src/cmd/gc/lex.c への変更は、mkpackage 関数内で lookup(package)->lexical = LPACK; という行が追加されたことです。これは、パッケージ名を字句解析器のシンボルテーブルに LPACK として登録することで、その名前がパッケージとして認識されるようにするための変更です。これにより、bug129 のような、ローカル変数がパッケージ名をシャドウイングする問題の解決に寄与します。

test/bugs/bug085.gotest/bugs/bug129.gotest/fixedbugs/ ディレクトリに移動されたのは、これらのバグが修正されたことを示しています。test/golden.out の変更は、これらのバグ修正によってコンパイラの出力(特にエラーメッセージ)が変化したことを反映しています。

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

src/cmd/gc/go.y

--- a/src/cmd/gc/go.y
+++ b/src/cmd/gc/go.y
@@ -55,7 +55,7 @@
 %type	<node>		simple_stmt osimple_stmt range_stmt semi_stmt
 %type	<node>		expr uexpr pexpr expr_list oexpr oexpr_list expr_list_r
 %type	<node>		exprsym3_list_r exprsym3
-%type	<node>		name onew_name new_name new_name_list_r new_field
+%type	<node>		name labelname onew_name new_name new_name_list_r new_field
 %type	<node>		vardcl_list_r vardcl Avardcl Bvardcl
 %type	<node>		interfacedcl_list_r interfacedcl interfacedcl1
 %type	<node>		structdcl_list_r structdcl embed
@@ -95,6 +95,23 @@
 %left			'{'
 %left			Condition
 
+/*
+ * resolve LPACKAGE vs not in favor of LPACKAGE
+ */
+%left			NotPackage
+%left			LPACKAGE
+
+/*
+ * resolve '.' vs not in favor of '.'
+ */
+%left			NotDot
+%left			'.'
+
+/*
+ * resolve '(' vs not in favor of '('
+ */
+%left			NotParen
+%left			'('
 
 %%
 file:
@@ -107,6 +124,7 @@ file:
 	}
 
 package:
+\t%prec NotPackage
 	{\
 	\tyyerror("package statement must be first");
 	\tmkpackage("main");
@@ -776,6 +794,8 @@ pexpr:
 	{\
 	\t$$ = nod(OLITERAL, N, N);
 	\t$$->val = $1;
+\t\tif($1.ctype == CTSTR)
+\t\t\t$$->type = types[TSTRING];
 	}
 |\tlaconst
 	{\
@@ -999,6 +1019,25 @@ name:
 	{\
 	\t$$ = oldname($1);
 	}
+\t/*
+\t * this rule introduces 1 reduce/reduce conflict
+\t * with the rule lpack: LPACK above.
+\t * the reduce/reduce conflict is only with
+\t * lookahead '.', in which case the correct
+\t * resolution is the lpack rule.  (and it wins
+\t * because it is above.)
+\t */
+|\tLPACK	%prec NotDot
+\t{\
+\t\t$$ = oldname($1);
+\t}
+
+labelname:
+\tname
+|\tkeyword
+\t{\
+\t\t$$ = oldname($1);
+\t}
 
 convtype:
 	latype
@@ -1311,6 +1350,7 @@ Afnres:
 	}
 
 Bfnres:
+\t%prec NotParen
 	{\
 	\t$$ = N;
 	}
@@ -1515,7 +1555,7 @@ Astmt:
 	{\
 	\t$$ = N;
 	}
-|\tnew_name ':'
+|\tlabelname ':'
 	{\
 	\t$$ = nod(OLABEL, $1, N);
 	}
@@ -1961,15 +2001,7 @@ lpack:
 	}
 
 laconst:
-\tLPACK
-\t{\
-\t\t// for LALR(1) reasons, using laconst works here
-\t\t// but lname does not.  even so, the messages make
-\t\t// more sense saying "var" instead of "const".
-\t\tyyerror("%s is package, not var", $1->name);
-\t\tYYERROR;
-\t}
-|\tLATYPE
+\tLATYPE
 	{\
 	\tyyerror("%s is type, not var", $1->name);
 	\tYYERROR;

src/cmd/gc/lex.c

--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -1341,6 +1341,9 @@ mkpackage(char* pkg)\
 		\ts->opackage = package;\
 		}\
 
+\t// declare this name as a package
+\tlookup(package)->lexical = LPACK;\
+\
 \tif(outfile == nil) {\
 \t\t// BOTCH need to get .6 from backend\
 \t\tp = strrchr(infile, '/');

test/bugs/bug085.go および test/bugs/bug129.go

これらのファイルは test/bugs/ から test/fixedbugs/ へ移動されました。これは、これらのバグが修正され、テストが「修正済みバグ」のカテゴリに属することを示す標準的なプラクティスです。

コアとなるコードの解説

src/cmd/gc/go.y の変更点

  1. %type <node> name labelname ...:
    • labelname という新しい型が name と並んで追加されました。これは、Go言語のラベル(goto文などで使用)が、通常の識別子だけでなく、キーワードも使用できるようになったことを示唆しています。
  2. %left ディレクティブの追加:
    • NotPackage, LPACKAGE, NotDot, ., NotParen, ( の間に優先順位が設定されました。これは、Yaccパーサーにおける還元/還元衝突やシフト/還元衝突を解決するための典型的な手法です。
      • LPACKAGENotPackage よりも高い優先順位を持つことで、package キーワードが他の識別子と衝突する可能性のある文脈で、package として正しく解釈されるようにします。
      • .NotDot よりも高い優先順位を持つことで、ドット演算子(.)が他の文脈で識別子と衝突する可能性のある文脈で、ドット演算子として正しく解釈されるようにします。
      • (NotParen よりも高い優先順位を持つことで、括弧が他の文脈で識別子と衝突する可能性のある文脈で、括弧として正しく解釈されるようにします。
  3. package: 規則への %prec NotPackage の追加:
    • これにより、package 宣言がファイルの先頭にない場合に yyerror("package statement must be first") が発生するよう、その優先順位が明示的に設定されました。これは、package 宣言の配置に関する文法的な制約を強化するものです。
  4. pexpr: 規則における文字列リテラルの型設定:
    • if($1.ctype == CTSTR) $$->type = types[TSTRING]; の追加により、文字列リテラルが構文解析された際に、そのノードの型が明示的に文字列型 (TSTRING) として設定されるようになりました。これにより、コンパイラが文字列リテラルを正しく型付けし、後続の型チェックフェーズで利用できるようになります。
  5. name: 規則への LPACK %prec NotDot の追加:
    • この変更は、name 規則と lpack 規則の間で発生していた還元/還元衝突を解決するためのものです。コメントにもあるように、ルックアヘッドが . の場合に衝突が発生していましたが、%prec NotDot を使用することで、LPACK. の前に来た場合に name として還元されることを防ぎ、lpack 規則が優先されるようにしています。これにより、package 名が識別子として誤って解釈されることを防ぎます。
  6. labelname: 規則の追加:
    • labelnamename または keyword のいずれかとして定義されました。これは、Go言語のラベルが、通常の識別子だけでなく、予約語(キーワード)も使用できるようになったことを意味します。これにより、goto 文などで使用されるラベルの柔軟性が向上します。
  7. Bfnres: 規則への %prec NotParen の追加:
    • この変更は、関数結果の構文解析における曖昧性を解消するためのものです。%prec NotParen を使用することで、括弧の有無によって異なる解釈がされる可能性のある文脈で、パーサーの動作を明確にしています。
  8. Astmt: 規則における new_name から labelname への変更:
    • new_name ':'labelname ':' に変更されました。これは、ラベルの定義において、new_name(新しい識別子)だけでなく、labelname(識別子またはキーワード)が使用できるようになったことを反映しています。これにより、labelname 規則の導入と一貫性が保たれます。
  9. laconst: 規則からの LPACK の削除:
    • laconst(定数)規則から LPACK(パッケージ名)のケースが削除されました。これは、パッケージ名が定数として扱われるべきではないという文法的な修正です。これにより、"syscall is package, not var" のようなエラーが適切に報告されるようになります。

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

  1. mkpackage 関数への lookup(package)->lexical = LPACK; の追加:
    • mkpackage 関数は、パッケージが宣言されたときに呼び出されます。この変更により、宣言されたパッケージ名が字句解析器のシンボルテーブル内で LPACK(パッケージを表すトークン)として明示的にマークされるようになりました。これにより、字句解析器がその名前を通常の識別子ではなく、パッケージ名として認識するようになります。これは bug129 のような、ローカル変数がパッケージ名をシャドウイングする問題の解決に直接寄与します。例えば、fmt というパッケージがインポートされている場合、fmt という名前が LPACK として登録されることで、コンパイラは fmt をパッケージとして優先的に扱い、ローカル変数との衝突を適切に処理できるようになります。

これらの変更は、Goコンパイラの初期段階における文法解析の堅牢性を高め、特定の構文上の曖昧性やバグを解消するために行われました。特に、Yaccの %prec ディレクティブを効果的に使用して、パーサーの動作を制御している点が注目されます。

関連リンク

参考にした情報源リンク