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

[インデックス 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句の導入は、以下の目的を達成するために行われました。

  1. 簡潔性: コレクションの要素とそのインデックス(またはキー)を同時に取得できる、より簡潔な構文を提供する。
  2. 安全性: インデックスの範囲外アクセスなどの一般的なエラーを防ぐ。
  3. 一貫性: 異なるコレクション型に対して統一されたイテレーション方法を提供する。
  4. 効率性: コンパイラがイテレーションを最適化しやすくする。

このコミットは、Go言語がまだ初期段階にあった2008年に行われており、言語の基本的な構文要素が確立されていく過程の一部として、range句がその重要な位置を占めることになったことを示しています。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語(または一般的なプログラミング言語)の概念に関する知識が必要です。

  • Go言語の構文解析 (Parsing): ソースコードを読み込み、その構造を解析して抽象構文木 (AST: Abstract Syntax Tree) を構築するプロセス。parser.goはこの役割を担います。
  • Go言語のコード生成 (Printing): 抽象構文木から、人間が読める形式のソースコードを生成するプロセス。printer.goはこの役割を担います。
  • トークン (Token): ソースコードを構成する最小単位。例えば、forrange:=、識別子、数値などがトークンです。Scannerはこれらのトークンを識別します。
  • 抽象構文木 (AST: Abstract Syntax Tree): ソースコードの構造を木構造で表現したもの。コンパイラやツールがコードを理解し、操作するために使用します。
  • forループ: 繰り返し処理を行うための制御構造。Go言語のforループは、C言語のforwhiledo-whileの機能を統合した柔軟な構文を持ちます。
  • コレクション型: 複数の値を格納できるデータ構造。Go言語では、配列 (arrays)、スライス (slices)、マップ (maps)、チャネル (channels) などがあります。
  • iota: Go言語のconst宣言で使用される特殊な識別子で、連続する定数に自動的に値を割り当てるために使われます。

技術的詳細

このコミットは、Go言語のコンパイラのフロントエンド、特に構文解析器(parser.go)とコードプリンター(printer.go)にrange句を認識・処理するための変更を加えています。

parser.go の変更点

parser.goは、Go言語のソースコードを解析し、抽象構文木(AST)を構築する役割を担っています。range句の導入に伴い、以下の主要な変更が行われました。

  1. ParseSimpleStat 関数のシグネチャ変更:

    • func (P *Parser) ParseSimpleStat() *AST.Stat から func (P *Parser) ParseSimpleStat(range_ok bool) *AST.Stat へと変更されました。
    • range_ok引数は、現在のコンテキストでrange句が許可されているかどうかを示すフラグです。これにより、forループの初期化部分でのみrange句を許可し、他の場所での誤用を防ぐことができます。
  2. key : value 構文のサポート:

    • ParseSimpleStat内で、range_oktrueの場合にScanner.COLON(コロン)トークンをチェックする新しいロジックが追加されました。
    • これは、for key : value := range collection のような形式(初期のGo言語のrange構文の実験的な形式、後にkey, value := range collectionに落ち着く)を解析するためのものです。
    • x.Len() == 1 のチェックは、左辺が単一の式である場合にのみkey : value形式を許可することを示しています。
  3. 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句が:=または=演算子と組み合わせて使用されることを強制します。
  4. ParseControlClause の変更:

    • forループの制御句を解析するParseControlClause関数内で、s.init = P.ParseSimpleStat(keyword == Scanner.FOR) のようにrange_ok引数が渡されるようになりました。
    • これにより、forループの初期化部分でのみrange句が有効になるように制御されます。
  5. ParseStatement の変更:

    • 一般的なステートメントを解析するParseStatement関数では、s = P.ParseSimpleStat(false) のようにrange_okfalseで渡されます。これは、通常のステートメントのコンテキストではrange句が許可されないことを意味します。

printer.go の変更点

printer.goは、ASTをGo言語のソースコードとして整形して出力する役割を担っています。

  1. Scanner.RANGE の出力:
    • Expr1関数内で、x.tok == Scanner.RANGE の場合にP.separator = blank; が追加されました。
    • これは、rangeキーワードの後にスペースを挿入し、range句の可読性を確保するための整形ルールです。

selftest2.go の変更点

selftest2.goは、新しいrange句の機能が正しく動作するかを検証するためのテストコードです。

  1. const 宣言の追加:

    • const /* enum2 */ ブロックが追加され、iotaを使った複数の定数宣言の例が示されています。これは直接range句とは関係ありませんが、Go言語の他の構文要素のテストも兼ねています。
  2. 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がどのように整形されて出力されるかという点にあります。

  1. 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 collectionfor i, x := range collectionのような、rangeキーワードが代入演算子(:==)の右辺に現れる場合の解析を扱います。
  2. ParseControlClauseParseStatement によるコンテキスト制御:

    • ParseControlClauseforifswitchなどの制御構造の句を解析します。この関数がParseSimpleStatを呼び出す際に、keyword == Scanner.FORの場合にのみrange_oktrueに設定することで、range句がforループの初期化部分でのみ有効であることを保証します。
    • 一方、ParseStatementは一般的なステートメントを解析するため、ParseSimpleStatを呼び出す際には常にrange_okfalseに設定し、range句が通常のステートメントとして単独で存在することを許可しません。
  3. printer.go における整形:

    • printer.goの変更は、rangeキーワードがASTに現れた際に、その後に適切な空白(セパレータ)を挿入することで、生成されるコードの可読性を保つためのものです。これは、コンパイラが生成するコードがGoの標準的なフォーマット(gofmtのようなツールが最終的に担う役割)に準拠するための初期のステップです。
  4. selftest2.go による機能検証:

    • selftest2.goに追加されたf3関数は、range句の様々な使用パターンを網羅的にテストしています。これには、インデックスのみ、インデックスと値の両方、マップのキーのみ、マップのキーと値の両方を取得するパターンが含まれます。
    • 特に興味深いのは、for i : x := range afor i : x = range m といった、コロンを使った初期の実験的な構文のテストケースが含まれている点です。これは、Go言語の設計が進化する過程で、様々な構文が試行錯誤されていたことを示しています。これらのテストケースが存在することで、パーサーがこれらの構文を正しく解釈できるかどうかが検証されます。

これらの変更は、Go言語がその初期段階で、コレクションのイテレーションという重要な機能に対して、いかに簡潔で強力な構文を提供しようとしていたかを示しています。range句は、Go言語の「Goらしい」コードを書く上で不可欠な要素となり、その後のGoプログラミングのスタイルに大きな影響を与えました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコードリポジトリ: https://github.com/golang/go
  • Go言語の初期の設計に関する議論やメーリングリストのアーカイブ(一般公開されている場合)
  • Go言語のコンパイラ設計に関する一般的な情報(AST、パーシング、プリンティングなど)