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

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

このコミットは、Go言語のfor...rangeループにおける新しい構文の導入に関するものです。具体的には、rangeキーワードを用いたイテレーションにおいて、変数の宣言(:=)と代入(=)の形式、およびキーと値のペアを扱うための複数の構文(a range m, a,b range m, a:b range m)を許可するようにコンパイラが変更されました。これにより、forループ内でrangeを使用する際の柔軟性が向上しました。

コミット

commit b79272d9a24256bee2755a21d6ee666e6eb6b9cb
Author: Ken Thompson <ken@golang.org>
Date:   Sat Dec 6 13:40:30 2008 -0800

    allowed syntax for range
    
    a range m (implies :=)
    a,b range m (implies :=)
    a:b range m (implies :=)
    
    a := range m
    a,b := range m
    a:b := range m
    
    a = range m
    a,b = range m
    a:b = range m
    
    R=r
    OCL=20676
    CL=20676

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

https://github.com/golang/go/commit/b79272d9a24256bee2755a21d6ee666e6eb6b9cb

元コミット内容

allowed syntax for range
    
a range m (implies :=)
a,b range m (implies :=)
a:b range m (implies :=)

a := range m
a,b := range m
a:b := range m

a = range m
a,b = range m
a:b = range m

変更の背景

Go言語は、その設計初期からシンプルさと効率性を重視しており、ループ構造もその例外ではありませんでした。forループはGoにおける唯一のループ構文であり、他の言語が提供する多様なループ(while, do-while, foreachなど)の機能をすべてカバーするように設計されています。for...rangeループは、配列、スライス、文字列、マップ、チャネルといった組み込み型をイテレートするための強力なメカニズムとして導入されました。

このコミットが行われた2008年12月は、Go言語がまだ初期開発段階にあった時期です。当時のGo言語は、現在の安定版とは異なる構文やセマンティクスを持つ部分がありました。for...rangeループの初期の実装では、イテレーション変数の宣言や代入に関する構文が限定的であった可能性があります。

この変更の背景には、開発者がより自然で柔軟な方法でrangeループを使用できるようにするという意図があったと考えられます。特に、:=(短い変数宣言)と=(代入)の両方でrangeを使用できるようにすること、そしてキーと値の両方、またはキーのみをイテレートする際の構文を明確にすることが目的でした。これにより、Go言語の表現力を高め、開発者がより簡潔で読みやすいコードを書けるようにすることが目指されました。

前提知識の解説

Go言語のfor...rangeループ

Go言語のfor...rangeループは、コレクション(配列、スライス、文字列、マップ、チャネル)の要素を順番に処理するための構文です。基本的な形式は以下の通りです。

for index, value := range collection {
    // index と value を使った処理
}
  • 配列とスライス: indexには要素のインデックスが、valueにはそのインデックスに対応する要素の値が返されます。
  • 文字列: indexにはUnicodeコードポイントの開始バイトオフセットが、valueにはそのコードポイントのルーン(rune)が返されます。
  • マップ: indexにはキーが、valueにはそのキーに対応する値が返されます。マップのイテレーション順序は保証されません。
  • チャネル: valueにはチャネルから受信した値が返されます。チャネルが閉じられるまでイテレーションが続きます。

indexまたはvalueのいずれか一方のみが必要な場合は、不要な方を_(ブランク識別子)で無視することができます。

for _, value := range collection { // 値のみが必要な場合
    // value を使った処理
}

for index := range collection { // インデックス/キーのみが必要な場合
    // index を使った処理
}

Go言語の変数宣言と代入

Go言語には、変数を宣言し値を割り当てるためのいくつかの方法があります。

  • 短い変数宣言 (:=): 関数内で新しい変数を宣言し、初期値を割り当てるための簡潔な方法です。Goコンパイラが自動的に変数の型を推論します。

    name := "Alice" // string型と推論される
    count := 10     // int型と推論される
    

    この構文は、既に宣言されている変数に対しては使用できません。少なくとも1つの新しい変数が宣言される必要があります。

  • 変数宣言 (var): varキーワードを使用して変数を宣言します。初期値を指定しない場合、変数はその型のゼロ値で初期化されます。

    var name string = "Bob"
    var count int
    
  • 代入 (=): 既に宣言されている変数に新しい値を割り当てます。

    name = "Charlie"
    count = 20
    

このコミットは、これらの変数宣言と代入のメカニズムがfor...rangeループの構文とどのように統合されるか、特にrangeキーワードの直後に変数を配置する新しい形式を導入しています。

技術的詳細

このコミットは、Goコンパイラの字句解析器(lexer)と構文解析器(parser)、そして抽象構文木(AST)のウォーク(walk)処理に大きな変更を加えています。主な変更点は以下の通りです。

  1. 新しい構文の導入: コミットメッセージに示されているように、以下の新しいrange構文が許可されるようになりました。

    • a range m (implies a := range m)
    • a,b range m (implies a,b := range m)
    • a:b range m (implies a:b := range m)
    • a := range m
    • a,b := range m
    • a:b := range m
    • a = range m
    • a,b = range m
    • a:b = range m

    ここで、abはイテレーション変数、mはイテレートされるマップ、配列、スライスなどのコレクションを表します。a:bという形式は、マップのキーと値のペアをイテレートする際に、キーと値をそれぞれabに割り当てることを意図しています。

  2. ASTノードの追加: src/cmd/gc/go.hにおいて、新しいASTノードタイプORANGEenumに追加されました。これは、rangeステートメントを表すために使用されます。

  3. 構文解析器の変更 (src/cmd/gc/go.y): Goコンパイラの構文解析器はYacc(またはBison)で記述されたgo.yファイルで定義されています。このファイルが変更され、新しいrange構文を認識し、対応するASTノードを生成するように更新されました。

    • orange_stmtという新しいプロダクションルールが追加され、range構文の様々なバリエーションを処理します。
    • for_headerプロダクションが変更され、orange_stmtforループのヘッダーとして受け入れるようになりました。
    • 特に注目すべきは、ocolasというプロダクションが追加され、:=の有無をオプションとして扱えるようになった点です。これにより、a range mのように:=を省略した場合でも、内部的には:=として扱われるようになります。
    • nod(ORANGE, $1, $4)のように、ORANGEノードが生成され、そのleftにはイテレーション変数(または変数のリスト)、rightにはイテレートされるコレクションが格納されます。etypeフィールドは、:=(短い変数宣言)が使用されたかどうかを示すフラグとして利用されます(0:=1=または省略形)。
  4. セマンティックウォークの変更 (src/cmd/gc/walk.c): walk.cファイルには、ASTを走査し、セマンティックチェックやコード生成のための変換を行う関数が含まれています。

    • dorange関数のシグネチャが変更されました。以前はdorange(Node *k, Node *v, Node *m, int local)のように複数の引数を取っていましたが、変更後はdorange(Node *nn)のように単一のORANGEノードを受け取るようになりました。これにより、rangeステートメントの処理がより構造化され、ASTノードを通じて必要な情報が渡されるようになりました。
    • dorange関数内で、ORANGEノードからキー変数(k)、値変数(v)、イテレート対象(m)、そしてlocalフラグ(:==か)を抽出するロジックが追加されました。
    • old2new関数がlocalフラグに基づいて呼び出されるようになりました。これは、新しい変数を宣言する場合(:=)にのみ新しい変数を生成し、既存の変数に代入する場合(=)は既存の変数を使用することを意味します。
  5. オペレーション名の追加 (src/cmd/gc/subr.c): src/cmd/gc/subr.cには、ASTノードのオペレーション名を文字列にマッピングする配列opnamesがあります。この配列にORANGEが追加され、デバッグやエラーメッセージの際にORANGEノードが正しく識別されるようになりました。

これらの変更により、Goコンパイラはfor...rangeループの新しい構文を正しく解析し、内部的なAST表現に変換し、最終的に実行可能なコードにコンパイルできるようになりました。特に、:==のセマンティクスの違いをdorange関数で適切に処理することで、Go言語の変数宣言ルールとの整合性が保たれています。

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

このコミットにおける主要なコード変更は以下の4つのファイルに集中しています。

  1. src/cmd/gc/go.h:

    • enum Opに新しいASTノードタイプ ORANGE が追加されました。
    • dorange関数のプロトタイプが変更され、引数がNode*型を1つだけ取るようになりました。
    --- a/src/cmd/gc/go.h
    +++ b/src/cmd/gc/go.h
    @@ -300,7 +300,7 @@ enum
     	ONAME, ONONAME,
     	ODOT, ODOTPTR, ODOTMETH, ODOTINTER,
     	ODCLFUNC, ODCLFIELD, ODCLARG,
    -	OLIST, OCMP, OPTR, OARRAY,
    +	OLIST, OCMP, OPTR, OARRAY, ORANGE,
     	ORETURN, OFOR, OIF, OSWITCH,
     	OAS, OASOP, OCASE, OXCASE, OFALL, OXFALL,
     	OGOTO, OPROC, ONEW, OEMPTY, OSELECT,
    @@ -806,7 +806,7 @@ int
     isandss(Type*, Node*);\n Node*\tconvas(Node*);\n void\tarrayconv(Type*, Node*);\n Node*\tcolas(Node*, Node*);\n-Node*\tdorange(Node*, Node*, Node*, int);\n+Node*\tdorange(Node*);\n Node*\treorder1(Node*);\n Node*\treorder2(Node*);\n Node*\treorder3(Node*);\n    ```
    
    
  2. src/cmd/gc/go.y:

    • orange_stmtという新しい構文ルールが追加され、rangeループの様々な構文バリエーションを定義しています。
    • for_headerルールが変更され、orange_stmtforループのヘッダーとして受け入れるようになりました。
    • ocolasという新しいルールが追加され、:=の有無をオプションとして扱えるようになりました。
    --- 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 semi_stmt
    +%type	<node>		simple_stmt osimple_stmt orange_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
    @@ -416,7 +416,6 @@ simple_stmt:
     	{
     		if(addtop != N)
     			fatal("exprsym3_list_r LCOLAS expr_list");
    -\n \t\t$$ = rev($1);\n \t\t$$ = colas($$, $3);\n \t\t$$ = nod(OAS, $$, $3);\n@@ -555,31 +554,62 @@ compound_stmt:
     		popdcl();
     	}
     
    +ocolas:
    +|\tLCOLAS
    +
    +orange_stmt:
    +\tosimple_stmt
    +|\texprsym3_list_r '=' LRANGE expr
    +\t{
    +\t\t$$ = nod(ORANGE, $1, $4);\n+\t\t$$->etype = 0;\t// := flag
    +\t}
    +|\texprsym3 ':' exprsym3 '=' LRANGE expr
    +\t{
    +\t\t$$ = nod(OLIST, $1, $3);\n+\t\t$$ = nod(ORANGE, $$, $6);\n+\t\t$$->etype = 0;\n+\t}
    +|\texprsym3_list_r ocolas LRANGE expr
    +\t{
    +\t\t$$ = nod(ORANGE, $1, $4);\n+\t\t$$->etype = 1;\n+\t}
    +|\texprsym3 ':' exprsym3 ocolas LRANGE expr
    +\t{
    +\t\t$$ = nod(OLIST, $1, $3);\n+\t\t$$ = nod(ORANGE, $$, $6);\n+\t\t$$->etype = 1;\n+\t}
    +
     for_header:
    -\tosimple_stmt ';' osimple_stmt ';' osimple_stmt
    +\tosimple_stmt ';' orange_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     	// init ; test ; incr
     	$$ = nod(OFOR, N, N);\n     	$$->ninit = $1;\n     	$$->ntest = $3;\n     	$$->nincr = $5;\n     }\n    -|\tosimple_stmt
    +|\torange_stmt
     	{
    -\t\t// test
    +\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
     	$$ = nod(OFOR, N, N);\n     	$$->ninit = N;\n     	$$->ntest = $1;\n     	$$->nincr = N;\n     }\n    -|\tnew_name ':' new_name LRANGE expr
    -	{\n    -	\t$$ = dorange($1, $3, $5, 1);\n    -	}\n    -|\tnew_name LRANGE expr
    -	{\n    -	\t$$ = dorange($1, N, $3, 1);\n    -	}\n     
     for_body:
     	for_header compound_stmt
    
  3. src/cmd/gc/subr.c:

    • opnames配列にORANGEの文字列表現が追加されました。
    --- a/src/cmd/gc/subr.c
    +++ b/src/cmd/gc/subr.c
    @@ -687,6 +687,7 @@ opnames[] =
     	[OREGISTER]	= "REGISTER",
     	[OINDREG]	= "INDREG",
     	[OSEND]		= "SEND",
    +	[ORANGE]	= "RANGE",
     	[ORECV]		= "RECV",
     	[OPTR]		= "PTR",
     	[ORETURN]	= "RETURN",
    
  4. src/cmd/gc/walk.c:

    • dorange関数の実装が変更され、単一のORANGEノードを受け取るように修正されました。
    • ORANGEノードからイテレーション変数とコレクションを抽出し、localフラグ(:==か)に基づいてold2new関数を呼び出すロジックが追加されました。
    --- a/src/cmd/gc/walk.c
    +++ b/src/cmd/gc/walk.c
    @@ -3033,14 +3033,32 @@ badt:
     	return nl;
     }
     
    +/*
    + * rewrite a range statement
    + * k and v are names/new_names
    + * m is an array or map
    + * local is =/0 or :=/1
    + */
     Node*\n-dorange(Node *k, Node *v, Node *m, int local)\n+dorange(Node *nn)\n     {
    +\tNode *k, *v, *m;\n     	Node *n, *hk, *on, *r, *a;\n     	Type *t, *th;\n    +\tint local;\n     
    -\tif(!local)\n    -\t\tfatal("only local varables now");
    +\tif(nn->op != ORANGE)\n    +\t\tfatal("dorange not ORANGE");
    +\n+\tk = nn->left;\n+\tm = nn->right;\n+\tlocal = nn->etype;\n+\n+\tv = N;\n+\tif(k->op == OLIST) {\n+\t\tv = k->right;\n+\t\tk = k->left;\n+\t}\n     
     	n = nod(OFOR, N, N);\n     
     @@ -3073,11 +3091,13 @@ ary:
     	n->nincr = nod(OASOP, hk, literal(1));
     	n->nincr->etype = OADD;\n     
    -\tk = old2new(k, hk->type);\n+\tif(local)\n+\t\tk = old2new(k, hk->type);\n     	n->nbody = nod(OAS, k, hk);\n     
     	if(v != N) {\n    -\t\tv = old2new(v, t->type);\n+\t\tif(local)\n+\t\t\tv = old2new(v, t->type);\n     	n->nbody = list(n->nbody,\n     	\tnod(OAS, v, nod(OINDEX, m, hk)) );\n     }\n@@ -3112,7 +3132,8 @@ map:
     	r = nod(OCALL, on, r);\n     	n->nincr = r;\n     
    -\tk = old2new(k, t->down);\n+\tif(local)\n+\t\tk = old2new(k, t->down);\n     	if(v == N) {\n     	\ton = syslook("mapiter1", 1);\n     	\targtype(on, th);\n@@ -3122,7 +3143,8 @@ map:
     	n->nbody = nod(OAS, k, r);\n     	goto out;\n     }\n    -\tv = old2new(v, t->type);\n+\tif(local)\n+\t\tv = old2new(v, t->type);\n     	on = syslook("mapiter2", 1);\n     	argtype(on, th);\n     	argtype(on, t->down);\
    

コアとなるコードの解説

src/cmd/gc/go.h

  • ORANGEの追加: ORANGEは、Goコンパイラがfor...rangeステートメントを内部的に表現するために使用する新しい抽象構文木(AST)ノードタイプです。コンパイラはソースコードを解析する際に、このORANGEノードを生成し、rangeループの構造と意味情報を保持します。
  • dorange関数のシグネチャ変更: dorange関数は、rangeステートメントをより低レベルのforループ構造に変換する役割を担っています。以前はイテレーション変数(キーと値)とコレクションを個別の引数として受け取っていましたが、変更後はORANGEノード全体を引数として受け取るようになりました。これにより、rangeステートメントに関するすべての情報が単一の構造体(ORANGEノード)にカプセル化され、関数のインターフェースが簡素化され、よりクリーンな設計になりました。

src/cmd/gc/go.y

このファイルはGo言語の文法を定義しており、Yacc/Bisonによって構文解析器が生成されます。

  • orange_stmtルールの追加: この新しいルールは、rangeステートメントの様々な構文バリエーションを認識するための中心的な役割を果たします。

    • exprsym3_list_r '=' LRANGE expr: a = range ma,b = range m のような代入形式を扱います。nod(ORANGE, $1, $4)ORANGEノードを生成し、$$->etype = 0;:=フラグをセットしています。これは、=による代入であっても、内部的には:=と同様に新しい変数を扱うことを示唆している可能性があります(ただし、walk.clocalフラグとして使用され、old2newの挙動を制御します)。
    • exprsym3 ':' exprsym3 '=' LRANGE expr: a:b = range m のように、キーと値のペアを代入する形式を扱います。nod(OLIST, $1, $3)でキーと値の変数をリストとしてまとめ、それをORANGEノードのleftに渡します。
    • exprsym3_list_r ocolas LRANGE expr: a range ma,b range m のように、:=を省略した形式を扱います。ocolasルールがLCOLAS:=)をオプションとして定義しているため、このルールで:=の有無を吸収します。$$->etype = 1;:=フラグをセットしています。
    • exprsym3 ':' exprsym3 ocolas LRANGE expr: a:b range m のように、キーと値のペアで:=を省略した形式を扱います。
  • for_headerルールの変更: forループのヘッダー部分にorange_stmtが直接現れることを許可するように変更されました。これにより、for range ...という簡潔な構文が可能になります。 if($3 != N && $3->op == ORANGE)のブロックは、forループのテスト部分(2番目のセミコロンと3番目のセミコロンの間)がORANGEノードである場合に、dorange関数を呼び出してrangeステートメントを処理するようにしています。 また、for_headerが単一のorange_stmtである場合も同様にdorangeを呼び出すように変更されています。

src/cmd/gc/subr.c

  • opnamesへのORANGE追加: opnames配列は、ASTノードのオペレーションコード(Op enumの値)に対応する文字列名を提供します。ORANGEがこの配列に追加されたことで、コンパイラのデバッグ出力やエラーメッセージにおいて、ORANGEノードが「RANGE」として表示されるようになり、可読性が向上します。

src/cmd/gc/walk.c

このファイルは、ASTを走査し、セマンティックチェックや最適化、中間コード生成のための変換を行う「ウォーカー」の役割を担っています。

  • dorange関数の実装変更: dorange関数は、ORANGEノードを受け取り、それをGoの内部的なforループ構造(OFORノード)に変換する中心的なロジックを含んでいます。
    • 関数冒頭で、引数nnORANGEノードであることを確認しています。
    • k = nn->left; m = nn->right; local = nn->etype; の行で、ORANGEノードからイテレーション変数(k)、イテレート対象のコレクション(m)、そしてlocalフラグ(:==か)を抽出しています。localフラグは、go.yetypeに設定された値(0または1)に対応します。
    • if(k->op == OLIST)ブロックは、a,b range ma:b range mのように複数のイテレーション変数が指定された場合に、それらをkvに分割します。
    • old2new関数の呼び出しにlocalフラグが使用されています。old2newは、新しい変数を宣言する必要がある場合(:=の場合)に新しいシンボルを生成し、既存の変数に代入する場合(=の場合)は既存のシンボルを使用するように制御します。これにより、:==のセマンティクスの違いが適切に処理されます。
    • rangeループの内部的な変換ロジック(配列/スライス、マップのイテレーション)は、hk(ハッシュキー)、on(ヘルパー関数)、r(結果)などのノードを生成し、最終的にOFORノードのninit(初期化)、ntest(テスト)、nincr(インクリメント)、nbody(ループ本体)に変換されます。

これらの変更により、Goコンパイラはfor...rangeループの新しい構文を正しく解釈し、Go言語のセマンティクスに沿った内部表現に変換できるようになりました。特に、go.yでの構文解析とwalk.cでのセマンティック変換の連携が重要であり、ORANGEノードがその間の情報伝達の役割を担っています。

関連リンク

参考にした情報源リンク