[インデックス 1581] ファイルの概要
このコミットは、Go言語のコンパイラの一部である src/cmd/gc/go.y
ファイルに対する変更です。go.y
は、Go言語の構文解析器(パーサー)を生成するためのYacc(Yet Another Compiler-Compiler)またはBisonの文法定義ファイルです。Goコンパイラは、このファイルに記述された文法規則に基づいてGoのソースコードを解析し、抽象構文木(AST)を構築します。具体的には、for
ループ、特に for...range
構文の解析に関連する文法規則が変更されています。
コミット
このコミットは、Go言語のコンパイラにおける range
構文の文法定義を整理し、クリーンアップすることを目的としています。具体的には、orange_stmt
という非終端記号を range_stmt
に改名し、for
ループのヘッダー部分の文法規則を簡素化しています。これにより、コンパイラのコードベースの可読性と保守性が向上し、将来的な言語機能の拡張に備えるものと考えられます。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3ec4675220f18c3bc6680e71c6ce09a76b641c8c
元コミット内容
commit 3ec4675220f18c3bc6680e71c6ce09a76b641c8c
Author: Russ Cox <rsc@golang.org>
Date: Wed Jan 28 15:41:50 2009 -0800
clean up range grammar
R=ken
OCL=23712
CL=23714
---
src/cmd/gc/go.y | 27 ++++++++++-----------------
1 file changed, 10 insertions(+), 17 deletions(-)
diff --git a/src/cmd/gc/go.y b/src/cmd/gc/go.y
index 4aafd0b0c1..d04991dc47 100644
--- a/src/cmd/gc/go.y
+++ b/src/cmd/gc/go.y
@@ -52,7 +52,7 @@
%type <node> Astmt Bstmt
%type <node> for_stmt for_body for_header
%type <node> if_stmt if_body if_header select_stmt
-%type <node> simple_stmt osimple_stmt orange_stmt semi_stmt
+%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
@@ -536,9 +536,8 @@ compound_stmt:
popdcl();
}
-orange_stmt:
- osimple_stmt
-| exprsym3_list_r '=' LRANGE expr
+range_stmt:
+ exprsym3_list_r '=' LRANGE expr
{
$$ = nod(ORANGE, $1, $4);
$$->etype = 0; // := flag
@@ -550,14 +549,8 @@ orange_stmt:
}
for_header:
-\tosimple_stmt ';' orange_stmt ';' osimple_stmt
+\tosimple_stmt ';' osimple_stmt ';' osimple_stmt
{
-\t\tif($3 != N && $3->op == ORANGE) {\n-\t\t\t$$ = dorange($3);\n-\t\t\t$$->ninit = list($$->ninit, $1);\n-\t\t\t$$->nincr = list($$->nincr, $5);\n-\t\t\tbreak;\n-\t\t}\n \t\t// init ; test ; incr
\t\tif($5 != N && $5->colas != 0)
\t\t\tyyerror("cannot declare in the for-increment");
\t\t$$ = nod(OFOR, N, N);
@@ -566,19 +559,19 @@ for_header:
\t\t$$->ntest = $3;
\t\t$$->nincr = $5;
}
-|\torange_stmt
+|\tosimple_stmt
{
-\t\t// range
-\t\tif($1 != N && $1->op == ORANGE) {\n-\t\t\t$$ = dorange($1);\n-\t\t\tbreak;\n-\t\t}\n \t\t// normal test
\t\t$$ = nod(OFOR, N, N);
\t\t$$->ninit = N;
\t\t$$->ntest = $1;
\t\t$$->nincr = N;
}
+|\trange_stmt
+\t{\n+\t\t$$ = dorange($1);\n+\t\taddtotop($$);\n+\t}\
for_body:
for_header compound_stmt
変更の背景
このコミットが行われた2009年1月は、Go言語がまだ一般に公開される前の非常に初期の開発段階でした。Go言語の設計と実装は活発に進められており、文法や内部構造は頻繁に変更されていました。
この「clean up range grammar」というコミットメッセージは、for...range
ループの文法解析に関する既存の実装が、何らかの理由で最適ではない、あるいは将来の拡張性を考慮すると問題があると考えられたことを示唆しています。具体的な背景としては、以下のような点が考えられます。
- 文法の整理と簡素化:
orange_stmt
という名前がrange
構文を直接的に示していないため、より分かりやすいrange_stmt
への改名が行われた可能性があります。また、for_header
内でのorange_stmt
の扱いが冗長であったり、他のfor
ループの形式と統合されていなかったりしたため、文法規則を整理し、より一貫性のある解析ロジックを目指したと考えられます。 - コードの可読性と保守性の向上: コンパイラのコードベースは複雑になりがちです。文法定義をクリーンアップすることで、将来の機能追加やバグ修正が容易になります。
- 解析効率の改善: 文法規則の整理は、パーサーの効率をわずかに向上させる可能性もあります。特に、曖昧な規則や重複する規則を排除することで、解析時のバックトラックを減らすことができます。
- 言語仕様の進化への対応: Go言語の
for...range
構文は、スライス、配列、文字列、マップ、チャネルなど、様々なデータ構造をイテレートするために使用されます。初期の段階では、これらのイテレーションのセマンティクスがまだ完全に固まっていなかった可能性があり、文法を整理することで、将来の言語仕様の変更に柔軟に対応できるようにしたと考えられます。
要するに、この変更は、Go言語の for...range
構文の解析をより堅牢で、理解しやすく、保守しやすいものにするための、初期段階における重要なリファクタリングの一環であったと言えます。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
1. Yacc/Bisonと文法定義
go.y
ファイルは、Yacc(Yet Another Compiler-Compiler)またはそのGNU版であるBisonの入力ファイルです。Yacc/Bisonは、BNF(Backus-Naur Form)に似た形式で記述された文法規則から、C言語(または他の言語)の構文解析器(パーサー)を自動生成するツールです。
- 文法規則(Grammar Rules): プログラミング言語の構文を定義する一連の規則です。例えば、「式は項の後に演算子と項が続く」といった形です。
- 非終端記号(Non-terminal Symbols): 文法規則の左辺に現れる記号で、さらに別の規則によって展開される記号です。例:
expression
,statement
,for_header
。 - 終端記号(Terminal Symbols): 文法規則の右辺に現れる記号で、これ以上展開されない記号です。通常、字句解析器(レキサー)によって識別されるトークンに対応します。例:
ID
(識別子),NUMBER
(数値),'+'
,';'
,LRANGE
(Goのrange
キーワードに対応するトークン)。 - アクション(Actions): 各文法規則の右辺に付随するC言語のコードブロックです。パーサーがその規則にマッチしたときに実行されます。通常、抽象構文木(AST)のノードを構築したり、セマンティックチェックを行ったりします。
$$
: 現在の規則の左辺の非終端記号に対応する値(通常はASTノードへのポインタ)を表します。$1
,$2
, ...: 現在の規則の右辺の記号に対応する値(ASTノードへのポインタなど)を表します。$1
は最初の記号、$2
は2番目の記号、といった具合です。
%type <node>
: Yacc/Bisonのディレクティブで、特定の非終端記号がどのような型の値を保持するかを宣言します。ここでは、node
型(おそらくASTノードを表す構造体へのポインタ)を保持することを示しています。
2. Go言語の for
ループと for...range
Go言語には、いくつかの for
ループの形式があります。
- 通常の
for
ループ: C言語やJavaに似た形式で、初期化ステートメント、条件式、後処理ステートメントを持ちます。for init; condition; post { // ... }
- 条件式のみの
for
ループ:for condition { // ... }
- 無限ループ:
for { // ... }
for...range
ループ: 配列、スライス、文字列、マップ、チャネルなどの要素をイテレートするために使用されます。
またはfor key, value := range collection { // ... }
またはfor _, value := range collection { // ... }
for key := range collection { // ... }
このコミットは、特に for...range
構文の解析に関連しています。
3. Goコンパイラの内部構造(初期段階)
Goコンパイラは、ソースコードを機械語に変換する過程で、以下の主要なフェーズを経ます。
- 字句解析(Lexical Analysis): ソースコードをトークン(キーワード、識別子、演算子など)のストリームに変換します。
- 構文解析(Syntax Analysis): トークンのストリームを文法規則に基づいて解析し、抽象構文木(AST)を構築します。
go.y
がこのフェーズで使用されます。 - 意味解析(Semantic Analysis): ASTに対して型チェック、名前解決、スコープチェックなどの意味的な検証を行います。
- 中間コード生成(Intermediate Code Generation): ASTをより低レベルの中間表現に変換します。
- 最適化(Optimization): 中間コードを最適化します。
- コード生成(Code Generation): 最終的な機械語コードを生成します。
このコミットは、構文解析フェーズにおける for...range
構文のAST構築ロジックに影響を与えます。
nod(OP, Left, Right)
: ASTノードを作成する関数。OP
はノードの種類(例:ORANGE
はfor...range
ループを表す内部オペレーションコード)、Left
とRight
はそのノードの子ノードです。$$->ninit
,$$->ntest
,$$->nincr
:for
ループのASTノードのフィールドで、それぞれ初期化ステートメント、条件式、インクリメントステートメントに対応する子ノードを格納します。dorange($1)
:range
構文のASTノードを処理し、適切なfor
ループのAST構造に変換する関数。addtotop($$)
: 生成されたASTノードを、現在のスコープのトップレベルに追加する関数。list(list_head, new_node)
: ASTノードのリストに新しいノードを追加する関数。N
:nil
またはnull
に相当する、ノードが存在しないことを示す定数。yyerror("message")
: 構文エラーを報告する関数。
技術的詳細
このコミットの主要な変更点は、go.y
ファイルにおける range
構文の文法規則の再定義と、それに伴う for_header
規則の簡素化です。
1. orange_stmt
から range_stmt
への改名と定義の変更
変更前:
orange_stmt:
osimple_stmt
| exprsym3_list_r '=' LRANGE expr
{
$$ = nod(ORANGE, $1, $4);
$$->etype = 0; // := flag
// ...
}
orange_stmt
は、osimple_stmt
(おそらくオプションのシンプルなステートメント)または exprsym3_list_r '=' LRANGE expr
のいずれかとして定義されていました。後者の規則は for key, value := range collection
のような range
構文に対応します。
変更後:
range_stmt:
exprsym3_list_r '=' LRANGE expr
{
$$ = nod(ORANGE, $1, $4);
$$->etype = 0; // := flag
// ...
}
orange_stmt
という非終端記号がrange_stmt
に改名されました。これは、その役割をより明確にするための命名規則の改善です。osimple_stmt
の選択肢がrange_stmt
から削除されました。これにより、range_stmt
は純粋にfor...range
構文の右辺(key, value := range collection
の部分)のみを表現するようになりました。これは、range
構文がfor
ループのヘッダー内でしか意味を持たないため、より厳密な文法定義になったと言えます。
アクションブロック $$ = nod(ORANGE, $1, $4);
は、ORANGE
という種類のASTノードを作成し、その子ノードとして $1
(exprsym3_list_r
、つまり key, value
の部分)と $4
(expr
、つまり collection
の部分)を設定しています。$$->etype = 0; // := flag
は、おそらく :=
演算子による変数宣言が行われたことを示すフラグを設定しています。
2. for_header
規則の簡素化と range_stmt
の統合
for_header
は、for
ループの括弧内の部分(例: init; condition; post
や range collection
)を解析する規則です。
変更前:
for_header:
osimple_stmt ';' orange_stmt ';' osimple_stmt
{
// ... init ; test ; incr ...
if($3 != N && $3->op == ORANGE) {
$$ = dorange($3);
$$->ninit = list($$->ninit, $1);
$$->nincr = list($$->nincr, $5);
break;
}
// ... normal test ...
}
| orange_stmt
{
// range
if($1 != N && $1->op == ORANGE) {
$$ = dorange($1);
break;
}
// normal test
// ...
}
変更前の for_header
は、for init; test; incr
形式と for range
形式の両方を同じ規則内で処理しようとしていました。特に、osimple_stmt ';' orange_stmt ';' osimple_stmt
の規則内で、$3
が ORANGE
オペレーションを持つ場合に range
ループとして特別扱いするロジックが含まれていました。これは、文法規則とセマンティックアクションが密結合しており、読みにくい構造でした。
変更後:
for_header:
osimple_stmt ';' osimple_stmt ';' osimple_stmt
{
// init ; test ; incr
if($5 != N && $5->colas != 0)
yyerror("cannot declare in the for-increment");
$$ = nod(OFOR, N, N);
$$->ninit = $1;
$$->ntest = $3;
$$->nincr = $5;
}
| osimple_stmt
{
// normal test
$$ = nod(OFOR, N, N);
$$->ninit = N;
$$->ntest = $1;
$$->nincr = N;
}
| range_stmt
{
$$ = dorange($1);
addtotop($$);
}
for_header
の最初の規則osimple_stmt ';' osimple_stmt ';' osimple_stmt
は、純粋にfor init; test; incr
形式のループを処理するように簡素化されました。以前のif($3 != N && $3->op == ORANGE)
のようなrange
構文の特殊処理が削除されました。これにより、この規則は通常のfor
ループの解析に特化し、より明確になりました。for_header
の2番目の規則osimple_stmt
は、for condition
形式のループを処理します。以前のorange_stmt
を含む複雑なロジックが削除され、シンプルな条件式のみのループに特化しました。- 新しい規則
range_stmt
がfor_header
に追加されました。これにより、for...range
構文は独立した規則として明確に定義され、解析ロジックが分離されました。$$ = dorange($1);
:range_stmt
から受け取ったASTノード($1
)をdorange
関数で処理し、最終的なfor
ループのASTノードを構築します。addtotop($$);
: 生成されたfor...range
ループのASTノードを、現在のスコープのトップレベルに追加します。これは、for...range
ループが新しいスコープを導入する可能性があるため、そのスコープ内で宣言された変数を適切に処理するために必要です。
変更の意図と効果
この変更は、Go言語の for
ループ、特に for...range
構文の解析ロジックを大幅に改善しています。
- 文法の明確化:
range_stmt
がfor...range
構文の右辺のみを表現するように特化され、for_header
からrange
構文の特殊処理が分離されたことで、文法規則がより明確で理解しやすくなりました。 - 関心の分離: 各文法規則が特定の種類の
for
ループに特化することで、コードの関心事が分離され、保守性が向上しました。 - エラーハンドリングの改善:
yyerror("cannot declare in the for-increment")
のようなエラーチェックが、適切な文法規則のコンテキストで行われるようになりました。 - 将来の拡張性: 文法が整理されたことで、将来的に
for
ループやrange
構文に新しい機能が追加された場合でも、既存のコードに大きな影響を与えることなく変更を加えやすくなります。
このコミットは、Go言語のコンパイラが初期段階でどのように進化し、文法定義が洗練されていったかを示す良い例です。
コアとなるコードの変更箇所
diff --git a/src/cmd/gc/go.y b/src/cmd/gc/go.y
index 4aafd0b0c1..d04991dc47 100644
--- a/src/cmd/gc/go.y
+++ b/src/cmd/gc/go.y
@@ -52,7 +52,7 @@
%type <node> Astmt Bstmt
%type <node> for_stmt for_body for_header
%type <node> if_stmt if_body if_header select_stmt
-%type <node> simple_stmt osimple_stmt orange_stmt semi_stmt
+%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
@@ -536,9 +536,8 @@ compound_stmt:
popdcl();
}
-orange_stmt:
- osimple_stmt
-| exprsym3_list_r '=' LRANGE expr
+range_stmt:
+ exprsym3_list_r '=' LRANGE expr
{
$$ = nod(ORANGE, $1, $4);
$$->etype = 0; // := flag
@@ -550,14 +549,8 @@ orange_stmt:
}
for_header:
-\tosimple_stmt ';' orange_stmt ';' osimple_stmt
+\tosimple_stmt ';' osimple_stmt ';' osimple_stmt
{
-\t\tif($3 != N && $3->op == ORANGE) {\n-\t\t\t$$ = dorange($3);\n-\t\t\t$$->ninit = list($$->ninit, $1);\n-\t\t\t$$->nincr = list($$->nincr, $5);\n-\t\t\tbreak;\n-\t\t}\n \t\t// init ; test ; incr
\t\tif($5 != N && $5->colas != 0)
\t\t\tyyerror("cannot declare in the for-increment");
\t\t$$ = nod(OFOR, N, N);
@@ -566,19 +559,19 @@ for_header:
\t\t$$->ntest = $3;
\t\t$$->nincr = $5;
}
-|\torange_stmt
+|\tosimple_stmt
{
-\t\t// range
-\t\tif($1 != N && $1->op == ORANGE) {\n-\t\t\t$$ = dorange($1);\n-\t\t\tbreak;\n-\t\t}\n \t\t// normal test
\t\t$$ = nod(OFOR, N, N);
\t\t$$->ninit = N;
\t\t$$->ntest = $1;
\t\t$$->nincr = N;
}
+|\trange_stmt
+\t{\n+\t\t$$ = dorange($1);\n+\t\taddtotop($$);\n+\t}\
for_body:
for_header compound_stmt
コアとなるコードの解説
1. %type
宣言の変更
- %type <node> simple_stmt osimple_stmt orange_stmt semi_stmt
+ %type <node> simple_stmt osimple_stmt range_stmt semi_stmt
- 変更内容:
orange_stmt
という非終端記号の型宣言がrange_stmt
に変更されました。 - 解説: これは、
orange_stmt
がrange_stmt
に改名されたことに伴う、Yacc/Bisonの型宣言の更新です。これにより、パーサーがrange_stmt
を処理する際に、それがnode
型の値を返すことを認識します。
2. orange_stmt
規則の削除と range_stmt
規則の追加
-orange_stmt:
- osimple_stmt
-| exprsym3_list_r '=' LRANGE expr
+range_stmt:
+ exprsym3_list_r '=' LRANGE expr
- 変更内容:
orange_stmt
という名前の規則が削除され、代わりにrange_stmt
という新しい名前の規則が追加されました。また、orange_stmt
にあったosimple_stmt
という選択肢がrange_stmt
から削除されました。 - 解説:
- 命名の明確化:
orange_stmt
はrange
構文を指す非公式な名前だったと考えられます。これをrange_stmt
に変更することで、その役割がより直感的に理解できるようになりました。 - 文法の厳密化: 以前の
orange_stmt
はosimple_stmt
(おそらく単なる式や代入文など)も許容していましたが、range_stmt
はexprsym3_list_r '=' LRANGE expr
というfor...range
構文の右辺に特化しました。これは、range
構文がfor
ループのコンテキストでのみ意味を持つため、文法をより厳密に定義し、不適切な使用を早期に検出できるようにするための変更です。 - アクションブロック: 規則に続く
{ ... }
内のコードは、この文法規則がマッチしたときに実行されるアクションです。$$ = nod(ORANGE, $1, $4);
:ORANGE
というオペレーションコードを持つASTノードを作成し、そのノードに$1
(exprsym3_list_r
、つまりkey, value
の部分)と$4
(expr
、つまりcollection
の部分)を子ノードとして関連付けています。$$
はこの規則の左辺(range_stmt
)が返すASTノードです。$$->etype = 0; // := flag
: 生成されたASTノードのetype
フィールドを0
に設定しています。コメントから、これは:=
演算子による変数宣言が行われたことを示すフラグであると推測されます。
- 命名の明確化:
3. for_header
規則の変更
for_header
は for
ループのヘッダー部分を解析する規則です。この部分が最も大きく変更されています。
3.1. 最初の規則の変更(for init; test; incr
形式)
for_header:
-\tosimple_stmt ';' orange_stmt ';' osimple_stmt
+\tosimple_stmt ';' osimple_stmt ';' osimple_stmt
{
-\t\tif($3 != N && $3->op == ORANGE) {\n-\t\t\t$$ = dorange($3);\n-\t\t\t$$->ninit = list($$->ninit, $1);\n-\t\t\t$$->nincr = list($$->nincr, $5);\n-\t\t\tbreak;\n-\t\t}\n \t\t// init ; test ; incr
\t\tif($5 != N && $5->colas != 0)
\t\t\tyyerror("cannot declare in the for-increment");
\t\t$$ = nod(OFOR, N, N);
\t\t$$->ninit = $1;
\t\t$$->ntest = $3;
\t\t$$->nincr = $5;
}
- 変更内容:
osimple_stmt ';' orange_stmt ';' osimple_stmt
がosimple_stmt ';' osimple_stmt ';' osimple_stmt
に変更されました。つまり、真ん中のorange_stmt
がosimple_stmt
に置き換えられました。- それに伴い、アクションブロック内の
if($3 != N && $3->op == ORANGE) { ... }
というrange
構文を特別扱いするロジックが削除されました。
- 解説:
- 関心の分離: 以前は、この規則が通常の
for init; test; incr
形式と、for
ループのtest
部分にrange
構文が誤って現れた場合の両方を処理しようとしていました。この変更により、この規則は純粋にfor init; test; incr
形式のループに特化しました。 $1
は初期化ステートメント、$3
は条件式、$5
はインクリメントステートメントに対応します。if($5 != N && $5->colas != 0) yyerror("cannot declare in the for-increment");
: これは、for
ループのインクリメント部分で新しい変数を宣言しようとした場合にエラーを報告するチェックです。Go言語では、for
ループのインクリメント部分で:=
を使って新しい変数を宣言することはできません。$$ = nod(OFOR, N, N);
:OFOR
(通常のfor
ループを表すオペレーションコード)を持つASTノードを作成します。$$->ninit = $1; $$->ntest = $3; $$->nincr = $5;
: 生成されたfor
ループのASTノードのninit
(初期化)、ntest
(条件)、nincr
(インクリメント)フィールドに、それぞれ対応する子ノードを設定しています。
- 関心の分離: 以前は、この規則が通常の
3.2. 2番目の規則の変更(for condition
形式)
-|\torange_stmt
+|\tosimple_stmt
{
-\t\t// range
-\t\tif($1 != N && $1->op == ORANGE) {\n-\t\t\t$$ = dorange($1);\n-\t\t\tbreak;\n-\t\t}\n \t\t// normal test
\t\t$$ = nod(OFOR, N, N);
\t\t$$->ninit = N;
\t\t$$->ntest = $1;
\t\t$$->nincr = N;
}
- 変更内容:
orange_stmt
がosimple_stmt
に変更されました。それに伴い、アクションブロック内のif($1 != N && $1->op == ORANGE) { ... }
というrange
構文を特別扱いするロジックが削除されました。 - 解説: 以前は、この規則も
for condition
形式とfor range
形式の両方を処理しようとしていました。この変更により、この規則は純粋にfor condition
形式のループに特化しました。$1
は条件式に対応します。$$ = nod(OFOR, N, N);
:OFOR
ノードを作成します。$$->ninit = N; $$->ntest = $1; $$->nincr = N;
: 初期化とインクリメントがないため、それらのフィールドはN
(nil)に設定され、ntest
フィールドに条件式が設定されます。
3.3. 新しい規則の追加(for...range
形式)
+|\trange_stmt
+\t{\n+\t\t$$ = dorange($1);\n+\t\taddtotop($$);\n+\t}\
- 変更内容:
range_stmt
という新しい規則がfor_header
に追加されました。 - 解説:
for...range
の独立した処理: この新しい規則は、for...range
ループのヘッダーを明示的に処理するために導入されました。これにより、for...range
構文の解析が他のfor
ループの形式から完全に分離され、文法がよりモジュール化されました。$1
は、先に定義されたrange_stmt
規則から返されたASTノード(for...range
の右辺に対応)です。$$ = dorange($1);
:dorange
関数は、range
構文のASTノードを受け取り、それをGoコンパイラの内部表現に適したfor
ループのAST構造に変換する役割を担います。これは、for...range
ループが内部的には通常のfor
ループに変換される(例えば、イテレータの初期化、条件チェック、次の要素への移動など)ため、その変換処理を行う関数です。addtotop($$);
: 生成されたfor...range
ループのASTノードを、現在のスコープのトップレベルに追加します。これは、for...range
ループが新しいスコープを導入し、その中でループ変数(key
,value
)が宣言されるため、それらの変数を適切にスコープ管理するために必要です。
これらの変更全体として、Goコンパイラの for
ループ解析のロジックが大幅に整理され、各 for
ループの形式がより明確かつ独立して処理されるようになりました。これにより、コンパイラのコードベースの可読性、保守性、そして将来の拡張性が向上しています。
関連リンク
- Go言語仕様: https://go.dev/ref/spec (特に "For statements" のセクション)
- Yacc/Bisonのドキュメント:
- Bison Manual: https://www.gnu.org/software/bison/manual/
- Yacc (Wikipedia): https://ja.wikipedia.org/wiki/Yacc
- Goコンパイラのソースコード (現在のバージョン): https://github.com/golang/go/tree/master/src/cmd/compile/internal/syntax (現在のGoコンパイラは
go.y
を直接使用していませんが、概念は共通しています)
参考にした情報源リンク
- Go言語の
for
ループに関する公式ドキュメントやチュートリアル - Yacc/Bisonの文法定義とアクションに関する一般的な情報
- Go言語の初期のコミット履歴と開発に関する議論(公開されている場合)
- Go言語のコンパイラ設計に関する一般的な知識# [インデックス 1581] ファイルの概要
このコミットは、Go言語のコンパイラの一部である src/cmd/gc/go.y
ファイルに対する変更です。go.y
は、Go言語の構文解析器(パーサー)を生成するためのYacc(Yet Another Compiler-Compiler)またはBisonの文法定義ファイルです。Goコンパイラは、このファイルに記述された文法規則に基づいてGoのソースコードを解析し、抽象構文木(AST)を構築します。具体的には、for
ループ、特に for...range
構文の解析に関連する文法規則が変更されています。
コミット
このコミットは、Go言語のコンパイラにおける range
構文の文法定義を整理し、クリーンアップすることを目的としています。具体的には、orange_stmt
という非終端記号を range_stmt
に改名し、for
ループのヘッダー部分の文法規則を簡素化しています。これにより、コンパイラのコードベースの可読性と保守性が向上し、将来的な言語機能の拡張に備えるものと考えられます。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3ec4675220f18c3bc6680e71c6ce09a76b641c8c
元コミット内容
commit 3ec4675220f18c3bc6680e71c6ce09a76b641c8c
Author: Russ Cox <rsc@golang.org>
Date: Wed Jan 28 15:41:50 2009 -0800
clean up range grammar
R=ken
OCL=23712
CL=23714
---
src/cmd/gc/go.y | 27 ++++++++++-----------------
1 file changed, 10 insertions(+), 17 deletions(-)\n
diff --git a/src/cmd/gc/go.y b/src/cmd/gc/go.y
index 4aafd0b0c1..d04991dc47 100644
--- a/src/cmd/gc/go.y
+++ b/src/cmd/gc/go.y
@@ -52,7 +52,7 @@
%type <node> Astmt Bstmt
%type <node> for_stmt for_body for_header
%type <node> if_stmt if_body if_header select_stmt
-%type <node> simple_stmt osimple_stmt orange_stmt semi_stmt
+%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
@@ -536,9 +536,8 @@ compound_stmt:
popdcl();
}
-orange_stmt:
- osimple_stmt
-| exprsym3_list_r '=' LRANGE expr
+range_stmt:
+ exprsym3_list_r '=' LRANGE expr
{
$$ = nod(ORANGE, $1, $4);
$$->etype = 0; // := flag
@@ -550,14 +549,8 @@ orange_stmt:
}
for_header:
-\tosimple_stmt ';' orange_stmt ';' osimple_stmt
+\tosimple_stmt ';' osimple_stmt ';' osimple_stmt
{
-\t\tif($3 != N && $3->op == ORANGE) {\n-\t\t\t$$ = dorange($3);\n-\t\t\t$$->ninit = list($$->ninit, $1);\n-\t\t\t$$->nincr = list($$->nincr, $5);\n-\t\t\tbreak;\n-\t\t}\n \t\t// init ; test ; incr
\t\tif($5 != N && $5->colas != 0)
\t\t\tyyerror("cannot declare in the for-increment");
\t\t$$ = nod(OFOR, N, N);
@@ -566,19 +559,19 @@ for_header:
\t\t$$->ntest = $3;
\t\t$$->nincr = $5;
}
-|\torange_stmt
+|\tosimple_stmt
{
-\t\t// range
-\t\tif($1 != N && $1->op == ORANGE) {\n-\t\t\t$$ = dorange($1);\n-\t\t\tbreak;\n-\t\t}\n \t\t// normal test
\t\t$$ = nod(OFOR, N, N);
\t\t$$->ninit = N;
\t\t$$->ntest = $1;
\t\t$$->nincr = N;
}
+|\trange_stmt
+\t{\n+\t\t$$ = dorange($1);\n+\t\taddtotop($$);\n+\t}\
for_body:
for_header compound_stmt
変更の背景
このコミットが行われた2009年1月は、Go言語がまだ一般に公開される前の非常に初期の開発段階でした。Go言語の設計と実装は活発に進められており、文法や内部構造は頻繁に変更されていました。Go言語は2009年11月にオープンソースプロジェクトとして公開されましたが、その設計は2007年からGoogle社内で進められていました。この時期は、言語のコア機能が固まりつつある段階であり、コンパイラの内部構造も試行錯誤が繰り返されていました。
この「clean up range grammar」というコミットメッセージは、for...range
ループの文法解析に関する既存の実装が、何らかの理由で最適ではない、あるいは将来の拡張性を考慮すると問題があると考えられたことを示唆しています。具体的な背景としては、以下のような点が考えられます。
- 文法の整理と簡素化:
orange_stmt
という名前は、range
構文を直接的に示しておらず、その役割が不明瞭でした。より分かりやすいrange_stmt
への改名が行われたのは、コードの意図を明確にするためです。また、for_header
内でのorange_stmt
の扱いが冗長であったり、他のfor
ループの形式と統合されていなかったりしたため、文法規則を整理し、より一貫性のある解析ロジックを目指したと考えられます。初期の言語開発では、機能が追加されるたびに文法が複雑になりがちであり、定期的な整理は不可欠です。 - コードの可読性と保守性の向上: コンパイラのコードベースは非常に複雑になりがちです。文法定義をクリーンアップすることで、将来の機能追加やバグ修正が容易になります。特に、Yacc/Bisonのようなパーサー生成ツールを使用する場合、文法規則の構造が直接的に生成されるパーサーのコードの複雑さに影響するため、簡潔で明確な文法は重要です。
- 解析効率の改善: 文法規則の整理は、パーサーの効率をわずかに向上させる可能性もあります。特に、曖昧な規則や重複する規則を排除することで、解析時のバックトラックを減らし、パーサーのパフォーマンスを向上させることができます。これは、大規模なコードベースを扱うコンパイラにとって重要な要素です。
- 言語仕様の進化への対応: Go言語の
for...range
構文は、スライス、配列、文字列、マップ、チャネルなど、様々なデータ構造をイテレートするために使用されます。初期の段階では、これらのイテレーションのセマンティクスがまだ完全に固まっていなかった可能性があり、文法を整理することで、将来の言語仕様の変更に柔軟に対応できるようにしたと考えられます。Go言語は、シンプルさと効率性を重視して設計されており、その設計思想はコンパイラの内部構造にも反映されています。
要するに、この変更は、Go言語の for...range
構文の解析をより堅牢で、理解しやすく、保守しやすいものにするための、初期段階における重要なリファクタリングの一環であったと言えます。これは、Go言語がその後の安定したリリースに向けて、基盤を固めていく過程で不可欠なステップでした。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
1. Yacc/Bisonと文法定義
go.y
ファイルは、Yacc(Yet Another Compiler-Compiler)またはそのGNU版であるBisonの入力ファイルです。Yacc/Bisonは、BNF(Backus-Naur Form)に似た形式で記述された文法規則から、C言語(または他の言語)の構文解析器(パーサー)を自動生成するツールです。Goコンパイラの初期バージョンでは、このアプローチが採用されていました。
- 文法規則(Grammar Rules): プログラミング言語の構文を定義する一連の規則です。例えば、「式は項の後に演算子と項が続く」といった形です。各規則は、非終端記号がどのように終端記号や他の非終端記号の組み合わせで構成されるかを示します。
- 非終端記号(Non-terminal Symbols): 文法規則の左辺に現れる記号で、さらに別の規則によって展開される記号です。これらは言語の抽象的な構文要素を表します。例:
expression
,statement
,for_header
。 - 終端記号(Terminal Symbols): 文法規則の右辺に現れる記号で、これ以上展開されない記号です。これらは字句解析器(レキサー)によって識別される最小単位のトークンに対応します。例:
ID
(識別子),NUMBER
(数値),'+'
,';'
,LRANGE
(Goのrange
キーワードに対応するトークン)。 - アクション(Actions): 各文法規則の右辺に付随するC言語のコードブロックです。パーサーがその規則にマッチしたときに実行されます。通常、抽象構文木(AST)のノードを構築したり、セマンティックチェックを行ったりします。
$$
: 現在の規則の左辺の非終端記号に対応する値(通常はASTノードへのポインタ)を表します。この値は、この規則が解析した構文要素のセマンティックな情報を保持します。$1
,$2
, ...: 現在の規則の右辺の記号に対応する値(ASTノードへのポインタなど)を表します。$1
は右辺の最初の記号、$2
は2番目の記号、といった具合です。これらの値は、子要素のセマンティックな情報を親要素に渡すために使用されます。
%type <node>
: Yacc/Bisonのディレクティブで、特定の非終端記号がどのような型の値を保持するかを宣言します。ここでは、node
型(おそらくASTノードを表す構造体へのポインタ)を保持することを示しています。これは、パーサーが構文解析中に構築するASTのノードの型を定義するために重要です。
2. Go言語の for
ループと for...range
Go言語には、いくつかの for
ループの形式があります。これらの形式は、それぞれ異なるイテレーションのパターンに対応しています。
- 通常の
for
ループ: C言語やJavaに似た形式で、初期化ステートメント、条件式、後処理ステートメントを持ちます。for init; condition; post { // ... }
init
はループ開始前に一度だけ実行され、condition
は各イテレーションの前に評価され、post
は各イテレーションの後に実行されます。 - 条件式のみの
for
ループ:
これはfor condition { // ... }
while
ループに相当します。condition
がtrue
である限りループが続行されます。 - 無限ループ:
条件が指定されていないため、明示的なfor { // ... }
break
ステートメントがない限り無限に実行されます。 for...range
ループ: 配列、スライス、文字列、マップ、チャネルなどの要素をイテレートするために使用されます。これはGo言語の特徴的なループ形式であり、コレクションの要素を簡潔に処理できます。
または、インデックス/キーが不要な場合:for key, value := range collection { // ... }
または、値が不要な場合:for _, value := range collection { // ... }
このコミットは、特にfor key := range collection { // ... }
for...range
構文の解析に関連しています。for...range
は、内部的には通常のfor
ループに変換されることが多く、その変換ロジックがコンパイラ内で実装されます。
3. Goコンパイラの内部構造(初期段階)
Goコンパイラは、ソースコードを機械語に変換する過程で、以下の主要なフェーズを経ます。このコミットは、特に構文解析フェーズに焦点を当てています。
- 字句解析(Lexical Analysis): ソースコードをトークン(キーワード、識別子、演算子など)のストリームに変換します。このフェーズはレキサー(またはスキャナー)によって行われます。
- 構文解析(Syntax Analysis): トークンのストリームを文法規則に基づいて解析し、抽象構文木(AST)を構築します。
go.y
がこのフェーズで使用され、パーサーが生成されます。ASTは、プログラムの構造を階層的に表現したデータ構造であり、後続のコンパイルフェーズの入力となります。 - 意味解析(Semantic Analysis): ASTに対して型チェック、名前解決、スコープチェックなどの意味的な検証を行います。このフェーズでは、プログラムが言語のセマンティックルールに準拠しているかを確認します。
- 中間コード生成(Intermediate Code Generation): ASTをより低レベルの中間表現に変換します。この中間表現は、特定のCPUアーキテクチャに依存しない形でプログラムを表現します。
- 最適化(Optimization): 中間コードを最適化し、実行効率を向上させます。
- コード生成(Code Generation): 最終的な機械語コードを生成します。
このコミットは、構文解析フェーズにおける for...range
構文のAST構築ロジックに影響を与えます。
nod(OP, Left, Right)
: ASTノードを作成する関数。OP
はノードの種類(例:ORANGE
はfor...range
ループを表す内部オペレーションコード)、Left
とRight
はそのノードの子ノードです。これらのノードは、プログラムの構造を表現するために使用されます。$$->ninit
,$$->ntest
,$$->nincr
:for
ループのASTノードのフィールドで、それぞれ初期化ステートメント、条件式、インクリメントステートメントに対応する子ノードを格納します。これらのフィールドは、通常のfor
ループの三つの部分を表現するために使用されます。dorange($1)
:range
構文のASTノードを処理し、適切なfor
ループのAST構造に変換する関数。これは、for...range
ループが内部的に通常のfor
ループに展開される際の変換ロジックの一部です。addtotop($$)
: 生成されたASTノードを、現在のスコープのトップレベルに追加する関数。これは、特に新しいスコープを導入する構文(例:for...range
ループ内で宣言される変数)の管理に重要です。list(list_head, new_node)
: ASTノードのリストに新しいノードを追加する関数。これは、複数のステートメントや式をリストとして管理するために使用されます。N
:nil
またはnull
に相当する、ノードが存在しないことを示す定数。yyerror("message")
: 構文エラーを報告する関数。パーサーが文法規則に違反する入力を見つけた場合に呼び出されます。
技術的詳細
このコミットの主要な変更点は、go.y
ファイルにおける range
構文の文法規則の再定義と、それに伴う for_header
規則の簡素化です。これらの変更は、Go言語の for...range
ループの解析をより構造化され、明確なものにすることを目的としています。
1. orange_stmt
から range_stmt
への改名と定義の変更
変更前:
orange_stmt:
osimple_stmt
| exprsym3_list_r '=' LRANGE expr
{
$$ = nod(ORANGE, $1, $4);
$$->etype = 0; // := flag
// ... その他のアクション ...
}
変更前の orange_stmt
は、osimple_stmt
(オプションのシンプルなステートメント)または exprsym3_list_r '=' LRANGE expr
のいずれかとして定義されていました。後者の規則は for key, value := range collection
のような range
構文の右辺に対応します。osimple_stmt
が含まれていたことで、orange_stmt
の意味が曖昧になっていました。
変更後:
range_stmt:
exprsym3_list_r '=' LRANGE expr
{
$$ = nod(ORANGE, $1, $4);
$$->etype = 0; // := flag
// ... その他のアクション ...
}
- 非終端記号の改名:
orange_stmt
という非終端記号がrange_stmt
に改名されました。これは、その役割をより明確にするための命名規則の改善です。range_stmt
という名前は、Go言語のrange
キーワードと直接的に関連付けられ、その意味が直感的に理解できるようになりました。 - 定義の厳密化:
osimple_stmt
の選択肢がrange_stmt
から削除されました。これにより、range_stmt
は純粋にfor...range
構文の右辺(key, value := range collection
の部分)のみを表現するようになりました。これは、range
構文がfor
ループのヘッダー内でしか意味を持たないため、より厳密な文法定義になったと言えます。この厳密化により、パーサーはrange
構文をより正確に識別し、誤った文法構造を早期に検出できるようになります。
アクションブロック $$ = nod(ORANGE, $1, $4);
は、ORANGE
という種類のASTノードを作成し、その子ノードとして $1
(exprsym3_list_r
、つまり key, value
の部分)と $4
(expr
、つまり collection
の部分)を設定しています。$$->etype = 0; // := flag
は、おそらく :=
演算子による変数宣言が行われたことを示すフラグを設定しています。これは、for...range
ループで新しい変数を宣言する際のセマンティックな情報をASTに付加するために使用されます。
2. for_header
規則の簡素化と range_stmt
の統合
for_header
は、for
ループの括弧内の部分(例: init; condition; post
や range collection
)を解析する規則です。この部分の変更は、for
ループの異なる形式の解析ロジックを分離し、明確化することを目的としています。
変更前:
for_header:
osimple_stmt ';' orange_stmt ';' osimple_stmt
{
// ... init ; test ; incr ...
if($3 != N && $3->op == ORANGE) {
$$ = dorange($3);
$$->ninit = list($$->ninit, $1);
$$->nincr = list($$->nincr, $5);
break;
}
// ... normal test ...
}
| orange_stmt
{
// range
if($1 != N && $1->op == ORANGE) {
$$ = dorange($1);
break;
}
// normal test
// ...
}
変更前の for_header
は、for init; test; incr
形式と for range
形式の両方を同じ規則内で処理しようとしていました。特に、osimple_stmt ';' orange_stmt ';' osimple_stmt
の規則内で、$3
が ORANGE
オペレーションを持つ場合に range
ループとして特別扱いするロジックが含まれていました。これは、文法規則とセマンティックアクションが密結合しており、読みにくい構造でした。また、orange_stmt
が単独で for_header
になる規則も存在し、ここでも range
構文の特殊処理が行われていました。
変更後:
for_header:
osimple_stmt ';' osimple_stmt ';' osimple_stmt
{
// init ; test ; incr
if($5 != N && $5->colas != 0)
yyerror("cannot declare in the for-increment");
$$ = nod(OFOR, N, N);
$$->ninit = $1;
$$->ntest = $3;
$$->nincr = $5;
}
| osimple_stmt
{
// normal test
$$ = nod(OFOR, N, N);
$$->ninit = N;
$$->ntest = $1;
$$->nincr = N;
}
| range_stmt
{
$$ = dorange($1);
addtotop($$);
}
-
最初の規則の簡素化:
for_header
の最初の規則osimple_stmt ';' osimple_stmt ';' osimple_stmt
は、純粋にfor init; test; incr
形式のループを処理するように簡素化されました。以前のif($3 != N && $3->op == ORANGE)
のようなrange
構文の特殊処理が削除されました。これにより、この規則は通常のfor
ループの解析に特化し、より明確になりました。$1
は初期化ステートメント、$3
は条件式、$5
はインクリメントステートメントに対応します。if($5 != N && $5->colas != 0) yyerror("cannot declare in the for-increment");
: これは、Go言語の仕様で許可されていない、for
ループのインクリメント部分での新しい変数宣言(:=
を使用)を検出するためのエラーチェックです。$$ = nod(OFOR, N, N);
:OFOR
(通常のfor
ループを表すオペレーションコード)を持つASTノードを作成します。$$->ninit = $1; $$->ntest = $3; $$->nincr = $5;
: 生成されたfor
ループのASTノードのninit
(初期化)、ntest
(条件)、nincr
(インクリメント)フィールドに、それぞれ対応する子ノードを設定しています。
-
2番目の規則の簡素化:
for_header
の2番目の規則osimple_stmt
は、for condition
形式のループを処理します。以前のorange_stmt
を含む複雑なロジックが削除され、シンプルな条件式のみのループに特化しました。$1
は条件式に対応します。$$ = nod(OFOR, N, N);
:OFOR
ノードを作成します。$$->ninit = N; $$->ntest = $1; $$->nincr = N;
: 初期化とインクリメントがないため、それらのフィールドはN
(nil)に設定され、ntest
フィールドに条件式が設定されます。
-
新しい
range_stmt
規則の追加: 新しい規則range_stmt
がfor_header
に追加されました。これにより、for...range
構文は独立した規則として明確に定義され、解析ロジックが分離されました。$1
は、先に定義されたrange_stmt
規則から返されたASTノード(for...range
の右辺に対応)です。$$ = dorange($1);
:dorange
関数は、range_stmt
から受け取ったASTノード($1
)を処理し、最終的なfor
ループのASTノードを構築する役割を担います。これは、for...range
ループが内部的には通常のfor
ループに変換される(例えば、イテレータの初期化、条件チェック、次の要素への移動など)ため、その変換処理を行う関数です。addtotop($$);
: 生成されたfor...range
ループのASTノードを、現在のスコープのトップレベルに追加します。これは、for...range
ループが新しいスコープを導入し、その中でループ変数(key
,value
)が宣言されるため、それらの変数を適切にスコープ管理するために必要です。
変更の意図と効果
この変更は、Go言語の for
ループ、特に for...range
構文の解析ロジックを大幅に改善しています。
- 文法の明確化:
range_stmt
がfor...range
構文の右辺のみを表現するように特化され、for_header
からrange
構文の特殊処理が分離されたことで、文法規則がより明確で理解しやすくなりました。これにより、パーサーの動作が予測しやすくなり、デバッグが容易になります。 - 関心の分離: 各文法規則が特定の種類の
for
ループに特化することで、コードの関心事が分離され、保守性が向上しました。これは、ソフトウェア設計における「単一責任の原則」に合致するもので、各モジュールが特定の機能に集中することで、変更の影響範囲を限定できます。 - エラーハンドリングの改善:
yyerror("cannot declare in the for-increment")
のようなエラーチェックが、適切な文法規則のコンテキストで行われるようになりました。これにより、より正確で分かりやすいエラーメッセージをユーザーに提供できるようになります。 - 将来の拡張性: 文法が整理されたことで、将来的に
for
ループやrange
構文に新しい機能が追加された場合でも、既存のコードに大きな影響を与えることなく変更を加えやすくなります。モジュール化された設計は、新しい機能の追加や既存機能の変更を容易にします。
このコミットは、Go言語のコンパイラが初期段階でどのように進化し、文法定義が洗練されていったかを示す良い例です。このような基盤の整備が、Go言語のその後の安定性と成長に貢献しました。
コアとなるコードの変更箇所
diff --git a/src/cmd/gc/go.y b/src/cmd/gc/go.y
index 4aafd0b0c1..d04991dc47 100644
--- a/src/cmd/gc/go.y
+++ b/src/cmd/gc/go.y
@@ -52,7 +52,7 @@
%type <node> Astmt Bstmt
%type <node> for_stmt for_body for_header
%type <node> if_stmt if_body if_header select_stmt
-%type <node> simple_stmt osimple_stmt orange_stmt semi_stmt
+%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
@@ -536,9 +536,8 @@ compound_stmt:
popdcl();
}
-orange_stmt:
- osimple_stmt
-| exprsym3_list_r '=' LRANGE expr
+range_stmt:
+ exprsym3_list_r '=' LRANGE expr
{
$$ = nod(ORANGE, $1, $4);
$$->etype = 0; // := flag
@@ -550,14 +549,8 @@ orange_stmt:
}
for_header:
-\tosimple_stmt ';' orange_stmt ';' osimple_stmt
+\tosimple_stmt ';' osimple_stmt ';' osimple_stmt
{
-\t\tif($3 != N && $3->op == ORANGE) {\n-\t\t\t$$ = dorange($3);\n-\t\t\t$$->ninit = list($$->ninit, $1);\n-\t\t\t$$->nincr = list($$->nincr, $5);\n-\t\t\tbreak;\n-\t\t}\n \t\t// init ; test ; incr
\t\tif($5 != N && $5->colas != 0)
\t\t\tyyerror("cannot declare in the for-increment");
\t\t$$ = nod(OFOR, N, N);
@@ -566,19 +559,19 @@ for_header:
\t\t$$->ntest = $3;
\t\t$$->nincr = $5;
}
-|\torange_stmt
+|\tosimple_stmt
{
-\t\t// range
-\t\tif($1 != N && $1->op == ORANGE) {\n-\t\t\t$$ = dorange($1);\n-\t\t\tbreak;\n-\t\t}\n \t\t// normal test
\t\t$$ = nod(OFOR, N, N);
\t\t$$->ninit = N;
\t\t$$->ntest = $1;
\t\t$$->nincr = N;
}
+|\trange_stmt
+\t{\n+\t\t$$ = dorange($1);\n+\t\taddtotop($$);\n+\t}\
for_body:
for_header compound_stmt
コアとなるコードの解説
1. %type
宣言の変更
- %type <node> simple_stmt osimple_stmt orange_stmt semi_stmt
+ %type <node> simple_stmt osimple_stmt range_stmt semi_stmt
- 変更内容:
orange_stmt
という非終端記号の型宣言がrange_stmt
に変更されました。 - 解説: Yacc/Bisonの文法ファイルでは、
%type
ディレクティブを使用して、非終端記号が保持するセマンティック値の型を宣言します。この変更は、orange_stmt
がrange_stmt
に改名されたことに伴う、型宣言の更新です。これにより、パーサーがrange_stmt
を処理する際に、それがnode
型(おそらく抽象構文木(AST)のノードを表す構造体へのポインタ)の値を返すことを認識し、型安全なAST構築を保証します。
2. orange_stmt
規則の削除と range_stmt
規則の追加
-orange_stmt:
- osimple_stmt
-| exprsym3_list_r '=' LRANGE expr
+range_stmt:
+ exprsym3_list_r '=' LRANGE expr
- 変更内容:
orange_stmt
という名前の規則が削除され、代わりにrange_stmt
という新しい名前の規則が追加されました。また、orange_stmt
にあったosimple_stmt
という選択肢がrange_stmt
から削除されました。 - 解説:
- 命名の明確化: 以前の
orange_stmt
は、その名前から直接的にrange
構文を連想しにくいものでした。これをrange_stmt
に変更することで、その役割がより直感的に理解できるようになりました。これは、コードの可読性と保守性を向上させるための重要なリファクタリングです。 - 文法の厳密化: 以前の
orange_stmt
はosimple_stmt
(おそらく単なる式や代入文など)も許容していましたが、range_stmt
はexprsym3_list_r '=' LRANGE expr
というfor...range
構文の右辺に特化しました。Go言語のrange
構文はfor
ループのコンテキストでのみ意味を持つため、この変更により文法がより厳密に定義され、不適切な文法構造を早期に検出できるようになります。これにより、パーサーの堅牢性が向上します。 - アクションブロック: 規則に続く
{ ... }
内のコードは、この文法規則がマッチしたときに実行されるセマンティックアクションです。$$ = nod(ORANGE, $1, $4);
:nod
関数は、Goコンパイラの内部でASTノードを作成するために使用される関数です。ここでは、ORANGE
というオペレーションコードを持つASTノードを作成し、そのノードに$1
(exprsym3_list_r
、つまりkey, value
の部分)と$4
(expr
、つまりcollection
の部分)を子ノードとして関連付けています。$$
はこの規則の左辺(range_stmt
)が返すASTノードであり、このノードがfor...range
構文のセマンティックな情報をカプセル化します。$$->etype = 0; // := flag
: 生成されたASTノードのetype
フィールドを0
に設定しています。コメントから、これは:=
演算子による変数宣言が行われたことを示すフラグであると推測されます。for...range
ループでは、ループ変数を:=
で宣言することが一般的であり、このフラグはそのセマンティックな側面をコンパイラに伝えるために使用されます。
- 命名の明確化: 以前の
3. for_header
規則の変更
for_header
は for
ループのヘッダー部分を解析する規則です。この部分が最も大きく変更されており、for
ループの異なる形式の解析ロジックが明確に分離されました。
3.1. 最初の規則の変更(for init; test; incr
形式)
for_header:
-\tosimple_stmt ';' orange_stmt ';' osimple_stmt
+\tosimple_stmt ';' osimple_stmt ';' osimple_stmt
{
-\t\tif($3 != N && $3->op == ORANGE) {\n-\t\t\t$$ = dorange($3);\n-\t\t\t$$->ninit = list($$->ninit, $1);\n-\t\t\t$$->nincr = list($$->nincr, $5);\n-\t\t\tbreak;\n-\t\t}\n \t\t// init ; test ; incr
\t\tif($5 != N && $5->colas != 0)
\t\t\tyyerror("cannot declare in the for-increment");
\t\t$$ = nod(OFOR, N, N);
\t\t$$->ninit = $1;
\t\t$$->ntest = $3;
\t\t$$->nincr = $5;
}
- 変更内容:
osimple_stmt ';' orange_stmt ';' osimple_stmt
がosimple_stmt ';' osimple_stmt ';' osimple_stmt
に変更されました。つまり、真ん中のorange_stmt
がosimple_stmt
に置き換えられました。- それに伴い、アクションブロック内の
if($3 != N && $3->op == ORANGE) { ... }
というrange
構文を特別扱いするロジックが削除されました。
- 解説:
- 関心の分離: 以前は、この規則が通常の
for init; test; incr
形式と、for
ループのtest
部分にrange
構文が誤って現れた場合の両方を処理しようとしていました。この変更により、この規則は純粋にfor init; test; incr
形式のループに特化しました。これにより、パーサーのロジックが簡素化され、各規則の責任が明確になりました。 $1
は初期化ステートメント、$3
は条件式、$5
はインクリメントステートメントに対応します。if($5 != N && $5->colas != 0) yyerror("cannot declare in the for-increment");
: これは、Go言語の仕様で許可されていない、for
ループのインクリメント部分での新しい変数宣言(:=
を使用)を検出するためのエラーチェックです。yyerror
関数は、構文エラーが発生したことをパーサーに通知し、適切なエラーメッセージを出力します。$$ = nod(OFOR, N, N);
:OFOR
(通常のfor
ループを表すオペレーションコード)を持つASTノードを作成します。$$->ninit = $1; $$->ntest = $3; $$->nincr = $5;
: 生成されたfor
ループのASTノードのninit
(初期化)、ntest
(条件)、nincr
(インクリメント)フィールドに、それぞれ対応する子ノードを設定しています。これにより、通常のfor
ループの構造がASTに正確に反映されます。
- 関心の分離: 以前は、この規則が通常の
3.2. 2番目の規則の変更(for condition
形式)
-|\torange_stmt
+|\tosimple_stmt
{
-\t\t// range
-\t\tif($1 != N && $1->op == ORANGE) {\n-\t\t\t$$ = dorange($1);\n-\t\t\tbreak;\n-\t\t}\n \t\t// normal test
\t\t$$ = nod(OFOR, N, N);
\t\t$$->ninit = N;
\t\t$$->ntest = $1;
\t\t$$->nincr = N;
}
- 変更内容:
orange_stmt
がosimple_stmt
に変更されました。それに伴い、アクションブロック内のif($1 != N && $1->op == ORANGE) { ... }
というrange
構文を特別扱いするロジックが削除されました。 - 解説: 以前は、この規則も
for condition
形式とfor range
形式の両方を処理しようとしていました。この変更により、この規則は純粋にfor condition
形式のループに特化しました。$1
は条件式に対応します。$$ = nod(OFOR, N, N);
:OFOR
ノードを作成します。$$->ninit = N; $$->ntest = $1; $$->nincr = N;
: 初期化とインクリメントがないため、それらのフィールドはN
(nil)に設定され、ntest
フィールドに条件式が設定されます。これにより、条件式のみのfor
ループのASTが正確に構築されます。
3.3. 新しい range_stmt
規則の追加(for...range
形式)
+|\trange_stmt
+\t{\n+\t\t$$ = dorange($1);\n+\t\taddtotop($$);\n+\t}\
- 変更内容:
range_stmt
という新しい規則がfor_header
に追加されました。 - 解説:
for...range
の独立した処理: この新しい規則は、for...range
ループのヘッダーを明示的に処理するために導入されました。これにより、for...range
構文の解析が他のfor
ループの形式から完全に分離され、文法がよりモジュール化されました。これは、Go言語のfor...range
が持つ独自のセマンティクスを適切に処理するために不可欠です。$1
は、先に定義されたrange_stmt
規則から返されたASTノード(for...range
の右辺に対応)です。$$ = dorange($1);
:dorange
関数は、range
構文のASTノードを受け取り、それをGoコンパイラの内部表現に適したfor
ループのAST構造に変換する役割を担います。for...range
ループは、内部的には通常のfor
ループに変換される(例えば、イテレータの初期化、条件チェック、次の要素への移動など)ため、この関数はその変換処理を行います。この変換は、コンパイラのバックエンドがfor...range
を効率的に処理できるようにするために重要です。addtotop($$);
: 生成されたfor...range
ループのASTノードを、現在のスコープのトップレベルに追加します。これは、for...range
ループが新しいスコープを導入し、その中でループ変数(key
,value
)が宣言されるため、それらの変数を適切にスコープ管理するために必要です。スコープ管理は、変数名の衝突を防ぎ、プログラムの正確性を保証するために不可欠なコンパイラの機能です。
これらの変更全体として、Goコンパイラの for
ループ解析のロジックが大幅に整理され、各 for
ループの形式がより明確かつ独立して処理されるようになりました。これにより、コンパイラのコードベースの可読性、保守性、そして将来の拡張性が向上しています。特に、go.y
のようなパーサー生成ツールを使用する環境では、文法規則の明確さと簡潔さが、生成されるパーサーの品質と効率に直結します。
関連リンク
- Go言語仕様: https://go.dev/ref/spec (特に "For statements" のセクションは、
for
ループの文法とセマンティクスを詳細に記述しています。) - Yacc/Bisonのドキュメント:
- Bison Manual: https://www.gnu.org/software/bison/manual/ (Bisonの公式マニュアルは、文法定義、アクション、およびパーサーの生成に関する包括的な情報を提供します。)
- Yacc (Wikipedia): https://ja.wikipedia.org/wiki/Yacc (Yaccの歴史、概念、および基本的な使用法について概観できます。)
- Goコンパイラのソースコード (現在のバージョン): https://github.com/golang/go/tree/master/src/cmd/compile/internal/syntax (現在のGoコンパイラは
go.y
を直接使用していませんが、概念は共通しています。Go 1.7以降、Goコンパイラは手書きのパーサーに移行しており、scanner.go
やparser.go
などのファイルで構文解析ロジックが実装されています。)
参考にした情報源リンク
- Go言語の
for
ループに関する公式ドキュメントやチュートリアル: Goの公式ウェブサイトやGo by Exampleなどのリソースは、for
ループの様々な形式とその使用法について詳しく説明しています。 - Yacc/Bisonの文法定義とアクションに関する一般的な情報: コンパイラ設計の教科書やオンラインのチュートリアルは、Yacc/Bisonの基本的な概念と、文法規則にセマンティックアクションを関連付ける方法について理解を深めるのに役立ちます。
- Go言語の初期のコミット履歴と開発に関する議論: Go言語の初期の開発段階におけるコミットメッセージやメーリングリストの議論は、言語設計の進化と、特定の変更がなぜ行われたのかについての貴重な洞察を提供します。
- Go言語のコンパイラ設計に関する一般的な知識: コンパイラ理論に関する一般的な知識は、
go.y
のような文法ファイルがコンパイルプロセス全体の中でどのように位置づけられるかを理解する上で不可欠です。特に、字句解析、構文解析、意味解析の各フェーズの役割を理解することが重要です。