[インデックス 1333] ファイルの概要
このコミットは、Go言語の初期開発段階において、range
句のサポートを追加するものです。range
句は、配列、スライス、マップ、チャネルといったコレクション型を反復処理するためのGo言語の基本的な構文要素です。この変更により、Go言語のパーサーとプリンターがrange
キーワードと関連する構文を正しく解釈し、処理できるようになります。
コミット
commit c9859e7bc6ba501e35c684d2fd407a3bcbb3478c
Author: Robert Griesemer <gri@golang.org>
Date: Thu Dec 11 17:45:45 2008 -0800
- support for range clauses
R=r
OCL=21030
CL=21030
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c9859e7bc6ba501e35c684d2fd407a3bcbb3478c
元コミット内容
このコミットの目的は、Go言語にrange
句のサポートを導入することです。具体的には、for
ループ内でコレクション(配列、スライス、マップ、チャネル)を反復処理するための新しい構文要素を追加します。これにより、コードの記述がより簡潔になり、イテレーションのパターンが標準化されます。
変更の背景
Go言語は、その設計段階からシンプルさと効率性を重視していました。コレクションの反復処理はプログラミングにおいて非常に一般的な操作であり、これを効率的かつ読みやすい方法で実現することは言語設計の重要な側面です。従来のC言語のようなインデックスベースのループや、他の言語に見られるイテレータパターンとは異なる、Goらしい簡潔なイテレーションメカニズムが必要とされていました。
range
句の導入は、以下の目的を達成するために行われました。
- 簡潔性: コレクションの要素とそのインデックス(またはキー)を同時に取得できる、より簡潔な構文を提供する。
- 安全性: インデックスの範囲外アクセスなどの一般的なエラーを防ぐ。
- 一貫性: 異なるコレクション型に対して統一されたイテレーション方法を提供する。
- 効率性: コンパイラがイテレーションを最適化しやすくする。
このコミットは、Go言語がまだ初期段階にあった2008年に行われており、言語の基本的な構文要素が確立されていく過程の一部として、range
句がその重要な位置を占めることになったことを示しています。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語(または一般的なプログラミング言語)の概念に関する知識が必要です。
- Go言語の構文解析 (Parsing): ソースコードを読み込み、その構造を解析して抽象構文木 (AST: Abstract Syntax Tree) を構築するプロセス。
parser.go
はこの役割を担います。 - Go言語のコード生成 (Printing): 抽象構文木から、人間が読める形式のソースコードを生成するプロセス。
printer.go
はこの役割を担います。 - トークン (Token): ソースコードを構成する最小単位。例えば、
for
、range
、:
、=
、識別子、数値などがトークンです。Scanner
はこれらのトークンを識別します。 - 抽象構文木 (AST: Abstract Syntax Tree): ソースコードの構造を木構造で表現したもの。コンパイラやツールがコードを理解し、操作するために使用します。
for
ループ: 繰り返し処理を行うための制御構造。Go言語のfor
ループは、C言語のfor
、while
、do-while
の機能を統合した柔軟な構文を持ちます。- コレクション型: 複数の値を格納できるデータ構造。Go言語では、配列 (arrays)、スライス (slices)、マップ (maps)、チャネル (channels) などがあります。
iota
: Go言語のconst
宣言で使用される特殊な識別子で、連続する定数に自動的に値を割り当てるために使われます。
技術的詳細
このコミットは、Go言語のコンパイラのフロントエンド、特に構文解析器(parser.go
)とコードプリンター(printer.go
)にrange
句を認識・処理するための変更を加えています。
parser.go
の変更点
parser.go
は、Go言語のソースコードを解析し、抽象構文木(AST)を構築する役割を担っています。range
句の導入に伴い、以下の主要な変更が行われました。
-
ParseSimpleStat
関数のシグネチャ変更:func (P *Parser) ParseSimpleStat() *AST.Stat
からfunc (P *Parser) ParseSimpleStat(range_ok bool) *AST.Stat
へと変更されました。range_ok
引数は、現在のコンテキストでrange
句が許可されているかどうかを示すフラグです。これにより、for
ループの初期化部分でのみrange
句を許可し、他の場所での誤用を防ぐことができます。
-
key : value
構文のサポート:ParseSimpleStat
内で、range_ok
がtrue
の場合にScanner.COLON
(コロン)トークンをチェックする新しいロジックが追加されました。- これは、
for key : value := range collection
のような形式(初期のGo言語のrange
構文の実験的な形式、後にkey, value := range collection
に落ち着く)を解析するためのものです。 x.Len() == 1
のチェックは、左辺が単一の式である場合にのみkey : value
形式を許可することを示しています。
-
range
キーワードの解析:Scanner.DEFINE
(:=
)、Scanner.ASSIGN
(=
) などの代入演算子を処理するswitch
文のブロック内で、P.tok == Scanner.RANGE
のチェックが追加されました。- これにより、
for i, x := range a
のようなrange
句を正しく解析できるようになります。 y = P.NewExpr(range_pos, Scanner.RANGE, nil, y)
は、range
キーワード自体をASTノードとして表現し、その右辺(イテレートされるコレクション)をy
として格納します。if tok != Scanner.DEFINE && tok != Scanner.ASSIGN
のチェックは、range
句が:=
または=
演算子と組み合わせて使用されることを強制します。
-
ParseControlClause
の変更:for
ループの制御句を解析するParseControlClause
関数内で、s.init = P.ParseSimpleStat(keyword == Scanner.FOR)
のようにrange_ok
引数が渡されるようになりました。- これにより、
for
ループの初期化部分でのみrange
句が有効になるように制御されます。
-
ParseStatement
の変更:- 一般的なステートメントを解析する
ParseStatement
関数では、s = P.ParseSimpleStat(false)
のようにrange_ok
がfalse
で渡されます。これは、通常のステートメントのコンテキストではrange
句が許可されないことを意味します。
- 一般的なステートメントを解析する
printer.go
の変更点
printer.go
は、ASTをGo言語のソースコードとして整形して出力する役割を担っています。
Scanner.RANGE
の出力:Expr1
関数内で、x.tok == Scanner.RANGE
の場合にP.separator = blank;
が追加されました。- これは、
range
キーワードの後にスペースを挿入し、range
句の可読性を確保するための整形ルールです。
selftest2.go
の変更点
selftest2.go
は、新しいrange
句の機能が正しく動作するかを検証するためのテストコードです。
-
const
宣言の追加:const /* enum2 */
ブロックが追加され、iota
を使った複数の定数宣言の例が示されています。これは直接range
句とは関係ありませんが、Go言語の他の構文要素のテストも兼ねています。
-
f3
関数の追加:f3
関数は、スライスとマップに対する様々なfor ... range
ループのバリエーションを示しています。for i := range a
: インデックス(またはキー)のみを取得するパターン。for i, x := range a
: インデックス(またはキー)と値の両方を取得するパターン。for i : x := range a
: 初期段階のkey : value
構文の例。for i range m
: マップのキーのみを取得するパターン。for i, x range m
: マップのキーと値の両方を取得するパターン。for i : x = range m
: 初期段階のkey : value
構文で、=
を使った例。- これらのテストケースは、パーサーがこれらの異なる
range
句の形式を正しく解釈できることを確認するために重要です。
コアとなるコードの変更箇所
usr/gri/pretty/parser.go
func (P *Parser) ParseSimpleStat(range_ok bool) *AST.Stat {
P.Trace("SimpleStat");
s := AST.BadStat;
x := P.ParseExpressionList();
is_range := false;
if range_ok && P.tok == Scanner.COLON {
pos := P.pos;
P.Next();
y := P.ParseExpression(1);
if x.Len() == 1 {
x = P.NewExpr(pos, Scanner.COLON, x, y);
is_range = true;
} else {
P.Error(pos, "expected initialization, found ':'");
}
}
switch P.tok {
// ... 既存のケース ...
case
Scanner.DEFINE, Scanner.ASSIGN, Scanner.ADD_ASSIGN,
Scanner.SUB_ASSIGN, Scanner.MUL_ASSIGN, Scanner.QUO_ASSIGN,
Scanner.REM_ASSIGN, Scanner.AND_ASSIGN, Scanner.OR_ASSIGN,
Scanner.XOR_ASSIGN, Scanner.SHL_ASSIGN, Scanner.SHR_ASSIGN:
// declaration/assignment
pos, tok := P.pos, P.tok;
P.Next();
y := AST.BadExpr;
if P.tok == Scanner.RANGE {
range_pos := P.pos;
P.Next();
y = P.ParseExpression(1);
y = P.NewExpr(range_pos, Scanner.RANGE, nil, y);
if tok != Scanner.DEFINE && tok != Scanner.ASSIGN {
P.Error(pos, "expected '=' or ':=', found '\" + Scanner.TokenString(tok) + \"'");
}
} else {
y = P.ParseExpressionList();
if is_range {
P.Error(y.pos, "expected 'range', found expression");
}
if xl, yl := x.Len(), y.Len(); xl > 1 && yl > 1 && xl != yl {
P.Error(x.pos, "arity of lhs doesn't match rhs");
}
}
s = AST.NewStat(x.pos, Scanner.EXPRSTAT);
s.expr = AST.NewExpr(pos, tok, x, y);
case Scanner.RANGE:
pos := P.pos;
P.Next();
y := P.ParseExpression(1);
y = P.NewExpr(pos, Scanner.RANGE, nil, y);
s = AST.NewStat(x.pos, Scanner.EXPRSTAT);
s.expr = AST.NewExpr(pos, Scanner.DEFINE, x, y);
// ... 既存のケース ...
}
// ...
}
func (P *Parser) ParseControlClause(keyword int) *AST.Stat {
// ...
if P.tok != Scanner.SEMICOLON {
s.init = P.ParseSimpleStat(keyword == Scanner.FOR); // ここでrange_okを渡す
// TODO check for range clause and exit if found
}
// ...
if keyword == Scanner.FOR {
// ...
if P.tok != Scanner.LBRACE {
s.post = P.ParseSimpleStat(false); // forのpost句ではrangeは許可しない
}
}
// ...
}
func (P *Parser) ParseStatement() *AST.Stat {
// ...
case Scanner.IDENT, Scanner.INT, Scanner.FLOAT, Scanner.STRING, Scanner.LPAREN, // operand
Scanner.LBRACK, Scanner.STRUCT, // composite type
Scanner.MUL, Scanner.AND, Scanner.ARROW: // unary
s = P.ParseSimpleStat(false); // 通常のステートメントではrangeは許可しない
// ...
}
usr/gri/pretty/printer.go
func (P *Printer) Expr1(x *AST.Expr, prec1 int) {
// ...
if x.x == nil {
// unary expression
P.Token(x.pos, x.tok);
if x.tok == Scanner.RANGE {
P.separator = blank; // rangeキーワードの後にスペースを挿入
}
} else {
// binary expression
P.Expr1(x.x, prec);
}
// ...
}
usr/gri/pretty/selftest2.go
// ...
const /* enum2 */ (
a, b = iota*2 + 1, iota*2;
c, d;
e, f;
)
// ...
func f3(a *[]int, m *map[string] int) {
println("A1");
for i := range a {
println(i);
}
println("A2");
for i, x := range a {
println(i, x);
}
println("A3");
for i : x := range a { // 初期段階の実験的な構文
println(i, x);
}
println("M1");
for i range m {
println(i);
}
println("M2");
for i, x range m {
println(i, x);
}
println("M3");
var i string;
var x int;
for i : x = range m { // 初期段階の実験的な構文
println(i, x);
}
}
func main() {
// ...
f3(&[]int{2, 3, 5, 7}, map[string]int{"two":2, "three":3, "five":5, "seven":7});
// ...
}
コアとなるコードの解説
このコミットの核心は、Go言語の構文解析器がrange
キーワードとそれに関連する構文パターンをどのように認識し、抽象構文木に変換するか、そしてそのASTがどのように整形されて出力されるかという点にあります。
-
ParseSimpleStat
の役割拡張:ParseSimpleStat
関数は、Go言語の様々な「単純なステートメント」を解析する汎用的な関数です。これには、変数宣言、代入、関数呼び出しなどが含まれます。- このコミットでは、
range_ok
という新しい引数を導入することで、この関数がfor
ループの初期化部分という特定のコンテキストでのみrange
句を解析できるようにしました。これにより、range
句が言語の他の部分で誤って使用されることを防ぎます。 - 特に注目すべきは、
Scanner.COLON
(コロン)をチェックする部分です。これは、初期のGo言語で検討されていたfor key : value := range collection
のような、コロンを使った多値代入のrange
構文をサポートするためのものです。最終的には、この構文はfor key, value := range collection
というカンマ区切りに落ち着きますが、このコミット時点ではコロンの可能性も考慮されていました。 Scanner.RANGE
トークンを直接検出するcase
文も追加されており、これはfor i := range collection
やfor i, x := range collection
のような、range
キーワードが代入演算子(:=
や=
)の右辺に現れる場合の解析を扱います。
-
ParseControlClause
とParseStatement
によるコンテキスト制御:ParseControlClause
はfor
、if
、switch
などの制御構造の句を解析します。この関数がParseSimpleStat
を呼び出す際に、keyword == Scanner.FOR
の場合にのみrange_ok
をtrue
に設定することで、range
句がfor
ループの初期化部分でのみ有効であることを保証します。- 一方、
ParseStatement
は一般的なステートメントを解析するため、ParseSimpleStat
を呼び出す際には常にrange_ok
をfalse
に設定し、range
句が通常のステートメントとして単独で存在することを許可しません。
-
printer.go
における整形:printer.go
の変更は、range
キーワードがASTに現れた際に、その後に適切な空白(セパレータ)を挿入することで、生成されるコードの可読性を保つためのものです。これは、コンパイラが生成するコードがGoの標準的なフォーマット(gofmt
のようなツールが最終的に担う役割)に準拠するための初期のステップです。
-
selftest2.go
による機能検証:selftest2.go
に追加されたf3
関数は、range
句の様々な使用パターンを網羅的にテストしています。これには、インデックスのみ、インデックスと値の両方、マップのキーのみ、マップのキーと値の両方を取得するパターンが含まれます。- 特に興味深いのは、
for i : x := range a
やfor i : x = range m
といった、コロンを使った初期の実験的な構文のテストケースが含まれている点です。これは、Go言語の設計が進化する過程で、様々な構文が試行錯誤されていたことを示しています。これらのテストケースが存在することで、パーサーがこれらの構文を正しく解釈できるかどうかが検証されます。
これらの変更は、Go言語がその初期段階で、コレクションのイテレーションという重要な機能に対して、いかに簡潔で強力な構文を提供しようとしていたかを示しています。range
句は、Go言語の「Goらしい」コードを書く上で不可欠な要素となり、その後のGoプログラミングのスタイルに大きな影響を与えました。
関連リンク
- Go言語の
for
ステートメントに関する公式ドキュメント(現在のバージョン): https://go.dev/ref/spec#For_statements - Go言語の
range
句に関する公式ドキュメント(現在のバージョン): https://go.dev/ref/spec#Range_clause
参考にした情報源リンク
- Go言語のソースコードリポジトリ: https://github.com/golang/go
- Go言語の初期の設計に関する議論やメーリングリストのアーカイブ(一般公開されている場合)
- Go言語のコンパイラ設計に関する一般的な情報(AST、パーシング、プリンティングなど)